WIP for Job Costing BOD-192

This commit is contained in:
Patrick Fic
2020-07-21 08:26:26 -07:00
parent 74aca37ee2
commit 068e1e8057
16 changed files with 463 additions and 5 deletions

View File

@@ -12169,6 +12169,27 @@
</concept_node>
</children>
</folder_node>
<concept_node>
<name>cost</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>
<folder_node>
<name>create</name>
<children>
@@ -12384,6 +12405,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>estimated</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>existing_jobs</name>
<definition_loaded>false</definition_loaded>
@@ -12426,6 +12468,48 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>gpdollars</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>gppercent</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>hrs_claimed</name>
<definition_loaded>false</definition_loaded>
@@ -12510,6 +12594,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>jobcosting</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>laborallocations</name>
<definition_loaded>false</definition_loaded>

View File

@@ -108,12 +108,17 @@ if (process.env.NODE_ENV === "development") {
middlewares.push(retryLink.concat(errorLink.concat(authLink.concat(link))));
const cache = new InMemoryCache();
const cache = new InMemoryCache({});
export const client = new ApolloClient({
link: ApolloLink.from(middlewares),
cache,
connectToDevTools: process.env.NODE_ENV !== "production",
defaultOptions: {
watchQuery: {
fetchPolicy: "cache-and-network",
},
},
});
export default function AppContainer() {

View File

@@ -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);

View File

@@ -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>
);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -0,0 +1,9 @@
.job-costing-parts-table-container {
display: block;
width: 100%;
}
.job-costing-parts-table {
border: black;
width: 100%;
}

View File

@@ -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,

View File

@@ -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 (

View File

@@ -130,6 +130,96 @@ export const SUBSCRIPTION_JOBS_IN_PRODUCTION = gql`
}
`;
export const QUERY_JOB_COSTING_DETAILS = gql`
query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
jobs_by_pk(id: $id) {
est_number
ro_number
clm_total
id
ded_amt
ded_status
depreciation_taxes
federal_tax_payable
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
labor_rate_desc
rate_atp
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
actual_in
status
joblines {
id
unq_seq
line_ind
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
}
invoices {
id
federal_tax_rate
local_tax_rate
state_tax_rate
is_credit_memo
invoicelines {
actual_cost
cost_center
id
quantity
}
}
}
}
`;
export const GET_JOB_BY_PK = gql`
query GET_JOB_BY_PK($id: uuid!) {
jobs_by_pk(id: $id) {

View File

@@ -78,6 +78,9 @@ const InvoicesListPage = lazy(() =>
import("../invoices/invoices.page.container")
);
const JobCostingModal = lazy(() =>
import("../../components/job-costing-modal/job-costing-modal.container")
);
const EnterInvoiceModalContainer = lazy(() =>
import("../../components/invoice-enter-modal/invoice-enter-modal.container")
);
@@ -162,6 +165,7 @@ export function Manage({ match, conflict }) {
<LoadingSpinner message={t("general.labels.loadingapp")} />
}>
<EnterInvoiceModalContainer />
<JobCostingModal />
<EmailOverlayContainer />
<TimeTicketModalContainer />
<PrintCenterModalContainer />

View File

@@ -19,6 +19,7 @@ const INITIAL_STATE = {
printCenter: { ...baseModal },
reconciliation: { ...baseModal },
payment: { ...baseModal },
jobCosting: { ...baseModal },
};
const modalsReducer = (state = INITIAL_STATE, action) => {

View File

@@ -50,3 +50,8 @@ export const selectPayment = createSelector(
[selectModals],
(modals) => modals.payment
);
export const selectJobCosting = createSelector(
[selectModals],
(modals) => modals.jobCosting
);

View File

@@ -753,6 +753,7 @@
"totals": "Totals",
"vehicle": "Vehicle"
},
"cost": "Cost",
"create": {
"jobinfo": "Job Info",
"newowner": "Create a new Owner instead. ",
@@ -765,12 +766,16 @@
"documents": "Documents",
"duplicateconfirm": "Are you sure you want to duplicate this job? Some elements of this job will not be duplicated.",
"employeeassignments": "Employee Assignments",
"estimated": "Estimated",
"existing_jobs": "Existing Jobs",
"federal_tax_amt": "Federal Taxes",
"gpdollars": "$ G.P.",
"gppercent": "% G.P.",
"hrs_claimed": "Hours Claimed",
"hrs_total": "Hours Total",
"inproduction": "In Production",
"job": "Job Details",
"jobcosting": "Job Costing",
"laborallocations": "Labor Allocations",
"lines": "Estimate Lines",
"local_tax_amt": "Local Taxes",

View File

@@ -753,6 +753,7 @@
"totals": "Totales",
"vehicle": "Vehículo"
},
"cost": "",
"create": {
"jobinfo": "",
"newowner": "",
@@ -765,12 +766,16 @@
"documents": "documentos",
"duplicateconfirm": "",
"employeeassignments": "",
"estimated": "",
"existing_jobs": "Empleos existentes",
"federal_tax_amt": "",
"gpdollars": "",
"gppercent": "",
"hrs_claimed": "",
"hrs_total": "",
"inproduction": "",
"job": "",
"jobcosting": "",
"laborallocations": "",
"lines": "Líneas estimadas",
"local_tax_amt": "",

View File

@@ -753,6 +753,7 @@
"totals": "Totaux",
"vehicle": "Véhicule"
},
"cost": "",
"create": {
"jobinfo": "",
"newowner": "",
@@ -765,12 +766,16 @@
"documents": "Les documents",
"duplicateconfirm": "",
"employeeassignments": "",
"estimated": "",
"existing_jobs": "Emplois existants",
"federal_tax_amt": "",
"gpdollars": "",
"gppercent": "",
"hrs_claimed": "",
"hrs_total": "",
"inproduction": "",
"job": "",
"jobcosting": "",
"laborallocations": "",
"lines": "Estimer les lignes",
"local_tax_amt": "",