WIP for Job Costing BOD-192
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import DatePicker from "react-datepicker";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
//To be used as a form element only.
|
||||
|
||||
const DateTimePicker = ({ value, onChange, onBlur }) => {
|
||||
const DateTimePicker = ({ value, onChange, onBlur }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChange = (value) => {
|
||||
@@ -30,4 +30,4 @@ const DateTimePicker = ({ value, onChange, onBlur }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DateTimePicker;
|
||||
export default forwardRef(DateTimePicker);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import JobCostingPartsTable from "../job-costing-parts-table/job-costing-parts-table.component";
|
||||
import { Row, Col } from "antd";
|
||||
|
||||
const colSpan = {
|
||||
md: { span: 24 },
|
||||
lg: { span: 12 },
|
||||
};
|
||||
|
||||
export default function JobCostingModalComponent({ job }) {
|
||||
return (
|
||||
<div>
|
||||
<Row gutter={[32, 32]}>
|
||||
<Col {...colSpan}>
|
||||
<JobCostingPartsTable job={job} />
|
||||
</Col>
|
||||
<Col {...colSpan}></Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useQuery } from "@apollo/react-hooks";
|
||||
import { Modal } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_JOB_COSTING_DETAILS } from "../../graphql/jobs.queries";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectJobCosting } from "../../redux/modals/modals.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import JobCostingModalComponent from "./job-costing-modal.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobCostingModal: selectJobCosting,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("jobCosting")),
|
||||
});
|
||||
|
||||
export function JobCostingModalContainer({
|
||||
jobCostingModal,
|
||||
toggleModalVisible,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { visible, context, actions } = jobCostingModal;
|
||||
const { jobId } = context;
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY_JOB_COSTING_DETAILS, {
|
||||
variables: { id: jobId },
|
||||
skip: !jobId,
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
title={t("jobs.labels.jobcosting")}
|
||||
onCancel={() => toggleModalVisible()}
|
||||
width='90%'
|
||||
destroyOnClose
|
||||
forceRender>
|
||||
{error ? <AlertComponent message={error.message} type='error' /> : null}
|
||||
{loading ? (
|
||||
<LoadingSpinner loading={loading} />
|
||||
) : (
|
||||
<JobCostingModalComponent job={data && data.jobs_by_pk} />
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobCostingModalContainer);
|
||||
@@ -0,0 +1,120 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import "./job-costing-parts-table.styles.scss";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function JobCostingPartsTable({ bodyshop, job }) {
|
||||
console.log("JobCostingPartsTable -> job", job);
|
||||
const { t } = useTranslation();
|
||||
const defaultProfits = bodyshop.md_responsibility_centers.defaults.profits;
|
||||
console.log("JobCostingPartsTable -> defaultProfits", defaultProfits);
|
||||
const defaultCosts = bodyshop.md_responsibility_centers.defaults.costs;
|
||||
|
||||
//Need to get the sums of the job lines by cost center.
|
||||
const jobLineTotalsByProfitCenter = job.joblines.reduce(
|
||||
(acc, val) => {
|
||||
const laborProfitCenter = defaultProfits[val.mod_lbr_ty];
|
||||
if (!!!laborProfitCenter)
|
||||
console.log(
|
||||
"Unknown cost/profit center mapping for labor.",
|
||||
val.mod_lbr_ty
|
||||
);
|
||||
|
||||
const rateName = `rate_${(val.mod_lbr_ty || "").toLowerCase()}`;
|
||||
const laborAmount = Dinero({
|
||||
amount: Math.round((job[rateName] || 0) * 100),
|
||||
}).multiply(val.mod_lb_hrs || 0);
|
||||
if (!!!acc.labor[laborProfitCenter])
|
||||
acc.labor[laborProfitCenter] = Dinero();
|
||||
acc.labor[laborProfitCenter] = acc.labor[laborProfitCenter].add(
|
||||
laborAmount
|
||||
);
|
||||
|
||||
const partsProfitCenter = defaultProfits[val.part_type];
|
||||
if (!!!partsProfitCenter)
|
||||
console.log(
|
||||
"Unknown cost/profit center mapping for parts.",
|
||||
val.part_type
|
||||
);
|
||||
const partsAmount = Dinero({
|
||||
amount: Math.round((val.act_price || 0) * 100),
|
||||
}).multiply(val.part_qty || 1);
|
||||
if (!!!acc.parts[partsProfitCenter])
|
||||
acc.parts[partsProfitCenter] = Dinero();
|
||||
acc.parts[partsProfitCenter] = acc.parts[partsProfitCenter].add(
|
||||
partsAmount
|
||||
);
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ parts: {}, labor: {} }
|
||||
);
|
||||
|
||||
const invoiceTotalsByProfitCenter = job.invoices.reduce(
|
||||
(inv_acc, inv_val) => {
|
||||
//At the invoice level.
|
||||
const t = inv_val.invoicelines.map((line_val) => {
|
||||
//At the invoice line level.
|
||||
console.log("JobCostingPartsTable -> line_val", line_val);
|
||||
if (!!!inv_acc[line_val.cost_center])
|
||||
inv_acc[line_val.cost_center] = Dinero();
|
||||
|
||||
inv_acc[line_val.cost_center] = inv_acc[line_val.cost_center].add(
|
||||
Dinero({
|
||||
amount: Math.round((line_val.actual_cost || 0) * 100),
|
||||
})
|
||||
.multiply(line_val.quantity)
|
||||
.multiply(inv_val.is_credit_memo ? -1 : 1)
|
||||
);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
return inv_acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
console.log(
|
||||
"JobCostingPartsTable -> invoiceTotalsByProfitCenter",
|
||||
invoiceTotalsByProfitCenter
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='job-costing-parts-table-container'>
|
||||
<table className='job-costing-parts-table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("bodyshop.fields.responsibilitycenter")}</th>
|
||||
<th>{t("jobs.labels.estimated")}</th>
|
||||
<th>{t("jobs.labels.cost")}</th>
|
||||
<th>{t("jobs.labels.gpdollars")}</th>
|
||||
<th>{t("jobs.labels.gppercent")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Cost Center</td>
|
||||
<td>$12345.45</td>
|
||||
<td>$123.45</td>
|
||||
<td>$1234.00</td>
|
||||
<td>50.58%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(JobCostingPartsTable);
|
||||
@@ -0,0 +1,9 @@
|
||||
.job-costing-parts-table-container {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.job-costing-parts-table {
|
||||
border: black;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -143,11 +143,20 @@ export function JobLinesComponent({
|
||||
</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.mod_lbr_ty"),
|
||||
dataIndex: "mod_lbr_ty",
|
||||
key: "mod_lbr_ty",
|
||||
|
||||
sorter: (a, b) => alphaSort(a.mod_lbr_ty, b.mod_lbr_ty),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "mod_lbr_ty" && state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.mod_lb_hrs"),
|
||||
dataIndex: "mod_lb_hrs",
|
||||
key: "mod_lb_hrs",
|
||||
responsive: ["lg"],
|
||||
|
||||
sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
|
||||
|
||||
@@ -24,6 +24,8 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
dispatch(setModalContext({ context: context, modal: "invoiceEnter" })),
|
||||
setPaymentContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||
setJobCostingContext: (context) =>
|
||||
dispatch(setModalContext({ context: context, modal: "jobCosting" })),
|
||||
});
|
||||
|
||||
export function JobsDetailHeaderActions({
|
||||
@@ -33,6 +35,7 @@ export function JobsDetailHeaderActions({
|
||||
setScheduleContext,
|
||||
setInvoiceEnterContext,
|
||||
setPaymentContext,
|
||||
setJobCostingContext,
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -123,6 +126,20 @@ export function JobsDetailHeaderActions({
|
||||
</Link>
|
||||
</Menu.Item>
|
||||
<JobsDetaiLheaderCsi job={job} />
|
||||
<Menu.Item
|
||||
key='jobcosting'
|
||||
onClick={() => {
|
||||
logImEXEvent("job_header_job_costing");
|
||||
|
||||
setJobCostingContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: job.id,
|
||||
},
|
||||
});
|
||||
}}>
|
||||
{t("jobs.labels.jobcosting")}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user