WFDASHDRIVER-APP/components/ItemDetailModal.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

296 lines
10 KiB
TypeScript

'use client';
import { OrderItem, PurchaseOrder } from '@/lib/types';
import { buildPoFileUrl } from '@/lib/poDocs';
import {
X,
FileText,
Share2,
Mail,
Check,
AlertCircle,
Copy,
ExternalLink,
} from 'lucide-react';
import { useState } from 'react';
interface ItemDetailModalProps {
item: OrderItem;
purchaseOrder: PurchaseOrder | null;
isOpen: boolean;
onClose: () => void;
}
function statusColor(status: string): string {
switch (status.toLowerCase()) {
case 'sent':
case 'approved':
return 'bg-[#e6f1ec] text-[#125a3e]';
case 'pending':
return 'bg-[#fcf3d8] text-[#8a5a02]';
case 'cancelled':
return 'bg-[#fbeaef] text-[#8a1b3a]';
default:
return 'bg-[#ebeae5] text-[#26251e]';
}
}
export default function ItemDetailModal({
item,
purchaseOrder,
isOpen,
onClose,
}: ItemDetailModalProps) {
const [copied, setCopied] = useState(false);
const [sharing, setSharing] = useState(false);
if (!isOpen) return null;
const poUrl = purchaseOrder ? buildPoFileUrl(purchaseOrder) : null;
const supplier = item.expand?.supplier;
const hasNotes = Boolean(item.notes?.trim());
const handleShare = async () => {
if (!poUrl || !purchaseOrder) return;
setSharing(true);
try {
if (navigator.share) {
await navigator.share({
title: `Purchase Order ${purchaseOrder.po_number}`,
text: `Purchase Order ${purchaseOrder.po_number} for ${supplier?.suppliername ?? 'your order'}`,
url: poUrl,
});
} else {
await navigator.clipboard.writeText(poUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2500);
}
} catch (err) {
if ((err as Error).name !== 'AbortError') {
console.error('Share failed:', err);
}
} finally {
setSharing(false);
}
};
const handleEmail = () => {
if (!purchaseOrder?.email_payload) return;
const { subject, bodyText } = purchaseOrder.email_payload;
const bodyWithLink = poUrl ? `${bodyText}\n\nPDF Download: ${poUrl}` : bodyText;
const mailto = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(bodyWithLink)}`;
window.location.href = mailto;
};
const handleCopyLink = async () => {
if (!poUrl) return;
try {
await navigator.clipboard.writeText(poUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2500);
} catch {
// clipboard may be blocked
}
};
return (
<div
className="fixed inset-0 z-[80] flex items-end sm:items-center justify-center"
role="dialog"
aria-modal="true"
aria-label="Item details"
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/30 backdrop-blur-sm"
onClick={onClose}
aria-hidden="true"
/>
{/* Sheet */}
<div className="relative w-full max-w-lg bg-[#ebeae5] rounded-t-2xl sm:rounded-2xl shadow-xl flex flex-col max-h-[90dvh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-[rgba(38,37,30,0.1)]">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-[#1f8a65] flex items-center justify-center">
<FileText size={18} className="text-white" aria-hidden="true" />
</div>
<div>
<h2 className="text-[15px] font-semibold text-[#26251e] leading-tight">
{purchaseOrder?.po_number ?? 'No PO'}
</h2>
{supplier && (
<p className="text-[11px] text-[rgba(38,37,30,0.55)] leading-tight mt-0.5">
{supplier.suppliername}
</p>
)}
</div>
</div>
<button
onClick={onClose}
aria-label="Close details"
className="w-9 h-9 rounded-xl flex items-center justify-center active:bg-[rgba(38,37,30,0.08)] transition-colors"
>
<X size={20} className="text-[#26251e]" aria-hidden="true" />
</button>
</div>
{/* Scrollable body */}
<div className="overflow-y-auto flex-1 p-5 space-y-5">
{/* PO summary grid */}
<div className="grid grid-cols-2 gap-3">
{purchaseOrder?.status && (
<div className="bg-[#f2f1ed] rounded-xl p-3">
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide mb-1">
Status
</p>
<span
className={`inline-block text-xs font-semibold px-2 py-0.5 rounded-lg ${statusColor(
purchaseOrder.status
)}`}
>
{purchaseOrder.status}
</span>
</div>
)}
{purchaseOrder?.total_amount != null && (
<div className="bg-[#f2f1ed] rounded-xl p-3">
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide mb-1">
PO Total
</p>
<p className="text-[14px] font-semibold text-[#26251e]">
R{(purchaseOrder.total_amount / 100).toFixed(2)}
</p>
</div>
)}
{item.expand?.order?.order_number && (
<div className="bg-[#f2f1ed] rounded-xl p-3">
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide mb-1">
Order Ref
</p>
<p className="text-[13px] font-medium text-[#26251e] leading-tight">
{item.expand.order.order_number}
</p>
</div>
)}
{supplier?.supplieraddress && (
<div className="bg-[#f2f1ed] rounded-xl p-3 col-span-2">
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide mb-1">
Supplier Address
</p>
<p className="text-[13px] font-medium text-[#26251e] leading-snug selectable">
{supplier.supplieraddress}
</p>
</div>
)}
</div>
{/* PO Document actions */}
{poUrl ? (
<div className="space-y-3">
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide">
Attached Document
</p>
<div className="flex items-center gap-2">
<a
href={poUrl}
target="_blank"
rel="noopener noreferrer"
className="flex-1 flex items-center justify-center gap-2 py-3 rounded-xl bg-[#26251e] text-white active:bg-[#3a3832] transition-colors touch-manipulation press-feedback-sm text-[13px] font-semibold"
>
<ExternalLink size={15} aria-hidden="true" />
View / Download PDF
</a>
</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={handleShare}
disabled={sharing}
className="flex items-center justify-center gap-2 py-3 rounded-xl bg-[#1f8a65] text-white active:bg-[#125a3e] transition-colors touch-manipulation press-feedback-sm text-[13px] font-semibold disabled:opacity-60"
>
{copied ? (
<>
<Check size={15} aria-hidden="true" />
Copied!
</>
) : (
<>
<Share2 size={15} aria-hidden="true" />
Share PDF
</>
)}
</button>
{purchaseOrder?.email_payload ? (
<button
onClick={handleEmail}
className="flex items-center justify-center gap-2 py-3 rounded-xl bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors touch-manipulation press-feedback-sm text-[13px] font-semibold"
>
<Mail size={15} aria-hidden="true" />
Email PO
</button>
) : (
<button
onClick={handleCopyLink}
className="flex items-center justify-center gap-2 py-3 rounded-xl bg-[#6c5a99] text-white active:bg-[#4a3d6b] transition-colors touch-manipulation press-feedback-sm text-[13px] font-semibold"
>
<Copy size={15} aria-hidden="true" />
Copy Link
</button>
)}
</div>
{copied && (
<p className="text-[11px] text-center text-[#125a3e] font-medium">
Link copied to clipboard
</p>
)}
</div>
) : (
<div className="flex items-center gap-2 py-3 px-4 rounded-xl bg-[#fcf3d8] border border-[#f6e3a4]">
<AlertCircle size={15} className="text-[#8a5a02] flex-shrink-0" />
<p className="text-[12px] text-[#8a5a02]">No document attached to this PO</p>
</div>
)}
{/* Buyer Notes */}
{hasNotes && (
<div className="space-y-2">
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide">
Buyer Notes
</p>
<div className="bg-[#fcf3d8] border border-[#f6e3a4] rounded-xl p-4">
<p className="text-[13px] text-[#8a5a02] leading-relaxed selectable">
{item.notes}
</p>
</div>
</div>
)}
{/* Collection / problem notes */}
{item.problem_notes && (
<div className="space-y-2">
<p className="text-[10px] text-[rgba(38,37,30,0.55)] uppercase tracking-wide">
Collection Notes
</p>
<div className="bg-[#fbeaef] border border-[#f3c4d2] rounded-xl p-4">
<p className="text-[13px] text-[#8a1b3a] leading-relaxed selectable">
{item.problem_notes}
</p>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="p-5 pt-3">
<button
onClick={onClose}
className="w-full py-3.5 rounded-xl bg-[#26251e] text-white active:bg-[#3a3832] transition-colors touch-manipulation press-feedback-sm text-[14px] font-semibold"
>
Close
</button>
</div>
</div>
</div>
);
}