Files
bodyshop/client/src/components/bill-form/bill-form.component.jsx
2024-03-21 15:16:08 -07:00

503 lines
17 KiB
JavaScript

import Icon, { UploadOutlined } from '@ant-design/icons';
import { useApolloClient } from '@apollo/client';
import { useSplitTreatments } from '@splitsoftware/splitio-react';
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from 'antd';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MdOpenInNew } from 'react-icons/md';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { createStructuredSelector } from 'reselect';
import { CHECK_BILL_INVOICE_NUMBER } from '../../graphql/bills.queries';
import { selectBodyshop } from '../../redux/user/user.selectors';
import dayjs from '../../utils/day';
import InstanceRenderManager from '../../utils/instanceRenderMgr';
import AlertComponent from '../alert/alert.component';
import BillFormLinesExtended from '../bill-form-lines-extended/bill-form-lines-extended.component';
import FormDatePicker from '../form-date-picker/form-date-picker.component';
import FormFieldsChanged from '../form-fields-changed-alert/form-fields-changed-alert.component';
import CurrencyInput from '../form-items-formatted/currency-form-item.component';
import JobSearchSelect from '../job-search-select/job-search-select.component';
import LayoutFormRow from '../layout-form-row/layout-form-row.component';
import VendorSearchSelect from '../vendor-search-select/vendor-search-select.component';
import BillFormLines from './bill-form.lines.component';
import { CalculateBillTotal } from './bill-form.totals.utility';
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({
bodyshop,
disabled,
form,
vendorAutoCompleteOptions,
lineData,
responsibilityCenters,
loadLines,
billEdit,
disableInvNumber,
job,
loadOutstandingReturns,
loadInventory,
preferredMake,
}) {
const { t } = useTranslation();
const client = useApolloClient();
const [discount, setDiscount] = useState(0);
const {
treatments: { Extended_Bill_Posting, ClosingPeriod },
} = useSplitTreatments({
attributes: {},
names: ['Extended_Bill_Posting', 'ClosingPeriod'],
splitKey: bodyshop.imexshopid,
});
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue('jobid'),
vendorId: opt.value,
},
});
};
const handleFederalTaxExemptSwitchToggle = (checked) => {
// Early gate
if (!checked) return;
const values = form.getFieldsValue('billlines');
// Gate bill lines
if (!values?.billlines?.length) return;
const billlines = values.billlines.map((b) => {
b.applicable_taxes.federal = false;
return b;
});
form.setFieldsValue({ billlines });
};
useEffect(() => {
if (job) form.validateFields(['is_credit_memo']);
}, [job, form]);
useEffect(() => {
const vendorId = form.getFieldValue('vendorid');
if (vendorId && vendorAutoCompleteOptions) {
const matchingVendors = vendorAutoCompleteOptions.filter((v) => v.id === vendorId);
if (matchingVendors.length === 1) {
setDiscount(matchingVendors[0].discount);
}
}
const jobId = form.getFieldValue('jobid');
if (jobId) {
loadLines({ variables: { id: jobId } });
if (form.getFieldValue('is_credit_memo') && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
},
});
}
}
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
return (
<div>
<FormFieldsChanged form={form} />
<Form.Item style={{ display: 'none' }} name="isinhouse" valuePropName="checked">
<Switch />
</Form.Item>
<LayoutFormRow grow>
<Form.Item
name="jobid"
label={t('bills.fields.ro_number')}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<JobSearchSelect
disabled={billEdit || disabled}
convertedOnly
notExported={false}
onBlur={() => {
if (
form.getFieldValue('jobid') !== null &&
form.getFieldValue('jobid') !== undefined
) {
loadLines({ variables: { id: form.getFieldValue('jobid') } });
if (
form.getFieldValue('vendorid') !== null &&
form.getFieldValue('vendorid') !== undefined
) {
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue('jobid'),
vendorId: form.getFieldValue('vendorid'),
},
});
}
}
}}
/>
</Form.Item>
<Form.Item
label={t('bills.fields.vendor')}
name="vendorid"
// style={{ display: billEdit ? "none" : null }}
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (value && !getFieldValue(['isinhouse']) && value === bodyshop.inhousevendorid) {
return Promise.reject(t('bills.validation.manualinhouse'));
}
return Promise.resolve();
},
}),
]}
>
<VendorSearchSelect
disabled={disabled}
options={vendorAutoCompleteOptions}
preferredMake={preferredMake}
onSelect={handleVendorSelect}
/>
</Form.Item>
</LayoutFormRow>
{job &&
job.ious &&
job.ious.length > 0 &&
job.ious.map((iou) => (
<Alert
key={iou.id}
type="warning"
message={
<Space>
{t('bills.labels.iouexists')}
<Link
target="_blank"
rel="noopener noreferrer"
to={`/manage/jobs/${iou.id}?tab=repairdata`}
>
<Space>
{iou.ro_number}
<Icon component={MdOpenInNew} />
</Space>
</Link>
</Space>
}
/>
))}
<LayoutFormRow>
<Form.Item
label={t('bills.fields.invoice_number')}
name="invoice_number"
validateTrigger="onBlur"
hasFeedback
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
async validator(rule, value) {
const vendorid = getFieldValue('vendorid');
if (vendorid && value) {
const response = await client.query({
query: CHECK_BILL_INVOICE_NUMBER,
variables: {
invoice_number: value,
vendorid: vendorid,
},
});
if (response.data.bills_aggregate.aggregate.count === 0) {
return Promise.resolve();
} else if (
response.data.bills_aggregate.nodes.length === 1 &&
response.data.bills_aggregate.nodes[0].id === form.getFieldValue('id')
) {
return Promise.resolve();
}
return Promise.reject(t('bills.validation.unique_invoice_number'));
} else {
return Promise.resolve();
}
},
}),
]}
>
<Input disabled={disabled || disableInvNumber} />
</Form.Item>
<Form.Item
label={t('bills.fields.date')}
name="date"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
({ getFieldValue }) => ({
validator(rule, value) {
if (ClosingPeriod.treatment === 'on' && bodyshop.accountingconfig.ClosingPeriod) {
if (
dayjs(value)
.startOf('day')
.isSameOrAfter(
dayjs(bodyshop.accountingconfig.ClosingPeriod[0]).startOf('day')
) &&
dayjs(value)
.startOf('day')
.isSameOrBefore(
dayjs(bodyshop.accountingconfig.ClosingPeriod[1]).endOf('day')
)
) {
return Promise.resolve();
} else {
return Promise.reject(t('bills.validation.closingperiod'));
}
} else {
return Promise.resolve();
}
},
}),
]}
>
<FormDatePicker disabled={disabled} />
</Form.Item>
<Form.Item
label={t('bills.fields.is_credit_memo')}
name="is_credit_memo"
valuePropName="checked"
rules={[
({ getFieldValue }) => ({
validator(rule, value) {
if (value === true && getFieldValue('jobid') && getFieldValue('vendorid')) {
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
// loadOutstandingReturns({
// variables: {
// jobId: form.getFieldValue("jobid"),
// vendorId: form.getFieldValue("vendorid"),
// },
// });
}
if (
!bodyshop.bill_allow_post_to_closed &&
job &&
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
job.status === bodyshop.md_ro_statuses.default_exported ||
job.status === bodyshop.md_ro_statuses.default_void) &&
(value === false || !value)
) {
return Promise.reject(t('bills.labels.onlycmforinvoiced'));
}
return Promise.resolve();
},
}),
]}
>
<Switch />
</Form.Item>
<Form.Item
label={t('bills.fields.total')}
name="total"
rules={[
{
required: true,
//message: t("general.validation.required"),
},
]}
>
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
{!billEdit && (
<Form.Item label={t('bills.fields.allpartslocation')} name="location">
<Select style={{ width: '10rem' }} disabled={disabled} allowClear>
{bodyshop.md_parts_locations.map((loc, idx) => (
<Select.Option key={idx} value={loc}>
{loc}
</Select.Option>
))}
</Select>
</Form.Item>
)}
</LayoutFormRow>
<LayoutFormRow>
{InstanceRenderManager({
imex: (
<Form.Item span={3} label={t('bills.fields.federal_tax_rate')} name="federal_tax_rate">
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
),
})}
<Form.Item span={3} label={t('bills.fields.state_tax_rate')} name="state_tax_rate">
<CurrencyInput min={0} disabled={disabled} />
</Form.Item>
{InstanceRenderManager({
imex: (
<>
<Form.Item span={3} label={t('bills.fields.local_tax_rate')} name="local_tax_rate">
<CurrencyInput min={0} />
</Form.Item>
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
<Form.Item
span={2}
label={t('bills.labels.federal_tax_exempt')}
name="federal_tax_exempt"
>
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
</Form.Item>
) : null}
</>
),
})}
<Form.Item shouldUpdate span={13}>
{() => {
const values = form.getFieldsValue([
'billlines',
'total',
'federal_tax_rate',
'state_tax_rate',
'local_tax_rate',
]);
let totals;
if (!!values.total && !!values.billlines && values.billlines.length > 0)
totals = CalculateBillTotal(values);
if (!!totals)
return (
<div align="right">
<Space wrap>
<Statistic
title={t('bills.labels.subtotal')}
value={totals.subtotal.toFormat()}
precision={2}
/>
{InstanceRenderManager({
imex: (
<Statistic
title={t('bills.labels.federal_tax')}
value={totals.federalTax.toFormat()}
precision={2}
/>
),
})}
<Statistic
title={t('bills.labels.state_tax')}
value={totals.stateTax.toFormat()}
precision={2}
/>
{InstanceRenderManager({
imex: (
<Statistic
title={t('bills.labels.local_tax')}
value={totals.localTax.toFormat()}
precision={2}
/>
),
})}
<Statistic
title={t('bills.labels.entered_total')}
value={totals.enteredTotal.toFormat()}
precision={2}
/>
<Statistic
title={t('bills.labels.bill_total')}
value={totals.invoiceTotal.toFormat()}
precision={2}
/>
<Statistic
title={t('bills.labels.discrepancy')}
valueStyle={{
color: totals.discrepancy.getAmount() === 0 ? 'green' : 'red',
}}
value={totals.discrepancy.toFormat()}
precision={2}
/>
</Space>
{form.getFieldValue('is_credit_memo') ? (
<AlertComponent type="warning" message={t('bills.labels.enteringcreditmemo')} />
) : null}
</div>
);
return null;
}}
</Form.Item>
</LayoutFormRow>
<Divider orientation="left">{t('bills.labels.bill_lines')}</Divider>
{Extended_Bill_Posting.treatment === 'on' ? (
<BillFormLinesExtended
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
/>
) : (
<BillFormLines
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
billEdit={billEdit}
/>
)}
<Divider orientation="left" style={{ display: billEdit ? 'none' : null }}>
{t('documents.labels.upload')}
</Divider>
<Form.Item
name="upload"
label="Upload"
style={{ display: billEdit ? 'none' : null }}
valuePropName="fileList"
getValueFromEvent={(e) => {
if (Array.isArray(e)) {
return e;
}
return e && e.fileList;
}}
>
<Upload.Dragger multiple={true} name="logo" beforeUpload={() => false} listType="picture">
<>
<p className="ant-upload-drag-icon">
<UploadOutlined />
</p>
<p className="ant-upload-text">Click or drag files to this area to upload.</p>
</>
</Upload.Dragger>
</Form.Item>
</div>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);