"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 && (
)}
);
}
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 (
);
}
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.
)}
);
}