From 7358c3ff3063a82767bec67373f3af8d17ed9fc5 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Wed, 3 Dec 2025 20:01:37 +0530 Subject: [PATCH] notification preferances added approver performance api altererd resume added for initiator also --- src/components/admin/AIConfig/AIConfig.tsx | 4 +- .../admin/AIConfig/AIParameters.tsx | 10 +- src/components/admin/ConfigurationManager.tsx | 6 +- .../settings/NotificationPreferences.tsx | 192 +++++++++++++++ .../settings/NotificationPreferencesModal.tsx | 224 ++++++++++++++++++ .../NotificationPreferencesSimple.tsx | 176 ++++++++++++++ .../ApprovalWorkflow/ApprovalStepCard.tsx | 6 +- src/hooks/useRequestDetails.ts | 12 +- .../ApproverPerformance.tsx | 24 +- .../ApproverPerformanceActionsStats.tsx | 44 ++-- .../components/ApproverPerformanceFilters.tsx | 5 +- .../ApproverPerformanceStatsCards.tsx | 10 +- .../hooks/useApproverPerformanceData.ts | 109 ++++----- .../hooks/useApproverPerformanceFilters.ts | 4 +- .../CreateRequest/utils/payloadBuilders.ts | 14 +- .../components/DashboardFiltersBar.tsx | 7 + .../components/sections/TATBreachReport.tsx | 3 +- .../Dashboard/hooks/useDashboardFilters.ts | 4 +- .../Dashboard/utils/dateRangeFormatter.ts | 61 +++++ .../components/QuickActionsSidebar.tsx | 13 +- .../components/tabs/OverviewTab.tsx | 20 +- src/pages/Requests/Requests.tsx | 7 +- src/pages/Requests/UserAllRequests.tsx | 3 + .../Requests/hooks/useRequestsFilters.ts | 6 +- src/pages/Settings/Settings.tsx | 115 +++++++-- src/services/dashboard.service.ts | 65 ++++- src/services/notificationApi.ts | 40 +++- src/services/userPreferenceApi.ts | 37 +++ src/utils/requestDetailHelpers.tsx | 5 + 29 files changed, 1047 insertions(+), 179 deletions(-) create mode 100644 src/components/settings/NotificationPreferences.tsx create mode 100644 src/components/settings/NotificationPreferencesModal.tsx create mode 100644 src/components/settings/NotificationPreferencesSimple.tsx create mode 100644 src/pages/Dashboard/utils/dateRangeFormatter.ts create mode 100644 src/services/userPreferenceApi.ts diff --git a/src/components/admin/AIConfig/AIConfig.tsx b/src/components/admin/AIConfig/AIConfig.tsx index 98ee1c6..4454dfe 100644 --- a/src/components/admin/AIConfig/AIConfig.tsx +++ b/src/components/admin/AIConfig/AIConfig.tsx @@ -59,7 +59,7 @@ export function AIConfig() { openaiApiKey: configMap['OPENAI_API_KEY'] || '', geminiApiKey: configMap['GEMINI_API_KEY'] || '', aiRemarkGeneration: configMap['AI_REMARK_GENERATION_ENABLED'] === 'true', - maxRemarkChars: parseInt(configMap['AI_REMARK_MAX_CHARACTERS'] || '500') + maxRemarkChars: parseInt(configMap['AI_MAX_REMARK_LENGTH'] || '2000') }); } catch (error: any) { console.error('Failed to load AI configurations:', error); @@ -81,7 +81,7 @@ export function AIConfig() { updateConfiguration('OPENAI_API_KEY', config.openaiApiKey), updateConfiguration('GEMINI_API_KEY', config.geminiApiKey), updateConfiguration('AI_REMARK_GENERATION_ENABLED', config.aiRemarkGeneration.toString()), - updateConfiguration('AI_REMARK_MAX_CHARACTERS', config.maxRemarkChars.toString()) + updateConfiguration('AI_MAX_REMARK_LENGTH', config.maxRemarkChars.toString()) ]); toast.success('AI configuration saved successfully'); diff --git a/src/components/admin/AIConfig/AIParameters.tsx b/src/components/admin/AIConfig/AIParameters.tsx index 16ef489..aed16a0 100644 --- a/src/components/admin/AIConfig/AIParameters.tsx +++ b/src/components/admin/AIConfig/AIParameters.tsx @@ -26,19 +26,19 @@ export function AIParameters({
onMaxRemarkCharsChange(parseInt(e.target.value) || 500)} + onChange={(e) => onMaxRemarkCharsChange(parseInt(e.target.value) || 2000)} className="border-gray-200 focus:border-re-green focus:ring-2 focus:ring-re-green/20" />

- Maximum character limit for AI-generated conclusion remarks (100-2000 characters) + Maximum character length for AI-generated conclusion remarks (500-5000 characters)

diff --git a/src/components/admin/ConfigurationManager.tsx b/src/components/admin/ConfigurationManager.tsx index 119b497..50152c4 100644 --- a/src/components/admin/ConfigurationManager.tsx +++ b/src/components/admin/ConfigurationManager.tsx @@ -258,9 +258,9 @@ export function ConfigurationManager({ onConfigUpdate }: ConfigurationManagerPro } }; - // Filter out notification rules, dashboard layout categories, and allow external sharing - const excludedCategories = ['NOTIFICATION_RULES', 'DASHBOARD_LAYOUT']; - const excludedConfigKeys = ['ALLOW_EXTERNAL_SHARING']; + // Filter out dashboard layout category and specific config keys + const excludedCategories = ['DASHBOARD_LAYOUT']; + const excludedConfigKeys = ['ALLOW_EXTERNAL_SHARING', 'NOTIFICATION_BATCH_DELAY_MS', 'AI_REMARK_MAX_CHARACTERS']; const filteredConfigurations = configurations.filter( config => !excludedCategories.includes(config.configCategory) && !excludedConfigKeys.includes(config.configKey) diff --git a/src/components/settings/NotificationPreferences.tsx b/src/components/settings/NotificationPreferences.tsx new file mode 100644 index 0000000..c18f4c1 --- /dev/null +++ b/src/components/settings/NotificationPreferences.tsx @@ -0,0 +1,192 @@ +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { Bell, Mail, MessageSquare, Loader2, CheckCircle, AlertCircle } from 'lucide-react'; +import { getNotificationPreferences, updateNotificationPreferences, NotificationPreferences } from '@/services/userPreferenceApi'; + +export function NotificationPreferencesCard() { + const [preferences, setPreferences] = useState({ + emailNotificationsEnabled: true, + pushNotificationsEnabled: true, + inAppNotificationsEnabled: true + }); + const [loading, setLoading] = useState(true); + const [updating, setUpdating] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + loadPreferences(); + }, []); + + const loadPreferences = async () => { + try { + setLoading(true); + setError(null); + const data = await getNotificationPreferences(); + setPreferences(data); + } catch (err: any) { + console.error('[NotificationPreferences] Failed to load preferences:', err); + setError(err.response?.data?.message || 'Failed to load notification preferences'); + } finally { + setLoading(false); + } + }; + + const handleToggle = async (key: keyof NotificationPreferences, value: boolean) => { + try { + setUpdating(key); + setError(null); + setSuccessMessage(null); + + const updateData = { [key]: value }; + const updated = await updateNotificationPreferences(updateData); + + setPreferences(updated); + + // Show success message + const prefName = key === 'emailNotificationsEnabled' ? 'Email' + : key === 'pushNotificationsEnabled' ? 'Push' + : 'In-App'; + setSuccessMessage(`${prefName} notifications ${value ? 'enabled' : 'disabled'}`); + setTimeout(() => setSuccessMessage(null), 3000); + } catch (err: any) { + console.error('[NotificationPreferences] Failed to update preference:', err); + setError(err.response?.data?.message || 'Failed to update notification preference'); + // Revert the UI change + loadPreferences(); + } finally { + setUpdating(null); + } + }; + + if (loading) { + return ( + + + + + + ); + } + + return ( + + +
+
+ +
+
+ Notification Preferences + Control how you receive notifications +
+
+
+ + {/* Success Message */} + {successMessage && ( +
+ +

{successMessage}

+
+ )} + + {/* Error Message */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Email Notifications */} +
+
+
+ +
+
+ +

Receive notifications via email

+
+
+
+ {updating === 'emailNotificationsEnabled' && ( + + )} + handleToggle('emailNotificationsEnabled', checked)} + disabled={updating === 'emailNotificationsEnabled'} + /> +
+
+ + {/* Push Notifications */} +
+
+
+ +
+
+ +

Receive browser push notifications

+
+
+
+ {updating === 'pushNotificationsEnabled' && ( + + )} + handleToggle('pushNotificationsEnabled', checked)} + disabled={updating === 'pushNotificationsEnabled'} + /> +
+
+ + {/* In-App Notifications */} +
+
+
+ +
+
+ +

Show notifications within the application

+
+
+
+ {updating === 'inAppNotificationsEnabled' && ( + + )} + handleToggle('inAppNotificationsEnabled', checked)} + disabled={updating === 'inAppNotificationsEnabled'} + /> +
+
+ +
+

+ These preferences control which notification channels you receive alerts through. + You'll still receive critical notifications regardless of these settings. +

+
+
+
+ ); +} + diff --git a/src/components/settings/NotificationPreferencesModal.tsx b/src/components/settings/NotificationPreferencesModal.tsx new file mode 100644 index 0000000..fa5be15 --- /dev/null +++ b/src/components/settings/NotificationPreferencesModal.tsx @@ -0,0 +1,224 @@ +import { useState, useEffect } from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { Bell, Mail, MessageSquare, Loader2, CheckCircle, AlertCircle, Settings } from 'lucide-react'; +import { getNotificationPreferences, updateNotificationPreferences, NotificationPreferences } from '@/services/userPreferenceApi'; +import { Separator } from '@/components/ui/separator'; + +interface NotificationPreferencesModalProps { + open: boolean; + onClose: () => void; +} + +export function NotificationPreferencesModal({ open, onClose }: NotificationPreferencesModalProps) { + const [preferences, setPreferences] = useState({ + emailNotificationsEnabled: true, + pushNotificationsEnabled: true, + inAppNotificationsEnabled: true + }); + const [loading, setLoading] = useState(true); + const [updating, setUpdating] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + if (open) { + loadPreferences(); + } + }, [open]); + + const loadPreferences = async () => { + try { + setLoading(true); + setError(null); + const data = await getNotificationPreferences(); + setPreferences(data); + } catch (err: any) { + console.error('[NotificationPreferences] Failed to load preferences:', err); + setError(err.response?.data?.message || 'Failed to load notification preferences'); + } finally { + setLoading(false); + } + }; + + const handleToggle = async (key: keyof NotificationPreferences, value: boolean) => { + try { + setUpdating(key); + setError(null); + setSuccessMessage(null); + + const updateData = { [key]: value }; + const updated = await updateNotificationPreferences(updateData); + + setPreferences(updated); + + // Show success message + const prefName = key === 'emailNotificationsEnabled' ? 'Email' + : key === 'pushNotificationsEnabled' ? 'Push' + : 'In-App'; + setSuccessMessage(`${prefName} notifications ${value ? 'enabled' : 'disabled'}`); + setTimeout(() => setSuccessMessage(null), 3000); + } catch (err: any) { + console.error('[NotificationPreferences] Failed to update preference:', err); + setError(err.response?.data?.message || 'Failed to update notification preference'); + // Revert the UI change + loadPreferences(); + } finally { + setUpdating(null); + } + }; + + return ( + + + +
+
+ +
+
+ Notification Preferences + + Customize how you receive notifications for workflow updates + +
+
+
+ + + + {loading ? ( +
+ +
+ ) : ( +
+ {/* Success Message */} + {successMessage && ( +
+ +

{successMessage}

+
+ )} + + {/* Error Message */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Email Notifications */} +
+
+
+
+ +
+
+ +

+ Receive important updates and alerts via email +

+
+
+
+ {updating === 'emailNotificationsEnabled' && ( + + )} + handleToggle('emailNotificationsEnabled', checked)} + disabled={updating === 'emailNotificationsEnabled'} + /> +
+
+
+ + {/* Push Notifications */} +
+
+
+
+ +
+
+ +

+ Get instant browser notifications for real-time updates +

+
+
+
+ {updating === 'pushNotificationsEnabled' && ( + + )} + handleToggle('pushNotificationsEnabled', checked)} + disabled={updating === 'pushNotificationsEnabled'} + /> +
+
+
+ + {/* In-App Notifications */} +
+
+
+
+ +
+
+ +

+ View notifications in the notification center +

+
+
+
+ {updating === 'inAppNotificationsEnabled' && ( + + )} + handleToggle('inAppNotificationsEnabled', checked)} + disabled={updating === 'inAppNotificationsEnabled'} + /> +
+
+
+ + + + {/* Info Section */} +
+

+ Note: These settings control your notification preferences across all channels. + Critical system alerts and urgent notifications may still be delivered regardless of these settings to ensure important information reaches you. +

+
+
+ )} +
+
+ ); +} + diff --git a/src/components/settings/NotificationPreferencesSimple.tsx b/src/components/settings/NotificationPreferencesSimple.tsx new file mode 100644 index 0000000..5967efa --- /dev/null +++ b/src/components/settings/NotificationPreferencesSimple.tsx @@ -0,0 +1,176 @@ +import { useState, useEffect } from 'react'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { Bell, Mail, MessageSquare, Loader2, CheckCircle, AlertCircle } from 'lucide-react'; +import { getNotificationPreferences, updateNotificationPreferences, NotificationPreferences } from '@/services/userPreferenceApi'; + +export function NotificationPreferencesSimple() { + const [preferences, setPreferences] = useState({ + emailNotificationsEnabled: true, + pushNotificationsEnabled: true, + inAppNotificationsEnabled: true + }); + const [loading, setLoading] = useState(true); + const [updating, setUpdating] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + loadPreferences(); + }, []); + + const loadPreferences = async () => { + try { + setLoading(true); + setError(null); + const data = await getNotificationPreferences(); + setPreferences(data); + } catch (err: any) { + console.error('[NotificationPreferences] Failed to load preferences:', err); + setError(err.response?.data?.message || 'Failed to load notification preferences'); + } finally { + setLoading(false); + } + }; + + const handleToggle = async (key: keyof NotificationPreferences, value: boolean) => { + try { + setUpdating(key); + setError(null); + setSuccessMessage(null); + + const updateData = { [key]: value }; + const updated = await updateNotificationPreferences(updateData); + + setPreferences(updated); + + // Show success message + const prefName = key === 'emailNotificationsEnabled' ? 'Email' + : key === 'pushNotificationsEnabled' ? 'Push' + : 'In-App'; + setSuccessMessage(`${prefName} notifications ${value ? 'enabled' : 'disabled'}`); + setTimeout(() => setSuccessMessage(null), 3000); + } catch (err: any) { + console.error('[NotificationPreferences] Failed to update preference:', err); + setError(err.response?.data?.message || 'Failed to update notification preference'); + // Revert the UI change + loadPreferences(); + } finally { + setUpdating(null); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Success Message */} + {successMessage && ( +
+ +

{successMessage}

+
+ )} + + {/* Error Message */} + {error && ( +
+ +

{error}

+
+ )} + + {/* Email Notifications */} +
+
+
+ +
+
+ +

Receive notifications via email

+
+
+
+ {updating === 'emailNotificationsEnabled' && ( + + )} + handleToggle('emailNotificationsEnabled', checked)} + disabled={updating === 'emailNotificationsEnabled'} + /> +
+
+ + {/* Push Notifications */} +
+
+
+ +
+
+ +

Receive browser push notifications

+
+
+
+ {updating === 'pushNotificationsEnabled' && ( + + )} + handleToggle('pushNotificationsEnabled', checked)} + disabled={updating === 'pushNotificationsEnabled'} + /> +
+
+ + {/* In-App Notifications */} +
+
+
+ +
+
+ +

Show notifications within the application

+
+
+
+ {updating === 'inAppNotificationsEnabled' && ( + + )} + handleToggle('inAppNotificationsEnabled', checked)} + disabled={updating === 'inAppNotificationsEnabled'} + /> +
+
+ +
+

+ These preferences control which notification channels you receive alerts through. + You'll still receive critical notifications regardless of these settings. +

+
+
+ ); +} + diff --git a/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx b/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx index 2ee7ceb..66bcc4c 100644 --- a/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx +++ b/src/components/workflow/ApprovalWorkflow/ApprovalStepCard.tsx @@ -639,9 +639,9 @@ export function ApprovalStepCard({

)} - {/* Skip Approver Button - Only show for initiator on pending/in-review/paused levels */} - {/* When paused, initiator can skip the approver which will negate the pause */} - {isInitiator && (isActive || isPaused || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && onSkipApprover && ( + {/* Skip Approver Button - Only show for initiator on pending/in-review levels (not when paused) */} + {/* User must resume first before skipping */} + {isInitiator && !isPaused && (isActive || step.status === 'pending') && !isCompleted && !isRejected && step.levelId && onSkipApprover && (
- {calculatedStats.completedActions > 0 ? `${calculatedStats.rejectionRate}%` : '0%'} + {approverStats && (approverStats.approvedCount + approverStats.rejectedCount) > 0 + ? `${Math.round((approverStats.rejectedCount / (approverStats.approvedCount + approverStats.rejectedCount)) * 100)}%` + : '0%'}
-
{calculatedStats.rejectedByApprover}
+
{approverStats?.rejectedCount || 0}
Rejected by Approver
-
{calculatedStats.pendingByApprover}
+
{approverStats?.pendingCount || 0}
Pending Actions
-
{calculatedStats.closedByApprover}
+
{approverStats?.closedCount || 0}
Closed Requests
-
{calculatedStats.total}
+
{approverStats?.totalApproved || 0}
Total Requests
@@ -89,24 +97,24 @@ export function ApproverPerformanceActionsStats({
- {approverStats?.tatCompliancePercent !== undefined ? `${approverStats.tatCompliancePercent}%` : (calculatedStats.completedActions > 0 ? `${calculatedStats.tatComplianceRate}%` : 'N/A')} + {approverStats?.tatCompliancePercent !== undefined ? `${approverStats.tatCompliancePercent}%` : 'N/A'}
-
{calculatedStats.compliant}
+
{approverStats?.withinTatCount || 0}
TAT Compliant
-
{calculatedStats.breached}
+
{approverStats?.breachedCount || 0}
TAT Breached
-
{calculatedStats.completedActions}
+
{approverStats ? (approverStats.approvedCount + approverStats.rejectedCount) : 0}
Completed Actions
diff --git a/src/pages/ApproverPerformance/components/ApproverPerformanceFilters.tsx b/src/pages/ApproverPerformance/components/ApproverPerformanceFilters.tsx index b39b5d3..3734fd3 100644 --- a/src/pages/ApproverPerformance/components/ApproverPerformanceFilters.tsx +++ b/src/pages/ApproverPerformance/components/ApproverPerformanceFilters.tsx @@ -136,9 +136,12 @@ export function ApproverPerformanceFilters({ + All Time Today This Week This Month + Last 7 Days + Last 30 Days Custom Range @@ -219,7 +222,7 @@ export function ApproverPerformanceFilters({ onTempStartDateChange(customStartDate); onTempEndDateChange(customEndDate); if (!customStartDate || !customEndDate) { - onDateRangeChange('month'); + onDateRangeChange('all'); } }} data-testid="cancel-date-button" diff --git a/src/pages/ApproverPerformance/components/ApproverPerformanceStatsCards.tsx b/src/pages/ApproverPerformance/components/ApproverPerformanceStatsCards.tsx index 91aca29..330aaaa 100644 --- a/src/pages/ApproverPerformance/components/ApproverPerformanceStatsCards.tsx +++ b/src/pages/ApproverPerformance/components/ApproverPerformanceStatsCards.tsx @@ -6,20 +6,16 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { CheckCircle, Clock, Target, Timer } from 'lucide-react'; import type { ApproverPerformance } from '@/services/dashboard.service'; -import type { ApproverPerformanceStats } from '../types/approverPerformance.types'; interface ApproverPerformanceStatsCardsProps { - approverStats: ApproverPerformance | null; - calculatedStats: ApproverPerformanceStats; + approverStats: ApproverPerformance; } export function ApproverPerformanceStatsCards({ - approverStats, - calculatedStats + approverStats }: ApproverPerformanceStatsCardsProps) { - if (!approverStats) return null; - const tatCompliance = approverStats?.tatCompliancePercent ?? calculatedStats.tatComplianceRate; + const tatCompliance = approverStats?.tatCompliancePercent ?? 0; return (
diff --git a/src/pages/ApproverPerformance/hooks/useApproverPerformanceData.ts b/src/pages/ApproverPerformance/hooks/useApproverPerformanceData.ts index 365ff95..fe5d0b5 100644 --- a/src/pages/ApproverPerformance/hooks/useApproverPerformanceData.ts +++ b/src/pages/ApproverPerformance/hooks/useApproverPerformanceData.ts @@ -42,29 +42,40 @@ export function useApproverPerformanceData({ const isInitialMount = useRef(true); - // Fetch approver performance stats + // Fetch stats for this approver (ONLY on date/priority/SLA changes) const fetchApproverStats = useCallback(async () => { - if (!approverId) return; + if (!approverId) { + return; + } try { - const result = await dashboardService.getApproverPerformance( - dateRange, - 1, - 100, + const dateRangeToSend = dateRange === 'all' ? undefined : dateRange; + console.log('[Stats] Fetching with filters:', { + dateRange: dateRangeToSend, customStartDate, - customEndDate + customEndDate, + priority: priorityFilter, + sla: slaComplianceFilter + }); + + const stats = await dashboardService.getSingleApproverStats( + approverId, + dateRangeToSend, + customStartDate, + customEndDate, + priorityFilter !== 'all' ? priorityFilter : undefined, + slaComplianceFilter !== 'all' ? slaComplianceFilter : undefined ); - const approver = result.performance.find((p: ApproverPerformance) => p.approverId === approverId); - if (approver) { - setApproverStats(approver); - } + console.log('[Stats] Received stats:', stats); + setApproverStats(stats); } catch (error) { - console.error('Failed to fetch approver stats:', error); + console.error('[ApproverPerformance] Failed to fetch approver stats:', error); + setApproverStats(null); } - }, [approverId, dateRange, customStartDate, customEndDate]); - - // Fetch requests for this approver + }, [approverId, dateRange, customStartDate, customEndDate, priorityFilter, slaComplianceFilter]); + + // Fetch requests for this approver (on ANY filter change) const fetchRequests = useCallback(async (page: number = 1) => { if (!approverId) { setLoading(false); @@ -79,7 +90,7 @@ export function useApproverPerformanceData({ approverId, page, itemsPerPage, - dateRange, + dateRange === 'all' ? undefined : dateRange, customStartDate, customEndDate, statusFilter !== 'all' ? statusFilter : undefined, @@ -91,23 +102,8 @@ export function useApproverPerformanceData({ setRequests(result.requests); setTotalRecords(result.pagination.totalRecords); setTotalPages(result.pagination.totalPages); - setCurrentPage(page); - - // For stats calculation, fetch ALL data (without pagination) - const statsResult = await dashboardService.getRequestsByApprover( - approverId, - 1, - 10000, - dateRange, - customStartDate, - customEndDate, - statusFilter !== 'all' ? statusFilter : undefined, - priorityFilter !== 'all' ? priorityFilter : undefined, - slaComplianceFilter !== 'all' ? slaComplianceFilter : undefined, - searchTerm || undefined - ); - - setAllFilteredRequests(statsResult.requests); + setCurrentPage(result.pagination.currentPage); + setAllFilteredRequests(result.requests); } catch (error) { console.error('Failed to fetch requests:', error); } finally { @@ -130,15 +126,29 @@ export function useApproverPerformanceData({ useEffect(() => { if (isInitialMount.current) { isInitialMount.current = false; - fetchApproverStats(); - fetchRequests(1); + fetchApproverStats(); // Fetch stats once on mount + fetchRequests(1); // Fetch requests once on mount } }, []); // Only run on mount - // Refetch when filters change + // Refetch stats ONLY when date/priority/SLA changes (NOT status or search) useEffect(() => { if (!isInitialMount.current) { fetchApproverStats(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + dateRange, + customStartDate, + customEndDate, + priorityFilter, + slaComplianceFilter + // NO statusFilter, NO searchTerm - stats don't depend on these + ]); + + // Refetch requests when ANY filter changes (including status and search) + useEffect(() => { + if (!isInitialMount.current) { fetchRequests(1); } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -150,35 +160,18 @@ export function useApproverPerformanceData({ priorityFilter, slaComplianceFilter, searchTerm - // fetchApproverStats and fetchRequests excluded to prevent infinite loops ]); const handleRefresh = useCallback(() => { setRefreshing(true); - fetchApproverStats(); - fetchRequests(1); + fetchApproverStats(); // Refresh stats + fetchRequests(1); // Refresh requests }, [fetchApproverStats, fetchRequests]); const handlePageChange = useCallback((page: number) => { - setCurrentPage(page); - // Use client-side pagination since we have allFilteredRequests - const startIdx = (page - 1) * itemsPerPage; - const endIdx = startIdx + itemsPerPage; - const paginatedRequests = allFilteredRequests.slice(startIdx, endIdx); - setRequests(paginatedRequests); - }, [allFilteredRequests, itemsPerPage]); - - // Update paginated data when allFilteredRequests changes - useEffect(() => { - const startIdx = (currentPage - 1) * itemsPerPage; - const endIdx = startIdx + itemsPerPage; - const paginatedRequests = allFilteredRequests.slice(startIdx, endIdx); - setRequests(paginatedRequests); - setTotalPages(Math.ceil(allFilteredRequests.length / itemsPerPage)); - if (currentPage > Math.ceil(allFilteredRequests.length / itemsPerPage) && allFilteredRequests.length > 0) { - setCurrentPage(1); - } - }, [allFilteredRequests, currentPage, itemsPerPage]); + // Use server-side pagination - fetch from backend + fetchRequests(page); + }, [fetchRequests]); return { requests, diff --git a/src/pages/ApproverPerformance/hooks/useApproverPerformanceFilters.ts b/src/pages/ApproverPerformance/hooks/useApproverPerformanceFilters.ts index 7eb0177..549bc39 100644 --- a/src/pages/ApproverPerformance/hooks/useApproverPerformanceFilters.ts +++ b/src/pages/ApproverPerformance/hooks/useApproverPerformanceFilters.ts @@ -13,7 +13,7 @@ export function useApproverPerformanceFilters() { const [statusFilter, setStatusFilter] = useState(searchParams.get('status') || 'all'); const [priorityFilter, setPriorityFilter] = useState(searchParams.get('priority') || 'all'); const [slaComplianceFilter, setSlaComplianceFilter] = useState(searchParams.get('slaCompliance') || 'all'); - const [dateRange, setDateRange] = useState((searchParams.get('dateRange') as DateRange) || 'month'); + const [dateRange, setDateRange] = useState((searchParams.get('dateRange') as DateRange) || 'all'); const [customStartDate, setCustomStartDate] = useState( searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined ); @@ -29,7 +29,7 @@ export function useApproverPerformanceFilters() { setStatusFilter('all'); setPriorityFilter('all'); setSlaComplianceFilter('all'); - setDateRange('month'); + setDateRange('all'); setCustomStartDate(undefined); setCustomEndDate(undefined); setTempCustomStartDate(undefined); diff --git a/src/pages/CreateRequest/utils/payloadBuilders.ts b/src/pages/CreateRequest/utils/payloadBuilders.ts index 5688757..7b55d8a 100644 --- a/src/pages/CreateRequest/utils/payloadBuilders.ts +++ b/src/pages/CreateRequest/utils/payloadBuilders.ts @@ -17,7 +17,7 @@ import { buildApprovalLevels } from './approvalLevelBuilders'; export function buildCreatePayload( formData: FormData, selectedTemplate: RequestTemplate | null, - user: any + _user: any ): CreateWorkflowPayload { // Filter out spectators who are also approvers (backend will handle validation) const approverEmails = new Set( @@ -41,11 +41,13 @@ export function buildCreatePayload( tat: a?.tat || '', tatType: a?.tatType || 'hours', })), - spectators: filteredSpectators.map((s) => ({ + spectators: filteredSpectators.map((s: any) => ({ + userId: s?.userId || '', + name: s?.name || '', email: s?.email || '', })), - // Note: participants array is auto-generated by backend - // No need to send it from frontend + ccList: [], // Auto-generated by backend + participants: [], // Auto-generated by backend from approvers and spectators }; } @@ -55,7 +57,7 @@ export function buildCreatePayload( */ export function buildUpdatePayload( formData: FormData, - user: any, + _user: any, documentsToDelete: string[] ): UpdateWorkflowPayload { const approvalLevels = buildApprovalLevels( @@ -68,7 +70,7 @@ export function buildUpdatePayload( description: formData.description, priority: formData.priority === 'express' ? 'EXPRESS' : 'STANDARD', approvalLevels, - // Note: participants array is auto-generated by backend + participants: [], // Auto-generated by backend from approval levels deleteDocumentIds: documentsToDelete.length > 0 ? documentsToDelete : undefined, }; } diff --git a/src/pages/Dashboard/components/DashboardFiltersBar.tsx b/src/pages/Dashboard/components/DashboardFiltersBar.tsx index 331699b..2c1b010 100644 --- a/src/pages/Dashboard/components/DashboardFiltersBar.tsx +++ b/src/pages/Dashboard/components/DashboardFiltersBar.tsx @@ -70,9 +70,12 @@ export function DashboardFiltersBar({ + All Time Today This Week This Month + Last 7 Days + Last 30 Days Custom Range @@ -169,9 +172,13 @@ export function DashboardFiltersBar({ + All Time Today This Week This Month + Last 7 Days + Last 30 Days + Custom Range
diff --git a/src/pages/Dashboard/components/sections/TATBreachReport.tsx b/src/pages/Dashboard/components/sections/TATBreachReport.tsx index 46d19c7..b950782 100644 --- a/src/pages/Dashboard/components/sections/TATBreachReport.tsx +++ b/src/pages/Dashboard/components/sections/TATBreachReport.tsx @@ -11,6 +11,7 @@ import type { CriticalAlertData } from '@/components/dashboard/CriticalAlertCard import { Pagination } from '@/components/common/Pagination'; import { formatBreachTime } from '../../utils/dashboardCalculations'; import { KPIClickFilters } from '../../components/types/dashboard.types'; +import { formatDateRangeDescription } from '../../utils/dateRangeFormatter'; interface TATBreachReportProps { breachedRequests: (CriticalRequest | CriticalAlertData)[]; @@ -56,7 +57,7 @@ export function TATBreachReport({
TAT Breach Report - Requests that breached defined turnaround time + Requests that breached TAT - {formatDateRangeDescription(dateRange, customStartDate, customEndDate)}
diff --git a/src/pages/Dashboard/hooks/useDashboardFilters.ts b/src/pages/Dashboard/hooks/useDashboardFilters.ts index 26b23e3..8723aaa 100644 --- a/src/pages/Dashboard/hooks/useDashboardFilters.ts +++ b/src/pages/Dashboard/hooks/useDashboardFilters.ts @@ -6,7 +6,7 @@ import { useState, useCallback } from 'react'; import { DateRange } from '@/services/dashboard.service'; export function useDashboardFilters() { - const [dateRange, setDateRange] = useState('month'); + const [dateRange, setDateRange] = useState('all'); const [customStartDate, setCustomStartDate] = useState(undefined); const [customEndDate, setCustomEndDate] = useState(undefined); const [showCustomDatePicker, setShowCustomDatePicker] = useState(false); @@ -42,7 +42,7 @@ export function useDashboardFilters() { setCustomStartDate(undefined); setCustomEndDate(undefined); setShowCustomDatePicker(false); - setDateRange('month'); + setDateRange('all'); }, []); return { diff --git a/src/pages/Dashboard/utils/dateRangeFormatter.ts b/src/pages/Dashboard/utils/dateRangeFormatter.ts new file mode 100644 index 0000000..e78694a --- /dev/null +++ b/src/pages/Dashboard/utils/dateRangeFormatter.ts @@ -0,0 +1,61 @@ +import { format } from 'date-fns'; +import type { DateRange } from '@/services/dashboard.service'; + +/** + * Format date range for display in dashboard components + * Returns a human-readable string describing the active date filter + */ +export function formatDateRangeText( + dateRange: DateRange, + customStartDate?: Date, + customEndDate?: Date, + prefix: string = 'for' +): string { + if (dateRange === 'custom' && customStartDate && customEndDate) { + return `${prefix} ${format(customStartDate, 'MMM d, yyyy')} - ${format(customEndDate, 'MMM d, yyyy')}`; + } + + const rangeMap: Record = { + 'all': '(All Time)', + 'today': `${prefix} Today`, + 'week': `${prefix} This Week`, + 'month': `${prefix} This Month`, + 'quarter': `${prefix} This Quarter`, + 'year': `${prefix} This Year`, + 'last30days': `${prefix} Last 30 Days`, + 'custom': `${prefix} Custom Range` + }; + + return rangeMap[dateRange] || `${prefix} Selected Period`; +} + +/** + * Format date range for card descriptions + */ +export function formatDateRangeDescription( + dateRange: DateRange, + customStartDate?: Date, + customEndDate?: Date +): string { + if (dateRange === 'all') { + return 'All historical data'; + } + + if (dateRange === 'custom' && customStartDate && customEndDate) { + return `Data from ${format(customStartDate, 'MMM d, yyyy')} to ${format(customEndDate, 'MMM d, yyyy')}`; + } + + const rangeMap: Record = { + 'all': 'All historical data', + 'today': "Today's data", + 'week': 'This week data', + 'month': 'This month data', + 'quarter': 'This quarter data', + 'year': 'This year data', + 'last30days': 'Last 30 days data', + 'custom': 'Custom date range' + }; + + return rangeMap[dateRange] || 'Filtered data'; +} + diff --git a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx index 4271f44..8269c80 100644 --- a/src/pages/RequestDetail/components/QuickActionsSidebar.tsx +++ b/src/pages/RequestDetail/components/QuickActionsSidebar.tsx @@ -8,6 +8,7 @@ import { Button } from '@/components/ui/button'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { UserPlus, Eye, CheckCircle, XCircle, Share2, Pause, Play, AlertCircle } from 'lucide-react'; import { getSharedRecipients, type SharedRecipient } from '@/services/summaryApi'; +import { useAuth } from '@/contexts/AuthContext'; interface QuickActionsSidebarProps { request: any; @@ -42,16 +43,22 @@ export function QuickActionsSidebar({ summaryId, refreshTrigger, }: QuickActionsSidebarProps) { + const { user } = useAuth(); const [sharedRecipients, setSharedRecipients] = useState([]); const [loadingRecipients, setLoadingRecipients] = useState(false); const isClosed = request?.status === 'closed'; const isPaused = request?.pauseInfo?.isPaused || false; + const pausedByUserId = request?.pauseInfo?.pausedBy?.userId; + const currentUserId = (user as any)?.userId || ''; + // Both approver AND initiator can pause (when not already paused and not closed) const canPause = !isPaused && !isClosed && (currentApprovalLevel || isInitiator); - // Both approver AND initiator can resume directly + + // Resume: Can be done by the person who paused OR by both initiator and approver const canResume = isPaused && onResume && (currentApprovalLevel || isInitiator); - // Retrigger is no longer needed since initiator can resume directly - const canRetrigger = false; // Disabled - kept for backwards compatibility + + // Retrigger: Only for initiator when approver paused (initiator asks approver to resume) + const canRetrigger = isPaused && isInitiator && pausedByUserId && pausedByUserId !== currentUserId && onRetrigger; // Fetch shared recipients when request is closed and summaryId is available useEffect(() => { diff --git a/src/pages/RequestDetail/components/tabs/OverviewTab.tsx b/src/pages/RequestDetail/components/tabs/OverviewTab.tsx index 23bf306..aa94b26 100644 --- a/src/pages/RequestDetail/components/tabs/OverviewTab.tsx +++ b/src/pages/RequestDetail/components/tabs/OverviewTab.tsx @@ -8,6 +8,7 @@ import { RichTextEditor } from '@/components/ui/rich-text-editor'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { FormattedDescription } from '@/components/common/FormattedDescription'; import { User, FileText, Mail, Phone, CheckCircle, RefreshCw, Loader2, Pause, Play, AlertCircle } from 'lucide-react'; +import { useAuth } from '@/contexts/AuthContext'; import { formatDateTime, formatDateShort } from '@/utils/dateFormatter'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; @@ -48,18 +49,21 @@ export function OverviewTab({ onResume, onRetrigger, currentUserIsApprover = false, - pausedByUserId, - currentUserId, + pausedByUserId: _pausedByUserId, + currentUserId: _currentUserId, }: OverviewTabProps) { void _onPause; // Marked as intentionally unused - available for future use - void pausedByUserId; // Kept for backwards compatibility - void currentUserId; // Kept for backwards compatibility + const { user } = useAuth(); const pauseInfo = request?.pauseInfo; const isPaused = pauseInfo?.isPaused || false; - // Both approver AND initiator can resume directly - const canResume = isPaused && (currentUserIsApprover || isInitiator); - // Retrigger is no longer needed since initiator can resume directly - const canRetrigger = false; // Disabled - kept for backwards compatibility + const pausedByUserId = pauseInfo?.pausedBy?.userId; + const currentUserId = (user as any)?.userId || ''; + + // Resume: Can be done by both initiator and approver + const canResume = isPaused && onResume && (currentUserIsApprover || isInitiator); + + // Retrigger: Only for initiator when approver paused (initiator asks approver to resume) + const canRetrigger = isPaused && isInitiator && pausedByUserId && pausedByUserId !== currentUserId && onRetrigger; return (
{/* Request Initiator Card */} diff --git a/src/pages/Requests/Requests.tsx b/src/pages/Requests/Requests.tsx index 5b50d9d..82e10e9 100644 --- a/src/pages/Requests/Requests.tsx +++ b/src/pages/Requests/Requests.tsx @@ -319,8 +319,8 @@ export function Requests({ onViewRequest }: RequestsProps) { slaCompliance: filters.slaComplianceFilter !== 'all' ? filters.slaComplianceFilter : undefined }; // All Requests (admin/normal user) should always have a date range - // Default to 'month' if no date range is selected - const statsDateRange = filters.dateRange || 'month'; + // Default to 'all' if no date range is selected + const statsDateRange = filters.dateRange || 'all'; fetchBackendStatsRef.current( statsDateRange, @@ -660,9 +660,12 @@ export function Requests({ onViewRequest }: RequestsProps) { + All Time Today This Week This Month + Last 7 Days + Last 30 Days Custom Range diff --git a/src/pages/Requests/UserAllRequests.tsx b/src/pages/Requests/UserAllRequests.tsx index 7e6880f..b28a180 100644 --- a/src/pages/Requests/UserAllRequests.tsx +++ b/src/pages/Requests/UserAllRequests.tsx @@ -580,9 +580,12 @@ export function UserAllRequests({ onViewRequest }: RequestsProps) { + All Time Today This Week This Month + Last 7 Days + Last 30 Days Custom Range diff --git a/src/pages/Requests/hooks/useRequestsFilters.ts b/src/pages/Requests/hooks/useRequestsFilters.ts index 28b3d8e..46dc1fe 100644 --- a/src/pages/Requests/hooks/useRequestsFilters.ts +++ b/src/pages/Requests/hooks/useRequestsFilters.ts @@ -20,7 +20,7 @@ export function useRequestsFilters() { const [approverFilterType, setApproverFilterType] = useState<'current' | 'any'>( searchParams.get('approverType') === 'any' ? 'any' : 'current' ); - const [dateRange, setDateRange] = useState((searchParams.get('dateRange') as DateRange) || 'month'); + const [dateRange, setDateRange] = useState((searchParams.get('dateRange') as DateRange) || 'all'); const [customStartDate, setCustomStartDate] = useState( searchParams.get('startDate') ? new Date(searchParams.get('startDate')!) : undefined ); @@ -101,7 +101,7 @@ export function useRequestsFilters() { setInitiatorFilter('all'); setApproverFilter('all'); setApproverFilterType('current'); - setDateRange('month'); + setDateRange('all'); setCustomStartDate(undefined); setCustomEndDate(undefined); setShowCustomDatePicker(false); @@ -138,7 +138,7 @@ export function useRequestsFilters() { departmentFilter !== 'all' || initiatorFilter !== 'all' || approverFilter !== 'all' || - dateRange !== 'month' || + dateRange !== 'all' || customStartDate || customEndDate ); diff --git a/src/pages/Settings/Settings.tsx b/src/pages/Settings/Settings.tsx index 2992e5c..8e50ab5 100644 --- a/src/pages/Settings/Settings.tsx +++ b/src/pages/Settings/Settings.tsx @@ -16,7 +16,9 @@ import { ConfigurationManager } from '@/components/admin/ConfigurationManager'; import { HolidayManager } from '@/components/admin/HolidayManager'; import { UserRoleManager } from '@/components/admin/UserRoleManager'; import { NotificationStatusModal } from '@/components/settings/NotificationStatusModal'; -import { useState } from 'react'; +import { NotificationPreferencesModal } from '@/components/settings/NotificationPreferencesModal'; +import { useState, useEffect } from 'react'; +import { getUserSubscriptions } from '@/services/notificationApi'; export function Settings() { const { user } = useAuth(); @@ -25,6 +27,26 @@ export function Settings() { const [notificationSuccess, setNotificationSuccess] = useState(false); const [notificationMessage, setNotificationMessage] = useState(); const [isEnablingNotifications, setIsEnablingNotifications] = useState(false); + const [showPreferencesModal, setShowPreferencesModal] = useState(false); + const [hasSubscription, setHasSubscription] = useState(false); + const [checkingSubscription, setCheckingSubscription] = useState(true); + + useEffect(() => { + checkSubscriptionStatus(); + }, []); + + const checkSubscriptionStatus = async () => { + try { + setCheckingSubscription(true); + const subscriptions = await getUserSubscriptions(); + setHasSubscription(subscriptions.length > 0); + } catch (error) { + console.error('Failed to check subscription status:', error); + setHasSubscription(false); + } finally { + setCheckingSubscription(false); + } + }; const handleEnableNotifications = async () => { setIsEnablingNotifications(true); @@ -93,6 +115,8 @@ export function Settings() { setNotificationSuccess(true); setNotificationMessage('Push notifications have been successfully enabled! You will now receive notifications for workflow updates, approvals, and TAT alerts.'); setShowNotificationModal(true); + // Recheck subscription status + await checkSubscriptionStatus(); } catch (error: any) { console.error('[Settings] Error enabling notifications:', error); setNotificationSuccess(false); @@ -177,7 +201,7 @@ export function Settings() { {/* User Settings Tab */}
- {/* Notification Settings */} + {/* Enable Push Notifications Setup */}
@@ -185,20 +209,38 @@ export function Settings() {
- Notifications - Manage notification preferences + Browser Push Setup + Register this browser for push notifications
+ {checkingSubscription ? ( +
+

Checking registration status...

+
+ ) : hasSubscription ? ( +
+

+ โœ“ This browser is already registered for push notifications. +

+
+ ) : ( +
+

+ Click below to register this browser for receiving push notifications. + This needs to be done once per browser/device. +

+
+ )}
@@ -257,16 +299,18 @@ export function Settings() {
Preferences - Application preferences + Notification and application preferences
-
-
-

User preferences will be available soon

-
-
+
@@ -292,7 +336,7 @@ export function Settings() { <> {/* Non-Admin User Settings Only */}
- {/* Notification Settings */} + {/* Enable Push Notifications Setup */}
@@ -300,20 +344,38 @@ export function Settings() {
- Notifications - Manage notification preferences + Browser Push Setup + Register this browser for push notifications
+ {checkingSubscription ? ( +
+

Checking registration status...

+
+ ) : hasSubscription ? ( +
+

+ โœ“ This browser is already registered for push notifications. +

+
+ ) : ( +
+

+ Click below to register this browser for receiving push notifications. + This needs to be done once per browser/device. +

+
+ )}
@@ -372,16 +434,18 @@ export function Settings() {
Preferences - Application preferences + Notification and application preferences
-
-
-

User preferences will be available soon

-
-
+
@@ -395,6 +459,11 @@ export function Settings() { success={notificationSuccess} message={notificationMessage} /> + + setShowPreferencesModal(false)} + /> ); } diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts index 216ace0..78ba9a3 100644 --- a/src/services/dashboard.service.ts +++ b/src/services/dashboard.service.ts @@ -119,9 +119,14 @@ export interface ApproverPerformance { approverId: string; approverName: string; totalApproved: number; + approvedCount: number; + rejectedCount: number; + closedCount: number; tatCompliancePercent: number; avgResponseHours: number; pendingCount: number; + withinTatCount: number; + breachedCount: number; } export interface UpcomingDeadline { @@ -158,7 +163,7 @@ export interface PriorityDistribution { complianceRate: number; } -export type DateRange = 'today' | 'week' | 'month' | 'quarter' | 'year' | 'last30days' | 'custom'; +export type DateRange = 'all' | 'today' | 'week' | 'month' | 'quarter' | 'year' | 'last30days' | 'custom'; class DashboardService { /** @@ -442,8 +447,17 @@ class DashboardService { /** * Get Approver Performance metrics with pagination + * Supports priority and SLA filters for consistent stats behavior */ - async getApproverPerformance(dateRange?: DateRange, page: number = 1, limit: number = 10, startDate?: Date, endDate?: Date): Promise<{ + async getApproverPerformance( + dateRange?: DateRange, + page: number = 1, + limit: number = 10, + startDate?: Date, + endDate?: Date, + priority?: string, + slaCompliance?: string + ): Promise<{ performance: ApproverPerformance[], pagination: { currentPage: number, @@ -453,11 +467,24 @@ class DashboardService { } }> { try { - const params: any = { dateRange, page, limit }; + const params: any = { + dateRange, + page, + limit: limit || 10 // Explicitly set limit (default 10 if not provided) + }; if (dateRange === 'custom' && startDate && endDate) { params.startDate = startDate.toISOString(); params.endDate = endDate.toISOString(); } + if (priority && priority !== 'all') { + params.priority = priority; + } + if (slaCompliance && slaCompliance !== 'all') { + params.slaCompliance = slaCompliance; + } + + console.log('[Dashboard Service] Fetching approver performance with params:', params); + const response = await apiClient.get('/dashboard/stats/approver-performance', { params }); return { performance: response.data.data, @@ -595,6 +622,36 @@ class DashboardService { } } + /** + * Get single approver stats only (dedicated endpoint for performance) + * Only respects date, priority, and SLA filters + */ + async getSingleApproverStats( + approverId: string, + dateRange?: DateRange, + startDate?: Date, + endDate?: Date, + priority?: string, + slaCompliance?: string + ): Promise { + try { + const params: any = { approverId }; + if (dateRange) params.dateRange = dateRange; + if (dateRange === 'custom' && startDate && endDate) { + params.startDate = startDate.toISOString(); + params.endDate = endDate.toISOString(); + } + if (priority && priority !== 'all') params.priority = priority; + if (slaCompliance && slaCompliance !== 'all') params.slaCompliance = slaCompliance; + + const response = await apiClient.get('/dashboard/stats/single-approver', { params }); + return response.data.data; + } catch (error) { + console.error('Failed to fetch single approver stats:', error); + throw error; + } + } + /** * Get requests filtered by approver ID for detailed performance analysis */ @@ -610,7 +667,7 @@ class DashboardService { slaCompliance?: string, search?: string ): Promise<{ - requests: any[], + requests: any[], pagination: { currentPage: number, totalPages: number, diff --git a/src/services/notificationApi.ts b/src/services/notificationApi.ts index 31c5b8c..26cb37c 100644 --- a/src/services/notificationApi.ts +++ b/src/services/notificationApi.ts @@ -12,27 +12,33 @@ export interface Notification { actionUrl?: string; actionRequired: boolean; metadata?: any; - sentVia: string[]; readAt?: string; createdAt: string; } -class NotificationApi { +export interface PushSubscription { + subscriptionId: string; + endpoint: string; + userAgent?: string; + createdAt: string; +} + +const notificationApi = { /** - * Get user's notifications + * Get user's notifications with pagination */ async list(params?: { page?: number; limit?: number; unreadOnly?: boolean }) { const response = await apiClient.get('/notifications', { params }); return response.data; - } + }, /** - * Get unread count + * Get unread notification count */ async getUnreadCount() { const response = await apiClient.get('/notifications/unread-count'); - return response.data.data.unreadCount; - } + return response.data; + }, /** * Mark notification as read @@ -40,15 +46,15 @@ class NotificationApi { async markAsRead(notificationId: string) { const response = await apiClient.patch(`/notifications/${notificationId}/read`); return response.data; - } + }, /** - * Mark all as read + * Mark all notifications as read */ async markAllAsRead() { const response = await apiClient.post('/notifications/mark-all-read'); return response.data; - } + }, /** * Delete notification @@ -57,8 +63,16 @@ class NotificationApi { const response = await apiClient.delete(`/notifications/${notificationId}`); return response.data; } -} +}; + +/** + * Get current user's push notification subscriptions + */ +export const getUserSubscriptions = async (): Promise => { + const response = await apiClient.get<{ success: boolean; data: { subscriptions: PushSubscription[]; count: number } }>( + '/notifications/subscriptions' + ); + return response.data.data.subscriptions; +}; -export const notificationApi = new NotificationApi(); export default notificationApi; - diff --git a/src/services/userPreferenceApi.ts b/src/services/userPreferenceApi.ts new file mode 100644 index 0000000..3af46af --- /dev/null +++ b/src/services/userPreferenceApi.ts @@ -0,0 +1,37 @@ +import apiClient from './authApi'; + +export interface NotificationPreferences { + emailNotificationsEnabled: boolean; + pushNotificationsEnabled: boolean; + inAppNotificationsEnabled: boolean; +} + +export interface UpdateNotificationPreferences { + emailNotificationsEnabled?: boolean; + pushNotificationsEnabled?: boolean; + inAppNotificationsEnabled?: boolean; +} + +/** + * Get current user's notification preferences + */ +export const getNotificationPreferences = async (): Promise => { + const response = await apiClient.get<{ success: boolean; data: NotificationPreferences }>( + '/user/preferences/notifications' + ); + return response.data.data; +}; + +/** + * Update current user's notification preferences + */ +export const updateNotificationPreferences = async ( + preferences: UpdateNotificationPreferences +): Promise => { + const response = await apiClient.put<{ success: boolean; data: NotificationPreferences }>( + '/user/preferences/notifications', + preferences + ); + return response.data.data; +}; + diff --git a/src/utils/requestDetailHelpers.tsx b/src/utils/requestDetailHelpers.tsx index 9ed2427..9eb26cb 100644 --- a/src/utils/requestDetailHelpers.tsx +++ b/src/utils/requestDetailHelpers.tsx @@ -72,6 +72,11 @@ export const getStatusConfig = (status: string) => { color: 'bg-yellow-100 text-yellow-800 border-yellow-200', label: 'pending' }; + case 'paused': + return { + color: 'bg-gray-400 text-gray-100 border-gray-500', + label: 'paused' + }; case 'in-review': return { color: 'bg-blue-100 text-blue-800 border-blue-200',