369 lines
14 KiB
TypeScript
369 lines
14 KiB
TypeScript
import { Form, Link, useLocation } from "@remix-run/react";
|
|
import type { Employee } from "@prisma/client";
|
|
import { useState, useEffect } from "react";
|
|
|
|
interface DashboardLayoutProps {
|
|
children: React.ReactNode;
|
|
user: Pick<Employee, "id" | "name" | "username" | "authLevel">;
|
|
}
|
|
|
|
export default function DashboardLayout({ children, user }: DashboardLayoutProps) {
|
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
|
|
// Initialize sidebar state from localStorage after hydration
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem('sidebar-collapsed');
|
|
if (saved) {
|
|
setSidebarCollapsed(JSON.parse(saved));
|
|
}
|
|
}, []);
|
|
|
|
const toggleSidebar = () => setSidebarOpen(!sidebarOpen);
|
|
|
|
const toggleCollapse = () => {
|
|
const newCollapsed = !sidebarCollapsed;
|
|
setSidebarCollapsed(newCollapsed);
|
|
// Persist to localStorage
|
|
localStorage.setItem('sidebar-collapsed', JSON.stringify(newCollapsed));
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Mobile sidebar overlay */}
|
|
{sidebarOpen && (
|
|
<div
|
|
className="fixed inset-0 z-40 bg-gray-600 bg-opacity-75 lg:hidden"
|
|
onClick={() => setSidebarOpen(false)}
|
|
/>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
<nav className="bg-white shadow-sm border-b border-gray-200 fixed w-full top-0 z-30">
|
|
<div className="max-w-full mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between h-16">
|
|
<div className="flex items-center">
|
|
{/* Mobile menu button */}
|
|
<button
|
|
type="button"
|
|
className="lg:hidden inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 mr-3"
|
|
onClick={toggleSidebar}
|
|
>
|
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Desktop collapse button */}
|
|
<button
|
|
type="button"
|
|
className="hidden lg:inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 mr-3"
|
|
onClick={toggleCollapse}
|
|
>
|
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
</svg>
|
|
</button>
|
|
|
|
<img
|
|
className="h-14 w-auto"
|
|
src="/logo03.png"
|
|
alt="Phosphat Report"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<div className="hidden sm:flex items-center space-x-2">
|
|
<div className="text-sm text-gray-700">
|
|
Welcome, <span className="font-medium">{user.name}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Form method="post" action="/logout">
|
|
<button
|
|
type="submit"
|
|
className="inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 shadow-sm transition duration-150 ease-in-out"
|
|
>
|
|
<svg className="h-4 w-4 sm:mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
|
</svg>
|
|
<span className="hidden sm:inline">Logout</span>
|
|
</button>
|
|
</Form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<div className="flex pt-16">
|
|
{/* Desktop Sidebar */}
|
|
<div className={`hidden lg:flex lg:flex-shrink-0 transition-all duration-300 ${sidebarCollapsed ? 'lg:w-16' : 'lg:w-64'
|
|
}`}>
|
|
<div className="flex flex-col w-full">
|
|
<div className="flex flex-col flex-grow bg-white shadow-sm border-r border-gray-200 pt-5 pb-4 overflow-y-auto">
|
|
<SidebarContent
|
|
user={user}
|
|
collapsed={sidebarCollapsed}
|
|
onItemClick={() => { }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Sidebar */}
|
|
<div className={`lg:hidden fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-lg transform transition-transform duration-300 ease-in-out ${sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
|
}`}>
|
|
<div className="flex flex-col h-full pt-16">
|
|
<div className="flex-1 flex flex-col pt-5 pb-4 overflow-y-auto">
|
|
<SidebarContent
|
|
user={user}
|
|
collapsed={false}
|
|
onItemClick={() => setSidebarOpen(false)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main content */}
|
|
<div className="flex-1 flex flex-col">
|
|
<main className="flex-1 p-4 sm:p-6 lg:p-8">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Sidebar Content Component
|
|
function SidebarContent({
|
|
user,
|
|
collapsed,
|
|
onItemClick
|
|
}: {
|
|
user: Pick<Employee, "id" | "name" | "username" | "authLevel">;
|
|
collapsed: boolean;
|
|
onItemClick: () => void;
|
|
}) {
|
|
const location = useLocation();
|
|
|
|
const isActive = (path: string) => {
|
|
if (path === '/dashboard') {
|
|
return location.pathname === '/dashboard';
|
|
}
|
|
return location.pathname.startsWith(path);
|
|
};
|
|
|
|
const NavItem = ({
|
|
to,
|
|
icon,
|
|
children,
|
|
onClick
|
|
}: {
|
|
to: string;
|
|
icon: React.ReactNode;
|
|
children: React.ReactNode;
|
|
onClick?: () => void;
|
|
}) => {
|
|
const active = isActive(to);
|
|
|
|
return (
|
|
<li>
|
|
<Link
|
|
to={to}
|
|
onClick={() => {
|
|
onClick?.();
|
|
onItemClick();
|
|
}}
|
|
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md transition-colors duration-150 ${active
|
|
? 'bg-indigo-100 text-indigo-900 border-r-2 border-indigo-500'
|
|
: 'text-gray-900 hover:bg-gray-50 hover:text-gray-900'
|
|
}`}
|
|
title={collapsed ? children?.toString() : undefined}
|
|
>
|
|
<div className={`mr-3 flex-shrink-0 h-6 w-6 ${active ? 'text-indigo-500' : 'text-gray-400 group-hover:text-gray-500'
|
|
}`}>
|
|
{icon}
|
|
</div>
|
|
{!collapsed && (
|
|
<span className="truncate">{children}</span>
|
|
)}
|
|
</Link>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
const SectionHeader = ({ children }: { children: React.ReactNode }) => (
|
|
<li className="mt-6">
|
|
{!collapsed && (
|
|
<div className="px-2 text-xs font-semibold text-gray-500 uppercase tracking-wider">
|
|
{children}
|
|
</div>
|
|
)}
|
|
{collapsed && <div className="border-t border-gray-200 mx-2"></div>}
|
|
</li>
|
|
);
|
|
|
|
return (
|
|
<nav className="mt-5 flex-1 px-2 space-y-1">
|
|
<ul className="space-y-1">
|
|
<NavItem
|
|
to="/dashboard"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6a2 2 0 01-2 2H10a2 2 0 01-2-2V5z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Dashboard
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/reports"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Shifts
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/report-sheet"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
|
</svg>
|
|
}
|
|
>
|
|
Reports
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/stoppages"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Stoppages
|
|
</NavItem>
|
|
|
|
{user.authLevel >= 2 && (
|
|
<>
|
|
<SectionHeader>Management</SectionHeader>
|
|
|
|
<NavItem
|
|
to="/areas"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Areas
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/dredger-locations"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
|
</svg>
|
|
}
|
|
>
|
|
Dredger Locations
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/reclamation-locations"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Reclamation Locations
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/equipment"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Equipment
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/foreman"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Foreman
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/workers"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Workers
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/employees"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Employees
|
|
</NavItem>
|
|
</>
|
|
)}
|
|
|
|
{user.authLevel === 3 && (
|
|
<>
|
|
<SectionHeader>Administration</SectionHeader>
|
|
|
|
<NavItem
|
|
to="/mail-settings"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Mail Settings
|
|
</NavItem>
|
|
|
|
<NavItem
|
|
to="/test-email"
|
|
icon={
|
|
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
</svg>
|
|
}
|
|
>
|
|
Test Email
|
|
</NavItem>
|
|
</>
|
|
)}
|
|
</ul>
|
|
</nav>
|
|
);
|
|
} |