diff --git a/app/page.tsx b/app/page.tsx index f4e0d4e..336bb1b 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,12 +1,13 @@ 'use client'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { OrderItem } from '@/lib/types'; import { useAuth } from '@/components/AuthProvider'; import BottomNav from '@/components/BottomNav'; import LocationTracker from '@/components/LocationTracker'; import CollectionItemCard from '@/components/CollectionItemCard'; +import SupplierGroup from '@/components/SupplierGroup'; import CollectModal from '@/components/CollectModal'; import ProblemModal from '@/components/ProblemModal'; import { @@ -26,9 +27,23 @@ import { Search, X, Package, - ListFilter, + LayoutList, + Columns, } from 'lucide-react'; +const SUPPLIER_COLORS = [ + { border: 'border-l-orange-500', dot: 'bg-orange-500', light: 'bg-orange-50', text: 'text-orange-700', ring: 'ring-orange-500/30' }, + { border: 'border-l-blue-500', dot: 'bg-blue-500', light: 'bg-blue-50', text: 'text-blue-700', ring: 'ring-blue-500/30' }, + { border: 'border-l-emerald-500', dot: 'bg-emerald-500', light: 'bg-emerald-50', text: 'text-emerald-700', ring: 'ring-emerald-500/30' }, + { border: 'border-l-violet-500', dot: 'bg-violet-500', light: 'bg-violet-50', text: 'text-violet-700', ring: 'ring-violet-500/30' }, + { border: 'border-l-rose-500', dot: 'bg-rose-500', light: 'bg-rose-50', text: 'text-rose-700', ring: 'ring-rose-500/30' }, + { border: 'border-l-cyan-500', dot: 'bg-cyan-500', light: 'bg-cyan-50', text: 'text-cyan-700', ring: 'ring-cyan-500/30' }, + { border: 'border-l-amber-500', dot: 'bg-amber-500', light: 'bg-amber-50', text: 'text-amber-700', ring: 'ring-amber-500/30' }, + { border: 'border-l-pink-500', dot: 'bg-pink-500', light: 'bg-pink-50', text: 'text-pink-700', ring: 'ring-pink-500/30' }, + { border: 'border-l-teal-500', dot: 'bg-teal-500', light: 'bg-teal-50', text: 'text-teal-700', ring: 'ring-teal-500/30' }, + { border: 'border-l-indigo-500', dot: 'bg-indigo-500', light: 'bg-indigo-50', text: 'text-indigo-700', ring: 'ring-indigo-500/30' }, +]; + export default function CollectionsPage() { const router = useRouter(); const { isLoggedIn, user, isLoading: authLoading } = useAuth(); @@ -42,6 +57,10 @@ export default function CollectionsPage() { const [showCollectModal, setShowCollectModal] = useState(false); const [showProblemModal, setShowProblemModal] = useState(false); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null); + const [viewMode, setViewMode] = useState<'compact' | 'detailed'>('compact'); + + // Track which supplier groups are expanded + const [expandedSuppliers, setExpandedSuppliers] = useState>(new Set()); useEffect(() => { if (!authLoading && !isLoggedIn) router.push('/login/'); @@ -51,11 +70,16 @@ export default function CollectionsPage() { if (!isAuthenticated()) return; if (showLoading) setIsLoading(true); setError(''); - try { const data = await getAssignedCollections(); setItems(data); setFilteredItems(data); + // Auto-expand all suppliers on fresh load + const allIds = new Set(); + data.forEach(i => { + if (i.expand?.supplier?.id) allIds.add(i.expand.supplier.id); + }); + setExpandedSuppliers(allIds); } catch (err) { console.error('Failed to load collections:', err); setError('Failed to load collections. Pull down to retry.'); @@ -69,7 +93,6 @@ export default function CollectionsPage() { useEffect(() => { let filtered = items; - if (searchQuery) { const query = searchQuery.toLowerCase(); filtered = filtered.filter((i) => { @@ -83,7 +106,6 @@ export default function CollectionsPage() { ); }); } - setFilteredItems(filtered); }, [items, searchQuery]); @@ -108,7 +130,6 @@ export default function CollectionsPage() { setShowCollectModal(false); showToast('Collection completed successfully'); } catch (err) { - console.error('Failed to collect:', err); showToast('Failed to update collection', 'error'); } }; @@ -120,7 +141,6 @@ export default function CollectionsPage() { setShowProblemModal(false); showToast('Problem reported successfully', 'warning'); } catch (err) { - console.error('Failed to report problem:', err); showToast('Failed to report problem', 'error'); } }; @@ -133,21 +153,40 @@ export default function CollectionsPage() { window.open(url, '_blank'); }; - // Group by supplier - const groupedBySupplier = filteredItems.reduce((acc, item) => { - const supplier = item.expand?.supplier; - if (!supplier) return acc; - const key = supplier.id; - if (!acc[key]) { - acc[key] = { supplier, items: [] }; - } - acc[key].items.push(item); - return acc; - }, {} as Record['supplier']; items: OrderItem[] }>); + const toggleSupplier = (supplierId: string) => { + setExpandedSuppliers(prev => { + const next = new Set(prev); + if (next.has(supplierId)) next.delete(supplierId); + else next.add(supplierId); + return next; + }); + }; - const groupedArray = Object.values(groupedBySupplier).sort((a, b) => - (a.supplier?.suppliername || '').localeCompare(b.supplier?.suppliername || '') - ); + const expandAll = () => { + const allIds = new Set(); + filteredItems.forEach(i => { + if (i.expand?.supplier?.id) allIds.add(i.expand.supplier.id); + }); + setExpandedSuppliers(allIds); + }; + + const collapseAll = () => setExpandedSuppliers(new Set()); + + // Group by supplier + const groupedArray = useMemo(() => { + const grouped = filteredItems.reduce((acc, item) => { + const supplier = item.expand?.supplier; + if (!supplier) return acc; + const key = supplier.id; + if (!acc[key]) acc[key] = { supplier, items: [] }; + acc[key].items.push(item); + return acc; + }, {} as Record['supplier']; items: OrderItem[] }>); + + return Object.values(grouped).sort((a, b) => + (a.supplier?.suppliername || '').localeCompare(b.supplier?.suppliername || '') + ); + }, [filteredItems]); if (authLoading) { return ( @@ -196,7 +235,7 @@ export default function CollectionsPage() { {/* Search bar */} -
+
- {/* Stats pill */} -
- - - {items.length} item{items.length !== 1 ? 's' : ''} - - {groupedArray.length > 0 && ( + {/* Stats + Controls bar */} +
+
- - {groupedArray.length} stop{groupedArray.length !== 1 ? 's' : ''} + + {items.length} item{items.length !== 1 ? 's' : ''} - )} + {groupedArray.length > 0 && ( + + + {groupedArray.length} stop{groupedArray.length !== 1 ? 's' : ''} + + )} +
+ + {/* View toggle */} +
+ + +
@@ -236,8 +299,8 @@ export default function CollectionsPage() {
{isLoading ? (
- {[...Array(4)].map((_, i) => ( -
+ {[...Array(3)].map((_, i) => ( +
@@ -270,39 +333,26 @@ export default function CollectionsPage() {

) : ( -
- {groupedArray.map(({ supplier, items: supplierItems }, groupIdx) => ( -
- {/* Supplier header */} -
-
- -
-
-

{supplier!.suppliername}

- {supplier!.supplieraddress && ( -

{supplier!.supplieraddress}

- )} -
- - {supplierItems.length} - -
+
+ {groupedArray.map(({ supplier, items: supplierItems }, groupIdx) => { + const color = SUPPLIER_COLORS[groupIdx % SUPPLIER_COLORS.length]; + const isExpanded = expandedSuppliers.has(supplier!.id); - {/* Items */} -
- {supplierItems.map((item) => ( - { setSelectedItem(i); setShowCollectModal(true); }} - onProblem={(i) => { setSelectedItem(i); setShowProblemModal(true); }} - onNavigate={handleNavigate} - /> - ))} -
-
- ))} + return ( + toggleSupplier(supplier!.id)} + onCollect={(item) => { setSelectedItem(item); setShowCollectModal(true); }} + onProblem={(item) => { setSelectedItem(item); setShowProblemModal(true); }} + onNavigate={handleNavigate} + /> + ); + })}
)}
@@ -314,7 +364,6 @@ export default function CollectionsPage() { onClose={() => { setShowCollectModal(false); setSelectedItem(null); }} onConfirm={handleCollect} /> -
void; onProblem: (item: OrderItem) => void; onNavigate: (lat: number, lng: number, name: string) => void; @@ -85,6 +87,8 @@ function getItemType(item: OrderItem): { export default function CollectionItemCard({ item, + supplierColor, + compact, onCollect, onProblem, onNavigate, @@ -97,13 +101,16 @@ export default function CollectionItemCard({ const hasCoords = coords !== null; const description = item.description || 'Unknown item'; const expectedQty = item.order_qty || item.quantity || 0; + const colorAccent = supplierColor?.border || 'border-l-orange-400'; return (
{/* Header */} diff --git a/components/SupplierGroup.tsx b/components/SupplierGroup.tsx new file mode 100644 index 0000000..1a7fd9e --- /dev/null +++ b/components/SupplierGroup.tsx @@ -0,0 +1,210 @@ +'use client'; + +import { OrderItem, Supplier } from '@/lib/types'; +import { cn, formatCurrency, parseCoordinates } from '@/lib/utils'; +import CollectionItemCard from '@/components/CollectionItemCard'; +import { + ChevronDown, + ChevronUp, + MapPin, + Navigation, + Package, + ShoppingCart, + Hammer, + Wrench, + AlertTriangle, +} from 'lucide-react'; + +interface SupplierColor { + border: string; + dot: string; + light: string; + text: string; + ring: string; +} + +interface SupplierGroupProps { + supplier: Supplier; + items: OrderItem[]; + color: SupplierColor; + isExpanded: boolean; + viewMode: 'compact' | 'detailed'; + onToggle: () => void; + onCollect: (item: OrderItem) => void; + onProblem: (item: OrderItem) => void; + onNavigate: (lat: number, lng: number, name: string) => void; +} + +function getItemIcon(item: OrderItem) { + if (item.item_status === 'problem' || item.problem_flag) return ; + const orderNumber = item.expand?.order?.order_number || ''; + if (orderNumber.toLowerCase().includes('breakdown')) return ; + if (orderNumber.toLowerCase().includes('repair')) return ; + if (item.is_stock_item) return ; + return ; +} + +export default function SupplierGroup({ + supplier, + items, + color, + isExpanded, + viewMode, + onToggle, + onCollect, + onProblem, + onNavigate, +}: SupplierGroupProps) { + const coords = parseCoordinates(supplier.coordinates); + const hasCoords = coords !== null; + const totalQty = items.reduce((sum, i) => sum + (i.order_qty || i.quantity || 0), 0); + + return ( +
+ {/* Supplier Header — always visible, tap to expand/collapse */} + + + {/* Expanded content */} + {isExpanded && ( +
+ {viewMode === 'compact' ? ( + /* COMPACT MODE: simple list with item name + qty */ +
+ {items.map((item) => { + const expectedQty = item.order_qty || item.quantity || 0; + return ( +
{ + // Determine if we should open collect or problem modal + // For now, just expand - user can use the action buttons + }} + > +
+ {getItemIcon(item)} + + {item.description || 'Unknown item'} + + {item.item_status === 'problem' && ( + + Problem + + )} +
+
+ {expectedQty}× +
+ + +
+
+
+ ); + })} +
+ ) : ( + /* DETAILED MODE: full item cards */ +
+ {items.map((item) => ( + + ))} +
+ )} + + {/* Supplier footer with navigate button */} + {hasCoords && coords && ( +
+ +
+ )} +
+ )} +
+ ); +} \ No newline at end of file