DD Lead treated as natiolwide user and in app notification enhanced created the in app notification screen with pagination
This commit is contained in:
parent
0e8d6bf49f
commit
37b3075a08
@ -46,6 +46,7 @@ import { DealerRelocationPage } from '@/features/relocation/pages/DealerRelocati
|
|||||||
import QuestionnaireBuilder from '@/components/admin/QuestionnaireBuilder';
|
import QuestionnaireBuilder from '@/components/admin/QuestionnaireBuilder';
|
||||||
import QuestionnaireList from '@/components/admin/QuestionnaireList';
|
import QuestionnaireList from '@/components/admin/QuestionnaireList';
|
||||||
import { WorkNotesPage } from '@/features/onboarding/pages/WorkNotesPage';
|
import { WorkNotesPage } from '@/features/onboarding/pages/WorkNotesPage';
|
||||||
|
import { NotificationsPage } from '@/pages/NotificationsPage';
|
||||||
import { Toaster } from '@/components/ui/sonner';
|
import { Toaster } from '@/components/ui/sonner';
|
||||||
import { User } from '@/lib/mock-data';
|
import { User } from '@/lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -158,6 +159,7 @@ export default function App() {
|
|||||||
'/approval-policies': 'Approval Policies',
|
'/approval-policies': 'Approval Policies',
|
||||||
'/fdd-dashboard': 'FDD Dashboard',
|
'/fdd-dashboard': 'FDD Dashboard',
|
||||||
'/fdd-details': 'Audit Workspace',
|
'/fdd-details': 'Audit Workspace',
|
||||||
|
'/notifications': 'Notifications',
|
||||||
};
|
};
|
||||||
return titles[pathname] || 'Dashboard';
|
return titles[pathname] || 'Dashboard';
|
||||||
};
|
};
|
||||||
@ -246,6 +248,7 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Other Modules */}
|
{/* Other Modules */}
|
||||||
<Route path="/users" element={<UserManagementPage />} />
|
<Route path="/users" element={<UserManagementPage />} />
|
||||||
|
<Route path="/notifications" element={<NotificationsPage />} />
|
||||||
<Route path="/approval-policies" element={
|
<Route path="/approval-policies" element={
|
||||||
(hasRole(['Super Admin', 'DD Admin']))
|
(hasRole(['Super Admin', 'DD Admin']))
|
||||||
? <ApprovalPoliciesPage />
|
? <ApprovalPoliciesPage />
|
||||||
|
|||||||
@ -25,13 +25,19 @@ export function Header({ title, onRefresh }: HeaderProps) {
|
|||||||
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
||||||
const { socket } = useSocket();
|
const { socket } = useSocket();
|
||||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||||
|
const [unreadCount, setUnreadCount] = useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchNotifications = async () => {
|
const fetchNotifications = async () => {
|
||||||
try {
|
try {
|
||||||
const res: any = await notificationService.getNotifications();
|
const res: any = await notificationService.getNotifications(1, 15);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setNotifications(res.data);
|
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) {
|
} catch (error) {
|
||||||
console.error('Fetch notifications error:', error);
|
console.error('Fetch notifications error:', error);
|
||||||
@ -44,7 +50,8 @@ export function Header({ title, onRefresh }: HeaderProps) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.on('notification', (newNotification: Notification) => {
|
socket.on('notification', (newNotification: Notification) => {
|
||||||
setNotifications(prev => [newNotification, ...prev]);
|
setNotifications(prev => [newNotification, ...prev].slice(0, 15));
|
||||||
|
setUnreadCount(prev => prev + 1);
|
||||||
toast(newNotification.title, {
|
toast(newNotification.title, {
|
||||||
description: newNotification.message,
|
description: newNotification.message,
|
||||||
action: newNotification.link ? {
|
action: newNotification.link ? {
|
||||||
@ -60,14 +67,20 @@ export function Header({ title, onRefresh }: HeaderProps) {
|
|||||||
}
|
}
|
||||||
}, [socket]);
|
}, [socket]);
|
||||||
|
|
||||||
const handleMarkAsRead = async (id: string) => {
|
const handleNotificationClick = async (notif: Notification) => {
|
||||||
try {
|
try {
|
||||||
const res: any = await notificationService.markAsRead(id);
|
if (!notif.isRead) {
|
||||||
|
const res: any = await notificationService.markAsRead(notif.id);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setNotifications(prev => prev.map(n => n.id === id ? { ...n, isRead: true } : n));
|
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) {
|
} 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();
|
const res: any = await notificationService.markAllAsRead();
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
|
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
|
||||||
|
setUnreadCount(0);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Mark all as read error:', error);
|
console.error('Mark all as read error:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const unreadCount = notifications.filter(n => !n.isRead).length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="bg-white border-b border-slate-200 px-6 py-4">
|
<header className="bg-white border-b border-slate-200 px-6 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -161,7 +173,7 @@ export function Header({ title, onRefresh }: HeaderProps) {
|
|||||||
key={notification.id}
|
key={notification.id}
|
||||||
className={`p-3 cursor-pointer flex items-start gap-3 ${!notification.isRead ? 'bg-blue-50/50' : ''
|
className={`p-3 cursor-pointer flex items-start gap-3 ${!notification.isRead ? 'bg-blue-50/50' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => handleMarkAsRead(notification.id)}
|
onClick={() => handleNotificationClick(notification)}
|
||||||
>
|
>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-slate-900 text-sm font-medium">{notification.title}</p>
|
<p className="text-slate-900 text-sm font-medium">{notification.title}</p>
|
||||||
@ -178,9 +190,12 @@ export function Header({ title, onRefresh }: HeaderProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 border-t text-center">
|
<div className="p-3 border-t text-center">
|
||||||
<p className="text-xs text-slate-400">
|
<button
|
||||||
Stay updated with your mentions and tasks
|
onClick={() => window.location.href = '/notifications'}
|
||||||
</p>
|
className="text-xs font-medium text-blue-600 hover:text-blue-800 transition-colors"
|
||||||
|
>
|
||||||
|
View All Notifications
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|||||||
@ -301,6 +301,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
url: doc.filePath,
|
url: doc.filePath,
|
||||||
})),
|
})),
|
||||||
],
|
],
|
||||||
|
participants: s.participants || []
|
||||||
};
|
};
|
||||||
setFnfCase(finalMapped);
|
setFnfCase(finalMapped);
|
||||||
|
|
||||||
|
|||||||
@ -26,8 +26,6 @@ import { EmailTemplates } from '@/features/master/components/EmailTemplates';
|
|||||||
import { LocationManagement } from '@/features/master/components/LocationManagement';
|
import { LocationManagement } from '@/features/master/components/LocationManagement';
|
||||||
import { ASMDialog } from '@/features/master/components/ASMDialog';
|
import { ASMDialog } from '@/features/master/components/ASMDialog';
|
||||||
import { ZMDialog } from '@/features/master/components/ZMDialog';
|
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 { ZoneDialog } from '@/features/master/components/ZoneDialog';
|
||||||
import { RegionDialog } from '@/features/master/components/RegionDialog';
|
import { RegionDialog } from '@/features/master/components/RegionDialog';
|
||||||
import { TemplateDialog } from '@/features/master/components/TemplateDialog';
|
import { TemplateDialog } from '@/features/master/components/TemplateDialog';
|
||||||
@ -40,7 +38,7 @@ import { RootState } from '@/store';
|
|||||||
export const MasterPage: React.FC = () => {
|
export const MasterPage: React.FC = () => {
|
||||||
const { fetchInitialData, fetchAreas } = useMasterData();
|
const { fetchInitialData, fetchAreas } = useMasterData();
|
||||||
const {
|
const {
|
||||||
asms, zonalManagerMappings, ddLeads,
|
asms, zonalManagerMappings,
|
||||||
allStates,
|
allStates,
|
||||||
allDistricts,
|
allDistricts,
|
||||||
users,
|
users,
|
||||||
@ -77,12 +75,6 @@ export const MasterPage: React.FC = () => {
|
|||||||
const [selectedZMZone, setSelectedZMZone] = useState('');
|
const [selectedZMZone, setSelectedZMZone] = useState('');
|
||||||
const [selectedZMRegions, setSelectedZMRegions] = useState<string[]>([]);
|
const [selectedZMRegions, setSelectedZMRegions] = useState<string[]>([]);
|
||||||
|
|
||||||
// DD-Lead Management State
|
|
||||||
const [showDDLeadDialog, setShowDDLeadDialog] = useState(false);
|
|
||||||
const [editingDDLeadId, setEditingDDLeadId] = useState<string | null>(null);
|
|
||||||
const [ddLeadManagerId, setDdLeadManagerId] = useState('');
|
|
||||||
const [ddLeadStatus, setDdLeadStatus] = useState<'active' | 'inactive'>('active');
|
|
||||||
const [selectedDDLeadZones, setSelectedDDLeadZones] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// Role Management State
|
// Role Management State
|
||||||
const [showRoleDialog, setShowRoleDialog] = useState(false);
|
const [showRoleDialog, setShowRoleDialog] = useState(false);
|
||||||
@ -207,13 +199,6 @@ export const MasterPage: React.FC = () => {
|
|||||||
setShowZMDialog(true);
|
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 () => {
|
const handleSaveZM = async () => {
|
||||||
if (!zmManagerId || !selectedZMZone) {
|
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 () => {
|
const handleSaveZone = async () => {
|
||||||
@ -536,14 +496,6 @@ export const MasterPage: React.FC = () => {
|
|||||||
<ASMManagement selectedZone={selectedZone} onAddASM={() => { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setShowASMDialog(true); }}
|
<ASMManagement selectedZone={selectedZone} onAddASM={() => { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setShowASMDialog(true); }}
|
||||||
onEditASM={handleEditASM} onDeleteASM={() => toast.error('ASM deletion restricted')} />
|
onEditASM={handleEditASM} onDeleteASM={() => toast.error('ASM deletion restricted')} />
|
||||||
|
|
||||||
<DDLeadManagement selectedZone={selectedZone}
|
|
||||||
onAddLead={() => {
|
|
||||||
setEditingDDLeadId(null); setDdLeadManagerId('');
|
|
||||||
setDdLeadStatus('active'); setSelectedDDLeadZones([]);
|
|
||||||
setShowDDLeadDialog(true);
|
|
||||||
}}
|
|
||||||
onEditLead={handleEditDDLead}
|
|
||||||
onDeleteLead={() => toast.error('DD-Lead deletion restricted')} />
|
|
||||||
|
|
||||||
<UserManagementTable userAssignedData={users.length > 0 ? users : asms} />
|
<UserManagementTable userAssignedData={users.length > 0 ? users : asms} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@ -641,19 +593,6 @@ export const MasterPage: React.FC = () => {
|
|||||||
onSave={handleSaveZM}
|
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}
|
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}
|
||||||
/>
|
/>
|
||||||
<DDLeadDialog
|
|
||||||
isOpen={showDDLeadDialog}
|
|
||||||
onOpenChange={setShowDDLeadDialog}
|
|
||||||
editingLeadId={editingDDLeadId}
|
|
||||||
leadManagerId={ddLeadManagerId}
|
|
||||||
setLeadManagerId={setDdLeadManagerId}
|
|
||||||
leadStatus={ddLeadStatus}
|
|
||||||
setLeadStatus={setDdLeadStatus}
|
|
||||||
selectedZones={selectedDDLeadZones}
|
|
||||||
setSelectedZones={setSelectedDDLeadZones}
|
|
||||||
onSave={handleSaveDDLead}
|
|
||||||
userAssignedData={users.length > 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}
|
|
||||||
/>
|
|
||||||
<TemplateDialog isOpen={showTemplateDialog} onOpenChange={setShowTemplateDialog} editingTemplate={editingTemplate} setEditingTemplate={setEditingTemplate} testDataInput={testDataInput} setTestDataInput={setTestDataInput} previewLoading={previewLoading} handlePreviewTemplate={handlePreviewTemplate} previewContent={previewContent} handleSaveTemplate={handleSaveTemplate} />
|
<TemplateDialog isOpen={showTemplateDialog} onOpenChange={setShowTemplateDialog} editingTemplate={editingTemplate} setEditingTemplate={setEditingTemplate} testDataInput={testDataInput} setTestDataInput={setTestDataInput} previewLoading={previewLoading} handlePreviewTemplate={handlePreviewTemplate} previewContent={previewContent} handleSaveTemplate={handleSaveTemplate} />
|
||||||
<LocationDialog isOpen={showLocationDialog} onOpenChange={setShowLocationDialog} editingLocationId={editingLocationId} locationState={locationState} setLocationState={setLocationState} locationDistrict={locationDistrict} setLocationDistrict={setLocationDistrict} locationCity={locationCity} setLocationCity={setLocationCity} locationActiveFrom={locationActiveFrom} setLocationActiveFrom={setLocationActiveFrom} locationActiveTo={locationActiveTo} setLocationActiveTo={setLocationActiveTo} locationStatus={locationStatus} setLocationStatus={setLocationStatus} allStates={allStates} allDistricts={allDistricts} onSave={handleSaveLocation} />
|
<LocationDialog isOpen={showLocationDialog} onOpenChange={setShowLocationDialog} editingLocationId={editingLocationId} locationState={locationState} setLocationState={setLocationState} locationDistrict={locationDistrict} setLocationDistrict={setLocationDistrict} locationCity={locationCity} setLocationCity={setLocationCity} locationActiveFrom={locationActiveFrom} setLocationActiveFrom={setLocationActiveFrom} locationActiveTo={locationActiveTo} setLocationActiveTo={setLocationActiveTo} locationStatus={locationStatus} setLocationStatus={setLocationStatus} allStates={allStates} allDistricts={allDistricts} onSave={handleSaveLocation} />
|
||||||
<RoleDialog isOpen={showRoleDialog} onOpenChange={setShowRoleDialog} role={editingRole} onSave={handleSaveRole} />
|
<RoleDialog isOpen={showRoleDialog} onOpenChange={setShowRoleDialog} role={editingRole} onSave={handleSaveRole} />
|
||||||
|
|||||||
@ -348,6 +348,16 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
|
|||||||
if (!appName || appName === 'Application' || appName === 'Resignation') setAppName(data.dealer?.businessName || 'Resignation');
|
if (!appName || appName === 'Application' || appName === 'Resignation') setAppName(data.dealer?.businessName || 'Resignation');
|
||||||
if (!regNumber) setRegNumber(data.resignationId || '');
|
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) {
|
} catch (error) {
|
||||||
console.error(`Failed to fetch ${requestType} details:`, error);
|
console.error(`Failed to fetch ${requestType} details:`, error);
|
||||||
|
|||||||
171
src/pages/NotificationsPage.tsx
Normal file
171
src/pages/NotificationsPage.tsx
Normal file
@ -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<Notification[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="p-6 max-w-5xl mx-auto">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-2">
|
||||||
|
<Bell className="w-6 h-6 text-blue-600" />
|
||||||
|
Notifications Center
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500 text-sm mt-1">View and manage all your alerts</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleMarkAllAsRead}
|
||||||
|
disabled={loading || !notifications.some(n => !n.isRead)}
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4 mr-2" />
|
||||||
|
Mark all as read
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="shadow-sm border-gray-200">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
) : notifications.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
|
||||||
|
<Bell className="w-12 h-12 text-gray-300 mb-4" />
|
||||||
|
<p className="text-lg font-medium">No notifications yet</p>
|
||||||
|
<p className="text-sm">When you get updates, they'll show up here.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-gray-100">
|
||||||
|
{notifications.map(notif => (
|
||||||
|
<div
|
||||||
|
key={notif.id}
|
||||||
|
className={`p-4 hover:bg-gray-50 transition-colors flex items-start gap-4 cursor-pointer ${!notif.isRead ? 'bg-blue-50/50' : ''}`}
|
||||||
|
onClick={() => handleNotificationClick(notif)}
|
||||||
|
>
|
||||||
|
<div className="mt-1 flex-shrink-0 cursor-pointer" onClick={(e) => handleMarkAsRead(notif.id, e)}>
|
||||||
|
{!notif.isRead ? (
|
||||||
|
<div className="w-3 h-3 bg-blue-600 rounded-full shadow-sm"></div>
|
||||||
|
) : (
|
||||||
|
<div className="w-3 h-3 border-2 border-gray-300 rounded-full"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex justify-between items-start mb-1">
|
||||||
|
<h4 className={`text-sm font-semibold ${!notif.isRead ? 'text-gray-900' : 'text-gray-700'}`}>
|
||||||
|
{notif.title}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center text-xs text-gray-500 whitespace-nowrap ml-4">
|
||||||
|
<Clock className="w-3 h-3 mr-1" />
|
||||||
|
{formatDistanceToNow(new Date(notif.createdAt), { addSuffix: true })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 line-clamp-2">{notif.message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pagination Controls */}
|
||||||
|
{!loading && totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between mt-6 px-4">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
Showing page <span className="font-medium text-gray-900">{page}</span> of{' '}
|
||||||
|
<span className="font-medium text-gray-900">{totalPages}</span>
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={page === 1}
|
||||||
|
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
disabled={page === totalPages}
|
||||||
|
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotificationsPage;
|
||||||
@ -14,8 +14,12 @@ export interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const notificationService = {
|
export const notificationService = {
|
||||||
getNotifications: async () => {
|
getNotifications: async (page?: number, limit?: number) => {
|
||||||
const response = await client.get(`${API_BASE}/notifications`);
|
let url = `${API_BASE}/notifications`;
|
||||||
|
if (page && limit) {
|
||||||
|
url += `?page=${page}&limit=${limit}`;
|
||||||
|
}
|
||||||
|
const response = await client.get(url);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user