Introduce a new SupplierGroup component and group collections by supplier. Add supplier color palette, per-supplier expand/collapse state, compact/detailed view modes, and controls to toggle/expand/collapse groups. Pass supplierColor and compact props to CollectionItemCard to adjust visuals, use useMemo for grouping, and auto-expand suppliers on initial load. Minor UI tweaks: adjust skeleton count/spacing, update stats/controls bar, improve toast styling, and remove some console logs.
210 lines
No EOL
8 KiB
TypeScript
210 lines
No EOL
8 KiB
TypeScript
'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 <AlertTriangle size={12} className="text-red-500" />;
|
||
const orderNumber = item.expand?.order?.order_number || '';
|
||
if (orderNumber.toLowerCase().includes('breakdown')) return <Hammer size={12} className="text-orange-500" />;
|
||
if (orderNumber.toLowerCase().includes('repair')) return <Wrench size={12} className="text-amber-500" />;
|
||
if (item.is_stock_item) return <Package size={12} className="text-violet-500" />;
|
||
return <ShoppingCart size={12} className="text-gray-400" />;
|
||
}
|
||
|
||
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 (
|
||
<div
|
||
className={cn(
|
||
'bg-white rounded-2xl card-shadow overflow-hidden transition-all duration-200',
|
||
`border-l-[4px] ${color.border}`
|
||
)}
|
||
>
|
||
{/* Supplier Header — always visible, tap to expand/collapse */}
|
||
<button
|
||
onClick={onToggle}
|
||
className="w-full text-left p-4 active:bg-gray-50/50 transition-colors"
|
||
>
|
||
<div className="flex items-center gap-3">
|
||
{/* Color dot */}
|
||
<div className={cn('w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0', color.light)}>
|
||
<MapPin size={16} className={color.text} />
|
||
</div>
|
||
|
||
<div className="flex-1 min-w-0">
|
||
<h2 className={cn('font-semibold text-sm truncate leading-snug', color.text)}>
|
||
{supplier.suppliername}
|
||
</h2>
|
||
{supplier.supplieraddress && (
|
||
<p className="text-[11px] text-gray-400 truncate mt-0.5">{supplier.supplieraddress}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Right side info */}
|
||
<div className="flex items-center gap-2 flex-shrink-0">
|
||
<div className="flex flex-col items-end gap-0.5">
|
||
<span className={cn('text-[11px] font-bold', color.text)}>
|
||
{items.length} item{items.length !== 1 ? 's' : ''}
|
||
</span>
|
||
<span className="text-[10px] text-gray-400">{totalQty} total</span>
|
||
</div>
|
||
<div className="flex items-center justify-center w-7 h-7">
|
||
{isExpanded ? (
|
||
<ChevronUp size={18} className="text-gray-400" />
|
||
) : (
|
||
<ChevronDown size={18} className="text-gray-400" />
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Compact preview line (only when collapsed in compact mode) */}
|
||
{viewMode === 'compact' && !isExpanded && (
|
||
<div className="mt-2 ml-12 flex items-center gap-1.5 flex-wrap">
|
||
{items.slice(0, 3).map((item, i) => (
|
||
<span key={item.id} className="text-[11px] text-gray-500 truncate max-w-[140px]">
|
||
{item.description || 'Item'}
|
||
{i < Math.min(items.length, 3) - 1 && ','}
|
||
</span>
|
||
))}
|
||
{items.length > 3 && (
|
||
<span className="text-[10px] text-gray-400 font-medium">
|
||
+{items.length - 3} more
|
||
</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
</button>
|
||
|
||
{/* Expanded content */}
|
||
{isExpanded && (
|
||
<div className="border-t border-gray-100 animate-fade-in">
|
||
{viewMode === 'compact' ? (
|
||
/* COMPACT MODE: simple list with item name + qty */
|
||
<div className="divide-y divide-gray-50">
|
||
{items.map((item) => {
|
||
const expectedQty = item.order_qty || item.quantity || 0;
|
||
return (
|
||
<div
|
||
key={item.id}
|
||
className="flex items-center justify-between px-4 py-3 hover:bg-gray-50/50 transition-colors cursor-pointer active:bg-gray-100"
|
||
onClick={() => {
|
||
// Determine if we should open collect or problem modal
|
||
// For now, just expand - user can use the action buttons
|
||
}}
|
||
>
|
||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
||
{getItemIcon(item)}
|
||
<span className="text-sm text-gray-800 truncate">
|
||
{item.description || 'Unknown item'}
|
||
</span>
|
||
{item.item_status === 'problem' && (
|
||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-red-100 text-red-700 font-medium flex-shrink-0">
|
||
Problem
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="flex items-center gap-3 flex-shrink-0 ml-2">
|
||
<span className="text-sm font-semibold text-gray-700">{expectedQty}×</span>
|
||
<div className="flex items-center gap-1">
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); onCollect(item); }}
|
||
className="w-8 h-8 rounded-lg bg-green-500 text-white flex items-center justify-center active:bg-green-600 transition-colors touch-manipulation"
|
||
>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
|
||
</button>
|
||
<button
|
||
onClick={(e) => { e.stopPropagation(); onProblem(item); }}
|
||
className="w-8 h-8 rounded-lg bg-red-500 text-white flex items-center justify-center active:bg-red-600 transition-colors touch-manipulation"
|
||
>
|
||
<AlertTriangle size={14} />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
) : (
|
||
/* DETAILED MODE: full item cards */
|
||
<div className="p-3 space-y-2">
|
||
{items.map((item) => (
|
||
<CollectionItemCard
|
||
key={item.id}
|
||
item={item}
|
||
supplierColor={color}
|
||
compact
|
||
onCollect={onCollect}
|
||
onProblem={onProblem}
|
||
onNavigate={onNavigate}
|
||
/>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Supplier footer with navigate button */}
|
||
{hasCoords && coords && (
|
||
<div className="px-4 pb-3">
|
||
<button
|
||
onClick={() => onNavigate(coords.latitude, coords.longitude, supplier.suppliername)}
|
||
className={cn(
|
||
'w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-sm font-semibold text-white transition-all touch-manipulation press-feedback',
|
||
color.dot
|
||
)}
|
||
>
|
||
<Navigation size={14} />
|
||
Navigate to {supplier.suppliername}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
} |