Files
bodyshop/client/src/pages/jobs-close/jobs-close.component.jsx
2025-01-21 17:20:46 -08:00

494 lines
18 KiB
JavaScript

import { DeleteFilled } from "@ant-design/icons";
import { useApolloClient, useMutation } from "@apollo/client";
import {
Alert,
Button,
Col,
Divider,
Form,
Input,
InputNumber,
Popconfirm,
Row,
Select,
Space,
Statistic,
Switch,
Typography
} from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
// import { useNavigate } from 'react-router-dom';
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import Dinero from "dinero.js";
import dayjs from "../../utils/day";
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";
import JobCloseRoGuardContainer from "../../components/job-close-ro-guard/job-close-ro-guard.container";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
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 = useNavigate();
const [closeJob] = useMutation(UPDATE_JOB);
const [loading, setLoading] = useState(false);
const notification = useNotification();
const {
treatments: { Qb_Multi_Ar, ClosingPeriod }
} = useSplitTreatments({
attributes: {},
names: ["Qb_Multi_Ar", "ClosingPeriod"],
splitKey: 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"
});
if (values.masterbypass) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobclosedwithbypass(),
type: "jobclosedwithbypass"
});
}
// history(`/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 ? dayjs(job.actual_in) : job.scheduled_in && dayjs(job.scheduled_in),
actual_completion: job.actual_completion
? dayjs(job.actual_completion)
: job.scheduled_completion && dayjs(job.scheduled_completion),
actual_delivery: job.actual_delivery
? dayjs(job.actual_delivery)
: job.scheduled_delivery && dayjs(job.scheduled_delivery),
date_invoiced: job.date_invoiced ? dayjs(job.date_invoiced) : dayjs(),
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="primary" 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>
}
/>
<JobCloseRoGuardContainer form={form} job={job} />
<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 || dayjs(value).isSameOrAfter(dayjs(), "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 (
dayjs(value).isSameOrAfter(dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf("day")) &&
dayjs(value).isSameOrBefore(dayjs(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>
{Qb_Multi_Ar.treatment === "on" && (
<>
<Divider>{t("jobs.labels.multipayers")}</Divider>
<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={() => {
if (!jobRO) {
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);