Files
bodyshop/client/src/pages/jobs-close/jobs-close.component.jsx
Allan Carr fa7d90d2a9 IO-2651 Audit Log Extension
Signed-off-by: Allan Carr <allan.carr@thinkimex.com>
2024-03-05 15:28:22 -08:00

544 lines
19 KiB
JavaScript

import { DeleteFilled } from "@ant-design/icons";
import { useApolloClient, useMutation } from "@apollo/client";
import {
Alert,
Button,
Col,
Divider,
Form,
Input,
InputNumber,
PageHeader,
Popconfirm,
Row,
Select,
Space,
Statistic,
Switch,
Typography,
notification,
} from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
//import { useHistory } from "react-router-dom";
import { useTreatments } from "@splitsoftware/splitio-react";
import Dinero from "dinero.js";
import moment from "moment";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import DateTimePicker from "../../components/form-date-time-picker/form-date-time-picker.component";
import FormsFieldChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../../components/form-items-formatted/currency-form-item.component";
import JobsScoreboardAdd from "../../components/job-scoreboard-add-button/job-scoreboard-add-button.component";
import JobsCloseAutoAllocate from "../../components/jobs-close-auto-allocate/jobs-close-auto-allocate.component";
import JobsCloseLines from "../../components/jobs-close-lines/jobs-close-lines.component";
import LayoutFormRow from "../../components/layout-form-row/layout-form-row.component";
import { generateJobLinesUpdatesForInvoicing } from "../../graphql/jobs-lines.queries";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const client = useApolloClient();
// const history = useHistory();
const [closeJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const { Qb_Multi_Ar } = useTreatments(
["Qb_Multi_Ar"],
{},
bodyshop && bodyshop.imexshopid
);
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop && bodyshop.imexshopid
);
const handleFinish = async ({ removefromproduction, ...values }) => {
setLoading(true);
const result = await client.mutate({
mutation: generateJobLinesUpdatesForInvoicing(values.joblines),
});
if (result.errors) {
return; // Abandon the rest of the close.
}
const closeResult = await closeJob({
variables: {
jobId: job.id,
job: {
status: bodyshop.md_ro_statuses.default_invoiced || "",
date_invoiced: values.date_invoiced,
actual_in: values.actual_in,
actual_completion: values.actual_completion,
actual_delivery: values.actual_delivery,
kmin: values.kmin,
kmout: values.kmout,
dms_allocation: values.dms_allocation,
...(removefromproduction ? { inproduction: false } : {}),
...(values.qb_multiple_payers
? { qb_multiple_payers: values.qb_multiple_payers }
: {}),
},
},
refetchQueries: ["QUERY_JOB_CLOSE_DETAILS"],
awaitRefetchQueries: true,
});
if (!result.errors) {
// notification["success"]({ message: t("jobs.successes.save") });
// form.resetFields();
} else {
notification["error"]({
message: t("job.errors.saving", {
error: JSON.stringify(result.errors),
}),
});
return; // Abandon the rest of the close.
}
if (!closeResult.errors) {
setLoading(false);
notification["success"]({
message: t("jobs.successes.closed"),
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobinvoiced(),
type: "jobinvoiced",
});
// history.push(`/manage/jobs/${job.id}`);
} else {
setLoading(false);
notification["error"]({
message: t("job.errors.closing", {
error: JSON.stringify(closeResult.errors),
}),
});
}
form.resetFields();
form.resetFields();
setLoading(false);
};
return (
<div>
<Form
layout="vertical"
form={form}
onFinish={handleFinish}
initialValues={{
joblines: job.joblines,
actual_in: job.actual_in
? moment(job.actual_in)
: job.scheduled_in && moment(job.scheduled_in),
actual_completion: job.actual_completion
? moment(job.actual_completion)
: job.scheduled_completion && moment(job.scheduled_completion),
actual_delivery: job.actual_delivery
? moment(job.actual_delivery)
: job.scheduled_delivery && moment(job.scheduled_delivery),
date_invoiced: job.date_invoiced
? moment(job.date_invoiced)
: moment(),
kmin: job.kmin,
kmout: job.kmout,
dms_allocation: job.dms_allocation,
qb_multiple_payers: job.qb_multiple_payers,
}}
scrollToFirstError
>
<PageHeader
title={t("jobs.labels.closejob", { ro_number: job.ro_number })}
extra={
<Space>
<JobsCloseAutoAllocate
joblines={job.joblines}
form={form}
disabled={!!job.date_exported || jobRO}
/>
<Popconfirm
onConfirm={() => form.submit()}
disabled={jobRO}
okText={t("general.labels.yes")}
cancelText={t("general.labels.no")}
title={t("jobs.labels.closeconfirm")}
>
<Button loading={loading} type="danger" disabled={jobRO}>
{t("general.actions.close")}
</Button>
</Popconfirm>
{(bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid) && (
<Link to={`/manage/dms?jobId=${job.id}`}>
<Button disabled={job.date_exported || !jobRO}>
{t("jobs.actions.sendtodms")}
</Button>
</Link>
)}
<JobsScoreboardAdd job={job} disabled={false} />
</Space>
}
/>
<Space wrap direction="vertical" style={{ width: "100%" }}>
<FormsFieldChanged form={form} />
{!job.actual_in && job.scheduled_in && (
<Alert
type="warning"
message={t("jobs.labels.actual_in_inferred")}
/>
)}
{!job.actual_completion && job.scheduled_completion && (
<Alert
type="warning"
message={t("jobs.labels.actual_completion_inferred")}
/>
)}
{!job.actual_delivery && job.scheduled_delivery && (
<Alert
type="warning"
message={t("jobs.labels.actual_delivery_inferred")}
/>
)}
</Space>
<LayoutFormRow>
<Form.Item
label={t("jobs.fields.actual_in")}
rules={[
{
required: true,
},
]}
name="actual_in"
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
<Form.Item shouldUpdate>
{() => {
return (
<Form.Item
label={t("jobs.fields.actual_completion")}
name="actual_completion"
rules={[
{
required: true,
},
]}
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
);
}}
</Form.Item>
<Form.Item
label={t("jobs.fields.actual_delivery")}
name="actual_delivery"
rules={[
{
required: true,
},
]}
>
<DateTimePicker disabled={jobRO} />
</Form.Item>
<Form.Item
label={t("jobs.fields.date_invoiced")}
name="date_invoiced"
rules={[
{
required: true,
},
({ getFieldValue }) => ({
validator(_, value) {
if (!bodyshop.cdk_dealerid) return Promise.resolve();
if (!value || moment(value).isSameOrAfter(moment(), "day")) {
return Promise.resolve();
}
return Promise.reject(
new Error(t("jobs.labels.dms.invoicedatefuture"))
);
},
}),
({ getFieldValue }) => ({
validator(_, value) {
if (
ClosingPeriod.treatment === "on" &&
bodyshop.accountingconfig.ClosingPeriod
) {
if (
moment(value).isSameOrAfter(
moment(
bodyshop.accountingconfig.ClosingPeriod[0]
).startOf("day")
) &&
moment(value).isSameOrBefore(
moment(
bodyshop.accountingconfig.ClosingPeriod[1]
).endOf("day")
)
) {
return Promise.resolve();
} else {
return Promise.reject(
new Error(t("jobs.labels.closingperiod"))
);
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<DateTimePicker
disabled={jobRO}
onlyFuture={!!bodyshop.cdk_dealerid}
/>
</Form.Item>
{!jobRO && job.inproduction && (
<Form.Item
label={t("jobs.actions.removefromproduction")}
name="removefromproduction"
valuePropName="checked"
>
<Switch />
</Form.Item>
)}
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
<Form.Item
label={t("jobs.fields.kmin")}
name="kmin"
rules={[
{
required: true,
},
]}
>
<InputNumber precision={0} disabled={jobRO} />
</Form.Item>
)}
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
<Form.Item
label={t("jobs.fields.kmout")}
name="kmout"
dependencies={["kmin"]}
hasFeedback
rules={[
{
required: true,
},
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue("kmin") <= value) {
return Promise.resolve();
}
return Promise.reject(
new Error(t("jobs.labels.dms.kmoutnotgreaterthankmin"))
);
},
}),
]}
>
<InputNumber precision={0} disabled={jobRO} />
</Form.Item>
)}
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
<Form.Item
label={t("jobs.fields.dms_allocation")}
name="dms_allocation"
rules={[
{
required: true,
},
]}
>
<Input disabled />
</Form.Item>
)}
</LayoutFormRow>
<Divider>{t("jobs.labels.multipayers")}</Divider>
{Qb_Multi_Ar.treatment === "on" && (
<Row gutter={[16, 16]}>
<Col lg={8} md={24}>
<Form.List
name={["qb_multiple_payers"]}
rules={[
({ getFieldValue }) => ({
validator(_, value) {
let totalAllocated = Dinero();
const payers = form.getFieldValue("qb_multiple_payers");
payers &&
payers.forEach((payer) => {
totalAllocated = totalAllocated.add(
Dinero({
amount: Math.round((payer?.amount || 0) * 100),
})
);
});
const discrep = job.job_totals
? Dinero(job.job_totals.totals.total_repairs).subtract(
totalAllocated
)
: Dinero();
return discrep.getAmount() >= 0
? Promise.resolve()
: Promise.reject(
new Error(
t("jobs.labels.additionalpayeroverallocation")
)
);
},
}),
]}
>
{(fields, { add, remove }) => {
return (
<div>
{fields.map((field, index) => (
<Form.Item key={field.key}>
<Space>
<Form.Item
label={t("jobs.fields.qb_multiple_payers.name")}
key={`${index}name`}
name={[field.name, "name"]}
rules={[
{
required: true,
},
]}
>
<Select
style={{ minWidth: "12rem" }}
disabled={jobRO}
>
{bodyshop.md_ins_cos.map((s) => (
<Select.Option key={s.name} value={s.name}>
{s.name}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
label={t("jobs.fields.qb_multiple_payers.amount")}
key={`${index}amount`}
name={[field.name, "amount"]}
rules={[
{
required: true,
},
]}
>
<CurrencyInput min={0} disabled={jobRO} />
</Form.Item>
<DeleteFilled
disabled={jobRO}
onClick={() => {
remove(field.name);
}}
/>
</Space>
</Form.Item>
))}
<Form.Item>
<Button
disabled={jobRO}
onClick={() => {
add();
}}
style={{ width: "100%" }}
>
{t("jobs.actions.dms.addpayer")}
</Button>
</Form.Item>
</div>
);
}}
</Form.List>
</Col>
<Col lg={16} md={24}>
<Form.Item shouldUpdate>
{() => {
//Perform Calculation to determine discrepancy.
let totalAllocated = Dinero();
const payers = form.getFieldValue("qb_multiple_payers");
payers &&
payers.forEach((payer) => {
totalAllocated = totalAllocated.add(
Dinero({
amount: Math.round((payer?.amount || 0) * 100),
})
);
});
const discrep = job.job_totals
? Dinero(job.job_totals.totals.total_repairs).subtract(
totalAllocated
)
: Dinero();
return (
<Space size="large" wrap align="center">
<Statistic
title={t("jobs.labels.total_cust_payable")}
value={(job.job_totals
? Dinero(job.job_totals.totals.custPayable)
: Dinero()
).toFormat()}
/>
<Divider type="vertical" />
<Statistic
title={t("jobs.labels.total_repairs")}
value={(job.job_totals
? Dinero(job.job_totals.totals.total_repairs)
: Dinero()
).toFormat()}
/>
<Typography.Title>-</Typography.Title>
<Statistic
title={t("jobs.labels.dms.totalallocated")}
value={totalAllocated.toFormat()}
/>
<Typography.Title>=</Typography.Title>
<Statistic
title={t("jobs.labels.pimraryamountpayable")}
valueStyle={{
color: discrep.getAmount() > 0 ? "green" : "red",
}}
value={discrep.toFormat()}
/>
</Space>
);
}}
</Form.Item>
</Col>
</Row>
)}
<Divider />
<JobsCloseLines job={job} />
</Form>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(JobsCloseComponent);