WFDASHDRIVER-APP/components/AreaGroup.tsx
SJCoetz 85c63b336c feat: add PO/Notes popovers, supplier color sidebar, fix popover clipping
- Add ItemDetailModal showing PO document (view/download/share/email) + buyer notes
- Add FileText (PO) and MessageSquare (notes) buttons to compact item list with popovers
- Fix photo upload: append to order_items.order_item_attachements (PB file field)
- Add colored left sidebar stripe per supplier group for visual distinction
- Fix popover z-index so they appear above following cards (overflow-visible + z-50)
- Extend PurchaseOrder type with documents, email_payload, supplier
- Add order_item_attachements and expand.purchase_order to OrderItem
- onShowDetails prop wired through SupplierGroup and AreaGroup

Co-authored-by: Orca <help@stably.ai>
2026-08-02 21:38:38 +02:00

244 lines
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;
onShowDetails: (item: OrderItem) => 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,
onShowDetails,
}: 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}
onShowDetails={onShowDetails}
/>
))}
</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>
);
}