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/job-employee-assignments/job-employee-assignments.component.jsx b/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx index d0b590cf1..44538e12c 100644 --- a/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx +++ b/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx @@ -15,7 +15,20 @@ const mapStateToProps = createStructuredSelector({ }); const mapDispatchToProps = () => ({}); -const iconStyle = { marginLeft: ".3rem" }; +const iconStyle = { + marginLeft: ".3rem" +}; + +const iconClickableStyle = { + marginLeft: ".3rem", + cursor: "pointer" +}; + +const iconDisabledStyle = { + marginLeft: ".3rem", + cursor: "not-allowed", + opacity: 0.5 +}; export function JobEmployeeAssignments({ bodyshop, @@ -58,7 +71,7 @@ export function JobEmployeeAssignments({ const renderAssigner = (operation) => { if (jobRO) { - return ; + return ; } const popContent = ( @@ -117,6 +130,7 @@ export function JobEmployeeAssignments({ { // Prevent the click from being re-interpreted as "outside" e.preventDefault(); @@ -129,9 +143,8 @@ export function JobEmployeeAssignments({ openFor(operation); } }} - style={{ display: "inline-flex", alignItems: "center" }} > - + ); diff --git a/client/src/components/parts-order-modal/parts-order-modal-price-change.component.jsx b/client/src/components/parts-order-modal/parts-order-modal-price-change.component.jsx index d211a02d4..aa6ef3384 100644 --- a/client/src/components/parts-order-modal/parts-order-modal-price-change.component.jsx +++ b/client/src/components/parts-order-modal/parts-order-modal-price-change.component.jsx @@ -34,13 +34,13 @@ export default function PartsOrderModalPriceChange({ form, field }) { { key: "custom", label: ( - e.stopPropagation()} - addonAfter="%" - onKeyUp={(e) => { - if (e.key === "Enter") { - const values = form.getFieldsValue(); - const { parts_order_lines } = values; + + e.stopPropagation()} + onKeyUp={(e) => { + if (e.key === "Enter") { + const values = form.getFieldsValue(); + const { parts_order_lines } = values; form.setFieldsValue({ parts_order_lines: { @@ -56,10 +56,12 @@ export default function PartsOrderModalPriceChange({ form, field }) { }); e.target.value = 0; } - }} - min={0} - max={100} - /> + }} + min={0} + max={100} + /> + % + ) } ], 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 2f875501b..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 (
- + @@ -199,7 +199,10 @@ export function PartsOrderModalComponent({ key={`${index}act_price`} name={[field.name, "act_price"]} > - } /> + + + + {isReturn && ( { logImEXEvent("parts_order_insert"); setSaving(true); - let insertResult; - insertResult = await insertPartOrder({ - variables: { - po: [ - { - ...values, - order_date: dayjs().format("YYYY-MM-DD"), - orderedby: currentUser.email, - jobid: jobId, - user_email: currentUser.email, - return: isReturn, - status: is_quote - ? bodyshop.md_order_statuses.default_quote || "Quote" - : bodyshop.md_order_statuses.default_ordered || "Ordered*" - } - ] - }, - refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"] + // 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 + }; }); - if (insertResult.errors) { + + const missingIdx = forcedLines.findIndex((l) => !l?.job_line_id); + if (missingIdx !== -1) { notification.error({ title: t("parts_orders.errors.creating"), - description: JSON.stringify(insertResult.errors) + description: `Missing job_line_id for parts line #${missingIdx + 1}` }); + setSaving(false); return; } + + let insertResult; + try { + insertResult = await insertPartOrder({ + variables: { + po: [ + { + ...values, + order_date: dayjs().format("YYYY-MM-DD"), + orderedby: currentUser.email, + jobid: jobId, + user_email: currentUser.email, + return: isReturn, + status: is_quote + ? bodyshop.md_order_statuses.default_quote || "Quote" + : 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"), + description: JSON.stringify(insertResult.errors) + }); + setSaving(false); + return; + } + } catch (err) { + notification.error({ + title: t("parts_orders.errors.creating"), + description: err?.message || String(err) + }); + 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 @@ -118,36 +157,54 @@ export function PartsOrderModalContainer({ type: isReturn ? "jobspartsreturn" : "jobspartsorder" }); - const jobLinesResult = await updateJobLines({ - variables: { - ids: values.parts_order_lines.data.filter((item) => item.job_line_id).map((item) => item.job_line_id), - status: isReturn - ? bodyshop.md_order_statuses.default_returned || "Returned*" - : is_quote - ? bodyshop.md_order_statuses.default_quote || "Quote" - : bodyshop.md_order_statuses.default_ordered || "Ordered*" - } - }); + // 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)); - if (!isReturn && removefrompartsqueue) { - await updateJob({ + try { + const jobLinesResult = await updateJobLines({ variables: { - jobId: jobId, - job: { - queued_for_parts: false - } + ids: jobLineIds, + status: isReturn + ? bodyshop.md_order_statuses.default_returned || "Returned*" + : is_quote + ? bodyshop.md_order_statuses.default_quote || "Quote" + : bodyshop.md_order_statuses.default_ordered || "Ordered*" } }); + + if (jobLinesResult.errors) { + notification.error({ + title: t("parts_orders.errors.updating_status"), + description: JSON.stringify(jobLinesResult.errors) + }); + } + } catch (err) { + notification.error({ + title: t("parts_orders.errors.updating_status"), + description: err?.message || String(err) + }); + console.error("Job lines update error:", err); } - if (jobLinesResult.errors) { - notification.error({ - title: t("parts_orders.errors.creating"), - description: JSON.stringify(jobLinesResult.errors) - }); + if (!isReturn && removefrompartsqueue) { + try { + await updateJob({ + variables: { + jobId: jobId, + job: { + queued_for_parts: false + } + } + }); + } catch (err) { + console.error("Update job queue error:", err); + } } if (values.vendorid === bodyshop.inhousevendorid) { + // Use linesToOrder for joblineid, and use forcedLines for the user-entered fields setBillEnterContext({ actions: { refetch: refetch }, context: { @@ -159,11 +216,12 @@ export function PartsOrderModalContainer({ isinhouse: true, date: dayjs(), total: 0, - billlines: values.parts_order_lines.data.map((p) => { + billlines: forcedLines.map((p, index) => { + const originalLine = linesToOrder?.[index]; return { - joblineid: p.job_line_id, + 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, @@ -178,6 +236,8 @@ export function PartsOrderModalContainer({ } } }); + + setSaving(false); toggleModalVisible(); return; } @@ -187,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( { @@ -233,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, @@ -241,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({ @@ -269,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(); }; @@ -290,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 }) { - + + + % + diff --git a/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx b/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx index 4aa25d687..9d08e463b 100644 --- a/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx +++ b/client/src/components/time-ticket-list/time-ticket-list-team-pay.component.jsx @@ -96,7 +96,10 @@ export function TimeTicketListTeamPay({ bodyshop, context }) { - + + + % + diff --git a/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx b/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx index 3897d3674..a8d5a3384 100644 --- a/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx +++ b/client/src/components/time-ticket-modal/time-ticket-modal.component.jsx @@ -184,13 +184,7 @@ export function TimeTicketModalComponent({ }} - +