Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2023-12-14 13:20:43 -05:00
parent a67fb3576c
commit 83e4fb3dc4
19 changed files with 623 additions and 594 deletions

View File

@@ -1,26 +1,16 @@
import Icon, { UploadOutlined } from "@ant-design/icons"; import Icon, {UploadOutlined} from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import {useApolloClient} from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react"; import {useTreatments} from "@splitsoftware/splitio-react";
import { import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd";
Alert,
Divider,
Form,
Input,
Select,
Space,
Statistic,
Switch,
Upload,
} from "antd";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import React, { useEffect, useState } from "react"; import React, {useEffect, useState} from "react";
import { useTranslation } from "react-i18next"; import {useTranslation} from "react-i18next";
import { MdOpenInNew } from "react-icons/md"; import {MdOpenInNew} from "react-icons/md";
import { connect } from "react-redux"; import {connect} from "react-redux";
import { Link } from "react-router-dom"; import {Link} from "react-router-dom";
import { createStructuredSelector } from "reselect"; import {createStructuredSelector} from "reselect";
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries"; import {CHECK_BILL_INVOICE_NUMBER} from "../../graphql/bills.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import {selectBodyshop} from "../../redux/user/user.selectors";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component"; import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component";
@@ -30,488 +20,488 @@ import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import BillFormLines from "./bill-form.lines.component"; import BillFormLines from "./bill-form.lines.component";
import { CalculateBillTotal } from "./bill-form.totals.utility"; import {CalculateBillTotal} from "./bill-form.totals.utility";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
const mapDispatchToProps = (dispatch) => ({}); const mapDispatchToProps = (dispatch) => ({});
export function BillFormComponent({ export function BillFormComponent({
bodyshop, bodyshop,
disabled, disabled,
form, form,
vendorAutoCompleteOptions, vendorAutoCompleteOptions,
lineData, lineData,
responsibilityCenters, responsibilityCenters,
loadLines, loadLines,
billEdit, billEdit,
disableInvNumber, disableInvNumber,
job, job,
loadOutstandingReturns, loadOutstandingReturns,
loadInventory, loadInventory,
preferredMake, preferredMake,
}) { }) {
const { t } = useTranslation(); const {t} = useTranslation();
const client = useApolloClient(); const client = useApolloClient();
const [discount, setDiscount] = useState(0); const [discount, setDiscount] = useState(0);
const { Extended_Bill_Posting } = useTreatments( const {Extended_Bill_Posting} = useTreatments(
["Extended_Bill_Posting"], ["Extended_Bill_Posting"],
{}, {},
bodyshop.imexshopid bodyshop.imexshopid
); );
const { ClosingPeriod } = useTreatments( const {ClosingPeriod} = useTreatments(
["ClosingPeriod"], ["ClosingPeriod"],
{}, {},
bodyshop.imexshopid bodyshop.imexshopid
); );
const handleVendorSelect = (props, opt) => { const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount); setDiscount(opt.discount);
opt && opt &&
!billEdit && !billEdit &&
loadOutstandingReturns({
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
};
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({ loadOutstandingReturns({
variables: { variables: {
jobId: jobId, jobId: form.getFieldValue("jobid"),
vendorId: vendorId, vendorId: opt.value,
}, },
}); });
} };
}
if (vendorId === bodyshop.inhousevendorid && !billEdit) { useEffect(() => {
loadInventory(); if (job) form.validateFields(["is_credit_memo"]);
} }, [job, form]);
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
return ( useEffect(() => {
<div> const vendorId = form.getFieldValue("vendorid");
<FormFieldsChanged form={form} /> if (vendorId && vendorAutoCompleteOptions) {
<Form.Item const matchingVendors = vendorAutoCompleteOptions.filter(
style={{ display: "none" }} (v) => v.id === vendorId
name="isinhouse" );
valuePropName="checked" if (matchingVendors.length === 1) {
> setDiscount(matchingVendors[0].discount);
<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) {
loadLines({ variables: { id: form.getFieldValue("jobid") } });
if (form.getFieldValue("vendorid") !== null) {
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>
} }
/> }
))} const jobId = form.getFieldValue("jobid");
<LayoutFormRow> if (jobId) {
<Form.Item loadLines({variables: {id: jobId}});
label={t("bills.fields.invoice_number")} if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
name="invoice_number" loadOutstandingReturns({
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: { variables: {
invoice_number: value, jobId: jobId,
vendorid: vendorid, vendorId: vendorId,
}, },
}); });
}
}
if (response.data.bills_aggregate.aggregate.count === 0) { if (vendorId === bodyshop.inhousevendorid && !billEdit) {
return Promise.resolve(); loadInventory();
} else if ( }
response.data.bills_aggregate.nodes.length === 1 && }, [
response.data.bills_aggregate.nodes[0].id === form,
form.getFieldValue("id") billEdit,
) { loadOutstandingReturns,
return Promise.resolve(); loadInventory,
} setDiscount,
return Promise.reject( vendorAutoCompleteOptions,
t("bills.validation.unique_invoice_number") loadLines,
); bodyshop.inhousevendorid,
} 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 ( return (
!bodyshop.bill_allow_post_to_closed && <div>
job && <FormFieldsChanged form={form}/>
(job.status === bodyshop.md_ro_statuses.default_invoiced || <Form.Item
job.status === bodyshop.md_ro_statuses.default_exported || style={{display: "none"}}
job.status === bodyshop.md_ro_statuses.default_void) && name="isinhouse"
(value === false || !value) valuePropName="checked"
) { >
return Promise.reject(t("bills.labels.onlycmforinvoiced")); <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) {
loadLines({variables: {id: form.getFieldValue("jobid")}});
if (form.getFieldValue("vendorid") !== null) {
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,
},
});
return Promise.resolve(); 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 ===
<Switch /> form.getFieldValue("id")
</Form.Item> ) {
<Form.Item return Promise.resolve();
label={t("bills.fields.total")} }
name="total" return Promise.reject(
rules={[ t("bills.validation.unique_invoice_number")
{ );
required: true, } else {
//message: t("general.validation.required"), return Promise.resolve();
}, }
]} },
> }),
<CurrencyInput min={0} disabled={disabled} /> ]}
</Form.Item> >
{!billEdit && ( <Input disabled={disabled || disableInvNumber}/>
<Form.Item label={t("bills.fields.allpartslocation")} name="location"> </Form.Item>
<Select style={{ width: "10rem" }} disabled={disabled} allowClear> <Form.Item
{bodyshop.md_parts_locations.map((loc, idx) => ( label={t("bills.fields.date")}
<Select.Option key={idx} value={loc}> name="date"
{loc} rules={[
</Select.Option> {
))} required: true,
</Select> //message: t("general.validation.required"),
</Form.Item> },
)} ({getFieldValue}) => ({
</LayoutFormRow> validator(rule, value) {
<LayoutFormRow> if (
<Form.Item ClosingPeriod.treatment === "on" &&
span={3} bodyshop.accountingconfig.ClosingPeriod
label={t("bills.fields.federal_tax_rate")} ) {
name="federal_tax_rate" if (
> dayjs(value)
<CurrencyInput min={0} disabled={disabled} /> .startOf("day")
</Form.Item> .isSameOrAfter(
<Form.Item dayjs(
span={3} bodyshop.accountingconfig.ClosingPeriod[0]
label={t("bills.fields.state_tax_rate")} ).startOf("day")
name="state_tax_rate" ) &&
> dayjs(value)
<CurrencyInput min={0} disabled={disabled} /> .startOf("day")
</Form.Item> .isSameOrBefore(
<Form.Item dayjs(
span={3} bodyshop.accountingconfig.ClosingPeriod[1]
label={t("bills.fields.local_tax_rate")} ).endOf("day")
name="local_tax_rate" )
> ) {
<CurrencyInput min={0} /> return Promise.resolve();
</Form.Item> } else {
<Form.Item shouldUpdate span={15}> return Promise.reject(t("bills.validation.closingperiod"));
{() => { }
const values = form.getFieldsValue([ } else {
"billlines", return Promise.resolve();
"total", }
"federal_tax_rate", },
"state_tax_rate", }),
"local_tax_rate", ]}
]); >
let totals; <FormDatePicker disabled={disabled}/>
if ( </Form.Item>
!!values.total && <Form.Item
!!values.billlines && label={t("bills.fields.is_credit_memo")}
values.billlines.length > 0 name="is_credit_memo"
) valuePropName="checked"
totals = CalculateBillTotal(values); rules={[
if (!!totals) ({getFieldValue}) => ({
return ( validator(rule, value) {
<div> if (
<Space wrap> value === true &&
<Statistic getFieldValue("jobid") &&
title={t("bills.labels.subtotal")} getFieldValue("vendorid")
value={totals.subtotal.toFormat()} ) {
precision={2} //Removed as this would cause an additional reload when validating the form on submit and clear the values.
/> // loadOutstandingReturns({
<Statistic // variables: {
title={t("bills.labels.federal_tax")} // jobId: form.getFieldValue("jobid"),
value={totals.federalTax.toFormat()} // vendorId: form.getFieldValue("vendorid"),
precision={2} // },
/> // });
<Statistic }
title={t("bills.labels.state_tax")}
value={totals.stateTax.toFormat()}
precision={2}
/>
<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" ? ( if (
<BillFormLinesExtended !bodyshop.bill_allow_post_to_closed &&
lineData={lineData} job &&
discount={discount} (job.status === bodyshop.md_ro_statuses.default_invoiced ||
form={form} job.status === bodyshop.md_ro_statuses.default_exported ||
responsibilityCenters={responsibilityCenters} job.status === bodyshop.md_ro_statuses.default_void) &&
disabled={disabled} (value === false || !value)
/> ) {
) : ( return Promise.reject(t("bills.labels.onlycmforinvoiced"));
<BillFormLines }
lineData={lineData}
discount={discount}
form={form}
responsibilityCenters={responsibilityCenters}
disabled={disabled}
billEdit={billEdit}
/>
)}
<Form.Item return Promise.resolve();
name="upload" },
label="Upload" }),
style={{ display: billEdit ? "none" : null }} ]}
valuePropName="fileList" >
getValueFromEvent={(e) => { <Switch/>
if (Array.isArray(e)) { </Form.Item>
return e; <Form.Item
} label={t("bills.fields.total")}
return e && e.fileList; name="total"
}} rules={[
> {
<Upload.Dragger required: true,
multiple={true} //message: t("general.validation.required"),
name="logo" },
beforeUpload={() => false} ]}
listType="picture" >
> <CurrencyInput min={0} disabled={disabled}/>
<> </Form.Item>
<p className="ant-upload-drag-icon"> {!billEdit && (
<UploadOutlined /> <Form.Item label={t("bills.fields.allpartslocation")} name="location">
</p> <Select style={{width: "10rem"}} disabled={disabled} allowClear>
<p className="ant-upload-text"> {bodyshop.md_parts_locations.map((loc, idx) => (
Click or drag files to this area to upload. <Select.Option key={idx} value={loc}>
</p> {loc}
</> </Select.Option>
</Upload.Dragger> ))}
</Form.Item> </Select>
</div> </Form.Item>
); )}
</LayoutFormRow>
<LayoutFormRow>
<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>
<Form.Item
span={3}
label={t("bills.fields.local_tax_rate")}
name="local_tax_rate"
>
<CurrencyInput min={0}/>
</Form.Item>
<Form.Item shouldUpdate span={15}>
{() => {
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>
<Space wrap>
<Statistic
title={t("bills.labels.subtotal")}
value={totals.subtotal.toFormat()}
precision={2}
/>
<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}
/>
<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}
/>
)}
<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); export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);

View File

@@ -14,10 +14,10 @@ export function getRange(dateParam, viewParam) {
start = dayjs(date).startOf("week"); start = dayjs(date).startOf("week");
end = dayjs(date).endOf("week"); end = dayjs(date).endOf("week");
} }
//if view is month: from dayjs(date).startOf('month').subtract(7, 'days') to dayjs(date).endOf('month').add(7, 'days'); i do additional 7 days math because you can see adjacent weeks on month view (that is the way how i generate my recurrent events for the Big Calendar, but if you need only start-end of month - just remove that math); //if view is month: from dayjs(date).startOf('month').subtract(7, 'day') to dayjs(date).endOf('month').add(7, 'day'); i do additional 7 days math because you can see adjacent weeks on month view (that is the way how i generate my recurrent events for the Big Calendar, but if you need only start-end of month - just remove that math);
else if (view === "month") { else if (view === "month") {
start = dayjs(date).startOf("month").subtract(7, "days"); start = dayjs(date).startOf("month").subtract(7, "day");
end = dayjs(date).endOf("month").add(7, "days"); end = dayjs(date).endOf("month").add(7, "day");
} }
// if view is agenda: from dayjs(date).startOf('day') to dayjs(date).endOf('day').add(1, 'month'); // if view is agenda: from dayjs(date).startOf('day') to dayjs(date).endOf('day').add(1, 'month');
else if (view === "agenda") { else if (view === "agenda") {

View File

@@ -201,7 +201,7 @@ export function ScheduleJobModalComponent({
{() => { {() => {
const values = form.getFieldsValue(); const values = form.getFieldsValue();
if (values.start) { if (values.start) {
calculateScheduleLoad(dayjs(values.start).add(3, "days")); calculateScheduleLoad(dayjs(values.start).add(3, "day"));
} }
return ( return (
<div className="schedule-job-modal"> <div className="schedule-job-modal">

View File

@@ -27,7 +27,7 @@ export function ScheduleVerifyIntegrity({ currentUser }) {
data: { arrJobs, compJobs, prodJobs }, data: { arrJobs, compJobs, prodJobs },
} = await client.query({ } = await client.query({
query: QUERY_SCHEDULE_LOAD_DATA, query: QUERY_SCHEDULE_LOAD_DATA,
variables: { start: dayjs(), end: dayjs().add(180, "days") }, variables: { start: dayjs(), end: dayjs().add(180, "day") },
}); });
//check that the leaving jobs are either in the arriving list, or in production. //check that the leaving jobs are either in the arriving list, or in production.

View File

@@ -17,7 +17,7 @@ const mapDispatchToProps = (dispatch) => ({
const rowGutter = [16, 16]; const rowGutter = [16, 16];
const statSpans = { xs: 24, sm: 3 }; const statSpans = { xs: 24, sm: 3 };
export function ScoreboardTimeticketsTargetsTable({ bodyshop }) { export function ScoreboardTimeTicketsTargetsTable({ bodyshop }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -282,4 +282,4 @@ export function ScoreboardTimeticketsTargetsTable({ bodyshop }) {
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(ScoreboardTimeticketsTargetsTable); )(ScoreboardTimeTicketsTargetsTable);

View File

@@ -17,7 +17,7 @@ export default function ScoreboardTimeTickets() {
const { start, end } = searchParams; const { start, end } = searchParams;
const startDate = start const startDate = start
? dayjs(start) ? dayjs(start)
: dayjs().startOf("week").subtract(7, "days"); : dayjs().startOf("week").subtract(7, "day");
const endDate = end ? dayjs(end) : dayjs().endOf("week"); const endDate = end ? dayjs(end) : dayjs().endOf("week");
const fixedPeriods = useMemo(() => { const fixedPeriods = useMemo(() => {

View File

@@ -16,7 +16,9 @@ const mapStateToProps = createStructuredSelector({
export function TechClockInComponent({ form, bodyshop, technician }) { export function TechClockInComponent({ form, bodyshop, technician }) {
const { t } = useTranslation(); const { t } = useTranslation();
const emps = bodyshop.employees.filter((e) => e.id === technician.id)[0]; const emps = bodyshop.employees.filter((e) => e.id === technician.id)[0];
return ( return (
<div> <div>
<LayoutFormRow grow noDivider> <LayoutFormRow grow noDivider>

View File

@@ -46,6 +46,16 @@ export function TechClockInContainer({
(e) => e.id === (technician && technician.id) (e) => e.id === (technician && technician.id)
)[0]; )[0];
const TechForm = () => {
if (technician) {
return <Form form={form} layout="vertical" onFinish={handleFinish} >
<TechClockInComponent form={form} />
</Form>
} else {
return <div />
}
}
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
const theTime = (await axios.post("/utils/time")).data; const theTime = (await axios.post("/utils/time")).data;
@@ -146,9 +156,7 @@ export function TechClockInContainer({
</Space> </Space>
} }
> >
<Form form={form} layout="vertical" onFinish={handleFinish}> <TechForm />
<TechClockInComponent form={form} />
</Form>
</Card> </Card>
); );
} }

View File

@@ -1,100 +1,101 @@
import { Card, List, Typography } from "antd"; import {Card, List, Typography} from "antd";
import React from "react"; import React from "react";
import { useQuery } from "@apollo/client"; import {useQuery} from "@apollo/client";
import { useTranslation } from "react-i18next"; import {useTranslation} from "react-i18next";
import { connect } from "react-redux"; import {connect} from "react-redux";
import { Link } from "react-router-dom"; import {Link} from "react-router-dom";
import { createStructuredSelector } from "reselect"; import {createStructuredSelector} from "reselect";
import { QUERY_ACTIVE_TIME_TICKETS } from "../../graphql/timetickets.queries"; import {QUERY_ACTIVE_TIME_TICKETS} from "../../graphql/timetickets.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import {selectTechnician} from "../../redux/tech/tech.selectors";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import {DateTimeFormatter} from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import DataLabel from "../data-label/data-label.component"; import DataLabel from "../data-label/data-label.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import TechClockOffButton from "../tech-job-clock-out-button/tech-job-clock-out-button.component"; import TechClockOffButton from "../tech-job-clock-out-button/tech-job-clock-out-button.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
technician: selectTechnician, technician: selectTechnician,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export function TechClockedInList({ technician }) { export function TechClockedInList({technician}) {
const { loading, error, data, refetch } = useQuery( const {loading, error, data, refetch} = useQuery(
QUERY_ACTIVE_TIME_TICKETS, QUERY_ACTIVE_TIME_TICKETS,
{ {
variables: { variables: {
employeeId: technician.id, employeeId: technician?.id,
}, },
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
} skip: !technician,
); }
);
const { t } = useTranslation(); const {t} = useTranslation();
if (loading) return <LoadingSpinner/>;
if (error) return <AlertComponent message={error.message} type="error"/>;
if (loading) return <LoadingSpinner />; return (
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<div>
{data.timetickets.length > 0 ? (
<div> <div>
<Typography.Title level={2}> {data?.timetickets?.length > 0 ? (
{t("timetickets.labels.alreadyclockedon")} <div>
</Typography.Title> <Typography.Title level={2}>
<List {t("timetickets.labels.alreadyclockedon")}
grid={{ </Typography.Title>
gutter: 32, <List
xs: 1, grid={{
sm: 2, gutter: 32,
md: 3, xs: 1,
lg: 4, sm: 2,
xl: 5, md: 3,
xxl: 6, lg: 4,
}} xl: 5,
dataSource={data.timetickets || []} xxl: 6,
renderItem={(ticket) => ( }}
<List.Item> dataSource={data.timetickets || []}
<Card renderItem={(ticket) => (
title={ <List.Item>
<Link to={`/tech/joblookup?selected=${ticket.job.id}`}> <Card
{`${ title={
ticket.job.ro_number || t("general.labels.na") <Link to={`/tech/joblookup?selected=${ticket.job.id}`}>
} ${OwnerNameDisplayFunction(ticket.job)}`} {`${
</Link> ticket.job.ro_number || t("general.labels.na")
} } ${OwnerNameDisplayFunction(ticket.job)}`}
actions={[ </Link>
<TechClockOffButton }
jobId={ticket.jobid} actions={[
timeTicketId={ticket.id} <TechClockOffButton
completedCallback={refetch} jobId={ticket.jobid}
/>, timeTicketId={ticket.id}
]} completedCallback={refetch}
> />,
<div> ]}
{` >
<div>
{`
${ticket.job.v_model_yr || ""} ${ ${ticket.job.v_model_yr || ""} ${
ticket.job.v_make_desc || "" ticket.job.v_make_desc || ""
} ${ticket.job.v_model_desc || ""}`} } ${ticket.job.v_model_desc || ""}`}
</div> </div>
<DataLabel label={t("timetickets.fields.clockon")}> <DataLabel label={t("timetickets.fields.clockon")}>
<DateTimeFormatter>{ticket.clockon}</DateTimeFormatter> <DateTimeFormatter>{ticket.clockon}</DateTimeFormatter>
</DataLabel> </DataLabel>
<DataLabel label={t("timetickets.fields.cost_center")}> <DataLabel label={t("timetickets.fields.cost_center")}>
{ticket.cost_center === "timetickets.labels.shift" {ticket.cost_center === "timetickets.labels.shift"
? t(ticket.cost_center) ? t(ticket.cost_center)
: ticket.cost_center} : ticket.cost_center}
</DataLabel> </DataLabel>
</Card> </Card>
</List.Item> </List.Item>
)} )}
></List> ></List>
</div>
) : null}
</div> </div>
) : null} );
</div>
);
} }
export default connect(mapStateToProps, mapDispatchToProps)(TechClockedInList); export default connect(mapStateToProps, mapDispatchToProps)(TechClockedInList);

View File

@@ -30,8 +30,9 @@ const TechJobStatistics = ({ technician }) => {
end: endDate.format("YYYY-MM-DD"), end: endDate.format("YYYY-MM-DD"),
fixedStart: dayjs().startOf("month").format("YYYY-MM-DD"), fixedStart: dayjs().startOf("month").format("YYYY-MM-DD"),
fixedEnd: dayjs().endOf("month").format("YYYY-MM-DD"), fixedEnd: dayjs().endOf("month").format("YYYY-MM-DD"),
employeeid: technician.id, employeeid: technician?.id,
}, },
skip: !technician,
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
} }

View File

@@ -33,7 +33,8 @@ export function TechLogin({
const navigate = useNavigate(); const navigate = useNavigate();
const handleFinish = (values) => { const handleFinish = (values) => {
techLoginStart(values); // Remap these because EmployeeID form name has previously been used in the project
techLoginStart({pin: values.pin, employeeid: values.techEmployeeId});
}; };
useEffect(() => { useEffect(() => {
@@ -49,7 +50,8 @@ export function TechLogin({
> >
<Form.Item <Form.Item
label={t("tech.fields.employeeid")} label={t("tech.fields.employeeid")}
name="employeeid" name="techEmployeeId"
rules={[ rules={[
{ {
required: true, required: true,

View File

@@ -37,7 +37,7 @@ export default function TimeTicketsDatesSelector() {
return ( return (
<DatePicker.RangePicker <DatePicker.RangePicker
defaultValue={[ defaultValue={[
start ? dayjs(start) : dayjs().startOf("week").subtract(7, "days"), start ? dayjs(start) : dayjs().startOf("week").subtract(7, "day"),
end ? dayjs(end) : dayjs().endOf("week"), end ? dayjs(end) : dayjs().endOf("week"),
]} ]}
format="MM/DD/YYYY" format="MM/DD/YYYY"

View File

@@ -23,7 +23,7 @@ export default function TimeTicketsAttendanceTable() {
variables: { variables: {
start: start start: start
? start ? start
: dayjs().startOf("week").subtract(7, "days").format("YYYY-MM-DD"), : dayjs().startOf("week").subtract(7, "day").format("YYYY-MM-DD"),
end: end ? end : dayjs().endOf("week").format("YYYY-MM-DD"), end: end ? end : dayjs().endOf("week").format("YYYY-MM-DD"),
}, },
}, },

View File

@@ -22,7 +22,7 @@ export default function TimeTicketsPayrollTable() {
variables: { variables: {
start: start start: start
? start ? start
: dayjs().startOf("week").subtract(7, "days").format("YYYY-MM-DD"), : dayjs().startOf("week").subtract(7, "day").format("YYYY-MM-DD"),
end: end ? end : dayjs().endOf("week").format("YYYY-MM-DD"), end: end ? end : dayjs().endOf("week").format("YYYY-MM-DD"),
}, },
}, },

View File

@@ -18,20 +18,36 @@ import {
} from "../../redux/application/application.actions"; } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
/**
* Mapping state to props
*/
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
}); });
/**
* Mapping dispatch to props
*/
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)), setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
}); });
/**
* ScoreboardContainer component
* @param {Object} props - The props that were defined by the caller of this component.
* @param {Function} props.setBreadcrumbs - Function to set breadcrumbs.
* @param {Function} props.setSelectedHeader - Function to set selected header.
*/
export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) { export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation(); const { t } = useTranslation();
const searchParams = queryString.parse(useLocation().search); const searchParams = queryString.parse(useLocation().search);
const { tab } = searchParams; const { tab } = searchParams;
const history = useNavigate(); const history = useNavigate();
/**
* useEffect hook to set document title, selected header and breadcrumbs
*/
useEffect(() => { useEffect(() => {
document.title = t("titles.scoreboard"); document.title = t("titles.scoreboard");
setSelectedHeader("scoreboard"); setSelectedHeader("scoreboard");
@@ -43,6 +59,9 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) {
]); ]);
}, [t, setBreadcrumbs, setSelectedHeader]); }, [t, setBreadcrumbs, setSelectedHeader]);
/**
* Render the component
*/
return ( return (
<FeatureWrapper featureName="scoreboard"> <FeatureWrapper featureName="scoreboard">
<RbacWrapper action="scoreboard:view"> <RbacWrapper action="scoreboard:view">
@@ -97,7 +116,11 @@ export function ScoreboardContainer({ setBreadcrumbs, setSelectedHeader }) {
</FeatureWrapper> </FeatureWrapper>
); );
} }
/**
* Connecting the component to Redux store
*/
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(ScoreboardContainer); )(ScoreboardContainer);

View File

@@ -37,7 +37,7 @@ export function TimeTicketsContainer({
const startDate = start const startDate = start
? dayjs(start) ? dayjs(start)
: dayjs().startOf("week").subtract(7, "days"); : dayjs().startOf("week").subtract(7, "day");
const endDate = end ? dayjs(end) : dayjs().endOf("week"); const endDate = end ? dayjs(end) : dayjs().endOf("week");
const { loading, error, data } = useQuery(QUERY_TIME_TICKETS_IN_RANGE, { const { loading, error, data } = useQuery(QUERY_TIME_TICKETS_IN_RANGE, {

View File

@@ -209,9 +209,9 @@ export function* calculateScheduleLoad({ payload: end }) {
const range = Math.round(dayjs.duration(end.diff(today)).asDays()) + 1; const range = Math.round(dayjs.duration(end.diff(today)).asDays()) + 1;
for (var day = 0; day < range; day++) { for (var day = 0; day < range; day++) {
const current = dayjs(today).add(day, "days").format("yyyy-MM-DD"); const current = dayjs(today).add(day, "day").format("yyyy-MM-DD");
const prev = dayjs(today) const prev = dayjs(today)
.add(day - 1, "days") .add(day - 1, "day")
.format("yyyy-MM-DD"); .format("yyyy-MM-DD");
if (!!!load[current]) { if (!!!load[current]) {
load[current] = {}; load[current] = {};

View File

@@ -1,10 +1,10 @@
import dayjs from "./day"; import dayjs from "./day";
const range = { const range = {
Today: [dayjs(), dayjs()], Today: [dayjs(), dayjs()],
"Last 14 days": [dayjs().subtract(14, "days"), dayjs()], "Last 14 days": [dayjs().subtract(14, "day"), dayjs()],
"Last 7 days": [dayjs().subtract(7, "days"), dayjs()], "Last 7 days": [dayjs().subtract(7, "day"), dayjs()],
"Next 7 days": [dayjs(), dayjs().add(7, "days")], "Next 7 days": [dayjs(), dayjs().add(7, "day")],
"Next 14 days": [dayjs(), dayjs().add(14, "days")], "Next 14 days": [dayjs(), dayjs().add(14, "day")],
"Last Month": [ "Last Month": [
dayjs().startOf("month").subtract(1, "month"), dayjs().startOf("month").subtract(1, "month"),
dayjs().startOf("month").subtract(1, "month").endOf("month"), dayjs().startOf("month").subtract(1, "month").endOf("month"),
@@ -15,13 +15,13 @@ const range = {
dayjs().startOf("month").add(1, "month").endOf("month"), dayjs().startOf("month").add(1, "month").endOf("month"),
], ],
"Last Quarter": [ "Last Quarter": [
dayjs().startOf("quarter").subtract(1, "quarters"), dayjs().startOf("quarter").subtract(1, "quarter"),
dayjs().startOf("quarter").subtract(1, "day"), dayjs().startOf("quarter").subtract(1, "day"),
], ],
"This Quarter": [ "This Quarter": [
dayjs().startOf("quarter"), dayjs().startOf("quarter"),
dayjs().startOf("quarter").add(1, "quarter").subtract(1, "day"), dayjs().startOf("quarter").add(1, "quarter").subtract(1, "day"),
], ],
"Last 90 Days": [dayjs().add(-90, "days"), dayjs()], "Last 90 Days": [dayjs().add(-90, "day"), dayjs()],
}; };
export default range; export default range;

View File

@@ -1,4 +1,5 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import dayjsBusinessDays from "dayjs-business-days2"; import dayjsBusinessDays from "dayjs-business-days2";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter"; import isSameOrAfter from "dayjs/plugin/isSameOrAfter";
import updateLocale from 'dayjs/plugin/updateLocale'; import updateLocale from 'dayjs/plugin/updateLocale';
@@ -30,6 +31,7 @@ import isYesterday from 'dayjs/plugin/isYesterday';
import objectSupport from 'dayjs/plugin/objectSupport'; import objectSupport from 'dayjs/plugin/objectSupport';
import toArray from 'dayjs/plugin/toArray'; import toArray from 'dayjs/plugin/toArray';
import toObject from 'dayjs/plugin/toObject'; import toObject from 'dayjs/plugin/toObject';
// import badMutable from 'dayjs/plugin/badMutable'; // import badMutable from 'dayjs/plugin/badMutable';
// import preParsePostFormat from 'dayjs/plugin/preParsePostFormat'; // import preParsePostFormat from 'dayjs/plugin/preParsePostFormat';
@@ -42,7 +44,6 @@ dayjs.extend(isYesterday);
dayjs.extend(isTomorrow); dayjs.extend(isTomorrow);
dayjs.extend(isToday); dayjs.extend(isToday);
dayjs.extend(localeData); dayjs.extend(localeData);
dayjs.extend(relativeTime);
dayjs.extend(quarterOfYear); dayjs.extend(quarterOfYear);
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
// dayjs.extend(preParsePostFormat); // TODO: This should not be needed // dayjs.extend(preParsePostFormat); // TODO: This should not be needed
@@ -57,6 +58,7 @@ dayjs.extend(calendar);
dayjs.extend(arraySupport); dayjs.extend(arraySupport);
dayjs.extend(advancedFormat); dayjs.extend(advancedFormat);
dayjs.extend(duration); dayjs.extend(duration);
dayjs.extend(relativeTime);
dayjs.extend(pluralGetSet); dayjs.extend(pluralGetSet);
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
dayjs.extend(utc); dayjs.extend(utc);