WFDASHDRIVER-APP/components/CollectionItemCard.tsx

557 lines
25 KiB
TypeScript
Raw Normal View History

'use client';
import { useState, useRef, useEffect } from 'react';
import { OrderItem } from '@/lib/types';
import { cn, formatDate, formatCurrency, parseCoordinates } from '@/lib/utils';
feat: popover stays above footer, partial collections must be justified Two improvements to the collection flow: 1. Popover clipping under BottomNav (last item in list) - Root cause: popovers were z-[60] *inside* a parent stacking context at z-30, so their effective page z-index was 30 — below BottomNav's z-50. Even if the z-index were right, the popover extends below the trigger button and could fall below the viewport on the last item in a long list. - Add lib/hooks.ts with usePopoverPlacement: measures the trigger's bottom edge and flips popover direction to "up" when there isn't ~240px of room below (reserving 80px for BottomNav + safe-area inset). - Bump the article/section/li stacking context from z-30 to z-[80] when a popover is open so the popover paints above BottomNav (z-50). Covers both compact-mode SupplierGroup rows and detailed-mode CollectionItemCard via the existing onPopoverChange callback. - For SupplierGroup: hooks live at the top level (record keyed by item.id) to avoid the Rules of Hooks trap of declaring hooks inside items.map(). 2. Partial collections must be justified - In CollectModal, when received qty < expected, require EITHER a reason (dropdown: out of stock / wrong item / quality issue / damaged / wrong qty packed / other) OR a photo before Confirm is enabled. Prevents silent partial submissions with no audit trail. - The picked reason flows into the auto-generated problem note so the operator sees "[Reason] driver note" in PB. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
2026-08-02 20:58:08 +00:00
import { usePopoverPlacement } from '@/lib/hooks';
import { getItemDisplayDescription, getItemDisplayQty } from '@/lib/pocketbase';
import {
Check,
AlertTriangle,
Navigation,
ChevronDown,
ChevronUp,
Package,
Wrench,
Hammer,
ShoppingCart,
AlertCircle,
Clock,
MapPin,
FileText,
MessageSquare,
ExternalLink,
} 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;
// Opens the ItemDetailModal showing PO document + buyer notes
onShowDetails: (item: OrderItem) => void;
// Notifies the parent that a popover has opened/closed so it can
// lift its own stacking layer — the card's z-30 alone can't escape
// the section's stacking context, so the section needs to know.
onPopoverChange?: (open: boolean) => 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 || '';
const poNumber = item.expand?.purchase_order?.po_number || '';
if (item.is_stock_item) {
return {
type: 'stock',
icon: <Package size={16} />,
color: 'text-[#4a3d6b]',
bgColor: 'bg-[#d4cae3]',
label: 'Stock',
};
}
if (orderNumber.startsWith('RPR') || poNumber.startsWith('RPR')) {
return {
type: 'repair',
icon: <Wrench size={16} />,
color: 'text-[#8a5a02]',
bgColor: 'bg-[#f6e3a4]',
label: 'Repair',
};
}
if (orderNumber.startsWith('BRK') || poNumber.startsWith('BRK')) {
return {
type: 'breakdown',
icon: <Hammer size={16} />,
color: 'text-[#8a5a02]',
bgColor: 'bg-[#f6e3a4]',
label: 'Breakdown',
};
}
return {
type: 'normal',
icon: <ShoppingCart size={16} />,
color: 'text-[#26251e]',
bgColor: 'bg-[#ebeae5]',
label: 'Order',
};
}
// Closes tooltip when clicking outside
function useClickOutside(ref: React.RefObject<HTMLElement | null>, onClose: () => void) {
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
onClose();
}
}
document.addEventListener('mousedown', handleClick);
return () => document.removeEventListener('mousedown', handleClick);
}, [ref, onClose]);
}
export default function CollectionItemCard({
item,
supplierColor,
compact,
onCollect,
onProblem,
onNavigate,
onShowDetails,
onPopoverChange,
}: CollectionItemProps) {
const [expanded, setExpanded] = useState(false);
const [showNotesTooltip, setShowNotesTooltip] = useState(false);
const [showPoTooltip, setShowPoTooltip] = useState(false);
const notesTooltipRef = useRef<HTMLDivElement>(null);
const poTooltipRef = useRef<HTMLDivElement>(null);
feat: popover stays above footer, partial collections must be justified Two improvements to the collection flow: 1. Popover clipping under BottomNav (last item in list) - Root cause: popovers were z-[60] *inside* a parent stacking context at z-30, so their effective page z-index was 30 — below BottomNav's z-50. Even if the z-index were right, the popover extends below the trigger button and could fall below the viewport on the last item in a long list. - Add lib/hooks.ts with usePopoverPlacement: measures the trigger's bottom edge and flips popover direction to "up" when there isn't ~240px of room below (reserving 80px for BottomNav + safe-area inset). - Bump the article/section/li stacking context from z-30 to z-[80] when a popover is open so the popover paints above BottomNav (z-50). Covers both compact-mode SupplierGroup rows and detailed-mode CollectionItemCard via the existing onPopoverChange callback. - For SupplierGroup: hooks live at the top level (record keyed by item.id) to avoid the Rules of Hooks trap of declaring hooks inside items.map(). 2. Partial collections must be justified - In CollectModal, when received qty < expected, require EITHER a reason (dropdown: out of stock / wrong item / quality issue / damaged / wrong qty packed / other) OR a photo before Confirm is enabled. Prevents silent partial submissions with no audit trail. - The picked reason flows into the auto-generated problem note so the operator sees "[Reason] driver note" in PB. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
2026-08-02 20:58:08 +00:00
// Button refs feed the placement hook so the popover flips above
// the button when there's not enough room below it (e.g. on the
// last item in a long list, where the default top-full direction
// would push the popover under the BottomNav).
const poButtonRef = useRef<HTMLButtonElement>(null);
const notesButtonRef = useRef<HTMLButtonElement>(null);
const { placement: poPlacement, measure: measurePoPlacement } = usePopoverPlacement(poButtonRef);
const { placement: notesPlacement, measure: measureNotesPlacement } = usePopoverPlacement(notesButtonRef);
useClickOutside(notesTooltipRef, () => setShowNotesTooltip(false));
useClickOutside(poTooltipRef, () => setShowPoTooltip(false));
// Bubble the open/close state up so the wrapping supplier/area
// group can lift itself above the *next* group in the list.
const popoverOpen = showNotesTooltip || showPoTooltip;
useEffect(() => {
onPopoverChange?.(popoverOpen);
}, [popoverOpen, onPopoverChange]);
const supplier = item.expand?.supplier;
const itemType = getItemType(item);
const coords = parseCoordinates(supplier?.coordinates);
const hasCoords = coords !== null;
const description = getItemDisplayDescription(item);
const expectedQty = getItemDisplayQty(item);
const po = item.expand?.purchase_order;
const order = item.expand?.order;
const poNumber = po?.po_number;
const orderNumber = order?.order_number;
const hasNotes = Boolean(item.notes?.trim());
const hasProblemNotes = Boolean(item.problem_notes?.trim());
const hasAnyNotes = hasNotes || hasProblemNotes;
return (
<article
className={cn(
compact
? 'bg-[#ebeae5] rounded-xl overflow-visible'
: 'bg-[#e6e5e0] rounded-2xl card-press overflow-visible border border-[rgba(38,37,30,0.1)]',
feat: popover stays above footer, partial collections must be justified Two improvements to the collection flow: 1. Popover clipping under BottomNav (last item in list) - Root cause: popovers were z-[60] *inside* a parent stacking context at z-30, so their effective page z-index was 30 — below BottomNav's z-50. Even if the z-index were right, the popover extends below the trigger button and could fall below the viewport on the last item in a long list. - Add lib/hooks.ts with usePopoverPlacement: measures the trigger's bottom edge and flips popover direction to "up" when there isn't ~240px of room below (reserving 80px for BottomNav + safe-area inset). - Bump the article/section/li stacking context from z-30 to z-[80] when a popover is open so the popover paints above BottomNav (z-50). Covers both compact-mode SupplierGroup rows and detailed-mode CollectionItemCard via the existing onPopoverChange callback. - For SupplierGroup: hooks live at the top level (record keyed by item.id) to avoid the Rules of Hooks trap of declaring hooks inside items.map(). 2. Partial collections must be justified - In CollectModal, when received qty < expected, require EITHER a reason (dropdown: out of stock / wrong item / quality issue / damaged / wrong qty packed / other) OR a photo before Confirm is enabled. Prevents silent partial submissions with no audit trail. - The picked reason flows into the auto-generated problem note so the operator sees "[Reason] driver note" in PB. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
2026-08-02 20:58:08 +00:00
// z-[80] (above the BottomNav's z-50) when a popover is open,
// so the popover can paint above the bottom chrome. The
// popover's own z-[60] is *inside* this article's stacking
// context, so without this lift the popover's effective page
// z-index is only 80 — wait, this is the article's z. The
// popover stacks at this z in the page context.
(showNotesTooltip || showPoTooltip) && 'relative z-[80]',
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>
{/* Main content */}
<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 flex-1">
{description}
</h3>
</div>
{/* PO and Order numbers — always visible in compact mode */}
{compact && (poNumber || orderNumber) && (
<div className="flex items-center gap-2 mb-0.5 -mt-0.5">
{poNumber && (
<span className="text-[10px] text-[rgba(38,37,30,0.5)] font-mono">
PO: {poNumber}
</span>
)}
{poNumber && orderNumber && (
<span className="text-[10px] text-[rgba(38,37,30,0.3)]" aria-hidden="true">·</span>
)}
{orderNumber && (
<span className="text-[10px] text-[rgba(38,37,30,0.5)]">
Order: {orderNumber}
</span>
)}
</div>
)}
{/* If the customer-facing description differs from the
supplier-facing one we just rendered, surface the
customer one as a smaller sub-line so the driver can
spot a discrepancy on pickup. */}
{item.description && item.po_description && item.description.trim() !== item.po_description.trim() && (
<p className="text-[11px] text-[rgba(38,37,30,0.55)] truncate -mt-0.5 mb-0.5">
customer line: {item.description}
</p>
)}
<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.sku && (
<span className="text-[10px] text-[rgba(38,37,30,0.55)] font-mono">#{item.sku}</span>
)}
{item.assigned_at && (
<span className="text-[10px] text-[rgba(38,37,30,0.45)] flex items-center gap-0.5">
<Clock size={9} aria-hidden="true" />
assigned {formatDate(item.assigned_at)}
</span>
)}
</div>
{supplier?.suppliername && !compact && (
<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>
{/* Right side — tooltip buttons always visible in compact mode */}
{compact ? (
<div className="flex flex-col items-center gap-1.5 flex-shrink-0">
{/* Price + chevron row */}
<div className="flex items-center gap-2">
{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>
{/* Always-visible tooltip buttons — shown as a row of 2 */}
<div className="flex items-center gap-1">
{/* PO tooltip button */}
<div className="relative">
<button
feat: popover stays above footer, partial collections must be justified Two improvements to the collection flow: 1. Popover clipping under BottomNav (last item in list) - Root cause: popovers were z-[60] *inside* a parent stacking context at z-30, so their effective page z-index was 30 — below BottomNav's z-50. Even if the z-index were right, the popover extends below the trigger button and could fall below the viewport on the last item in a long list. - Add lib/hooks.ts with usePopoverPlacement: measures the trigger's bottom edge and flips popover direction to "up" when there isn't ~240px of room below (reserving 80px for BottomNav + safe-area inset). - Bump the article/section/li stacking context from z-30 to z-[80] when a popover is open so the popover paints above BottomNav (z-50). Covers both compact-mode SupplierGroup rows and detailed-mode CollectionItemCard via the existing onPopoverChange callback. - For SupplierGroup: hooks live at the top level (record keyed by item.id) to avoid the Rules of Hooks trap of declaring hooks inside items.map(). 2. Partial collections must be justified - In CollectModal, when received qty < expected, require EITHER a reason (dropdown: out of stock / wrong item / quality issue / damaged / wrong qty packed / other) OR a photo before Confirm is enabled. Prevents silent partial submissions with no audit trail. - The picked reason flows into the auto-generated problem note so the operator sees "[Reason] driver note" in PB. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
2026-08-02 20:58:08 +00:00
ref={poButtonRef}
onClick={(e) => {
e.stopPropagation();
measurePoPlacement();
setShowPoTooltip(v => !v);
setShowNotesTooltip(false);
}}
aria-label="Show PO details"
aria-expanded={showPoTooltip}
className="w-6 h-6 rounded-md flex items-center justify-center bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors"
>
<FileText size={11} aria-hidden="true" />
</button>
{/* PO tooltip popover */}
{showPoTooltip && (
<div
ref={poTooltipRef}
feat: popover stays above footer, partial collections must be justified Two improvements to the collection flow: 1. Popover clipping under BottomNav (last item in list) - Root cause: popovers were z-[60] *inside* a parent stacking context at z-30, so their effective page z-index was 30 — below BottomNav's z-50. Even if the z-index were right, the popover extends below the trigger button and could fall below the viewport on the last item in a long list. - Add lib/hooks.ts with usePopoverPlacement: measures the trigger's bottom edge and flips popover direction to "up" when there isn't ~240px of room below (reserving 80px for BottomNav + safe-area inset). - Bump the article/section/li stacking context from z-30 to z-[80] when a popover is open so the popover paints above BottomNav (z-50). Covers both compact-mode SupplierGroup rows and detailed-mode CollectionItemCard via the existing onPopoverChange callback. - For SupplierGroup: hooks live at the top level (record keyed by item.id) to avoid the Rules of Hooks trap of declaring hooks inside items.map(). 2. Partial collections must be justified - In CollectModal, when received qty < expected, require EITHER a reason (dropdown: out of stock / wrong item / quality issue / damaged / wrong qty packed / other) OR a photo before Confirm is enabled. Prevents silent partial submissions with no audit trail. - The picked reason flows into the auto-generated problem note so the operator sees "[Reason] driver note" in PB. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
2026-08-02 20:58:08 +00:00
className={cn(
'absolute right-0 z-[60] w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3',
// Flip above the button when there's not enough
// room below — keeps the popover out from under
// the BottomNav on the last item in a long list.
poPlacement === 'down' ? 'top-full mt-1' : 'bottom-full mb-1'
)}
>
{po ? (
<>
<div className="flex items-center justify-between mb-2">
<p className="text-[11px] font-semibold text-[#26251e] uppercase tracking-wide">Purchase Order</p>
<button
onClick={(e) => { e.stopPropagation(); setShowPoTooltip(false); }}
aria-label="Close"
className="w-5 h-5 rounded flex items-center justify-center hover:bg-[rgba(38,37,30,0.08)]"
>
<span className="text-[rgba(38,37,30,0.4)] text-sm">×</span>
</button>
</div>
<dl className="space-y-1.5 text-xs">
<div className="flex justify-between">
<dt className="text-[rgba(38,37,30,0.55)]">PO Number</dt>
<dd className="text-[#26251e] font-medium font-mono">{po.po_number}</dd>
</div>
{po.status && (
<div className="flex justify-between">
<dt className="text-[rgba(38,37,30,0.55)]">Status</dt>
<dd className="text-[#26251e] font-medium capitalize">{po.status}</dd>
</div>
)}
{po.total_amount != null && (
<div className="flex justify-between">
<dt className="text-[rgba(38,37,30,0.55)]">Total</dt>
<dd className="text-[#26251e] font-medium">R{(po.total_amount / 100).toFixed(2)}</dd>
</div>
)}
{orderNumber && (
<div className="flex justify-between">
<dt className="text-[rgba(38,37,30,0.55)]">Order</dt>
<dd className="text-[#26251e] font-medium">{orderNumber}</dd>
</div>
)}
</dl>
<button
onClick={(e) => { e.stopPropagation(); setShowPoTooltip(false); onShowDetails(item); }}
className="mt-3 w-full flex items-center justify-center gap-2 py-2 rounded-lg bg-[#6c5a99] text-white text-[12px] font-semibold active:bg-[#4a3d6b] transition-colors"
>
<ExternalLink size={13} aria-hidden="true" />
View PO &amp; Document
</button>
</>
) : (
<div className="text-center py-2">
<p className="text-xs text-[rgba(38,37,30,0.55)]">No PO attached</p>
<button
onClick={(e) => { e.stopPropagation(); setShowPoTooltip(false); onShowDetails(item); }}
className="mt-2 text-[11px] text-[#6c5a99] font-medium hover:underline"
>
View details
</button>
</div>
)}
</div>
)}
</div>
{/* Notes tooltip button */}
{hasAnyNotes && (
<div className="relative">
<button
feat: popover stays above footer, partial collections must be justified Two improvements to the collection flow: 1. Popover clipping under BottomNav (last item in list) - Root cause: popovers were z-[60] *inside* a parent stacking context at z-30, so their effective page z-index was 30 — below BottomNav's z-50. Even if the z-index were right, the popover extends below the trigger button and could fall below the viewport on the last item in a long list. - Add lib/hooks.ts with usePopoverPlacement: measures the trigger's bottom edge and flips popover direction to "up" when there isn't ~240px of room below (reserving 80px for BottomNav + safe-area inset). - Bump the article/section/li stacking context from z-30 to z-[80] when a popover is open so the popover paints above BottomNav (z-50). Covers both compact-mode SupplierGroup rows and detailed-mode CollectionItemCard via the existing onPopoverChange callback. - For SupplierGroup: hooks live at the top level (record keyed by item.id) to avoid the Rules of Hooks trap of declaring hooks inside items.map(). 2. Partial collections must be justified - In CollectModal, when received qty < expected, require EITHER a reason (dropdown: out of stock / wrong item / quality issue / damaged / wrong qty packed / other) OR a photo before Confirm is enabled. Prevents silent partial submissions with no audit trail. - The picked reason flows into the auto-generated problem note so the operator sees "[Reason] driver note" in PB. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
2026-08-02 20:58:08 +00:00
ref={notesButtonRef}
onClick={(e) => {
e.stopPropagation();
measureNotesPlacement();
setShowNotesTooltip(v => !v);
setShowPoTooltip(false);
}}
aria-label="Show notes"
aria-expanded={showNotesTooltip}
className="w-6 h-6 rounded-md flex items-center justify-center bg-[#8a5a02] text-white active:bg-[#6b4501] transition-colors"
>
<MessageSquare size={11} aria-hidden="true" />
</button>
{/* Notes tooltip popover */}
{showNotesTooltip && (
<div
ref={notesTooltipRef}
feat: popover stays above footer, partial collections must be justified Two improvements to the collection flow: 1. Popover clipping under BottomNav (last item in list) - Root cause: popovers were z-[60] *inside* a parent stacking context at z-30, so their effective page z-index was 30 — below BottomNav's z-50. Even if the z-index were right, the popover extends below the trigger button and could fall below the viewport on the last item in a long list. - Add lib/hooks.ts with usePopoverPlacement: measures the trigger's bottom edge and flips popover direction to "up" when there isn't ~240px of room below (reserving 80px for BottomNav + safe-area inset). - Bump the article/section/li stacking context from z-30 to z-[80] when a popover is open so the popover paints above BottomNav (z-50). Covers both compact-mode SupplierGroup rows and detailed-mode CollectionItemCard via the existing onPopoverChange callback. - For SupplierGroup: hooks live at the top level (record keyed by item.id) to avoid the Rules of Hooks trap of declaring hooks inside items.map(). 2. Partial collections must be justified - In CollectModal, when received qty < expected, require EITHER a reason (dropdown: out of stock / wrong item / quality issue / damaged / wrong qty packed / other) OR a photo before Confirm is enabled. Prevents silent partial submissions with no audit trail. - The picked reason flows into the auto-generated problem note so the operator sees "[Reason] driver note" in PB. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
2026-08-02 20:58:08 +00:00
className={cn(
'absolute right-0 z-[60] w-64 bg-white border border-[rgba(38,37,30,0.15)] rounded-xl shadow-lg p-3',
notesPlacement === 'down' ? 'top-full mt-1' : 'bottom-full mb-1'
)}
>
<div className="flex items-center justify-between mb-2">
<p className="text-[11px] font-semibold text-[#26251e] uppercase tracking-wide">Buyer Notes</p>
<button
onClick={(e) => { e.stopPropagation(); setShowNotesTooltip(false); }}
aria-label="Close"
className="w-5 h-5 rounded flex items-center justify-center hover:bg-[rgba(38,37,30,0.08)]"
>
<span className="text-[rgba(38,37,30,0.4)] text-sm">×</span>
</button>
</div>
{hasNotes ? (
<>
<p className="text-[11px] text-[#8a5a02] bg-[#fcf3d8] border border-[#f6e3a4] rounded-lg p-2 selectable leading-snug">
{item.notes}
</p>
{hasProblemNotes && (
<p className="text-[11px] text-[#8a1b3a] bg-[#fbeaef] border border-[#f3c4d2] rounded-lg p-2 mt-2 selectable leading-snug">
<span className="font-semibold">Problem: </span>{item.problem_notes}
</p>
)}
</>
) : hasProblemNotes ? (
<p className="text-[11px] text-[#8a1b3a] bg-[#fbeaef] border border-[#f3c4d2] rounded-lg p-2 selectable leading-snug">
{item.problem_notes}
</p>
) : (
<p className="text-xs text-[rgba(38,37,30,0.55)] text-center py-1">No notes</p>
)}
</div>
)}
</div>
)}
</div>
</div>
) : (
/* Full/non-compact right side */
<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">
{orderNumber && (
<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">{orderNumber}</dd>
</div>
)}
{poNumber && (
<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">{poNumber}</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="space-y-2 pt-1">
{/* Row 1 — core workflow */}
<div className="grid grid-cols-3 gap-2">
<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>
{/* Row 2 — PO document and buyer notes */}
<div className="grid grid-cols-2 gap-2">
<button
onClick={() => onShowDetails(item)}
aria-label={`View PO document for ${description}`}
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
>
<FileText size={18} strokeWidth={2.5} aria-hidden="true" />
<span className="text-xs font-semibold">View PO</span>
</button>
<button
onClick={() => onShowDetails(item)}
aria-label={`View buyer notes for ${description}`}
className="flex flex-col items-center justify-center gap-1 py-3 rounded-xl bg-[#8a5a02] text-white active:bg-[#6b4501] transition-colors touch-manipulation press-feedback-sm min-h-[56px]"
>
<MessageSquare size={18} strokeWidth={2.5} aria-hidden="true" />
<span className="text-xs font-semibold">Buyer Notes</span>
</button>
</div>
</div>
</div>
</div>
)}
</article>
);
}