feat(orders): cascade company and rep from selected customer in review modal
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 <noreply@anthropic.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
d3c3e3908d
commit
f99ab7cf1a
1 changed files with 65 additions and 3 deletions
|
|
@ -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<Record<string, ItemDraft>>(
|
||||
() =>
|
||||
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({
|
|||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Company</Label>
|
||||
<p className="h-9 inline-flex items-center px-3 rounded-md border bg-muted/30 text-sm">
|
||||
{company?.company || "(unknown)"}
|
||||
{cascadeCompanyName || company?.company || "(unknown)"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Rep</Label>
|
||||
<p className="h-9 inline-flex items-center px-3 rounded-md border bg-muted/30 text-sm">
|
||||
{persistedOrder.rep || "—"}
|
||||
{cascadeRepName || persistedOrder.rep || "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue