pmtool / src /components /layout /header.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
20.5 kB
import React, { useState, useRef, useEffect } from "react";
import { Bell, ChevronDown, Menu, Search, X, Download, LogIn, LogOut, Loader2 } from "lucide-react";
import { useNavigate, useLocation } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Logo from "@/components/ui/logo";
import { useIsMobile } from "@/hooks/use-mobile";
import { useAuth } from "@/lib/auth-context";
import { toast } from "@/lib/custom-toast";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useAssetRequestNotifications } from "@/hooks/useAssetRequestNotifications";
import { attendanceApi } from "@/services/attendanceApi";
import { getConfettiOriginFromElement, triggerAttendanceConfetti } from "@/lib/confetti";
import CheckOutReminderDialog from "@/components/CheckOutReminderDialog";
interface HeaderProps {
toggleSidebar: () => void;
isSidebarOpen: boolean;
}
const Header = ({ toggleSidebar, isSidebarOpen }: HeaderProps) => {
const isMobile = useIsMobile();
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [userMenuOpen, setUserMenuOpen] = useState(false);
const [downloadMenuOpen, setDownloadMenuOpen] = useState(false);
const [now, setNow] = useState(() => new Date());
const [showCheckOutReminder, setShowCheckOutReminder] = useState(false);
const [reminderDismissed, setReminderDismissed] = useState(false);
const quickAttendanceBtnRef = useRef<HTMLButtonElement>(null);
const userMenuRef = useRef<HTMLDivElement>(null);
const downloadMenuRef = useRef<HTMLDivElement>(null);
const { logout, userData, isAuthenticated } = useAuth();
const queryClient = useQueryClient();
const navigate = useNavigate();
const location = useLocation();
const { notificationCount, hasNotifications } = useAssetRequestNotifications();
// Check if we're on the download app page
const isDownloadPage = location.pathname === "/download-app";
const employeeId = userData?.employeeId;
const isClient = userData?.roleName?.toLowerCase() === "client";
const { data: todayAttendance, isLoading: todayAttendanceLoading } = useQuery({
queryKey: ["attendance-today", employeeId ?? 0],
queryFn: () => {
if (!employeeId) return Promise.resolve(null);
return attendanceApi.getToday(employeeId);
},
enabled: isAuthenticated && !isDownloadPage && !isClient && !!employeeId,
refetchInterval: 60_000,
});
const invalidateAttendance = async () => {
if (employeeId) {
await queryClient.invalidateQueries({ queryKey: ["attendance-today", employeeId] });
}
await queryClient.invalidateQueries({ queryKey: ["attendance"] });
};
const checkInMutation = useMutation({
mutationFn: async () => {
if (!employeeId) throw new Error("Employee ID not found");
return attendanceApi.checkIn(employeeId);
},
onSuccess: async () => {
toast.success("Checked in successfully");
triggerAttendanceConfetti("checkIn", getConfettiOriginFromElement(quickAttendanceBtnRef.current));
await invalidateAttendance();
},
onError: (error: any) => {
toast.error(error?.response?.data || error?.message || "Failed to check in");
},
});
const checkOutMutation = useMutation({
mutationFn: async () => {
if (!employeeId) throw new Error("Employee ID not found");
return attendanceApi.checkOut(employeeId);
},
onSuccess: async () => {
toast.success("Checked out successfully");
triggerAttendanceConfetti("checkOut", getConfettiOriginFromElement(quickAttendanceBtnRef.current));
await invalidateAttendance();
},
onError: (error: any) => {
toast.error(error?.response?.data || error?.message || "Failed to check out");
},
});
const isCheckedIn = !!todayAttendance?.inTime;
const isCheckedOut = !!todayAttendance?.outTime;
const canCheckIn = !todayAttendanceLoading && !checkInMutation.isPending && !isCheckedIn;
const canCheckOut = !todayAttendanceLoading && !checkOutMutation.isPending && isCheckedIn && !isCheckedOut;
const showQuickAttendance = isAuthenticated && !isDownloadPage && !isClient && !!employeeId;
useEffect(() => {
if (!isAuthenticated || isDownloadPage || !isCheckedIn || isCheckedOut) {
setNow(new Date());
return;
}
const intervalId = window.setInterval(() => setNow(new Date()), 1000);
return () => window.clearInterval(intervalId);
}, [isAuthenticated, isDownloadPage, isCheckedIn, isCheckedOut]);
const parseTimeToToday = (time: string) => {
const parts = time.split(":").map(p => parseInt(p, 10));
const [hh, mm, ss] = [parts[0] ?? 0, parts[1] ?? 0, parts[2] ?? 0];
const dt = new Date(now);
dt.setHours(hh, mm, ss, 0);
return dt;
};
const officeDurationText = (() => {
if (!todayAttendance?.inTime) return null;
const start = parseTimeToToday(todayAttendance.inTime);
const end = todayAttendance.outTime ? parseTimeToToday(todayAttendance.outTime) : now;
const diffMs = Math.max(0, end.getTime() - start.getTime());
const totalSeconds = Math.floor(diffMs / 1000);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const pad2 = (n: number) => n.toString().padStart(2, "0");
return `${pad2(hours)}:${pad2(minutes)}:${pad2(seconds)}`;
})();
// Handle navigation to home/dashboard
const goToHomePage = () => {
if (isAuthenticated) {
// For client role, redirect to production-bugs page instead of dashboard
if (userData?.roleName?.toLowerCase() === "client") {
navigate('/production-bugs');
} else {
// For other roles, redirect to dashboard
navigate('/');
}
} else {
// For non-authenticated users, redirect to login
navigate('/login');
}
};
// Check if it's past 6:30 PM and show checkout reminder
useEffect(() => {
if (!isAuthenticated || isDownloadPage || isClient || !isCheckedIn || isCheckedOut || reminderDismissed) {
return;
}
const currentHour = now.getHours();
const currentMinute = now.getMinutes();
// Check if it's 6:30 PM or later (18:30 in 24-hour format)
const isPast630PM = currentHour > 18 || (currentHour === 18 && currentMinute >= 30);
if (isPast630PM && !showCheckOutReminder) {
setShowCheckOutReminder(true);
}
}, [now, isAuthenticated, isDownloadPage, isClient, isCheckedIn, isCheckedOut, reminderDismissed, showCheckOutReminder]);
// Handle outside clicks to close the menus
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) {
setUserMenuOpen(false);
}
if (downloadMenuRef.current && !downloadMenuRef.current.contains(event.target as Node)) {
setDownloadMenuOpen(false);
}
};
if (userMenuOpen || downloadMenuOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside as any);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside as any);
};
}, [userMenuOpen, downloadMenuOpen]);
const handleLogout = () => {
logout();
toast.success("Logged out successfully");
navigate('/login');
};
const handleReminderClose = () => {
setShowCheckOutReminder(false);
setReminderDismissed(true);
};
const handleReminderCheckOut = async () => {
await checkOutMutation.mutateAsync();
setShowCheckOutReminder(false);
};
const toggleUserMenu = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setUserMenuOpen(prev => !prev);
};
const toggleDownloadMenu = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setDownloadMenuOpen(prev => !prev);
};
// Get initials for avatar fallback
const getInitials = () => {
if (!userData) return "U";
const first = userData.firstName?.charAt(0) || '';
const last = userData.lastName?.charAt(0) || '';
return (first + last).toUpperCase();
};
// Get display name
const getDisplayName = () => {
if (!userData) return "User";
return `${userData.firstName} ${userData.lastName}`;
};
// Generate avatar URL
const getAvatarUrl = () => {
if (!userData) return '';
return `https://ui-avatars.com/api/?name=${userData.firstName}+${userData.lastName}&background=random`;
};
// Download handlers
const handleDownloadAPK = () => {
window.open('/pm-tool.apk', '_blank');
setDownloadMenuOpen(false);
};
const handleVisitDownloadPage = () => {
navigate('/download-app');
setDownloadMenuOpen(false);
};
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-background/80 backdrop-blur-lg border-b h-16 flex items-center justify-between px-4 transition-all duration-300">
<div className="flex items-center gap-4">
{isMobile && isAuthenticated && (
<Button
variant="ghost"
size="icon"
onClick={toggleSidebar}
aria-label="Toggle sidebar"
>
{isSidebarOpen ? (
<X className="h-5 w-5" />
) : (
<Menu className="h-5 w-5" />
)}
</Button>
)}
{(!isMobile || !isSearchOpen) && (
<div
className="cursor-pointer"
onClick={goToHomePage}
title="Go to Dashboard"
>
<Logo />
</div>
)}
</div>
<div className={`flex-1 flex ${isMobile ? "justify-end" : "justify-center"} max-w-xl mx-auto`}>
{!isDownloadPage && isAuthenticated && (isSearchOpen || !isMobile) && (
<div className={`relative w-full max-w-md ${isMobile ? "absolute left-0 right-0 px-4" : ""}`}>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Search..."
className="w-full pl-9 bg-secondary/50 border-secondary-foreground/10 focus-visible:ring-primary/20"
/>
{isMobile && (
<Button
variant="ghost"
size="icon"
className="absolute right-5 top-1/2 transform -translate-y-1/2"
onClick={() => setIsSearchOpen(false)}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
)}
{isMobile && !isSearchOpen && !isDownloadPage && isAuthenticated && (
<Button
variant="ghost"
size="icon"
onClick={() => setIsSearchOpen(true)}
>
<Search className="h-5 w-5" />
</Button>
)}
</div>
<div className="flex items-center gap-3">
{/* Custom Download Menu Implementation */}
<div ref={downloadMenuRef} className="relative">
<Button
variant="ghost"
size="icon"
className="text-primary hover:text-primary/80"
onClick={toggleDownloadMenu}
>
<Download className="h-5 w-5" />
</Button>
{downloadMenuOpen && (
<div className="absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-popover border border-border z-50">
<div className="py-2 px-4 border-b">
<p className="font-medium">Download Options</p>
</div>
<div className="py-1">
<button
className="flex w-full px-4 py-2 text-sm hover:bg-accent"
onClick={handleDownloadAPK}
>
Download APK Directly
</button>
<button
className="flex w-full px-4 py-2 text-sm hover:bg-accent"
onClick={handleVisitDownloadPage}
>
Visit Download Page
</button>
</div>
</div>
)}
</div>
{isAuthenticated ? (
<>
{!isDownloadPage && !!officeDurationText && (
<div className="flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm text-muted-foreground">
<span className="text-muted-foreground">In Office</span>
<span className="font-medium text-foreground tabular-nums">{officeDurationText}</span>
</div>
)}
{showQuickAttendance && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
ref={quickAttendanceBtnRef}
variant="outline"
size={isMobile ? "icon" : "sm"}
className={isMobile ? "" : "gap-2"}
disabled={!canCheckIn && !canCheckOut}
onClick={() => {
if (canCheckIn) {
checkInMutation.mutate();
return;
}
if (canCheckOut) {
checkOutMutation.mutate();
}
}}
>
{(todayAttendanceLoading || checkInMutation.isPending || checkOutMutation.isPending) ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : canCheckIn ? (
<LogIn className="h-4 w-4" />
) : canCheckOut ? (
<LogOut className="h-4 w-4" />
) : (
<LogOut className="h-4 w-4" />
)}
{!isMobile && (
<span>
{canCheckIn ? "Check In" : canCheckOut ? "Check Out" : "Done"}
</span>
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{canCheckIn
? "Quick Check In"
: canCheckOut
? "Quick Check Out"
: "Attendance completed for today"}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="relative"
onClick={() => {
const isAdmin = userData?.roleId === 1 || userData?.roleName?.toLowerCase() === 'admin';
navigate(isAdmin ? '/admin/asset-requests' : '/asset-requests');
}}
>
<Bell className="h-5 w-5" />
{hasNotifications && (
<>
<span className="absolute top-1 right-1 w-2 h-2 bg-primary rounded-full" />
{notificationCount > 0 && (
<span className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-xs rounded-full h-5 w-5 flex items-center justify-center font-medium">
{notificationCount > 9 ? '9+' : notificationCount}
</span>
)}
</>
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>
{hasNotifications
? `${notificationCount} asset request ${notificationCount === 1 ? 'notification' : 'notifications'}`
: 'No new notifications'}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
{/* Custom User Menu Implementation */}
<div ref={userMenuRef} className="relative">
<Button
variant="ghost"
className="flex items-center gap-2"
onClick={toggleUserMenu}
>
<Avatar className="h-8 w-8">
<AvatarImage src={getAvatarUrl()} />
<AvatarFallback>{getInitials()}</AvatarFallback>
</Avatar>
{!isMobile && (
<>
<div className="flex flex-col items-start text-sm">
<span className="font-medium">{getDisplayName()}</span>
<span className="text-xs text-muted-foreground">
{userData?.roleName || "User"}
</span>
</div>
<ChevronDown className="h-4 w-4 text-muted-foreground" />
</>
)}
</Button>
{userMenuOpen && (
<div className="absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-popover border border-border z-50">
<div className="py-2 px-4 border-b">
<p className="font-medium">My Account</p>
</div>
<div className="py-1">
<button
className="flex w-full px-4 py-2 text-sm hover:bg-accent"
onClick={() => {
navigate('/profile');
setUserMenuOpen(false);
}}
>
Profile
</button>
<button
className="flex w-full px-4 py-2 text-sm hover:bg-accent"
onClick={() => {
navigate('/settings');
setUserMenuOpen(false);
}}
>
Settings
</button>
</div>
<div className="border-t py-1">
<button
className="flex w-full px-4 py-2 text-sm hover:bg-accent"
onClick={() => {
handleLogout();
setUserMenuOpen(false);
}}
>
Log out
</button>
</div>
</div>
)}
</div>
</>
) : (
<div className="flex items-center gap-3">
<Button
variant="default"
size="sm"
onClick={() => navigate('/login')}
className="font-medium"
>
Log In
</Button>
</div>
)}
</div>
{/* Check-Out Reminder Dialog */}
<CheckOutReminderDialog
isOpen={showCheckOutReminder}
onClose={handleReminderClose}
onCheckOut={handleReminderCheckOut}
isLoading={checkOutMutation.isPending}
userName={userData?.firstName || 'User'}
workDuration={officeDurationText || '00:00:00'}
/>
</header>
);
};
export default Header;