diff --git a/client/src/components/job-close-ro-guard/job-close-ro-gaurd.labor.jsx b/client/src/components/job-close-ro-guard/job-close-ro-gaurd.labor.jsx index e856bf240..51233e151 100644 --- a/client/src/components/job-close-ro-guard/job-close-ro-gaurd.labor.jsx +++ b/client/src/components/job-close-ro-guard/job-close-ro-gaurd.labor.jsx @@ -16,11 +16,15 @@ const mapDispatchToProps = () => ({}); export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardLabor); export function JobCloseRoGuardLabor({ job, bodyshop, warningCallback }) { + const jobId = job?.id ?? null; + const { loading, error, data, refetch } = useQuery(GET_LINE_TICKET_BY_PK, { - variables: { id: job.id }, + variables: { id: jobId }, + skip: !jobId, fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); + const { treatments: { Enhanced_Payroll } } = useTreatmentsWithConfig({ @@ -29,12 +33,13 @@ export function JobCloseRoGuardLabor({ job, bodyshop, warningCallback }) { splitKey: bodyshop.imexshopid }); + if (!jobId) return ; if (loading) return ; if (error) return ; return Enhanced_Payroll.treatment === "on" ? ( ) : ( ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); +const mapDispatchToProps = () => ({}); const iconStyle = { marginLeft: ".3rem" }; @@ -31,163 +29,199 @@ export function JobEmployeeAssignments({ loading }) { const { t } = useTranslation(); - const [assignment, setAssignment] = useState({ - operation: null, - employeeid: null - }); - const [visibility, setVisibility] = useState(false); - const onChange = (value, option) => { - setAssignment({ ...assignment, employeeid: value, name: option.name }); + // Which assignment popover is currently open: "body" | "prep" | "refinish" | "csr" | null + const [openOperation, setOpenOperation] = useState(null); + + // Current selection inside the popover + const [selected, setSelected] = useState({ employeeid: null, name: null }); + + const employeeOptions = (bodyshop?.employees || []) + .filter((emp) => emp.active) + .map((emp) => ({ + value: emp.id, + label: `${emp.first_name} ${emp.last_name}` + })); + + const getPopupContainer = () => document.querySelector("#time-ticket-modal") || document.body; + + const openFor = (operation) => { + if (jobRO) return; + setSelected({ employeeid: null, name: null }); + setOpenOperation(operation); }; - const popContent = ( - - - - - - - + + {!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}}",