Merged in release/2023-04-28 (pull request #728)

IO-2190 Factor Paint Costs from Scale over to Job Costing & Autohouse

Approved-by: Patrick Fic
This commit is contained in:
Allan Carr
2023-04-27 19:47:13 +00:00
committed by Patrick Fic
14 changed files with 238 additions and 22 deletions

View File

@@ -1,4 +1,4 @@
<babeledit_project be_version="2.7.1" version="1.2"> <babeledit_project version="1.2" be_version="2.7.1">
<!-- <!--
BabelEdit project file BabelEdit project file
@@ -8906,6 +8906,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>use_paint_scale_data</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>uselocalmediaserver</name> <name>uselocalmediaserver</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -16339,6 +16360,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>excel</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>exceptiontitle</name> <name>exceptiontitle</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -33467,6 +33509,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>savetojobnotes</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>
<folder_node> <folder_node>
@@ -37589,6 +37652,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>dms_posting_sheet</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>envelope_return_address</name> <name>envelope_return_address</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -41242,6 +41326,48 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>exported_gsr_by_ro</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>exported_gsr_by_ro_labor</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>gsr_by_atp</name> <name>gsr_by_atp</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -82,7 +82,7 @@ export function JobsDetailRates({ jobRO, form, job, bodyshop }) {
> >
<CurrencyInput disabled={jobRO || bodyshop.cdk_dealerid} /> <CurrencyInput disabled={jobRO || bodyshop.cdk_dealerid} />
</Form.Item> </Form.Item>
<Space align="end"> <Space align="center">
<Form.Item label={t("jobs.fields.ca_bc_pvrt")} name="ca_bc_pvrt"> <Form.Item label={t("jobs.fields.ca_bc_pvrt")} name="ca_bc_pvrt">
<CurrencyInput disabled={jobRO} min={0} /> <CurrencyInput disabled={jobRO} min={0} />
</Form.Item> </Form.Item>

View File

@@ -86,6 +86,7 @@ export function NoteUpsertModalContainer({
{ ...values, jobid: jobId, created_by: currentUser.email }, { ...values, jobid: jobId, created_by: currentUser.email },
], ],
}, },
refetchQueries: ["QUERY_NOTES_BY_JOB_PK"],
}); });
if (AdditionalNoteInserts.length > 0) { if (AdditionalNoteInserts.length > 0) {

View File

@@ -82,13 +82,13 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext }) {
setVisible(false); setVisible(false);
setNoteUpsertContext({ setNoteUpsertContext({
context: { context: {
jobId: record.jobId, jobId: record.id,
text: note, text: note,
}, },
}); });
}} }}
> >
Save to Job Notes {t("notes.actions.savetojobnotes")}
</Button> </Button>
</Space> </Space>
</div> </div>

View File

@@ -0,0 +1,39 @@
import Dinero from "dinero.js";
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div
style={{
backgroundColor: "white",
border: "1px solid gray",
padding: "0.5rem",
}}
>
<p style={{ margin: "0" }}>{label}</p>
{payload.map((data, index) => {
if (data.dataKey === "sales")
return (
<p
style={{ margin: "10px 0", color: data.color }}
key={index}
>{`${data.name} : ${Dinero({
amount: data.value,
}).toFormat()}`}</p>
);
return (
<p
style={{ margin: "10px 0", color: data.color }}
key={index}
>{`${data.name} : ${data.value}`}</p>
);
})}
</div>
);
}
return null;
};
export default CustomTooltip;

View File

@@ -18,6 +18,7 @@ import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util"; import * as Utils from "../scoreboard-targets-table/scoreboard-targets-table.util";
import _ from "lodash"; import _ from "lodash";
import CustomTooltip from "./chart-custom-tooltip";
const graphProps = { const graphProps = {
strokeWidth: 3, strokeWidth: 3,
@@ -55,6 +56,14 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
}; };
} }
const yesterdaySales = acc.length > 0 && acc[acc.length - 1].sales;
const sales =
sbEntriesByDate[val]?.reduce((acc, sb) => {
return acc + sb.job.job_totals.totals.subtotal.amount;
}, 0) || 0;
const accSales = yesterdaySales + sales;
const theValue = { const theValue = {
date: moment(val).format("D ddd"), date: moment(val).format("D ddd"),
paintHrs: _.round(dayhrs.painthrs, 1), paintHrs: _.round(dayhrs.painthrs, 1),
@@ -73,6 +82,7 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
: dayhrs.painthrs + dayhrs.bodyhrs, : dayhrs.painthrs + dayhrs.bodyhrs,
1 1
), ),
sales: accSales,
}; };
return [...acc, theValue]; return [...acc, theValue];
@@ -87,8 +97,15 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
> >
<CartesianGrid stroke="#f5f5f5" /> <CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} /> <XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} />
<YAxis strokeWidth={graphProps.strokeWidth} /> <YAxis
<Tooltip /> strokeWidth={graphProps.strokeWidth}
allowDataOverflow
dataKey="sales"
yAxisId="right"
orientation="right"
/>
<YAxis yAxisId="left" strokeWidth={graphProps.strokeWidth} />
<Tooltip content={<CustomTooltip />} />
<Legend /> <Legend />
<Area <Area
type="monotone" type="monotone"
@@ -96,6 +113,7 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
dataKey="accHrs" dataKey="accHrs"
fill="lightgreen" fill="lightgreen"
stroke="green" stroke="green"
yAxisId="left"
/> />
<Bar <Bar
name="Body Hours" name="Body Hours"
@@ -103,6 +121,7 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
stackId="day" stackId="day"
barSize={20} barSize={20}
fill="darkblue" fill="darkblue"
yAxisId="left"
/> />
<Bar <Bar
name="Paint Hours" name="Paint Hours"
@@ -110,12 +129,23 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
stackId="day" stackId="day"
barSize={20} barSize={20}
fill="darkred" fill="darkred"
yAxisId="left"
/> />
<Line <Line
name="Target Hours" name="Target Hours"
type="monotone" type="monotone"
dataKey="accTargetHrs" dataKey="accTargetHrs"
stroke="#ff7300" stroke="#ff7300"
yAxisId="left"
strokeWidth={graphProps.strokeWidth}
/>
<Line
name="Sales"
type="monotone"
dataKey="sales"
stroke="#8F00FF"
yAxisId="right"
strokeWidth={graphProps.strokeWidth} strokeWidth={graphProps.strokeWidth}
/> />
</ComposedChart> </ComposedChart>

View File

@@ -10,7 +10,7 @@ import { connect } from "react-redux";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next";
const { Title } = Typography; const { Title } = Typography;
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -19,6 +19,7 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = (dispatch) => ({});
const TechJobStatistics = ({ technician }) => { const TechJobStatistics = ({ technician }) => {
const { t } = useTranslation();
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const { start, end } = searchParams; const { start, end } = searchParams;
@@ -78,21 +79,21 @@ const TechJobStatistics = ({ technician }) => {
if (error) return <AlertComponent message={error.message} type="error" />; if (error) return <AlertComponent message={error.message} type="error" />;
return ( return (
<Card title="Productive Hours Statistics"> <Card title={t("scoreboard.labels.productivestatistics")}>
<Space size={100}> <Space size={100}>
<Col> <Col>
<Title level={5}>This Week</Title> <Title level={5}>{t("scoreboard.labels.thisweek")}</Title>
<Space size={20}> <Space size={20}>
<Statistic <Statistic
title="Productive Hours" title={t("timetickets.fields.productivehrs")}
value={totals.week.productivehrs.toFixed(2)} value={totals.week.productivehrs.toFixed(2)}
/> />
<Statistic <Statistic
title="Actual Hours" title={t("timetickets.fields.actualhrs")}
value={totals.week.actualhrs.toFixed(2)} value={totals.week.actualhrs.toFixed(2)}
/> />
<Statistic <Statistic
title="Efficiency %" title={t("timetickets.labels.efficiency")}
value={ value={
totals.week.actualhrs totals.week.actualhrs
? `${( ? `${(
@@ -105,18 +106,18 @@ const TechJobStatistics = ({ technician }) => {
</Space> </Space>
</Col> </Col>
<Col> <Col>
<Title level={5}>This Month</Title> <Title level={5}>{t("scoreboard.labels.thismonth")}</Title>
<Space size={20}> <Space size={20}>
<Statistic <Statistic
title="Productive Hours" title={t("timetickets.fields.productivehrs")}
value={totals.month.productivehrs.toFixed(2)} value={totals.month.productivehrs.toFixed(2)}
/> />
<Statistic <Statistic
title="Actual Hours" title={t("timetickets.fields.actualhrs")}
value={totals.month.actualhrs.toFixed(2)} value={totals.month.actualhrs.toFixed(2)}
/> />
<Statistic <Statistic
title="Efficiency %" title={t("timetickets.labels.efficiency")}
value={ value={
totals.month.actualhrs totals.month.actualhrs
? `${( ? `${(

View File

@@ -117,6 +117,7 @@ export const QUERY_BODYSHOP = gql`
md_parts_scan md_parts_scan
enforce_conversion_category enforce_conversion_category
tt_enforce_hours_for_tech_console tt_enforce_hours_for_tech_console
use_paint_scale_data
employees { employees {
user_email user_email
id id

View File

@@ -4,7 +4,15 @@ export const INSERT_NEW_NOTE = gql`
mutation INSERT_NEW_NOTE($noteInput: [notes_insert_input!]!) { mutation INSERT_NEW_NOTE($noteInput: [notes_insert_input!]!) {
insert_notes(objects: $noteInput) { insert_notes(objects: $noteInput) {
returning { returning {
created_at
created_by
critical
id id
jobid
private
text
updated_at
audit
} }
} }
} }
@@ -15,8 +23,8 @@ export const QUERY_NOTES_BY_JOB_PK = gql`
jobs_by_pk(id: $id) { jobs_by_pk(id: $id) {
id id
ro_number ro_number
vehicle{ vehicle {
jobs{ jobs {
id id
ro_number ro_number
status status

View File

@@ -19,6 +19,7 @@ export const QUERY_SCOREBOARD = gql`
v_make_desc v_make_desc
v_model_desc v_model_desc
v_model_yr v_model_yr
job_totals
} }
} }
} }

View File

@@ -52,6 +52,7 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container"; import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";
import UndefinedToNull from "../../utils/undefinedtonull"; import UndefinedToNull from "../../utils/undefinedtonull";
import NoteUpsertModalComponent from "../../components/note-upsert-modal/note-upsert-modal.container";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -97,7 +98,11 @@ export function JobsDetailPage({
variables: { variables: {
jobId: job.id, jobId: job.id,
job: { job: {
...UndefinedToNull(values, ["alt_transport", "category", "referral_source"]), ...UndefinedToNull(values, [
"alt_transport",
"category",
"referral_source",
]),
parts_tax_rates: { parts_tax_rates: {
...job.parts_tax_rates, ...job.parts_tax_rates,
...values.parts_tax_rates, ...values.parts_tax_rates,
@@ -231,6 +236,7 @@ export function JobsDetailPage({
<ScheduleJobModalContainer /> <ScheduleJobModalContainer />
<JobReconciliationModal /> <JobReconciliationModal />
<JobLineUpsertModalContainer /> <JobLineUpsertModalContainer />
<NoteUpsertModalComponent />
<Form <Form
form={form} form={form}
name="JobDetailForm" name="JobDetailForm"

View File

@@ -1962,7 +1962,8 @@
"actions": "Actions", "actions": "Actions",
"deletenote": "Delete Note", "deletenote": "Delete Note",
"edit": "Edit Note", "edit": "Edit Note",
"new": "New Note" "new": "New Note",
"savetojobnotes": "Save to Job Notes"
}, },
"errors": { "errors": {
"inserting": "Error inserting note. {{error}}" "inserting": "Error inserting note. {{error}}"

View File

@@ -1962,7 +1962,8 @@
"actions": "Comportamiento", "actions": "Comportamiento",
"deletenote": "Borrar nota", "deletenote": "Borrar nota",
"edit": "Editar nota", "edit": "Editar nota",
"new": "Nueva nota" "new": "Nueva nota",
"savetojobnotes": ""
}, },
"errors": { "errors": {
"inserting": "" "inserting": ""

View File

@@ -1962,7 +1962,8 @@
"actions": "actes", "actions": "actes",
"deletenote": "Supprimer la note", "deletenote": "Supprimer la note",
"edit": "Note éditée", "edit": "Note éditée",
"new": "Nouvelle note" "new": "Nouvelle note",
"savetojobnotes": ""
}, },
"errors": { "errors": {
"inserting": "" "inserting": ""