phosphat-report-app/app/components/DashboardLayout.tsx

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