IO-233 CDK Allocations Summary
This commit is contained in:
@@ -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
|
||||
@@ -3620,6 +3620,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</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>
|
||||
</folder_node>
|
||||
<concept_node>
|
||||
@@ -16893,6 +16914,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</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>
|
||||
<name>export</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -72,9 +72,11 @@ export function BillEnterModalLinesComponent({
|
||||
quantity: opt.part_qty || 1,
|
||||
actual_price: opt.cost,
|
||||
cost_center: opt.part_type
|
||||
? responsibilityCenters.defaults.costs[
|
||||
? responsibilityCenters.defaults &&
|
||||
(responsibilityCenters.defaults.costs[
|
||||
opt.part_type
|
||||
] || null
|
||||
] ||
|
||||
null)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "antd";
|
||||
import { Button, Dropdown, Menu } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -12,11 +12,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
||||
const { t } = useTranslation();
|
||||
const handleAllocate = () => {
|
||||
logImEXEvent("jobs_close_allocate_auto");
|
||||
|
||||
const { defaults } = bodyshop.md_responsibility_centers;
|
||||
|
||||
const handleAllocate = (defaults) => {
|
||||
form.setFieldsValue({
|
||||
joblines: joblines.map((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
|
||||
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")) {
|
||||
ret.profitcenter_part = defaults.profits["MASH"];
|
||||
} else if (lineDesc.includes("paint/materials")) {
|
||||
@@ -48,8 +45,36 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleAllocate} disabled={disabled}>
|
||||
const handleAutoAllocateClick = () => {
|
||||
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")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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 { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
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 {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
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({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -39,6 +46,13 @@ export const socket = SocketIO(
|
||||
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
const { t } = useTranslation();
|
||||
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(() => {
|
||||
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);
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
@@ -98,39 +115,18 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
|
||||
reconnect
|
||||
</Button>
|
||||
|
||||
<Timeline pending={socket.connected && "Processing..."} 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>
|
||||
<DmsAllocationsSummary
|
||||
socket={socket}
|
||||
job={data ? data.jobs_by_pk : null}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<DmsLogEvents socket={socket} logs={logs} />
|
||||
</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) => {
|
||||
if (bodyshop.cdk_dealerid) return "cdk";
|
||||
else {
|
||||
|
||||
@@ -233,7 +233,8 @@
|
||||
},
|
||||
"dms": {
|
||||
"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",
|
||||
"enforce_class": "Enforce Class on Conversion?",
|
||||
@@ -1059,6 +1060,7 @@
|
||||
"changestatus": "Change Status",
|
||||
"convert": "Convert",
|
||||
"deliver": "Deliver",
|
||||
"dmsautoallocate": "DMS Auto Allocate",
|
||||
"export": "Export",
|
||||
"exportcustdata": "Export Customer Data",
|
||||
"exportselected": "Export Selected",
|
||||
|
||||
@@ -233,7 +233,8 @@
|
||||
},
|
||||
"dms": {
|
||||
"dms_acctnumber": "",
|
||||
"dms_wip_acctnumber": ""
|
||||
"dms_wip_acctnumber": "",
|
||||
"mappingname": ""
|
||||
},
|
||||
"email": "",
|
||||
"enforce_class": "",
|
||||
@@ -1059,6 +1060,7 @@
|
||||
"changestatus": "Cambiar Estado",
|
||||
"convert": "Convertir",
|
||||
"deliver": "",
|
||||
"dmsautoallocate": "",
|
||||
"export": "",
|
||||
"exportcustdata": "",
|
||||
"exportselected": "",
|
||||
|
||||
@@ -233,7 +233,8 @@
|
||||
},
|
||||
"dms": {
|
||||
"dms_acctnumber": "",
|
||||
"dms_wip_acctnumber": ""
|
||||
"dms_wip_acctnumber": "",
|
||||
"mappingname": ""
|
||||
},
|
||||
"email": "",
|
||||
"enforce_class": "",
|
||||
@@ -1059,6 +1060,7 @@
|
||||
"changestatus": "Changer le statut",
|
||||
"convert": "Convertir",
|
||||
"deliver": "",
|
||||
"dmsautoallocate": "",
|
||||
"export": "",
|
||||
"exportcustdata": "",
|
||||
"exportselected": "",
|
||||
|
||||
Reference in New Issue
Block a user