fix(orders): seed Send PO body with this PO's line count, not the order's

This commit is contained in:
SJCoetz 2026-07-09 22:43:52 +02:00
parent 44db71e6d7
commit dfe52a62d9

View file

@ -103,6 +103,11 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
const [sendBcc, setSendBcc] = useState("");
const [sendSubject, setSendSubject] = useState("");
const [sendMessage, setSendMessage] = useState("");
// True while the modal is seeding the default body. We have to wait
// for the PO-level line count to come back from PB before we can
// build a non-misleading body, so the textarea is disabled until
// the count is resolved.
const [sendMessageLoading, setSendMessageLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [deleteOpen, setDeleteOpen] = useState(false);
@ -269,32 +274,64 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
// enough that they can hit Send immediately. Seeding from the same
// source guarantees the previewed text matches what the supplier
// actually receives.
const openSendPo = (po: PurchaseOrder) => {
//
// The line count must come from THIS PO, not from the parent order
// (an order can have multiple POs with disjoint line sets). The
// `items` state on this page is order-level, so we fetch the PO's
// items on demand here. The textarea + Send button stay disabled
// until the count resolves, so the operator never sees a body that
// promises a different line count than the supplier will receive.
const openSendPo = async (po: PurchaseOrder) => {
setSendPo(po);
setSendTo(po.expand?.supplier?.email || "");
setSendCc("");
setSendBcc("");
setSendSubject(`Purchase Order ${po.po_number}`);
// Seed the message textarea with the supplier-personalised default.
// We deliberately pass ONLY the supplier-facing context (PO
// number, supplier name, line count). End-customer context is
// not surfaced here even if we have it — see the confidentiality
// note in `po-email-template.ts`. Operators wanting to add
// specifics can still type over the textarea before sending.
//
// The textarea is plain-text (operator can edit freely), so we
// seed it with the plain-text form of the default body. The
// route converts it to HTML at send time via the operator's
// current text — operators who want to preserve the rich
// formatting should not edit it; operators who want to type
// their own message get a clean single-paragraph wrap.
setSendMessage(
buildDefaultPoEmailBody({
poNumber: po.po_number || po.id,
supplierName: po.expand?.supplier?.suppliername || "",
lineCount: items.length,
}).text
);
setSendMessage("");
setSendMessageLoading(true);
try {
const poItems = await getPocketBase()
.collection("order_items")
.getFullList<OrderItem>({
filter: `purchase_order = "${po.id}"`,
fields: "id",
});
// Seed the message textarea with the supplier-personalised default.
// We deliberately pass ONLY the supplier-facing context (PO
// number, supplier name, line count). End-customer context is
// not surfaced here even if we have it — see the confidentiality
// note in `po-email-template.ts`. Operators wanting to add
// specifics can still type over the textarea before sending.
//
// The textarea is plain-text (operator can edit freely), so we
// seed it with the plain-text form of the default body. The
// route converts it to HTML at send time via the operator's
// current text — operators who want to preserve the rich
// formatting should not edit it; operators who want to type
// their own message get a clean single-paragraph wrap.
setSendMessage(
buildDefaultPoEmailBody({
poNumber: po.po_number || po.id,
supplierName: po.expand?.supplier?.suppliername || "",
lineCount: poItems.length,
}).text
);
} catch {
// Fall back to a generic body if the count lookup fails. The
// route still uses its own (correct) count at send time, so
// the worst case is the operator sees a slightly misleading
// preview — not a wrong email.
setSendMessage(
buildDefaultPoEmailBody({
poNumber: po.po_number || po.id,
supplierName: po.expand?.supplier?.suppliername || "",
lineCount: 0,
}).text
);
toast.error("Couldn't load line count for preview; check the PO before sending.");
} finally {
setSendMessageLoading(false);
}
};
const submitSendPo = async () => {
@ -988,7 +1025,10 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
<Dialog
open={sendPo !== null}
onOpenChange={(open) => {
if (!open && !sendBusy) setSendPo(null);
if (!open && !sendBusy) {
setSendPo(null);
setSendMessageLoading(false);
}
}}
>
<DialogContent>
@ -1052,9 +1092,12 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
placeholder="Hi Supplier, please see attached PO. Confirm receipt and ETA. Thanks — WFDash Orders"
rows={5}
maxLength={2000}
disabled={sendMessageLoading}
/>
<p className="text-xs text-muted-foreground">
{sendMessage.length}/2000
{sendMessageLoading
? "Loading line count for preview…"
: `${sendMessage.length}/2000`}
</p>
</div>
{sendPo?.status === "mail_failed" && sendPo?.email_error && (
@ -1074,7 +1117,12 @@ export default function OrderDetailPage({ params }: { params: Promise<{ id: stri
</Button>
<Button
onClick={submitSendPo}
disabled={sendBusy || !sendTo.trim() || !sendSubject.trim()}
disabled={
sendBusy ||
sendMessageLoading ||
!sendTo.trim() ||
!sendSubject.trim()
}
>
{sendBusy && <Loader2 className="mr-1.5 h-4 w-4 animate-spin" />}
<Send className="mr-1.5 h-4 w-4" />