import {useApolloClient, useMutation} from "@apollo/client"; import {useSplitTreatments} from "@splitsoftware/splitio-react"; import {Button, Checkbox, Form, Modal, notification, Space} from "antd"; import _ from "lodash"; import React, {useEffect, useMemo, useState} from "react"; import {useTranslation} from "react-i18next"; import {connect} from "react-redux"; import {createStructuredSelector} from "reselect"; import {INSERT_NEW_BILL} from "../../graphql/bills.queries"; import {UPDATE_INVENTORY_LINES} from "../../graphql/inventory.queries"; import {UPDATE_JOB_LINE} from "../../graphql/jobs-lines.queries"; import {QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB,} from "../../graphql/jobs.queries"; import {MUTATION_MARK_RETURN_RECEIVED} from "../../graphql/parts-orders.queries"; import {insertAuditTrail} from "../../redux/application/application.actions"; import {toggleModalVisible} from "../../redux/modals/modals.actions"; import {selectBillEnterModal} from "../../redux/modals/modals.selectors"; import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors"; import AuditTrailMapping from "../../utils/AuditTrailMappings"; import {GenerateDocument} from "../../utils/RenderTemplate"; import {TemplateList} from "../../utils/TemplateConstants"; import confirmDialog from "../../utils/asyncConfirm"; import useLocalStorage from "../../utils/useLocalStorage"; import BillFormContainer from "../bill-form/bill-form.container"; import {CalculateBillTotal} from "../bill-form/bill-form.totals.utility"; import {handleUpload as handleLocalUpload} from "../documents-local-upload/documents-local-upload.utility"; import {handleUpload} from "../documents-upload/documents-upload.utility"; const mapStateToProps = createStructuredSelector({ billEnterModal: selectBillEnterModal, bodyshop: selectBodyshop, currentUser: selectCurrentUser, }); const mapDispatchToProps = (dispatch) => ({ toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")), insertAuditTrail: ({jobid, billid, operation}) => dispatch(insertAuditTrail({jobid, billid, operation})), }); const Templates = TemplateList("job_special"); function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop, currentUser, insertAuditTrail, }) { const [form] = Form.useForm(); const {t} = useTranslation(); const [enterAgain, setEnterAgain] = useState(false); const [insertBill] = useMutation(INSERT_NEW_BILL); const [updateJobLines] = useMutation(UPDATE_JOB_LINE); const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED); const [updateInventoryLines] = useMutation(UPDATE_INVENTORY_LINES); const [loading, setLoading] = useState(false); const client = useApolloClient(); const [generateLabel, setGenerateLabel] = useLocalStorage( "enter_bill_generate_label", false ); const {treatments: {Enhanced_Payroll}} = useSplitTreatments({ attributes: {}, names: ["Enhanced_Payroll"], splitKey: bodyshop.imexshopid, }); const formValues = useMemo(() => { return { ...billEnterModal.context.bill, //Added as a part of IO-2436 for capturing parts price changes. billlines: billEnterModal.context?.bill?.billlines?.map((line) => ({ ...line, original_actual_price: line.actual_price, })), jobid: (billEnterModal.context.job && billEnterModal.context.job.id) || null, federal_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) || 0, state_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) || 0, local_tax_rate: (bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) || 0, }; }, [billEnterModal, bodyshop]); const handleFinish = async (values) => { let totals = CalculateBillTotal(values); if (totals.discrepancy.getAmount() !== 0) { if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) { return; } } setLoading(true); const { upload, location, outstanding_returns, inventory, ...remainingValues } = values; let adjustmentsToInsert = {}; let payrollAdjustmentsToInsert = []; const r1 = await insertBill({ variables: { bill: [ { ...remainingValues, billlines: { data: remainingValues.billlines && remainingValues.billlines.map((i) => { const { deductedfromlbr, lbr_adjustment, location: lineLocation, part_type, create_ppc, original_actual_price, ...restI } = i; if (Enhanced_Payroll.treatment === "on") { if ( deductedfromlbr && true //payroll is on ) { payrollAdjustmentsToInsert.push({ id: i.joblineid, convertedtolbr: true, convertedtolbr_data: { mod_lb_hrs: lbr_adjustment.mod_lb_hrs * -1, mod_lbr_ty: lbr_adjustment.mod_lbr_ty, }, }); } } else { if (deductedfromlbr) { adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] = (adjustmentsToInsert[lbr_adjustment.mod_lbr_ty] || 0) - restI.actual_price / lbr_adjustment.rate; } } return { ...restI, deductedfromlbr: deductedfromlbr, lbr_adjustment, joblineid: i.joblineid === "noline" ? null : i.joblineid, applicable_taxes: { federal: (i.applicable_taxes && i.applicable_taxes.federal) || false, state: (i.applicable_taxes && i.applicable_taxes.state) || false, local: (i.applicable_taxes && i.applicable_taxes.local) || false, }, }; }), }, }, ], }, refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"], }); await Promise.all( payrollAdjustmentsToInsert.map((li) => { return updateJobLines({ variables: { lineId: li.id, line: { convertedtolbr: li.convertedtolbr, convertedtolbr_data: li.convertedtolbr_data, }, }, }); }) ); const adjKeys = Object.keys(adjustmentsToInsert); if (adjKeys.length > 0) { //Query the adjustments, merge, and update them. const existingAdjustments = await client.query({ query: QUERY_JOB_LBR_ADJUSTMENTS, variables: { id: values.jobid, }, }); const newAdjustments = _.cloneDeep( existingAdjustments.data.jobs_by_pk.lbr_adjustments ); adjKeys.forEach((key) => { newAdjustments[key] = (newAdjustments[key] || 0) + adjustmentsToInsert[key]; insertAuditTrail({ jobid: values.jobid, operation: AuditTrailMapping.jobmodifylbradj({ mod_lbr_ty: key, hours: adjustmentsToInsert[key].toFixed(1), }), }); }); const jobUpdate = client.mutate({ mutation: UPDATE_JOB, variables: { jobId: values.jobid, job: {lbr_adjustments: newAdjustments}, }, }); if (!!jobUpdate.errors) { notification["error"]({ message: t("jobs.errors.saving", { message: JSON.stringify(jobUpdate.errors), }), }); return; } } const markPolReceived = outstanding_returns && outstanding_returns.filter((o) => o.cm_received === true); if (markPolReceived && markPolReceived.length > 0) { const r2 = await updatePartsOrderLines({ variables: {partsLineIds: markPolReceived.map((p) => p.id)}, }); if (!!r2.errors) { setLoading(false); setEnterAgain(false); notification["error"]({ message: t("parts_orders.errors.updating", { message: JSON.stringify(r2.errors), }), }); } } if (!!r1.errors) { setLoading(false); setEnterAgain(false); notification["error"]({ message: t("bills.errors.creating", { message: JSON.stringify(r1.errors), }), }); } const billId = r1.data.insert_bills.returning[0].id; const markInventoryConsumed = inventory && inventory.filter((i) => i.consumefrominventory); if (markInventoryConsumed && markInventoryConsumed.length > 0) { const r2 = await updateInventoryLines({ variables: { InventoryIds: markInventoryConsumed.map((p) => p.id), consumedbybillid: billId, }, }); if (!!r2.errors) { setLoading(false); setEnterAgain(false); notification["error"]({ message: t("inventory.errors.updating", { message: JSON.stringify(r2.errors), }), }); } } //If it's not a credit memo, update the statuses. if (!values.is_credit_memo) { await Promise.all( remainingValues.billlines .filter((il) => il.joblineid !== "noline") .map((li) => { return updateJobLines({ variables: { lineId: li.joblineid, line: { location: li.location || location, status: bodyshop.md_order_statuses.default_received || "Received*", //Added parts price changes. ...(li.create_ppc && li.original_actual_price !== li.actual_price ? { act_price_before_ppc: li.original_actual_price, act_price: li.actual_price, } : {}), }, }, }); }) ); } ///////////////////////// if (upload && upload.length > 0) { //insert Each of the documents? if (bodyshop.uselocalmediaserver) { upload.forEach((u) => { handleLocalUpload({ ev: {file: u.originFileObj}, context: { jobid: values.jobid, invoice_number: remainingValues.invoice_number, vendorid: remainingValues.vendorid, }, }); }); } else { upload.forEach((u) => { handleUpload( {file: u.originFileObj}, { bodyshop: bodyshop, uploaded_by: currentUser.email, jobId: values.jobid, billId: billId, tagsArray: null, callback: null, } ); }); } } /////////////////////////// setLoading(false); notification["success"]({ message: t("bills.successes.created"), }); if (generateLabel) { GenerateDocument( { name: Templates.parts_invoice_label_single.key, variables: { id: billId, }, }, {}, "p" ); } if (billEnterModal.actions.refetch) billEnterModal.actions.refetch(); insertAuditTrail({ jobid: values.jobid, billid: billId, operation: AuditTrailMapping.billposted( r1.data.insert_bills.returning[0].invoice_number ), }); if (enterAgain) { // form.resetFields(); form.setFieldsValue({ ...formValues, billlines: [], }); form.resetFields(); } else { toggleModalVisible(); } setEnterAgain(false); }; const handleCancel = () => { const r = window.confirm(t("general.labels.cancel")); if (r === true) { toggleModalVisible(); } }; useEffect(() => { if (enterAgain) form.submit(); }, [enterAgain, form]); useEffect(() => { if (billEnterModal.open) { form.setFieldsValue(formValues); } else { form.resetFields(); } }, [billEnterModal.open, form, formValues]); return ( form.submit()} onCancel={handleCancel} afterClose={() => { form.resetFields(); setLoading(false); }} footer={ setGenerateLabel(e.target.checked)} > {t("bills.labels.generatepartslabel")} {billEnterModal.context && billEnterModal.context.id ? null : ( )} } destroyOnClose >
{ setEnterAgain(false); }} >
); } export default connect( mapStateToProps, mapDispatchToProps )(BillEnterModalContainer);