import { gql, useApolloClient, useLazyQuery, useMutation, useQuery, } from "@apollo/client"; import { useTreatments } from "@splitsoftware/splitio-react"; import { Col, notification, Row } from "antd"; import Axios from "axios"; import Dinero from "dinero.js"; import moment from "moment"; import queryString from "query-string"; import React, { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { useHistory, useLocation } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { DELETE_AVAILABLE_JOB, QUERY_AVAILABLE_JOBS, QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK, } from "../../graphql/available-jobs.queries"; import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries"; import { INSERT_NEW_NOTE } from "../../graphql/notes.queries"; import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import { selectBodyshop, selectCurrentUser, } from "../../redux/user/user.selectors"; import confirmDialog from "../../utils/asyncConfirm"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import CriticalPartsScan from "../../utils/criticalPartsScan"; import AlertComponent from "../alert/alert.component"; import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component"; import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.container"; import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util"; import HeaderFields from "./jobs-available-supplement.headerfields"; import JobsAvailableTableComponent from "./jobs-available-table.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, currentUser: selectCurrentUser, }); const mapDispatchToProps = (dispatch) => ({ insertAuditTrail: ({ jobid, operation }) => dispatch(insertAuditTrail({ jobid, operation })), }); export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail, }) { const { CriticalPartsScanning } = useTreatments( ["CriticalPartsScanning"], {}, bodyshop.imexshopid ); const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_JOBS, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", }); const { clm_no, availableJobId } = queryString.parse(useLocation().search); const history = useHistory(); const { t } = useTranslation(); const [ownerModalVisible, setOwnerModalVisible] = useState(false); const [jobModalVisible, setJobModalVisible] = useState(false); const [selectedJob, setSelectedJob] = useState(null); const [selectedOwner, setSelectedOwner] = useState(null); const [insertLoading, setInsertLoading] = useState(false); const [insertNote] = useMutation(INSERT_NEW_NOTE); const [deleteJob] = useMutation(DELETE_AVAILABLE_JOB); const [updateJob] = useMutation(UPDATE_JOB); const [insertNewJob] = useMutation(INSERT_NEW_JOB); const client = useApolloClient(); const estDataLazyLoad = useLazyQuery(QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK); const [loadEstData, estDataRaw] = estDataLazyLoad; const importOptionsState = useState({ overrideHeaders: false }); const importOptions = importOptionsState[0]; const modalSearchState = useState(""); //Import Scenario const onOwnerFindModalOk = async () => { logImEXEvent("job_import_new"); setOwnerModalVisible(false); setInsertLoading(true); const estData = replaceEmpty(estDataRaw.data.available_jobs_by_pk); if (!(estData && estData.est_data)) { //We don't have the right data. Error! setInsertLoading(false); notification["error"]({ message: t("jobs.errors.creating", { error: "No job data present." }), }); return; } //IO-539 Check for Parts Rate on PAL for SGI use case. await CheckTaxRates(estData.est_data, bodyshop); const newTotals = ( await Axios.post("/job/totals", { job: { ...estData.est_data, joblines: estData.est_data.joblines.data, }, }) ).data; let existingVehicles; if (estData.est_data.v_vin) { //There's vehicle data, need to double check the VIN. existingVehicles = await client.query({ query: SEARCH_VEHICLE_BY_VIN, variables: { vin: estData.est_data.v_vin || estData.est_data.vehicle.data.v_vin, }, }); } const newJob = { ...estData.est_data, clm_total: Dinero(newTotals.totals.total_repairs).toFormat("0.00"), owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat("0.00"), job_totals: newTotals, date_open: moment(), status: bodyshop.md_ro_statuses.default_imported, notes: { data: { created_by: currentUser.email, audit: true, text: t("jobs.labels.importnote"), }, }, queued_for_parts: true, ...(existingVehicles && existingVehicles.data.vehicles.length > 0 ? { vehicleid: existingVehicles.data.vehicles[0].id, vehicle: null } : {}), }; if (selectedOwner) { newJob.ownerid = selectedOwner; delete newJob.owner; } if (newJob.vehicleid) { delete newJob.vehicle; } insertNewJob({ variables: { job: newJob, }, }) .then((r) => { if (CriticalPartsScanning.treatment === "on") { CriticalPartsScan(r.data.insert_jobs.returning[0].id); } notification["success"]({ message: t("jobs.successes.created"), onClick: () => { history.push(`/manage/jobs/${r.data.insert_jobs.returning[0].id}`); }, }); //Job has been inserted. Clean up the available jobs record. insertAuditTrail({ jobid: r.data.insert_jobs.returning[0].id, operation: AuditTrailMapping.jobimported(), }); deleteJob({ variables: { id: estData.id }, }).then((r) => { refetch(); setInsertLoading(false); }); }) .catch((r) => { //error while inserting notification["error"]({ message: t("jobs.errors.creating", { error: r.message }), }); refetch(); setInsertLoading(false); }); }; //Suplement scenario const onJobFindModalOk = async () => { logImEXEvent("job_import_supplement"); setJobModalVisible(false); setInsertLoading(true); const estData = estDataRaw.data.available_jobs_by_pk; if (!(estData && estData.est_data)) { //We don't have the right data. Error! setInsertLoading(false); notification["error"]({ message: t("jobs.errors.creating", { error: "No job data present." }), }); } else { //create upsert job let supp = replaceEmpty({ ...estData.est_data }); //IO-539 Check for Parts Rate on PAL for SGI use case. await CheckTaxRates(supp, bodyshop); delete supp.owner; delete supp.vehicle; delete supp.ins_co_nm; if (!importOptions.overrideHeaders) { HeaderFields.forEach((item) => delete supp[item]); } let suppDelta = await GetSupplementDelta( client, selectedJob, supp.joblines.data ); delete supp.joblines; if (suppDelta !== null) { await client.mutate({ mutation: gql` ${suppDelta} `, }); } const updateResult = await updateJob({ variables: { jobId: selectedJob, job: { ...supp, // clm_total: Dinero(newTotals.totals.total_repairs).toFormat("0.00"), // owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat( // "0.00" // ), // job_totals: newTotals, // queued_for_parts: true, }, }, }); if (CriticalPartsScanning.treatment === "on") { CriticalPartsScan(updateResult.data.update_jobs.returning[0].id); } if (updateResult.errors) { //error while inserting notification["error"]({ message: t("jobs.errors.creating", { error: JSON.stringify(updateResult.errors), }), }); refetch(); setInsertLoading(false); return; } const newTotals = await Axios.post("/job/totalsssu", { id: selectedJob, }); if (newTotals.status !== 200) { notification["error"]({ message: t("jobs.errors.totalscalc"), }); setInsertLoading(false); return; } notification["success"]({ message: t("jobs.successes.supplemented"), onClick: () => { history.push( `/manage/jobs/${updateResult.data.update_jobs.returning[0].id}` ); }, }); //Job has been inserted. Clean up the available jobs record. deleteJob({ variables: { id: estData.id }, }).then((r) => { refetch(); setInsertLoading(false); }); await insertNote({ variables: { noteInput: [ { jobid: selectedJob, created_by: currentUser.email, audit: true, text: t("jobs.labels.supplementnote"), }, ], }, }); insertAuditTrail({ jobid: selectedJob, operation: AuditTrailMapping.jobsupplement(), }); } }; const owner = estDataRaw.data && estDataRaw.data.available_jobs_by_pk && estDataRaw.data.available_jobs_by_pk.est_data && estDataRaw.data.available_jobs_by_pk.est_data.owner && estDataRaw.data.available_jobs_by_pk.est_data.owner.data && !estDataRaw.data.available_jobs_by_pk.issupplement ? estDataRaw.data.available_jobs_by_pk.est_data.owner.data : null; const onOwnerModalCancel = () => { setOwnerModalVisible(false); setSelectedOwner(null); }; const onJobModalCancel = () => { setJobModalVisible(false); modalSearchState[1](""); setSelectedJob(null); }; const addJobAsNew = (record) => { loadEstData({ variables: { id: record.id } }); setOwnerModalVisible(true); }; const addJobAsSupp = useCallback((record) => { loadEstData({ variables: { id: record.id } }); modalSearchState[1](record.clm_no); setJobModalVisible(true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (availableJobId && clm_no) addJobAsSupp({ id: availableJobId, clm_no: clm_no }); }, [addJobAsSupp, availableJobId, clm_no]); if (error) return ; return ( ); } export default connect( mapStateToProps, mapDispatchToProps )(JobsAvailableContainer); function replaceEmpty(someObj, replaceValue = null) { const replacer = (key, value) => value === "" ? replaceValue || null : value; //^ because you seem to want to replace (strings) "null" or "undefined" too const temp = JSON.stringify(someObj, replacer); return JSON.parse(temp); } async function CheckTaxRates(estData, bodyshop) { //LKQ Check if ( !estData.parts_tax_rates?.PAL || estData.parts_tax_rates?.PAL?.prt_tax_rt === null || estData.parts_tax_rates?.PAL?.prt_tax_rt === 0 ) { const res = await confirmDialog( `ImEX Online has detected that there is a missing tax rate for LKQ parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.` ); if (res) { if (!estData.parts_tax_rates.PAL) { estData.parts_tax_rates.PAL = { prt_discp: 0, prt_mktyp: true, prt_mkupp: 0, prt_type: "PAL", }; } estData.parts_tax_rates.PAL.prt_tax_rt = bodyshop.bill_tax_rates.state_tax_rate / 100; estData.parts_tax_rates.PAL.prt_tax_in = true; } } //PAC Check if ( !estData.parts_tax_rates?.PAC || estData.parts_tax_rates?.PAC?.prt_tax_rt === null || estData.parts_tax_rates?.PAC?.prt_tax_rt === 0 ) { const res = await confirmDialog( `ImEX Online has detected that there is a missing tax rate for rechromed parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.` ); if (res) { if (!estData.parts_tax_rates.PAC) { estData.parts_tax_rates.PAC = { prt_discp: 0, prt_mktyp: true, prt_mkupp: 0, prt_type: "PAC", }; } estData.parts_tax_rates.PAC.prt_tax_rt = bodyshop.bill_tax_rates.state_tax_rate / 100; estData.parts_tax_rates.PAC.prt_tax_in = true; } } //PAM Check if ( !estData.parts_tax_rates?.PAM || estData.parts_tax_rates?.PAM?.prt_tax_rt === null || estData.parts_tax_rates?.PAM?.prt_tax_rt === 0 ) { const res = await confirmDialog( `ImEX Online has detected that there is a missing tax rate for remanufactured parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.` ); if (res) { if (!estData.parts_tax_rates.PAM) { estData.parts_tax_rates.PAM = { prt_discp: 0, prt_mktyp: true, prt_mkupp: 0, prt_type: "PAM", }; } estData.parts_tax_rates.PAM.prt_tax_rt = bodyshop.bill_tax_rates.state_tax_rate / 100; estData.parts_tax_rates.PAM.prt_tax_in = true; } } if ( !estData.parts_tax_rates?.PAR || estData.parts_tax_rates?.PAR?.prt_tax_rt === null || estData.parts_tax_rates?.PAR?.prt_tax_rt === 0 ) { const res = await confirmDialog( `ImEX Online has detected that there is a missing tax rate for recored parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.` ); if (res) { if (!estData.parts_tax_rates.PAR) { estData.parts_tax_rates.PAR = { prt_discp: 0, prt_mktyp: true, prt_mkupp: 0, prt_type: "PAR", }; } estData.parts_tax_rates.PAR.prt_tax_rt = bodyshop.bill_tax_rates.state_tax_rate / 100; estData.parts_tax_rates.PAR.prt_tax_in = true; } } //IO-1387 If a sublet line is NOT R&R, use the labor tax. If it is, use the sublet tax rate. //Currently limited to SK shops only. //if (bodyshop.region_config === "CA_SK") { estData.joblines.data.forEach((jl, index) => { if ( (jl.part_type === "PASL" || jl.part_type === "PAS") && jl.lbr_op !== "OP11" ) { estData.joblines.data[index].tax_part = jl.lbr_tax; } //Set markup lines and tax lines as taxable. //900510 is a mark up. 900510 is a discount. if (jl.db_ref === "900510") { estData.joblines.data[index].tax_part = true; } }); //} }