WFDASHDRIVER-APP/components/PoActions.tsx
Dave de16082ff7 feat: one invoice photo per PO, one-tap PO PDF, save lock, sharper photos
- Confirm Collection locks while saving ("Saving…") and the page drops a
  second collect for an item already in flight. Repeat taps on slow data
  re-ran the save and re-uploaded the photo (7 identical copies on one
  item on 2026-10-16).
- PO headers (and a single-PO supplier's list) get an Invoice button:
  photograph the supplier invoice once per PO, then "add pg 2", "pg 3".
  Pages go onto one customer line of the PO as invoice-<PO>-p<N>.jpg;
  WFDASH Collection Confirm already gathers photos per PO card. The page
  count is also kept on the device so it survives lines dropping off.
- PO button opens the PO PDF in one tap (shared buildPoFileUrl in
  lib/poDocs.ts, also used by the item detail modal).
- Collect modal says whether the PO's invoice is already uploaded, so
  the per-item photo is only for problems with the item.
- Photos now upload at 2000px / q0.8 (was 1200px / q0.7) so small print
  on invoices stays readable for the AI checks.
- Supplier header counts and detailed view now include items in PO
  grouping mode (they showed 0 items before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:43:35 +02:00

120 lines
4 KiB
TypeScript

'use client';
import { useRef, useState } from 'react';
import { Camera, FileText, Loader2, Check } from 'lucide-react';
import { OrderItem, PurchaseOrder } from '@/lib/types';
import { buildPoFileUrl, countInvoicePages, invoicePhotoTarget } from '@/lib/poDocs';
import { cn } from '@/lib/utils';
interface PoActionsProps {
purchaseOrder: PurchaseOrder | null;
items: OrderItem[];
/** Uploads one invoice page for this PO. Rejects on failure. */
onAddInvoicePage: (items: OrderItem[], file: File) => Promise<void>;
/** False hides the photo button (e.g. the caller can't upload). */
canPhoto?: boolean;
className?: string;
}
// The two PO-level buttons on a PO header: open the PO PDF in one tap,
// and photograph the supplier invoice once for the whole PO (with
// "Page 2", "Page 3"… for multi-page invoices).
export default function PoActions({
purchaseOrder,
items,
onAddInvoicePage,
canPhoto: photoAllowed = true,
className,
}: PoActionsProps) {
const inputRef = useRef<HTMLInputElement>(null);
const [uploading, setUploading] = useState(false);
const pdfUrl = buildPoFileUrl(purchaseOrder);
// Stock pickups have nowhere to store a photo, so a PO made only of
// pickups gets no photo button.
const canPhoto = photoAllowed && invoicePhotoTarget(items) !== null;
const pages = countInvoicePages(items);
if (!pdfUrl && !canPhoto) return null;
const handleFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
// Reset so taking the same shot again still fires onChange.
e.target.value = '';
if (!file || uploading) return;
setUploading(true);
try {
await onAddInvoicePage(items, file);
} catch {
// The page handler already told the driver; nothing to add here.
} finally {
setUploading(false);
}
};
const poLabel = purchaseOrder?.po_number ? `PO ${purchaseOrder.po_number}` : 'this PO';
return (
<div className={cn('flex items-center gap-1 flex-shrink-0', className)}>
{pdfUrl && (
<a
href={pdfUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={`Open the PDF for ${poLabel}`}
className="h-7 px-2 rounded-md bg-[#efeaf7] text-[#6c5a99] text-[11px] font-semibold flex items-center gap-1 active:bg-[#ded5ef] transition-colors touch-manipulation"
>
<FileText size={11} aria-hidden="true" />
<span>PO</span>
</a>
)}
{canPhoto && (
<>
<button
type="button"
onClick={() => inputRef.current?.click()}
disabled={uploading}
aria-busy={uploading}
aria-label={
pages === 0
? `Take a photo of the invoice for ${poLabel}`
: `Add page ${pages + 1} of the invoice for ${poLabel}`
}
className={cn(
'h-7 px-2 rounded-md text-[11px] font-semibold flex items-center gap-1 transition-colors touch-manipulation',
pages === 0
? 'bg-[#26251e] text-white active:bg-black'
: 'bg-[#e6f1ec] text-[#125a3e] active:bg-[#cfe6db]',
uploading && 'opacity-60',
)}
>
{uploading ? (
<Loader2 size={11} className="animate-spin" aria-hidden="true" />
) : pages === 0 ? (
<Camera size={11} aria-hidden="true" />
) : (
<Check size={11} aria-hidden="true" />
)}
<span>
{uploading
? 'Uploading…'
: pages === 0
? 'Invoice'
: `${pages} pg · add pg ${pages + 1}`}
</span>
</button>
<input
ref={inputRef}
type="file"
accept="image/*"
capture="environment"
onChange={handleFile}
className="hidden"
aria-hidden="true"
tabIndex={-1}
/>
</>
)}
</div>
);
}