diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index d733f5431..d2bf2ca20 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -124,7 +124,7 @@ export function BillFormComponent({ return (
- + diff --git a/client/src/components/parts-order-modal/parts-order-modal.component.jsx b/client/src/components/parts-order-modal/parts-order-modal.component.jsx index 032559081..edcbd82f4 100644 --- a/client/src/components/parts-order-modal/parts-order-modal.component.jsx +++ b/client/src/components/parts-order-modal/parts-order-modal.component.jsx @@ -58,7 +58,7 @@ export function PartsOrderModalComponent({ return (
- + diff --git a/client/src/components/parts-order-modal/parts-order-modal.container.jsx b/client/src/components/parts-order-modal/parts-order-modal.container.jsx index e60ea5d6e..b909209f5 100644 --- a/client/src/components/parts-order-modal/parts-order-modal.container.jsx +++ b/client/src/components/parts-order-modal/parts-order-modal.container.jsx @@ -80,8 +80,30 @@ export function PartsOrderModalContainer({ const handleFinish = async ({ order_type, removefrompartsqueue, is_quote, ...values }) => { logImEXEvent("parts_order_insert"); setSaving(true); - let insertResult; + // Force job_line_id from context so it never gets dropped by AntD form submission behavior. + const submittedLines = values?.parts_order_lines?.data ?? []; + const forcedLines = submittedLines.map((p, index) => { + const originalLine = linesToOrder?.[index]; + const jobLineId = isReturn ? originalLine?.joblineid : originalLine?.id; + + return { + ...p, + job_line_id: jobLineId + }; + }); + + const missingIdx = forcedLines.findIndex((l) => !l?.job_line_id); + if (missingIdx !== -1) { + notification.error({ + title: t("parts_orders.errors.creating"), + description: `Missing job_line_id for parts line #${missingIdx + 1}` + }); + setSaving(false); + return; + } + + let insertResult; try { insertResult = await insertPartOrder({ variables: { @@ -95,12 +117,16 @@ export function PartsOrderModalContainer({ return: isReturn, status: is_quote ? bodyshop.md_order_statuses.default_quote || "Quote" - : bodyshop.md_order_statuses.default_ordered || "Ordered*" + : bodyshop.md_order_statuses.default_ordered || "Ordered*", + + // override nested lines to guarantee linkage + parts_order_lines: { data: forcedLines } } ] }, refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"] }); + if (insertResult.errors) { notification.error({ title: t("parts_orders.errors.creating"), @@ -109,18 +135,20 @@ export function PartsOrderModalContainer({ setSaving(false); return; } - } catch (error) { + } catch (err) { notification.error({ title: t("parts_orders.errors.creating"), - description: error.message + description: err?.message || String(err) }); - console.error("Parts order insert error:", error); + console.error("Parts order insert error:", err); setSaving(false); return; } + notification.success({ - title: values.isReturn ? t("parts_orders.successes.return_created") : t("parts_orders.successes.created") + title: isReturn ? t("parts_orders.successes.return_created") : t("parts_orders.successes.created") }); + insertAuditTrail({ jobid: jobId, operation: isReturn @@ -129,8 +157,8 @@ export function PartsOrderModalContainer({ type: isReturn ? "jobspartsreturn" : "jobspartsorder" }); - // Use linesToOrder from context instead of form values to preserve job_line_id - const jobLineIds = linesToOrder + // Use linesToOrder from context instead of form values to preserve job line ids + const jobLineIds = (linesToOrder ?? []) .filter((line) => (isReturn ? line.joblineid : line.id)) .map((line) => (isReturn ? line.joblineid : line.id)); @@ -152,12 +180,12 @@ export function PartsOrderModalContainer({ description: JSON.stringify(jobLinesResult.errors) }); } - } catch (error) { + } catch (err) { notification.error({ title: t("parts_orders.errors.updating_status"), - description: error.message + description: err?.message || String(err) }); - console.error("Job lines update error:", error); + console.error("Job lines update error:", err); } if (!isReturn && removefrompartsqueue) { @@ -170,13 +198,13 @@ export function PartsOrderModalContainer({ } } }); - } catch (error) { - console.error("Update job queue error:", error); + } catch (err) { + console.error("Update job queue error:", err); } } if (values.vendorid === bodyshop.inhousevendorid) { - // Use linesToOrder for job_line_id, merge with form values for other fields + // Use linesToOrder for joblineid, and use forcedLines for the user-entered fields setBillEnterContext({ actions: { refetch: refetch }, context: { @@ -188,12 +216,12 @@ export function PartsOrderModalContainer({ isinhouse: true, date: dayjs(), total: 0, - billlines: values.parts_order_lines.data.map((p, index) => { - const originalLine = linesToOrder[index]; + billlines: forcedLines.map((p, index) => { + const originalLine = linesToOrder?.[index]; return { joblineid: isReturn ? originalLine?.joblineid : originalLine?.id, actual_price: p.act_price, - actual_cost: 0, //p.act_price, + actual_cost: 0, // p.act_price, line_desc: p.line_desc, line_remarks: p.line_remarks, part_type: p.part_type, @@ -208,6 +236,8 @@ export function PartsOrderModalContainer({ } } }); + + setSaving(false); toggleModalVisible(); return; } @@ -217,9 +247,8 @@ export function PartsOrderModalContainer({ const Templates = TemplateList("partsorder", context); if (sendType === "e") { - const matchingVendor = data.vendors.filter((item) => item.id === values.vendorid)[0]; - - let vendorEmails = matchingVendor?.email && matchingVendor.email.split(RegExp("[;,]")); + const matchingVendor = data?.vendors?.find((item) => item.id === values.vendorid); + const vendorEmails = matchingVendor?.email && matchingVendor.email.split(RegExp("[;,]")); GenerateDocument( { @@ -263,7 +292,7 @@ export function PartsOrderModalContainer({ notification ); } else if (sendType === "oec") { - //Send to Partner OEC. + // Send to Partner OEC. try { const partsOrder = await client.query({ query: QUERY_PARTS_ORDER_OEC, @@ -271,26 +300,21 @@ export function PartsOrderModalContainer({ id: insertResult.data.insert_parts_orders.returning[0].id } }); + let po; - //Massage the data based on the split. Should they be able to overwrite OEC pricing? + // Massage the data based on the split. Should they be able to overwrite OEC pricing? if (OEConnection_PriceChange.treatment === "on") { - //Set the flag to include the override. po = _.cloneDeep(partsOrder.data.parts_orders_by_pk); po.parts_order_lines.forEach((pol) => { pol.priceChange = true; }); } - const oecResponse = await axios.post( - "http://localhost:1337/oec/", - - po || partsOrder.data.parts_orders_by_pk, - { - headers: { - Authorization: `Bearer ${await auth.currentUser.getIdToken()}` - } + const oecResponse = await axios.post("http://localhost:1337/oec/", po || partsOrder.data.parts_orders_by_pk, { + headers: { + Authorization: `Bearer ${await auth.currentUser.getIdToken()}` } - ); + }); if (oecResponse.data && oecResponse.data.success === false) { notification.error({ @@ -299,17 +323,18 @@ export function PartsOrderModalContainer({ }) }); } - } catch (error) { - console.log("Error OEC.", error); + } catch (err) { + console.log("Error OEC.", err); notification.error({ title: t("parts_orders.errors.oec", { - error: JSON.stringify(error.message) + error: JSON.stringify(err?.message || String(err)) }) }); setSaving(false); return; } } + setSaving(false); toggleModalVisible(); }; @@ -320,7 +345,6 @@ export function PartsOrderModalContainer({ deliver_by: isReturn ? dayjs(new Date()) : null, vendorid: vendorId, returnfrombill: returnFromBill, - parts_order_lines: { data: linesToOrder ? linesToOrder.reduce((acc, value) => { diff --git a/client/src/components/parts-receive-modal/parts-receive-modal.component.jsx b/client/src/components/parts-receive-modal/parts-receive-modal.component.jsx index 4e6165344..3133fbe93 100644 --- a/client/src/components/parts-receive-modal/parts-receive-modal.component.jsx +++ b/client/src/components/parts-receive-modal/parts-receive-modal.component.jsx @@ -46,10 +46,10 @@ export function PartsReceiveModalComponent({ bodyshop, form }) { {fields.map((field, index) => (
- + - + diff --git a/client/src/components/parts-receive-modal/parts-receive-modal.container.jsx b/client/src/components/parts-receive-modal/parts-receive-modal.container.jsx index 1de8feb68..a2d7ae870 100644 --- a/client/src/components/parts-receive-modal/parts-receive-modal.container.jsx +++ b/client/src/components/parts-receive-modal/parts-receive-modal.container.jsx @@ -37,39 +37,85 @@ export function PartsReceiveModalContainer({ partsReceiveModal, toggleModalVisib const handleFinish = async (values) => { logImEXEvent("parts_order_receive"); setLoading(true); - const result = await Promise.all( - values.partsorderlines.map((li) => { - return receivePartsLine({ - variables: { - lineId: li.joblineid, - line: { - location: li.location, - status: bodyshop.md_order_statuses.default_received || "Received*" - }, - orderLineId: li.id, - orderLine: { - status: bodyshop.md_order_statuses.default_received || "Received*" - } - } - }); - }) - ); - result.forEach((jobLinesResult) => { - if (jobLinesResult.errors) { + try { + const submittedLines = values.partsorderlines ?? []; + + // Preserve ids from modal context, merge editable fields from form submission (e.g. location) + const mergedLines = (partsorderlines ?? []).map((ctxLine, idx) => ({ + ...ctxLine, + ...submittedLines[idx] + })); + + // Optional: hard guard to catch the exact failure early with a better message + const missing = mergedLines + .map((l, idx) => ({ + idx, + orderLineId: l?.id, + jobLineId: l?.joblineid // adjust if your ctx uses job_line_id instead + })) + .filter((x) => !x.orderLineId || !x.jobLineId); + + if (missing.length) { notification.error({ title: t("parts_orders.errors.creating"), - description: JSON.stringify(jobLinesResult.errors) + description: `Missing required ids for lines: ${missing + .map((m) => `#${m.idx + 1} (orderLineId=${m.orderLineId}, jobLineId=${m.jobLineId})`) + .join(", ")}` }); + return; } - }); - notification.success({ - title: t("parts_orders.successes.received") - }); - setLoading(false); - if (refetch) refetch(); - toggleModalVisible(); + const results = await Promise.allSettled( + mergedLines.map((li) => + receivePartsLine({ + variables: { + lineId: li.joblineid, + line: { + location: li.location, + status: bodyshop.md_order_statuses.default_received || "Received*" + }, + orderLineId: li.id, + orderLine: { + status: bodyshop.md_order_statuses.default_received || "Received*" + } + }, + // Ensures GraphQL errors come back on the result when possible (instead of only throwing) + errorPolicy: "all" + }) + ) + ); + + const errors = []; + results.forEach((r, idx) => { + if (r.status === "rejected") { + errors.push({ idx, message: r.reason?.message ?? String(r.reason) }); + return; + } + if (r.value?.errors?.length) { + errors.push({ + idx, + message: r.value.errors.map((e) => e.message).join(" | ") + }); + } + }); + + if (errors.length) { + errors.forEach((e) => + notification.error({ + title: t("parts_orders.errors.creating"), + description: `Line ${e.idx + 1}: ${e.message}` + }) + ); + return; // keep modal open so user can retry + } + + notification.success({ title: t("parts_orders.successes.received") }); + if (refetch) refetch(); + toggleModalVisible(); + } finally { + setLoading(false); + } }; const initialValues = { diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx index c8f44a81b..a947f7d1c 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx @@ -1500,7 +1500,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { - +