diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 73e9c07c0..8e12e6d81 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -6135,6 +6135,221 @@ + + md_ro_guard + + + enabled + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_ar + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_bills + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_cm + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_labor + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_ppd + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_profit + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_sublet + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + masterbypass + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + totalgppercent_minimum + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + md_tasks_presets @@ -10663,6 +10878,27 @@ + + md_ro_guard + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + md_tasks_presets false @@ -11067,6 +11303,32 @@ + + roguard + + + title + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + scheduling false @@ -21835,6 +22097,27 @@ + + act_price_before_ppc + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + ah_detail_line false @@ -30775,6 +31058,27 @@ labels + + accountsreceivable + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + act_price_ppc false @@ -33089,6 +33393,27 @@ + + masterbypass + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + materials @@ -33262,6 +33587,27 @@ + + outstanding_ppd + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + outstanding_reconciliation_discrep false @@ -33514,6 +33860,27 @@ + + performance + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + pimraryamountpayable false @@ -33772,7 +34139,7 @@ - profileadjustments + ppdnotexported false @@ -33793,7 +34160,7 @@ - profitbypassrequired + profileadjustments false @@ -34175,6 +34542,74 @@ + + ro_guard + + + enforce_validation + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforced + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + roguard + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + roguardwarnings false diff --git a/client/src/components/job-bills-total/job-bills-total.component.jsx b/client/src/components/job-bills-total/job-bills-total.component.jsx index e48f52bce..1c9f2a3c2 100644 --- a/client/src/components/job-bills-total/job-bills-total.component.jsx +++ b/client/src/components/job-bills-total/job-bills-total.component.jsx @@ -1,326 +1,316 @@ -import { Alert, Card, Col, Row, Space, Statistic, Tooltip, Typography } from "antd"; -import Dinero from "dinero.js"; -import React from "react"; -import { useTranslation } from "react-i18next"; -import InstanceRenderManager from "../../utils/instanceRenderMgr"; -import AlertComponent from "../alert/alert.component"; -import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; -import "./job-bills-total.styles.scss"; +import { Alert, Card, Col, Row, Space, Statistic, Tooltip, Typography } from 'antd'; +import Dinero from 'dinero.js'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import InstanceRenderManager from '../../utils/instanceRenderMgr'; +import AlertComponent from '../alert/alert.component'; +import LoadingSkeleton from '../loading-skeleton/loading-skeleton.component'; +import './job-bills-total.styles.scss'; export default function JobBillsTotalComponent({ - loading, - bills, - partsOrders, - jobTotals, - showWarning, - warningCallback - }) { - const {t} = useTranslation(); + loading, + bills, + partsOrders, + jobTotals, + showWarning, + warningCallback, +}) { + const { t } = useTranslation(); - if (loading) return ; - if (!!!jobTotals){ - -if(showWarning && warningCallback && typeof warningCallback === 'function'){ - warningCallback(t("jobs.errors.nofinancial")) -} - return ( - - ); - } - - const totals = jobTotals; - - let billTotals = Dinero(); - let billCms = Dinero(); - let lbrAdjustments = Dinero(); - let totalReturns = Dinero(); - let totalReturnsMarkedNotReceived = Dinero(); - let totalReturnsMarkedReceived = Dinero(); - - partsOrders.forEach((p) => - p.parts_order_lines.forEach((pol) => { - if (p.return) { - totalReturns = totalReturns.add( - Dinero({ - amount: Math.round((pol.act_price || 0) * 100), - }).multiply(pol.quantity) - ); - - if (pol.cm_received === null) { - return; //TODO:AIO This was previously removed. Check if functionality impacted. - // Skip this calculation for bills posted prior to the CNR change. - } else { - if (pol.cm_received === false) { - totalReturnsMarkedNotReceived = totalReturnsMarkedNotReceived.add( - Dinero({ - amount: Math.round((pol.act_price || 0) * 100), - }).multiply(pol.quantity) - ); - } else { - totalReturnsMarkedReceived = totalReturnsMarkedReceived.add( - Dinero({ - amount: Math.round((pol.act_price || 0) * 100), - }).multiply(pol.quantity) - ); - } - } - } - }) - ); - - bills.forEach((i) => - i.billlines.forEach((il) => { - if (!i.is_credit_memo) { - billTotals = billTotals.add( - Dinero({ - amount: Math.round((il.actual_price || 0) * 100), - }).multiply(il.quantity) - ); - } else { - billCms = billCms.add( - Dinero({ - amount: Math.round((il.actual_price || 0) * 100), - }).multiply(il.quantity) - ); - } - if (il.deductedfromlbr) { - lbrAdjustments = lbrAdjustments.add( - Dinero({ - amount: Math.round((il.actual_price || 0) * 100), - }).multiply(il.quantity) - ); - } - }) - ); - - const totalPartsSublet = Dinero(totals.parts.parts.total) - .add(Dinero(totals.parts.sublets.total)) - .add(Dinero(totals.additional.shipping)) - .add(Dinero(totals.additional.towing)) - .add( InstanceRenderManager({imex: Dinero(), rome: Dinero(totals.additional.additionalCosts),promanager: "USE_ROME" })) ; // Additional costs were captured for Rome, but not imex. - - const discrepancy = totalPartsSublet.subtract(billTotals); - - const discrepWithLbrAdj = discrepancy.add(lbrAdjustments); - - const discrepWithCms = discrepWithLbrAdj.add(totalReturns); - const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number. - - + if (loading) return ; + if (!!!jobTotals) { if (showWarning && warningCallback && typeof warningCallback === 'function') { - if ( - discrepWithCms.getAmount() !== 0 || - discrepWithLbrAdj.getAmount() !== 0 || - discrepancy.getAmount() !== 0 - ) { - warningCallback(t('jobs.labels.outstanding_reconciliation_discrep')); - } - if (calculatedCreditsNotReceived.getAmount() > 0) { - warningCallback(t('jobs.labels.outstanding_credit_memos')); - } + warningCallback({ key: 'bills', warning: t('jobs.errors.nofinancial') }); } + return ; + } + const totals = jobTotals; - return ( - - - - - - } - > - - - - - - } - > - - - = - - } - > - - - + - - } - > - - - = - - } - > - - - + - - } - > - - - = - - } - > - - - + let billTotals = Dinero(); + let billCms = Dinero(); + let lbrAdjustments = Dinero(); + let totalReturns = Dinero(); + let totalReturnsMarkedNotReceived = Dinero(); + let totalReturnsMarkedReceived = Dinero(); - {showWarning && ( - discrepWithCms.getAmount() !== 0 || - discrepWithLbrAdj.getAmount() !== 0 || - discrepancy.getAmount() !== 0 - ) && } - - - - - - - } - > - - - - } - > - = 0 - ? calculatedCreditsNotReceived.toFormat() - : Dinero().toFormat() - } - /> - - - } - > - = 0 - ? totalReturnsMarkedNotReceived.toFormat() - : Dinero().toFormat() - } - /> - - - {showWarning && ( - calculatedCreditsNotReceived.getAmount() > 0 - ) && } - - - - ); + partsOrders.forEach((p) => + p.parts_order_lines.forEach((pol) => { + if (p.return) { + totalReturns = totalReturns.add( + Dinero({ + amount: Math.round((pol.act_price || 0) * 100), + }).multiply(pol.quantity) + ); + + if (pol.cm_received === null) { + return; //TODO:AIO This was previously removed. Check if functionality impacted. + // Skip this calculation for bills posted prior to the CNR change. + } else { + if (pol.cm_received === false) { + totalReturnsMarkedNotReceived = totalReturnsMarkedNotReceived.add( + Dinero({ + amount: Math.round((pol.act_price || 0) * 100), + }).multiply(pol.quantity) + ); + } else { + totalReturnsMarkedReceived = totalReturnsMarkedReceived.add( + Dinero({ + amount: Math.round((pol.act_price || 0) * 100), + }).multiply(pol.quantity) + ); + } + } + } + }) + ); + + bills.forEach((i) => + i.billlines.forEach((il) => { + if (!i.is_credit_memo) { + billTotals = billTotals.add( + Dinero({ + amount: Math.round((il.actual_price || 0) * 100), + }).multiply(il.quantity) + ); + } else { + billCms = billCms.add( + Dinero({ + amount: Math.round((il.actual_price || 0) * 100), + }).multiply(il.quantity) + ); + } + if (il.deductedfromlbr) { + lbrAdjustments = lbrAdjustments.add( + Dinero({ + amount: Math.round((il.actual_price || 0) * 100), + }).multiply(il.quantity) + ); + } + }) + ); + + const totalPartsSublet = Dinero(totals.parts.parts.total) + .add(Dinero(totals.parts.sublets.total)) + .add(Dinero(totals.additional.shipping)) + .add(Dinero(totals.additional.towing)) + .add( + InstanceRenderManager({ + imex: Dinero(), + rome: Dinero(totals.additional.additionalCosts), + promanager: 'USE_ROME', + }) + ); // Additional costs were captured for Rome, but not imex. + + const discrepancy = totalPartsSublet.subtract(billTotals); + + const discrepWithLbrAdj = discrepancy.add(lbrAdjustments); + + const discrepWithCms = discrepWithLbrAdj.add(totalReturns); + const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number. + + if (showWarning && warningCallback && typeof warningCallback === 'function') { + if ( + discrepWithCms.getAmount() !== 0 || + discrepWithLbrAdj.getAmount() !== 0 || + discrepancy.getAmount() !== 0 + ) { + warningCallback({ + key: 'bills', + warning: t('jobs.labels.outstanding_reconciliation_discrep'), + }); + } + if (calculatedCreditsNotReceived.getAmount() > 0) { + warningCallback({ key: 'cm', warning: t('jobs.labels.outstanding_credit_memos') }); + } + } + + return ( + + + + + + } + > + + + - + + } + > + + + = + + } + > + + + + + + } + > + + + = + + } + > + + + + + + } + > + + + = + + } + > + + + + + {showWarning && + (discrepWithCms.getAmount() !== 0 || + discrepWithLbrAdj.getAmount() !== 0 || + discrepancy.getAmount() !== 0) && ( + + )} + + + + + + + } + > + + + + } + > + = 0 + ? calculatedCreditsNotReceived.toFormat() + : Dinero().toFormat() + } + /> + + + } + > + = 0 + ? totalReturnsMarkedNotReceived.toFormat() + : Dinero().toFormat() + } + /> + + + {showWarning && calculatedCreditsNotReceived.getAmount() > 0 && ( + + )} + + + + ); } diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.ar.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.ar.jsx index 7a096a179..1bd4cd575 100644 --- a/client/src/components/job-close-ro-guard/job-close-ro-guard.ar.jsx +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.ar.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Alert, Card } from 'antd'; import { useTranslation } from 'react-i18next'; @@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch) => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardProfit); -export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form }) { +export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallback }) { const { t } = useTranslation(); const total = useMemo(() => { @@ -39,6 +39,12 @@ export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form }) { return Dinero({ amount: 0 }).subtract(total); }, [job, total]); + useEffect(() => { + if (balance.getAmount !== 0) { + warningCallback({ key: 'ar', warning: t('jobs.labels.outstanding_ar') }); + } + }, [balance, t, warningCallback]); + return ( {total.toFormat()} @@ -49,7 +55,11 @@ export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form }) { {balance.toFormat()} {balance.getAmount !== 0 && ( - + )} ); diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx index acbe5536f..0fe5f076f 100644 --- a/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx @@ -1,6 +1,6 @@ import React, { useCallback, useState } from 'react'; - -import { Badge, Card, Col, Collapse, Row, Space } from 'antd'; +import { LockOutlined } from '@ant-design/icons'; +import { Badge, Card, Col, Collapse, Form, Input, Row, Space, Tooltip } from 'antd'; import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { createStructuredSelector } from 'reselect'; @@ -25,16 +25,20 @@ const mapDispatchToProps = (dispatch) => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardContainer); -const colSpans = { - md: 24, - lg: 12, - xl: 8, -}; export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) { const { t } = useTranslation(); const [warnings, setWarnings] = useState([]); - const warningCallback = useCallback((warning) => setWarnings((state) => [...state, warning]), []); + const warningCallback = useCallback( + ({ key, warning }) => + setWarnings((state) => { + if (state.find((s) => s.key === key)) return state; + return [...state, { key, warning }]; + }), + [] + ); + + if (!bodyshop?.md_ro_guard?.enabled) return null; return ( <> @@ -48,8 +52,15 @@ export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) { } >
    - {warnings.map((warning, index) => ( -
  • {warning}
  • + {warnings.map((w, index) => ( +
  • + {bodyshop.md_ro_guard[`enforce_${w.key}`] && ( + + + + )} + {w.warning} +
  • ))}
@@ -72,16 +83,135 @@ export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) { - + + - - - + ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_bills && + warnings.find((w) => w.key === 'bills') + ) { + return Promise.reject(t('jobs.labels.ro_guard.enforce_bills')); + } + return Promise.resolve(); + }, + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_cm && + warnings.find((w) => w.key === 'cm') + ) { + return Promise.reject( + t('translation.jobs.labels.ro_guard.enforce_validation', { + message: t('jobs.labels.ro_guard.enforce_cm'), + }) + ); + } + return Promise.resolve(); + }, + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_profit && + warnings.find((w) => w.key === 'profit') + ) { + return Promise.reject( + t('translation.jobs.labels.ro_guard.enforce_validation', { + message: t('jobs.labels.ro_guard.enforce_profit'), + }) + ); + } + return Promise.resolve(); + }, + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_ar && + warnings.find((w) => w.key === 'ar') + ) { + return Promise.reject( + t('translation.jobs.labels.ro_guard.enforce_validation', { + message: t('jobs.labels.ro_guard.enforce_ar'), + }) + ); + } + return Promise.resolve(); + }, + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_sublet && + warnings.find((w) => w.key === 'sublet') + ) { + return Promise.reject( + t('translation.jobs.labels.ro_guard.enforce_validation', { + message: t('jobs.labels.ro_guard.enforce_sublet'), + }) + ); + } + return Promise.resolve(); + }, + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_ppd && + warnings.find((w) => w.key === 'ppd') + ) { + return Promise.reject( + t('translation.jobs.labels.ro_guard.enforce_validation', { + message: t('jobs.labels.ro_guard.enforce_ppd'), + }) + ); + } + return Promise.resolve(); + }, + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_labor && + warnings.find((w) => w.key === 'labor') + ) { + return Promise.reject( + t('translation.jobs.labels.ro_guard.enforce_validation', { + message: t('jobs.labels.ro_guard.enforce_labor'), + }) + ); + } + return Promise.resolve(); + }, + }), + ]} + > + } + type="password" + placeholder="Password" + disabled={jobRO} + /> + @@ -95,3 +225,7 @@ export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) { ); } + +function PasswordCheck({ bodyshop, value }) { + return value === bodyshop?.md_ro_guard?.masterbypass; +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.ppd.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.ppd.jsx index 372b19cc8..e445427ce 100644 --- a/client/src/components/job-close-ro-guard/job-close-ro-guard.ppd.jsx +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.ppd.jsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useEffect } from 'react'; -import { Card, Table } from 'antd'; +import { Alert, Card, Table } from 'antd'; import { t } from 'i18next'; import { connect } from 'react-redux'; import { createStructuredSelector } from 'reselect'; @@ -18,11 +18,17 @@ const mapDispatchToProps = (dispatch) => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRGuardPpd); -export function JobCloseRGuardPpd({ job, jobRO, bodyshop, form }) { - const subletsNotDone = job?.joblines.filter( +export function JobCloseRGuardPpd({ job, jobRO, bodyshop, form, warningCallback }) { + const linesWithPPD = job?.joblines.filter( (j) => j.act_price_before_ppc !== 0 && j.act_price_before_ppc !== null ); + useEffect(() => { + if (linesWithPPD.length > 0) { + warningCallback({ key: 'ppd', warning: t('jobs.labels.outstanding_sublets') }); + } + }, [linesWithPPD.length, warningCallback]); + const columns = [ { title: t('joblines.fields.line_desc'), @@ -71,15 +77,22 @@ export function JobCloseRGuardPpd({ job, jobRO, bodyshop, form }) { ]; return ( - + + {linesWithPPD.length > 0 && ( + + )} ); } diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.profit.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.profit.jsx index 1cb10d4f0..4cbbca357 100644 --- a/client/src/components/job-close-ro-guard/job-close-ro-guard.profit.jsx +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.profit.jsx @@ -42,11 +42,11 @@ export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallb }, [job.id]); const enforceProfitPassword = - parseFloat(costingData?.summaryData.gppercent) < bodyshop?.md_ro_guard?.totalgppercent_minimum; //TODO Add bodyshop related values. + parseFloat(costingData?.summaryData.gppercent) < bodyshop?.md_ro_guard?.totalgppercent_minimum; useEffect(() => { if (enforceProfitPassword && typeof warningCallback === 'function') { - warningCallback(t('jobs.labels.profitbypassrequired')); + warningCallback({ key: 'profit', warning: t('jobs.labels.profitbypassrequired') }); } }, [enforceProfitPassword, t, warningCallback]); @@ -55,32 +55,6 @@ export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallb return ( - - {enforceProfitPassword && ( - ({ - validator(_, value) { - if ( - parseFloat(costingData?.summaryData.gppercent) < - bodyshop?.md_ro_guard?.totalgppercent_minimum && - value !== bodyshop.md_ro_guard.profitbypasspassword - ) { - return Promise.reject(t('jobs.labels.profitbypassrequired')); - } - return Promise.resolve(); - }, - }), - ]} - > - } type="password" placeholder="Password" /> - - )} ); } diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.styles.scss b/client/src/components/job-close-ro-guard/job-close-ro-guard.styles.scss index a00fe4e07..51f790b77 100644 --- a/client/src/components/job-close-ro-guard/job-close-ro-guard.styles.scss +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.styles.scss @@ -3,3 +3,8 @@ height: 100%; } } +.ro-guard-col-50 { + .ant-card { + height: 50%; + } +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.sublet.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.sublet.jsx index 14a029def..e44e58adf 100644 --- a/client/src/components/job-close-ro-guard/job-close-ro-guard.sublet.jsx +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.sublet.jsx @@ -63,7 +63,7 @@ export function JobCloseRGuardSublet({ job, jobRO, bodyshop, form, warningCallba useEffect(() => { if (subletsNotDone.length > 0) { - warningCallback(t('jobs.labels.outstanding_sublets')); + warningCallback({ key: 'sublet', warning: t('jobs.labels.outstanding_sublets') }); } }, [subletsNotDone.length, warningCallback]); diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.tt-lifecycle.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.tt-lifecycle.jsx index f9df8f64f..1b0f60514 100644 --- a/client/src/components/job-close-ro-guard/job-close-ro-guard.tt-lifecycle.jsx +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.tt-lifecycle.jsx @@ -18,7 +18,6 @@ export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardTTLif export function JobCloseRoGuardTTLifeCycle({ job, jobRO, bodyshop, form }) { return (
- //TODO Add Touch Time
); diff --git a/client/src/components/labor-allocations-table/labor-allocations-table.component.jsx b/client/src/components/labor-allocations-table/labor-allocations-table.component.jsx index deecd950f..c047a3193 100644 --- a/client/src/components/labor-allocations-table/labor-allocations-table.component.jsx +++ b/client/src/components/labor-allocations-table/labor-allocations-table.component.jsx @@ -205,7 +205,7 @@ export function LaborAllocationsTable({ ); if (summary.difference !== 0 && typeof warningCallback === 'function') { - warningCallback(t('jobs.labels.outstandinghours')); + warningCallback({key: 'labor',warning: t('jobs.labels.outstandinghours')}); } diff --git a/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx b/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx index e466f4aeb..c717bff22 100644 --- a/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx +++ b/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx @@ -219,7 +219,7 @@ export function PayrollLaborAllocationsTable({ ); if (summary.difference !== 0 && typeof warningCallback === 'function') { - warningCallback(t('jobs.labels.outstandinghours')); + warningCallback({key: 'labor', warning: t('jobs.labels.outstandinghours')}); } return ( diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx index ae116411b..adc92d711 100644 --- a/client/src/components/shop-info/shop-info.component.jsx +++ b/client/src/components/shop-info/shop-info.component.jsx @@ -1,130 +1,147 @@ -import {useSplitTreatments} from "@splitsoftware/splitio-react"; -import {Button, Card, Tabs} from "antd"; -import React from "react"; -import {useTranslation} from "react-i18next"; -import {connect} from "react-redux"; -import {createStructuredSelector} from "reselect"; -import {selectBodyshop} from "../../redux/user/user.selectors"; -import ShopInfoGeneral from "./shop-info.general.component"; -import ShopInfoIntakeChecklistComponent from "./shop-info.intake.component"; -import ShopInfoLaborRates from "./shop-info.laborrates.component"; -import ShopInfoOrderStatusComponent from "./shop-info.orderstatus.component"; -import ShopInfoPartsScan from "./shop-info.parts-scan"; -import ShopInfoRbacComponent from "./shop-info.rbac.component"; -import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycenters.component"; -import ShopInfoROStatusComponent from "./shop-info.rostatus.component"; -import ShopInfoSchedulingComponent from "./shop-info.scheduling.component"; -import ShopInfoSpeedPrint from "./shop-info.speedprint.component"; -import {useLocation, useNavigate} from "react-router-dom"; -import ShopInfoTaskPresets from "./shop-info.task-presets.component"; -import queryString from "query-string"; -import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import { useSplitTreatments } from '@splitsoftware/splitio-react'; +import { Button, Card, Tabs } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; +import { createStructuredSelector } from 'reselect'; +import { selectBodyshop } from '../../redux/user/user.selectors'; +import ShopInfoGeneral from './shop-info.general.component'; +import ShopInfoIntakeChecklistComponent from './shop-info.intake.component'; +import ShopInfoLaborRates from './shop-info.laborrates.component'; +import ShopInfoOrderStatusComponent from './shop-info.orderstatus.component'; +import ShopInfoPartsScan from './shop-info.parts-scan'; +import ShopInfoRbacComponent from './shop-info.rbac.component'; +import ShopInfoResponsibilityCenterComponent from './shop-info.responsibilitycenters.component'; +import ShopInfoROStatusComponent from './shop-info.rostatus.component'; +import ShopInfoSchedulingComponent from './shop-info.scheduling.component'; +import ShopInfoSpeedPrint from './shop-info.speedprint.component'; +import { useLocation, useNavigate } from 'react-router-dom'; +import ShopInfoTaskPresets from './shop-info.task-presets.component'; +import queryString from 'query-string'; +import InstanceRenderManager from '../../utils/instanceRenderMgr'; +import ShopInfoRoGuard from './shop-info.roguard.component'; const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, + bodyshop: selectBodyshop, }); const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) + //setUserLanguage: language => dispatch(setUserLanguage(language)) }); export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent); -export function ShopInfoComponent({bodyshop, form, saveLoading}) { +export function ShopInfoComponent({ bodyshop, form, saveLoading }) { + const { + treatments: { CriticalPartsScanning, Enhanced_Payroll }, + } = useSplitTreatments({ + attributes: {}, + names: ['CriticalPartsScanning', 'Enhanced_Payroll'], + splitKey: bodyshop.imexshopid, + }); - const {treatments: {CriticalPartsScanning, Enhanced_Payroll}} = useSplitTreatments({ - attributes: {}, - names: ["CriticalPartsScanning","Enhanced_Payroll"], - splitKey: bodyshop.imexshopid, - }); + const { t } = useTranslation(); + const history = useNavigate(); + const location = useLocation(); + const search = queryString.parse(location.search); - const {t} = useTranslation(); - const history = useNavigate(); - const location = useLocation(); - const search = queryString.parse(location.search); - - const tabItems = [ + const tabItems = [ + { + key: 'general', + label: t('bodyshop.labels.shopinfo'), + children: , + }, + { + key: 'speedprint', + label: t('bodyshop.labels.speedprint'), + children: , + }, + { + key: 'rbac', + label: t('bodyshop.labels.rbac'), + children: , + }, + { + key: 'roStatus', + label: t('bodyshop.labels.jobstatuses'), + children: , + }, + { + key: 'scheduling', + label: t('bodyshop.labels.scheduling'), + children: , + }, + { + key: 'orderStatus', + label: t('bodyshop.labels.orderstatuses'), + children: , + }, + { + key: 'responsibilityCenters', + label: t('bodyshop.labels.responsibilitycenters.title'), + children: , + }, + ...InstanceRenderManager({ + imex: [ { - key: "general", - label: t("bodyshop.labels.shopinfo"), - children: , + key: 'checklists', + label: t('bodyshop.labels.checklists'), + children: , }, + ], + rome: 'USE_IMEX', + promanager: [], + }), + { + key: 'laborrates', + label: t('bodyshop.labels.laborrates'), + children: , + }, + ...(CriticalPartsScanning.treatment === 'on' + ? [ + { + key: 'partsscan', + label: t('bodyshop.labels.partsscan'), + children: , + }, + ] + : []), + ...(Enhanced_Payroll.treatment === 'on' + ? [ + { + key: 'task-presets', + label: t('bodyshop.labels.task-presets'), + children: , + }, + ] + : []), + ...InstanceRenderManager({ + imex: [ { - key: "speedprint", - label: t("bodyshop.labels.speedprint"), - children: , + key: 'roguard', + label: t('bodyshop.labels.roguard.title'), + children: , }, - { - key: "rbac", - label: t("bodyshop.labels.rbac"), - children: , - }, - { - key: "roStatus", - label: t("bodyshop.labels.jobstatuses"), - children: , - }, - { - key: "scheduling", - label: t("bodyshop.labels.scheduling"), - children: , - }, - { - key: "orderStatus", - label: t("bodyshop.labels.orderstatuses"), - children: , - }, - { - key: "responsibilityCenters", - label: t("bodyshop.labels.responsibilitycenters.title"), - children: , - }, - ...InstanceRenderManager({imex: [ { - key: "checklists", - label: t("bodyshop.labels.checklists"), - children: , - }], rome: "USE_IMEX", promanager:[]}) - , - { - key: "laborrates", - label: t("bodyshop.labels.laborrates"), - children: , - }, - ...(CriticalPartsScanning.treatment === "on" - ? [ - { - key: "partsscan", - label: t("bodyshop.labels.partsscan"), - children: , - }, - ] - : []), - ...Enhanced_Payroll.treatment === "on" ? [ - { - key: 'task-presets', - label: t("bodyshop.labels.task-presets"), - children: - }]: [] - ]; - return ( - form.submit()} - > - {t("general.actions.save")} - - } - > - - history({ - search: `?tab=${search.tab}&subtab=${key}`, - }) - } - items={tabItems} - /> - - ); + ], + rome: 'USE_IMEX', + promanager: [], + }), + ]; + return ( + form.submit()}> + {t('general.actions.save')} + + } + > + + history({ + search: `?tab=${search.tab}&subtab=${key}`, + }) + } + items={tabItems} + /> + + ); } diff --git a/client/src/components/shop-info/shop-info.roguard.component.jsx b/client/src/components/shop-info/shop-info.roguard.component.jsx new file mode 100644 index 000000000..e58134482 --- /dev/null +++ b/client/src/components/shop-info/shop-info.roguard.component.jsx @@ -0,0 +1,111 @@ +import { Form, Input, InputNumber, Switch } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import LayoutFormRow from '../layout-form-row/layout-form-row.component'; + +export default function ShopInfoRoGuard({ form }) { + const { t } = useTranslation(); + + return ( +
+ + + + + + + {() => { + const disabled = !form.getFieldValue(['md_ro_guard', 'enabled']); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }} + +
+ ); +} diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index a8efb8d0b..b449762ad 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -136,6 +136,7 @@ export const QUERY_BODYSHOP = gql` tt_enforce_hours_for_tech_console md_tasks_presets use_paint_scale_data + md_ro_guard employee_teams( order_by: { name: asc } where: { active: { _eq: true } } @@ -266,6 +267,7 @@ export const UPDATE_SHOP = gql` enforce_conversion_category tt_enforce_hours_for_tech_console md_tasks_presets + md_ro_guard employee_teams( order_by: { name: asc } where: { active: { _eq: true } } diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 58bb2c902..6fab31fff 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -367,6 +367,18 @@ }, "md_payment_types": "Payment Types", "md_referral_sources": "Referral Sources", + "md_ro_guard": { + "enabled": "RO Guard Enabled?", + "enforce_ar": "Enforce AR Balance", + "enforce_bills": "Enforce Bill Discrepancy", + "enforce_cm": "Enforce Credit Memo Entry", + "enforce_labor": "Enforce Labor Allocation", + "enforce_ppd": "Enforce PPD Sync", + "enforce_profit": "Enforce Profit Requirement", + "enforce_sublet": "Enforce Sublet Completion", + "masterbypass": "Master Bypass Password (not encrypted)", + "totalgppercent_minimum": "Minimum Total Gross Profit %" + }, "md_tasks_presets": { "enable_tasks": "Enable Hour Flagging", "hourstype": "Hour Types", @@ -639,6 +651,7 @@ "laborrates": "Labor Rates", "licensing": "Licensing", "md_parts_scan": "Parts Scan Rules", + "md_ro_guard": "RO Guard", "md_tasks_presets": "Tasks Presets", "md_to_emails": "Preset To Emails", "md_to_emails_emails": "Emails", @@ -660,6 +673,9 @@ "tax_accounts": "Tax Accounts", "title": "Responsibility Centers" }, + "roguard": { + "title": "RO Guard" + }, "scheduling": "SMART Scheduling", "scoreboardsetup": "Scoreboard Setup", "shopinfo": "Shop Information", @@ -1356,6 +1372,7 @@ }, "fields": { "act_price": "Retail Price", + "act_price_before_ppc": "Original Part Price", "ah_detail_line": "Mark as Detail Labor Line (Autohouse Only)", "assigned_team": "Team", "assigned_team_name": "Team {{name}}", @@ -1824,6 +1841,7 @@ "scheddates": "Schedule Dates" }, "labels": { + "accountsreceivable": "Accounts Receivable", "act_price_ppc": "New Part Price", "actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).", "actual_delivery_inferred": "$t(jobs.fields.actual_delivery) inferred using $t(jobs.fields.scheduled_delivery).", @@ -1943,6 +1961,7 @@ "mapa": "Paint Materials", "markforreexport": "Mark for Re-export", "mash": "Shop Materials", + "masterbypass": "Master Bypass Password", "materials": { "mapa": "" }, @@ -1953,6 +1972,7 @@ "othertotal": "Other Totals", "outstanding_ar": "A balance is outstanding on this RO. Payments can still be entered when the job is closed. ", "outstanding_credit_memos": "Outstanding credit memos have not been entered against this job. Credit Memos may still be posted once the job is closed.", + "outstanding_ppd": "There are outstanding PPDs that may not have been synced back to the estimate.", "outstanding_reconciliation_discrep": "At least one discrepancy is not 0. This may indicate that this job is not properly reconciled and should not be closed.", "outstanding_sublets": "There are sublet lines on the job which have not been marked as completed. ", "outstandinghours": "There are outstanding hours on the job that have not been paid or have been overpaid.", @@ -1965,6 +1985,7 @@ "partsfilter": "Parts Only", "partssubletstotal": "Parts & Sublets Total", "partstotal": "Parts Total (ex. Taxes)", + "performance": "Performance", "pimraryamountpayable": "Total Primary Payable", "plitooltips": { "billtotal": "The total amount of all bill lines that have been posted against this RO (not including credits, taxes, or labor adjustments).", @@ -1979,8 +2000,8 @@ "totalreturns": "The total retail amount of returns created for this job." }, "ppc": "This line contains a part price change.", + "ppdnotexported": "PPDs not Exported", "profileadjustments": "Profile Disc./Mkup", - "profitbypassrequired": "Profit margin requirements not met. Bypass password required.", "prt_dsmk_total": "Line Item Adjustment", "rates": "Rates", "rates_subtotal": "All Rates Subtotal", @@ -2000,6 +2021,11 @@ "relatedros": "Related ROs", "remove_from_ar": "Remove from AR", "returntotals": "Return Totals", + "ro_guard": { + "enforce_validation": "Master Bypass Required: {{message}}", + "enforced": "This check has been enforced by your shop manager. Enter the master bypass password to close the Job." + }, + "roguard": "RO Guard", "roguardwarnings": "RO Guard Warnings", "rosaletotal": "RO Parts Total", "sale_additional": "Sales - Additional", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index 537f66575..5560513dc 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -367,6 +367,18 @@ }, "md_payment_types": "", "md_referral_sources": "", + "md_ro_guard": { + "enabled": "", + "enforce_ar": "", + "enforce_bills": "", + "enforce_cm": "", + "enforce_labor": "", + "enforce_ppd": "", + "enforce_profit": "", + "enforce_sublet": "", + "masterbypass": "", + "totalgppercent_minimum": "" + }, "md_tasks_presets": { "enable_tasks": "", "hourstype": "", @@ -639,6 +651,7 @@ "laborrates": "", "licensing": "", "md_parts_scan": "", + "md_ro_guard": "", "md_tasks_presets": "", "md_to_emails": "", "md_to_emails_emails": "", @@ -660,6 +673,9 @@ "tax_accounts": "", "title": "" }, + "roguard": { + "title": "" + }, "scheduling": "", "scoreboardsetup": "", "shopinfo": "", @@ -1356,6 +1372,7 @@ }, "fields": { "act_price": "Precio actual", + "act_price_before_ppc": "", "ah_detail_line": "", "assigned_team": "", "assigned_team_name": "", @@ -1824,6 +1841,7 @@ "scheddates": "" }, "labels": { + "accountsreceivable": "", "act_price_ppc": "", "actual_completion_inferred": "", "actual_delivery_inferred": "", @@ -1943,6 +1961,7 @@ "mapa": "", "markforreexport": "", "mash": "", + "masterbypass": "", "materials": { "mapa": "" }, @@ -1953,6 +1972,7 @@ "othertotal": "", "outstanding_ar": "", "outstanding_credit_memos": "", + "outstanding_ppd": "", "outstanding_reconciliation_discrep": "", "outstanding_sublets": "", "outstandinghours": "", @@ -1965,6 +1985,7 @@ "partsfilter": "", "partssubletstotal": "", "partstotal": "", + "performance": "", "pimraryamountpayable": "", "plitooltips": { "billtotal": "", @@ -1979,8 +2000,8 @@ "totalreturns": "" }, "ppc": "", + "ppdnotexported": "", "profileadjustments": "", - "profitbypassrequired": "", "prt_dsmk_total": "", "rates": "Tarifas", "rates_subtotal": "", @@ -2000,6 +2021,11 @@ "relatedros": "", "remove_from_ar": "", "returntotals": "", + "ro_guard": { + "enforce_validation": "", + "enforced": "" + }, + "roguard": "", "roguardwarnings": "", "rosaletotal": "", "sale_additional": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index e1424b9a1..dcf8821f9 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -367,6 +367,18 @@ }, "md_payment_types": "", "md_referral_sources": "", + "md_ro_guard": { + "enabled": "", + "enforce_ar": "", + "enforce_bills": "", + "enforce_cm": "", + "enforce_labor": "", + "enforce_ppd": "", + "enforce_profit": "", + "enforce_sublet": "", + "masterbypass": "", + "totalgppercent_minimum": "" + }, "md_tasks_presets": { "enable_tasks": "", "hourstype": "", @@ -639,6 +651,7 @@ "laborrates": "", "licensing": "", "md_parts_scan": "", + "md_ro_guard": "", "md_tasks_presets": "", "md_to_emails": "", "md_to_emails_emails": "", @@ -660,6 +673,9 @@ "tax_accounts": "", "title": "" }, + "roguard": { + "title": "" + }, "scheduling": "", "scoreboardsetup": "", "shopinfo": "", @@ -1356,6 +1372,7 @@ }, "fields": { "act_price": "Prix actuel", + "act_price_before_ppc": "", "ah_detail_line": "", "assigned_team": "", "assigned_team_name": "", @@ -1824,6 +1841,7 @@ "scheddates": "" }, "labels": { + "accountsreceivable": "", "act_price_ppc": "", "actual_completion_inferred": "", "actual_delivery_inferred": "", @@ -1943,6 +1961,7 @@ "mapa": "", "markforreexport": "", "mash": "", + "masterbypass": "", "materials": { "mapa": "" }, @@ -1953,6 +1972,7 @@ "othertotal": "", "outstanding_ar": "", "outstanding_credit_memos": "", + "outstanding_ppd": "", "outstanding_reconciliation_discrep": "", "outstanding_sublets": "", "outstandinghours": "", @@ -1965,6 +1985,7 @@ "partsfilter": "", "partssubletstotal": "", "partstotal": "", + "performance": "", "pimraryamountpayable": "", "plitooltips": { "billtotal": "", @@ -1979,8 +2000,8 @@ "totalreturns": "" }, "ppc": "", + "ppdnotexported": "", "profileadjustments": "", - "profitbypassrequired": "", "prt_dsmk_total": "", "rates": "Les taux", "rates_subtotal": "", @@ -2000,6 +2021,11 @@ "relatedros": "", "remove_from_ar": "", "returntotals": "", + "ro_guard": { + "enforce_validation": "", + "enforced": "" + }, + "roguard": "", "roguardwarnings": "", "rosaletotal": "", "sale_additional": "", diff --git a/client/src/utils/instanceRenderMgr.js b/client/src/utils/instanceRenderMgr.js index 92c77f4f9..36f8f115b 100644 --- a/client/src/utils/instanceRenderMgr.js +++ b/client/src/utils/instanceRenderMgr.js @@ -25,7 +25,11 @@ export default function InstanceRenderManager({ propToReturn = imex; break; case 'ROME': - propToReturn = rome; //TODO:AIO Implement USE_IMEX + if (rome === 'USE_IMEX') { + propToReturn = imex; + } else { + propToReturn = rome; + } break; case 'PROMANAGER': //Return the rome prop if USE_ROME.