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 */}