IO-233 CDK Allocations Summary

This commit is contained in:
Patrick Fic
2021-08-10 13:42:53 -07:00
parent 124d68ef68
commit ed8eb51c2f
10 changed files with 419 additions and 49 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project version="1.2" be_version="2.7.1"> <babeledit_project be_version="2.7.1" version="1.2">
<!-- <!--
BabelEdit project file BabelEdit project file
@@ -3620,6 +3620,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>mappingname</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children> </children>
</folder_node> </folder_node>
<concept_node> <concept_node>
@@ -16893,6 +16914,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>dmsautoallocate</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>export</name> <name>export</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -72,9 +72,11 @@ export function BillEnterModalLinesComponent({
quantity: opt.part_qty || 1, quantity: opt.part_qty || 1,
actual_price: opt.cost, actual_price: opt.cost,
cost_center: opt.part_type cost_center: opt.part_type
? responsibilityCenters.defaults.costs[ ? responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[
opt.part_type opt.part_type
] || null ] ||
null)
: null, : null,
}; };
} }

View File

@@ -0,0 +1,147 @@
import { Table } from "antd";
import React, { useMemo } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import Dinero from "dinero.js";
import { useTranslation } from "react-i18next";
import _ from "lodash";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(DmsAllocationsSummary);
export function DmsAllocationsSummary({ bodyshop, job }) {
const { t } = useTranslation();
const allocationsSummary = useMemo(() => {
const profitCenterHash = job.joblines.reduce((acc, val) => {
//Check the Parts Assignment
if (val.profitcenter_part) {
if (!acc[val.profitcenter_part]) acc[val.profitcenter_part] = Dinero();
acc[val.profitcenter_part] = acc[val.profitcenter_part].add(
Dinero({ amount: Math.round((val.act_price || 0) * 100) }).multiply(
val.part_qty || 0
)
);
}
if (val.profitcenter_labor) {
//Check the Labor Assignment.
if (!acc[val.profitcenter_labor])
acc[val.profitcenter_labor] = Dinero();
acc[val.profitcenter_labor] = acc[val.profitcenter_labor].add(
Dinero({
amount: Math.round(
job[`rate_${val.mod_lbr_ty.toLowerCase()}`] * 100
),
}).multiply(val.mod_lb_hrs)
);
}
return acc;
}, {});
const costCenterHash = job.bills.reduce((bill_acc, bill_val) => {
bill_val.billlines.map((line_val) => {
if (!bill_acc[line_val.cost_center])
bill_acc[line_val.cost_center] = Dinero();
bill_acc[line_val.cost_center] = bill_acc[line_val.cost_center].add(
Dinero({
amount: Math.round((line_val.actual_cost || 0) * 100),
})
.multiply(line_val.quantity)
.multiply(bill_val.is_credit_memo ? -1 : 1)
);
return null;
});
return bill_acc;
}, {});
console.log(
"🚀 ~ file: dms-allocations-summary.component.jsx ~ line 69 ~ costCenterHash",
costCenterHash
);
return _.union(
Object.keys(profitCenterHash),
Object.keys(costCenterHash)
).map((key) => {
console.log("Key", key);
const profitCenter = bodyshop.md_responsibility_centers.profits.find(
(c) => c.name === key
);
const costCenter = bodyshop.md_responsibility_centers.costs.find(
(c) => c.name === key
);
return {
center: key,
sale: profitCenterHash[key]
? profitCenterHash[key].toFormat()
: Dinero().toFormat(),
cost: costCenterHash[key]
? costCenterHash[key].toFormat()
: Dinero().toFormat(),
profitCenter,
costCenter,
};
});
}, [job, bodyshop.md_responsibility_centers]);
const columns = [
{
title: t("job.fields.dms.center"),
dataIndex: "center",
key: "center",
},
{
title: t("job.fields.dms.sale"),
dataIndex: "sale",
key: "sale",
},
{
title: t("job.fields.dms.cost"),
dataIndex: "cost",
key: "cost",
},
{
title: t("job.fields.dms.sale_dms_acctnumber"),
dataIndex: "sale_dms_acctnumber",
key: "sale_dms_acctnumber",
render: (text, record) =>
record.profitCenter && record.profitCenter.dms_acctnumber,
},
{
title: t("job.fields.dms.cost_dms_acctnumber"),
dataIndex: "cost_dms_acctnumber",
key: "cost_dms_acctnumber",
render: (text, record) =>
record.costCenter && record.costCenter.dms_acctnumber,
},
{
title: t("job.fields.dms.dms_wip_acctnumber"),
dataIndex: "dms_wip_acctnumber",
key: "dms_wip_acctnumber",
render: (text, record) =>
record.costCenter && record.costCenter.dms_wip_acctnumber,
},
];
return (
<Table
pagination={{ position: "top", defaultPageSize: 50 }}
columns={columns}
rowKey="center"
dataSource={allocationsSummary}
/>
);
}

View File

@@ -0,0 +1,55 @@
import { Divider, Space, Tag, Timeline } from "antd";
import moment from "moment";
import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
export function DmsLogEvents({ socket, logs, bodyshop }) {
return (
<Timeline pending reverse={true}>
{logs.map((log, idx) => (
<Timeline.Item key={idx} color={LogLevelHierarchy(log.level)}>
<Space wrap align="start" style={{}}>
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
<span>{moment(log.timestamp).format("MM/DD/YYYY HH:MM:ss")}</span>
<Divider type="vertical" />
<span>{log.message}</span>
</Space>
</Timeline.Item>
))}
</Timeline>
);
}
function LogLevelHierarchy(level) {
switch (level) {
case "TRACE":
return "pink";
case "DEBUG":
return "orange";
case "INFO":
return "blue";
case "WARNING":
return "yellow";
case "ERROR":
return "red";
default:
return 0;
}
}

View File

@@ -1,4 +1,4 @@
import { Button } from "antd"; import { Button, Dropdown, Menu } from "antd";
import _ from "lodash"; import _ from "lodash";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -12,11 +12,8 @@ const mapStateToProps = createStructuredSelector({
export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) { export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
const { t } = useTranslation(); const { t } = useTranslation();
const handleAllocate = () => {
logImEXEvent("jobs_close_allocate_auto");
const { defaults } = bodyshop.md_responsibility_centers;
const handleAllocate = (defaults) => {
form.setFieldsValue({ form.setFieldsValue({
joblines: joblines.map((jl) => { joblines: joblines.map((jl) => {
const ret = _.cloneDeep(jl); const ret = _.cloneDeep(jl);
@@ -32,7 +29,7 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
} }
//Verify that this is also manually updated in server/job-costing //Verify that this is also manually updated in server/job-costing
if (!jl.part_type && !jl.mod_lbr_ty) { if (!jl.part_type && !jl.mod_lbr_ty) {
const lineDesc = jl.line_desc.toLowerCase(); const lineDesc = jl.line_desc && jl.line_desc.toLowerCase();
if (lineDesc.includes("shop materials")) { if (lineDesc.includes("shop materials")) {
ret.profitcenter_part = defaults.profits["MASH"]; ret.profitcenter_part = defaults.profits["MASH"];
} else if (lineDesc.includes("paint/materials")) { } else if (lineDesc.includes("paint/materials")) {
@@ -48,8 +45,36 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
}); });
}; };
return ( const handleAutoAllocateClick = () => {
<Button onClick={handleAllocate} disabled={disabled}> logImEXEvent("jobs_close_allocate_auto");
const { defaults } = bodyshop.md_responsibility_centers;
handleAllocate(defaults);
};
const handleMenuClick = ({ item, key, keyPath, domEvent }) => {
logImEXEvent("jobs_close_allocate_auto_dms");
handleAllocate(
bodyshop.md_responsibility_centers.dms_defaults.find(
(x) => x.name === key
)
);
};
const overlay = (
<Menu onClick={handleMenuClick}>
{bodyshop.md_responsibility_centers.dms_defaults.map((mapping) => (
<Menu.Item key={mapping.name}>{mapping.name}</Menu.Item>
))}
</Menu>
);
return bodyshop.cdk_dealerid ? (
<Dropdown overlay={overlay}>
<Button disabled={disabled}>{t("jobs.actions.dmsautoallocate")}</Button>
</Dropdown>
) : (
<Button onClick={handleAutoAllocateClick} disabled={disabled}>
{t("jobs.actions.autoallocate")} {t("jobs.actions.autoallocate")}
</Button> </Button>
); );

View File

@@ -1871,3 +1871,100 @@ export const FIND_JOBS_BY_CLAIM = gql`
} }
} }
`; `;
export const QUERY_JOB_EXPORT_DMS = gql`
query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
ro_number
invoice_allocation
ins_co_id
id
ded_amt
ded_status
depreciation_taxes
other_amount_payable
towing_payable
storage_payable
adjustment_bottom_line
federal_tax_rate
state_tax_rate
local_tax_rate
tax_tow_rt
tax_str_rt
tax_paint_mat_rt
tax_sub_rt
tax_lbr_rt
tax_levies_rt
parts_tax_rates
job_totals
rate_la1
rate_la2
rate_la3
rate_la4
rate_laa
rate_lab
rate_lad
rate_lae
rate_laf
rate_lag
rate_lam
rate_lar
rate_las
rate_lau
rate_ma2s
rate_ma2t
rate_ma3s
rate_mabl
rate_macs
rate_mahw
rate_mapa
rate_mash
rate_matd
status
date_exported
date_invoiced
voided
scheduled_completion
actual_completion
scheduled_delivery
actual_delivery
scheduled_in
actual_in
bills {
id
federal_tax_rate
local_tax_rate
state_tax_rate
is_credit_memo
billlines {
actual_cost
cost_center
id
quantity
}
}
joblines(where: { removed: { _eq: false } }) {
id
removed
tax_part
line_desc
prt_dsmk_p
prt_dsmk_m
part_type
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
profitcenter_labor
profitcenter_part
prt_dsmk_p
}
}
}
`;

View File

@@ -1,16 +1,23 @@
import { Result, Timeline, Space, Tag, Divider, Button } from "antd"; import { useQuery } from "@apollo/client";
import { Button, Result } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
import AlertComponent from "../../components/alert/alert.component";
import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component";
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { auth } from "../../firebase/firebase.utils";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
import { import {
setBreadcrumbs, setBreadcrumbs,
setSelectedHeader, setSelectedHeader,
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
import SocketIO from "socket.io-client";
import { auth } from "../../firebase/firebase.utils";
import moment from "moment";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -39,6 +46,13 @@ export const socket = SocketIO(
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) { export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const search = queryString.parse(useLocation().search);
const { jobId } = search;
const { loading, error, data } = useQuery(QUERY_JOB_EXPORT_DMS, {
variables: { id: jobId },
skip: !jobId,
});
useEffect(() => { useEffect(() => {
document.title = t("titles.dms"); document.title = t("titles.dms");
@@ -71,10 +85,13 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
}; };
}, []); }, []);
if (!bodyshop.cdk_dealerid) return <Result status="404" />; if (!jobId || !bodyshop.cdk_dealerid) return <Result status="404" />;
const dmsType = determineDmsType(bodyshop); const dmsType = determineDmsType(bodyshop);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<div> <div>
<Button <Button
@@ -98,39 +115,18 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
reconnect reconnect
</Button> </Button>
<Timeline pending={socket.connected && "Processing..."} reverse={true}> <DmsAllocationsSummary
{logs.map((log, idx) => ( socket={socket}
<Timeline.Item key={idx} color={LogLevelHierarchy(log.level)}> job={data ? data.jobs_by_pk : null}
<Space wrap align="start" style={{}}> />
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
<span>{moment(log.timestamp).format("MM/DD/YYYY HH:MM:ss")}</span> <div>
<Divider type="vertical" /> <DmsLogEvents socket={socket} logs={logs} />
<span>{log.message}</span> </div>
</Space>
</Timeline.Item>
))}
</Timeline>
</div> </div>
); );
} }
function LogLevelHierarchy(level) {
switch (level) {
case "TRACE":
return "pink";
case "DEBUG":
return "orange";
case "INFO":
return "blue";
case "WARNING":
return "yellow";
case "ERROR":
return "red";
default:
return 0;
}
}
const determineDmsType = (bodyshop) => { const determineDmsType = (bodyshop) => {
if (bodyshop.cdk_dealerid) return "cdk"; if (bodyshop.cdk_dealerid) return "cdk";
else { else {

View File

@@ -233,7 +233,8 @@
}, },
"dms": { "dms": {
"dms_acctnumber": "DMS Account #", "dms_acctnumber": "DMS Account #",
"dms_wip_acctnumber": "DMS W.I.P. Account #" "dms_wip_acctnumber": "DMS W.I.P. Account #",
"mappingname": "DMS Mapping Name"
}, },
"email": "General Shop Email", "email": "General Shop Email",
"enforce_class": "Enforce Class on Conversion?", "enforce_class": "Enforce Class on Conversion?",
@@ -1059,6 +1060,7 @@
"changestatus": "Change Status", "changestatus": "Change Status",
"convert": "Convert", "convert": "Convert",
"deliver": "Deliver", "deliver": "Deliver",
"dmsautoallocate": "DMS Auto Allocate",
"export": "Export", "export": "Export",
"exportcustdata": "Export Customer Data", "exportcustdata": "Export Customer Data",
"exportselected": "Export Selected", "exportselected": "Export Selected",

View File

@@ -233,7 +233,8 @@
}, },
"dms": { "dms": {
"dms_acctnumber": "", "dms_acctnumber": "",
"dms_wip_acctnumber": "" "dms_wip_acctnumber": "",
"mappingname": ""
}, },
"email": "", "email": "",
"enforce_class": "", "enforce_class": "",
@@ -1059,6 +1060,7 @@
"changestatus": "Cambiar Estado", "changestatus": "Cambiar Estado",
"convert": "Convertir", "convert": "Convertir",
"deliver": "", "deliver": "",
"dmsautoallocate": "",
"export": "", "export": "",
"exportcustdata": "", "exportcustdata": "",
"exportselected": "", "exportselected": "",

View File

@@ -233,7 +233,8 @@
}, },
"dms": { "dms": {
"dms_acctnumber": "", "dms_acctnumber": "",
"dms_wip_acctnumber": "" "dms_wip_acctnumber": "",
"mappingname": ""
}, },
"email": "", "email": "",
"enforce_class": "", "enforce_class": "",
@@ -1059,6 +1060,7 @@
"changestatus": "Changer le statut", "changestatus": "Changer le statut",
"convert": "Convertir", "convert": "Convertir",
"deliver": "", "deliver": "",
"dmsautoallocate": "",
"export": "", "export": "",
"exportcustdata": "", "exportcustdata": "",
"exportselected": "", "exportselected": "",