From 105f169e459b54ce259f21d0a6e6a4280edd2023 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Sun, 2 Aug 2026 22:58:08 +0200 Subject: [PATCH] feat: popover stays above footer, partial collections must be justified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: Orca --- components/AreaGroup.tsx | 11 ++--- components/CollectModal.tsx | 75 +++++++++++++++++++++++++++++-- components/CollectionItemCard.tsx | 46 ++++++++++++++++--- components/SupplierGroup.tsx | 61 ++++++++++++++++++------- lib/hooks.ts | 46 +++++++++++++++++++ 5 files changed, 211 insertions(+), 28 deletions(-) create mode 100644 lib/hooks.ts diff --git a/components/AreaGroup.tsx b/components/AreaGroup.tsx index 22b3054..f0495e4 100644 --- a/components/AreaGroup.tsx +++ b/components/AreaGroup.tsx @@ -85,11 +85,12 @@ export default function AreaGroup({ color.light, `border-[rgba(38,37,30,0.1)]`, `ring-1 ${color.ring}`, - // Lift to z-30 when a popover is open so it can escape this - // section's stacking context and paint above the next area - // group. Sibling sections otherwise share z-10 and render in - // document order, which would hide a popover behind the next. - childPopoverOpen ? 'z-30' : 'z-10' + // z-[80] (above BottomNav's z-50) when a card popover is open + // so the popover can paint above the bottom chrome. The + // popover's own z-[60] is *inside* this section's stacking + // context, so without this lift the popover would be hidden + // behind the BottomNav. + childPopoverOpen ? 'z-[80]' : 'z-10' )} aria-label={`${label} area group`} > diff --git a/components/CollectModal.tsx b/components/CollectModal.tsx index e445edd..36bb149 100644 --- a/components/CollectModal.tsx +++ b/components/CollectModal.tsx @@ -4,7 +4,7 @@ import { useState, useRef } from 'react'; import { OrderItem } from '@/lib/types'; import { cn, formatCurrency } from '@/lib/utils'; import { getItemDisplayDescription, getItemDisplayQty } from '@/lib/pocketbase'; -import { X, Minus, Plus, Check, Camera, ImagePlus, Trash2 } from 'lucide-react'; +import { X, Minus, Plus, Check, Camera, ImagePlus, Trash2, AlertCircle } from 'lucide-react'; interface CollectModalProps { item: OrderItem | null; @@ -30,6 +30,12 @@ export default function CollectModal({ const [error, setError] = useState(''); const [photo, setPhoto] = useState(null); const [photoPreviewUrl, setPhotoPreviewUrl] = useState(null); + // Reason the driver is collecting less than the expected qty. We + // require *something* on a partial — either a picked reason or a + // photo — so a partial collection can't be silently submitted with + // no audit trail. Over-collections still pass through with just + // notes (the driver may genuinely have nothing visual to add). + const [partialReason, setPartialReason] = useState(''); const cameraInputRef = useRef(null); const libraryInputRef = useRef(null); @@ -39,6 +45,8 @@ export default function CollectModal({ const parsedQty = parseFloat(receivedQty) || 0; const isPartial = parsedQty > 0 && parsedQty < expectedQty; const isOverQty = parsedQty > expectedQty; + // True once the driver has justified the partial one way or another. + const partialJustified = partialReason.trim() !== '' || photo !== null; const displayDescription = getItemDisplayDescription(item); const handlePhotoSelected = (file: File | undefined | null) => { @@ -73,7 +81,23 @@ export default function CollectModal({ return; } - onConfirm(item, qty, notes, photo); + // Partial collections must be justified — either a picked reason + // or a photo. Without one we refuse to submit so the operator + // always has audit context for a short pickup. + if (isPartial && !partialJustified) { + setError('Please tell us why the quantity is short, or take a photo to continue'); + return; + } + + // Prefix the driver's free-text notes with the structured reason + // when provided, so the operator sees both in the problem note + // without losing the original wording. The reason goes first + // because that's the most actionable piece. + const finalNotes = isPartial && partialReason + ? `[${partialReason}] ${notes}`.trim() + : notes; + + onConfirm(item, qty, finalNotes, photo); }; const adjustQty = (delta: number) => { @@ -191,6 +215,50 @@ export default function CollectModal({ )} + {/* Partial-collection justification. Only shown when the + driver enters a qty less than expected. We require EITHER + a picked reason OR a photo so the operator always has + some audit context for the short pickup. The reason + flows through into the auto-generated problem note. */} + {isPartial && ( +
+
+
+ + + {!partialReason && !photo && ( +

+ Required: pick a reason or attach a photo below to continue. +

+ )} +
+ )} + {/* Notes */}