Merged in release/2021-10-01 (pull request #228)

release/2021-10-01

Approved-by: Patrick Fic
This commit is contained in:
Patrick Fic
2021-09-28 20:19:33 +00:00
6 changed files with 268 additions and 106 deletions

View File

@@ -1,11 +1,24 @@
import { useMutation } from "@apollo/client"; import { useMutation, useLazyQuery } from "@apollo/client";
import { Button, Card, Form, InputNumber, notification, Popover } from "antd"; import {
Button,
Card,
Form,
InputNumber,
notification,
Popover,
Space,
} from "antd";
import moment from "moment"; import moment from "moment";
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries"; import {
INSERT_SCOREBOARD_ENTRY,
QUERY_SCOREBOARD_ENTRY,
UPDATE_SCOREBOARD_ENTRY,
} from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
export default function ScoreboardAddButton({ export default function ScoreboardAddButton({
job, job,
@@ -14,17 +27,46 @@ export default function ScoreboardAddButton({
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [insertScoreboardEntry] = useMutation(INSERT_SCOREBOARD_ENTRY); const [insertScoreboardEntry] = useMutation(INSERT_SCOREBOARD_ENTRY);
const [updateScoreboardEntry] = useMutation(UPDATE_SCOREBOARD_ENTRY);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [form] = Form.useForm(); const [form] = Form.useForm();
const [visibility, setVisibility] = useState(false); const [visibility, setVisibility] = useState(false);
const [callQuery, { loading: entryLoading, data: entryData }] = useLazyQuery(
QUERY_SCOREBOARD_ENTRY
);
useEffect(() => {
if (visibility) {
callQuery({ variables: { jobid: job.id } });
}
}, [visibility, job.id, callQuery]);
useEffect(() => {
console.log("UE", entryData);
if (entryData && entryData.scoreboard && entryData.scoreboard[0]) {
console.log("Setting FOrm");
form.setFieldsValue(entryData.scoreboard[0]);
}
}, [entryData, form]);
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("job_close_add_to_scoreboard"); logImEXEvent("job_close_add_to_scoreboard");
setLoading(true); setLoading(true);
const result = await insertScoreboardEntry({ let result;
variables: { sbInput: [{ jobid: job.id, ...values }] },
}); if (entryData && entryData.scoreboard && entryData.scoreboard[0]) {
result = await updateScoreboardEntry({
variables: {
sbId: entryData.scoreboard[0].id,
sbInput: values,
},
});
} else {
result = await insertScoreboardEntry({
variables: { sbInput: [{ jobid: job.id, ...values }] },
});
}
if (!!result.errors) { if (!!result.errors) {
notification["error"]({ notification["error"]({
@@ -44,53 +86,62 @@ export default function ScoreboardAddButton({
const overlay = ( const overlay = (
<Card> <Card>
<div> <div>
<Form {entryLoading ? (
form={form} <LoadingSpinner />
layout="vertical" ) : (
onFinish={handleFinish} <Form
initialValues={{}} form={form}
> layout="vertical"
<Form.Item onFinish={handleFinish}
label={t("scoreboard.fields.date")} initialValues={{}}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
> >
<FormDatePicker /> <Form.Item
</Form.Item> label={t("scoreboard.fields.date")}
<Form.Item name="date"
label={t("scoreboard.fields.bodyhrs")} rules={[
name="bodyhrs" {
rules={[ required: true,
{ //message: t("general.validation.required"),
required: true, },
//message: t("general.validation.required"), ]}
}, >
]} <FormDatePicker />
> </Form.Item>
<InputNumber precision={1} /> <Form.Item
</Form.Item> label={t("scoreboard.fields.bodyhrs")}
<Form.Item name="bodyhrs"
label={t("scoreboard.fields.painthrs")} rules={[
name="painthrs" {
rules={[ required: true,
{ //message: t("general.validation.required"),
required: true, },
//message: t("general.validation.required"), ]}
}, >
]} <InputNumber precision={1} />
> </Form.Item>
<InputNumber precision={1} /> <Form.Item
</Form.Item> label={t("scoreboard.fields.painthrs")}
name="painthrs"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<InputNumber precision={1} />
</Form.Item>
<Button type="primary" htmlType="submit"> <Space wrap>
{t("general.actions.save")} <Button type="primary" htmlType="submit">
</Button> {t("general.actions.save")}
</Form> </Button>
<Button onClick={() => setVisibility(false)}>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
)}
</div> </div>
</Card> </Card>
); );
@@ -99,7 +150,7 @@ export default function ScoreboardAddButton({
setLoading(true); setLoading(true);
const v = job.joblines.reduce( const v = job.joblines.reduce(
(acc, val) => { (acc, val) => {
if (val.mod_lbr_ty === "LAB") if (val.mod_lbr_ty !== "LAR")
acc = { ...acc, bodyhrs: acc.bodyhrs + val.mod_lb_hrs }; acc = { ...acc, bodyhrs: acc.bodyhrs + val.mod_lb_hrs };
if (val.mod_lbr_ty === "LAR") if (val.mod_lbr_ty === "LAR")
acc = { ...acc, painthrs: acc.painthrs + val.mod_lb_hrs }; acc = { ...acc, painthrs: acc.painthrs + val.mod_lb_hrs };

View File

@@ -140,16 +140,17 @@ export function JobsAvailableContainer({
: {}), : {}),
}; };
if (selectedOwner) {
newJob.ownerid = selectedOwner;
delete newJob.owner;
}
if (newJob.vehicleid) {
delete newJob.vehicle;
}
insertNewJob({ insertNewJob({
variables: { variables: {
job: selectedOwner job: newJob,
? Object.assign(
{},
newJob,
{ owner: null },
{ ownerid: selectedOwner }
)
: newJob,
}, },
}) })
.then((r) => { .then((r) => {
@@ -199,11 +200,10 @@ export function JobsAvailableContainer({
message: t("jobs.errors.creating", { error: "No job data present." }), message: t("jobs.errors.creating", { error: "No job data present." }),
}); });
} else { } else {
//IO-539 Check for Parts Rate on PAL for SGI use case.
await CheckTaxRates(estData, bodyshop);
//create upsert job //create upsert job
let supp = replaceEmpty({ ...estData.est_data }); let supp = replaceEmpty({ ...estData.est_data });
//IO-539 Check for Parts Rate on PAL for SGI use case.
await CheckTaxRates(supp, bodyshop);
delete supp.owner; delete supp.owner;
delete supp.vehicle; delete supp.vehicle;
@@ -391,101 +391,104 @@ function replaceEmpty(someObj, replaceValue = null) {
value === "" ? replaceValue || null : value; value === "" ? replaceValue || null : value;
//^ because you seem to want to replace (strings) "null" or "undefined" too //^ because you seem to want to replace (strings) "null" or "undefined" too
const temp = JSON.stringify(someObj, replacer); const temp = JSON.stringify(someObj, replacer);
console.log("Parsed", JSON.parse(temp));
return JSON.parse(temp); return JSON.parse(temp);
} }
async function CheckTaxRates(estData, bodyshop) { async function CheckTaxRates(estData, bodyshop) {
console.log(
"🚀 ~ file: jobs-available-table.container.jsx ~ line 398 ~ estData",
estData
);
//LKQ Check //LKQ Check
if ( if (
!estData.est_data.parts_tax_rates?.PAL || !estData.parts_tax_rates?.PAL ||
estData.est_data.parts_tax_rates?.PAL?.prt_tax_rt === null || estData.parts_tax_rates?.PAL?.prt_tax_rt === null ||
estData.est_data.parts_tax_rates?.PAL?.prt_tax_rt === 0 estData.parts_tax_rates?.PAL?.prt_tax_rt === 0
) { ) {
const res = await confirmDialog( const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for LKQ parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.` `ImEX Online has detected that there is a missing tax rate for LKQ parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
); );
if (res) { if (res) {
if (!estData.est_data.parts_tax_rates.PAL) { if (!estData.parts_tax_rates.PAL) {
estData.est_data.parts_tax_rates.PAL = { estData.parts_tax_rates.PAL = {
prt_discp: 0, prt_discp: 0,
prt_mktyp: true, prt_mktyp: true,
prt_mkupp: 0, prt_mkupp: 0,
prt_type: "PAL", prt_type: "PAL",
}; };
} }
estData.est_data.parts_tax_rates.PAL.prt_tax_rt = estData.parts_tax_rates.PAL.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100; bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAL.prt_tax_in = true; estData.parts_tax_rates.PAL.prt_tax_in = true;
} }
} }
//PAC Check //PAC Check
if ( if (
!estData.est_data.parts_tax_rates?.PAC || !estData.parts_tax_rates?.PAC ||
estData.est_data.parts_tax_rates?.PAC?.prt_tax_rt === null || estData.parts_tax_rates?.PAC?.prt_tax_rt === null ||
estData.est_data.parts_tax_rates?.PAC?.prt_tax_rt === 0 estData.parts_tax_rates?.PAC?.prt_tax_rt === 0
) { ) {
const res = await confirmDialog( const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for rechromed parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.` `ImEX Online has detected that there is a missing tax rate for rechromed parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
); );
if (res) { if (res) {
if (!estData.est_data.parts_tax_rates.PAC) { if (!estData.parts_tax_rates.PAC) {
estData.est_data.parts_tax_rates.PAC = { estData.parts_tax_rates.PAC = {
prt_discp: 0, prt_discp: 0,
prt_mktyp: true, prt_mktyp: true,
prt_mkupp: 0, prt_mkupp: 0,
prt_type: "PAC", prt_type: "PAC",
}; };
} }
estData.est_data.parts_tax_rates.PAC.prt_tax_rt = estData.parts_tax_rates.PAC.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100; bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAC.prt_tax_in = true; estData.parts_tax_rates.PAC.prt_tax_in = true;
} }
} }
//PAM Check //PAM Check
if ( if (
!estData.est_data.parts_tax_rates?.PAM || !estData.parts_tax_rates?.PAM ||
estData.est_data.parts_tax_rates?.PAM?.prt_tax_rt === null || estData.parts_tax_rates?.PAM?.prt_tax_rt === null ||
estData.est_data.parts_tax_rates?.PAM?.prt_tax_rt === 0 estData.parts_tax_rates?.PAM?.prt_tax_rt === 0
) { ) {
const res = await confirmDialog( const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for remanufactured parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.` `ImEX Online has detected that there is a missing tax rate for remanufactured parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
); );
if (res) { if (res) {
if (!estData.est_data.parts_tax_rates.PAM) { if (!estData.parts_tax_rates.PAM) {
estData.est_data.parts_tax_rates.PAM = { estData.parts_tax_rates.PAM = {
prt_discp: 0, prt_discp: 0,
prt_mktyp: true, prt_mktyp: true,
prt_mkupp: 0, prt_mkupp: 0,
prt_type: "PAM", prt_type: "PAM",
}; };
} }
estData.est_data.parts_tax_rates.PAM.prt_tax_rt = estData.parts_tax_rates.PAM.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100; bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAM.prt_tax_in = true; estData.parts_tax_rates.PAM.prt_tax_in = true;
} }
} }
if ( if (
!estData.est_data.parts_tax_rates?.PAR || !estData.parts_tax_rates?.PAR ||
estData.est_data.parts_tax_rates?.PAR?.prt_tax_rt === null || estData.parts_tax_rates?.PAR?.prt_tax_rt === null ||
estData.est_data.parts_tax_rates?.PAR?.prt_tax_rt === 0 estData.parts_tax_rates?.PAR?.prt_tax_rt === 0
) { ) {
const res = await confirmDialog( const res = await confirmDialog(
`ImEX Online has detected that there is a missing tax rate for recored parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.` `ImEX Online has detected that there is a missing tax rate for recored parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
); );
if (res) { if (res) {
if (!estData.est_data.parts_tax_rates.PAR) { if (!estData.parts_tax_rates.PAR) {
estData.est_data.parts_tax_rates.PAR = { estData.parts_tax_rates.PAR = {
prt_discp: 0, prt_discp: 0,
prt_mktyp: true, prt_mktyp: true,
prt_mkupp: 0, prt_mkupp: 0,
prt_type: "PAR", prt_type: "PAR",
}; };
} }
estData.est_data.parts_tax_rates.PAR.prt_tax_rt = estData.parts_tax_rates.PAR.prt_tax_rt =
bodyshop.bill_tax_rates.state_tax_rate / 100; bodyshop.bill_tax_rates.state_tax_rate / 100;
estData.est_data.parts_tax_rates.PAR.prt_tax_in = true; estData.parts_tax_rates.PAR.prt_tax_in = true;
} }
} }
} }

View File

@@ -18,6 +18,11 @@ 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";
const graphProps = {
strokeWidth: 3,
};
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
@@ -51,7 +56,7 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
} }
const theValue = { const theValue = {
date: moment(val).format("D dd"), date: moment(val).format("D ddd"),
paintHrs: _.round(dayhrs.painthrs, 1), paintHrs: _.round(dayhrs.painthrs, 1),
bodyHrs: _.round(dayhrs.bodyhrs, 1), bodyHrs: _.round(dayhrs.bodyhrs, 1),
accTargetHrs: _.round( accTargetHrs: _.round(
@@ -81,36 +86,37 @@ export function ScoreboardChart({ sbEntriesByDate, bodyshop }) {
margin={{ top: 20, right: 20, bottom: 20, left: 20 }} margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
> >
<CartesianGrid stroke="#f5f5f5" /> <CartesianGrid stroke="#f5f5f5" />
<XAxis dataKey="date" /> <XAxis dataKey="date" strokeWidth={graphProps.strokeWidth} />
<YAxis /> <YAxis strokeWidth={graphProps.strokeWidth} />
<Tooltip /> <Tooltip />
<Legend /> <Legend />
<Area <Area
type="monotone" type="monotone"
name="Accumulated Hours" name="Accumulated Hours"
dataKey="accHrs" dataKey="accHrs"
fill="#8884d8" fill="lightgreen"
stroke="#8884d8" stroke="green"
/> />
<Bar <Bar
name="Body Hours" name="Body Hours"
dataKey="bodyHrs" dataKey="bodyHrs"
stackId="day" stackId="day"
barSize={20} barSize={20}
fill="#cecece" fill="darkblue"
/> />
<Bar <Bar
name="Paint Hours" name="Paint Hours"
dataKey="paintHrs" dataKey="paintHrs"
stackId="day" stackId="day"
barSize={20} barSize={20}
fill="#413ea0" fill="darkred"
/> />
<Line <Line
name="Target Hours" name="Target Hours"
type="monotone" type="monotone"
dataKey="accTargetHrs" dataKey="accTargetHrs"
stroke="#ff7300" stroke="#ff7300"
strokeWidth={graphProps.strokeWidth}
/> />
</ComposedChart> </ComposedChart>
</ResponsiveContainer> </ResponsiveContainer>

View File

@@ -1,12 +1,33 @@
import { Col, Row } from "antd"; import { Col, Row } from "antd";
import React from "react"; import React, { useEffect } from "react";
import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component"; import ScoreboardChart from "../scoreboard-chart/scoreboard-chart.component";
import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component"; import ScoreboardLastDays from "../scoreboard-last-days/scoreboard-last-days.component";
import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component"; import ScoreboardTargetsTable from "../scoreboard-targets-table/scoreboard-targets-table.component";
export default function ScoreboardDisplayComponent({ scoreboardSubscription }) { import { connect } from "react-redux";
const { data } = scoreboardSubscription; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import moment from "moment";
import { useApolloClient } from "@apollo/client";
import { GET_BLOCKED_DAYS } from "../../graphql/scoreboard.queries";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ScoreboardDisplayComponent);
export function ScoreboardDisplayComponent({
bodyshop,
scoreboardSubscription,
}) {
const { data } = scoreboardSubscription;
const client = useApolloClient();
const scoreBoardlist = (data && data.scoreboard) || []; const scoreBoardlist = (data && data.scoreboard) || [];
const sbEntriesByDate = {}; const sbEntriesByDate = {};
@@ -19,6 +40,29 @@ export default function ScoreboardDisplayComponent({ scoreboardSubscription }) {
sbEntriesByDate[entryDate].push(i); sbEntriesByDate[entryDate].push(i);
}); });
useEffect(() => {
//Update the locals.
async function setMomentSettings() {
const {
data: { appointments },
} = await client.query({
query: GET_BLOCKED_DAYS,
variables: {
start: moment().startOf("month"),
end: moment().endOf("month"),
},
});
moment.updateLocale("ca", {
workingWeekdays: translateSettingsToWorkingDays(bodyshop.workingdays),
holidays: appointments.map((h) => moment(h.start).format("MM-DD-YYYY")),
holidayFormat: "MM-DD-YYYY",
});
}
setMomentSettings();
}, [client, bodyshop]);
return ( return (
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={24}> <Col span={24}>
@@ -35,3 +79,30 @@ export default function ScoreboardDisplayComponent({ scoreboardSubscription }) {
</Row> </Row>
); );
} }
function translateSettingsToWorkingDays(workingdays) {
const days = [];
if (workingdays.monday) {
days.push(1);
}
if (workingdays.tuesday) {
days.push(2);
}
if (workingdays.wednesday) {
days.push(3);
}
if (workingdays.thursday) {
days.push(4);
}
if (workingdays.friday) {
days.push(5);
}
if (workingdays.saturday) {
days.push(6);
}
if (workingdays.sunday) {
days.push(0);
}
return days;
}

View File

@@ -1,8 +1,8 @@
import moment from "moment-business-days"; import moment from "moment-business-days";
moment.updateLocale("ca", { // moment.updateLocale("ca", {
workingWeekdays: [1, 2, 3, 4, 5], // workingWeekdays: [1, 2, 3, 4, 5, 6],
}); // });
export const CalculateWorkingDaysThisMonth = () => { export const CalculateWorkingDaysThisMonth = () => {
return moment().endOf("month").businessDaysIntoMonth(); return moment().endOf("month").businessDaysIntoMonth();

View File

@@ -51,3 +51,34 @@ export const UPDATE_SCOREBOARD_ENTRY = gql`
} }
} }
`; `;
export const QUERY_SCOREBOARD_ENTRY = gql`
query QUERY_SCOREBOARD_ENTRY($jobid: uuid!) {
scoreboard(where: { jobid: { _eq: $jobid } }) {
bodyhrs
date
id
painthrs
}
}
`;
export const GET_BLOCKED_DAYS = gql`
query GET_BLOCKED_DAYS($start: timestamptz, $end: timestamptz) {
appointments(
where: {
_and: [
{ block: { _eq: true } }
{ canceled: { _eq: false } }
{ start: { _gte: $start } }
{ end: { _lte: $end } }
]
}
) {
id
block
start
end
}
}
`;