car_mms/app/components/layout/Sidebar.tsx
2025-09-11 14:22:27 +03:00

257 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ReactNode, useState, useEffect } from 'react';
import { Link, useLocation } from '@remix-run/react';
import { getResponsiveClasses, defaultLayoutConfig, type LayoutConfig } from '~/lib/layout-utils';
interface SidebarProps {
isCollapsed: boolean;
onToggle: () => void;
isMobile: boolean;
isOpen: boolean;
onClose: () => void;
userAuthLevel: number;
}
interface NavigationItem {
name: string;
href: string;
icon: ReactNode;
authLevel?: number; // Minimum auth level required
}
const navigationItems: NavigationItem[] = [
{
name: 'لوحة التحكم',
href: '/dashboard',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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 2v6H8V5z" />
</svg>
),
},
{
name: 'العملاء',
href: '/customers',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
),
},
{
name: 'المركبات',
href: '/vehicles',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6 0a1 1 0 001 1h4a1 1 0 001-1m-6 0V9a1 1 0 00-1-1v0a1 1 0 00-1 1v8a1 1 0 001 1z" />
</svg>
),
},
{
name: 'زيارات الصيانة',
href: '/maintenance-visits',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
},
{
name: 'المصروفات',
href: '/expenses',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
),
authLevel: 2, // Admin and above
},
{
name: 'التقارير المالية',
href: '/financial-reports',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
authLevel: 2, // Admin and above
},
{
name: 'إدارة المستخدمين',
href: '/users',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
),
authLevel: 2, // Admin and above
},
{
name: 'إعدادات النظام',
href: '/settings',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
authLevel: 2, // Admin and above
},
];
export function Sidebar({ isCollapsed, onToggle, isMobile, isOpen, onClose, userAuthLevel }: SidebarProps) {
const location = useLocation();
// Filter navigation items based on user auth level
const filteredNavItems = navigationItems.filter(item =>
!item.authLevel || userAuthLevel <= item.authLevel
);
// Close sidebar on route change for mobile
useEffect(() => {
if (isMobile && isOpen) {
onClose();
}
}, [location.pathname, isMobile, isOpen, onClose]);
if (isMobile) {
return (
<>
{/* Mobile overlay */}
{isOpen && (
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40 transition-opacity duration-300"
onClick={onClose}
/>
)}
{/* Mobile Sidebar */}
<div className={`
fixed top-0 right-0 h-full w-64 bg-white shadow-lg z-50 transform transition-transform duration-300 ease-in-out
${isOpen ? 'translate-x-0' : 'translate-x-full'}
`} dir="rtl">
{/* Mobile Header */}
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
<div className="flex items-center">
<div className="h-8 w-8 bg-blue-600 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<h1 className="ml-3 text-lg font-semibold text-gray-900">نظام الصيانة</h1>
</div>
<button
onClick={onClose}
className="p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100"
>
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Mobile Navigation */}
<nav className="mt-5 px-2 flex-1 overflow-y-auto">
<div className="space-y-1">
{filteredNavItems.map((item) => {
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={`
group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 relative
${isActive ? 'bg-blue-100 text-blue-900' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}
`}
>
{isActive && <div className="absolute right-0 top-0 bottom-0 w-1 bg-blue-600 rounded-l-md"></div>}
<div className={`flex-shrink-0 ${isActive ? 'text-blue-600' : 'text-gray-400 group-hover:text-gray-500'}`}>
{item.icon}
</div>
<span className="ml-3">{item.name}</span>
</Link>
);
})}
</div>
</nav>
</div>
</>
);
}
// Desktop Sidebar
return (
<div className={`
fixed top-0 right-0 h-full bg-white shadow-lg z-40 transition-all duration-300 ease-in-out
${isCollapsed ? 'w-16' : 'w-64'}
`} dir="rtl">
{/* Desktop Header */}
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
<div className="flex items-center flex-1">
<div className="h-8 w-8 bg-blue-600 rounded-full flex items-center justify-center">
<svg className="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
{!isCollapsed && (
<h1 className="ml-3 text-lg font-semibold text-gray-900">نظام الصيانة</h1>
)}
</div>
<button
onClick={onToggle}
className="p-1 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<svg
className={`h-4 w-4 transform transition-transform duration-200 ${isCollapsed ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
</div>
{/* Desktop Navigation */}
<nav className="mt-5 px-2 flex-1 overflow-y-auto">
<div className="space-y-1">
{filteredNavItems.map((item) => {
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={`
group flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors duration-150 relative
${isActive ? 'bg-blue-100 text-blue-900' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}
${isCollapsed ? 'justify-center' : ''}
`}
title={isCollapsed ? item.name : undefined}
>
{isActive && <div className="absolute right-0 top-0 bottom-0 w-1 bg-blue-600 rounded-l-md"></div>}
<div className={`flex-shrink-0 ${isActive ? 'text-blue-600' : 'text-gray-400 group-hover:text-gray-500'}`}>
{item.icon}
</div>
{!isCollapsed && (
<span className="ml-3 truncate">{item.name}</span>
)}
</Link>
);
})}
</div>
</nav>
{/* Desktop Footer */}
{!isCollapsed && (
<div className="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-200">
<div className="text-xs text-gray-500 text-center">
نظام إدارة صيانة السيارات
</div>
</div>
)}
</div>
);
}