WFDASHDRIVER-APP/components/CollectionItemCard.tsx
SJCoetz b80551fe4d refactor(ui): rebrand app, fix supplier naming
- rename `supplier` field to `suppliers` in OrderItem interface and PocketBase expand queries
- complete visual rebrand to warm cream (#f2f1ed) and dark brown (#26251e) color palette with custom CSS variables
- add accessibility improvements: skip-to-main content link, focus-visible styles, reduced motion support, and proper ARIA labels
- update all component styling to match new theme, including offline indicator, install prompt, login page, modals, map, bottom nav and global CSS
- standardize status colors: use green for success, red for errors, retain orange (#f54e00) as primary accent CTAs
2026-07-05 22:32:20 +02:00

269 lines
No EOL
9.8 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?: { 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.problem_flag) {
return {
type: 'problem',
icon: <AlertTriangle size={16} />,
color: 'text-[#b3224a]',
bgColor: 'bg-[#fbeaef]',
label: 'Problem',
};
}
const orderNumber = item.expand?.order?.order_number || '';
if (orderNumber.toLowerCase().includes('breakdown')) {
return {
type: 'breakdown',
icon: <Hammer size={16} />,
color: 'text-[#8a5a02]',
bgColor: 'bg-[#fcf3d8]',
label: 'Breakdown',
};
}
if (orderNumber.toLowerCase().includes('repair')) {
return {
type: 'repair',
icon: <Wrench size={16} />,
color: 'text-[#8a5a02]',
bgColor: 'bg-[#f6e3a4]',
label: 'Repair',
};
}
if (item.is_stock_item) {
return {
type: 'stock',
icon: <Package size={16} />,
color: 'text-[#4a3d6b]',
bgColor: 'bg-[#ece8f3]',
label: 'Stock',
};
}
return {
type: 'normal',
icon: <ShoppingCart size={16} />,
color: 'text-[#26251e]',
bgColor: 'bg-[#ebeae5]',
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;
return (
<article
className={cn(
compact
? 'bg-[#ebeae5] rounded-xl overflow-hidden'
: 'bg-[#e6e5e0] rounded-2xl card-press overflow-hidden border border-[rgba(38,37,30,0.1)]',
item.problem_flag && 'ring-1 ring-[#cf2d56]'
)}
>
{/* Header */}
<button
className="w-full text-left p-4 active:bg-[rgba(38,37,30,0.04)] transition-colors"
onClick={() => setExpanded(!expanded)}
aria-expanded={expanded}
aria-controls={`item-${item.id}-details`}
>
<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
)} aria-hidden="true">
<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-[#26251e] 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-[#f3c4d2] text-[#8a1b3a]',
itemType.type === 'breakdown' && 'bg-[#f6e3a4] text-[#8a5a02]',
itemType.type === 'repair' && 'bg-[#f6e3a4] text-[#8a5a02]',
itemType.type === 'stock' && 'bg-[#d4cae3] text-[#4a3d6b]',
itemType.type === 'normal' && 'bg-[#ebeae5] text-[#26251e]'
)}>
{itemType.label}
</span>
<span className="text-xs text-[rgba(38,37,30,0.6)]">Qty: {expectedQty}</span>
{item.collection_date && (
<span className="text-xs text-[rgba(38,37,30,0.55)] flex items-center gap-0.5">
<Clock size={10} aria-hidden="true" />
{formatDate(item.collection_date)}
</span>
)}
</div>
{supplier?.suppliername && (
<div className="flex items-center gap-1 mt-1.5">
<MapPin size={12} className="text-[rgba(38,37,30,0.4)] flex-shrink-0" aria-hidden="true" />
<span className="text-xs text-[rgba(38,37,30,0.55)] 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-[#125a3e]">
{formatCurrency(item.selling_price * expectedQty)}
</span>
)}
{expanded ? (
<ChevronUp size={18} className="text-[rgba(38,37,30,0.55)]" aria-hidden="true" />
) : (
<ChevronDown size={18} className="text-[rgba(38,37,30,0.55)]" aria-hidden="true" />
)}
</div>
</div>
</button>
{/* Expanded Details */}
{expanded && (
<div
id={`item-${item.id}-details`}
className="px-4 pb-4 border-t border-[rgba(38,37,30,0.08)] animate-fade-in"
>
<div className="pt-3 space-y-3">
{/* Notes */}
{item.notes && (
<div className="bg-[#fcf3d8] border border-[#f6e3a4] rounded-xl p-3">
<p className="text-xs text-[#8a5a02] flex items-start gap-2">
<AlertCircle size={14} className="flex-shrink-0 mt-0.5" aria-hidden="true" />
<span className="selectable">{item.notes}</span>
</p>
</div>
)}
{item.problem_notes && (
<div className="bg-[#fbeaef] border border-[#f3c4d2] rounded-xl p-3">
<p className="text-xs text-[#8a1b3a] flex items-start gap-2">
<AlertTriangle size={14} className="flex-shrink-0 mt-0.5" aria-hidden="true" />
<span className="selectable">{item.problem_notes}</span>
</p>
</div>
)}
{/* Details grid */}
<dl className="grid grid-cols-2 gap-2 text-xs">
{item.expand?.order?.order_number && (
<div className="bg-[#ebeae5] rounded-lg p-2">
<dt className="text-[rgba(38,37,30,0.55)]">Order</dt>
<dd className="text-[#26251e] font-medium">{item.expand.order.order_number}</dd>
</div>
)}
{item.expand?.purchase_order?.po_number && (
<div className="bg-[#ebeae5] rounded-lg p-2">
<dt className="text-[rgba(38,37,30,0.55)]">PO</dt>
<dd className="text-[#26251e] font-medium">{item.expand.purchase_order.po_number}</dd>
</div>
)}
{supplier?.supplieraddress && (
<div className="bg-[#ebeae5] rounded-lg p-2 col-span-2">
<dt className="text-[rgba(38,37,30,0.55)]">Address</dt>
<dd className="text-[#26251e] font-medium selectable">{supplier.supplieraddress}</dd>
</div>
)}
</dl>
{/* Action Buttons */}
<div className="grid grid-cols-3 gap-2 pt-1">
<button
onClick={() => onCollect(item)}
aria-label={`Mark ${description} as collected`}
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#1f8a65] text-white active:bg-[#125a3e] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
>
<Check size={18} strokeWidth={2.5} aria-hidden="true" />
<span className="text-xs font-semibold">Collect</span>
</button>
<button
onClick={() => onProblem(item)}
aria-label={`Report problem with ${description}`}
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#cf2d56] text-white active:bg-[#8a1b3a] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
>
<AlertTriangle size={18} strokeWidth={2.5} aria-hidden="true" />
<span className="text-xs font-semibold">Problem</span>
</button>
{hasCoords && coords && supplier ? (
<button
onClick={() => onNavigate(coords.latitude, coords.longitude, supplier.suppliername)}
aria-label={`Navigate to ${supplier.suppliername}`}
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#f54e00] text-white active:bg-[#d44200] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
>
<Navigation size={18} strokeWidth={2.5} aria-hidden="true" />
<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-[rgba(38,37,30,0.06)] text-[rgba(38,37,30,0.4)] min-h-[56px]" aria-label="No GPS coordinates available">
<Navigation size={18} aria-hidden="true" />
<span className="text-xs font-medium">No GPS</span>
</div>
)}
</div>
</div>
</div>
)}
</article>
);
}