diff --git a/client/src/components/job-detail-lines/job-lines.component.jsx b/client/src/components/job-detail-lines/job-lines.component.jsx
index d6a0320e3..f0d2d54ef 100644
--- a/client/src/components/job-detail-lines/job-lines.component.jsx
+++ b/client/src/components/job-detail-lines/job-lines.component.jsx
@@ -9,10 +9,10 @@ import {
WarningFilled
} from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
-import { useMutation } from "@apollo/client";
-import { Button, Dropdown, Input, Space, Table, Tag } from "antd";
+import { gql, useMutation } from "@apollo/client";
+import { Button, Dropdown, Input, Modal, Select, Space, Table, Tag, Typography } from "antd";
import axios from "axios";
-import { useState } from "react";
+import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -47,6 +47,19 @@ import JobLinesExpander from "./job-lines-expander.component";
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
import JobLinesExpanderSimple from "./jobs-lines-expander-simple.component";
import { logImEXEvent } from "../../firebase/firebase.utils";
+import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
+
+const UPDATE_JOB_LINES_LOCATION_BULK = gql`
+ mutation UPDATE_JOB_LINES_LOCATION_BULK($ids: [uuid!]!, $location: String!) {
+ update_joblines(where: { id: { _in: $ids } }, _set: { location: $location }) {
+ affected_rows
+ returning {
+ id
+ location
+ }
+ }
+ }
+`;
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -83,6 +96,9 @@ export function JobLinesComponent({
isPartsEntry
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
+ const [bulkUpdateLocations] = useMutation(UPDATE_JOB_LINES_LOCATION_BULK);
+ const notification = useNotification();
+
const {
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
@@ -103,9 +119,83 @@ export function JobLinesComponent({
}
});
+ // Bulk location modal state
+ const [bulkLocationOpen, setBulkLocationOpen] = useState(false);
+ const [bulkLocation, setBulkLocation] = useState(null);
+ const [bulkLocationSaving, setBulkLocationSaving] = useState(false);
+
const { t } = useTranslation();
const jobIsPrivate = bodyshop.md_ins_cos.find((c) => c.name === job.ins_co_nm)?.private;
+ const selectedLineIds = useMemo(() => selectedLines.map((l) => l?.id).filter(Boolean), [selectedLines]);
+
+ const commonSelectedLocation = useMemo(() => {
+ const locs = selectedLines
+ .map((l) => (typeof l?.location === "string" ? l.location : ""))
+ .map((x) => x.trim())
+ .filter(Boolean);
+
+ if (locs.length === 0) return null;
+
+ const uniq = _.uniq(locs);
+ return uniq.length === 1 ? uniq[0] : null;
+ }, [selectedLines]);
+
+ const openBulkLocationModal = () => {
+ setBulkLocation(commonSelectedLocation);
+ setBulkLocationOpen(true);
+ logImEXEvent("joblines_bulk_location_open", { count: selectedLineIds.length });
+ };
+
+ const closeBulkLocationModal = () => {
+ setBulkLocationOpen(false);
+ setBulkLocation(null);
+ };
+
+ const saveBulkLocation = async () => {
+ if (selectedLineIds.length === 0) return;
+
+ setBulkLocationSaving(true);
+
+ try {
+ const locationToSave = (bulkLocation ?? "").toString();
+
+ const result = await bulkUpdateLocations({
+ variables: {
+ ids: selectedLineIds,
+ location: locationToSave
+ }
+ });
+
+ if (!result?.errors) {
+ // Keep UI selection consistent without waiting for refetch
+ setSelectedLines((prev) =>
+ prev.map((l) => (l && selectedLineIds.includes(l.id) ? { ...l, location: locationToSave } : l))
+ );
+
+ notification["success"]({ message: t("joblines.successes.saved") });
+
+ logImEXEvent("joblines_bulk_location_saved", {
+ count: selectedLineIds.length,
+ location: locationToSave
+ });
+
+ closeBulkLocationModal();
+ if (refetch) refetch();
+ } else {
+ notification["error"]({
+ message: t("joblines.errors.saving", { error: JSON.stringify(result.errors) })
+ });
+ }
+ } catch (error) {
+ notification["error"]({
+ message: t("joblines.errors.saving", { error: error?.message || String(error) })
+ });
+ } finally {
+ setBulkLocationSaving(false);
+ }
+ };
+
const columns = [
{
title: "#",
@@ -171,46 +261,16 @@ export function JobLinesComponent({
? ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG", "PAO"]
: ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"]
},
- {
- text: t("joblines.fields.part_types.PAN"),
- value: ["PAN"]
- },
- {
- text: t("joblines.fields.part_types.PAP"),
- value: ["PAP"]
- },
- {
- text: t("joblines.fields.part_types.PAL"),
- value: ["PAL"]
- },
- {
- text: t("joblines.fields.part_types.PAA"),
- value: ["PAA"]
- },
- {
- text: t("joblines.fields.part_types.PAG"),
- value: ["PAG"]
- },
- {
- text: t("joblines.fields.part_types.PAS"),
- value: ["PAS"]
- },
- {
- text: t("joblines.fields.part_types.PASL"),
- value: ["PASL"]
- },
- {
- text: t("joblines.fields.part_types.PAC"),
- value: ["PAC"]
- },
- {
- text: t("joblines.fields.part_types.PAR"),
- value: ["PAR"]
- },
- {
- text: t("joblines.fields.part_types.PAM"),
- value: ["PAM"]
- },
+ { text: t("joblines.fields.part_types.PAN"), value: ["PAN"] },
+ { text: t("joblines.fields.part_types.PAP"), value: ["PAP"] },
+ { text: t("joblines.fields.part_types.PAL"), value: ["PAL"] },
+ { text: t("joblines.fields.part_types.PAA"), value: ["PAA"] },
+ { text: t("joblines.fields.part_types.PAG"), value: ["PAG"] },
+ { text: t("joblines.fields.part_types.PAS"), value: ["PAS"] },
+ { text: t("joblines.fields.part_types.PASL"), value: ["PASL"] },
+ { text: t("joblines.fields.part_types.PAC"), value: ["PAC"] },
+ { text: t("joblines.fields.part_types.PAR"), value: ["PAR"] },
+ { text: t("joblines.fields.part_types.PAM"), value: ["PAM"] },
...(isPartsEntry
? [
{
@@ -220,7 +280,6 @@ export function JobLinesComponent({
]
: [])
],
-
onFilter: (value, record) => value.includes(record.part_type),
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
},
@@ -246,7 +305,6 @@ export function JobLinesComponent({
title: t("joblines.fields.mod_lbr_ty"),
dataIndex: "mod_lbr_ty",
key: "mod_lbr_ty",
-
sorter: (a, b) => alphaSort(a.mod_lbr_ty, b.mod_lbr_ty),
sortOrder: state.sortedInfo.columnKey === "mod_lbr_ty" && state.sortedInfo.order,
render: (text, record) => (record.mod_lbr_ty ? t(`joblines.fields.lbr_types.${record.mod_lbr_ty}`) : null)
@@ -255,7 +313,6 @@ export function JobLinesComponent({
title: t("joblines.fields.mod_lb_hrs"),
dataIndex: "mod_lb_hrs",
key: "mod_lb_hrs",
-
sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order
},
@@ -310,18 +367,12 @@ export function JobLinesComponent({
sorter: (a, b) => alphaSort(a.status, b.status),
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: state.filteredInfo.status || null,
-
filters:
(jobLines &&
jobLines
.map((l) => l.status)
.filter(onlyUnique)
- .map((s) => {
- return {
- text: s || t("dashboard.errors.status"),
- value: [s]
- };
- })) ||
+ .map((s) => ({ text: s || t("dashboard.errors.status"), value: [s] }))) ||
[],
onFilter: (value, record) => value.includes(record.status),
render: (text, record) =>