+ {refinish ? (
+
+
{`${refinish.first_name || ""} ${refinish.last_name || ""}`}
+
{
- if (!jobRO) {
- setAssignment({ operation: "refinish" });
- setVisibility(true);
- }
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (!jobRO) handleRemove("refinish");
}}
/>
- )}
-
-
- {csr ? (
-
- {`${csr.first_name || ""} ${csr.last_name || ""}`}
- !jobRO && handleRemove("csr")}
- />
-
- ) : (
-
+ ) : (
+ renderAssigner("refinish")
+ )}
+
+
+
+ {csr ? (
+
+ {`${csr.first_name || ""} ${csr.last_name || ""}`}
+ {
- if (!jobRO) {
- setAssignment({ operation: "csr" });
- setVisibility(true);
- }
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if (!jobRO) handleRemove("csr");
}}
/>
- )}
-
-
-
+
+ ) : (
+ renderAssigner("csr")
+ )}
+
+
);
}
diff --git a/client/src/components/job-employee-assignments/job-employee-assignments.container.jsx b/client/src/components/job-employee-assignments/job-employee-assignments.container.jsx
index f9b1b1fd5..3957e1035 100644
--- a/client/src/components/job-employee-assignments/job-employee-assignments.container.jsx
+++ b/client/src/components/job-employee-assignments/job-employee-assignments.container.jsx
@@ -11,9 +11,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
-const mapStateToProps = createStructuredSelector({
- //currentUser: selectCurrentUser
-});
+const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
@@ -26,55 +24,69 @@ export function JobEmployeeAssignmentsContainer({ job, refetch, insertAuditTrail
const notification = useNotification();
const handleAdd = async (assignment) => {
- setLoading(true);
const { operation, employeeid, name } = assignment;
- logImEXEvent("job_assign_employee", { operation });
+ const empAssignment = determineFieldName(operation);
- let empAssignment = determineFieldName(operation);
+ if (!job?.id || !empAssignment || !employeeid) return;
- const result = await updateJob({
- variables: { jobId: job.id, job: { [empAssignment]: employeeid } }
- });
- if (refetch) refetch();
-
- if (!result.errors) {
- insertAuditTrail({
- jobid: job.id,
- operation: AuditTrailMapping.jobassignmentchange(operation, name),
- type: "jobassignmentchange"
- });
- } else {
- notification.error({
- title: t("jobs.errors.assigning", {
- message: JSON.stringify(result.errors)
- })
- });
- }
- setLoading(false);
- };
- const handleRemove = async (operation) => {
setLoading(true);
- logImEXEvent("job_unassign_employee", { operation });
+ try {
+ logImEXEvent("job_assign_employee", { operation });
- let empAssignment = determineFieldName(operation);
- const result = await updateJob({
- variables: { jobId: job.id, job: { [empAssignment]: null } }
- });
+ const result = await updateJob({
+ variables: { jobId: job.id, job: { [empAssignment]: employeeid } }
+ });
- if (!result.errors) {
- insertAuditTrail({
- jobid: job.id,
- operation: AuditTrailMapping.jobassignmentremoved(operation),
- type: "jobassignmentremoved"
- });
- } else {
- notification.error({
- title: t("jobs.errors.assigning", {
- message: JSON.stringify(result.errors)
- })
- });
+ if (typeof refetch === "function") await refetch();
+
+ if (!result.errors) {
+ insertAuditTrail({
+ jobid: job.id,
+ operation: AuditTrailMapping.jobassignmentchange(operation, name),
+ type: "jobassignmentchange"
+ });
+ } else {
+ notification.error({
+ title: t("jobs.errors.assigning", {
+ message: JSON.stringify(result.errors)
+ })
+ });
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRemove = async (operation) => {
+ const empAssignment = determineFieldName(operation);
+ if (!job?.id || !empAssignment) return;
+
+ setLoading(true);
+ try {
+ logImEXEvent("job_unassign_employee", { operation });
+
+ const result = await updateJob({
+ variables: { jobId: job.id, job: { [empAssignment]: null } }
+ });
+
+ if (typeof refetch === "function") await refetch();
+
+ if (!result.errors) {
+ insertAuditTrail({
+ jobid: job.id,
+ operation: AuditTrailMapping.jobassignmentremoved(operation),
+ type: "jobassignmentremoved"
+ });
+ } else {
+ notification.error({
+ title: t("jobs.errors.assigning", {
+ message: JSON.stringify(result.errors)
+ })
+ });
+ }
+ } finally {
+ setLoading(false);
}
- setLoading(false);
};
return (
@@ -102,7 +114,6 @@ const determineFieldName = (operation) => {
return "employee_csr";
case "refinish":
return "employee_refinish";
-
default:
return null;
}
diff --git a/client/src/components/jobs-detail-labor/jobs-detail-labor.container.jsx b/client/src/components/jobs-detail-labor/jobs-detail-labor.container.jsx
index 95d2ca219..da751c755 100644
--- a/client/src/components/jobs-detail-labor/jobs-detail-labor.container.jsx
+++ b/client/src/components/jobs-detail-labor/jobs-detail-labor.container.jsx
@@ -4,9 +4,11 @@ import AlertComponent from "../alert/alert.component";
import JobsDetailLaborComponent from "./jobs-detail-labor.component";
export default function JobsDetailLaborContainer({ jobId, techConsole, job }) {
+ const id = jobId ?? null;
+
const { loading, error, data, refetch } = useQuery(GET_LINE_TICKET_BY_PK, {
- variables: { id: jobId },
- skip: !jobId,
+ variables: { id },
+ skip: !id,
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
@@ -16,7 +18,7 @@ export default function JobsDetailLaborContainer({ jobId, techConsole, job }) {
return (
e.id === technician?.id)[0];
@@ -63,6 +65,7 @@ export function TechClockOffButton({
const status = values.status;
delete values.status;
setLoading(true);
+
const result = await updateTimeticket({
variables: {
timeticketId: timeTicketId,
@@ -95,10 +98,11 @@ export function TechClockOffButton({
title: t("timetickets.successes.clockedout")
});
}
+
if (!isShiftTicket) {
const job_update_result = await updateJobStatus({
variables: {
- jobId: jobId,
+ jobId: id,
status: status
}
});
@@ -115,6 +119,7 @@ export function TechClockOffButton({
});
}
}
+
setLoading(false);
if (completedCallback) completedCallback();
};
@@ -139,7 +144,6 @@ export function TechClockOffButton({
rules={[
{
required: true
- //message: t("general.validation.required"),
}
]}
>
@@ -151,7 +155,6 @@ export function TechClockOffButton({
rules={[
{
required: true
- //message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
@@ -179,9 +182,7 @@ export function TechClockOffButton({
if (value > costCenterDiff)
return Promise.reject(t("timetickets.validation.hoursenteredmorethanavailable"));
- else {
- return Promise.resolve();
- }
+ return Promise.resolve();
}
})
]}
@@ -190,13 +191,13 @@ export function TechClockOffButton({
) : null}
+
@@ -228,7 +229,6 @@ export function TechClockOffButton({
rules={[
{
required: true
- //message: t("general.validation.required"),
}
]}
>
@@ -239,12 +239,15 @@ export function TechClockOffButton({
)}
+
+
+
{!isShiftTicket && (
-
+
)}
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 9f7e5b8e4..3897d3674 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
@@ -1,7 +1,7 @@
import { useLazyQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
-import { useEffect } from "react";
+import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -40,6 +40,7 @@ export function TimeTicketModalComponent({
isOpen
}) {
const { t } = useTranslation();
+
const {
treatments: { Enhanced_Payroll }
} = useTreatmentsWithConfig({
@@ -48,49 +49,68 @@ export function TimeTicketModalComponent({
splitKey: bodyshop.imexshopid
});
- const [loadLineTicketData, { called, loading, data: lineTicketData }] = useLazyQuery(GET_LINE_TICKET_BY_PK, {
+ const [loadLineTicketData, { loading, data: lineTicketData }] = useLazyQuery(GET_LINE_TICKET_BY_PK, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
- // Watch the jobid field so we can refetch the bottom section without relying on jobid changes
const watchedJobId = Form.useWatch("jobid", form);
+ // Local mutable memory: dedupe jobid-driven fetches without re-rendering.
+ const lastFetchedJobIdRef = useRef(null);
+
+ // Reset + fetch when job changes (never during render)
+ useEffect(() => {
+ if (!isOpen) {
+ lastFetchedJobIdRef.current = null;
+ return;
+ }
+
+ if (!watchedJobId) {
+ lastFetchedJobIdRef.current = null;
+ return;
+ }
+
+ if (lastFetchedJobIdRef.current === watchedJobId) return;
+
+ lastFetchedJobIdRef.current = watchedJobId;
+ loadLineTicketData({ variables: { id: watchedJobId } });
+ }, [isOpen, watchedJobId, loadLineTicketData]);
+
+ // Force refresh (e.g., after Save / external refresh bump)
useEffect(() => {
if (!isOpen) return;
if (!watchedJobId) return;
- if (!lineTicketRefreshKey) return;
+ if (lineTicketRefreshKey === 0) return;
- loadLineTicketData({ id: watchedJobId });
- }, [lineTicketRefreshKey, watchedJobId, isOpen]);
+ loadLineTicketData({ variables: { id: watchedJobId } });
+ }, [lineTicketRefreshKey, isOpen, watchedJobId, loadLineTicketData]);
- const CostCenterSelect = ({ emps, value, ...props }) => {
- return (
-
- );
- };
+ const CostCenterSelect = ({ emps, value, ...props }) => (
+
+ );
- const MemoInput = ({ value, ...props }) => {
- return ;
- };
+ const MemoInput = ({ value, ...props }) => (
+
+ );
return (
@@ -102,8 +122,7 @@ export function TimeTicketModalComponent({
label={t("timetickets.fields.ro_number")}
rules={[
{
- required: !(form.getFieldValue("cost_center") === "timetickets.labels.shift")
- //message: t("general.validation.required"),
+ required: form.getFieldValue("cost_center") !== "timetickets.labels.shift"
}
]}
>
@@ -115,25 +134,25 @@ export function TimeTicketModalComponent({
)}
+
+
@@ -141,29 +160,23 @@ export function TimeTicketModalComponent({
disabled={employeeSelectDisabled || disabled}
options={employeeAutoCompleteOptions}
onSelect={(value) => {
- const emps = employeeAutoCompleteOptions && employeeAutoCompleteOptions.filter((e) => e.id === value)[0];
-
+ const emps = employeeAutoCompleteOptions?.find((e) => e.id === value);
form.setFieldsValue({ flat_rate: emps?.flat_rate });
}}
/>
+
prev.employeeid !== cur.employeeid}>
{() => {
const employeeId = form.getFieldValue("employeeid");
- const emps =
- employeeAutoCompleteOptions && employeeAutoCompleteOptions.filter((e) => e.id === employeeId)[0];
+ const emps = employeeAutoCompleteOptions?.find((e) => e.id === employeeId);
return (
@@ -185,52 +198,46 @@ export function TimeTicketModalComponent({
{() => (
- <>
- ({
- validator(rule, value) {
- if (!bodyshop.tt_enforce_hours_for_tech_console) {
- return Promise.resolve();
- }
- if (!value || getFieldValue("cost_center") === null || !lineTicketData) return Promise.resolve();
+ ({
+ validator(rule, value) {
+ if (!bodyshop.tt_enforce_hours_for_tech_console) return Promise.resolve();
+ if (!value || getFieldValue("cost_center") === null || !lineTicketData) return Promise.resolve();
- //Check the cost center,
- const totals = CalculateAllocationsTotals(
- bodyshop,
- lineTicketData.joblines,
- lineTicketData.timetickets,
- lineTicketData.jobs_by_pk.lbr_adjustments
- );
+ const totals = CalculateAllocationsTotals(
+ bodyshop,
+ lineTicketData.joblines,
+ lineTicketData.timetickets,
+ lineTicketData.jobs_by_pk.lbr_adjustments
+ );
- const fieldTypeToCheck = bodyshopHasDmsKey(bodyshop) ? "mod_lbr_ty" : "cost_center";
+ const fieldTypeToCheck = bodyshopHasDmsKey(bodyshop) ? "mod_lbr_ty" : "cost_center";
- const costCenterDiff =
- Math.round(
- totals.find((total) => total[fieldTypeToCheck] === getFieldValue("cost_center"))?.difference *
- 10
- ) / 10;
+ const costCenterDiff =
+ Math.round(
+ (totals.find((total) => total[fieldTypeToCheck] === getFieldValue("cost_center"))?.difference ||
+ 0) * 10
+ ) / 10;
- if (value > costCenterDiff)
- return Promise.reject(t("timetickets.validation.hoursenteredmorethanavailable"));
- else {
- return Promise.resolve();
- }
+ if (value > costCenterDiff) {
+ return Promise.reject(t("timetickets.validation.hoursenteredmorethanavailable"));
}
- }),
- {
- required: form.getFieldValue("cost_center") !== "timetickets.labels.shift"
- //message: t("general.validation.required"),
+ return Promise.resolve();
}
- ]}
- >
-
-
- >
+ }),
+ {
+ required: form.getFieldValue("cost_center") !== "timetickets.labels.shift"
+ }
+ ]}
+ >
+
+
)}
+
- {
- <>
-
-
-
- ({
- validator(rule, value) {
- const clockon = getFieldValue("clockon");
- if (!value) return Promise.resolve();
- if (!clockon && value) return Promise.reject(t("timetickets.validation.clockoffwithoutclockon"));
- // TODO - Verify this exists
- if (value?.isSameOrAfter && !value.isSameOrAfter(clockon))
- return Promise.reject(t("timetickets.validation.clockoffmustbeafterclockon"));
-
- return Promise.resolve();
- }
+ <>
+
+
-
+
+
+ ({
+ validator(rule, value) {
+ const clockon = getFieldValue("clockon");
+ if (!value) return Promise.resolve();
+ if (!clockon && value) return Promise.reject(t("timetickets.validation.clockoffwithoutclockon"));
+ if (value?.isSameOrAfter && !value.isSameOrAfter(clockon)) {
+ return Promise.reject(t("timetickets.validation.clockoffmustbeafterclockon"));
+ }
+ return Promise.resolve();
}
- />
-
- >
- }
+ })
+ ]}
+ >
+
+
+ >
+
{() => (
@@ -323,15 +326,7 @@ export function TimeTicketModalComponent({
-
- {() => {
- const jobid = form.getFieldValue("jobid");
- if ((!called && jobid) || (jobid && lineTicketData?.jobs_by_pk?.id !== jobid && !loading)) {
- loadLineTicketData({ id: jobid });
- }
- return ;
- }}
-
+
);
}
@@ -341,17 +336,20 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
if (loading) return ;
if (!lineTicketData) return null;
if (!jobid) return null;
+
return (
+
+
{!hideTimeTickets && (
)}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 9ccdd0c0e..c3c562745 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -1128,7 +1128,8 @@
"actions": {
"addvacation": "Add Vacation",
"new": "New Employee",
- "newrate": "New Rate"
+ "newrate": "New Rate",
+ "select": "Select Employee"
},
"errors": {
"delete": "Error encountered while deleting employee. {{message}}",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 0e4fa7aea..3737dcd90 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -1128,7 +1128,8 @@
"actions": {
"addvacation": "",
"new": "Nuevo empleado",
- "newrate": ""
+ "newrate": "",
+ "select": ""
},
"errors": {
"delete": "Se encontró un error al eliminar al empleado. {{message}}",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 7b8a38072..b6708d7a6 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -1128,7 +1128,8 @@
"actions": {
"addvacation": "",
"new": "Nouvel employé",
- "newrate": ""
+ "newrate": "",
+ "select": ""
},
"errors": {
"delete": "Erreur rencontrée lors de la suppression de l'employé. {{message}}",