WFDASHDRIVER-APP/components/CollectionItemCard.tsx
SJCoetz 0d3272da46 Refactor collections to order items
Convert the app from using legacy Collection models to OrderItem-based data. Updated pages (app/page and app/map/page) and all related components (CollectionMap, CollectionItemCard, CollectModal, ProblemModal, LocationTracker) to use OrderItem and new field names (supplier, order, item_status, qty_collected/collected_date, problem_flag/notes, etc.). PocketBase client now queries/updates the order_items collection and maps status/field changes accordingly. Simplified location tracking logic and cleaned up map/navigation UI and Suspense fallbacks. Added import_suppliers.py script for migrating suppliers from NocoDB and included .DS_Store artifacts.
2026-05-25 00:47:47 +02:00

240 lines
7.5 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,
} from 'lucide-react';
interface CollectionItemProps {
item: OrderItem;
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;
label: string;
} {
if (item.item_status === 'problem' || item.problem_flag) {
return {
type: 'problem',
icon: <AlertTriangle size={16} />,
color: 'text-red-500',
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',
label: 'Breakdown',
};
}
if (orderNumber.toLowerCase().includes('repair')) {
return {
type: 'repair',
icon: <Wrench size={16} />,
color: 'text-amber-500',
label: 'Repair',
};
}
if (item.is_stock_item) {
return {
type: 'stock',
icon: <Package size={16} />,
color: 'text-purple-500',
label: 'Stock',
};
}
return {
type: 'normal',
icon: <ShoppingCart size={16} />,
color: 'text-orange-500',
label: 'Order',
};
}
export default function CollectionItemCard({
item,
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;
return (
<div
className={cn(
'bg-white rounded-xl card-shadow mb-3 overflow-hidden animate-fade-in',
(item.item_status === 'problem' || item.problem_flag) && 'border-l-4 border-red-500',
item.item_status === 'assigned' && 'border-l-4 border-orange-500',
item.driver_status === 'assigned' && 'border-l-4 border-orange-500'
)}
>
{/* Header */}
<div
className="p-4 cursor-pointer active:bg-gray-50 transition-colors"
onClick={() => setExpanded(!expanded)}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className={cn('flex-shrink-0', itemType.color)}>
{itemType.icon}
</span>
<h3 className="font-semibold text-sm leading-tight truncate">
{description}
</h3>
<span
className={cn(
'text-[10px] px-1.5 py-0.5 rounded-full font-medium flex-shrink-0',
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-purple-100 text-purple-700',
itemType.type === 'normal' && 'bg-orange-100 text-orange-700'
)}
>
{itemType.label}
</span>
</div>
<div className="text-xs text-gray-500 mt-1 space-y-0.5">
<p className="font-medium text-gray-700">
{supplier?.suppliername || 'Unknown Supplier'}
</p>
<p>Expected: {expectedQty} units</p>
{item.expand?.order?.order_number && <p>Order: {item.expand.order.order_number}</p>}
{item.expand?.purchase_order?.po_number && <p>PO: {item.expand.purchase_order.po_number}</p>}
{item.collection_date && (
<p>Planned: {formatDate(item.collection_date)}</p>
)}
{item.selling_price && (
<p className="text-green-600 font-medium">
{formatCurrency(item.selling_price * expectedQty)}
</p>
)}
</div>
</div>
<div className="flex flex-col items-end gap-2 ml-2">
{expanded ? (
<ChevronUp size={20} className="text-gray-400" />
) : (
<ChevronDown size={20} className="text-gray-400" />
)}
</div>
</div>
</div>
{/* Expanded Details */}
{expanded && (
<div className="px-4 pb-4 border-t border-gray-100 pt-3">
{item.notes && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-2 mb-3">
<p className="text-xs text-yellow-800 flex items-start gap-1.5">
<AlertCircle size={14} className="flex-shrink-0 mt-0.5" />
<span>{item.notes}</span>
</p>
</div>
)}
{item.problem_notes && (
<div className="bg-red-50 border border-red-200 rounded-lg p-2 mb-3">
<p className="text-xs text-red-700 flex items-start gap-1.5">
<AlertTriangle size={14} className="flex-shrink-0 mt-0.5" />
<span>{item.problem_notes}</span>
</p>
</div>
)}
{supplier?.supplieraddress && (
<p className="text-xs text-gray-500 mb-3">
<strong>Address:</strong> {supplier.supplieraddress}
</p>
)}
{/* Action Buttons */}
<div className="grid grid-cols-3 gap-2">
<button
onClick={(e) => {
e.stopPropagation();
onCollect(item);
}}
className={cn(
'flex items-center justify-center gap-1.5 py-2.5 px-3 rounded-lg text-sm font-medium',
'bg-green-500 text-white active:bg-green-600 transition-colors',
'touch-manipulation'
)}
>
<Check size={16} strokeWidth={2.5} />
<span>Collect</span>
</button>
<button
onClick={(e) => {
e.stopPropagation();
onProblem(item);
}}
className={cn(
'flex items-center justify-center gap-1.5 py-2.5 px-3 rounded-lg text-sm font-medium',
'bg-red-500 text-white active:bg-red-600 transition-colors',
'touch-manipulation'
)}
>
<AlertTriangle size={16} strokeWidth={2.5} />
<span>Problem</span>
</button>
{hasCoords && coords && supplier && (
<button
onClick={(e) => {
e.stopPropagation();
onNavigate(coords.latitude, coords.longitude, supplier.suppliername);
}}
className={cn(
'flex items-center justify-center gap-1.5 py-2.5 px-3 rounded-lg text-sm font-medium',
'bg-orange-500 text-white active:bg-orange-600 transition-colors',
'touch-manipulation'
)}
>
<Navigation size={16} strokeWidth={2.5} />
<span>Navigate</span>
</button>
)}
</div>
</div>
)}
</div>
);
}