Merge branch 'master-beta' into master-AIO

This commit is contained in:
Patrick Fic
2024-03-13 11:38:19 -04:00
70 changed files with 67646 additions and 4411 deletions

View File

@@ -1,21 +1,20 @@
import {Card, Checkbox, Input, Space, Table} from "antd";
import {Card, Checkbox, Input, Space, Table} from "antd";import queryString from "query-string";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {connect } from "react-redux";
import {Link} from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {alphaSort, dateSort} from "../../utils/sorters";
import PayableExportButton from "../payable-export-button/payable-export-button.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import {DateFormatter} from "../../utils/DateFormatter";
import queryString from "query-string";
import { createStructuredSelector } from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {DateFormatter} from "../../utils/DateFormatter";
import { pageLimit } from "../../utils/config";
import {alphaSort, dateSort} from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
import PayableExportButton from "../payable-export-button/payable-export-button.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
import {pageLimit} from "../../utils/config";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -147,7 +146,7 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<PayableExportButton

View File

@@ -8,14 +8,16 @@ import {logImEXEvent} from "../../firebase/firebase.utils";
import {selectBodyshop} from "../../redux/user/user.selectors";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {DateFormatter, DateTimeFormatter} from "../../utils/DateFormatter";
import {pageLimit } from "../../utils/config";
import {alphaSort, dateSort} from "../../utils/sorters";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {pageLimit} from "../../utils/config";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -75,7 +77,11 @@ export function AccountingPayablesTableComponent({
dataIndex: "owner",
key: "owner",
ellipsis: true,
sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
sorter: (a, b) =>
alphaSort(
OwnerNameDisplayFunction(a.job),
OwnerNameDisplayFunction(b.job)
),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
@@ -94,7 +100,9 @@ export function AccountingPayablesTableComponent({
title: t("payments.fields.amount"),
dataIndex: "amount",
key: "amount",
render: (text, record) => (
sorter: (a, b) => a.amount - b.amount,
sortOrder:
state.sortedInfo.columnKey === "amount" && state.sortedInfo.order,render: (text, record) => (
<CurrencyFormatter>{record.amount}</CurrencyFormatter>
),
},
@@ -111,19 +119,21 @@ export function AccountingPayablesTableComponent({
{
title: t("payments.fields.created_at"),
dataIndex: "created_at",
key: "created_at",
key: "created_at",sorter: (a, b) => dateSort(a.created_at, b.created_at),
sortOrder:
state.sortedInfo.columnKey === "created_at" && state.sortedInfo.order,
render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
),
},
{
title: t("payments.fields.exportedat"),
dataIndex: "exportedat",
key: "exportedat",
render: (text, record) => (
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
),
},
//{
// title: t("payments.fields.exportedat"),
// dataIndex: "exportedat",
// key: "exportedat",
// render: (text, record) => (
// <DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
// ),
//},
{
title: t("exportlogs.labels.attempts"),
dataIndex: "attempts",
@@ -137,7 +147,7 @@ export function AccountingPayablesTableComponent({
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
sorter: (a, b) => a.clm_total - b.clm_total,
render: (text, record) => (
<PaymentExportButton

View File

@@ -4,17 +4,19 @@ import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import {logImEXEvent} from "../../firebase/firebase.utils";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import {alphaSort, dateSort} from "../../utils/sorters";
import {alphaSort, dateSort, statusSort } from "../../utils/sorters";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectBodyshop} from "../../redux/user/user.selectors";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import {DateFormatter} from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -63,7 +65,7 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.status"),
dataIndex: "status",
key: "status",
sorter: (a, b) => a.status - b.status,
sorter: (a, b) => statusSort(a, b, bodyshop.md_ro_statuses.statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
},
@@ -83,7 +85,8 @@ export function AccountingReceivablesTableComponent({
title: t("jobs.fields.owner"),
dataIndex: "owner",
key: "owner",
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
@@ -103,7 +106,15 @@ export function AccountingReceivablesTableComponent({
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,render: (text, record) => {
return record.vehicleid ? (
<Link to={"/manage/vehicles/" + record.vehicleid}>
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${

View File

@@ -5,10 +5,22 @@ import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {DELETE_BILL} from "../../graphql/bills.queries";
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
import {insertAuditTrail} from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
export default function BillDeleteButton({bill, callback}) {
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(BillDeleteButton);
export function BillDeleteButton({ bill, jobid, callback, insertAuditTrail }) {
const [loading, setLoading] = useState(false);
const {t} = useTranslation();
const { t } = useTranslation();
const [deleteBill] = useMutation(DELETE_BILL);
const handleDelete = async () => {
@@ -35,7 +47,12 @@ export default function BillDeleteButton({bill, callback}) {
});
if (!!!result.errors) {
notification["success"]({message: t("bills.successes.deleted")});
notification["success"]({ message: t("bills.successes.deleted") });
insertAuditTrail({
jobid: jobid,
operation: AuditTrailMapping.billdeleted(bill.invoice_number),
type: "billdeleted",
});
if (callback && typeof callback === "function") callback(bill.id);
} else {

View File

@@ -30,8 +30,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
@@ -145,7 +145,8 @@ export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail,
jobid: bill.jobid,
billid: search.billid,
operation: AuditTrailMapping.billupdated(bill.invoice_number),
});
type: "billupdated",
});
await refetch();
form.setFieldsValue(transformData(data));

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
dispatch(setModalContext({context: context, modal: "partsOrder"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(

View File

@@ -32,8 +32,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
insertAuditTrail: ({jobid, billid, operation}) =>
dispatch(insertAuditTrail({jobid, billid, operation})),
insertAuditTrail: ({jobid, billid, operation, type}) =>
dispatch(insertAuditTrail({jobid, billid, operation, type })),
});
const Templates = TemplateList("job_special");
@@ -212,7 +212,7 @@ function BillEnterModalContainer({
mod_lbr_ty: key,
hours: adjustmentsToInsert[key].toFixed(1),
}),
});
type: "jobmodifylbradj",});
});
const jobUpdate = client.mutate({
@@ -369,7 +369,8 @@ function BillEnterModalContainer({
operation: AuditTrailMapping.billposted(
r1.data.insert_bills.returning[0].invoice_number
),
});
type: "billposted",
});
if (enterAgain) {
// form.resetFields();

View File

@@ -50,17 +50,17 @@ export function BillsListTableComponent({
const Templates = TemplateList("bill");
const bills = billsQuery.data ? billsQuery.data.bills : [];
const {refetch} = billsQuery;
const { refetch } = billsQuery;
const recordActions = (record, showView = false) => (
<Space wrap>
{showView && (
<Button onClick={() => handleOnRowClick(record)}>
<EditFilled/>
<EditFilled />
</Button>
)}
<BillDeleteButton bill={record}/>
<BillDeleteButton bill={record} jobid={job.id} />
<BillDetailEditReturnComponent
data={{bills_by_pk: {...record, jobid: job.id}}}
data={{ bills_by_pk: { ...record, jobid: job.id } }}
disabled={
record.is_credit_memo ||
record.vendorid === bodyshop.inhousevendorid ||

View File

@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type})),
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
});
@@ -88,7 +88,7 @@ const CardPaymentModalComponent = ({
insertAuditTrail({
jobid: payment.jobid,
operation: AuditTrailMapping.failedpayment(),
})
type: "failedpayment",})
);
});
};

View File

@@ -1,22 +1,34 @@
import {SyncOutlined, WarningFilled} from "@ant-design/icons";
import {Button, Card, Dropdown, Input, Space, Table, Tooltip,} from "antd";
import { SyncOutlined, WarningFilled } from "@ant-design/icons";
import {
Button,
Card,
Dropdown,
Input,
Space,
Table,
Tooltip,
} from "antd";
import dayjs from "../../utils/day";
import React, {useState} from "react";
import {useTranslation} from "react-i18next";
import {Link} from "react-router-dom";
import {DateTimeFormatter} from "../../utils/DateFormatter";
import {GenerateDocument} from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants";
import {alphaSort} from "../../utils/sorters";
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
export default function CourtesyCarsList({loading, courtesycars, refetch}) {
export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: {text: ""},
});
const [searchText, setSearchText] = useState("");
const {t} = useTranslation();
const [filter, setFilter] = useLocalStorage(
"filter_courtesy_cars_list",
null
);
const { t } = useTranslation();
const columns = [
{
@@ -42,6 +54,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
dataIndex: "status",
key: "status",
sorter: (a, b) => alphaSort(a.status, b.status),
filteredValue: filter?.status || null,
filters: [
{
text: t("courtesycars.status.in"),
@@ -64,7 +77,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
render: (text, record) => {
const {nextservicedate, nextservicekm, mileage, insuranceexpires} =
const { nextservicedate, nextservicekm, mileage, insuranceexpires } =
record;
const mileageOver = nextservicekm ? nextservicekm <= mileage : false;
@@ -75,19 +88,23 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
const insuranceOver =
insuranceexpires &&
dayjs(insuranceexpires).endOf("day").isBefore(dayjs());
return (
<Space>
{t(record.status)}
{(mileageOver || dueForService || insuranceOver) && (
<Tooltip title={(mileageOver || dueForService) && insuranceOver
? t("contracts.labels.insuranceexpired") +
" / " +
t("contracts.labels.cardueforservice")
: insuranceOver
? t("contracts.labels.insuranceexpired")
: t("contracts.labels.cardueforservice")
}>
<WarningFilled style={{color: "tomato"}}/>
<Tooltip
title={
(mileageOver || dueForService) && insuranceOver
? t("contracts.labels.insuranceexpired") +
" / " +
t("contracts.labels.cardueforservice")
: insuranceOver
? t("contracts.labels.insuranceexpired")
: t("contracts.labels.cardueforservice")
}
>
<WarningFilled style={{ color: "tomato" }} />
</Tooltip>
)}
</Space>
@@ -99,6 +116,7 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
dataIndex: "readiness",
key: "readiness",
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
filteredValue: filter?.readiness || null,
filters: [
{
text: t("courtesycars.readiness.ready"),
@@ -214,7 +232,8 @@ export default function CourtesyCarsList({loading, courtesycars, refetch}) {
];
const handleTableChange = (pagination, filters, sorter) => {
setState({...state, filteredInfo: filters, sortedInfo: sorter});
setState({ ...state, sortedInfo: sorter });
setFilter(filters);
};
const tableData = searchText

View File

@@ -4,7 +4,7 @@ import {
PauseCircleOutlined,
} from "@ant-design/icons";
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
import moment from "moment";
import dayjs from "../../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@@ -74,7 +74,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
}
});
appt.sort(function (a, b) {
return new moment(a.start) - new moment(b.start);
return dayjs(a.start) - dayjs(b.start);
});
const tvFontSize = 16;
@@ -419,7 +419,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
return (
<Card
title={t("dashboard.titles.scheduledindate", {
date: moment().startOf("day").format("MM/DD/YYYY"),
date: dayjs().startOf("day").format("MM/DD/YYYY"),
})}
extra={
<Space>
@@ -449,9 +449,9 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
}
export const DashboardScheduledInTodayGql = `
scheduled_in_today: appointments(where: {start: {_gte: "${moment()
scheduled_in_today: appointments(where: {start: {_gte: "${dayjs()
.startOf("day")
.toISOString()}", _lte: "${moment()
.toISOString()}", _lte: "${dayjs()
.endOf("day")
.toISOString()}"}, canceled: {_eq: false}, block: {_neq: true}}) {
canceled

View File

@@ -53,7 +53,7 @@ export default function ScheduleEventContainer({bodyshop, event, refetch}) {
insertAuditTrail({
jobid: event.job.id,
operation: AuditTrailMapping.appointmentcancel(lost_sale_reason),
})
type: "appointmentcancel",})
);
}
if (!!jobUpdate.errors) {

View File

@@ -22,8 +22,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobChecklistForm({
@@ -177,7 +177,7 @@ export function JobChecklistForm({
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
),
});
type: "jobchecklist",});
} else {
notification["error"]({
message: t("checklist.errors.complete", {

View File

@@ -15,8 +15,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -47,7 +47,8 @@ export function JobEmployeeAssignmentsContainer({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentchange(operation, name),
});
type: "jobassignmentchange",
});
if (!!result.errors) {
notification["error"]({
@@ -77,7 +78,7 @@ export function JobEmployeeAssignmentsContainer({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobassignmentremoved(operation),
});
type: "jobassignmentremoved",});
setLoading(false);
};

View File

@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -102,7 +102,7 @@ export function JobLineConvertToLabor({
hours: calculateAdjustment({mod_lbr_ty, job, jobline}).toFixed(1),
mod_lbr_ty,
}),
});
type: "jobmodifylbradj",});
setLoading(false);
setVisibility(false);
};

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
@@ -32,7 +32,7 @@ export function JobsAdminStatus({insertAuditTrail, bodyshop, job}) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobstatuschange(status),
});
type: "admin_jobstatuschange",});
// refetch();
})
.catch((error) => {

View File

@@ -20,8 +20,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
@@ -57,7 +57,7 @@ export function JobsAdminDatesChange({insertAuditTrail, job}) {
? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key]
),
});
type: "admin_jobfieldchange",});
});
if (!!!result.errors) {

View File

@@ -23,8 +23,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -59,7 +59,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkforreexport(),
});
type: "admin_jobmarkforreexport",});
} else {
notification["error"]({
message: t("jobs.errors.saving", {
@@ -99,7 +99,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobmarkexported(),
});
type: "admin_jobmarkexported",});
} else {
notification["error"]({
message: t("jobs.errors.saving", {
@@ -124,7 +124,7 @@ export function JobAdminMarkReexport({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobuninvoice(),
});
type: "admin_jobuninvoice",});
} else {
notification["error"]({
message: t("jobs.errors.saving", {

View File

@@ -10,8 +10,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminRemoveAR);
@@ -34,7 +34,7 @@ export function JobsAdminRemoveAR({insertAuditTrail, job}) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_job_remove_from_ar(value),
});
type: "admin_job_remove_from_ar",});
setSwitchValue(value);
} else {
notification["error"]({

View File

@@ -14,8 +14,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminUnvoid);
@@ -46,6 +46,7 @@ export function JobsAdminUnvoid({
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.admin_jobunvoid(),
type: "admin_jobunvoid",
});
} else {
notification["error"]({

View File

@@ -39,8 +39,8 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,}) {
@@ -194,10 +194,11 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
});
//Job has been inserted. Clean up the available jobs record.
insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(),
});
insertAuditTrail({
jobid: r.data.insert_jobs.returning[0].id,
operation: AuditTrailMapping.jobimported(),
type: "jobimported",
});
await deleteJob({
variables: {id: estData.id},
@@ -324,24 +325,25 @@ export function JobsAvailableContainer({bodyshop, currentUser, insertAuditTrail,
setInsertLoading(false);
});
await insertNote({
variables: {
noteInput: [
{
jobid: selectedJob,
created_by: currentUser.email,
audit: true,
text: t("jobs.labels.supplementnote"),
},
],
},
});
insertAuditTrail({
jobid: selectedJob,
operation: AuditTrailMapping.jobsupplement(),
});
}
};
await insertNote({
variables: {
noteInput: [
{
jobid: selectedJob,
created_by: currentUser.email,
audit: true,
text: t("jobs.labels.supplementnote"),
},
],
},
});
insertAuditTrail({
jobid: selectedJob,
operation: AuditTrailMapping.jobsupplement(),
type: "jobsupplement",
});
}
};
const owner =
estDataRaw.data &&

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobsChangeStatus({job, bodyshop, jobRO, insertAuditTrail}) {
@@ -35,7 +35,7 @@ export function JobsChangeStatus({job, bodyshop, jobRO, insertAuditTrail}) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobstatuschange(status),
});
type: "jobstatuschange",});
// refetch();
})
.catch((error) => {

View File

@@ -9,7 +9,9 @@ import {createStructuredSelector} from "reselect";
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
import {UPDATE_JOB} from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
@@ -17,6 +19,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
@@ -37,9 +44,10 @@ export function JobsCloseExportButton({
disabled,
setSelectedJobs,
refetch,
insertAuditTrail,
}) {
const history = useNavigate();
const {t} = useTranslation();
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOB);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
const [loading, setLoading] = useState(false);
@@ -178,6 +186,10 @@ export function JobsCloseExportButton({
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map((job) => job.id)
);
@@ -189,12 +201,20 @@ export function JobsCloseExportButton({
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobexported(),type: "jobexported",
});
updateJobCache([
...new Set(
successfulTransactions.map(
@@ -224,4 +244,7 @@ export function JobsCloseExportButton({
);
}
export default connect(mapStateToProps, null)(JobsCloseExportButton);
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsCloseExportButton);

View File

@@ -17,8 +17,8 @@ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobsConvertButton({
@@ -70,7 +70,8 @@ export function JobsConvertButton({
operation: AuditTrailMapping.jobconverted(
res.data.update_jobs.returning[0].ro_number
),
});
type: "jobconverted",
});
setOpen(false);
}

View File

@@ -29,6 +29,7 @@ export default function AddToProduction(
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobinproductionchange(!remove),
type: "jobinproductionchange",
})
);
if (completionCallback) completionCallback();

View File

@@ -50,8 +50,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({context: context, modal: "timeTicket"})),
setCardPaymentContext: (context) =>
dispatch(setModalContext({context: context, modal: "cardPayment"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
setTimeTicketTaskContext: (context) =>
dispatch(setModalContext({context: context, modal: "timeTicketTask"})),
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
@@ -242,6 +242,11 @@ export function JobsDetailHeaderActions({
message: JSON.stringify(result.errors),
}),
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobvoid(),
type: "jobvoid",
});
return;
}
if (e.key === "email")
@@ -356,6 +361,11 @@ export function JobsDetailHeaderActions({
notification["success"]({
message: t("jobs.successes.voided"),
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobvoid(),
type: "jobvoid",
});
//go back to jobs list.
history(`/manage/`);
} else {
@@ -472,9 +482,29 @@ export function JobsDetailHeaderActions({
? !job.production_vars.alert
: true
),
type: "alertToggle",
});
};
const handleSuspend = (e) => {
logImEXEvent("production_toggle_alert");
//e.stopPropagation();
updateJob({
variables: {
jobId: job.id,
job: {
suspended: !job.suspended,
},
},
});
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobsuspend(
!!job.suspended ? !job.suspended : true
),
type: "jobsuspend",
});
};
// Function to handle OK
const handleCancelScheduleOK = async () => {
@@ -504,24 +534,12 @@ export function JobsDetailHeaderActions({
jobid: job.id,
operation:
AuditTrailMapping.appointmentcancel(lost_sale_reason),
type: "appointmentcancel",
});
}
};
const handleSuspend = (e) => {
logImEXEvent("production_toggle_alert");
//e.stopPropagation();
updateJob({
variables: {
jobId: job.id,
job: {
suspended: !job.suspended,
},
},
});
};
const popOverContent = (
<Card>
<div>

View File

@@ -9,7 +9,12 @@ import {createStructuredSelector} from "reselect";
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
import {INSERT_EXPORT_LOG} from "../../graphql/accounting.queries";
import {UPDATE_JOBS} from "../../graphql/jobs.queries";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import { insertAuditTrail } from "../../redux/application/application.actions";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import client from "../../utils/GraphQLClient";
const mapStateToProps = createStructuredSelector({
@@ -17,6 +22,11 @@ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
function updateJobCache(items) {
client.cache.modify({
id: "ROOT_QUERY",
@@ -38,8 +48,9 @@ export function JobsExportAllButton({
loadingCallback,
completedCallback,
refetch,
insertAuditTrail,
}) {
const {t} = useTranslation();
const { t } = useTranslation();
const [updateJob] = useMutation(UPDATE_JOBS);
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
@@ -168,47 +179,66 @@ export function JobsExportAllButton({
},
});
if (!!!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id
)
);
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
}
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && successfulTransactions.length > 0) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
updateJobCache([
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "jobid"
: "id"
]
)
),
]);
}
}
})
);
if (!!!jobUpdateResponse.errors) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
jobUpdateResponse.data.update_jobs.returning.forEach((job) => {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
});
updateJobCache(
jobUpdateResponse.data.update_jobs.returning.map(
(job) => job.id
)
);
} else {
notification["error"]({
message: t("jobs.errors.exporting", {
error: JSON.stringify(jobUpdateResponse.error),
}),
});
}
}
if (
bodyshop.accountingconfig &&
bodyshop.accountingconfig.qbo &&
successfulTransactions.length > 0
) {
notification.open({
type: "success",
key: "jobsuccessexport",
message: t("jobs.successes.exported"),
});
const successfulTransactionsSet = [
...new Set(
successfulTransactions.map(
(st) =>
st[
bodyshop.accountingconfig && bodyshop.accountingconfig.qbo
? "jobid"
: "id"
]
)
),
];
if (successfulTransactionsSet.length > 0) {
insertAuditTrail({
jobid: successfulTransactionsSet[0],
operation: AuditTrailMapping.jobexported(),
type: "jobexported",
});
}
updateJobCache(successfulTransactionsSet);
}
}
})
);
if (!!completedCallback) completedCallback([]);
if (!!loadingCallback) loadingCallback(false);
@@ -222,4 +252,7 @@ export function JobsExportAllButton({
);
}
export default connect(mapStateToProps, null)(JobsExportAllButton);
export default connect(
mapStateToProps,
mapDispatchToProps
)(JobsExportAllButton);

View File

@@ -17,6 +17,7 @@ import AlertComponent from "../alert/alert.component";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import { setJoyRideSteps } from "../../redux/application/application.actions";
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -148,7 +149,8 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
ellipsis: true,
responsive: ["md"],
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) =>
alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
render: (text, record) => {
@@ -193,7 +195,8 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
key: "status",
ellipsis: true,
sorter: (a, b) => alphaSort(a.status, b.status),
sorter: (a, b) =>
statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses),
sortOrder:
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
filteredValue: filter?.status || null,
@@ -224,7 +227,15 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
dataIndex: "vehicle",
key: "vehicle",
ellipsis: true,
render: (text, record) => {
sorter: (a, b) =>
alphaSort(
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,render: (text, record) => {
return record.vehicleid ? (
<Link
to={"/manage/vehicles/" + record.vehicleid}
@@ -271,7 +282,9 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
dataIndex: "ins_co_nm",
key: "ins_co_nm",
ellipsis: true,
filteredValue: filter?.ins_co_nm || null,
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
sortOrder:
state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,filteredValue: filter?.ins_co_nm || null,
filters:
(jobs &&
jobs
@@ -307,7 +320,13 @@ export function JobsList({bodyshop,setJoyRideSteps}) {
dataIndex: "jobs.labels.estimator",
key: "jobs.labels.estimator",
ellipsis: true,
responsive: ["xl"],
responsive: ["xl"],sorter: (a, b) =>
alphaSort(
`${a.est_ct_fn || ""} ${a.est_ct_ln || ""}`.trim(),
`${b.est_ct_fn || ""} ${b.est_ct_ln || ""}`.trim()
),
sortOrder:
state.sortedInfo.columnKey === "estimator" && state.sortedInfo.order,
filterSearch: true,
filteredValue: filter?.estimator || null,
filters:

View File

@@ -16,8 +16,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(JobNotesContainer);
@@ -46,7 +46,7 @@ export function JobNotesContainer({jobId, insertAuditTrail}) {
insertAuditTrail({
jobid: jobId,
operation: AuditTrailMapping.jobnotedeleted(),
});
type: "jobnotedeleted",});
});
setDeleteLoading(false);
};

View File

@@ -13,8 +13,8 @@ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export default connect(
mapStateToProps,
@@ -69,7 +69,7 @@ export function LaborAllocationsAdjustmentEdit({
values.hours -
((adjustments && adjustments[mod_lbr_ty]) || 0).toFixed(1),
}),
});
type: "jobmodifylbradj",});
}
setLoading(false);
setOpen(false);

View File

@@ -19,8 +19,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function NoteUpsertModalContainer({
@@ -70,7 +70,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteupdated(),
});
type: "jobnoteupdated",});
});
if (refetch) refetch();
toggleModalVisible();
@@ -102,7 +102,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({
jobid: newJobId,
operation: AuditTrailMapping.jobnoteadded(),
});
type: "jobnoteadded",});
});
}
@@ -115,7 +115,7 @@ export function NoteUpsertModalContainer({
insertAuditTrail({
jobid: context.jobId,
operation: AuditTrailMapping.jobnoteadded(),
});
type: "jobnoteadded",});
}
};

View File

@@ -1,19 +1,28 @@
import {useApolloClient, useMutation, useQuery} from "@apollo/client";
import {Form, Modal, notification} from "antd";
import dayjs from "../../utils/day";
import React, {useEffect, useState} from "react";
import {useTranslation} from "react-i18next";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {auth, logImEXEvent} from "../../firebase/firebase.utils";
import {UPDATE_JOB_LINE_STATUS} from "../../graphql/jobs-lines.queries";
import {INSERT_NEW_PARTS_ORDERS, QUERY_PARTS_ORDER_OEC,} from "../../graphql/parts-orders.queries";
import {QUERY_ALL_VENDORS_FOR_ORDER} from "../../graphql/vendors.queries";
import {insertAuditTrail} from "../../redux/application/application.actions";
import {setEmailOptions} from "../../redux/email/email.actions";
import {setModalContext, toggleModalVisible,} from "../../redux/modals/modals.actions";
import {selectPartsOrder} from "../../redux/modals/modals.selectors";
import {selectBodyshop, selectCurrentUser,} from "../../redux/user/user.selectors";
import { useMutation, useQuery, useApolloClient } from "@apollo/client";
import { Form, Modal, notification } from "antd";
import dayjs from '../../utils/day';
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { logImEXEvent, auth } from "../../firebase/firebase.utils";
import { UPDATE_JOB_LINE_STATUS } from "../../graphql/jobs-lines.queries";
import {
INSERT_NEW_PARTS_ORDERS,
QUERY_PARTS_ORDER_OEC,
} from "../../graphql/parts-orders.queries";
import { QUERY_ALL_VENDORS_FOR_ORDER } from "../../graphql/vendors.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { setEmailOptions } from "../../redux/email/email.actions";
import {
setModalContext,
toggleModalVisible,
} from "../../redux/modals/modals.actions";
import { selectPartsOrder } from "../../redux/modals/modals.selectors";
import {
selectBodyshop,
selectCurrentUser,
} from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import {GenerateDocument} from "../../utils/RenderTemplate";
import {TemplateList} from "../../utils/TemplateConstants";
@@ -36,8 +45,8 @@ const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("partsOrder")),
setBillEnterContext: (context) =>
dispatch(setModalContext({context: context, modal: "billEnter"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function PartsOrderModalContainer({
@@ -135,7 +144,8 @@ export function PartsOrderModalContainer({
: AuditTrailMapping.jobspartsorder(
insertResult.data.insert_parts_orders.returning[0].order_number
),
});
type: isReturn ? "jobspartsreturn" : "jobspartsorder",
});
const jobLinesResult = await updateJobLines({
variables: {

View File

@@ -52,8 +52,8 @@ const PaymentExpandedRowComponent = ({record, bodyshop}) => {
await insertPayment({
variables: {
paymentInput: {
amount: -refund_response.data.amount,
transactionid: payment_response.response.receiptelements.transid,
amount: -refund_response?.data?.amount,
transactionid: payment_response?.response?.receiptelements?.transid,
payer: record.payer,
type: "Refund",
jobid: payment_response.jobid,

View File

@@ -2,14 +2,17 @@ import {Button, Form, Input, Space} from "antd";
import {PageHeader} from "@ant-design/pro-layout";
import React from "react";
import {useTranslation} from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
selectAuthLevel,
selectBodyshop,
} from "../../redux/user/user.selectors";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, {PhoneItemFormatterValidation,} from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import {HasRbacAccess} from "../rbac-wrapper/rbac-wrapper.component";
import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {selectAuthLevel, selectBodyshop,} from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
authLevel: selectAuthLevel,
@@ -43,13 +46,16 @@ export function PhonebookFormComponent({
return (
<div>
<PageHeader
title={`${form.getFieldValue("firstname") || ""} ${
title={<Form.Item shouldUpdate>
{() =>`${form.getFieldValue("firstname") || ""} ${
form.getFieldValue("lastname") || ""
}${
form.getFieldValue("company")
? ` - ${form.getFieldValue("company")}`
: ""
}`}
: ""}`
}
</Form.Item>
}
extra={
<Space>
<Button

View File

@@ -31,8 +31,8 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionBoardKanbanComponent({
@@ -134,7 +134,8 @@ export function ProductionBoardKanbanComponent({
insertAuditTrail({
jobid: card.id,
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
});
type: "jobstatuschange",
});
if (update.errors) {
notification["error"]({

View File

@@ -27,6 +27,7 @@ export function ProductionColumnsComponent({
bodyshop,
data,
tableState,
refetch,
}) {
const [columns, setColumns] = columnState;
const {t} = useTranslation();
@@ -50,6 +51,7 @@ export function ProductionColumnsComponent({
data,
state: tableState,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
refetch,
});
const menu = {

View File

@@ -13,8 +13,8 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionListColumnAlert({record, insertAuditTrail}) {
@@ -46,7 +46,7 @@ export function ProductionListColumnAlert({record, insertAuditTrail}) {
? !record.production_vars.alert
: true
),
}).then(() => {
type: "alertToggle",}).then(() => {
if (record.refetch) record.refetch();
});
};

View File

@@ -1,5 +1,5 @@
import {BranchesOutlined, PauseCircleOutlined} from "@ant-design/icons";
import {Space, Tooltip} from "antd";
import {Checkbox,Space, Tooltip} from "antd";
import i18n from "i18next";
import dayjs from "../../utils/day";
import {Link} from "react-router-dom";
@@ -10,7 +10,9 @@ import {onlyUnique} from "../../utils/arrayHelper";
import {alphaSort, dateSort, statusSort} from "../../utils/sorters";
import JobAltTransportChange from "../job-at-change/job-at-change.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import OwnerNameDisplay, {
OwnerNameDisplayFunction,
} from "../owner-name-display/owner-name-display.component";
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
import ProductionListColumnAlert from "./production-list-columns.alert.component";
import ProductionListColumnBodyPriority from "./production-list-columns.bodypriority.component";
@@ -28,7 +30,7 @@ import ProductionlistColumnTouchTime from "./prodution-list-columns.touchtime.co
import {store} from "../../redux/store";
import {setModalContext} from "../../redux/modals/modals.actions";
const r = ({technician, state, activeStatuses, data, bodyshop}) => {
const r = ({technician, state, activeStatuses, data, bodyshop, refetch}) => {
return [
{
title: i18n.t("jobs.actions.viewdetail"),
@@ -109,7 +111,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
<OwnerNameDisplay ownerObject={record}/>
</Link>
),
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder:
state.sortedInfo.columnKey === "ownr" && state.sortedInfo.order,
},
@@ -120,8 +122,10 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
ellipsis: true,
sorter: (a, b) =>
alphaSort(
a.v_make_desc + a.v_model_desc,
b.v_make_desc + b.v_model_desc
`${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
a.v_model_desc || ""
}`,
`${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
),
sortOrder:
state.sortedInfo.columnKey === "vehicle" && state.sortedInfo.order,
@@ -316,7 +320,21 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
dataIndex: "special_coverage_policy",
key: "special_coverage_policy",
ellipsis: true,
},
sorter: (a, b) =>
Number(a.special_coverage_policy) - Number(b.special_coverage_policy),
sortOrder:
state.sortedInfo.columnKey === "special_coverage_policy" &&
state.sortedInfo.order,
filters: [
{ text: "True", value: true },
{ text: "False", value: false },
],
onFilter: (value, record) =>
value.includes(record.special_coverage_policy),
render: (text, record) => (
<Checkbox disabled checked={record.special_coverage_policy} />
),
},
{
title: i18n.t("jobs.fields.alt_transport"),
@@ -327,7 +345,16 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
sortOrder:
state.sortedInfo.columnKey === "alt_transport" &&
state.sortedInfo.order,
render: (text, record) => (
filters:
(bodyshop &&
bodyshop.appt_alt_transport.map((s) => {
return {
text: s,
value: [s],
};
})) ||
[],
onFilter: (value, record) => value.includes(record.alt_transport),render: (text, record) => (
<div>
{record.alt_transport}
<JobAltTransportChange job={record}/>
@@ -407,7 +434,11 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
title: i18n.t("production.labels.alert"),
dataIndex: "alert",
key: "alert",
sorter: (a, b) =>
Number(a.production_vars?.alert || false) -
Number(b.production_vars?.alert || false),
sortOrder:
state.sortedInfo.columnKey === "alert" && state.sortedInfo.order,
render: (text, record) => <ProductionListColumnAlert record={record}/>,
},
{
@@ -498,6 +529,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
),
render: (text, record) => (
<ProductionListEmployeeAssignment
refetch={refetch}
record={record}
type="employee_body"
/>
@@ -518,6 +550,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
refetch={refetch}
type="employee_prep"
/>
),
@@ -534,7 +567,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} type="employee_csr"/>
<ProductionListEmployeeAssignment refetch={refetch} record={record} type="employee_csr"/>
),
},
{
@@ -554,6 +587,7 @@ const r = ({technician, state, activeStatuses, data, bodyshop}) => {
render: (text, record) => (
<ProductionListEmployeeAssignment
record={record}
refetch={refetch}
type="employee_refinish"
/>
),

View File

@@ -16,14 +16,15 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionListEmpAssignment({
insertAuditTrail,
bodyshop,
record,
refetch,
type,
}) {
const {t} = useTranslation();
@@ -46,7 +47,8 @@ export function ProductionListEmpAssignment({
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobassignmentchange(empAssignment, name),
});
type: "jobassignmentchange",
});
if (!!result.errors) {
notification["error"]({
@@ -55,6 +57,9 @@ export function ProductionListEmpAssignment({
}),
});
}
await refetch();
setLoading(false);
};
const handleRemove = async (operation) => {
@@ -71,7 +76,8 @@ export function ProductionListEmpAssignment({
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobassignmentremoved(empAssignment),
});
type: "jobassignmentremoved",
});
if (!!result.errors) {
notification["error"]({
@@ -80,6 +86,9 @@ export function ProductionListEmpAssignment({
}),
});
}
await refetch();
setLoading(false);
};

View File

@@ -12,8 +12,8 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionListColumnCategory({record, bodyshop}) {

View File

@@ -5,16 +5,15 @@ import {connect} from "react-redux";
import {createStructuredSelector} from "reselect";
import {logImEXEvent} from "../../firebase/firebase.utils";
import {UPDATE_JOB} from "../../graphql/jobs.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import {insertAuditTrail} from "../../redux/application/application.actions";
import {insertAuditTrail} from "../../redux/application/application.actions";import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ProductionListColumnStatus({
@@ -41,7 +40,8 @@ export function ProductionListColumnStatus({
insertAuditTrail({
jobid: record.id,
operation: AuditTrailMapping.jobstatuschange(key),
});
type: "jobstatuschange",
});
setLoading(false);
};

View File

@@ -18,6 +18,7 @@ const mapStateToProps = createStructuredSelector({
});
export function ProductionListTable({
refetch,
bodyshop,
technician,
currentUser,
@@ -38,6 +39,7 @@ export function ProductionListTable({
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state,
data: data,
@@ -95,6 +97,7 @@ export function ProductionListTable({
...ProductionListColumns({
technician,
state,
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
}).find((e) => e.key === k.key),

View File

@@ -64,6 +64,7 @@ export function ProductionListTable({loading, data, refetch, bodyshop, technicia
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state,
data,
@@ -84,6 +85,7 @@ export function ProductionListTable({loading, data, refetch, bodyshop, technicia
...ProductionListColumns({
bodyshop,
technician,
refetch,
state,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
@@ -262,6 +264,7 @@ export function ProductionListTable({loading, data, refetch, bodyshop, technicia
state={state}
setState={setState}
setColumns={setColumns}
refetch={refetch}
data={data}
/>

View File

@@ -6,7 +6,7 @@ import {useTranslation} from "react-i18next";
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import {generateInternalReflections} from "./report-center-modal-utils";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
return (
@@ -31,10 +31,9 @@ function FiltersSection({filters, form, bodyshop}) {
const {t} = useTranslation();
return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')}
style={{marginTop: '10px'}}>
<Card type='inner' title={t('reportcenter.labels.advanced_filters_filters')} style={{marginTop: '10px'}}>
<Form.List name={["filters"]}>
{(fields, {add, remove, move}) => {
{(fields, {add, remove}) => {
return (
<div>
{fields.map((field, index) => (
@@ -72,7 +71,8 @@ function FiltersSection({filters, form, bodyshop}) {
</Col>
<Col span={6}>
<Form.Item
dependencies={[['filters', field.name, "field"]]}>
dependencies={[['filters', field.name, "field"],['filters', field.name, "value"]]}
>
{
() => {
const name = form.getFieldValue(['filters', field.name, "field"]);
@@ -82,7 +82,6 @@ function FiltersSection({filters, form, bodyshop}) {
key={`${index}operator`}
label={t('reportcenter.labels.advanced_filters_filter_operator')}
name={[field.name, "operator"]}
dependencies={[]}
rules={[
{
required: true,
@@ -92,20 +91,32 @@ function FiltersSection({filters, form, bodyshop}) {
>
<Select
getPopupContainer={trigger => trigger.parentNode}
options={getWhereOperatorsByType(type)}/>
options={ getWhereOperatorsByType(type)}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], undefined);
}}
/>
</Form.Item>
}
}
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
dependencies={[['filters', field.name, "field"]]}>
<Form.Item dependencies={[
['filters', field.name, "field"],
['filters', field.name, "operator"]
]}
>
{
() => {
// Because it looks cleaner than inlining.
const name = form.getFieldValue(['filters', field.name, "field"]);
const type = filters.find(f => f.name === name)?.type;
const reflector = filters.find(f => f.name === name)?.reflector;
const operator = form.getFieldValue(['filters', field.name, "operator"]);
const operatorType = operator ? getWhereOperatorsByType(type).find((o) => o.value === operator)?.type : null;
return <Form.Item
key={`${index}value`}
@@ -131,14 +142,29 @@ function FiltersSection({filters, form, bodyshop}) {
return generateInternalReflections({
bodyshop,
upperPath,
finalPath
finalPath,
t
});
};
const reflections = reflector ? generateReflections(reflector) : [];
const fieldPath = [[field.name, "value"]];
// We have reflections so we will use a select box
if (reflections.length > 0) {
// We have reflections and the operator type is array, so we will use a select box with multiple options
if (operatorType === "array") {
return (
<Select
disabled={!operator}
mode="multiple"
options={reflections}
getPopupContainer={trigger => trigger.parentNode}
onChange={(value) => {
form.setFieldValue(fieldPath, value);
}}
/>
);
}
return (
<Select
options={reflections}
@@ -150,16 +176,50 @@ function FiltersSection({filters, form, bodyshop}) {
);
}
// We have a type of number, so we will use a number input
if (type === "number") {
return (
<InputNumber
disabled={!operator}
onChange={(value) => form.setFieldValue(fieldPath, value)}/>
);
}
// We have a type of date, so we will use a date picker
if (type === "date") {
return (
<FormDatePicker
disabled={!operator}
onChange={(date) => form.setFieldValue(fieldPath, date)}
/>
);
}
// we have a type of boolean, so we will use a select box with a true or false option.
if (type === "boolean" || type === "bool") {
return (
<Select
disabled={!operator}
getPopupContainer={trigger => trigger.parentNode}
options={[
{
label: t('reportcenter.labels.advanced_filters_true'),
value: true
},
{
label: t('reportcenter.labels.advanced_filters_false'),
value: false
}
]}
onChange={(value) => form.setFieldValue(fieldPath, value)}
/>
);
}
return (
<Input
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/>
disabled={!operator}
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}
/>
);
})()
}
@@ -206,13 +266,12 @@ function FiltersSection({filters, form, bodyshop}) {
* @returns {JSX.Element}
* @constructor
*/
function SortersSection({sorters, form}) {
function SortersSection({sorters}) {
const {t} = useTranslation();
return (
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')}
style={{marginTop: '10px'}}>
<Card type='inner' title={t('reportcenter.labels.advanced_filters_sorters')} style={{marginTop: '10px'}}>
<Form.List name={["sorters"]}>
{(fields, {add, remove, move}) => {
{(fields, {add, remove}) => {
return (
<div>
Sorters

View File

@@ -8,6 +8,20 @@ import {uniqBy} from "lodash";
*/
const getValueFromPath = (obj, path) => path.split('.').reduce((prev, curr) => prev?.[curr], obj);
/**
* Generate options from array
* @param bodyshop
* @param path
* @returns {unknown[]}
*/
const generateOptionsFromArray = (bodyshop, path) => {
const options = getValueFromPath(bodyshop, path);
return uniqBy(options.map((value) => ({
label: value,
value: value,
})), 'value');
}
/**
* Valid internal reflections
* Note: This is intended for future functionality
@@ -42,19 +56,44 @@ const generateOptionsFromObject = (bodyshop, path, labelPath, valuePath) => {
* Generate special reflections
* @param bodyshop
* @param finalPath
* @param t - i18n
* @returns {{label: *, value: *}[]|{label: *, value: *}[]|{label: string, value: *}[]|*[]}
*/
const generateSpecialReflections = (bodyshop, finalPath) => {
const generateSpecialReflections = (bodyshop, finalPath, t) => {
switch (finalPath) {
case 'payment_payers':
return [
{
label: t("payments.labels.customer"),
value: t("payments.labels.customer"),
},
{
label: t("payments.labels.insurance"),
value: t("payments.labels.insurance"),
},
// This is a weird one supposedly only used by one shop and could potentially be
// placed behind a SplitSDK
{
label: t("payments.labels.external"),
value: t("payments.labels.external"),
}
];
case 'payment_types':
return generateOptionsFromArray(bodyshop, 'md_payment_types');
case 'alt_transports':
return generateOptionsFromArray(bodyshop, 'appt_alt_transport');
case 'lost_sale_reasons':
return generateOptionsFromArray(bodyshop, 'md_lost_sale_reasons');
// Special case because Referral Sources is an Array, not an Object.
case 'referral_source':
return generateOptionsFromArray(bodyshop, 'md_referral_sources');
case 'class':
return generateOptionsFromArray(bodyshop, 'md_classes');
case 'cost_centers':
return generateOptionsFromObject(bodyshop, 'md_responsibility_centers.costs', 'name', 'name');
// Special case because Categories is an Array, not an Object.
case 'categories':
const catOptions = getValueFromPath(bodyshop, 'md_categories');
return uniqBy(catOptions.map((value) => ({
label: value,
value: value,
})), 'value');
return generateOptionsFromArray(bodyshop, 'md_categories');
case 'insurance_companies':
return generateOptionsFromObject(bodyshop, 'md_ins_cos', 'name', 'name');
case 'employee_teams':
@@ -105,12 +144,13 @@ const generateBodyshopReflections = (bodyshop, finalPath) => {
* @param bodyshop
* @param upperPath
* @param finalPath
* @param t - i18n
* @returns {{label: *, value: *}[]|[]|{label: *, value: *}[]|{label: string, value: *}[]|{label: *, value: *}[]|*[]}
*/
const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
const generateInternalReflections = ({bodyshop, upperPath, finalPath, t}) => {
switch (upperPath) {
case 'special':
return generateSpecialReflections(bodyshop, finalPath);
return generateSpecialReflections(bodyshop, finalPath, t);
case 'bodyshop':
return generateBodyshopReflections(bodyshop, finalPath);
default:
@@ -118,4 +158,4 @@ const generateInternalReflections = ({bodyshop, upperPath, finalPath}) => {
}
};
export {generateInternalReflections,}
export {generateInternalReflections}

View File

@@ -30,8 +30,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
toggleModalVisible: () => dispatch(toggleModalVisible("schedule")),
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function ScheduleJobModalContainer({
@@ -142,7 +142,7 @@ export function ScheduleJobModalContainer({
operation: AuditTrailMapping.appointmentinsert(
DateTimeFormat(values.start)
),
});
type: "appointmentinsert",});
}
if (!!appt.errors) {

View File

@@ -26,6 +26,8 @@ export function ScoreboardDayStats({bodyshop, date, entries}) {
return acc + value.bodyhrs;
}, 0);
const numJobs = entries.length;
return (
<Card
title={dayjs(date).format("D - ddd")}
@@ -34,17 +36,18 @@ export function ScoreboardDayStats({bodyshop, date, entries}) {
>
<Statistic
valueStyle={{color: dailyBodyTarget > bodyHrs ? "red" : "green"}}
label="B"
label="Body"
value={bodyHrs.toFixed(1)}
/>
<Statistic
valueStyle={{color: dailyPaintTarget > paintHrs ? "red" : "green"}}
label="P"
label="Refinish"
value={paintHrs.toFixed(1)}
/>
<Divider style={{margin: 0}}/>
<Statistic value={(bodyHrs + paintHrs).toFixed(1)}/>
<Statistic label="Total" value={(bodyHrs + paintHrs).toFixed(1)}/>
<Divider style={{ margin: 0 }} />
<Statistic label="Jobs" value={numJobs} />
</Card>
);
}

View File

@@ -26,227 +26,260 @@ export function ScoreboardTargetsTable({bodyshop, scoreBoardlist}) {
const values = useMemo(() => {
const dateHash = _.groupBy(scoreBoardlist, "date");
let ret = {
todayBody: 0,
todayPaint: 0,
weeklyPaint: 0,
weeklyBody: 0,
toDateBody: 0,
toDatePaint: 0,
};
let ret = {
todayBody: 0,
todayPaint: 0,
todayJobs: 0,
weeklyPaint: 0,
weeklyJobs: 0,
weeklyBody: 0,
toDateBody: 0,
toDatePaint: 0,
toDateJobs: 0,
};
const today = dayjs();
if (dateHash[today.format("YYYY-MM-DD")]) {
dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
ret.todayBody = ret.todayBody + d.bodyhrs;
ret.todayPaint = ret.todayPaint + d.painthrs;
});
}
const today = dayjs();
if (dateHash[today.format("YYYY-MM-DD")]) {
dateHash[today.format("YYYY-MM-DD")].forEach((d) => {
ret.todayBody = ret.todayBody + d.bodyhrs;
ret.todayPaint = ret.todayPaint + d.painthrs;
ret.todayJobs++;
});
}
let StartOfWeek = dayjs().startOf("week");
while (StartOfWeek.isSameOrBefore(today)) {
if (dateHash[StartOfWeek.format("YYYY-MM-DD")]) {
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
});
}
StartOfWeek = StartOfWeek.add(1, "day");
}
let StartOfWeek = dayjs().startOf("week");
while (StartOfWeek.isSameOrBefore(today)) {
if (dateHash[StartOfWeek.format("YYYY-MM-DD")]) {
dateHash[StartOfWeek.format("YYYY-MM-DD")].forEach((d) => {
ret.weeklyBody = ret.weeklyBody + d.bodyhrs;
ret.weeklyPaint = ret.weeklyPaint + d.painthrs;
ret.weeklyJobs++;
});
}
StartOfWeek = StartOfWeek.add(1, "day");
}
let startOfMonth = dayjs().startOf("month");
while (startOfMonth.isSameOrBefore(today)) {
if (dateHash[startOfMonth.format("YYYY-MM-DD")]) {
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
ret.toDateBody = ret.toDateBody + d.bodyhrs;
ret.toDatePaint = ret.toDatePaint + d.painthrs;
});
}
startOfMonth = startOfMonth.add(1, "day");
}
let startOfMonth = dayjs().startOf("month");
while (startOfMonth.isSameOrBefore(today)) {
if (dateHash[startOfMonth.format("YYYY-MM-DD")]) {
dateHash[startOfMonth.format("YYYY-MM-DD")].forEach((d) => {
ret.toDateBody = ret.toDateBody + d.bodyhrs;
ret.toDatePaint = ret.toDatePaint + d.painthrs;
ret.toDateJobs++;
});
}
startOfMonth = startOfMonth.add(1, "day");
}
return ret;
}, [scoreBoardlist]);
return (
<Card
title={t("scoreboard.labels.targets")}
extra={<ScoreboardJobsList scoreBoardlist={scoreBoardlist}/>}
>
<Row gutter={rowGutter}>
<Col xs={24} sm={{offset: 0, span: 4}} lg={{span: 4}}>
<Statistic
title={t("scoreboard.labels.workingdays")}
value={Util.CalculateWorkingDaysThisMonth()}
prefix={<CalendarOutlined/>}
/>
</Col>
<Col xs={24} sm={{offset: 0, span: 20}} lg={{offset: 0, span: 20}}>
<Row>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.dailytarget")}
value={bodyshop.scoreboard_target.dailyBodyTarget}
prefix="B"
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.dailyactual")}
value={values.todayBody.toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.weeklytarget")}
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.weeklyactual")}
value={values.weeklyBody.toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.monthlytarget")}
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.asoftodaytarget")}
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.todateactual")}
value={values.toDateBody.toFixed(1)}
/>
</Col>
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={bodyshop.scoreboard_target.dailyPaintTarget}
prefix="P"
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.todayPaint.toFixed(1)}/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.weeklyPaint.toFixed(1)}/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.toDatePaint.toFixed(1)}/>
</Col>
</Row>
<Row>
<Divider style={{margin: 5}}/>
</Row>
<Row>
<Col {...statSpans}></Col>
<Col {...statSpans}>
<Statistic
value={(values.todayPaint + values.todayBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
/>
</Col>
</Row>
</Col>
</Row>
</Card>
);
return (
<Card
title={t("scoreboard.labels.targets")}
extra={<ScoreboardJobsList scoreBoardlist={scoreBoardlist} />}
>
<Row gutter={rowGutter}>
<Col xs={24} sm={{ offset: 0, span: 4 }} lg={{ span: 4 }}>
<Statistic
title={t("scoreboard.labels.workingdays")}
value={Util.CalculateWorkingDaysThisMonth()}
prefix={<CalendarOutlined />}
/>
</Col>
<Col xs={24} sm={{ offset: 0, span: 20 }} lg={{ offset: 0, span: 20 }}>
<Row>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.dailytarget")}
value={bodyshop.scoreboard_target.dailyBodyTarget}
prefix={t("scoreboard.labels.bodyabbrev")}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.dailyactual")}
value={values.todayBody.toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.weeklytarget")}
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.weeklyactual")}
value={values.weeklyBody.toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.monthlytarget")}
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.asoftodaytarget")}
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
title={t("scoreboard.labels.todateactual")}
value={values.toDateBody.toFixed(1)}
/>
</Col>
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={bodyshop.scoreboard_target.dailyPaintTarget}
prefix={t("scoreboard.labels.refinishabbrev")}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.todayPaint.toFixed(1)} />
</Col>
<Col {...statSpans}>
<Statistic
value={Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.weeklyPaint.toFixed(1)} />
</Col>
<Col {...statSpans}>
<Statistic
value={Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.toDatePaint.toFixed(1)} />
</Col>
</Row>
<Row>
<Divider style={{ margin: 5 }} />
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.total")}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.todayPaint + values.todayBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.WeeklyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.weeklyPaint + values.weeklyBody).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.MonthlyTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyBodyTarget,
bodyshop
) +
Util.AsOfTodayTargetHrs(
bodyshop.scoreboard_target.dailyPaintTarget,
bodyshop
)
).toFixed(1)}
/>
</Col>
<Col {...statSpans}>
<Statistic
value={(values.toDatePaint + values.toDateBody).toFixed(1)}
/>
</Col>
</Row>
<Row>
<Divider style={{ margin: 5 }} />
</Row>
<Row>
<Col {...statSpans}>
<Statistic
value={"\u00A0"}
prefix={t("scoreboard.labels.jobs")}
/>
</Col>
<Col {...statSpans}>
<Statistic value={values.todayJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.weeklyJobs} />
</Col>
<Col {...statSpans} />
<Col {...statSpans} />
<Col {...statSpans}>
<Statistic value={values.toDateJobs} />
</Col>
</Row>
</Col>
</Row>
</Card>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps
mapStateToProps,
mapDispatchToProps
)(ScoreboardTargetsTable);

View File

@@ -40,6 +40,7 @@ export const INSERT_AUDIT_TRAIL = gql`
bodyshopid
created
operation
type
useremail
}
}

View File

@@ -16,9 +16,14 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
import {OwnerNameDisplayFunction} from "../../components/owner-name-display/owner-name-display.component";
import {auth} from "../../firebase/firebase.utils";
import {QUERY_JOB_EXPORT_DMS} from "../../graphql/jobs.queries";
import {setBreadcrumbs, setSelectedHeader,} from "../../redux/application/application.actions";
import {
insertAuditTrail,
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
import {selectBodyshop} from "../../redux/user/user.selectors";
import InstanceRenderManager from '../../utils/instanceRenderMgr';
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -27,6 +32,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(insertAuditTrail({ jobid, operation, type })),
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
@@ -46,7 +53,12 @@ export const socket = SocketIO(
}
);
export function DmsContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) {
export function DmsContainer({
bodyshop,
setBreadcrumbs,
setSelectedHeader,
insertAuditTrail,
}) {
const {t} = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useNavigate();
@@ -104,6 +116,10 @@ export function DmsContainer({bodyshop, setBreadcrumbs, setSelectedHeader}) {
notification.success({
message: t("jobs.successes.exported"),
});
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
type: "jobexported",});
history("/manage/accounting/receivables");
});

View File

@@ -12,7 +12,8 @@ import AlertComponent from "../../components/alert/alert.component";
import {QUERY_EXPORT_LOG_PAGINATED} from "../../graphql/accounting.queries";
import {selectBodyshop} from "../../redux/user/user.selectors";
import {DateTimeFormatter} from "../../utils/DateFormatter";
import {pageLimit} from "../../utils/config";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "./../../utils/sorters";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -34,11 +35,42 @@ export function ExportLogsPageComponent({bodyshop}) {
limit: pageLimit,
order: [
{
[sortcolumn || "created_at"]: sortorder
...(sortcolumn === "ro_number"
? {
job: {
[sortcolumn|| "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
: "desc",},
}
: sortcolumn === "invoice_number"
? {
bill: {
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
}
: sortcolumn === "paymentnum"
? {
payment: {
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
},
}
: {
[sortcolumn || "created_at"]: sortorder
? sortorder === "descend"
? "desc"
: "asc"
: "desc",
}),
},
],
},
@@ -68,7 +100,8 @@ export function ExportLogsPageComponent({bodyshop}) {
title: t("general.labels.created_at"),
dataIndex: "created_at",
key: "created_at",
render: (text, record) => (
sorter: (a, b) => dateSort(a.created_at, b.created_at),
sortOrder: sortcolumn === "created_at" && sortorder,render: (text, record) => (
<DateTimeFormatter>{record.created_at}</DateTimeFormatter>
),
},
@@ -81,7 +114,8 @@ export function ExportLogsPageComponent({bodyshop}) {
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
sortOrder: sortcolumn === "ro_number" && sortorder,
render: (text, record) =>
record.job && (
<Link to={`/manage/jobs/${record.job.id}`}>
@@ -93,7 +127,8 @@ export function ExportLogsPageComponent({bodyshop}) {
title: t("bills.fields.invoice_number"),
dataIndex: "invoice_number",
key: "invoice_number",
render: (text, record) =>
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder: sortcolumn === "invoice_number" && sortorder,render: (text, record) =>
record.bill && (
<Link to={"/manage/bills?billid=" + (record.bill && record.bill.id)}>
{record.bill && record.bill.invoice_number}
@@ -103,7 +138,8 @@ export function ExportLogsPageComponent({bodyshop}) {
{
title: t("payments.fields.paymentnum"),
dataIndex: "paymentnum",
key: "paymentnum",
key: "paymentnum",sorter: (a, b) => alphaSort(a.paymentnum, b.paymentnum),
sortOrder: sortcolumn === "paymentnum" && sortorder,
render: (text, record) =>
record.payment && (
<Link
@@ -119,7 +155,13 @@ export function ExportLogsPageComponent({bodyshop}) {
{
title: t("general.labels.successful"),
dataIndex: "successful",
key: "successful",
key: "successful",sorter: (a, b) => Number(a.successful) - Number(b.successful),
sortOrder: sortcolumn === "successful" && sortorder,
filters: [
{ text: "True", value: true },
{ text: "False", value: false },
],
onFilter: (value, record) => record.successful === value,
render: (text, record) => (
<Checkbox disabled checked={record.successful}/>
),

View File

@@ -48,11 +48,11 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobsCloseComponent({job, bodyshop, jobRO, insertAuditTrail,}) {
export function JobsCloseComponent({job, bodyshop, jobRO, insertAuditTrail}) {
const {t} = useTranslation();
const [form] = Form.useForm();
const client = useApolloClient();
@@ -118,7 +118,7 @@ export function JobsCloseComponent({job, bodyshop, jobRO, insertAuditTrail,}) {
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobinvoiced(),
});
type: "jobinvoiced",});
// history(`/manage/jobs/${job.id}`);
} else {
setLoading(false);

View File

@@ -23,6 +23,7 @@ import {createStructuredSelector} from "reselect";
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component";
import JobAuditTrail from "../../components/job-audit-trail/job-audit-trail.component";
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container";
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container";
import JobReconciliationModal from "../../components/job-reconciliation-modal/job-reconciliation.modal.container";
import JobSyncButton from "../../components/job-sync-button/job-sync-button.component";
@@ -51,7 +52,6 @@ import UndefinedToNull from "../../utils/undefinedtonull";
import _ from "lodash";
import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component";
import {DateTimeFormat} from "../../utils/DateFormatter";
import JobLifecycleComponent from "../../components/job-lifecycle/job-lifecycle.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
@@ -62,8 +62,8 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setPrintCenterContext: (context) =>
dispatch(setModalContext({context: context, modal: "printCenter"})),
insertAuditTrail: ({jobid, operation}) =>
dispatch(insertAuditTrail({jobid, operation})),
insertAuditTrail: ({jobid, operation, type}) =>
dispatch(insertAuditTrail({jobid, operation, type })),
});
export function JobsDetailPage({
@@ -205,7 +205,7 @@ export function JobsDetailPage({
? DateTimeFormat(changedAuditFields[key])
: changedAuditFields[key]
),
});
type: "jobfieldchange",});
});
await refetch();
@@ -344,7 +344,7 @@ export function JobsDetailPage({
{
key: 'lifecycle',
icon: <BarsOutlined/>,
label: t('menus.jobsdetail.lifecycle'),
label: t("menus.jobsdetail.lifecycle"),
children: <JobLifecycleComponent job={job} statuses={bodyshop.md_ro_statuses}/>
},
{

View File

@@ -54,9 +54,9 @@ export const setOnline = (isOnline) => ({
payload: isOnline,
});
export const insertAuditTrail = ({jobid, billid, operation}) => ({
export const insertAuditTrail = ({jobid, billid, operation, type}) => ({
type: ApplicationActionTypes.INSERT_AUDIT_TRAIL,
payload: {jobid, billid, operation},
payload: {jobid, billid, operation, type },
});
export const setProblemJobs = (problemJobs) => ({
type: ApplicationActionTypes.SET_PROBLEM_JOBS,

View File

@@ -263,7 +263,7 @@ export function* onInsertAuditTrail() {
}
export function* insertAuditTrailSaga({
payload: {jobid, billid, operation},
payload: {jobid, billid, operation, type},
}) {
const state = yield select();
const bodyshop = state.user.bodyshop;
@@ -275,18 +275,18 @@ export function* insertAuditTrailSaga({
jobid,
billid,
operation,
useremail: currentUser.email,
type,useremail: currentUser.email,
},
};
yield client.mutate({
mutation: INSERT_AUDIT_TRAIL,
variables,
update(cache, {data}) {
update(cache, { data }) {
cache.modify({
fields: {
audit_trail(existingAuditTrail, {readField}) {
audit_trail(existingAuditTrail, { readField }) {
const newAuditTrail = cache.writeQuery({
data: data.insert_audit_trail_one,
data: data,
query: INSERT_AUDIT_TRAIL,
variables,
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,8 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.appointmentcancel", {lost_sale_reason}),
appointmentinsert: (start) =>
i18n.t("audit_trail.messages.appointmentinsert", {start}),
billdeleted: (invoice_number) =>
i18n.t("audit_trail.messages.billdeleted", { invoice_number }),
billposted: (invoice_number) =>
i18n.t("audit_trail.messages.billposted", {invoice_number}),
billupdated: (invoice_number) =>
@@ -30,6 +32,7 @@ const AuditTrailMapping = {
i18n.t("audit_trail.messages.jobintake", {status, email,scheduled_completion}),
jobdelivery: (status, email, actual_completion) =>
i18n.t("audit_trail.messages.jobdelivery", {status, email,actual_completion}),
jobexported: () => i18n.t("audit_trail.messages.jobexported"),
jobfieldchange: (field, value) =>
i18n.t("audit_trail.messages.jobfieldchanged", {field, value}),
jobimported: () => i18n.t("audit_trail.messages.jobimported"),
@@ -57,6 +60,8 @@ const AuditTrailMapping = {
jobstatuschange: (status) =>
i18n.t("audit_trail.messages.jobstatuschange", {status}),
jobsupplement: () => i18n.t("audit_trail.messages.jobsupplement"),
jobsuspend: (status) => i18n.t("audit_trail.messages.jobsuspend", { status }),
jobvoid: () => i18n.t("audit_trail.messages.jobvoid"),
};
export default AuditTrailMapping;

View File

@@ -2,22 +2,66 @@ import {Kind, parse, print, visit} from "graphql";
import client from "./GraphQLClient";
import {gql} from "@apollo/client";
/* eslint-disable no-loop-func */
/**
* The available operators for filtering (string)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const STRING_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_like", label: "contains"},
{value: "_nlike", label: "does not contain"},
{value: "_ilike", label: "contains case-insensitive"},
{value: "_nilike", label: "does not contain case-insensitive"}
{value: "_nilike", label: "does not contain case-insensitive"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
];
/**
* The available operators for filtering (dates)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const DATE_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
];
/**
* The available operators for filtering (booleans)
* @type {[{label: string, value: string},{label: string, value: string}]}
*/
const BOOLEAN_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
];
/**
* The available operators for filtering (numbers)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const NUMBER_OPERATORS = [
{value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"}
{value: "_lte", label: "less than or equal"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
];
/**
* The available operators for sorting
* @type {[{label: string, value: string},{label: string, value: string}]}
*/
const ORDER_BY_OPERATORS = [
{value: "asc", label: "ascending"},
{value: "desc", label: "descending"}
@@ -31,7 +75,6 @@ export function getOrderOperatorsByType() {
return ORDER_BY_OPERATORS;
}
/**
* Get the available operators for filtering
* @param type
@@ -40,13 +83,14 @@ export function getOrderOperatorsByType() {
export function getWhereOperatorsByType(type = 'string') {
const operators = {
string: STRING_OPERATORS,
number: NUMBER_OPERATORS
number: NUMBER_OPERATORS,
boolean: BOOLEAN_OPERATORS,
bool: BOOLEAN_OPERATORS,
date: DATE_OPERATORS
};
return operators[type];
}
/* eslint-disable no-loop-func */
/**
* Parse a GraphQL query into an AST
* @param query
@@ -78,11 +122,9 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
// Parse the query and apply the filters and sorters
const ast = parseQuery(templateQueryToExecute);
let filterFields = [];
if (templateObject?.filters && templateObject?.filters?.length) {
applyFilters(ast, templateObject.filters, filterFields);
wrapFiltersInAnd(ast, filterFields);
applyFilters(ast, templateObject.filters);
}
if (templateObject?.sorters && templateObject?.sorters?.length) {
@@ -109,7 +151,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
return {contextData, useShopSpecificTemplate};
}
/**
* Apply sorters to the AST
* @param ast
@@ -170,10 +211,59 @@ export function applySorters(ast, sorters) {
});
}
/**
* Apply Top Level Sub to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyTopLevelSub(node, fieldPath, filterField) {
// Find or create the where argument for the top-level subfield
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []},
};
const topLevelSubSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSubSelection) {
topLevelSubSelection.arguments = topLevelSubSelection.arguments || [];
topLevelSubSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: path},
value: {kind: Kind.OBJECT, fields: []}
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(filterField);
} else { // Directly under the top level
whereArg.value.fields.push(filterField);
}
}
/**
* Apply filters to the AST
* @param ast
* @param filters
* @returns {ASTNode}
*/
export function applyFilters(ast, filters) {
return visit(ast, {
@@ -182,192 +272,197 @@ export function applyFilters(ast, filters) {
filters.forEach(filter => {
const fieldPath = filter.field.split('.');
let topLevel = false;
let topLevelSub = false;
// Determine if the filter should be applied at the top level
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
if (fieldPath.length === 2) {
topLevel = true;
}
if (topLevel) {
// Construct the filter for a top-level application
const targetFieldName = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
const nestedFilter = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetFieldName},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
// Find or create the where argument for the top-level field
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []},
};
const topLevelSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: path},
value: {kind: Kind.OBJECT, fields: []}
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(nestedFilter);
} else { // Directly under the top level
whereArg.value.fields.push(nestedFilter);
}
} else {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
let whereArgFound = false;
// Iterate over the fieldPath, except for the last entry, to navigate the structure
for (let i = 0; i < fieldPath.length - 1; i++) {
const fieldName = fieldPath[i];
let fieldFound = false;
// Check if the current selection has a selectionSet and selections
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
// Look for the field in the current selection's selections
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
if (selection) {
// Move down the AST to the found selection
currentSelection = selection;
fieldFound = true;
}
}
// If the field was not found in the current path, it's an issue
if (!fieldFound) {
console.error(`Field ${fieldName} not found in the current selection.`);
return; // Exit the loop and function due to error
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied
// Check if the 'where' argument already exists in the current selection
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
if (whereArg) {
whereArgFound = true;
} else {
// If not found, create a new 'where' argument for the current selection
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
});
}
// Assuming the last entry in fieldPath is the field to apply the filter on
const targetField = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
// Construct the filter field object
const filterField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetField},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
// Add the filter field to the 'where' clause of the current selection
if (whereArgFound) {
whereArg.value.fields.push(filterField);
} else {
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
}
if (fieldPath.length > 2 && fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevelSub = true;
}
// Construct the filter for a top-level application
const targetFieldName = fieldPath[fieldPath.length - 1];
let filterValue = createFilterValue(filter);
let filterField = createFilterField(targetFieldName, filter, filterValue);
if (topLevel) {
applyTopLevelFilter(node, fieldPath, filterField);
} else if (topLevelSub) {
applyTopLevelSub(node, fieldPath, filterField);
} else {
applyNestedFilter(node, fieldPath, filterField);
}
});
}
}
});
}
/**
* Create a filter value based on the filter
* @param filter
* @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
*/
function createFilterValue(filter) {
if (Array.isArray(filter.value)) {
// If it's an array, create a list value with the array items
return {
kind: Kind.LIST,
values: filter.value.map(item => ({
kind: getGraphQLKind(item),
value: item,
})),
};
} else {
// If it's not an array, use the existing logic
return {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
}
}
/**
* Create a filter field based on the target field and filter
* @param targetFieldName
* @param filter
* @param filterValue
* @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}}
*/
function createFilterField(targetFieldName, filter, filterValue) {
return {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetFieldName},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
}
/**
* Apply a top-level filter to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyTopLevelFilter(node, fieldPath, filterField) {
// Find or create the where argument for the top-level field
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
whereArg = {
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []},
};
const topLevelSelection = node.selectionSet.selections.find(selection =>
selection.name.value === fieldPath[0]
);
if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// Correctly position the nested filter without an extra 'where'
if (fieldPath.length > 2) { // More than one level deep
let currentField = whereArg.value;
fieldPath.slice(1, -1).forEach((path, index) => {
let existingField = currentField.fields.find(f => f.name.value === path);
if (!existingField) {
existingField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: path},
value: {kind: Kind.OBJECT, fields: []}
};
currentField.fields.push(existingField);
}
currentField = existingField.value;
});
currentField.fields.push(filterField);
} else { // Directly under the top level
whereArg.value.fields.push(filterField);
}
}
/**
* Apply a nested filter to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyNestedFilter(node, fieldPath, filterField) {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
// Iterate over the fieldPath, except for the last entry, to navigate the structure
for (let i = 0; i < fieldPath.length - 1; i++) {
const fieldName = fieldPath[i];
let fieldFound = false;
// Check if the current selection has a selectionSet and selections
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
// Look for the field in the current selection's selections
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
if (selection) {
// Move down the AST to the found selection
currentSelection = selection;
fieldFound = true;
}
}
// If the field was not found in the current path, it's an issue
if (!fieldFound) {
console.error(`Field ${fieldName} not found in the current selection.`);
return; // Exit the loop and function due to error
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied
// Check if the 'where' argument already exists in the current selection
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
// If not found, create a new 'where' argument for the current selection
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
});
}
// Add the filter field to the 'where' clause of the current selection
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
}
/**
* Get the GraphQL kind for a value
* @param value
* @returns {Kind|Kind.INT}
*/
function getGraphQLKind(value) {
if (typeof value === 'number') {
if (Array.isArray(value)) {
return Kind.LIST;
} else if (typeof value === 'number') {
return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
} else if (typeof value === 'boolean') {
return Kind.BOOLEAN;
} else if (typeof value === 'string') {
return Kind.STRING;
} else if (value instanceof Date) {
return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string
}
// Extend with more types as needed
}
/**
* Wrap filters in an 'and' object
* @param ast
* @param filterFields
*/
export function wrapFiltersInAnd(ast, filterFields) {
visit(ast, {
OperationDefinition: {
enter(node) {
node.selectionSet.selections.forEach((selection) => {
let whereArg = selection.arguments.find(arg => arg.name.value === 'where');
if (filterFields.length > 1) {
const andFilter = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: '_and'},
value: {kind: Kind.LIST, values: filterFields}
};
whereArg.value.fields.push(andFilter);
} else if (filterFields.length === 1) {
whereArg.value.fields.push(filterFields[0].fields[0]);
}
});
}
}
});
}
/* eslint-enable no-loop-func */
/* eslint-enable no-loop-func */