From 9769dbb5ae3027e8c9130ecfbe8f0f7230e94f12 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Sun, 12 Jul 2026 12:23:13 +0200 Subject: [PATCH] feat(collection-map): sidebar with legend, driver cards, unassigned compact (page split) --- src/app/(app)/collection-map/page.tsx | 53 ++--- src/app/(app)/collection-map/sidebar.tsx | 276 +++++++++++++++++++++++ 2 files changed, 301 insertions(+), 28 deletions(-) create mode 100644 src/app/(app)/collection-map/sidebar.tsx diff --git a/src/app/(app)/collection-map/page.tsx b/src/app/(app)/collection-map/page.tsx index bba795c..f6eee90 100644 --- a/src/app/(app)/collection-map/page.tsx +++ b/src/app/(app)/collection-map/page.tsx @@ -63,6 +63,12 @@ import { unassignedPoolPin, } from "@/components/leaflet-markers"; import { splitSupplierBucket } from "./pin-model"; +import { + DriverCardSection, + LegendCard, + MissingCoordsCard, + UnassignedCompactCard, +} from "./sidebar"; import type { OrderItem, User as UserType } from "@/lib/types"; import { cn } from "@/lib/utils"; @@ -85,7 +91,7 @@ const DARK_TILE_ATTRIBUTION = '© ({ open: false, row: null, initialDriverId: "", initialDate: "" }); + const [showAllDrivers, setShowAllDrivers] = useState(false); + /* -- data load ----------------------------------------------------------- */ const fetchRows = useCallback(async () => { setLoading(true); @@ -725,33 +733,22 @@ export default function CollectionMapPage() { - {/* Sidebar */} -
- {/* Missing-coord suppliers */} - {suppliersWithoutCoords.length > 0 && ( - - - - - Missing supplier coordinates - - - -

- {suppliersWithoutCoords.length} supplier{suppliersWithoutCoords.length === 1 ? "" : "s"} - {" "}have stops today but no coordinates. Update on /supplier-map. -

- - Open supplier map - - -
-
- )} -
+ {/* Sidebar — Legend + Driver cards + Unassigned compact + Missing coords */} + {/* TODO(driver-cards Task 5): bottom list removal is finalised below */} diff --git a/src/app/(app)/collection-map/sidebar.tsx b/src/app/(app)/collection-map/sidebar.tsx new file mode 100644 index 0000000..827227e --- /dev/null +++ b/src/app/(app)/collection-map/sidebar.tsx @@ -0,0 +1,276 @@ +"use client"; + +/** + * Collection-map sidebar inner components. + * + * Extracted from page.tsx when the page grows past the size budget. The + * page composes these in a fixed order: Legend → Driver cards → + * Unassigned → Missing coords. + * + * Types: `StopItem` is owned by page.tsx and re-exported here via type + * import (no runtime dependency). + */ +import React, { useState } from "react"; +import Link from "next/link"; +import { + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + Circle as CircleIcon, + ExternalLink, + MapPin, +} from "lucide-react"; +import { driverLabelFor } from "@/lib/geo"; +import { + aggregateDriverStatus, + type DriverStatus, +} from "./pin-model"; +import type { StopItem } from "./page"; + +function MissingCoordsCard({ items }: { items: string[] }) { + return ( + + + + + Missing supplier coordinates + + + +

+ {items.length} supplier{items.length === 1 ? "" : "s"} have stops + today but no coordinates. Update on /supplier-map. +

+ + Open supplier map + + +
+
+ ); +} + +function LegendCard() { + return ( + + + Legend + + +
+ + A + + Assigned to driver (letter = A/B/C…) +
+
+ + ✓ + + Pool, supplier confirmed ready +
+
+ + ✕ + + Pool, supplier hasn't confirmed yet +
+
+ + Stop has been collected (green anchor) +
+
+
+ ); +} + +type DriverCardProps = { + driver: { id: string; name: string; items: StopItem[] }; + driversWithItems: { id: string; name: string; items: StopItem[] }[]; + onOpenAssign: (row: StopItem) => void; +}; + +function DriverCard({ driver, driversWithItems, onOpenAssign }: DriverCardProps) { + const status = aggregateDriverStatus(driver.items); + const [expanded, setExpanded] = useState(false); + const visibleItems = expanded ? driver.items : driver.items.slice(0, 4); + const letter = driverLabelFor(driversWithItems, driver.id); + return ( +
+
+ + {letter} + + {driver.name} +
+ +
    + {visibleItems.map((item) => ( + onOpenAssign(item)} /> + ))} + {driver.items.length > 4 && ( +
  • + +
  • + )} +
+
+ ); +} + +function CountsStrip({ status }: { status: DriverStatus }) { + const parts = [`${status.total} stops`]; + if (status.collected > 0) parts.push(`${status.collected} collected`); + if (status.waiting > 0) parts.push(`${status.waiting} waiting`); + return ( +

+ {parts[0]} + {status.collected > 0 && ( + + + {status.collected} collected + + )} + {status.waiting > 0 && ( + + + {status.waiting} waiting + + )} +

+ ); +} + +function StopRow({ item, onOpen }: { item: StopItem; onOpen: () => void }) { + const pill = (() => { + if (item.item_status === "collected") { + return { + label: "Collected", + cls: "bg-emerald-100 text-emerald-900 border-emerald-300", + }; + } + if (item.ready_for_collection) { + return { + label: "Ready", + cls: "bg-blue-100 text-blue-900 border-blue-300", + }; + } + return { + label: "Waiting", + cls: "bg-amber-100 text-amber-900 border-amber-300", + }; + })(); + return ( +
  • + +
  • + ); +} + +function DriverCardSection({ + driversWithItems, + expanded, + onToggle, + onOpenAssign, +}: { + driversWithItems: DriverCardProps["driver"][]; + expanded: boolean; + onToggle: () => void; + onOpenAssign: (row: StopItem) => void; +}) { + const visible = expanded ? driversWithItems : driversWithItems.slice(0, 3); + return ( + + + + Drivers with stops + + + + {visible.length === 0 ? ( +

    No drivers have stops yet.

    + ) : ( + visible.map((d) => ( + + )) + )} + {driversWithItems.length > 3 && ( + + )} +
    +
    + ); +} + +function UnassignedCompactCard({ count }: { count: number }) { + return ( + + + + {count} unassigned stop{count === 1 ? "" : "s"} + + + Review + + + + ); +} + +export { + MissingCoordsCard, + LegendCard, + DriverCard, + CountsStrip, + StopRow, + DriverCardSection, + UnassignedCompactCard, +};