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.
263 lines
No EOL
9.2 KiB
TypeScript
263 lines
No EOL
9.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { OrderItem } from '@/lib/types';
|
|
import { cn, formatDate, formatCurrency, parseCoordinates } from '@/lib/utils';
|
|
import {
|
|
Check,
|
|
AlertTriangle,
|
|
Navigation,
|
|
ChevronDown,
|
|
ChevronUp,
|
|
Package,
|
|
Wrench,
|
|
Hammer,
|
|
ShoppingCart,
|
|
AlertCircle,
|
|
Clock,
|
|
MapPin,
|
|
} from 'lucide-react';
|
|
|
|
interface CollectionItemProps {
|
|
item: OrderItem;
|
|
supplierColor?: { border: string; dot: string; light: string; text: string; ring: string };
|
|
compact?: boolean;
|
|
onCollect: (item: OrderItem) => void;
|
|
onProblem: (item: OrderItem) => void;
|
|
onNavigate: (lat: number, lng: number, name: string) => void;
|
|
}
|
|
|
|
function getItemType(item: OrderItem): {
|
|
type: string;
|
|
icon: React.ReactNode;
|
|
color: string;
|
|
bgColor: string;
|
|
label: string;
|
|
} {
|
|
if (item.item_status === 'problem' || item.problem_flag) {
|
|
return {
|
|
type: 'problem',
|
|
icon: <AlertTriangle size={16} />,
|
|
color: 'text-red-600',
|
|
bgColor: 'bg-red-50',
|
|
label: 'Problem',
|
|
};
|
|
}
|
|
|
|
const orderNumber = item.expand?.order?.order_number || '';
|
|
|
|
if (orderNumber.toLowerCase().includes('breakdown')) {
|
|
return {
|
|
type: 'breakdown',
|
|
icon: <Hammer size={16} />,
|
|
color: 'text-orange-600',
|
|
bgColor: 'bg-orange-50',
|
|
label: 'Breakdown',
|
|
};
|
|
}
|
|
|
|
if (orderNumber.toLowerCase().includes('repair')) {
|
|
return {
|
|
type: 'repair',
|
|
icon: <Wrench size={16} />,
|
|
color: 'text-amber-600',
|
|
bgColor: 'bg-amber-50',
|
|
label: 'Repair',
|
|
};
|
|
}
|
|
|
|
if (item.is_stock_item) {
|
|
return {
|
|
type: 'stock',
|
|
icon: <Package size={16} />,
|
|
color: 'text-violet-600',
|
|
bgColor: 'bg-violet-50',
|
|
label: 'Stock',
|
|
};
|
|
}
|
|
|
|
return {
|
|
type: 'normal',
|
|
icon: <ShoppingCart size={16} />,
|
|
color: 'text-orange-600',
|
|
bgColor: 'bg-orange-50',
|
|
label: 'Order',
|
|
};
|
|
}
|
|
|
|
export default function CollectionItemCard({
|
|
item,
|
|
supplierColor,
|
|
compact,
|
|
onCollect,
|
|
onProblem,
|
|
onNavigate,
|
|
}: CollectionItemProps) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
const supplier = item.expand?.supplier;
|
|
const itemType = getItemType(item);
|
|
const coords = parseCoordinates(supplier?.coordinates);
|
|
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 (
|
|
<div
|
|
className={cn(
|
|
compact
|
|
? 'bg-gray-50 rounded-xl overflow-hidden'
|
|
: 'bg-white rounded-2xl card-shadow card-press overflow-hidden',
|
|
(item.item_status === 'problem' || item.problem_flag) && 'ring-1 ring-red-400',
|
|
item.item_status === 'assigned' && !compact && `border-l-[3px] ${colorAccent}`
|
|
)}
|
|
>
|
|
{/* Header */}
|
|
<button
|
|
className="w-full text-left p-4 active:bg-gray-50/50 transition-colors"
|
|
onClick={() => setExpanded(!expanded)}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
{/* Type icon */}
|
|
<div className={cn(
|
|
'w-9 h-9 rounded-xl flex items-center justify-center flex-shrink-0 mt-0.5',
|
|
itemType.bgColor
|
|
)}>
|
|
<span className={itemType.color}>{itemType.icon}</span>
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-0.5">
|
|
<h3 className="font-semibold text-[15px] text-gray-900 truncate leading-snug">
|
|
{description}
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 flex-wrap">
|
|
<span className={cn(
|
|
'text-[10px] px-2 py-0.5 rounded-full font-semibold uppercase tracking-wide',
|
|
itemType.type === 'problem' && 'bg-red-100 text-red-700',
|
|
itemType.type === 'breakdown' && 'bg-orange-100 text-orange-700',
|
|
itemType.type === 'repair' && 'bg-amber-100 text-amber-700',
|
|
itemType.type === 'stock' && 'bg-violet-100 text-violet-700',
|
|
itemType.type === 'normal' && 'bg-orange-100 text-orange-700'
|
|
)}>
|
|
{itemType.label}
|
|
</span>
|
|
<span className="text-xs text-gray-400">Qty: {expectedQty}</span>
|
|
{item.collection_date && (
|
|
<span className="text-xs text-gray-400 flex items-center gap-0.5">
|
|
<Clock size={10} />
|
|
{formatDate(item.collection_date)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{supplier?.suppliername && (
|
|
<div className="flex items-center gap-1 mt-1.5">
|
|
<MapPin size={12} className="text-gray-400 flex-shrink-0" />
|
|
<span className="text-xs text-gray-500 truncate">{supplier.suppliername}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
{item.selling_price && expectedQty > 0 && (
|
|
<span className="text-xs font-semibold text-green-600">
|
|
{formatCurrency(item.selling_price * expectedQty)}
|
|
</span>
|
|
)}
|
|
{expanded ? (
|
|
<ChevronUp size={18} className="text-gray-400" />
|
|
) : (
|
|
<ChevronDown size={18} className="text-gray-400" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</button>
|
|
|
|
{/* Expanded Details */}
|
|
{expanded && (
|
|
<div className="px-4 pb-4 border-t border-gray-100 animate-fade-in">
|
|
<div className="pt-3 space-y-3">
|
|
{/* Notes */}
|
|
{item.notes && (
|
|
<div className="bg-amber-50 border border-amber-100 rounded-xl p-3">
|
|
<p className="text-xs text-amber-800 flex items-start gap-2">
|
|
<AlertCircle size={14} className="flex-shrink-0 mt-0.5" />
|
|
<span className="selectable">{item.notes}</span>
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{item.problem_notes && (
|
|
<div className="bg-red-50 border border-red-100 rounded-xl p-3">
|
|
<p className="text-xs text-red-700 flex items-start gap-2">
|
|
<AlertTriangle size={14} className="flex-shrink-0 mt-0.5" />
|
|
<span className="selectable">{item.problem_notes}</span>
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Details grid */}
|
|
<div className="grid grid-cols-2 gap-2 text-xs">
|
|
{item.expand?.order?.order_number && (
|
|
<div className="bg-gray-50 rounded-lg p-2">
|
|
<span className="text-gray-400 block">Order</span>
|
|
<span className="text-gray-700 font-medium">{item.expand.order.order_number}</span>
|
|
</div>
|
|
)}
|
|
{item.expand?.purchase_order?.po_number && (
|
|
<div className="bg-gray-50 rounded-lg p-2">
|
|
<span className="text-gray-400 block">PO</span>
|
|
<span className="text-gray-700 font-medium">{item.expand.purchase_order.po_number}</span>
|
|
</div>
|
|
)}
|
|
{supplier?.supplieraddress && (
|
|
<div className="bg-gray-50 rounded-lg p-2 col-span-2">
|
|
<span className="text-gray-400 block">Address</span>
|
|
<span className="text-gray-700 font-medium selectable">{supplier.supplieraddress}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="grid grid-cols-3 gap-2 pt-1">
|
|
<button
|
|
onClick={() => onCollect(item)}
|
|
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-green-500 text-white active:bg-green-600 transition-colors touch-manipulation press-feedback-sm"
|
|
>
|
|
<Check size={18} strokeWidth={2.5} />
|
|
<span className="text-xs font-semibold">Collect</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => onProblem(item)}
|
|
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-red-500 text-white active:bg-red-600 transition-colors touch-manipulation press-feedback-sm"
|
|
>
|
|
<AlertTriangle size={18} strokeWidth={2.5} />
|
|
<span className="text-xs font-semibold">Problem</span>
|
|
</button>
|
|
|
|
{hasCoords && coords && supplier ? (
|
|
<button
|
|
onClick={() => onNavigate(coords.latitude, coords.longitude, supplier.suppliername)}
|
|
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-orange-500 text-white active:bg-orange-600 transition-colors touch-manipulation press-feedback-sm"
|
|
>
|
|
<Navigation size={18} strokeWidth={2.5} />
|
|
<span className="text-xs font-semibold">Navigate</span>
|
|
</button>
|
|
) : (
|
|
<div className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-gray-100 text-gray-400">
|
|
<Navigation size={18} />
|
|
<span className="text-xs font-medium">No GPS</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |