diff --git a/src/App.tsx b/src/App.tsx index 40c4ab6..77c5d47 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,6 +46,7 @@ import { DealerRelocationPage } from '@/features/relocation/pages/DealerRelocati import QuestionnaireBuilder from '@/components/admin/QuestionnaireBuilder'; import QuestionnaireList from '@/components/admin/QuestionnaireList'; import { WorkNotesPage } from '@/features/onboarding/pages/WorkNotesPage'; +import { NotificationsPage } from '@/pages/NotificationsPage'; import { Toaster } from '@/components/ui/sonner'; import { User } from '@/lib/mock-data'; import { toast } from 'sonner'; @@ -158,6 +159,7 @@ export default function App() { '/approval-policies': 'Approval Policies', '/fdd-dashboard': 'FDD Dashboard', '/fdd-details': 'Audit Workspace', + '/notifications': 'Notifications', }; return titles[pathname] || 'Dashboard'; }; @@ -246,6 +248,7 @@ export default function App() { {/* Other Modules */} } /> + } /> diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 635aee5..0b2e931 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -25,13 +25,19 @@ export function Header({ title, onRefresh }: HeaderProps) { const { user: currentUser } = useSelector((state: RootState) => state.auth); const { socket } = useSocket(); const [notifications, setNotifications] = useState([]); + const [unreadCount, setUnreadCount] = useState(0); useEffect(() => { const fetchNotifications = async () => { try { - const res: any = await notificationService.getNotifications(); + const res: any = await notificationService.getNotifications(1, 15); if (res.success) { setNotifications(res.data); + if (res.pagination && res.pagination.unreadCount !== undefined) { + setUnreadCount(res.pagination.unreadCount); + } else { + setUnreadCount(res.data.filter((n: any) => !n.isRead).length); + } } } catch (error) { console.error('Fetch notifications error:', error); @@ -44,7 +50,8 @@ export function Header({ title, onRefresh }: HeaderProps) { useEffect(() => { if (socket) { socket.on('notification', (newNotification: Notification) => { - setNotifications(prev => [newNotification, ...prev]); + setNotifications(prev => [newNotification, ...prev].slice(0, 15)); + setUnreadCount(prev => prev + 1); toast(newNotification.title, { description: newNotification.message, action: newNotification.link ? { @@ -60,14 +67,20 @@ export function Header({ title, onRefresh }: HeaderProps) { } }, [socket]); - const handleMarkAsRead = async (id: string) => { + const handleNotificationClick = async (notif: Notification) => { try { - const res: any = await notificationService.markAsRead(id); - if (res.success) { - setNotifications(prev => prev.map(n => n.id === id ? { ...n, isRead: true } : n)); + if (!notif.isRead) { + const res: any = await notificationService.markAsRead(notif.id); + if (res.success) { + setNotifications(prev => prev.map(n => n.id === notif.id ? { ...n, isRead: true } : n)); + setUnreadCount(prev => Math.max(0, prev - 1)); + } + } + if (notif.link) { + window.location.href = notif.link; } } catch (error) { - console.error('Mark as read error:', error); + console.error('Notification click error:', error); } }; @@ -76,14 +89,13 @@ export function Header({ title, onRefresh }: HeaderProps) { const res: any = await notificationService.markAllAsRead(); if (res.success) { setNotifications(prev => prev.map(n => ({ ...n, isRead: true }))); + setUnreadCount(0); } } catch (error) { console.error('Mark all as read error:', error); } }; - const unreadCount = notifications.filter(n => !n.isRead).length; - return (
@@ -161,7 +173,7 @@ export function Header({ title, onRefresh }: HeaderProps) { key={notification.id} className={`p-3 cursor-pointer flex items-start gap-3 ${!notification.isRead ? 'bg-blue-50/50' : '' }`} - onClick={() => handleMarkAsRead(notification.id)} + onClick={() => handleNotificationClick(notification)} >

{notification.title}

@@ -178,9 +190,12 @@ export function Header({ title, onRefresh }: HeaderProps) { )}
-

- Stay updated with your mentions and tasks -

+
diff --git a/src/features/fnf/pages/FnFDetails.tsx b/src/features/fnf/pages/FnFDetails.tsx index 9291f8b..b33d5c7 100644 --- a/src/features/fnf/pages/FnFDetails.tsx +++ b/src/features/fnf/pages/FnFDetails.tsx @@ -301,6 +301,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { url: doc.filePath, })), ], + participants: s.participants || [] }; setFnfCase(finalMapped); diff --git a/src/features/master/pages/MasterPage.tsx b/src/features/master/pages/MasterPage.tsx index 7fdab34..4b055fc 100644 --- a/src/features/master/pages/MasterPage.tsx +++ b/src/features/master/pages/MasterPage.tsx @@ -26,8 +26,6 @@ import { EmailTemplates } from '@/features/master/components/EmailTemplates'; import { LocationManagement } from '@/features/master/components/LocationManagement'; import { ASMDialog } from '@/features/master/components/ASMDialog'; import { ZMDialog } from '@/features/master/components/ZMDialog'; -import { DDLeadManagement } from '@/features/master/components/DDLeadManagement'; -import { DDLeadDialog } from '@/features/master/components/DDLeadDialog'; import { ZoneDialog } from '@/features/master/components/ZoneDialog'; import { RegionDialog } from '@/features/master/components/RegionDialog'; import { TemplateDialog } from '@/features/master/components/TemplateDialog'; @@ -40,7 +38,7 @@ import { RootState } from '@/store'; export const MasterPage: React.FC = () => { const { fetchInitialData, fetchAreas } = useMasterData(); const { - asms, zonalManagerMappings, ddLeads, + asms, zonalManagerMappings, allStates, allDistricts, users, @@ -77,12 +75,6 @@ export const MasterPage: React.FC = () => { const [selectedZMZone, setSelectedZMZone] = useState(''); const [selectedZMRegions, setSelectedZMRegions] = useState([]); - // DD-Lead Management State - const [showDDLeadDialog, setShowDDLeadDialog] = useState(false); - const [editingDDLeadId, setEditingDDLeadId] = useState(null); - const [ddLeadManagerId, setDdLeadManagerId] = useState(''); - const [ddLeadStatus, setDdLeadStatus] = useState<'active' | 'inactive'>('active'); - const [selectedDDLeadZones, setSelectedDDLeadZones] = useState([]); // Role Management State const [showRoleDialog, setShowRoleDialog] = useState(false); @@ -207,13 +199,6 @@ export const MasterPage: React.FC = () => { setShowZMDialog(true); }; - const handleEditDDLead = (lead: any) => { - setEditingDDLeadId(lead.id); - setDdLeadManagerId(lead.id); - setDdLeadStatus(lead.status?.toLowerCase() === 'active' ? 'active' : 'inactive'); - setSelectedDDLeadZones(lead.assignedZoneIds || []); - setShowDDLeadDialog(true); - }; const handleSaveZM = async () => { if (!zmManagerId || !selectedZMZone) { @@ -242,31 +227,6 @@ export const MasterPage: React.FC = () => { } }; - const handleSaveDDLead = async () => { - if (!ddLeadManagerId) { - toast.error('Manager user is required'); - return; - } - try { - const payload = { - userId: ddLeadManagerId, - zoneIds: selectedDDLeadZones, - status: ddLeadStatus - }; - - const res = await (masterService as any).saveDDLead(payload) as any; - if (res.success) { - toast.success(`DD Lead ${editingDDLeadId ? 'updated' : 'assigned'} successfully`); - setShowDDLeadDialog(false); - fetchInitialData(); - } else { - toast.error(res.message || 'Failed to save DD Lead'); - } - } catch (error: any) { - const msg = error?.response?.data?.message || error?.message || 'Failed to save DD Lead'; - toast.error(msg); - } - }; const handleSaveZone = async () => { @@ -536,14 +496,6 @@ export const MasterPage: React.FC = () => { { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setShowASMDialog(true); }} onEditASM={handleEditASM} onDeleteASM={() => toast.error('ASM deletion restricted')} /> - { - setEditingDDLeadId(null); setDdLeadManagerId(''); - setDdLeadStatus('active'); setSelectedDDLeadZones([]); - setShowDDLeadDialog(true); - }} - onEditLead={handleEditDDLead} - onDeleteLead={() => toast.error('DD-Lead deletion restricted')} /> 0 ? users : asms} /> @@ -641,19 +593,6 @@ export const MasterPage: React.FC = () => { onSave={handleSaveZM} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} /> - 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles, assignedZoneIds: (ddLeads.find(l => l.id === u.id)?.assignedZoneIds || [])})) : asms} - /> diff --git a/src/features/onboarding/pages/WorkNotesPage.tsx b/src/features/onboarding/pages/WorkNotesPage.tsx index 962a017..306100c 100644 --- a/src/features/onboarding/pages/WorkNotesPage.tsx +++ b/src/features/onboarding/pages/WorkNotesPage.tsx @@ -348,6 +348,16 @@ export function WorkNotesPage(props: Partial) { if (!appName || appName === 'Application' || appName === 'Resignation') setAppName(data.dealer?.businessName || 'Resignation'); if (!regNumber) setRegNumber(data.resignationId || ''); } + } else if (requestType === 'fnf') { + const { API } = await import('@/api/API'); + const res: any = await API.getFnFSettlementById(requestId); + if (res.data?.success) { + data = res.data.fnf; + if (externalParticipants.length === 0 && data.participants) setExternalParticipants(data.participants || []); + const dealerName = data.outlet?.dealer?.fullName || data.dealer?.fullName || "F&F Settlement"; + if (!appName || appName === 'Application' || appName === 'F&F Settlement') setAppName(dealerName); + if (!regNumber) setRegNumber(data.settlementId || ''); + } } } catch (error) { console.error(`Failed to fetch ${requestType} details:`, error); diff --git a/src/pages/NotificationsPage.tsx b/src/pages/NotificationsPage.tsx new file mode 100644 index 0000000..c32114a --- /dev/null +++ b/src/pages/NotificationsPage.tsx @@ -0,0 +1,171 @@ +import React, { useState, useEffect } from 'react'; +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Bell, Check, Clock, ChevronLeft, ChevronRight } from 'lucide-react'; +import { notificationService, Notification } from '@/services/notification.service'; +import { useNavigate } from 'react-router-dom'; +import { formatDistanceToNow } from 'date-fns'; + +export const NotificationsPage: React.FC = () => { + const [notifications, setNotifications] = useState([]); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const limit = 15; + const navigate = useNavigate(); + + const fetchNotifications = async (currentPage: number) => { + try { + setLoading(true); + const res: any = await notificationService.getNotifications(currentPage, limit); + if (res.success) { + setNotifications(res.data); + if (res.pagination) { + setTotalPages(res.pagination.totalPages); + } + } + } catch (error) { + console.error('Failed to fetch notifications', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchNotifications(page); + }, [page]); + + const handleMarkAsRead = async (id: string, e: React.MouseEvent) => { + e.stopPropagation(); + await notificationService.markAsRead(id); + fetchNotifications(page); + }; + + const handleMarkAllAsRead = async () => { + await notificationService.markAllAsRead(); + fetchNotifications(page); + }; + + const handleNotificationClick = async (notif: Notification) => { + if (!notif.isRead) { + await notificationService.markAsRead(notif.id); + } + if (notif.link) { + // Support both relative or absolute links dynamically + const url = notif.link; + try { + const parsedUrl = new URL(url); + if (parsedUrl.origin === window.location.origin) { + navigate(parsedUrl.pathname + parsedUrl.search + parsedUrl.hash); + } else { + window.open(url, '_blank'); + } + } catch (e) { + navigate(url); + } + } else { + fetchNotifications(page); // refresh list manually if no link + } + }; + + return ( +
+
+
+

+ + Notifications Center +

+

View and manage all your alerts

+
+ +
+ + + + {loading ? ( +
+
+
+ ) : notifications.length === 0 ? ( +
+ +

No notifications yet

+

When you get updates, they'll show up here.

+
+ ) : ( +
+ {notifications.map(notif => ( +
handleNotificationClick(notif)} + > +
handleMarkAsRead(notif.id, e)}> + {!notif.isRead ? ( +
+ ) : ( +
+ )} +
+
+
+

+ {notif.title} +

+
+ + {formatDistanceToNow(new Date(notif.createdAt), { addSuffix: true })} +
+
+

{notif.message}

+
+
+ ))} +
+ )} +
+
+ + {/* Pagination Controls */} + {!loading && totalPages > 1 && ( +
+

+ Showing page {page} of{' '} + {totalPages} +

+
+ + +
+
+ )} +
+ ); +}; + +export default NotificationsPage; diff --git a/src/services/notification.service.ts b/src/services/notification.service.ts index f1ad5f2..a3805f8 100644 --- a/src/services/notification.service.ts +++ b/src/services/notification.service.ts @@ -14,8 +14,12 @@ export interface Notification { } export const notificationService = { - getNotifications: async () => { - const response = await client.get(`${API_BASE}/notifications`); + getNotifications: async (page?: number, limit?: number) => { + let url = `${API_BASE}/notifications`; + if (page && limit) { + url += `?page=${page}&limit=${limit}`; + } + const response = await client.get(url); return response.data; },