262 lines
9.8 KiB
TypeScript
262 lines
9.8 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { Search, Bell, User, Settings, LogOut } from 'lucide-react';
|
||
import { useUser } from '../../context/UserContext';
|
||
import { useAuth } from '../../context/AuthContext';
|
||
|
||
interface Notification {
|
||
id: string;
|
||
type: 'timecard' | 'schedule' | 'payroll' | 'general';
|
||
title: string;
|
||
time: string;
|
||
path: string;
|
||
}
|
||
|
||
export const TopBar: React.FC = () => {
|
||
const { user } = useUser();
|
||
const { logout } = useAuth();
|
||
const navigate = useNavigate();
|
||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||
const [showNotifications, setShowNotifications] = useState(false);
|
||
|
||
// Initialize notifications from localStorage or use default
|
||
const getDefaultNotifications = (): Notification[] => {
|
||
const timecardPath = ['manager', 'hr', 'admin'].includes(user.role) ? '/team-timecards' : '/timecards';
|
||
const schedulePath = ['manager', 'hr', 'admin'].includes(user.role) ? '/team-schedules' : '/schedule';
|
||
|
||
return [
|
||
{
|
||
id: '1',
|
||
type: 'timecard',
|
||
title: 'New timecard submitted',
|
||
time: '2 hours ago',
|
||
path: timecardPath,
|
||
},
|
||
{
|
||
id: '2',
|
||
type: 'schedule',
|
||
title: 'Schedule updated',
|
||
time: '5 hours ago',
|
||
path: schedulePath,
|
||
},
|
||
{
|
||
id: '3',
|
||
type: 'payroll',
|
||
title: 'Payroll reminder',
|
||
time: '1 day ago',
|
||
path: '/payroll-runs',
|
||
},
|
||
];
|
||
};
|
||
|
||
const [notifications, setNotifications] = useState<Notification[]>(() => {
|
||
const saved = localStorage.getItem('notifications');
|
||
if (saved) {
|
||
try {
|
||
const parsed = JSON.parse(saved);
|
||
// If saved notifications exist, use them; otherwise use defaults
|
||
return parsed.length > 0 ? parsed : getDefaultNotifications();
|
||
} catch {
|
||
return getDefaultNotifications();
|
||
}
|
||
}
|
||
return getDefaultNotifications();
|
||
});
|
||
|
||
// Save notifications to localStorage whenever they change
|
||
React.useEffect(() => {
|
||
localStorage.setItem('notifications', JSON.stringify(notifications));
|
||
}, [notifications]);
|
||
|
||
const handleLogout = () => {
|
||
logout();
|
||
navigate('/login');
|
||
};
|
||
|
||
const handleNotificationClick = (path: string) => {
|
||
navigate(path);
|
||
setShowNotifications(false);
|
||
};
|
||
|
||
const handleDismissNotification = (id: string, e: React.MouseEvent) => {
|
||
e.stopPropagation(); // Prevent navigation when clicking dismiss
|
||
setNotifications(prev => prev.filter(n => n.id !== id));
|
||
};
|
||
|
||
const handleClearAllNotifications = () => {
|
||
setNotifications([]);
|
||
};
|
||
|
||
// Close menus when clicking outside
|
||
React.useEffect(() => {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
const target = event.target as HTMLElement;
|
||
if (!target.closest('.notification-menu') && !target.closest('.notification-button')) {
|
||
setShowNotifications(false);
|
||
}
|
||
if (!target.closest('.profile-menu') && !target.closest('.profile-button')) {
|
||
setShowProfileMenu(false);
|
||
}
|
||
};
|
||
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||
}, []);
|
||
|
||
return (
|
||
<div className="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6 sticky top-0 z-50">
|
||
<div className="flex items-center gap-6">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
|
||
<span className="text-white font-bold text-lg">A</span>
|
||
</div>
|
||
<span className="text-xl font-bold text-gray-900">Axion</span>
|
||
</div>
|
||
<div className="hidden md:flex items-center gap-2 bg-gray-100 rounded-lg px-4 py-2 w-64">
|
||
<Search className="w-4 h-4 text-gray-400" />
|
||
<input
|
||
type="text"
|
||
placeholder="Search..."
|
||
className="bg-transparent border-none outline-none flex-1 text-sm"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-4">
|
||
<div className="relative">
|
||
<button
|
||
onClick={() => setShowNotifications(!showNotifications)}
|
||
className="notification-button relative p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||
>
|
||
<Bell className="w-5 h-5 text-gray-600" />
|
||
{notifications.length > 0 && (
|
||
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full"></span>
|
||
)}
|
||
</button>
|
||
|
||
{showNotifications && (
|
||
<div className="notification-menu absolute right-0 mt-2 w-80 bg-white rounded-lg shadow-lg border border-gray-200 z-50 max-h-96 overflow-y-auto">
|
||
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
|
||
<h3 className="text-lg font-semibold text-gray-900">Notifications</h3>
|
||
{notifications.length > 0 && (
|
||
<button
|
||
onClick={handleClearAllNotifications}
|
||
className="text-xs text-gray-500 hover:text-gray-700 font-medium"
|
||
>
|
||
Clear all
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div className="divide-y divide-gray-200">
|
||
{notifications.length === 0 ? (
|
||
<div className="p-8 text-center">
|
||
<Bell className="w-8 h-8 text-gray-300 mx-auto mb-2" />
|
||
<p className="text-sm text-gray-500">No notifications</p>
|
||
</div>
|
||
) : (
|
||
notifications.map((notification) => {
|
||
const getColor = () => {
|
||
switch (notification.type) {
|
||
case 'timecard':
|
||
return 'bg-blue-500';
|
||
case 'schedule':
|
||
return 'bg-green-500';
|
||
case 'payroll':
|
||
return 'bg-yellow-500';
|
||
default:
|
||
return 'bg-gray-500';
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div
|
||
key={notification.id}
|
||
className="p-4 hover:bg-gray-50 cursor-pointer relative group"
|
||
onClick={() => handleNotificationClick(notification.path)}
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<div className={`w-2 h-2 ${getColor()} rounded-full mt-2`}></div>
|
||
<div className="flex-1">
|
||
<p className="text-sm font-medium text-gray-900">{notification.title}</p>
|
||
<p className="text-xs text-gray-500 mt-1">{notification.time}</p>
|
||
</div>
|
||
<button
|
||
onClick={(e) => handleDismissNotification(notification.id, e)}
|
||
className="opacity-0 group-hover:opacity-100 text-gray-400 hover:text-gray-600 transition-opacity ml-2"
|
||
title="Dismiss"
|
||
>
|
||
<span className="text-lg">×</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})
|
||
)}
|
||
</div>
|
||
{notifications.length > 0 && (
|
||
<div className="p-3 border-t border-gray-200 text-center">
|
||
<button
|
||
onClick={handleClearAllNotifications}
|
||
className="text-sm text-primary-600 hover:text-primary-700 font-medium"
|
||
>
|
||
Clear all notifications
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="relative">
|
||
<button
|
||
onClick={() => setShowProfileMenu(!showProfileMenu)}
|
||
className="profile-button flex items-center gap-2 px-3 py-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||
>
|
||
<div className="w-8 h-8 bg-primary-500 rounded-full flex items-center justify-center">
|
||
<span className="text-white text-sm font-medium">
|
||
{user.name.split(' ').map(n => n[0]).join('')}
|
||
</span>
|
||
</div>
|
||
<span className="hidden md:block text-sm font-medium text-gray-700">{user.name}</span>
|
||
<User className="w-4 h-4 text-gray-600" />
|
||
</button>
|
||
|
||
{showProfileMenu && (
|
||
<div className="profile-menu absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
|
||
<button
|
||
onClick={() => {
|
||
navigate('/profile');
|
||
setShowProfileMenu(false);
|
||
}}
|
||
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-gray-50 text-sm text-gray-700 text-left"
|
||
>
|
||
<User className="w-4 h-4" />
|
||
My Profile
|
||
</button>
|
||
<button
|
||
onClick={() => {
|
||
navigate('/settings');
|
||
setShowProfileMenu(false);
|
||
}}
|
||
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-gray-50 text-sm text-gray-700 text-left"
|
||
>
|
||
<Settings className="w-4 h-4" />
|
||
Settings
|
||
</button>
|
||
<hr className="my-2" />
|
||
<button
|
||
onClick={handleLogout}
|
||
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-gray-50 text-sm text-red-600 text-left"
|
||
>
|
||
<LogOut className="w-4 h-4" />
|
||
Log Out
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|