From f99ab7cf1a75da76b61e1e9070469787a2dcd101 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Fri, 31 Jul 2026 15:30:31 +0200 Subject: [PATCH] feat(orders): cascade company and rep from selected customer in review modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the operator changes the customer in the Review Order modal, the Company and Rep labels now reactively reflect the selected customer's expand.company.company name and expand.rep.name (with email fallback). On save, the modal writes the new company relation id and the new rep display name to the order — so a customer change actually re-parents the order rather than just changing the display. Guards in the save body prevent two PB errors: - company is skipped when the customer is cleared (orders.company is required, sending '' would 400) - rep is skipped when the new customer has no rep, so existing rep values are preserved The cascade uses useMemo derived from customerId, not a useEffect, so the label updates in the same render as the dropdown change. Co-Authored-By: Claude Co-authored-by: Orca --- src/components/orders-review-modal.tsx | 68 ++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/src/components/orders-review-modal.tsx b/src/components/orders-review-modal.tsx index ffa98c3..0b55c48 100644 --- a/src/components/orders-review-modal.tsx +++ b/src/components/orders-review-modal.tsx @@ -135,6 +135,23 @@ export function OrdersReviewModal({ const [persistedItems, setPersistedItems] = useState(items); const [orderNumber, setOrderNumber] = useState(order.order_number); const [customerId, setCustomerId] = useState(order.customer); + + // Customer-cascade: when the operator changes the customer, the + // Company + Rep labels are derived from the selected customer's + // `expand.company` and `expand.rep` relations. The selected + // customer's `company` id is also written back to the order on save + // (along with the rep's display name). useMemo here — not useEffect + // — so the label updates during the same render as the dropdown + // change, with no extra renders. + const selectedCustomer = useMemo( + () => customers.find((c) => c.id === customerId) ?? null, + [customers, customerId], + ); + const cascadeCompanyName = selectedCustomer?.expand?.company?.company ?? ""; + const cascadeRepName = + selectedCustomer?.expand?.rep?.name ?? + selectedCustomer?.expand?.rep?.email ?? + ""; const [itemDrafts, setItemDrafts] = useState>( () => Object.fromEntries( @@ -203,6 +220,21 @@ export function OrdersReviewModal({ const dirty = useMemo(() => { if (orderNumber !== persistedOrder.order_number) return true; if (customerId !== persistedOrder.customer) return true; + // Company cascades from the selected customer; the modal only + // writes it when the new value differs from the persisted one + // (matching the save-body guard below). + if ( + selectedCustomer?.company && + selectedCustomer.company !== persistedOrder.company + ) + return true; + // Rep cascades as the rep's display name; only considered dirty + // when we have a new name AND it differs from the persisted text. + if ( + cascadeRepName && + cascadeRepName !== (persistedOrder.rep ?? "") + ) + return true; for (const it of persistedItems) { const d = itemDrafts[it.id]; if (!d) continue; @@ -230,7 +262,16 @@ export function OrdersReviewModal({ } return false; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [orderNumber, customerId, itemDrafts, persistedOrder.id]); + }, [ + orderNumber, + customerId, + selectedCustomer?.company, + cascadeRepName, + itemDrafts, + persistedOrder.id, + persistedOrder.company, + persistedOrder.rep, + ]); // Header totals — read from the live draft state so the totals // update as the operator edits. New rows are included with whatever @@ -343,6 +384,8 @@ export function OrdersReviewModal({ const body: { order_number?: string; customer?: string; + company?: string; + rep?: string; items?: Array<{ id: string; quantity?: number | null; @@ -361,6 +404,25 @@ export function OrdersReviewModal({ if (orderNumber !== persistedOrder.order_number) body.order_number = orderNumber; if (customerId !== persistedOrder.customer) body.customer = customerId; + // Cascade company from the selected customer. Skipped when the + // customer is cleared (we'd then be sending `company: ""` which PB + // would 400 on because the orders.company field is required). + if ( + selectedCustomer?.company && + selectedCustomer.company !== persistedOrder.company + ) { + body.company = selectedCustomer.company; + } + // Cascade rep from the selected customer's expand.rep. Skipped + // when no rep on the new customer (so an order that previously + // had a rep keeps it). + if ( + selectedCustomer?.expand?.rep && + cascadeRepName && + cascadeRepName !== (persistedOrder.rep ?? "") + ) { + body.rep = cascadeRepName; + } const dirtyItems = persistedItems .map((it) => { @@ -685,14 +747,14 @@ export function OrdersReviewModal({

- {company?.company || "(unknown)"} + {cascadeCompanyName || company?.company || "(unknown)"}

- {persistedOrder.rep || "—"} + {cascadeRepName || persistedOrder.rep || "—"}