diff --git a/client/src/components/dms-post-form/rr-dms-post-form.jsx b/client/src/components/dms-post-form/rr-dms-post-form.jsx index a1d8154e6..c887826fd 100644 --- a/client/src/components/dms-post-form/rr-dms-post-form.jsx +++ b/client/src/components/dms-post-form/rr-dms-post-form.jsx @@ -208,16 +208,16 @@ export default function RRPostForm({ }); }; - // Check if early RO was created (job has dms_id) - const hasEarlyRO = !!job?.dms_id; + // Check if early RO was created (job has all early RO fields) + const hasEarlyRO = !!(job?.dms_id && job?.dms_customer_id && job?.dms_advisor_id); return ( {hasEarlyRO && ( - ✅ Early RO Created: {job.dms_id} + ✅ {t("jobs.labels.dms.earlyro.created")} {job.dms_id}
- This will update the existing RO with full job data. + {t("jobs.labels.dms.earlyro.willupdate")}
)}
= ); diff --git a/client/src/components/jobs-convert-button/jobs-convert-button.component.jsx b/client/src/components/jobs-convert-button/jobs-convert-button.component.jsx index 88019d0ce..72de99502 100644 --- a/client/src/components/jobs-convert-button/jobs-convert-button.component.jsx +++ b/client/src/components/jobs-convert-button/jobs-convert-button.component.jsx @@ -106,8 +106,8 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO, insertAuditTr setEarlyRoCreated(true); // Mark early RO as created setEarlyRoCreatedThisSession(true); // Mark as created in this session notification.success({ - title: t("jobs.successes.early_ro_created", "Early RO Created"), - message: `RO Number: ${result.roNumber || "N/A"}` + title: t("jobs.successes.early_ro_created"), + description: `RO Number: ${result.roNumber || "N/A"}` }); // Delay refetch to keep success message visible for 2 seconds setTimeout(() => { diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index c63f5a302..21d9522b1 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -2001,6 +2001,9 @@ export const QUERY_JOB_CLOSE_DETAILS = gql` qb_multiple_payers lbr_adjustments ownr_ea + dms_id + dms_customer_id + dms_advisor_id payments { amount created_at diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx index d43f22a08..22d79a0bb 100644 --- a/client/src/pages/dms/dms.container.jsx +++ b/client/src/pages/dms/dms.container.jsx @@ -426,6 +426,24 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse if (data.jobs_by_pk?.date_exported) return ; + // Check if Reynolds mode requires early RO + const hasEarlyRO = !!(data.jobs_by_pk?.dms_id && data.jobs_by_pk?.dms_customer_id && data.jobs_by_pk?.dms_advisor_id); + + if (isRrMode && !hasEarlyRO) { + return ( + + + + } + /> + ); + } + return (
diff --git a/client/src/pages/jobs-admin/jobs-admin.page.jsx b/client/src/pages/jobs-admin/jobs-admin.page.jsx index 65635618b..e7f9606d8 100644 --- a/client/src/pages/jobs-admin/jobs-admin.page.jsx +++ b/client/src/pages/jobs-admin/jobs-admin.page.jsx @@ -1,10 +1,12 @@ -import { useQuery } from "@apollo/client/react"; -import { Button, Card, Col, Result, Row, Space, Typography } from "antd"; -import { useEffect, useState } from "react"; +import { useMutation, useQuery } from "@apollo/client/react"; +import { Button, Card, Col, Form, Input, Modal, Result, Row, Select, Space, Switch, Typography } from "antd"; +import { useEffect, useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { useParams } from "react-router-dom"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; +import { some } from "lodash"; +import axios from "axios"; import AlertComponent from "../../components/alert/alert.component"; import JobCalculateTotals from "../../components/job-calculate-totals/job-calculate-totals.component"; import ScoreboardAddButton from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component"; @@ -21,14 +23,16 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com import NotFound from "../../components/not-found/not-found.component"; import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component"; import RREarlyROModal from "../../components/dms-post-form/rr-early-ro-modal"; -import { GET_JOB_BY_PK } from "../../graphql/jobs.queries"; +import { GET_JOB_BY_PK, CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries"; import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions"; +import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { createStructuredSelector } from "reselect"; import { useSocket } from "../../contexts/SocketIO/useSocket"; import { useNotification } from "../../contexts/Notifications/notificationContext"; import { DMS_MAP, getDmsMode } from "../../utils/dmsUtils"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop @@ -36,7 +40,8 @@ const mapStateToProps = createStructuredSelector({ const mapDispatchToProps = (dispatch) => ({ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), - setSelectedHeader: (key) => dispatch(setSelectedHeader(key)) + setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), + insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) }); const colSpan = { @@ -50,7 +55,7 @@ const cardStyle = { height: "100%" }; -export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop }) { +export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop, insertAuditTrail }) { const { jobId } = useParams(); const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, { variables: { id: jobId }, @@ -61,6 +66,11 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop const { socket } = useSocket(); // Extract socket from context const notification = useNotification(); const [showEarlyROModal, setShowEarlyROModal] = useState(false); + const [showConvertModal, setShowConvertModal] = useState(false); + const [convertLoading, setConvertLoading] = useState(false); + const [form] = Form.useForm(); + const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO); + const allFormValues = Form.useWatch([], form); // Get Fortellis treatment for proper DMS mode detection const { @@ -105,13 +115,53 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop const handleEarlyROSuccess = (result) => { notification.success({ - title: t("jobs.successes.early_ro_created", "Early RO Created"), - message: `RO Number: ${result.roNumber || "N/A"}` + title: t("jobs.successes.early_ro_created"), + description: `RO Number: ${result.roNumber || "N/A"}` }); setShowEarlyROModal(false); refetch?.(); }; + const handleConvert = async ({ employee_csr, category, ...values }) => { + if (!job?.id) return; + setConvertLoading(true); + const res = await mutationConvertJob({ + variables: { + jobId: job.id, + job: { + converted: true, + ...(bodyshop?.enforce_conversion_csr ? { employee_csr } : {}), + ...(bodyshop?.enforce_conversion_category ? { category } : {}), + ...values + } + } + }); + + if (values.ca_gst_registrant) { + await axios.post("/job/totalsssu", { + id: job.id + }); + } + + if (!res.errors) { + refetch(); + notification.success({ + title: t("jobs.successes.converted") + }); + + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobconverted(res.data.update_jobs.returning[0].ro_number), + type: "jobconverted" + }); + + setShowConvertModal(false); + } + setConvertLoading(false); + }; + + const submitDisabled = useCallback(() => some(allFormValues, (v) => v === undefined), [allFormValues]); + if (loading) return ; if (error) return ; if (!data.jobs_by_pk) return ; @@ -138,7 +188,12 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop {isReynoldsMode && job?.converted && !job?.dms_id && !job?.dms_customer_id && !job?.dms_advisor_id && ( + )} + {isReynoldsMode && !job?.converted && !job?.dms_id && ( + )} @@ -176,6 +231,161 @@ export function JobsCloseContainer({ setBreadcrumbs, setSelectedHeader, bodyshop socket={socket} job={job} /> + + {/* Convert without Early RO Modal */} + setShowConvertModal(false)} + title={t("jobs.actions.convertwithoutearlyro", "Convert without Early RO")} + footer={null} + width={700} + destroyOnHidden + > + + + + + {bodyshop?.enforce_class && ( + + + + )} + {bodyshop?.enforce_referral && ( + <> + + + + + + + + )} + {bodyshop?.enforce_conversion_csr && ( + + + + )} + {bodyshop?.enforce_conversion_category && ( + + + + )} + {bodyshop?.region_config?.toLowerCase().startsWith("ca") && ( + + + + )} + + + + + + + + + + + + + ); } diff --git a/client/src/pages/jobs-close/jobs-close.component.jsx b/client/src/pages/jobs-close/jobs-close.component.jsx index 9a429cb2a..6587e8399 100644 --- a/client/src/pages/jobs-close/jobs-close.component.jsx +++ b/client/src/pages/jobs-close/jobs-close.component.jsx @@ -9,6 +9,7 @@ import { Form, Input, InputNumber, + Modal, Popconfirm, Row, Select, @@ -42,7 +43,7 @@ import { setModalContext } from "../../redux/modals/modals.actions.js"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import dayjs from "../../utils/day"; -import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js"; +import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils.js"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -71,6 +72,11 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set const notification = useNotification(); const hasDMSKey = bodyshopHasDmsKey(bodyshop); + const dmsMode = getDmsMode(bodyshop, "off"); + const isReynoldsMode = dmsMode === DMS_MAP.reynolds; + const hasEarlyRO = !!(job?.dms_id && job?.dms_customer_id && job?.dms_advisor_id); + const canSendToDMS = !isReynoldsMode || hasEarlyRO; + const [showEarlyROModal, setShowEarlyROModal] = useState(false); const { treatments: { Qb_Multi_Ar, ClosingPeriod } @@ -82,18 +88,18 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set const handleFinish = async ({ removefromproduction, ...values }) => { setLoading(true); - + // Validate that all joblines have valid IDs - const joblinesWithIds = values.joblines.filter(jl => jl && jl.id); + const joblinesWithIds = values.joblines.filter((jl) => jl && jl.id); if (joblinesWithIds.length !== values.joblines.length) { notification.error({ title: t("jobs.errors.invalidjoblines"), - message: t("jobs.errors.missingjoblineids") + description: t("jobs.errors.missingjoblineids") }); setLoading(false); return; } - + const result = await client.mutate({ mutation: generateJobLinesUpdatesForInvoicing(values.joblines) }); @@ -208,9 +214,17 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set {bodyshopHasDmsKey(bodyshop) && ( - - - + <> + {canSendToDMS ? ( + + + + ) : ( + + )} + )} + + +
); } diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 3252b160c..626ab5e4c 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1047,7 +1047,9 @@ }, "dms": { "errors": { - "alreadyexported": "This job has already been sent to the DMS. If you need to resend it, please use admin permissions to mark the job for re-export." + "alreadyexported": "This job has already been sent to the DMS. If you need to resend it, please use admin permissions to mark the job for re-export.", + "earlyrorequired": "Early RO Required", + "earlyrorequired.message": "This job requires an early Repair Order to be created before posting to Reynolds. Please use the admin panel to create the early RO first." }, "labels": { "refreshallocations": "Refresh to see DMS Allocations." @@ -1244,6 +1246,7 @@ "deselectall": "Deselect All", "download": "Download", "edit": "Edit", + "gotoadmin": "Go to Admin Panel", "login": "Login", "next": "Next", "ok": "Ok", @@ -1622,11 +1625,13 @@ "changestatus": "Change Status", "changestimator": "Change Estimator", "convert": "Convert", + "convertwithoutearlyro": "Convert without Early RO", "createiou": "Create IOU", "deliver": "Deliver", "deliver_quick": "Quick Deliver", "dms": { "addpayer": "Add Payer", + "createearlyro": "Create RR RO", "createnewcustomer": "Create New Customer", "findmakemodelcode": "Find Make/Model Code", "getmakes": "Get Makes", @@ -1635,6 +1640,7 @@ }, "post": "Post", "refetchmakesmodels": "Refetch Make and Model Codes", + "update_ro": "Update RO", "usegeneric": "Use Generic Customer", "useselected": "Use Selected Customer" }, @@ -2108,6 +2114,11 @@ "damageto": "Damage to $t(jobs.fields.area_of_damage_impact.{{area_of_damage}}).", "defaultstory": "B/S RO: {{ro_number}}. Owner: {{ownr_nm}}. Insurance Co: {{ins_co_nm}}. Claim/PO #: {{clm_po}}", "disablebillwip": "Cost and WIP for bills has been ignored per shop configuration.", + "earlyro": { + "created": "Early RO Created:", + "fields": "Required fields:", + "willupdate": "This will update the existing RO with full job data." + }, "invoicedatefuture": "Invoice date must be today or in the future for CDK posting.", "kmoutnotgreaterthankmin": "Mileage out must be greater than mileage in.", "logs": "Logs", @@ -2265,6 +2276,7 @@ "delete": "Job deleted successfully.", "deleted": "Job deleted successfully.", "duplicated": "Job duplicated successfully. ", + "early_ro_created": "Early RO Created", "exported": "Job(s) exported successfully. ", "invoiced": "Job closed and invoiced successfully.", "ioucreated": "IOU created successfully. Click to see.", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 20789b76c..664c5081c 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -1047,7 +1047,9 @@ }, "dms": { "errors": { - "alreadyexported": "" + "alreadyexported": "", + "earlyrorequired": "", + "earlyrorequired.message": "" }, "labels": { "refreshallocations": "" @@ -1244,6 +1246,7 @@ "deselectall": "", "download": "", "edit": "Editar", + "gotoadmin": "", "login": "", "next": "", "ok": "", @@ -1622,11 +1625,13 @@ "changestatus": "Cambiar Estado", "changestimator": "", "convert": "Convertir", + "convertwithoutearlyro": "", "createiou": "", "deliver": "", "deliver_quick": "", "dms": { "addpayer": "", + "createearlyro": "", "createnewcustomer": "", "findmakemodelcode": "", "getmakes": "", @@ -1635,6 +1640,7 @@ }, "post": "", "refetchmakesmodels": "", + "update_ro": "", "usegeneric": "", "useselected": "" }, @@ -2108,6 +2114,11 @@ "damageto": "", "defaultstory": "", "disablebillwip": "", + "earlyro": { + "created": "", + "fields": "", + "willupdate": "" + }, "invoicedatefuture": "", "kmoutnotgreaterthankmin": "", "logs": "", @@ -2265,6 +2276,7 @@ "delete": "", "deleted": "Trabajo eliminado con éxito.", "duplicated": "", + "early_ro_created": "", "exported": "", "invoiced": "", "ioucreated": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 75be07749..67a14d5ff 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -1047,7 +1047,9 @@ }, "dms": { "errors": { - "alreadyexported": "" + "alreadyexported": "", + "earlyrorequired": "", + "earlyrorequired.message": "" }, "labels": { "refreshallocations": "" @@ -1244,6 +1246,7 @@ "deselectall": "", "download": "", "edit": "modifier", + "gotoadmin": "", "login": "", "next": "", "ok": "", @@ -1622,11 +1625,13 @@ "changestatus": "Changer le statut", "changestimator": "", "convert": "Convertir", + "convertwithoutearlyro": "", "createiou": "", "deliver": "", "deliver_quick": "", "dms": { "addpayer": "", + "createearlyro": "", "createnewcustomer": "", "findmakemodelcode": "", "getmakes": "", @@ -1635,6 +1640,7 @@ }, "post": "", "refetchmakesmodels": "", + "update_ro": "", "usegeneric": "", "useselected": "" }, @@ -2108,6 +2114,11 @@ "damageto": "", "defaultstory": "", "disablebillwip": "", + "earlyro": { + "created": "", + "fields": "", + "willupdate": "" + }, "invoicedatefuture": "", "kmoutnotgreaterthankmin": "", "logs": "", @@ -2265,6 +2276,7 @@ "delete": "", "deleted": "Le travail a bien été supprimé.", "duplicated": "", + "early_ro_created": "", "exported": "", "invoiced": "", "ioucreated": "",