207 lines
7.3 KiB
TypeScript
207 lines
7.3 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Bell, RefreshCw, HelpCircle, User as UserIcon } from 'lucide-react';
|
|
import { Button } from '../ui/button';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from '../ui/dropdown-menu';
|
|
import { Badge } from '../ui/badge';
|
|
import { toast } from 'sonner';
|
|
|
|
import { useSelector } from 'react-redux';
|
|
import { RootState } from '../../store';
|
|
import { notificationService, Notification } from '../../services/notification.service';
|
|
import { useSocket } from '../../context/SocketContext';
|
|
import { formatDistanceToNow } from 'date-fns';
|
|
|
|
interface HeaderProps {
|
|
title: string;
|
|
onRefresh?: () => void;
|
|
}
|
|
|
|
export function Header({ title, onRefresh }: HeaderProps) {
|
|
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
|
const { socket } = useSocket();
|
|
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
const [unreadCount, setUnreadCount] = useState<number>(0);
|
|
|
|
useEffect(() => {
|
|
const fetchNotifications = async () => {
|
|
try {
|
|
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);
|
|
}
|
|
};
|
|
|
|
fetchNotifications();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (socket) {
|
|
socket.on('notification', (newNotification: Notification) => {
|
|
setNotifications(prev => [newNotification, ...prev].slice(0, 15));
|
|
setUnreadCount(prev => prev + 1);
|
|
toast(newNotification.title, {
|
|
description: newNotification.message,
|
|
action: newNotification.link ? {
|
|
label: 'View',
|
|
onClick: () => window.location.href = newNotification.link!
|
|
} : undefined
|
|
});
|
|
});
|
|
|
|
return () => {
|
|
socket.off('notification');
|
|
};
|
|
}
|
|
}, [socket]);
|
|
|
|
const handleNotificationClick = async (notif: Notification) => {
|
|
try {
|
|
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('Notification click error:', error);
|
|
}
|
|
};
|
|
|
|
const handleMarkAllAsRead = async () => {
|
|
try {
|
|
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);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<header className="bg-white border-b border-slate-200 px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-slate-900">{title}</h1>
|
|
<p className="text-slate-600">Manage and track dealership applications</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-3">
|
|
{/* Current User Info */}
|
|
{currentUser && (
|
|
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
|
|
<div className="w-8 h-8 bg-re-red rounded-full flex items-center justify-center">
|
|
<UserIcon className="w-4 h-4 text-white" />
|
|
</div>
|
|
<div className="text-left">
|
|
<p className="text-slate-900">{currentUser.name}</p>
|
|
<p className="text-slate-600">{currentUser.role}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Refresh Button */}
|
|
{onRefresh && (
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={onRefresh}
|
|
title="Refresh"
|
|
>
|
|
<RefreshCw className="w-4 h-4" />
|
|
</Button>
|
|
)}
|
|
|
|
{/* Help */}
|
|
<Button variant="outline" size="icon" title="Help">
|
|
<HelpCircle className="w-4 h-4" />
|
|
</Button>
|
|
|
|
{/* Notifications */}
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="icon" className="relative">
|
|
<Bell className="w-4 h-4" />
|
|
{unreadCount > 0 && (
|
|
<Badge
|
|
variant="destructive"
|
|
className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center text-xs"
|
|
>
|
|
{unreadCount}
|
|
</Badge>
|
|
)}
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-80">
|
|
<div className="p-3 border-b flex items-center justify-between">
|
|
<p className="font-semibold text-slate-900">Notifications</p>
|
|
{unreadCount > 0 && (
|
|
<button
|
|
onClick={handleMarkAllAsRead}
|
|
className="text-xs text-blue-600 hover:underline"
|
|
>
|
|
Mark all read
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="max-h-96 overflow-y-auto custom-scrollbar">
|
|
{notifications.length === 0 ? (
|
|
<div className="p-8 text-center text-slate-500">
|
|
No notifications yet
|
|
</div>
|
|
) : (
|
|
notifications.map((notification) => (
|
|
<DropdownMenuItem
|
|
key={notification.id}
|
|
className={`p-3 cursor-pointer flex items-start gap-3 ${!notification.isRead ? 'bg-blue-50/50' : ''
|
|
}`}
|
|
onClick={() => handleNotificationClick(notification)}
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-slate-900 text-sm font-medium">{notification.title}</p>
|
|
<p className="text-slate-600 text-xs mt-1 leading-relaxed">{notification.message}</p>
|
|
<p className="text-slate-400 text-[10px] mt-2">
|
|
{formatDistanceToNow(new Date(notification.createdAt), { addSuffix: true })}
|
|
</p>
|
|
</div>
|
|
{!notification.isRead && (
|
|
<div className="w-2 h-2 bg-blue-600 rounded-full mt-1.5 flex-shrink-0"></div>
|
|
)}
|
|
</DropdownMenuItem>
|
|
))
|
|
)}
|
|
</div>
|
|
<div className="p-3 border-t text-center">
|
|
<button
|
|
onClick={() => window.location.href = '/notifications'}
|
|
className="text-xs font-medium text-blue-600 hover:text-blue-800 transition-colors"
|
|
>
|
|
View All Notifications
|
|
</button>
|
|
</div>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|