MIF_E31222379_WEB/app/components/layoutpengelola/sidebar.tsx

533 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useMemo } from "react";
import { Link, useLocation } from "@remix-run/react";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { ScrollArea } from "~/components/ui/scroll-area";
import { Separator } from "~/components/ui/separator";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from "~/components/ui/collapsible";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "~/components/ui/tooltip";
import {
LayoutDashboard,
Recycle,
Users,
TrendingUp,
FileText,
CreditCard,
BarChart3,
Settings,
MessageCircle,
MapPin,
Package,
DollarSign,
UserCheck,
Building2,
Newspaper,
HelpCircle,
Bell,
Shield,
ChevronDown,
X,
Truck,
Calendar,
ClipboardList,
Target,
Timer,
AlertCircle,
CheckCircle,
Route,
Phone,
Search
} from "lucide-react";
import { cn } from "~/lib/utils";
interface SidebarProps {
isOpen: boolean;
isCollapsed: boolean;
isHovered: boolean;
isMobile: boolean;
onClose: () => void;
onMouseEnter: () => void;
onMouseLeave: () => void;
}
interface MenuItem {
title: string;
icon?: React.ReactNode;
href?: string;
badge?: string;
badgeVariant?: "pro" | "new" | "urgent";
children?: MenuItem[];
}
const operationalMenuItems: MenuItem[] = [
{
title: "Dashboard",
icon: <LayoutDashboard className="w-5 h-5" />,
children: [
{
title: "Overview Harian",
href: "/pengelola/dashboard"
},
{
title: "Monitoring Real-time",
href: "/pengelola/dashboard/monitoring",
badge: "new"
},
{ title: "Target vs Pencapaian", href: "/pengelola/dashboard/targets" },
{ title: "Jadwal Hari Ini", href: "/pengelola/dashboard/schedule" }
]
},
{
title: "Operasional Sampah",
icon: <Recycle className="w-5 h-5" />,
children: [
{
title: "Pengumpulan Harian",
href: "/pengelola/dashboard/collection",
badge: "urgent"
},
{ title: "Sortir & Klasifikasi", href: "/pengelola/dashboard/sorting" },
{ title: "Stok Sampah", href: "/pengelola/dashboard/inventory" },
{ title: "Kualitas Check", href: "/pengelola/dashboard/quality" },
{
title: "Input Data Berat",
href: "/pengelola/dashboard/weight-input",
badge: "new"
}
]
},
{
title: "Explore",
icon: <Search className="w-5 h-5" />,
href: "/pengelola/dashboard/explorewaste"
},
{
title: "Manajemen Pengepul",
icon: <Users className="w-5 h-5" />,
children: [
{ title: "Daftar Pengepul Aktif", href: "/pengelola/collectors/active" },
{ title: "Jadwal Pickup", href: "/pengelola/collectors/schedule" },
{ title: "Performa Pengepul", href: "/pengelola/collectors/performance" },
{
title: "Pengepul Pending",
href: "/pengelola/collectors/pending",
badge: "urgent"
},
{ title: "Rating & Feedback", href: "/pengelola/collectors/feedback" }
]
},
{
title: "Transaksi & Pembayaran",
icon: <CreditCard className="w-5 h-5" />,
children: [
{
title: "Transaksi Hari Ini",
href: "/pengelola/dashboard/transactions"
}
]
}
];
const fieldMenuItems: MenuItem[] = [
{
title: "Komunikasi",
icon: <MessageCircle className="w-5 h-5" />,
children: [
{ title: "Chat Pengepul", href: "/pengelola/dashboard/chat" },
{
title: "Broadcast Message",
href: "/pengelola/dashboard/broadcast"
}
]
}
];
const reportingMenuItems: MenuItem[] = [
{
title: "Pengaturan",
icon: <Settings className="w-5 h-5" />,
children: [
{
title: "Profil Pengelola",
href: "/pengelola/dashboard/pengaturan"
}
]
}
];
function MenuSection({
title,
items,
isCollapsed,
isHovered
}: {
title: string;
items: MenuItem[];
isCollapsed: boolean;
isHovered: boolean;
}) {
const showText = !isCollapsed || isHovered;
return (
<div className="space-y-2">
{showText && (
<h3 className="mb-4 px-3 text-xs uppercase text-gray-500 dark:text-gray-300 font-semibold tracking-wider">
{title}
</h3>
)}
<ul className="space-y-1">
{items.map((item, index) => (
<MenuItemComponent
key={index}
item={item}
isCollapsed={isCollapsed}
isHovered={isHovered}
/>
))}
</ul>
</div>
);
}
function MenuItemComponent({
item,
isCollapsed,
isHovered
}: {
item: MenuItem;
isCollapsed: boolean;
isHovered: boolean;
}) {
const location = useLocation();
const pathname = location.pathname;
const [isOpen, setIsOpen] = useState(false);
const hasChildren = item.children && item.children.length > 0;
const showText = !isCollapsed || isHovered;
// Check if any child is active
const isChildActive = useMemo(() => {
if (!hasChildren) return false;
return item.children!.some((child) => child.href === pathname);
}, [hasChildren, item.children, pathname]);
// Auto open if child is active
useMemo(() => {
if (isChildActive && !isOpen) {
setIsOpen(true);
}
}, [isChildActive, isOpen]);
// For collapsed state without hover, show tooltip
if (isCollapsed && !isHovered) {
return (
<li>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
className={cn(
"w-full h-12 p-0 justify-center transition-colors",
isChildActive && "bg-blue-100 dark:bg-blue-900/30"
)}
asChild={!hasChildren}
>
{hasChildren ? (
<div
className={cn(
"text-gray-700 dark:text-gray-100",
isChildActive && "text-blue-700 dark:text-blue-400"
)}
>
{item.icon}
</div>
) : (
<Link to={item.href || "#"}>
<div
className={cn(
"text-gray-700 dark:text-gray-100",
pathname === item.href &&
"text-blue-700 dark:text-blue-400"
)}
>
{item.icon}
</div>
</Link>
)}
</Button>
</TooltipTrigger>
<TooltipContent side="right" className="ml-2">
<p>{item.title}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</li>
);
}
if (hasChildren) {
return (
<li>
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
className={cn(
"w-full justify-between px-3 py-2.5 h-auto transition-colors",
"hover:bg-gray-100 dark:hover:bg-gray-700",
isChildActive &&
"bg-blue-50 text-blue-700 hover:bg-blue-100 dark:bg-blue-900/20 dark:text-blue-400 dark:hover:bg-blue-900/30"
)}
>
<div className="flex items-center gap-3">
<span
className={cn(
"transition-colors",
isChildActive
? "text-blue-600 dark:text-blue-400"
: "text-gray-700 dark:text-gray-100"
)}
>
{item.icon}
</span>
{showText && (
<span
className={cn(
"text-sm font-medium text-gray-900 dark:text-gray-100",
isChildActive && "text-blue-700 dark:text-blue-400"
)}
>
{item.title}
</span>
)}
</div>
{showText && (
<ChevronDown
className={cn(
"h-4 w-4 transition-transform",
isOpen && "rotate-180",
isChildActive && "text-blue-600 dark:text-blue-400"
)}
/>
)}
</Button>
</CollapsibleTrigger>
{showText && (
<CollapsibleContent className="ml-8 mt-1 space-y-1">
{item.children?.map((child, childIndex) => {
const isActive = pathname === child.href;
return (
<Link
key={childIndex}
to={child.href || "#"}
className={cn(
"flex items-center justify-between px-3 py-2 text-sm rounded-md transition-colors",
"hover:bg-gray-100 dark:hover:bg-gray-700",
isActive
? "bg-blue-100 text-blue-700 font-medium hover:bg-blue-200 dark:bg-blue-900/40 dark:text-blue-400 dark:hover:bg-blue-900/50"
: "text-gray-600 dark:text-gray-200"
)}
>
<span className="flex items-center gap-2">
{isActive && (
<div className="w-1 h-4 bg-blue-600 rounded-full" />
)}
{child.title}
</span>
{child.badge && (
<Badge
variant={
child.badge === "urgent"
? "destructive"
: child.badge === "pro"
? "secondary"
: "default"
}
className={cn(
"text-xs",
child.badge === "urgent" &&
"bg-red-100 text-red-700 hover:bg-red-200",
child.badge === "new" &&
"bg-green-100 text-green-700 hover:bg-green-200"
)}
>
{child.badge}
</Badge>
)}
</Link>
);
})}
</CollapsibleContent>
)}
</Collapsible>
</li>
);
}
// Single menu item (no children)
const isActive = pathname === item.href;
return (
<li>
<Button
asChild
variant="ghost"
className={cn(
"w-full justify-start px-3 py-2.5 h-auto transition-colors",
"hover:bg-gray-100 dark:hover:bg-gray-700",
isActive &&
"bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/40 dark:text-blue-400 dark:hover:bg-blue-900/50"
)}
>
<Link to={item.href || "#"}>
<div className="flex items-center gap-3">
<span
className={cn(
"transition-colors",
isActive
? "text-blue-600 dark:text-blue-400"
: "text-gray-700 dark:text-gray-100"
)}
>
{item.icon}
</span>
{showText && (
<span
className={cn(
"text-sm font-medium text-gray-900 dark:text-gray-100",
isActive && "text-blue-700 dark:text-blue-400"
)}
>
{item.title}
</span>
)}
</div>
</Link>
</Button>
</li>
);
}
export function PengelolaSidebar({
isOpen,
isCollapsed,
isHovered,
isMobile,
onClose,
onMouseEnter,
onMouseLeave
}: SidebarProps) {
const sidebarWidth = isCollapsed && !isHovered ? "w-20" : "w-72";
const showText = !isCollapsed || isHovered;
return (
<aside
className={cn(
"fixed top-0 left-0 z-50 h-full bg-white dark:bg-gray-800",
"border-r border-gray-200 dark:border-gray-700",
"transition-all duration-300 ease-in-out",
isMobile
? cn("w-72", isOpen ? "translate-x-0" : "-translate-x-full")
: cn(sidebarWidth, "translate-x-0")
)}
onMouseEnter={!isMobile ? onMouseEnter : undefined}
onMouseLeave={!isMobile ? onMouseLeave : undefined}
>
{/* Logo */}
<div className="h-16 flex items-center justify-between px-4 border-b border-gray-200 dark:border-gray-700">
{showText && (
<Link to="/pengelola/dashboard" className="flex items-center gap-2">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white text-sm font-bold">
</div>
<div className="text-lg font-bold text-gray-900 dark:text-white">
RIjig Pengelola
</div>
</Link>
)}
{!showText && (
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white text-sm font-bold mx-auto">
</div>
)}
{isMobile && (
<Button
variant="ghost"
size="icon"
className="lg:hidden"
onClick={onClose}
>
<X className="h-5 w-5" />
</Button>
)}
</div>
{/* Navigation */}
<ScrollArea className="flex-1 h-[calc(100vh-64px)]">
<div className="py-6 px-3 space-y-6">
<MenuSection
title="Operasional Harian"
items={operationalMenuItems}
isCollapsed={isCollapsed}
isHovered={isHovered}
/>
<MenuSection
title="Field Operations"
items={fieldMenuItems}
isCollapsed={isCollapsed}
isHovered={isHovered}
/>
<MenuSection
title="Reporting & Settings"
items={reportingMenuItems}
isCollapsed={isCollapsed}
isHovered={isHovered}
/>
</div>
{/* Footer CTA - Only show when not collapsed or when hovered */}
{showText && (
<div className="p-4 mx-3 mb-6 rounded-2xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700">
<div className="flex items-center gap-2 mb-2">
<Truck className="w-4 h-4 text-blue-600 dark:text-blue-400" />
<h4 className="font-semibold text-blue-900 dark:text-blue-100 text-sm">
Pengelola Dashboard
</h4>
</div>
<p className="mb-4 text-blue-700 dark:text-blue-200 text-xs">
Kelola operasional harian pengumpulan dan pengolahan sampah secara
efisien.
</p>
<div className="flex gap-2">
<Button
size="sm"
className="flex-1 bg-blue-600 hover:bg-blue-700 text-white text-xs"
>
Emergency Call
</Button>
<Button
size="sm"
variant="outline"
className="flex-1 text-xs border-blue-600 text-blue-600 hover:bg-blue-50 dark:border-blue-500 dark:text-blue-300 dark:hover:bg-blue-900/20"
>
Quick Report
</Button>
</div>
</div>
)}
</ScrollArea>
</aside>
);
}