- Removed the Supplier/Invoice/Area and Compact/Detailed switches; the list is always compact and grouped supplier → PO. Six layouts behind small icons confused drivers. - POs start open, so opening a supplier shows its items straight away. - PO header: the full PO number and item count on line one; the PO / Invoice / Collect all / flag buttons on line two (bigger, 36px) only while the PO is open. The number used to be squeezed to "PO2026…" or hidden by the buttons. - Collect all / flag-all only show when the PO has more than one item. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
4.6 KiB
TypeScript
135 lines
4.6 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;
|
|
/** 'lg' = full-width stacked buttons, for the PO bottom sheet. */
|
|
size?: 'sm' | 'lg';
|
|
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,
|
|
size = 'sm',
|
|
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';
|
|
const lg = size === 'lg';
|
|
const btn = lg
|
|
? 'h-12 w-full rounded-xl text-[15px] font-semibold flex items-center justify-center gap-2 transition-colors touch-manipulation'
|
|
: 'h-9 px-3 rounded-lg text-[13px] font-semibold flex items-center gap-1.5 transition-colors touch-manipulation';
|
|
const icon = lg ? 18 : 14;
|
|
|
|
return (
|
|
<div className={cn(lg ? 'flex flex-col gap-2' : '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={cn(
|
|
btn,
|
|
lg ? 'bg-[#26251e] text-white active:bg-black' : 'bg-[#efeaf7] text-[#6c5a99] active:bg-[#ded5ef]',
|
|
)}
|
|
>
|
|
<FileText size={icon} aria-hidden="true" />
|
|
<span>{lg ? 'Open PO PDF' : '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(
|
|
btn,
|
|
pages > 0
|
|
? 'bg-[#e6f1ec] text-[#125a3e] active:bg-[#cfe6db]'
|
|
: lg
|
|
? 'bg-white text-[#26251e] border border-[rgba(38,37,30,0.2)] active:bg-[#f2f1ed]'
|
|
: 'bg-[#26251e] text-white active:bg-black',
|
|
uploading && 'opacity-60',
|
|
)}
|
|
>
|
|
{uploading ? (
|
|
<Loader2 size={icon} className="animate-spin" aria-hidden="true" />
|
|
) : pages === 0 ? (
|
|
<Camera size={icon} aria-hidden="true" />
|
|
) : (
|
|
<Check size={icon} aria-hidden="true" />
|
|
)}
|
|
<span>
|
|
{uploading
|
|
? 'Uploading…'
|
|
: pages === 0
|
|
? lg ? 'Invoice photo' : 'Invoice'
|
|
: lg
|
|
? `Invoice: ${pages} page${pages === 1 ? '' : 's'} · add page ${pages + 1}`
|
|
: `${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>
|
|
);
|
|
}
|