WFDASHDRIVER-APP/components/AreaGroup.tsx
SJCoetz 2a7720cd1f feat(grouping): add area grouping, refine UI and cleanup
- update dev server port to 4001 in package.json
- remove unused formatCurrency import from SupplierGroup component
- refactor cluster label generation for better multi-supplier display
- overhaul AreaGroup UI to use chip-style supplier tags with updated typography
- expand collapseAll handler to support both supplier and area group modes
2026-07-10 02:01:57 +02:00

242 lines
8.9 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.

'use client';
import { OrderItem } from '@/lib/types';
import { cn } from '@/lib/utils';
import CollectionItemCard from '@/components/CollectionItemCard';
import {
ChevronDown,
ChevronUp,
MapPin,
Navigation,
Package,
ShoppingCart,
Hammer,
Wrench,
AlertTriangle,
Layers,
} from 'lucide-react';
interface AreaColor {
dot: string;
light: string;
text: string;
ring: string;
}
interface AreaGroupProps {
clusterId: string;
label: string;
items: OrderItem[];
center: { latitude: number; longitude: number };
color: AreaColor;
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.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 AreaGroup({
clusterId,
label,
items,
center,
color,
isExpanded,
viewMode,
onToggle,
onCollect,
onProblem,
onNavigate,
}: AreaGroupProps) {
const totalQty = items.reduce((sum, i) => sum + (i.order_qty || i.quantity || 0), 0);
const uniqueSuppliers = Array.from(
new Set(
items
.map(i => i.expand?.supplier?.suppliername)
.filter((n): n is string => !!n)
)
);
const hasCoords = center.latitude !== 0 && center.longitude !== 0;
return (
<section
className={cn(
'rounded-2xl overflow-hidden transition-all duration-200 border',
color.light,
`border-[rgba(38,37,30,0.1)]`,
`ring-1 ${color.ring}`
)}
aria-label={`${label} area group`}
>
<button
onClick={onToggle}
aria-expanded={isExpanded}
aria-controls={`area-${clusterId}-items`}
className="w-full text-left p-4 active:bg-[#ebeae5] transition-colors"
>
<div className="flex items-center gap-3">
<div className={cn('w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0', color.dot)} aria-hidden="true">
<Layers size={16} className="text-white" />
</div>
<div className="flex-1 min-w-0">
<h2 className={cn('font-bold text-[17px] leading-snug', color.text)}>
{label}
</h2>
{uniqueSuppliers.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-1.5">
{uniqueSuppliers.map((name) => (
<span
key={name}
className={cn(
'inline-flex items-center gap-1 px-2.5 py-1 rounded-lg text-[12px] font-semibold',
color.light,
color.text,
`ring-1 ${color.ring}`
)}
>
<MapPin size={10} aria-hidden="true" />
{name}
</span>
))}
</div>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex flex-col items-end gap-0.5">
<span className="text-[12px] font-bold text-[#26251e]">
{items.length} item{items.length !== 1 ? 's' : ''}
</span>
<span className="text-[11px] text-[rgba(38,37,30,0.55)]">{totalQty} total</span>
</div>
<div className="flex items-center justify-center w-7 h-7" aria-hidden="true">
{isExpanded ? (
<ChevronUp size={18} className="text-[rgba(38,37,30,0.55)]" />
) : (
<ChevronDown size={18} className="text-[rgba(38,37,30,0.55)]" />
)}
</div>
</div>
</div>
{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-[rgba(38,37,30,0.6)] truncate max-w-[140px]">
{item.description || 'Item'}
{i < Math.min(items.length, 3) - 1 && ','}
</span>
))}
{items.length > 3 && (
<span className="text-[10px] text-[rgba(38,37,30,0.4)] font-medium">
+{items.length - 3} more
</span>
)}
</div>
)}
</button>
{isExpanded && (
<div
id={`area-${clusterId}-items`}
className="border-t border-[rgba(38,37,30,0.08)] animate-fade-in"
>
{viewMode === 'compact' ? (
<ul className="divide-y divide-[rgba(38,37,30,0.05)]" role="list">
{items.map((item) => {
const expectedQty = item.order_qty || item.quantity || 0;
return (
<li
key={item.id}
className="flex items-center justify-between px-4 py-3 hover:bg-[#ebeae5] transition-colors"
>
<div className="flex items-center gap-2.5 min-w-0 flex-1">
{getItemIcon(item)}
<div className="min-w-0 flex-1">
<span className="text-sm text-[#26251e] truncate block">
{item.description || 'Unknown item'}
</span>
{item.expand?.supplier?.suppliername && (
<span className="text-[10px] text-[rgba(38,37,30,0.45)]">
{item.expand.supplier.suppliername}
</span>
)}
</div>
{item.problem_flag && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-[#fbeaef] text-[#8a1b3a] 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-[rgba(38,37,30,0.7)]">{expectedQty}×</span>
<div className="flex items-center gap-1">
<button
onClick={() => onCollect(item)}
aria-label={`Collect ${item.description || 'item'}`}
className="w-10 h-10 rounded-lg bg-[#1f8a65] text-white flex items-center justify-center active:bg-[#125a3e] transition-colors touch-manipulation min-h-[40px] min-w-[40px]"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
</button>
<button
onClick={() => onProblem(item)}
aria-label={`Report problem for ${item.description || 'item'}`}
className="w-10 h-10 rounded-lg bg-[#cf2d56] text-white flex items-center justify-center active:bg-[#8a1b3a] transition-colors touch-manipulation min-h-[40px] min-w-[40px]"
>
<AlertTriangle size={16} aria-hidden="true" />
</button>
</div>
</div>
</li>
);
})}
</ul>
) : (
<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>
)}
{hasCoords && (
<div className="px-4 pb-3">
<button
onClick={() => onNavigate(center.latitude, center.longitude, label)}
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 min-h-[44px]',
color.dot
)}
>
<Navigation size={14} aria-hidden="true" />
Navigate to {label}
</button>
</div>
)}
</div>
)}
</section>
);
}