544 lines
19 KiB
JavaScript
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);
|