"use client"; import React, { useEffect, useState } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; import { LayoutDashboard, Users, Clock, FileSpreadsheet, Settings, Monitor, LogOut, Menu, X, ChevronRight, Scan, History, Sun, Moon, Building2, MessageSquare, User, TrendingUp, Calendar, FileText, Shield, Bell, BellRing, HelpCircle } from "lucide-react"; import { getAccessToken, getUserProfile, clearTokens, fetchApi } from "@/app/utils/api"; import CommandPalette from "@/components/CommandPalette"; function NavLink({ item, isActive, isCollapsed, onClick }: { item: { name: string; href: string; icon: any }, isActive: boolean, isCollapsed: boolean, onClick?: () => void }) { const Icon = item.icon; return (
{!isCollapsed && (

{item.name}

)} ); } export default function SidebarLayout({ children }: { children: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); const [sidebarOpen, setSidebarOpen] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false); const [user, setUser] = useState(null); const [authorized, setAuthorized] = useState(false); const [notifications, setNotifications] = useState([]); const [showNotifications, setShowNotifications] = useState(false); const [showExitConfirm, setShowExitConfirm] = useState(false); const [appClosed, setAppClosed] = useState(false); useEffect(() => { if (typeof window === "undefined" || pathname !== "/dashboard") return; // Push dummy state to capture back button window.history.pushState(null, "", window.location.href); const handlePopState = () => { // Re-push to prevent browser from leaving window.history.pushState(null, "", window.location.href); setShowExitConfirm(true); }; window.addEventListener("popstate", handlePopState); return () => { window.removeEventListener("popstate", handlePopState); }; }, [pathname]); function playNotificationSound() { if (typeof window !== "undefined") { try { const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext; const audioCtx = new AudioContextClass(); if (audioCtx.state === "suspended") audioCtx.resume(); const now = audioCtx.currentTime; // Double chime const osc1 = audioCtx.createOscillator(); const gain1 = audioCtx.createGain(); osc1.type = "sine"; osc1.frequency.setValueAtTime(587.33, now); // D5 gain1.gain.setValueAtTime(0, now); gain1.gain.linearRampToValueAtTime(0.15, now + 0.05); gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.3); osc1.connect(gain1); gain1.connect(audioCtx.destination); osc1.start(now); osc1.stop(now + 0.35); const osc2 = audioCtx.createOscillator(); const gain2 = audioCtx.createGain(); osc2.type = "sine"; osc2.frequency.setValueAtTime(880, now + 0.1); // A5 gain2.gain.setValueAtTime(0, now + 0.1); gain2.gain.linearRampToValueAtTime(0.15, now + 0.15); gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.45); osc2.connect(gain2); gain2.connect(audioCtx.destination); osc2.start(now + 0.1); osc2.stop(now + 0.5); } catch (e) { console.error("Audio Context playback failed", e); } } } const fetchNotifications = async () => { try { const res = await fetchApi("/notifications"); if (Array.isArray(res)) { setNotifications(prev => { // Play sound ONLY if there is a NEW unread notification compared to what we currently have const hasNewUnread = res.some(newN => !newN.is_read && !prev.some(oldN => oldN.id === newN.id)); if (hasNewUnread && prev.length > 0) { playNotificationSound(); } return res; }); } } catch (e) { console.error("Failed to fetch notifications", e); } }; // Helper to convert base64 VAPID key to Uint8Array function urlB64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/\-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } const registerPushSubscription = async () => { if (!('serviceWorker' in navigator) || !('PushManager' in window)) { console.warn("Push notifications are not supported in this browser."); return; } try { const res = await fetchApi("/push/vapid-public-key"); if (!res || !res.publicKey) return; const registration = await navigator.serviceWorker.ready; const subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlB64ToUint8Array(res.publicKey) }); await fetchApi("/push/subscribe", { method: "POST", body: JSON.stringify({ subscription }) }); console.info("PWA push subscription registered successfully."); } catch (err) { console.error("Failed to register PWA push subscription:", err); } }; // Register Service Worker for PWA useEffect(() => { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') .then((reg) => { console.info('Service Worker registered scope:', reg.scope); }) .catch((err) => { console.error('Service worker registration failed:', err); }); } }, []); // Request location permission on startup/login const requestLocationPermission = () => { if (typeof window !== "undefined" && 'geolocation' in navigator) { navigator.geolocation.getCurrentPosition( (pos) => { console.info("Location permission granted on startup:", pos.coords.latitude, pos.coords.longitude); }, (err) => { console.warn("Location permission denied on startup:", err.message); }, { enableHighAccuracy: true, timeout: 5000, maximumAge: 0 } ); } }; useEffect(() => { if (authorized) { fetchNotifications(); const interval = setInterval(fetchNotifications, 10000); // Poll every 10 seconds // Request notification permission and register subscription if (typeof window !== "undefined" && 'Notification' in window) { if (Notification.permission === 'default') { Notification.requestPermission().then(permission => { if (permission === 'granted') { registerPushSubscription(); } }); } else if (Notification.permission === 'granted') { registerPushSubscription(); } } // Request location permission requestLocationPermission(); return () => clearInterval(interval); } }, [authorized]); const markAsRead = async (id: number) => { try { await fetchApi(`/notifications/${id}/read`, { method: "PUT" }); fetchNotifications(); } catch (e) { console.error(e); } }; const markAllAsRead = async () => { try { const unread = notifications.filter(n => !n.is_read); await Promise.all(unread.map(n => fetchApi(`/notifications/${n.id}/read`, { method: "PUT" }))); fetchNotifications(); } catch (e) { console.error(e); } }; const [theme, setTheme] = useState<"light" | "dark">("light"); const [currentTime, setCurrentTime] = useState(""); const [currentDate, setCurrentDate] = useState(""); useEffect(() => { const updateTime = () => { const now = new Date(); setCurrentTime(now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })); setCurrentDate(now.toLocaleDateString([], { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })); }; updateTime(); const interval = setInterval(updateTime, 1000); return () => clearInterval(interval); }, []); // Load theme and sidebar state from localStorage on client side useEffect(() => { const saved = localStorage.getItem("sidebar_collapsed"); if (saved === "true") { setIsCollapsed(true); } const savedTheme = localStorage.getItem("theme"); const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; const initialTheme = (savedTheme as "light" | "dark") || (systemPrefersDark ? "dark" : "light"); setTheme(initialTheme); if (initialTheme === "dark") { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } }, []); const toggleTheme = () => { const nextTheme = theme === "dark" ? "light" : "dark"; setTheme(nextTheme); localStorage.setItem("theme", nextTheme); if (nextTheme === "dark") { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } }; const toggleCollapse = () => { const nextVal = !isCollapsed; setIsCollapsed(nextVal); localStorage.setItem("sidebar_collapsed", String(nextVal)); }; useEffect(() => { const token = getAccessToken(); const profile = getUserProfile(); if (!token || !profile) { clearTokens(); router.push("/"); } else { setUser(profile); setAuthorized(true); // Auto-redirect employees to dashboard if they attempt to access any admin views if (profile?.role?.name === "Employee" && pathname !== "/dashboard" && pathname !== "/tickets" && pathname !== "/profile" && pathname !== "/calendar") { router.push("/dashboard"); } else if (profile?.role?.name !== "Super Admin" && (pathname === "/tenants" || pathname === "/users" || pathname === "/analytics")) { router.push("/dashboard"); } } }, [router, pathname]); const [currentQuery, setCurrentQuery] = useState(""); useEffect(() => { if (typeof window !== "undefined") { const handleUpdate = () => { setCurrentQuery(window.location.search); }; handleUpdate(); const interval = setInterval(handleUpdate, 200); return () => clearInterval(interval); } }, []); const isLinkActive = (href: string) => { if (href.includes("?")) { const [linkPath, linkSearch] = href.split("?"); if (pathname !== linkPath) return false; const linkParams = new URLSearchParams(linkSearch); const currentParams = new URLSearchParams(currentQuery); return linkParams.get("tab") === currentParams.get("tab"); } else { if (href === "/dashboard") { const currentParams = new URLSearchParams(currentQuery); if (currentParams.has("tab")) return false; return pathname === "/dashboard"; } return pathname === href || pathname.startsWith(href + "/"); } }; if (!authorized) { return (

Authenticating...

); } const handleLogout = () => { clearTokens(); router.push("/"); }; const initials = user?.email ? user.email[0].toUpperCase() : "A"; const isEmployee = user?.role?.name === "Employee"; const isSuperAdmin = user?.role?.name === "Super Admin"; const getVisibleNavItems = () => { const role = user?.role?.name; if (role === "Super Admin") { return [ { name: "Dashboard", href: "/dashboard", icon: LayoutDashboard }, { name: "Organizations", href: "/tenants", icon: Building2 }, { name: "Analytics", href: "/analytics", icon: TrendingUp }, { name: "Helpdesk", href: "/tickets", icon: MessageSquare }, { name: "Audit Logs", href: "/audit", icon: History }, { name: "Settings", href: "/settings", icon: Settings }, { name: "Kiosk Mode", href: "/kiosk", icon: Scan }, ]; } else if (role === "Admin") { return [ { name: "Dashboard", href: "/dashboard", icon: LayoutDashboard }, { name: "Attendance", href: "/attendance", icon: Clock }, { name: "Employees", href: "/employees", icon: Users }, { name: "Leave", href: "/leaves", icon: Calendar }, { name: "Helpdesk", href: "/tickets", icon: MessageSquare }, { name: "Reports", href: "/reports", icon: FileSpreadsheet }, { name: "Settings", href: "/settings", icon: Settings }, { name: "Kiosk Mode", href: "/kiosk", icon: Scan }, ]; } else if (role === "HR") { return [ { name: "Dashboard", href: "/dashboard", icon: LayoutDashboard }, { name: "Attendance", href: "/attendance", icon: Clock }, { name: "Employees", href: "/employees", icon: Users }, { name: "Leave", href: "/leaves", icon: Calendar }, { name: "Helpdesk", href: "/tickets", icon: MessageSquare }, { name: "Reports", href: "/reports", icon: FileSpreadsheet }, { name: "Kiosk Mode", href: "/kiosk", icon: Scan }, ]; } else { return [ { name: "Dashboard", href: "/dashboard", icon: LayoutDashboard }, { name: "Attendance", href: "/dashboard?tab=attendance", icon: Clock }, { name: "Leave", href: "/dashboard?tab=leave", icon: Calendar }, { name: "Calendar", href: "/calendar", icon: Calendar }, { name: "Contact HR", href: "/tickets", icon: MessageSquare }, ]; } }; const visibleNavItems = getVisibleNavItems(); return (
{/* Ambient background */}
{/* ─── Desktop Sidebar ─── */} {/* ─── Mobile Top Bar ─── */}
NetraID {currentTime && ( {currentTime.split(" ")[0]} {currentTime.split(" ")[1] || ""} )}
{/* Notification Bell with Dropdown (Mobile) */}
{showNotifications && ( <>
setShowNotifications(false)} />
Notifications {notifications.filter(n => !n.is_read).length > 0 && ( )}
{notifications.length === 0 ? (

No new notifications

) : ( notifications.map((n) => (
{ if (!n.is_read) markAsRead(n.id); }} className={`p-2.5 rounded-xl border text-[11px] transition-all cursor-pointer text-left ${ n.is_read ? "bg-[var(--bg-surface)] border-[var(--border-subtle)] text-[var(--text-muted)]" : "bg-[var(--border-subtle)]/30 border-cyan-500/20 text-[var(--text-primary)] hover:bg-[var(--border-subtle)]/50" }`} >
{n.title} {n.priority}

{n.message}

{new Date(n.created_at).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}
)) )}
)}
{/* Theme Toggle Mobile */}
{/* ─── Mobile Drawer ─── */}
setSidebarOpen(false)} >
{/* ─── Main Content ─── */}
{/* Desktop Top Navbar */}
{currentDate} {currentTime && ( <> | {currentTime} )}
{/* Notification Bell with Dropdown (Desktop) */}
{showNotifications && ( <>
setShowNotifications(false)} />
Notifications {notifications.filter(n => !n.is_read).length > 0 && ( )}
{notifications.length === 0 ? (

No new notifications

) : ( notifications.map((n) => (
{ if (!n.is_read) markAsRead(n.id); }} className={`p-2.5 rounded-xl border text-[11px] transition-all cursor-pointer text-left ${ n.is_read ? "bg-[var(--bg-surface)] border-[var(--border-subtle)] text-[var(--text-muted)]" : "bg-[var(--border-subtle)]/30 border-cyan-500/20 text-[var(--text-primary)] hover:bg-[var(--border-subtle)]/50" }`} >
{n.title} {n.priority}

{n.message}

{new Date(n.created_at).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" })}
)) )}
)}
{children}
{/* Exit App Confirmation Modal */} {showExitConfirm && (

Exit Application?

Are you sure you want to close the NetraID application?

)} {/* Branded Goodbye Overlay Fallback */} {appClosed && (

NetraID Closed

The session has been terminated safely. You can now close this browser tab or swipe away the application window.

)}
); }