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 || "—"}