Axion/src/components/Layout/TopBar.tsx
2025-12-07 19:37:52 -04:00

262 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
};