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 { useApolloClient } from "@apollo/client";
import { useTreatments } from "@splitsoftware/splitio-react";
import {
Alert,
Divider,
Form,
Input,
Select,
Space,
Statistic,
Switch,
Upload,
} from "antd";
import Icon, {UploadOutlined} from "@ant-design/icons";
import {useApolloClient} from "@apollo/client";
import {useTreatments} from "@splitsoftware/splitio-react";
import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd";
import dayjs from "../../utils/day";
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 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 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";
@@ -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 VendorSearchSelect from "../vendor-search-select/vendor-search-select.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({
bodyshop: selectBodyshop,
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 { Extended_Bill_Posting } = useTreatments(
["Extended_Bill_Posting"],
{},
bodyshop.imexshopid
);
const { ClosingPeriod } = useTreatments(
["ClosingPeriod"],
{},
bodyshop.imexshopid
);
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 {Extended_Bill_Posting} = useTreatments(
["Extended_Bill_Posting"],
{},
bodyshop.imexshopid
);
const {ClosingPeriod} = useTreatments(
["ClosingPeriod"],
{},
bodyshop.imexshopid
);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
const handleVendorSelect = (props, opt) => {
setDiscount(opt.discount);
opt &&
!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) {
opt &&
!billEdit &&
loadOutstandingReturns({
variables: {
jobId: jobId,
vendorId: vendorId,
},
variables: {
jobId: form.getFieldValue("jobid"),
vendorId: opt.value,
},
});
}
}
};
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
useEffect(() => {
if (job) form.validateFields(["is_credit_memo"]);
}, [job, form]);
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) {
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>
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);
}
/>
))}
<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,
}
const jobId = form.getFieldValue("jobid");
if (jobId) {
loadLines({variables: {id: jobId}});
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
loadOutstandingReturns({
variables: {
invoice_number: value,
vendorid: vendorid,
jobId: jobId,
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 (vendorId === bodyshop.inhousevendorid && !billEdit) {
loadInventory();
}
}, [
form,
billEdit,
loadOutstandingReturns,
loadInventory,
setDiscount,
vendorAutoCompleteOptions,
loadLines,
bodyshop.inhousevendorid,
]);
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 (
<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) {
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();
},
}),
]}
>
<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>
<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>
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"),
// },
// });
}
{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}
/>
)}
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"));
}
<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>
);
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>
<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);

View File

@@ -14,10 +14,10 @@ export function getRange(dateParam, viewParam) {
start = dayjs(date).startOf("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") {
start = dayjs(date).startOf("month").subtract(7, "days");
end = dayjs(date).endOf("month").add(7, "days");
start = dayjs(date).startOf("month").subtract(7, "day");
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');
else if (view === "agenda") {

View File

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

View File

@@ -27,7 +27,7 @@ export function ScheduleVerifyIntegrity({ currentUser }) {
data: { arrJobs, compJobs, prodJobs },
} = await client.query({
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,8 @@ export function TechLogin({
const navigate = useNavigate();
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(() => {
@@ -49,7 +50,8 @@ export function TechLogin({
>
<Form.Item
label={t("tech.fields.employeeid")}
name="employeeid"
name="techEmployeeId"
rules={[
{
required: true,

View File

@@ -37,7 +37,7 @@ export default function TimeTicketsDatesSelector() {
return (
<DatePicker.RangePicker
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"),
]}
format="MM/DD/YYYY"

View File

@@ -23,7 +23,7 @@ export default function TimeTicketsAttendanceTable() {
variables: {
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"),
},
},

View File

@@ -22,7 +22,7 @@ export default function TimeTicketsPayrollTable() {
variables: {
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"),
},
},

View File

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

View File

@@ -37,7 +37,7 @@ export function TimeTicketsContainer({
const startDate = start
? dayjs(start)
: dayjs().startOf("week").subtract(7, "days");
: dayjs().startOf("week").subtract(7, "day");
const endDate = end ? dayjs(end) : dayjs().endOf("week");
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;
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)
.add(day - 1, "days")
.add(day - 1, "day")
.format("yyyy-MM-DD");
if (!!!load[current]) {
load[current] = {};

View File

@@ -1,10 +1,10 @@
import dayjs from "./day";
const range = {
Today: [dayjs(), dayjs()],
"Last 14 days": [dayjs().subtract(14, "days"), dayjs()],
"Last 7 days": [dayjs().subtract(7, "days"), dayjs()],
"Next 7 days": [dayjs(), dayjs().add(7, "days")],
"Next 14 days": [dayjs(), dayjs().add(14, "days")],
"Last 14 days": [dayjs().subtract(14, "day"), dayjs()],
"Last 7 days": [dayjs().subtract(7, "day"), dayjs()],
"Next 7 days": [dayjs(), dayjs().add(7, "day")],
"Next 14 days": [dayjs(), dayjs().add(14, "day")],
"Last Month": [
dayjs().startOf("month").subtract(1, "month"),
dayjs().startOf("month").subtract(1, "month").endOf("month"),
@@ -15,13 +15,13 @@ const range = {
dayjs().startOf("month").add(1, "month").endOf("month"),
],
"Last Quarter": [
dayjs().startOf("quarter").subtract(1, "quarters"),
dayjs().startOf("quarter").subtract(1, "quarter"),
dayjs().startOf("quarter").subtract(1, "day"),
],
"This Quarter": [
dayjs().startOf("quarter"),
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;

View File

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