- 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>
120 lines
4 KiB
TypeScript
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>
|
|
);
|
|
}
|