Compare commits
57 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e8ea736c5 | ||
|
|
3737fe457f | ||
|
|
bb4e8eb5bd | ||
|
|
27a07e8d5d | ||
|
|
e2618eee83 | ||
|
|
66c51a4be5 | ||
|
|
d5afcaeaab | ||
|
|
c332ec11b7 | ||
|
|
cf31290f05 | ||
|
|
dbbab910b6 | ||
|
|
abf01b4966 | ||
|
|
a965f9edf5 | ||
|
|
f02ca05eba | ||
|
|
a182ea0869 | ||
|
|
7bc2d41a68 | ||
|
|
5277e90946 | ||
|
|
15ea4e6afa | ||
|
|
5b3b6a409c | ||
|
|
d92bab113e | ||
|
|
93c6e2b601 | ||
|
|
19a90571f6 | ||
|
|
736e9cedfa | ||
|
|
c433103e1b | ||
|
|
2892fdbb58 | ||
|
|
c45f38e47b | ||
|
|
54a58c9fbc | ||
|
|
1934ae0758 | ||
|
|
953e70efef | ||
|
|
a6bae390e5 | ||
|
|
cf9d8d649d | ||
|
|
a25051c4c2 | ||
|
|
d5c3152631 | ||
|
|
66c425bf96 | ||
|
|
ffad0dfbf7 | ||
|
|
17285fc029 | ||
|
|
401e3cff73 | ||
|
|
865680e019 | ||
|
|
9f97ca0336 | ||
|
|
5df38f8612 | ||
|
|
63c5719420 | ||
|
|
d6c80f1420 | ||
|
|
fade927c9e | ||
|
|
9f472ce1d0 | ||
|
|
47a56e32b9 | ||
|
|
f13f79acb6 | ||
|
|
bfa9fddb9e | ||
|
|
28abd9707e | ||
|
|
5f621e1ae0 | ||
|
|
624414799e | ||
|
|
72091e9eae | ||
|
|
9cfacdd025 | ||
|
|
655e516246 | ||
|
|
7b12f0a3b9 | ||
|
|
5c4267f3ef | ||
|
|
e79e512291 | ||
|
|
f0064abfbe | ||
|
|
32bdea559e |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -130,3 +130,5 @@ test-output.txt
|
||||
server/job/test/fixtures
|
||||
|
||||
.github
|
||||
_reference/ragmate/.ragmate.env
|
||||
docker_data
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
client_max_body_size 50M;
|
||||
client_body_buffer_size 5M;
|
||||
client_body_buffer_size 5M;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import { Card, Checkbox, Input, Space, Table } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -16,12 +16,13 @@ import PayableExportAll from "../payable-export-all-button/payable-export-all-bu
|
||||
import PayableExportButton from "../payable-export-button/payable-export-button.component";
|
||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
import useLocalStorage from "./../../utils/useLocalStorage";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -31,7 +32,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
||||
const { t } = useTranslation();
|
||||
const [selectedBills, setSelectedBills] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
const [state, setState] = useLocalStorage("accounting-payables-table-state", {
|
||||
sortedInfo: {},
|
||||
search: ""
|
||||
});
|
||||
@@ -181,7 +182,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
onSelect: (record, selected, selectedRows) => {
|
||||
setSelectedBills(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card, Input, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -10,6 +10,7 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { exportPageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||
@@ -21,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -31,7 +32,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
||||
const { t } = useTranslation();
|
||||
const [selectedPayments, setSelectedPayments] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
const [state, setState] = useLocalStorage("accounting-payments-table-state", {
|
||||
sortedInfo: {},
|
||||
search: ""
|
||||
});
|
||||
@@ -194,7 +195,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
onSelect: (record, selected, selectedRows) => {
|
||||
setSelectedPayments(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Card, Input, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -10,6 +10,7 @@ import { exportPageLimit } from "../../utils/config";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||
@@ -20,7 +21,7 @@ import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
|
||||
@@ -30,7 +31,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||
const [transInProgress, setTransInProgress] = useState(false);
|
||||
|
||||
const [state, setState] = useState({
|
||||
const [state, setState] = useLocalStorage("accounting-receivables-table-state", {
|
||||
sortedInfo: {},
|
||||
search: ""
|
||||
});
|
||||
@@ -207,7 +208,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)),
|
||||
onSelect: (record, selected, selectedRows, nativeEvent) => {
|
||||
onSelect: (record, selected, selectedRows) => {
|
||||
setSelectedJobs(selectedRows.map((i) => i.id));
|
||||
},
|
||||
getCheckboxProps: (record) => ({
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||
import dayjs from "../../../utils/day";
|
||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../../owner-name-display/owner-name-display.component";
|
||||
import DashboardRefreshRequired from "../refresh-required.component";
|
||||
|
||||
export default function DashboardScheduledDeliveryToday({ data, ...cardProps }) {
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {},
|
||||
filteredInfo: {}
|
||||
});
|
||||
const [isTvModeScheduledDelivery, setIsTvModeScheduledDelivery] = useLocalStorage("isTvModeScheduledDelivery", false);
|
||||
if (!data) return null;
|
||||
if (!data.scheduled_delivery_today) return <DashboardRefreshRequired {...cardProps} />;
|
||||
|
||||
const scheduledDeliveryToday = data.scheduled_delivery_today.map((item) => {
|
||||
const joblines_body = item.joblines
|
||||
? item.joblines.filter((l) => l.mod_lbr_ty !== "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||
: 0;
|
||||
const joblines_ref = item.joblines
|
||||
? item.joblines.filter((l) => l.mod_lbr_ty === "LAR").reduce((acc, val) => acc + val.mod_lb_hrs, 0)
|
||||
: 0;
|
||||
return {
|
||||
...item,
|
||||
joblines_body,
|
||||
joblines_ref
|
||||
};
|
||||
});
|
||||
|
||||
const tvFontSize = 18;
|
||||
const tvFontWeight = "bold";
|
||||
|
||||
const tvColumns = [
|
||||
{
|
||||
title: t("jobs.fields.scheduled_delivery"),
|
||||
dataIndex: "scheduled_delivery",
|
||||
key: "scheduled_delivery",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
|
||||
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.jobid} onClick={(e) => e.stopPropagation()}>
|
||||
<Space>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</Space>
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
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} onClick={(e) => e.stopPropagation()}>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
|
||||
</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{`${
|
||||
record.v_model_yr || ""
|
||||
} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order,
|
||||
filters:
|
||||
(scheduledDeliveryToday &&
|
||||
scheduledDeliveryToday
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport),
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.alt_transport}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filters:
|
||||
(scheduledDeliveryToday &&
|
||||
scheduledDeliveryToday
|
||||
.map((j) => j.status)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || t("dashboard.errors.status"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.status),
|
||||
render: (text, record) => <span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.status}</span>
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lab"),
|
||||
dataIndex: "joblines_body",
|
||||
key: "joblines_body",
|
||||
sorter: (a, b) => a.joblines_body - b.joblines_body,
|
||||
sortOrder: state.sortedInfo.columnKey === "joblines_body" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.joblines_body.toFixed(1)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.lar"),
|
||||
dataIndex: "joblines_ref",
|
||||
key: "joblines_ref",
|
||||
sorter: (a, b) => a.joblines_ref - b.joblines_ref,
|
||||
sortOrder: state.sortedInfo.columnKey === "joblines_ref" && state.sortedInfo.order,
|
||||
align: "right",
|
||||
render: (text, record) => (
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>{record.joblines_ref.toFixed(1)}</span>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.scheduled_delivery"),
|
||||
dataIndex: "scheduled_delivery",
|
||||
key: "scheduled_delivery",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => dateSort(a.scheduled_delivery, b.scheduled_delivery),
|
||||
sortOrder: state.sortedInfo.columnKey === "scheduled_delivery" && state.sortedInfo.order,
|
||||
render: (text, record) => <TimeFormatter>{record.scheduled_delivery}</TimeFormatter>
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
|
||||
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => (
|
||||
<Link to={"/manage/jobs/" + record.jobid} onClick={(e) => e.stopPropagation()}>
|
||||
<Space>
|
||||
{record.ro_number || t("general.labels.na")}
|
||||
{record.production_vars && record.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{record.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{record.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Space>
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.owner"),
|
||||
dataIndex: "owner",
|
||||
key: "owner",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
return record.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</Link>
|
||||
) : (
|
||||
<span>
|
||||
<OwnerNameDisplay ownerObject={record} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("dashboard.labels.phone"),
|
||||
dataIndex: "ownr_ph",
|
||||
key: "ownr_ph",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => (
|
||||
<Space size="small" wrap>
|
||||
<ChatOpenButton phone={record.ownr_ph1} jobid={record.jobid} />
|
||||
|
||||
<ChatOpenButton phone={record.ownr_ph2} jobid={record.jobid} />
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ownr_ea"),
|
||||
dataIndex: "ownr_ea",
|
||||
key: "ownr_ea",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
render: (text, record) => <a href={`mailto:${record.ownr_ea}`}>{record.ownr_ea}</a>
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
key: "vehicle",
|
||||
ellipsis: true,
|
||||
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} onClick={(e) => e.stopPropagation()}>
|
||||
{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}
|
||||
</Link>
|
||||
) : (
|
||||
<span>{`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${record.v_model_desc || ""}`}</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.ins_co_nm"),
|
||||
dataIndex: "ins_co_nm",
|
||||
key: "ins_co_nm",
|
||||
ellipsis: true,
|
||||
responsive: ["md"],
|
||||
sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
|
||||
sortOrder: state.sortedInfo.columnKey === "ins_co_nm" && state.sortedInfo.order,
|
||||
filters:
|
||||
(scheduledDeliveryToday &&
|
||||
scheduledDeliveryToday
|
||||
.map((j) => j.ins_co_nm)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || t("dashboard.errors.insco"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.ins_co_nm)
|
||||
},
|
||||
{
|
||||
title: t("appointments.fields.alt_transport"),
|
||||
dataIndex: "alt_transport",
|
||||
key: "alt_transport",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sortOrder: state.sortedInfo.columnKey === "alt_transport" && state.sortedInfo.order,
|
||||
filters:
|
||||
(scheduledDeliveryToday &&
|
||||
scheduledDeliveryToday
|
||||
.map((j) => j.alt_transport)
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
.sort((a, b) => alphaSort(a.text, b.text))) ||
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.alt_transport)
|
||||
}
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={t("dashboard.titles.scheduleddeliverydate", {
|
||||
date: dayjs().startOf("day").format("MM/DD/YYYY")
|
||||
})}
|
||||
extra={
|
||||
<Space>
|
||||
<Typography.Text>{t("general.labels.tvmode")}</Typography.Text>
|
||||
<Switch
|
||||
onClick={() => setIsTvModeScheduledDelivery(!isTvModeScheduledDelivery)}
|
||||
defaultChecked={isTvModeScheduledDelivery}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
{...cardProps}
|
||||
>
|
||||
<div style={{ height: "100%" }}>
|
||||
<Table
|
||||
onChange={handleTableChange}
|
||||
pagination={false}
|
||||
columns={isTvModeScheduledDelivery ? tvColumns : columns}
|
||||
scroll={{ x: true, y: "calc(100% - 2em)" }}
|
||||
rowKey="id"
|
||||
style={{ height: "85%" }}
|
||||
dataSource={scheduledDeliveryToday}
|
||||
size={isTvModeScheduledDelivery ? "small" : "middle"}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export const DashboardScheduledDeliveryTodayGql = `
|
||||
scheduled_delivery_today: jobs(where: {
|
||||
date_invoiced: {_is_null: true},
|
||||
ro_number: {_is_null: false},
|
||||
voided: {_eq: false},
|
||||
scheduled_delivery: {_gte: "${dayjs().startOf("day").toISOString()}",
|
||||
_lte: "${dayjs().endOf("day").toISOString()}"}}) {
|
||||
alt_transport
|
||||
clm_no
|
||||
jobid: id
|
||||
joblines(where: {removed: {_eq: false}}) {
|
||||
mod_lb_hrs
|
||||
mod_lbr_ty
|
||||
}
|
||||
ins_co_nm
|
||||
iouparent
|
||||
ownerid
|
||||
ownr_co_nm
|
||||
ownr_ea
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
production_vars
|
||||
ro_number
|
||||
scheduled_delivery
|
||||
status
|
||||
suspended
|
||||
v_make_desc
|
||||
v_model_desc
|
||||
v_model_yr
|
||||
v_vin
|
||||
vehicleid
|
||||
}
|
||||
`;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||
import dayjs from "../../../utils/day";
|
||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||
@@ -169,7 +169,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport",
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -313,7 +313,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Ins. Co.*",
|
||||
text: s || t("dashboard.errors.insco"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -335,7 +335,7 @@ export default function DashboardScheduledInToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport",
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined } from "@ant-design/icons";
|
||||
import { Card, Space, Switch, Table, Tooltip, Typography } from "antd";
|
||||
import dayjs from "../../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TimeFormatter } from "../../../utils/DateFormatter";
|
||||
import { onlyUnique } from "../../../utils/arrayHelper";
|
||||
import dayjs from "../../../utils/day";
|
||||
import { alphaSort, dateSort } from "../../../utils/sorters";
|
||||
import useLocalStorage from "../../../utils/useLocalStorage";
|
||||
import ChatOpenButton from "../../chat-open-button/chat-open-button.component";
|
||||
@@ -138,7 +138,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport*",
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -154,7 +154,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
ellipsis: true,
|
||||
sorter: (a, b) => alphaSort(a.alt_transport, b.alt_transport),
|
||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
filters:
|
||||
(scheduledOutToday &&
|
||||
@@ -163,7 +163,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Status*",
|
||||
text: s || t("dashboard.errors.status"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -306,7 +306,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Ins. Co.*",
|
||||
text: s || t("dashboard.errors.insco"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
@@ -328,7 +328,7 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Alt. Transport*",
|
||||
text: s || t("dashboard.errors.atp"),
|
||||
value: [s]
|
||||
};
|
||||
})
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import i18next from "i18next";
|
||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
||||
import {
|
||||
DashboardTotalProductionHours,
|
||||
DashboardTotalProductionHoursGql
|
||||
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
|
||||
import DashboardProjectedMonthlySales, {
|
||||
DashboardProjectedMonthlySalesGql
|
||||
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
|
||||
import DashboardMonthlyRevenueGraph, {
|
||||
DashboardMonthlyRevenueGraphGql
|
||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
|
||||
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
|
||||
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
|
||||
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
|
||||
import JobLifecycleDashboardComponent, {
|
||||
JobLifecycleDashboardGQL
|
||||
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
||||
import DashboardMonthlyEmployeeEfficiency, {
|
||||
DashboardMonthlyEmployeeEfficiencyGql
|
||||
} from "../dashboard-components/monthly-employee-efficiency/monthly-employee-efficiency.component.jsx";
|
||||
import DashboardMonthlyJobCosting from "../dashboard-components/monthly-job-costing/monthly-job-costing.component.jsx";
|
||||
import DashboardMonthlyLaborSales from "../dashboard-components/monthly-labor-sales/monthly-labor-sales.component.jsx";
|
||||
import DashboardMonthlyPartsSales from "../dashboard-components/monthly-parts-sales/monthly-parts-sales.component.jsx";
|
||||
import DashboardMonthlyRevenueGraph, {
|
||||
DashboardMonthlyRevenueGraphGql
|
||||
} from "../dashboard-components/monthly-revenue-graph/monthly-revenue-graph.component.jsx";
|
||||
import DashboardProjectedMonthlySales, {
|
||||
DashboardProjectedMonthlySalesGql
|
||||
} from "../dashboard-components/pojected-monthly-sales/projected-monthly-sales.component.jsx";
|
||||
import DashboardScheduledDeliveryToday, {
|
||||
DashboardScheduledDeliveryTodayGql
|
||||
} from "../dashboard-components/scheduled-delivery-today/scheduled-delivery-today.component.jsx";
|
||||
import DashboardScheduledInToday, {
|
||||
DashboardScheduledInTodayGql
|
||||
} from "../dashboard-components/scheduled-in-today/scheduled-in-today.component.jsx";
|
||||
import DashboardScheduledOutToday, {
|
||||
DashboardScheduledOutTodayGql
|
||||
} from "../dashboard-components/scheduled-out-today/scheduled-out-today.component.jsx";
|
||||
import JobLifecycleDashboardComponent, {
|
||||
JobLifecycleDashboardGQL
|
||||
} from "../dashboard-components/job-lifecycle/job-lifecycle-dashboard.component.jsx";
|
||||
import DashboardTotalProductionDollars from "../dashboard-components/total-production-dollars/total-production-dollars.component.jsx";
|
||||
import {
|
||||
DashboardTotalProductionHours,
|
||||
DashboardTotalProductionHoursGql
|
||||
} from "../dashboard-components/total-production-hours/total-production-hours.component.jsx";
|
||||
|
||||
const componentList = {
|
||||
ProductionDollars: {
|
||||
@@ -118,6 +121,15 @@ const componentList = {
|
||||
w: 10,
|
||||
h: 3
|
||||
},
|
||||
ScheduleDeliveryToday: {
|
||||
label: i18next.t("dashboard.titles.scheduleddeliverytoday"),
|
||||
component: DashboardScheduledDeliveryToday,
|
||||
gqlFragment: DashboardScheduledDeliveryTodayGql,
|
||||
minW: 6,
|
||||
minH: 2,
|
||||
w: 10,
|
||||
h: 3
|
||||
},
|
||||
JobLifecycle: {
|
||||
label: i18next.t("dashboard.titles.joblifecycle"),
|
||||
component: JobLifecycleDashboardComponent,
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Typography
|
||||
} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -24,14 +23,14 @@ import i18n from "../../translations/i18n";
|
||||
import dayjs from "../../utils/day";
|
||||
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
||||
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
|
||||
@@ -93,7 +92,9 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
})
|
||||
: ""
|
||||
}`.slice(0, 239),
|
||||
inservicedate: dayjs("2019-01-01")
|
||||
inservicedate: dayjs(
|
||||
`${(job.v_model_yr && (job.v_model_yr < 100 ? (job.v_model_yr >= (dayjs().year() + 1) % 100 ? 1900 + parseInt(job.v_model_yr) : 2000 + parseInt(job.v_model_yr)) : job.v_model_yr)) || 2019}-01-01`
|
||||
)
|
||||
}}
|
||||
>
|
||||
<LayoutFormRow grow>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DatePicker, Space, TimePicker } from "antd";
|
||||
import PropTypes from "prop-types";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -94,7 +94,24 @@ const DateTimePicker = ({
|
||||
showTime={false}
|
||||
format="MM/DD/YYYY"
|
||||
value={value ? dayjs(value) : null}
|
||||
onChange={handleChange}
|
||||
onChange={(dateValue) => {
|
||||
if (dateValue) {
|
||||
// When date changes, preserve the existing time if it exists
|
||||
if (value && dayjs(value).isValid()) {
|
||||
const existingTime = dayjs(value);
|
||||
const newDateTime = dayjs(dateValue)
|
||||
.hour(existingTime.hour())
|
||||
.minute(existingTime.minute())
|
||||
.second(existingTime.second());
|
||||
handleChange(newDateTime);
|
||||
} else {
|
||||
// If no existing time, just set the date without time
|
||||
handleChange(dateValue);
|
||||
}
|
||||
} else {
|
||||
handleChange(dateValue);
|
||||
}
|
||||
}}
|
||||
placeholder={t("general.labels.date")}
|
||||
onBlur={handleBlur}
|
||||
disabledDate={handleDisabledDate}
|
||||
@@ -105,13 +122,25 @@ const DateTimePicker = ({
|
||||
<TimePicker
|
||||
format="hh:mm a"
|
||||
minuteStep={15}
|
||||
value={value && dayjs(value).hour() === 0 && dayjs(value).minute() === 0 ? null : dayjs(value)}
|
||||
defaultOpenValue={dayjs(value)
|
||||
.hour(dayjs().hour())
|
||||
.minute(Math.floor(dayjs().minute() / 15) * 15)
|
||||
.second(0)}
|
||||
onChange={(value) => {
|
||||
handleChange(value);
|
||||
onBlur();
|
||||
onChange={(timeValue) => {
|
||||
if (timeValue) {
|
||||
// When time changes, combine it with the existing date
|
||||
const existingDate = dayjs(value);
|
||||
const newDateTime = existingDate
|
||||
.hour(timeValue.hour())
|
||||
.minute(timeValue.minute())
|
||||
.second(0);
|
||||
handleChange(newDateTime);
|
||||
} else {
|
||||
// If time is cleared, just update with null time but keep date
|
||||
handleChange(timeValue);
|
||||
}
|
||||
if (onBlur) onBlur();
|
||||
}}
|
||||
placeholder={t("general.labels.time")}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const LaborTypeFormItem = ({ value, onChange }, ref) => {
|
||||
const LaborTypeFormItem = ({ value }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!value) return null;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const PartTypeFormItem = ({ value, onChange }, ref) => {
|
||||
const PartTypeFormItem = ({ value }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!value) return null;
|
||||
|
||||
return <div>{t(`joblines.fields.part_types.${value}`)}</div>;
|
||||
return (
|
||||
<div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{t(`joblines.fields.part_types.${value}`)}</div>
|
||||
);
|
||||
};
|
||||
export default forwardRef(PartTypeFormItem);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Dinero from "dinero.js";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
import { forwardRef } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
@@ -8,23 +7,24 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
const ReadOnlyFormItem = ({ bodyshop, value, type = "text", onChange }, ref) => {
|
||||
const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
|
||||
if (!value) return null;
|
||||
switch (type) {
|
||||
case "employee":
|
||||
case "employee": {
|
||||
const emp = bodyshop.employees.find((e) => e.id === value);
|
||||
return `${emp?.first_name} ${emp?.last_name}`;
|
||||
}
|
||||
|
||||
case "text":
|
||||
return <div>{value}</div>;
|
||||
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||
case "currency":
|
||||
return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>;
|
||||
default:
|
||||
return <div>{value}</div>;
|
||||
return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -385,7 +385,9 @@ export function ScheduleEventComponent({
|
||||
previousEvent: event.id,
|
||||
color: event.color,
|
||||
alt_transport: event.job && event.job.alt_transport,
|
||||
note: event.note
|
||||
note: event.note,
|
||||
scheduled_in: event.job && event.job.scheduled_in,
|
||||
scheduled_completion: event.job && event.job.scheduled_completion
|
||||
}
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { PushpinFilled, PushpinOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { UPDATE_NOTE } from "../../graphql/notes.queries";
|
||||
|
||||
function JobNotesPinToggle({ note }) {
|
||||
const [updateNote] = useMutation(UPDATE_NOTE);
|
||||
|
||||
const handlePinToggle = () => {
|
||||
updateNote({
|
||||
variables: {
|
||||
noteId: note.id,
|
||||
note: { pinned: !note.pinned }
|
||||
},
|
||||
refetchQueries: ["GET_JOB_BY_PK", "QUERY_JOB_CARD_DETAILS", "QUERY_PARTS_QUEUE_CARD_DETAILS"]
|
||||
});
|
||||
};
|
||||
|
||||
return note.pinned ? (
|
||||
<PushpinFilled size="large" onClick={handlePinToggle} style={{ color: "gold" }} />
|
||||
) : (
|
||||
<PushpinOutlined size="large" onClick={handlePinToggle} />
|
||||
);
|
||||
}
|
||||
|
||||
export default JobNotesPinToggle;
|
||||
@@ -1,16 +1,16 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Dropdown } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -24,7 +24,6 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [availableStatuses, setAvailableStatuses] = useState([]);
|
||||
const [otherStages, setOtherStages] = useState([]);
|
||||
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
||||
const notification = useNotification();
|
||||
|
||||
@@ -32,7 +31,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
mutationUpdateJobstatus({
|
||||
variables: { jobId: job.id, status: status }
|
||||
})
|
||||
.then((r) => {
|
||||
.then(() => {
|
||||
notification["success"]({ message: t("jobs.successes.save") });
|
||||
insertAuditTrail({
|
||||
jobid: job.id,
|
||||
@@ -41,7 +40,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
});
|
||||
// refetch();
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
notification["error"]({ message: t("jobs.errors.saving") });
|
||||
});
|
||||
};
|
||||
@@ -51,19 +50,14 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
if (job && bodyshop) {
|
||||
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) {
|
||||
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses);
|
||||
if (bodyshop.md_ro_statuses.production_statuses[0])
|
||||
setOtherStages([bodyshop.md_ro_statuses.production_statuses[0]]);
|
||||
} else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) {
|
||||
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses);
|
||||
setOtherStages([bodyshop.md_ro_statuses.default_imported, bodyshop.md_ro_statuses.default_delivered]);
|
||||
} else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) {
|
||||
setAvailableStatuses(
|
||||
bodyshop.md_ro_statuses.post_production_statuses.filter(
|
||||
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported
|
||||
)
|
||||
);
|
||||
if (bodyshop.md_ro_statuses.production_statuses[0])
|
||||
setOtherStages([bodyshop.md_ro_statuses.production_statuses[0]]);
|
||||
} else {
|
||||
console.log("Status didn't match any restrictions. Allowing all status changes.");
|
||||
setAvailableStatuses(bodyshop.md_ro_statuses.statuses);
|
||||
@@ -76,16 +70,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
...availableStatuses.map((item) => ({
|
||||
key: item,
|
||||
label: item
|
||||
})),
|
||||
...(job.converted
|
||||
? [
|
||||
{ type: "divider" },
|
||||
...otherStages.map((item) => ({
|
||||
key: item,
|
||||
label: item
|
||||
}))
|
||||
]
|
||||
: [])
|
||||
}))
|
||||
],
|
||||
onClick: (e) => updateJobStatus(e.key)
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { WarningOutlined } from "@ant-design/icons";
|
||||
import { Form, Select, Space, Tooltip } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -8,14 +8,13 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
|
||||
import PartTypeFormItem from "../form-items-formatted/part-type-form-item.component";
|
||||
import ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
|
||||
import { WarningOutlined } from "@ant-design/icons";
|
||||
import "./jobs-close-lines.styles.scss";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -24,7 +23,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
||||
return (
|
||||
<div>
|
||||
<Form.List name={["joblines"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
{(fields) => {
|
||||
return (
|
||||
<table className="jobs-close-table">
|
||||
<thead>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Form, Statistic, Tooltip } from "antd";
|
||||
import React, { useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import FormRow from "../layout-form-row/layout-form-row.component";
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -43,14 +43,14 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.estimate_sent_approval")} name="estimate_sent_approval">
|
||||
<DateTimePicker
|
||||
disabled={true}
|
||||
disabled={jobRO}
|
||||
value={job.estimate_sent_approval ? dayjs(job.estimate_sent_approval) : null}
|
||||
placeholder={t("general.labels.na")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.estimate_approved")} name="estimate_approved">
|
||||
<DateTimePicker
|
||||
disabled={true}
|
||||
disabled={jobRO}
|
||||
value={job.estimate_approved ? dayjs(job.estimate_approved) : null}
|
||||
placeholder={t("general.labels.na")}
|
||||
/>
|
||||
@@ -63,7 +63,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
</Form.Item>
|
||||
<Tooltip title={t("jobs.labels.scheduledinchange")}>
|
||||
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
</Tooltip>
|
||||
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
||||
@@ -110,16 +110,16 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
||||
</FormRow>
|
||||
<FormRow header={t("jobs.forms.admindates")}>
|
||||
<Form.Item label={t("jobs.fields.date_invoiced")} name="date_invoiced">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.date_exported")} name="date_exported">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.date_void")} name="date_void">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.date_lost_sale")} name="date_lost_sale">
|
||||
<DateTimePicker disabled={true || jobRO} />
|
||||
<DateTimePicker disabled={true} />
|
||||
</Form.Item>
|
||||
</FormRow>
|
||||
</div>
|
||||
|
||||
@@ -673,7 +673,9 @@ export function JobsDetailHeaderActions({
|
||||
context: {
|
||||
jobId: job.id,
|
||||
job: job,
|
||||
alt_transport: job.alt_transport
|
||||
alt_transport: job.alt_transport,
|
||||
scheduled_in: job.scheduled_in,
|
||||
scheduled_completion: job.scheduled_completion
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1090,11 +1092,7 @@ export function JobsDetailHeaderActions({
|
||||
{t("menus.jobsactions.deletejob")}
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deletewatchers")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
showCancel={false}
|
||||
>
|
||||
<Popconfirm title={t("jobs.labels.deletewatchers")} onClick={(e) => e.stopPropagation()} showCancel={false}>
|
||||
{t("menus.jobsactions.deletejob")}
|
||||
</Popconfirm>
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import JobAltTransportChange from "../job-at-change/job-at-change.component";
|
||||
import JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
|
||||
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import PinnedJobNotes from "../pinned-job-notes/pinned-job-notes.component.jsx";
|
||||
import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
|
||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
@@ -102,254 +103,257 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail })
|
||||
};
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
||||
<Col {...colSpan}>
|
||||
<Card title={"Job Status"} style={{ height: "100%" }}>
|
||||
<div>
|
||||
<DataLabel label={t("jobs.fields.status")}>
|
||||
<>
|
||||
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
|
||||
<Col {...colSpan}>
|
||||
<Card title={"Job Status"} style={{ height: "100%" }}>
|
||||
<div>
|
||||
<DataLabel label={t("jobs.fields.status")}>
|
||||
<Space wrap>
|
||||
{job.status}
|
||||
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
|
||||
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{job.iouparent && (
|
||||
<Link to={`/manage/jobs/${job.iouparent}`}>
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
</Link>
|
||||
)}
|
||||
{job.production_vars && job.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{job.status === bodyshop.md_ro_statuses.default_scheduled && job.scheduled_in ? (
|
||||
<Tag>
|
||||
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format("YYYY-MM-DD")}`}>
|
||||
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
|
||||
</Link>
|
||||
</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<ProductionListColumnComment record={job} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
||||
{job.po_number}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||
{job.alt_transport}
|
||||
<JobAltTransportChange job={job} />
|
||||
</DataLabel>
|
||||
{job?.cccontracts?.length > 0 && (
|
||||
<DataLabel label={t("jobs.labels.contracts")}>
|
||||
{job.cccontracts.map((c, index) => (
|
||||
<Space key={c.id} wrap>
|
||||
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
|
||||
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
||||
{index !== job.cccontracts.length - 1 ? "," : null}
|
||||
</Link>
|
||||
</Space>
|
||||
))}
|
||||
</DataLabel>
|
||||
)}
|
||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||
<ProductionListColumnProductionNote record={job} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={!!job.estimate_sent_approval}
|
||||
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{job.estimate_sent_approval && (
|
||||
<span style={{ color: "#888" }}>
|
||||
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
|
||||
</span>
|
||||
)}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.estimate_approved")}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={!!job.estimate_approved}
|
||||
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{job.estimate_approved && (
|
||||
<span style={{ color: "#888" }}>
|
||||
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
|
||||
</span>
|
||||
)}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<Space wrap>
|
||||
{job.status}
|
||||
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>}
|
||||
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{job.iouparent && (
|
||||
<Link to={`/manage/jobs/${job.iouparent}`}>
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
</Link>
|
||||
)}
|
||||
{job.production_vars && job.production_vars.alert ? (
|
||||
<ExclamationCircleFilled className="production-alert" />
|
||||
) : null}
|
||||
{job.status === bodyshop.md_ro_statuses.default_scheduled && job.scheduled_in ? (
|
||||
<Tag>
|
||||
<Link to={`/manage/schedule?date=${dayjs(job.scheduled_in).format("YYYY-MM-DD")}`}>
|
||||
<DateTimeFormatter>{job.scheduled_in}</DateTimeFormatter>
|
||||
</Link>
|
||||
{job.special_coverage_policy && (
|
||||
<Tag color="tomato">
|
||||
<Space>
|
||||
<WarningFilled />
|
||||
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
|
||||
</Space>
|
||||
</Tag>
|
||||
) : null}
|
||||
)}
|
||||
{job.ca_gst_registrant && (
|
||||
<Tag color="geekblue">
|
||||
<Space>
|
||||
<WarningFilled />
|
||||
<span>{t("jobs.fields.ca_gst_registrant")}</span>
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
{job.hit_and_run && (
|
||||
<Tag color="green">
|
||||
<Space>
|
||||
<WarningFilled />
|
||||
<span>{t("jobs.fields.hit_and_run")}</span>
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<ProductionListColumnComment record={job} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
||||
{job.po_number}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||
{job.alt_transport}
|
||||
<JobAltTransportChange job={job} />
|
||||
</DataLabel>
|
||||
{job?.cccontracts?.length > 0 && (
|
||||
<DataLabel label={t("jobs.labels.contracts")}>
|
||||
{job.cccontracts.map((c, index) => (
|
||||
<Space key={c.id} wrap>
|
||||
<Link to={`/manage/courtesycars/contracts/${c.id}`}>
|
||||
{`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
|
||||
{index !== job.cccontracts.length - 1 ? "," : null}
|
||||
</Link>
|
||||
</Space>
|
||||
))}
|
||||
</DataLabel>
|
||||
)}
|
||||
<DataLabel label={t("jobs.fields.production_vars.note")}>
|
||||
<ProductionListColumnProductionNote record={job} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.estimate_sent_approval")}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={!!job.estimate_sent_approval}
|
||||
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{job.estimate_sent_approval && (
|
||||
<span style={{ color: "#888" }}>
|
||||
<DateTimeFormatter>{job.estimate_sent_approval}</DateTimeFormatter>
|
||||
</span>
|
||||
)}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.estimate_approved")}>
|
||||
<Space>
|
||||
<Checkbox
|
||||
checked={!!job.estimate_approved}
|
||||
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{job.estimate_approved && (
|
||||
<span style={{ color: "#888" }}>
|
||||
<DateTimeFormatter>{job.estimate_approved}</DateTimeFormatter>
|
||||
</span>
|
||||
)}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
</DataLabel>
|
||||
<Space wrap>
|
||||
{job.special_coverage_policy && (
|
||||
<Tag color="tomato">
|
||||
<Space>
|
||||
<WarningFilled />
|
||||
<span>{t("jobs.labels.specialcoveragepolicy")}</span>
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
{job.ca_gst_registrant && (
|
||||
<Tag color="geekblue">
|
||||
<Space>
|
||||
<WarningFilled />
|
||||
<span>{t("jobs.fields.ca_gst_registrant")}</span>
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
{job.hit_and_run && (
|
||||
<Tag color="green">
|
||||
<Space>
|
||||
<WarningFilled />
|
||||
<span>{t("jobs.fields.hit_and_run")}</span>
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col {...colSpan}>
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
disabled ? (
|
||||
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
|
||||
) : (
|
||||
<Link to={`/manage/owners/${job.owner.id}`}>
|
||||
{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||
{disabled ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||
{disabled ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel key="3" label={t("owners.fields.address")}>
|
||||
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
|
||||
job.ownr_city || ""
|
||||
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||
{disabled ? (
|
||||
<>{job.ownr_ea || ""}</>
|
||||
) : job.ownr_ea ? (
|
||||
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
||||
) : null}
|
||||
</DataLabel>
|
||||
{job.owner?.tax_number && (
|
||||
<DataLabel key="5" label={t("owners.fields.tax_number")}>
|
||||
{job.owner?.tax_number || ""}
|
||||
</DataLabel>
|
||||
)}
|
||||
<DataLabel label={t("owners.fields.note")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{job.owner?.note || ""}
|
||||
</DataLabel>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col {...colSpan}>
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
job.vehicle ? (
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col {...colSpan}>
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
disabled ? (
|
||||
<>{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")} </>
|
||||
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
|
||||
) : (
|
||||
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
|
||||
{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")}
|
||||
<Link to={`/manage/owners/${job.owner.id}`}>
|
||||
{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<span></span>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<DataLabel key="2" label={t("vehicles.fields.plate_no")}>
|
||||
{`${job.plate_no || t("general.labels.na")} (${`${job.plate_st || t("general.labels.na")}`})`}
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
|
||||
<VehicleVinDisplay>{`${job.v_vin || t("general.labels.na")}`}</VehicleVinDisplay>
|
||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||
job.v_vin?.length !== 17 ? (
|
||||
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} />
|
||||
) : null
|
||||
) : null}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.regie_number")}>{job.regie_number || t("general.labels.na")}</DataLabel>
|
||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
||||
</DataLabel>
|
||||
{job.vehicle && job.vehicle.notes && (
|
||||
<DataLabel
|
||||
label={t("vehicles.fields.notes")}
|
||||
valueStyle={{ whiteSpace: "pre-wrap" }}
|
||||
valueClassName={notesClamped ? "clamp" : ""}
|
||||
onValueClick={() => setNotesClamped(!notesClamped)}
|
||||
>
|
||||
{job.vehicle.notes}
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||
{disabled ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
)}
|
||||
{job.vehicle && job.vehicle.v_paint_codes && (
|
||||
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
|
||||
<span style={{ whiteSpace: "pre" }}>
|
||||
{Object.keys(job.vehicle.v_paint_codes)
|
||||
.filter(
|
||||
(key) =>
|
||||
job.vehicle.v_paint_codes[key] !== "" &&
|
||||
job.vehicle.v_paint_codes[key] !== null &&
|
||||
job.vehicle.v_paint_codes[key] !== undefined
|
||||
)
|
||||
.map((key, idx) => (
|
||||
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
|
||||
))}
|
||||
</span>
|
||||
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||
{disabled ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col {...colSpan}>
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>}
|
||||
id={"job-employee-assignments"}
|
||||
>
|
||||
<div>
|
||||
<JobEmployeeAssignments job={job} />
|
||||
<Divider style={{ margin: ".5rem" }} />
|
||||
<DataLabel label={t("jobs.labels.labor_hrs")}>
|
||||
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} / {(bodyHrs + refinishHrs).toFixed(1)}
|
||||
</DataLabel>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<DataLabel key="3" label={t("owners.fields.address")}>
|
||||
{`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
|
||||
job.ownr_city || ""
|
||||
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||
{disabled ? (
|
||||
<>{job.ownr_ea || ""}</>
|
||||
) : job.ownr_ea ? (
|
||||
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
||||
) : null}
|
||||
</DataLabel>
|
||||
{job.owner?.tax_number && (
|
||||
<DataLabel key="5" label={t("owners.fields.tax_number")}>
|
||||
{job.owner?.tax_number || ""}
|
||||
</DataLabel>
|
||||
)}
|
||||
<DataLabel label={t("owners.fields.note")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
{job.owner?.note || ""}
|
||||
</DataLabel>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col {...colSpan}>
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
job.vehicle ? (
|
||||
disabled ? (
|
||||
<>{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")} </>
|
||||
) : (
|
||||
<Link to={job.vehicle && `/manage/vehicles/${job.vehicle.id}`}>
|
||||
{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<span></span>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<DataLabel key="2" label={t("vehicles.fields.plate_no")}>
|
||||
{`${job.plate_no || t("general.labels.na")} (${`${job.plate_st || t("general.labels.na")}`})`}
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
|
||||
<VehicleVinDisplay>{`${job.v_vin || t("general.labels.na")}`}</VehicleVinDisplay>
|
||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||
job.v_vin?.length !== 17 ? (
|
||||
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} />
|
||||
) : null
|
||||
) : null}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.regie_number")}>{job.regie_number || t("general.labels.na")}</DataLabel>
|
||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
||||
</DataLabel>
|
||||
{job.vehicle && job.vehicle.notes && (
|
||||
<DataLabel
|
||||
label={t("vehicles.fields.notes")}
|
||||
valueStyle={{ whiteSpace: "pre-wrap" }}
|
||||
valueClassName={notesClamped ? "clamp" : ""}
|
||||
onValueClick={() => setNotesClamped(!notesClamped)}
|
||||
>
|
||||
{job.vehicle.notes}
|
||||
</DataLabel>
|
||||
)}
|
||||
{job.vehicle && job.vehicle.v_paint_codes && (
|
||||
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
|
||||
<span style={{ whiteSpace: "pre" }}>
|
||||
{Object.keys(job.vehicle.v_paint_codes)
|
||||
.filter(
|
||||
(key) =>
|
||||
job.vehicle.v_paint_codes[key] !== "" &&
|
||||
job.vehicle.v_paint_codes[key] !== null &&
|
||||
job.vehicle.v_paint_codes[key] !== undefined
|
||||
)
|
||||
.map((key, idx) => (
|
||||
<Tag key={idx}>{job.vehicle.v_paint_codes[key]}</Tag>
|
||||
))}
|
||||
</span>
|
||||
</DataLabel>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col {...colSpan}>
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>}
|
||||
id={"job-employee-assignments"}
|
||||
>
|
||||
<div>
|
||||
<JobEmployeeAssignments job={job} />
|
||||
<Divider style={{ margin: ".5rem" }} />
|
||||
<DataLabel label={t("jobs.labels.labor_hrs")}>
|
||||
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} / {(bodyHrs + refinishHrs).toFixed(1)}
|
||||
</DataLabel>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<PinnedJobNotes job={job} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, Card, Input, Space, Table, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
@@ -20,7 +20,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
@@ -203,6 +203,8 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
|
||||
return (
|
||||
<Card
|
||||
id="all-jobs-list"
|
||||
title={t("titles.bc.jobs-all")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{search.search && (
|
||||
@@ -256,6 +258,7 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
rowKey="id"
|
||||
dataSource={search?.search ? openSearchResults : jobs}
|
||||
onChange={handleTableChange}
|
||||
id="all-jobs-list-table"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, SyncOut
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Card, Grid, Input, Space, Table, Tooltip } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
@@ -22,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export function JobsList({ bodyshop }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
@@ -342,13 +342,14 @@ export function JobsList({ bodyshop }) {
|
||||
type: "radio"
|
||||
}}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record, rowIndex) => {
|
||||
onRow={(record) => {
|
||||
return {
|
||||
onClick: (event) => {
|
||||
onClick: () => {
|
||||
handleOnRowClick(record);
|
||||
}
|
||||
};
|
||||
}}
|
||||
id="active-jobs-list-table"
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
import JobNotesPinToggle from "../job-notes-pin-toggle/job-notes-pin-toggle.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly
|
||||
@@ -47,6 +48,9 @@ export function JobNotesComponent({
|
||||
key: "icons",
|
||||
width: 80,
|
||||
filteredValue: filter?.icons || null,
|
||||
defaultSortOrder: "desc",
|
||||
multiple: 1,
|
||||
sorter: (a, b) => a.pinned - b.pinned,
|
||||
filters: [
|
||||
{
|
||||
text: t("notes.labels.usernotes"),
|
||||
@@ -63,6 +67,7 @@ export function JobNotesComponent({
|
||||
{record.critical ? <WarningFilled style={{ margin: 4, color: "red" }} /> : null}
|
||||
{record.private ? <EyeInvisibleFilled style={{ margin: 4 }} /> : null}
|
||||
{record.audit ? <AuditOutlined style={{ margin: 4 }} /> : null}
|
||||
<JobNotesPinToggle note={record} />
|
||||
</span>
|
||||
)
|
||||
},
|
||||
@@ -100,6 +105,7 @@ export function JobNotesComponent({
|
||||
dataIndex: "updated_at",
|
||||
key: "updated_at",
|
||||
defaultSortOrder: "descend",
|
||||
multiple: 2,
|
||||
width: 200,
|
||||
sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at),
|
||||
render: (text, record) => <DateTimeFormatter>{record.updated_at}</DateTimeFormatter>
|
||||
|
||||
@@ -23,17 +23,22 @@ export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
|
||||
return (
|
||||
<>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={8}>
|
||||
<Col span={6}>
|
||||
<Form.Item label={t("notes.fields.critical")} name="critical" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Col span={6}>
|
||||
<Form.Item label={t("notes.fields.private")} name="private" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Col span={6}>
|
||||
<Form.Item label={t("notes.fields.pinned")} name="pinned" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Form.Item label={t("notes.fields.type")} name="type" initialValue="general">
|
||||
<Select
|
||||
options={[
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { Form, Modal } from "antd";
|
||||
import React, { useEffect } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { GET_JOB_BY_PK } from "../../graphql/jobs.queries.js";
|
||||
import { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
@@ -12,7 +14,6 @@ import { selectNoteUpsert } from "../../redux/modals/modals.selectors";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import NoteUpsertModalComponent from "./note-upsert-modal.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -42,6 +43,8 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { client } = useApolloClient();
|
||||
|
||||
useEffect(() => {
|
||||
//Required to prevent infinite looping.
|
||||
if (existingNote && open) {
|
||||
@@ -65,8 +68,9 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
||||
variables: {
|
||||
noteId: existingNote.id,
|
||||
note: values
|
||||
}
|
||||
}).then((r) => {
|
||||
},
|
||||
refetchQueries: ["GET_JOB_BY_PK", "QUERY_JOB_CARD_DETAILS", "QUERY_PARTS_QUEUE_CARD_DETAILS"]
|
||||
}).then(() => {
|
||||
notification["success"]({
|
||||
message: t("notes.successes.updated")
|
||||
});
|
||||
@@ -86,6 +90,33 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
||||
variables: {
|
||||
noteInput: [{ ...values, jobid: jobId, created_by: currentUser.email }]
|
||||
},
|
||||
update(cache, { data: { updateNote: updatedNote } }) {
|
||||
try {
|
||||
const existingJob = cache.readQuery({
|
||||
query: GET_JOB_BY_PK,
|
||||
variables: { id: jobId }
|
||||
});
|
||||
|
||||
if (existingJob) {
|
||||
cache.writeQuery({
|
||||
query: GET_JOB_BY_PK,
|
||||
variables: { id: jobId },
|
||||
data: {
|
||||
...existingJob,
|
||||
job: {
|
||||
...existingJob.job,
|
||||
notes: updatedNote.pinned
|
||||
? [updatedNote, ...existingJob.job.notes]
|
||||
: existingJob.job.notes.filter((n) => n.id !== updatedNote.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// Cache miss is okay, query hasn't been executed yet
|
||||
console.log("Cache miss for GET_JOB_BY_PK");
|
||||
}
|
||||
},
|
||||
refetchQueries: ["QUERY_NOTES_BY_JOB_PK"]
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Card, Divider, Space } from "antd";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import JobNotesPinToggle from "../job-notes-pin-toggle/job-notes-pin-toggle.component";
|
||||
|
||||
function PinnedJobNotes({ job }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pinnedNotes = useMemo(() => {
|
||||
return job?.notes?.filter((note) => note.pinned); //This will be typically filtered, but adding this to maximize flexibility of the component.
|
||||
}, [job.notes]);
|
||||
|
||||
return pinnedNotes?.length ? (
|
||||
<>
|
||||
<Divider />
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
{pinnedNotes?.map((note) => (
|
||||
<Card
|
||||
key={note.id}
|
||||
title={`${t("notes.labels.pinned_note")} - ${t(`notes.fields.types.${note.type}`)}`}
|
||||
extra={<JobNotesPinToggle note={note} />}
|
||||
>
|
||||
{note.text}
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
export default PinnedJobNotes;
|
||||
@@ -59,6 +59,7 @@ const ret = {
|
||||
"shop:dashboard": 3,
|
||||
"shop:rbac": 5,
|
||||
"shop:reportcenter": 2,
|
||||
"shop:responsibilitycenter": 4, // Updated from "shop:responsibility" to "shop:responsibilitycenter"
|
||||
"shop:templates": 4,
|
||||
"shop:vendors": 2,
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Form, Modal } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import {
|
||||
CANCEL_APPOINTMENT_BY_ID,
|
||||
@@ -19,9 +19,9 @@ import { selectSchedule } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { DateTimeFormat } from "../../utils/DateFormatter";
|
||||
import dayjs from "../../utils/day";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import ScheduleJobModalComponent from "./schedule-job-modal.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -72,7 +72,7 @@ export function ScheduleJobModalContainer({
|
||||
variables: { jobid: jobId },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
skip: !open || !!!jobId
|
||||
skip: !open || !jobId
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -93,12 +93,12 @@ export function ScheduleJobModalContainer({
|
||||
logImEXEvent("schedule_new_appointment");
|
||||
|
||||
setLoading(true);
|
||||
if (!!previousEvent) {
|
||||
if (previousEvent) {
|
||||
const cancelAppt = await cancelAppointment({
|
||||
variables: { appid: previousEvent }
|
||||
});
|
||||
|
||||
if (!!cancelAppt.errors) {
|
||||
if (cancelAppt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.canceling", {
|
||||
message: JSON.stringify(cancelAppt.errors)
|
||||
@@ -146,7 +146,7 @@ export function ScheduleJobModalContainer({
|
||||
});
|
||||
}
|
||||
|
||||
if (!!appt.errors) {
|
||||
if (appt.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(appt.errors)
|
||||
@@ -172,7 +172,7 @@ export function ScheduleJobModalContainer({
|
||||
}
|
||||
});
|
||||
|
||||
if (!!jobUpdate.errors) {
|
||||
if (jobUpdate.errors) {
|
||||
notification["error"]({
|
||||
message: t("appointments.errors.saving", {
|
||||
message: JSON.stringify(jobUpdate.errors)
|
||||
@@ -222,9 +222,9 @@ export function ScheduleJobModalContainer({
|
||||
initialValues={{
|
||||
notifyCustomer: !!(job && job.ownr_ea),
|
||||
email: (job && job.ownr_ea) || "",
|
||||
start: null,
|
||||
// smartDates: [],
|
||||
scheduled_completion: null,
|
||||
start: context.scheduled_in,
|
||||
scheduled_completion: context.scheduled_completion ,
|
||||
color: context.color,
|
||||
alt_transport: context.alt_transport,
|
||||
note: context.note
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Form } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries";
|
||||
import dayjs from "../../utils/day";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||
import ShopInfoComponent from "./shop-info.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { FEATURE_CONFIGS, useFormDataPreservation } from "./useFormDataPreservation";
|
||||
|
||||
export default function ShopInfoContainer() {
|
||||
const [form] = Form.useForm();
|
||||
@@ -22,16 +23,24 @@ export default function ShopInfoContainer() {
|
||||
});
|
||||
const notification = useNotification();
|
||||
|
||||
const handleFinish = (values) => {
|
||||
const combinedFeatureConfig = {
|
||||
...FEATURE_CONFIGS.general,
|
||||
...FEATURE_CONFIGS.responsibilitycenters
|
||||
};
|
||||
|
||||
// Use form data preservation for all shop-info features
|
||||
const { createSubmissionHandler } = useFormDataPreservation(form, data?.bodyshops[0], combinedFeatureConfig);
|
||||
|
||||
const handleFinish = createSubmissionHandler((values) => {
|
||||
setSaveLoading(true);
|
||||
logImEXEvent("shop_update");
|
||||
|
||||
updateBodyshop({
|
||||
variables: { id: data.bodyshops[0].id, shop: values }
|
||||
})
|
||||
.then((r) => {
|
||||
.then(() => {
|
||||
notification["success"]({ message: t("bodyshop.successes.save") });
|
||||
refetch().then((_) => form.resetFields());
|
||||
refetch().then(() => form.resetFields());
|
||||
})
|
||||
.catch((error) => {
|
||||
notification["error"]({
|
||||
@@ -39,7 +48,7 @@ export default function ShopInfoContainer() {
|
||||
});
|
||||
});
|
||||
setSaveLoading(false);
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) form.resetFields();
|
||||
|
||||
@@ -145,124 +145,168 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("bodyshop.labels.accountingsetup")} id="accountingsetup">
|
||||
{HasFeatureAccess({ featureName: "export", bodyshop }) && (
|
||||
<>
|
||||
<Form.Item label={t("bodyshop.labels.qbo")} valuePropName="checked" name={["accountingconfig", "qbo"]}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.qbo_usa")}
|
||||
shouldUpdate
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "qbo_usa"]}
|
||||
>
|
||||
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.labels.qbo_departmentid")} name={["accountingconfig", "qbo_departmentid"]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.accountingtiers")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "tiers"]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={2}>2</Radio>
|
||||
<Radio value={3}>3</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.2tiersetup")}
|
||||
shouldUpdate
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "twotierpref"]}
|
||||
>
|
||||
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.printlater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "printlater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.emaillater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "emaillater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.inhousevendorid")}
|
||||
name={"inhousevendorid"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.default_adjustment_rate")}
|
||||
name={"default_adjustment_rate"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{HasFeatureAccess({ featureName: "bills", bodyshop }) && (
|
||||
<>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
{[
|
||||
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
||||
? [
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||
key="qbo"
|
||||
label={t("bodyshop.labels.qbo")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "qbo"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item key="qbo_usa_wrapper" shouldUpdate noStyle>
|
||||
{() => (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.qbo_usa")}
|
||||
shouldUpdate
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "qbo_usa"]}
|
||||
>
|
||||
<Switch disabled={!form.getFieldValue(["accountingconfig", "qbo"])} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
)
|
||||
}),
|
||||
<Form.Item
|
||||
key="qbo_departmentid"
|
||||
label={t("bodyshop.labels.qbo_departmentid")}
|
||||
name={["accountingconfig", "qbo_departmentid"]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="accountingtiers"
|
||||
label={t("bodyshop.labels.accountingtiers")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "tiers"]}
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value={2}>2</Radio>
|
||||
<Radio value={3}>3</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>,
|
||||
<Form.Item key="twotierpref_wrapper" shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.2tiersetup")}
|
||||
shouldUpdate
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["accountingconfig", "tiers"]) === 2
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
name={["accountingconfig", "twotierpref"]}
|
||||
>
|
||||
<Radio.Group disabled={form.getFieldValue(["accountingconfig", "tiers"]) === 3}>
|
||||
<Radio value="name">{t("bodyshop.labels.2tiername")}</Radio>
|
||||
<Radio value="source">{t("bodyshop.labels.2tiersource")}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="printlater"
|
||||
label={t("bodyshop.labels.printlater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "printlater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="emaillater"
|
||||
label={t("bodyshop.labels.emaillater")}
|
||||
valuePropName="checked"
|
||||
name={["accountingconfig", "emaillater"]}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
]
|
||||
: []),
|
||||
<Form.Item
|
||||
key="inhousevendorid"
|
||||
label={t("bodyshop.fields.inhousevendorid")}
|
||||
name={"inhousevendorid"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="default_adjustment_rate"
|
||||
label={t("bodyshop.fields.default_adjustment_rate")}
|
||||
name={"default_adjustment_rate"}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} precision={2} />
|
||||
</Form.Item>,
|
||||
InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item key="federal_tax_id" label={t("bodyshop.fields.federal_tax_id")} name="federal_tax_id">
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)
|
||||
}),
|
||||
<Form.Item key="state_tax_id" label={t("bodyshop.fields.state_tax_id")} name="state_tax_id">
|
||||
<Input />
|
||||
</Form.Item>,
|
||||
...(HasFeatureAccess({ featureName: "bills", bodyshop })
|
||||
? [
|
||||
InstanceRenderManager({
|
||||
imex: (
|
||||
<Form.Item
|
||||
key="invoice_federal_tax_rate"
|
||||
label={t("bodyshop.fields.invoice_federal_tax_rate")}
|
||||
name={["bill_tax_rates", "federal_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)
|
||||
}),
|
||||
<Form.Item
|
||||
key="invoice_state_tax_rate"
|
||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||
name={["bill_tax_rates", "state_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="invoice_local_tax_rate"
|
||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||
name={["bill_tax_rates", "local_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
@@ -272,117 +316,118 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
)
|
||||
})}
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_state_tax_rate")}
|
||||
name={["bill_tax_rates", "state_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.invoice_local_tax_rate")}
|
||||
name={["bill_tax_rates", "local_tax_rate"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item
|
||||
name={["md_payment_types"]}
|
||||
label={t("bodyshop.fields.md_payment_types")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_categories"]}
|
||||
label={t("bodyshop.fields.md_categories")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
{HasFeatureAccess({ featureName: "export", bodyshop }) && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_classes"]}
|
||||
label={t("bodyshop.fields.md_classes")}
|
||||
rules={[
|
||||
({ getFieldValue }) => {
|
||||
return {
|
||||
required: getFieldValue("enforce_class"),
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
};
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item name={["enforce_class"]} label={t("bodyshop.fields.enforce_class")} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{ClosingPeriod.treatment === "on" && (
|
||||
<Form.Item
|
||||
name={["accountingconfig", "ClosingPeriod"]}
|
||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||
>
|
||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{ADPPayroll.treatment === "on" && (
|
||||
<Form.Item name={["accountingconfig", "companyCode"]} label={t("bodyshop.fields.companycode")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
{ADPPayroll.treatment === "on" && (
|
||||
<Form.Item name={["accountingconfig", "batchID"]} label={t("bodyshop.fields.batchid")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
]
|
||||
: []),
|
||||
<Form.Item
|
||||
key="md_payment_types"
|
||||
name={["md_payment_types"]}
|
||||
label={t("bodyshop.fields.md_payment_types")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="md_categories"
|
||||
name={["md_categories"]}
|
||||
label={t("bodyshop.fields.md_categories")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>,
|
||||
...(HasFeatureAccess({ featureName: "export", bodyshop })
|
||||
? [
|
||||
<Form.Item
|
||||
key="ReceivableCustomField1"
|
||||
name={["accountingconfig", "ReceivableCustomField1"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 1 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="ReceivableCustomField2"
|
||||
name={["accountingconfig", "ReceivableCustomField2"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 2 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="ReceivableCustomField3"
|
||||
name={["accountingconfig", "ReceivableCustomField3"]}
|
||||
label={t("bodyshop.fields.ReceivableCustomField", { number: 3 })}
|
||||
>
|
||||
{ReceivableCustomFieldSelect}
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="md_classes"
|
||||
name={["md_classes"]}
|
||||
label={t("bodyshop.fields.md_classes")}
|
||||
rules={[
|
||||
({ getFieldValue }) => {
|
||||
return {
|
||||
required: getFieldValue("enforce_class"),
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
};
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="enforce_class"
|
||||
name={["enforce_class"]}
|
||||
label={t("bodyshop.fields.enforce_class")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
...(ClosingPeriod.treatment === "on"
|
||||
? [
|
||||
<Form.Item
|
||||
key="ClosingPeriod"
|
||||
name={["accountingconfig", "ClosingPeriod"]}
|
||||
label={t("bodyshop.fields.closingperiod")} //{t("reportcenter.labels.dates")}
|
||||
>
|
||||
<DatePicker.RangePicker format="MM/DD/YYYY" presets={DatePickerRanges} />
|
||||
</Form.Item>
|
||||
]
|
||||
: []),
|
||||
...(ADPPayroll.treatment === "on"
|
||||
? [
|
||||
<Form.Item
|
||||
key="companyCode"
|
||||
name={["accountingconfig", "companyCode"]}
|
||||
label={t("bodyshop.fields.companycode")}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
]
|
||||
: []),
|
||||
...(ADPPayroll.treatment === "on"
|
||||
? [
|
||||
<Form.Item
|
||||
key="batchID"
|
||||
name={["accountingconfig", "batchID"]}
|
||||
label={t("bodyshop.fields.batchid")}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
]
|
||||
: [])
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
</LayoutFormRow>
|
||||
<FeatureWrapper featureName="scoreboard" noauth={() => null}>
|
||||
<LayoutFormRow header={t("bodyshop.labels.scoreboardsetup")} id="scoreboardsetup">
|
||||
@@ -446,211 +491,255 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
</LayoutFormRow>
|
||||
</FeatureWrapper>
|
||||
<LayoutFormRow header={t("bodyshop.labels.systemsettings")} id="systemsettings">
|
||||
<Form.Item
|
||||
name={["md_referral_sources"]}
|
||||
label={t("bodyshop.fields.md_referral_sources")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item name={["enforce_referral"]} label={t("bodyshop.fields.enforce_referral")} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["enforce_conversion_csr"]}
|
||||
label={t("bodyshop.fields.enforce_conversion_csr")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["enforce_conversion_category"]}
|
||||
label={t("bodyshop.fields.enforce_conversion_category")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["target_touchtime"]}
|
||||
label={t("bodyshop.fields.target_touchtime")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0.1} precision={1} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.use_fippa")} name={["use_fippa"]} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_hour_split.prep")}
|
||||
name={["md_hour_split", "prep"]}
|
||||
dependencies={[["md_hour_split", "paint"]]}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!value && !getFieldValue(["md_hour_split", "paint"])) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (value + getFieldValue(["md_hour_split", "paint"]) === 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t("bodyshop.validation.larsplit"));
|
||||
{[
|
||||
<Form.Item
|
||||
key="md_referral_sources"
|
||||
name={["md_referral_sources"]}
|
||||
label={t("bodyshop.fields.md_referral_sources")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_hour_split.paint")}
|
||||
name={["md_hour_split", "paint"]}
|
||||
dependencies={[["md_hour_split", "prep"]]}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!value && !getFieldValue(["md_hour_split", "paint"])) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (value + getFieldValue(["md_hour_split", "prep"]) === 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t("bodyshop.validation.larsplit"));
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="enforce_referral"
|
||||
name={["enforce_referral"]}
|
||||
label={t("bodyshop.fields.enforce_referral")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="enforce_conversion_csr"
|
||||
name={["enforce_conversion_csr"]}
|
||||
label={t("bodyshop.fields.enforce_conversion_csr")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="enforce_conversion_category"
|
||||
name={["enforce_conversion_category"]}
|
||||
label={t("bodyshop.fields.enforce_conversion_category")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="target_touchtime"
|
||||
name={["target_touchtime"]}
|
||||
label={t("bodyshop.fields.target_touchtime")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.jc_hourly_rates.mapa")} name={["jc_hourly_rates", "mapa"]}>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("bodyshop.fields.jc_hourly_rates.mash")} name={["jc_hourly_rates", "mash"]}>
|
||||
<CurrencyInput />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["use_paint_scale_data"]}
|
||||
label={t("bodyshop.fields.use_paint_scale_data")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["attach_pdf_to_email"]}
|
||||
label={t("bodyshop.fields.attach_pdf_to_email")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_from_emails"]}
|
||||
label={t("bodyshop.fields.md_from_emails")}
|
||||
// rules={[
|
||||
// {
|
||||
// //message: t("general.validation.required"),
|
||||
// type: "array",
|
||||
// },
|
||||
// ]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_email_cc", "parts_order"]}
|
||||
label={t("bodyshop.fields.md_email_cc", { template: "parts_orders" })}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["md_email_cc", "parts_return_slip"]}
|
||||
label={t("bodyshop.fields.md_email_cc", { template: "parts_returns" })}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
|
||||
{HasFeatureAccess({ featureName: "timetickets", bodyshop }) && (
|
||||
<>
|
||||
<Form.Item
|
||||
name={["tt_allow_post_to_invoiced"]}
|
||||
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["tt_enforce_hours_for_tech_console"]}
|
||||
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["bill_allow_post_to_closed"]}
|
||||
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
<Form.Item
|
||||
name={["md_ded_notes"]}
|
||||
label={t("bodyshop.fields.md_ded_notes")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
||||
name={["md_functionality_toggles", "parts_queue_toggle"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={["last_name_first"]} label={t("bodyshop.fields.last_name_first")} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={["uselocalmediaserver"]}
|
||||
label={t("bodyshop.fields.uselocalmediaserver")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name={["localmediaserverhttp"]} label={t("bodyshop.fields.localmediaserverhttp")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={["localmediaservernetwork"]} label={t("bodyshop.fields.localmediaservernetwork")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name={["localmediatoken"]} label={t("bodyshop.fields.localmediatoken")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0.1} precision={1} />
|
||||
</Form.Item>,
|
||||
<Form.Item key="use_fippa" label={t("bodyshop.fields.use_fippa")} name={["use_fippa"]} valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="md_hour_split_prep"
|
||||
label={t("bodyshop.fields.md_hour_split.prep")}
|
||||
name={["md_hour_split", "prep"]}
|
||||
dependencies={[["md_hour_split", "paint"]]}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!value && !getFieldValue(["md_hour_split", "paint"])) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (value + getFieldValue(["md_hour_split", "paint"]) === 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t("bodyshop.validation.larsplit"));
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="md_hour_split_paint"
|
||||
label={t("bodyshop.fields.md_hour_split.paint")}
|
||||
name={["md_hour_split", "paint"]}
|
||||
dependencies={[["md_hour_split", "prep"]]}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(rule, value) {
|
||||
if (!value && !getFieldValue(["md_hour_split", "paint"])) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (value + getFieldValue(["md_hour_split", "prep"]) === 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(t("bodyshop.validation.larsplit"));
|
||||
}
|
||||
})
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={1} precision={2} />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="jc_hourly_rates_mapa"
|
||||
label={t("bodyshop.fields.jc_hourly_rates.mapa")}
|
||||
name={["jc_hourly_rates", "mapa"]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="jc_hourly_rates_mash"
|
||||
label={t("bodyshop.fields.jc_hourly_rates.mash")}
|
||||
name={["jc_hourly_rates", "mash"]}
|
||||
>
|
||||
<CurrencyInput />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="use_paint_scale_data"
|
||||
name={["use_paint_scale_data"]}
|
||||
label={t("bodyshop.fields.use_paint_scale_data")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="attach_pdf_to_email"
|
||||
name={["attach_pdf_to_email"]}
|
||||
label={t("bodyshop.fields.attach_pdf_to_email")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="md_from_emails"
|
||||
name={["md_from_emails"]}
|
||||
label={t("bodyshop.fields.md_from_emails")}
|
||||
// rules={[
|
||||
// {
|
||||
// //message: t("general.validation.required"),
|
||||
// type: "array",
|
||||
// },
|
||||
// ]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="md_email_cc_parts_order"
|
||||
name={["md_email_cc", "parts_order"]}
|
||||
label={t("bodyshop.fields.md_email_cc", { template: "parts_orders" })}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="md_email_cc_parts_return_slip"
|
||||
name={["md_email_cc", "parts_return_slip"]}
|
||||
label={t("bodyshop.fields.md_email_cc", { template: "parts_returns" })}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>,
|
||||
...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
|
||||
? [
|
||||
<Form.Item
|
||||
key="tt_allow_post_to_invoiced"
|
||||
name={["tt_allow_post_to_invoiced"]}
|
||||
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="tt_enforce_hours_for_tech_console"
|
||||
name={["tt_enforce_hours_for_tech_console"]}
|
||||
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="bill_allow_post_to_closed"
|
||||
name={["bill_allow_post_to_closed"]}
|
||||
label={t("bodyshop.fields.bill_allow_post_to_closed")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
]
|
||||
: []),
|
||||
<Form.Item
|
||||
key="md_ded_notes"
|
||||
name={["md_ded_notes"]}
|
||||
label={t("bodyshop.fields.md_ded_notes")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="parts_queue_toggle"
|
||||
label={t("bodyshop.fields.md_functionality_toggles.parts_queue_toggle")}
|
||||
name={["md_functionality_toggles", "parts_queue_toggle"]}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="last_name_first"
|
||||
name={["last_name_first"]}
|
||||
label={t("bodyshop.fields.last_name_first")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="uselocalmediaserver"
|
||||
name={["uselocalmediaserver"]}
|
||||
label={t("bodyshop.fields.uselocalmediaserver")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="localmediaserverhttp"
|
||||
name={["localmediaserverhttp"]}
|
||||
label={t("bodyshop.fields.localmediaserverhttp")}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>,
|
||||
<Form.Item
|
||||
key="localmediaservernetwork"
|
||||
name={["localmediaservernetwork"]}
|
||||
label={t("bodyshop.fields.localmediaservernetwork")}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>,
|
||||
<Form.Item key="localmediatoken" name={["localmediatoken"]} label={t("bodyshop.fields.localmediatoken")}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
]}
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("bodyshop.labels.shop_enabled_features")} id="sharing">
|
||||
<Form.Item
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
140
client/src/components/shop-info/useFormDataPreservation.js
Normal file
140
client/src/components/shop-info/useFormDataPreservation.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import { useEffect } from "react";
|
||||
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
/**
|
||||
* Custom hook to preserve form data for conditionally hidden fields based on feature access
|
||||
* @param {Object} form - Ant Design form instance
|
||||
* @param {Object} bodyshop - Bodyshop data for feature access checks (also contains existing database values)
|
||||
* @param {Object} featureConfig - Configuration object defining which features and their associated fields to preserve
|
||||
*/
|
||||
export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
|
||||
const getNestedValue = (obj, path) => {
|
||||
return path.reduce((current, key) => current?.[key], obj);
|
||||
};
|
||||
|
||||
const setNestedValue = (obj, path, value) => {
|
||||
const lastKey = path[path.length - 1];
|
||||
const parentPath = path.slice(0, -1);
|
||||
|
||||
const parent = parentPath.reduce((current, key) => {
|
||||
if (!current[key]) current[key] = {};
|
||||
return current[key];
|
||||
}, obj);
|
||||
|
||||
parent[lastKey] = value;
|
||||
};
|
||||
|
||||
const preserveHiddenFormData = () => {
|
||||
const preservationData = {};
|
||||
let hasDataToPreserve = false;
|
||||
|
||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||
|
||||
if (!hasAccess) {
|
||||
fieldPaths.forEach((fieldPath) => {
|
||||
const currentValues = form.getFieldsValue();
|
||||
let value = getNestedValue(currentValues, fieldPath);
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
value = getNestedValue(bodyshop, fieldPath);
|
||||
}
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
setNestedValue(preservationData, fieldPath, value);
|
||||
hasDataToPreserve = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (hasDataToPreserve) {
|
||||
form.setFieldsValue(preservationData);
|
||||
}
|
||||
};
|
||||
|
||||
const getCompleteFormValues = () => {
|
||||
const currentFormValues = form.getFieldsValue();
|
||||
const completeValues = { ...currentFormValues };
|
||||
|
||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||
|
||||
if (!hasAccess) {
|
||||
fieldPaths.forEach((fieldPath) => {
|
||||
let value = getNestedValue(currentFormValues, fieldPath);
|
||||
if (value === undefined || value === null) {
|
||||
value = getNestedValue(bodyshop, fieldPath);
|
||||
}
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
setNestedValue(completeValues, fieldPath, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return completeValues;
|
||||
};
|
||||
|
||||
const createSubmissionHandler = (originalHandler) => {
|
||||
return () => {
|
||||
const completeValues = getCompleteFormValues();
|
||||
|
||||
// Call the original handler with complete values including hidden data
|
||||
return originalHandler(completeValues);
|
||||
};
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
preserveHiddenFormData();
|
||||
}, [bodyshop]);
|
||||
|
||||
return { preserveHiddenFormData, getCompleteFormValues, createSubmissionHandler };
|
||||
};
|
||||
|
||||
/**
|
||||
* Predefined feature configurations for common shop-info components
|
||||
*/
|
||||
export const FEATURE_CONFIGS = {
|
||||
responsibilitycenters: {
|
||||
export: [
|
||||
["md_responsibility_centers", "costs"],
|
||||
["md_responsibility_centers", "profits"],
|
||||
["md_responsibility_centers", "defaults"],
|
||||
["md_responsibility_centers", "dms_defaults"],
|
||||
["md_responsibility_centers", "taxes", "itemexemptcode"],
|
||||
["md_responsibility_centers", "taxes", "invoiceexemptcode"],
|
||||
["md_responsibility_centers", "ar"],
|
||||
["md_responsibility_centers", "refund"],
|
||||
["md_responsibility_centers", "sales_tax_codes"],
|
||||
["md_responsibility_centers", "ttl_adjustment"],
|
||||
["md_responsibility_centers", "ttl_tax_adjustment"]
|
||||
]
|
||||
},
|
||||
general: {
|
||||
export: [
|
||||
["accountingconfig", "qbo"],
|
||||
["accountingconfig", "qbo_usa"],
|
||||
["accountingconfig", "qbo_departmentid"],
|
||||
["accountingconfig", "tiers"],
|
||||
["accountingconfig", "twotierpref"],
|
||||
["accountingconfig", "printlater"],
|
||||
["accountingconfig", "emaillater"],
|
||||
["accountingconfig", "ReceivableCustomField1"],
|
||||
["accountingconfig", "ReceivableCustomField2"],
|
||||
["accountingconfig", "ReceivableCustomField3"],
|
||||
["md_classes"],
|
||||
["enforce_class"],
|
||||
["accountingconfig", "ClosingPeriod"],
|
||||
["accountingconfig", "companyCode"],
|
||||
["accountingconfig", "batchID"]
|
||||
],
|
||||
bills: [
|
||||
["bill_tax_rates", "federal_tax_rate"],
|
||||
["bill_tax_rates", "state_tax_rate"],
|
||||
["bill_tax_rates", "local_tax_rate"]
|
||||
],
|
||||
timetickets: [["tt_allow_post_to_invoiced"], ["tt_enforce_hours_for_tech_console"], ["bill_allow_post_to_closed"]]
|
||||
}
|
||||
};
|
||||
@@ -52,6 +52,7 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
|
||||
{discount && discount !== 0 ? <Tag color="green">{`${discount * 100}%`}</Tag> : null}
|
||||
</div>
|
||||
);
|
||||
@@ -116,6 +117,11 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
|
||||
{o.name}
|
||||
</div>
|
||||
<Space style={{ marginLeft: "1rem" }}>
|
||||
{o.tags?.map((tag, idx) => (
|
||||
<Tag key={idx} style={{ marginLeft: "0.5rem" }}>
|
||||
{tag}
|
||||
</Tag>
|
||||
))}
|
||||
{o.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
|
||||
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
|
||||
</Space>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Divider, Form, Input, InputNumber, Space, Switch } from "antd";
|
||||
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -179,6 +179,18 @@ export function VendorsFormComponent({
|
||||
}
|
||||
</LayoutFormRow>
|
||||
|
||||
<Form.Item
|
||||
name="tags"
|
||||
label={t("vendor.fields.tags")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
type: "array"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" />
|
||||
</Form.Item>
|
||||
{DmsAp.treatment === "on" && (
|
||||
<Form.Item label={t("vendors.fields.dmsid")} name="dmsid">
|
||||
<Input />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Input, Space, Table } from "antd";
|
||||
import { Button, Card, Input, Space, Table, Tag } from "antd";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -38,6 +38,18 @@ export default function VendorsListComponent({ handleNewVendor, loading, handleO
|
||||
title: t("vendors.fields.city"),
|
||||
dataIndex: "city",
|
||||
key: "city"
|
||||
},
|
||||
{
|
||||
title: t("vendors.fields.tags"),
|
||||
dataIndex: "tags",
|
||||
key: "tags",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{record?.tags?.map((tag, idx) => (
|
||||
<Tag key={idx}>{tag}</Tag>
|
||||
))}
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -158,7 +158,10 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
auth: { token, bodyshopId: bodyshop.id },
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 2000,
|
||||
reconnectionDelayMax: 10000
|
||||
reconnectionDelayMax: 60000
|
||||
// randomizationFactor: 0.5,
|
||||
// transports: ["websocket", "polling"], // Add this to prefer WebSocket with polling fallback
|
||||
// rememberUpgrade: true
|
||||
});
|
||||
|
||||
socketRef.current = socketInstance;
|
||||
@@ -249,7 +252,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleConnect = () => {
|
||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||
setClientId(socketInstance.id);
|
||||
|
||||
@@ -31,6 +31,8 @@ export const QUERY_ALL_ACTIVE_APPOINTMENTS = gql`
|
||||
color
|
||||
note
|
||||
job {
|
||||
scheduled_in
|
||||
scheduled_completion
|
||||
alt_transport
|
||||
ro_number
|
||||
ownr_ln
|
||||
|
||||
@@ -713,6 +713,19 @@ export const GET_JOB_BY_PK = gql`
|
||||
v_model_yr
|
||||
v_model_desc
|
||||
v_vin
|
||||
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
|
||||
created_at
|
||||
created_by
|
||||
critical
|
||||
id
|
||||
jobid
|
||||
private
|
||||
text
|
||||
updated_at
|
||||
audit
|
||||
type
|
||||
pinned
|
||||
}
|
||||
vehicle {
|
||||
id
|
||||
jobs {
|
||||
@@ -959,6 +972,8 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
critical
|
||||
private
|
||||
created_at
|
||||
pinned
|
||||
type
|
||||
}
|
||||
updated_at
|
||||
clm_total
|
||||
@@ -984,6 +999,7 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
key
|
||||
type
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1048,6 +1064,8 @@ export const QUERY_TECH_JOB_DETAILS = gql`
|
||||
critical
|
||||
private
|
||||
created_at
|
||||
pinned
|
||||
type
|
||||
}
|
||||
updated_at
|
||||
documents(order_by: { created_at: desc }) {
|
||||
@@ -2323,7 +2341,7 @@ export const QUERY_JOBS_TECH_ASIGNED_TO_BY_TEAM = gql`
|
||||
`;
|
||||
|
||||
export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||
query QUERY_JOB_CARD_DETAILS($id: uuid!) {
|
||||
query QUERY_PARTS_QUEUE_CARD_DETAILS($id: uuid!) {
|
||||
jobs_by_pk(id: $id) {
|
||||
actual_completion
|
||||
actual_delivery
|
||||
@@ -2349,6 +2367,19 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||
start
|
||||
status
|
||||
}
|
||||
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
|
||||
created_at
|
||||
created_by
|
||||
critical
|
||||
id
|
||||
jobid
|
||||
private
|
||||
text
|
||||
updated_at
|
||||
audit
|
||||
type
|
||||
pinned
|
||||
}
|
||||
clm_no
|
||||
clm_total
|
||||
comment
|
||||
|
||||
@@ -14,6 +14,7 @@ export const INSERT_NEW_NOTE = gql`
|
||||
updated_at
|
||||
audit
|
||||
type
|
||||
pinned
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +44,7 @@ export const QUERY_NOTES_BY_JOB_PK = gql`
|
||||
updated_at
|
||||
audit
|
||||
type
|
||||
pinned
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +65,7 @@ export const UPDATE_NOTE = gql`
|
||||
updated_at
|
||||
audit
|
||||
type
|
||||
pinned
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const QUERY_VENDOR_BY_ID = gql`
|
||||
active
|
||||
phone
|
||||
dmsid
|
||||
tags
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -54,6 +55,7 @@ export const QUERY_ALL_VENDORS = gql`
|
||||
city
|
||||
phone
|
||||
active
|
||||
tags
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -89,6 +91,7 @@ export const QUERY_ALL_VENDORS_FOR_ORDER = gql`
|
||||
email
|
||||
active
|
||||
phone
|
||||
tags
|
||||
}
|
||||
jobs(where: { id: { _eq: $jobId } }) {
|
||||
v_make_desc
|
||||
@@ -105,6 +108,7 @@ export const SEARCH_VENDOR_AUTOCOMPLETE = gql`
|
||||
cost_center
|
||||
active
|
||||
favorite
|
||||
tags
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -124,6 +128,7 @@ export const SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR = gql`
|
||||
email
|
||||
state
|
||||
active
|
||||
tags
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd";
|
||||
import axios from "axios";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
@@ -13,8 +13,9 @@ import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||
import useLocalStorage from "../../utils/useLocalStorage";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
@@ -27,7 +28,7 @@ export function BillsListPage({ loading, data, refetch, total, setPartsOrderCont
|
||||
const [searchLoading, setSearchLoading] = useState(false);
|
||||
const { page } = search;
|
||||
const history = useNavigate();
|
||||
const [state, setState] = useState({
|
||||
const [state, setState] = useLocalStorage("bills_list_sort", {
|
||||
sortedInfo: {},
|
||||
filteredInfo: { text: "" }
|
||||
});
|
||||
|
||||
@@ -271,7 +271,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
{
|
||||
required: true
|
||||
},
|
||||
({ getFieldValue }) => ({
|
||||
() => ({
|
||||
validator(_, value) {
|
||||
if (!bodyshop.cdk_dealerid) return Promise.resolve();
|
||||
if (!value || dayjs(value).isSameOrAfter(dayjs(), "day")) {
|
||||
@@ -280,7 +280,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
return Promise.reject(new Error(t("jobs.labels.dms.invoicedatefuture")));
|
||||
}
|
||||
}),
|
||||
({ getFieldValue }) => ({
|
||||
() => ({
|
||||
validator(_, value) {
|
||||
if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
|
||||
if (
|
||||
@@ -369,8 +369,8 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
<Form.List
|
||||
name={["qb_multiple_payers"]}
|
||||
rules={[
|
||||
({ getFieldValue }) => ({
|
||||
validator(_, value) {
|
||||
() => ({
|
||||
validator() {
|
||||
let totalAllocated = Dinero();
|
||||
|
||||
const payers = form.getFieldValue("qb_multiple_payers");
|
||||
@@ -492,7 +492,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
<Statistic
|
||||
title={t("jobs.labels.pimraryamountpayable")}
|
||||
valueStyle={{
|
||||
color: discrep.getAmount() > 0 ? "green" : "red"
|
||||
color: discrep.getAmount() >= 0 ? "green" : "red"
|
||||
}}
|
||||
value={discrep.toFormat()}
|
||||
/>
|
||||
|
||||
@@ -426,6 +426,11 @@
|
||||
"messagingtext": "Messaging Preset Text",
|
||||
"noteslabel": "Note Label",
|
||||
"notestext": "Note Text",
|
||||
"notifications": {
|
||||
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
|
||||
"invalid_followers": "Invalid selection. Please select valid employees.",
|
||||
"placeholder": "Search for employees"
|
||||
},
|
||||
"partslocation": "Parts Location",
|
||||
"phone": "Phone",
|
||||
"prodtargethrs": "Production Target Hours",
|
||||
@@ -512,6 +517,7 @@
|
||||
"dashboard": "Shop -> Dashboard",
|
||||
"rbac": "Shop -> RBAC",
|
||||
"reportcenter": "Shop -> Report Center",
|
||||
"responsibilitycenter": "Shop -> Responsibility Centers",
|
||||
"templates": "Shop -> Templates",
|
||||
"vendors": "Shop -> Vendors"
|
||||
},
|
||||
@@ -648,15 +654,9 @@
|
||||
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
|
||||
"uselocalmediaserver": "Use Local Media Server?",
|
||||
"website": "Website",
|
||||
"zip_post": "Zip/Postal Code",
|
||||
"notifications": {
|
||||
"description": "Select employees to automatically follow new jobs and receive notifications for job updates.",
|
||||
"placeholder": "Search for employees",
|
||||
"invalid_followers": "Invalid selection. Please select valid employees."
|
||||
}
|
||||
"zip_post": "Zip/Postal Code"
|
||||
},
|
||||
"labels": {
|
||||
"consent_settings": "Phone Number Opt-Out List",
|
||||
"2tiername": "Name => RO",
|
||||
"2tiersetup": "2 Tier Setup",
|
||||
"2tiersource": "Source => RO",
|
||||
@@ -667,6 +667,7 @@
|
||||
"apptcolors": "Appointment Colors",
|
||||
"businessinformation": "Business Information",
|
||||
"checklists": "Checklists",
|
||||
"consent_settings": "Phone Number Opt-Out List",
|
||||
"csiq": "CSI Questions",
|
||||
"customtemplates": "Custom Templates",
|
||||
"defaultcostsmapping": "Default Costs Mapping",
|
||||
@@ -704,6 +705,9 @@
|
||||
"messagingpresets": "Messaging Presets",
|
||||
"notemplatesavailable": "No templates available to add.",
|
||||
"notespresets": "Notes Presets",
|
||||
"notifications": {
|
||||
"followers": "Notifications"
|
||||
},
|
||||
"orderstatuses": "Order Statuses",
|
||||
"partslocations": "Parts Locations",
|
||||
"partsscan": "Parts Scanning",
|
||||
@@ -734,10 +738,7 @@
|
||||
"ssbuckets": "Job Size Definitions",
|
||||
"systemsettings": "System Settings",
|
||||
"task-presets": "Task Presets",
|
||||
"workingdays": "Working Days",
|
||||
"notifications": {
|
||||
"followers": "Notifications"
|
||||
}
|
||||
"workingdays": "Working Days"
|
||||
},
|
||||
"operations": {
|
||||
"contains": "Contains",
|
||||
@@ -783,6 +784,15 @@
|
||||
"completed": "Job checklist completed."
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"associated_owners": "Associated Owners",
|
||||
"created_at": "Opt-Out Date",
|
||||
"no_owners": "No Associated Owners",
|
||||
"phone_1": "Phone 1",
|
||||
"phone_2": "Phone 2",
|
||||
"phone_number": "Phone Number",
|
||||
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
|
||||
},
|
||||
"contracts": {
|
||||
"actions": {
|
||||
"changerate": "Change Contract Rates",
|
||||
@@ -975,7 +985,10 @@
|
||||
"addcomponent": "Add Component"
|
||||
},
|
||||
"errors": {
|
||||
"atp": "No Alt. Transport*",
|
||||
"insco": "No Ins. Co.*",
|
||||
"refreshrequired": "You must refresh the dashboard data to see this component.",
|
||||
"status": "No Status*",
|
||||
"updatinglayout": "Error saving updated layout {{message}}"
|
||||
},
|
||||
"labels": {
|
||||
@@ -998,6 +1011,8 @@
|
||||
"productiondollars": "Total Dollars in Production",
|
||||
"productionhours": "Total Hours in Production",
|
||||
"projectedmonthlysales": "Projected Monthly Sales",
|
||||
"scheduleddeliverydate": "Scheduled Delivery Date: {{date}}",
|
||||
"scheduleddeliverytoday": "Scheduled Delivery Today",
|
||||
"scheduledindate": "Scheduled In Today: {{date}}",
|
||||
"scheduledintoday": "Scheduled In Today",
|
||||
"scheduledoutdate": "Scheduled Out Today: {{date}}",
|
||||
@@ -1230,11 +1245,11 @@
|
||||
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
|
||||
"notfound": "No record was found.",
|
||||
"sizelimit": "The selected items exceed the size limit.",
|
||||
"submit-for-testing": "Error submitting Job for testing.",
|
||||
"sub_status": {
|
||||
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.",
|
||||
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
|
||||
}
|
||||
},
|
||||
"submit-for-testing": "Error submitting Job for testing."
|
||||
},
|
||||
"itemtypes": {
|
||||
"contract": "CC Contract",
|
||||
@@ -1654,8 +1669,6 @@
|
||||
"adjustment_bottom_line": "Adjustments",
|
||||
"adjustmenthours": "Adjustment Hours",
|
||||
"alt_transport": "Alt. Trans.",
|
||||
"estimate_sent_approval": "Estimate Sent for Approval",
|
||||
"estimate_approved": "Estimate Approved",
|
||||
"area_of_damage_impact": {
|
||||
"10": "Left Front Side",
|
||||
"11": "Left Front Corner",
|
||||
@@ -1778,6 +1791,8 @@
|
||||
"est_ct_ln": "Estimator Last Name",
|
||||
"est_ea": "Estimator Email",
|
||||
"est_ph1": "Estimator Phone #",
|
||||
"estimate_approved": "Estimate Approved",
|
||||
"estimate_sent_approval": "Estimate Sent for Approval",
|
||||
"federal_tax_payable": "Federal Tax Payable",
|
||||
"federal_tax_rate": "Federal Tax Rate",
|
||||
"flat_rate_ats": "Flat Rate ATS?",
|
||||
@@ -1961,8 +1976,6 @@
|
||||
"scheddates": "Schedule Dates"
|
||||
},
|
||||
"labels": {
|
||||
"sent": "",
|
||||
"approved": "",
|
||||
"accountsreceivable": "Accounts Receivable",
|
||||
"act_price_ppc": "New Part Price",
|
||||
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
|
||||
@@ -1977,6 +1990,7 @@
|
||||
"alreadyaddedtoscoreboard": "Job has already been added to scoreboard. Saving will update the previous entry.",
|
||||
"alreadyclosed": "This Job has already been closed.",
|
||||
"appointmentconfirmation": "Send confirmation to customer?",
|
||||
"approved": "",
|
||||
"associationwarning": "Any changes to associations will require updating the data from the new parent record to the Job.",
|
||||
"audit": "Audit Trail",
|
||||
"available": "Available",
|
||||
@@ -2167,6 +2181,7 @@
|
||||
"sales": "Sales",
|
||||
"savebeforeconversion": "You have unsaved changes on the Job. Please save them before converting it. ",
|
||||
"scheduledinchange": "The scheduled in is based off the latest appointment. To change this date, please schedule or reschedule the Job. ",
|
||||
"sent": "",
|
||||
"specialcoveragepolicy": "Special Coverage Policy Applies",
|
||||
"state_tax_amt": "Provincial/State Taxes",
|
||||
"subletsnotcompleted": "Outstanding Sublets",
|
||||
@@ -2383,15 +2398,16 @@
|
||||
},
|
||||
"errors": {
|
||||
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
||||
"no_consent": "This phone number has opted-out of Messaging.",
|
||||
"noattachedjobs": "No Jobs have been associated to this conversation. ",
|
||||
"updatinglabel": "Error updating label. {{error}}",
|
||||
"no_consent": "This phone number has opted-out of Messaging."
|
||||
"updatinglabel": "Error updating label. {{error}}"
|
||||
},
|
||||
"labels": {
|
||||
"addlabel": "Add a label to this conversation.",
|
||||
"archive": "Archive",
|
||||
"maxtenimages": "You can only select up to a maximum of 10 images at a time.",
|
||||
"messaging": "Messaging",
|
||||
"no_consent": "Opted-out",
|
||||
"noallowtxt": "This customer has not indicated their permission to be messaged.",
|
||||
"nojobs": "Not associated to any Job.",
|
||||
"nopush": "Polling Mode Enabled",
|
||||
@@ -2401,8 +2417,7 @@
|
||||
"selectmedia": "Select Media",
|
||||
"sentby": "Sent by {{by}} at {{time}}",
|
||||
"typeamessage": "Send a message...",
|
||||
"unarchive": "Unarchive",
|
||||
"no_consent": "Opted-out"
|
||||
"unarchive": "Unarchive"
|
||||
},
|
||||
"render": {
|
||||
"conversation_list": "Conversation List"
|
||||
@@ -2422,6 +2437,7 @@
|
||||
"fields": {
|
||||
"createdby": "Created By",
|
||||
"critical": "Critical",
|
||||
"pinned": "Pinned",
|
||||
"private": "Private",
|
||||
"text": "Contents",
|
||||
"type": "Type",
|
||||
@@ -2440,6 +2456,7 @@
|
||||
"addtorelatedro": "Add to Related ROs",
|
||||
"newnoteplaceholder": "Add a note...",
|
||||
"notetoadd": "Note to Add",
|
||||
"pinned_note": "Pinned Note",
|
||||
"systemnotes": "System Notes",
|
||||
"usernotes": "User Notes"
|
||||
},
|
||||
@@ -2462,11 +2479,15 @@
|
||||
"fcm": "Push"
|
||||
},
|
||||
"labels": {
|
||||
"auto-add": "Automatically watch Jobs I import",
|
||||
"auto-add-success": "Auto watcher status successfully changed.",
|
||||
"auto-add-failure": "Something went wrong updating your auto watcher status.",
|
||||
"add-watchers": "Add Watchers",
|
||||
"add-watchers-team": "Add Team Members",
|
||||
"auto-add": "Automatically watch Jobs I import",
|
||||
"auto-add-description": "",
|
||||
"auto-add-failure": "Something went wrong updating your auto watcher status.",
|
||||
"auto-add-off": "",
|
||||
"auto-add-on": "",
|
||||
"auto-add-success": "Auto watcher status successfully changed.",
|
||||
"employee-notification": "Notifications are disabled because you do not have an associated Employee record.",
|
||||
"employee-search": "Search for an Employee",
|
||||
"mark-all-read": "Mark All Read",
|
||||
"new-notification-title": "New Notification:",
|
||||
@@ -2483,8 +2504,7 @@
|
||||
"teams-search": "Search for a Team",
|
||||
"unwatch": "Unwatch",
|
||||
"watch": "Watch",
|
||||
"watching-issue": "Watching",
|
||||
"employee-notification": "Notifications are disabled because you do not have an associated Employee record."
|
||||
"watching-issue": "Watching"
|
||||
},
|
||||
"scenarios": {
|
||||
"alternate-transport-changed": "Alternate Transport Changed",
|
||||
@@ -3294,17 +3314,10 @@
|
||||
"updated": "Scoreboard updated."
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Phone Number Opt-Out List"
|
||||
},
|
||||
"tasks": {
|
||||
"labels": {
|
||||
"my_tasks_center": "Task Center",
|
||||
"go_to_job": "Go to Job",
|
||||
"overdue": "Overdue",
|
||||
"due_today": "Today",
|
||||
"upcoming": "Upcoming",
|
||||
"no_due_date": "Incomplete",
|
||||
"ro-number": "RO #{{ro_number}}",
|
||||
"no_tasks": "No Tasks Found"
|
||||
},
|
||||
"actions": {
|
||||
"edit": "Edit Task",
|
||||
"new": "New Task",
|
||||
@@ -3319,9 +3332,6 @@
|
||||
"myTasks": "Mine",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"errors": {
|
||||
"load_failure": "Failed to load Tasks."
|
||||
},
|
||||
"date_presets": {
|
||||
"completion": "Completion",
|
||||
"day": "Day",
|
||||
@@ -3335,6 +3345,9 @@
|
||||
"tomorrow": "Tomorrow",
|
||||
"two_weeks": "Two Weeks"
|
||||
},
|
||||
"errors": {
|
||||
"load_failure": "Failed to load Tasks."
|
||||
},
|
||||
"failures": {
|
||||
"completed": "Failed to toggle Task completion.",
|
||||
"created": "Failed to create Task.",
|
||||
@@ -3369,6 +3382,16 @@
|
||||
"remind_at": "Remind At",
|
||||
"title": "Title"
|
||||
},
|
||||
"labels": {
|
||||
"due_today": "Today",
|
||||
"go_to_job": "Go to Job",
|
||||
"my_tasks_center": "Task Center",
|
||||
"no_due_date": "Incomplete",
|
||||
"no_tasks": "No Tasks Found",
|
||||
"overdue": "Overdue",
|
||||
"ro-number": "RO #{{ro_number}}",
|
||||
"upcoming": "Upcoming"
|
||||
},
|
||||
"placeholders": {
|
||||
"assigned_to": "Select an Employee",
|
||||
"billid": "Select a Bill",
|
||||
@@ -3518,7 +3541,7 @@
|
||||
"dashboard": "Dashboard",
|
||||
"dms": "DMS Export",
|
||||
"export-logs": "Export Logs",
|
||||
"feature-request": "Feature Requet",
|
||||
"feature-request": "Feature Request",
|
||||
"inventory": "Inventory",
|
||||
"jobs": "Jobs",
|
||||
"jobs-active": "Active Jobs",
|
||||
@@ -3868,6 +3891,7 @@
|
||||
"state": "Province/State",
|
||||
"street1": "Street",
|
||||
"street2": "Address 2",
|
||||
"tags": "Tags",
|
||||
"taxid": "Tax ID",
|
||||
"terms": "Payment Terms",
|
||||
"zip": "Zip/Postal Code"
|
||||
@@ -3884,18 +3908,6 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": "You must enter a unique vendor name."
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"phone_number": "Phone Number",
|
||||
"associated_owners": "Associated Owners",
|
||||
"created_at": "Opt-Out Date",
|
||||
"no_owners": "No Associated Owners",
|
||||
"phone_1": "Phone 1",
|
||||
"phone_2": "Phone 2",
|
||||
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Phone Number Opt-Out List"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +426,11 @@
|
||||
"messagingtext": "",
|
||||
"noteslabel": "",
|
||||
"notestext": "",
|
||||
"notifications": {
|
||||
"description": "",
|
||||
"invalid_followers": "",
|
||||
"placeholder": ""
|
||||
},
|
||||
"partslocation": "",
|
||||
"phone": "",
|
||||
"prodtargethrs": "",
|
||||
@@ -512,6 +517,7 @@
|
||||
"dashboard": "",
|
||||
"rbac": "",
|
||||
"reportcenter": "",
|
||||
"responsibilitycenter": "",
|
||||
"templates": "",
|
||||
"vendors": ""
|
||||
},
|
||||
@@ -648,15 +654,9 @@
|
||||
"use_paint_scale_data": "",
|
||||
"uselocalmediaserver": "",
|
||||
"website": "",
|
||||
"zip_post": "",
|
||||
"notifications": {
|
||||
"description": "",
|
||||
"placeholder": "",
|
||||
"invalid_followers": ""
|
||||
}
|
||||
"zip_post": ""
|
||||
},
|
||||
"labels": {
|
||||
"consent_settings": "",
|
||||
"2tiername": "",
|
||||
"2tiersetup": "",
|
||||
"2tiersource": "",
|
||||
@@ -667,6 +667,7 @@
|
||||
"apptcolors": "",
|
||||
"businessinformation": "",
|
||||
"checklists": "",
|
||||
"consent_settings": "",
|
||||
"csiq": "",
|
||||
"customtemplates": "",
|
||||
"defaultcostsmapping": "",
|
||||
@@ -704,6 +705,9 @@
|
||||
"messagingpresets": "",
|
||||
"notemplatesavailable": "",
|
||||
"notespresets": "",
|
||||
"notifications": {
|
||||
"followers": ""
|
||||
},
|
||||
"orderstatuses": "",
|
||||
"partslocations": "",
|
||||
"partsscan": "",
|
||||
@@ -734,10 +738,7 @@
|
||||
"ssbuckets": "",
|
||||
"systemsettings": "",
|
||||
"task-presets": "",
|
||||
"workingdays": "",
|
||||
"notifications": {
|
||||
"followers": ""
|
||||
}
|
||||
"workingdays": ""
|
||||
},
|
||||
"operations": {
|
||||
"contains": "",
|
||||
@@ -783,6 +784,15 @@
|
||||
"completed": ""
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"associated_owners": "",
|
||||
"created_at": "",
|
||||
"no_owners": "",
|
||||
"phone_1": "",
|
||||
"phone_2": "",
|
||||
"phone_number": "",
|
||||
"text_body": ""
|
||||
},
|
||||
"contracts": {
|
||||
"actions": {
|
||||
"changerate": "",
|
||||
@@ -975,7 +985,10 @@
|
||||
"addcomponent": ""
|
||||
},
|
||||
"errors": {
|
||||
"atp": "",
|
||||
"insco": "",
|
||||
"refreshrequired": "",
|
||||
"status": "",
|
||||
"updatinglayout": ""
|
||||
},
|
||||
"labels": {
|
||||
@@ -998,6 +1011,8 @@
|
||||
"productiondollars": "",
|
||||
"productionhours": "",
|
||||
"projectedmonthlysales": "",
|
||||
"scheduleddeliverydate": "",
|
||||
"scheduleddeliverytoday": "",
|
||||
"scheduledindate": "",
|
||||
"scheduledintoday": "",
|
||||
"scheduledoutdate": "",
|
||||
@@ -1230,11 +1245,11 @@
|
||||
"fcm": "",
|
||||
"notfound": "",
|
||||
"sizelimit": "",
|
||||
"submit-for-testing": "",
|
||||
"sub_status": {
|
||||
"expired": "",
|
||||
"trial-expired": ""
|
||||
}
|
||||
},
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
"itemtypes": {
|
||||
"contract": "",
|
||||
@@ -1646,8 +1661,6 @@
|
||||
"voiding": ""
|
||||
},
|
||||
"fields": {
|
||||
"estimate_sent_approval": "",
|
||||
"estimate_approved": "",
|
||||
"active_tasks": "",
|
||||
"actual_completion": "Realización real",
|
||||
"actual_delivery": "Entrega real",
|
||||
@@ -1778,6 +1791,8 @@
|
||||
"est_ct_ln": "Apellido del tasador",
|
||||
"est_ea": "Correo electrónico del tasador",
|
||||
"est_ph1": "Número de teléfono del tasador",
|
||||
"estimate_approved": "",
|
||||
"estimate_sent_approval": "",
|
||||
"federal_tax_payable": "Impuesto federal por pagar",
|
||||
"federal_tax_rate": "",
|
||||
"flat_rate_ats": "",
|
||||
@@ -1961,8 +1976,6 @@
|
||||
"scheddates": ""
|
||||
},
|
||||
"labels": {
|
||||
"sent": "",
|
||||
"approved": "",
|
||||
"accountsreceivable": "",
|
||||
"act_price_ppc": "",
|
||||
"actual_completion_inferred": "",
|
||||
@@ -1977,6 +1990,7 @@
|
||||
"alreadyaddedtoscoreboard": "",
|
||||
"alreadyclosed": "",
|
||||
"appointmentconfirmation": "¿Enviar confirmación al cliente?",
|
||||
"approved": "",
|
||||
"associationwarning": "",
|
||||
"audit": "",
|
||||
"available": "",
|
||||
@@ -2167,6 +2181,7 @@
|
||||
"sales": "",
|
||||
"savebeforeconversion": "",
|
||||
"scheduledinchange": "",
|
||||
"sent": "",
|
||||
"specialcoveragepolicy": "",
|
||||
"state_tax_amt": "",
|
||||
"subletsnotcompleted": "",
|
||||
@@ -2383,15 +2398,16 @@
|
||||
},
|
||||
"errors": {
|
||||
"invalidphone": "",
|
||||
"no_consent": "",
|
||||
"noattachedjobs": "",
|
||||
"updatinglabel": "",
|
||||
"no_consent": ""
|
||||
"updatinglabel": ""
|
||||
},
|
||||
"labels": {
|
||||
"addlabel": "",
|
||||
"archive": "",
|
||||
"maxtenimages": "",
|
||||
"messaging": "Mensajería",
|
||||
"no_consent": "",
|
||||
"noallowtxt": "",
|
||||
"nojobs": "",
|
||||
"nopush": "",
|
||||
@@ -2401,8 +2417,7 @@
|
||||
"selectmedia": "",
|
||||
"sentby": "",
|
||||
"typeamessage": "Enviar un mensaje...",
|
||||
"unarchive": "",
|
||||
"no_consent": ""
|
||||
"unarchive": ""
|
||||
},
|
||||
"render": {
|
||||
"conversation_list": ""
|
||||
@@ -2422,6 +2437,7 @@
|
||||
"fields": {
|
||||
"createdby": "Creado por",
|
||||
"critical": "Crítico",
|
||||
"pinned": "",
|
||||
"private": "Privado",
|
||||
"text": "Contenido",
|
||||
"type": "",
|
||||
@@ -2440,6 +2456,7 @@
|
||||
"addtorelatedro": "",
|
||||
"newnoteplaceholder": "Agrega una nota...",
|
||||
"notetoadd": "",
|
||||
"pinned_note": "",
|
||||
"systemnotes": "",
|
||||
"usernotes": ""
|
||||
},
|
||||
@@ -2462,13 +2479,15 @@
|
||||
"fcm": ""
|
||||
},
|
||||
"labels": {
|
||||
"auto-add-on": "",
|
||||
"auto-add-off": "",
|
||||
"auto-add-success": "",
|
||||
"auto-add-failure": "",
|
||||
"auto-add-description": "",
|
||||
"add-watchers": "",
|
||||
"add-watchers-team": "",
|
||||
"auto-add": "",
|
||||
"auto-add-description": "",
|
||||
"auto-add-failure": "",
|
||||
"auto-add-off": "",
|
||||
"auto-add-on": "",
|
||||
"auto-add-success": "",
|
||||
"employee-notification": "",
|
||||
"employee-search": "",
|
||||
"mark-all-read": "",
|
||||
"new-notification-title": "",
|
||||
@@ -2485,8 +2504,7 @@
|
||||
"teams-search": "",
|
||||
"unwatch": "",
|
||||
"watch": "",
|
||||
"watching-issue": "",
|
||||
"employee-notification": ""
|
||||
"watching-issue": ""
|
||||
},
|
||||
"scenarios": {
|
||||
"alternate-transport-changed": "",
|
||||
@@ -3296,17 +3314,10 @@
|
||||
"updated": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": ""
|
||||
},
|
||||
"tasks": {
|
||||
"labels": {
|
||||
"my_tasks_center": "",
|
||||
"go_to_job": "",
|
||||
"overdue": "",
|
||||
"due_today": "",
|
||||
"upcoming": "",
|
||||
"no_due_date": "",
|
||||
"ro-number": "",
|
||||
"no_tasks": ""
|
||||
},
|
||||
"actions": {
|
||||
"edit": "",
|
||||
"new": "",
|
||||
@@ -3321,9 +3332,6 @@
|
||||
"myTasks": "",
|
||||
"refresh": ""
|
||||
},
|
||||
"errors": {
|
||||
"load_failure": ""
|
||||
},
|
||||
"date_presets": {
|
||||
"completion": "",
|
||||
"day": "",
|
||||
@@ -3337,6 +3345,9 @@
|
||||
"tomorrow": "",
|
||||
"two_weeks": ""
|
||||
},
|
||||
"errors": {
|
||||
"load_failure": ""
|
||||
},
|
||||
"failures": {
|
||||
"completed": "",
|
||||
"created": "",
|
||||
@@ -3371,6 +3382,16 @@
|
||||
"remind_at": "",
|
||||
"title": ""
|
||||
},
|
||||
"labels": {
|
||||
"due_today": "",
|
||||
"go_to_job": "",
|
||||
"my_tasks_center": "",
|
||||
"no_due_date": "",
|
||||
"no_tasks": "",
|
||||
"overdue": "",
|
||||
"ro-number": "",
|
||||
"upcoming": ""
|
||||
},
|
||||
"placeholders": {
|
||||
"assigned_to": "",
|
||||
"billid": "",
|
||||
@@ -3870,6 +3891,7 @@
|
||||
"state": "Provincia del estado",
|
||||
"street1": "calle",
|
||||
"street2": "Dirección 2",
|
||||
"tags": "",
|
||||
"taxid": "Identificación del impuesto",
|
||||
"terms": "Términos de pago",
|
||||
"zip": "código postal"
|
||||
@@ -3886,18 +3908,6 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": ""
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"phone_number": "",
|
||||
"associated_owners": "",
|
||||
"created_at": "",
|
||||
"no_owners": "",
|
||||
"phone_1": "",
|
||||
"phone_2": "",
|
||||
"text_body": ""
|
||||
},
|
||||
"settings": {
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +426,11 @@
|
||||
"messagingtext": "",
|
||||
"noteslabel": "",
|
||||
"notestext": "",
|
||||
"notifications": {
|
||||
"description": "",
|
||||
"invalid_followers": "",
|
||||
"placeholder": ""
|
||||
},
|
||||
"partslocation": "",
|
||||
"phone": "",
|
||||
"prodtargethrs": "",
|
||||
@@ -512,6 +517,7 @@
|
||||
"dashboard": "",
|
||||
"rbac": "",
|
||||
"reportcenter": "",
|
||||
"responsibilitycenter": "",
|
||||
"templates": "",
|
||||
"vendors": ""
|
||||
},
|
||||
@@ -648,15 +654,9 @@
|
||||
"use_paint_scale_data": "",
|
||||
"uselocalmediaserver": "",
|
||||
"website": "",
|
||||
"zip_post": "",
|
||||
"notifications": {
|
||||
"description": "",
|
||||
"placeholder": "",
|
||||
"invalid_followers": ""
|
||||
}
|
||||
"zip_post": ""
|
||||
},
|
||||
"labels": {
|
||||
"consent_settings": "",
|
||||
"2tiername": "",
|
||||
"2tiersetup": "",
|
||||
"2tiersource": "",
|
||||
@@ -667,6 +667,7 @@
|
||||
"apptcolors": "",
|
||||
"businessinformation": "",
|
||||
"checklists": "",
|
||||
"consent_settings": "",
|
||||
"csiq": "",
|
||||
"customtemplates": "",
|
||||
"defaultcostsmapping": "",
|
||||
@@ -704,6 +705,9 @@
|
||||
"messagingpresets": "",
|
||||
"notemplatesavailable": "",
|
||||
"notespresets": "",
|
||||
"notifications": {
|
||||
"followers": ""
|
||||
},
|
||||
"orderstatuses": "",
|
||||
"partslocations": "",
|
||||
"partsscan": "",
|
||||
@@ -734,10 +738,7 @@
|
||||
"ssbuckets": "",
|
||||
"systemsettings": "",
|
||||
"task-presets": "",
|
||||
"workingdays": "",
|
||||
"notifications": {
|
||||
"followers": ""
|
||||
}
|
||||
"workingdays": ""
|
||||
},
|
||||
"operations": {
|
||||
"contains": "",
|
||||
@@ -783,6 +784,15 @@
|
||||
"completed": ""
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"associated_owners": "Associated Owners",
|
||||
"created_at": "Opt-Out Date",
|
||||
"no_owners": "No Associated Owners",
|
||||
"phone_1": "Phone 1",
|
||||
"phone_2": "Phone 2",
|
||||
"phone_number": "Phone Number",
|
||||
"text_body": ""
|
||||
},
|
||||
"contracts": {
|
||||
"actions": {
|
||||
"changerate": "",
|
||||
@@ -975,7 +985,10 @@
|
||||
"addcomponent": ""
|
||||
},
|
||||
"errors": {
|
||||
"atp": "",
|
||||
"insco": "",
|
||||
"refreshrequired": "",
|
||||
"status": "",
|
||||
"updatinglayout": ""
|
||||
},
|
||||
"labels": {
|
||||
@@ -998,6 +1011,8 @@
|
||||
"productiondollars": "",
|
||||
"productionhours": "",
|
||||
"projectedmonthlysales": "",
|
||||
"scheduleddeliverydate": "",
|
||||
"scheduleddeliverytoday": "",
|
||||
"scheduledindate": "",
|
||||
"scheduledintoday": "",
|
||||
"scheduledoutdate": "",
|
||||
@@ -1230,11 +1245,11 @@
|
||||
"fcm": "",
|
||||
"notfound": "",
|
||||
"sizelimit": "",
|
||||
"submit-for-testing": "",
|
||||
"sub_status": {
|
||||
"expired": "",
|
||||
"trial-expired": ""
|
||||
}
|
||||
},
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
"itemtypes": {
|
||||
"contract": "",
|
||||
@@ -1646,8 +1661,6 @@
|
||||
"voiding": ""
|
||||
},
|
||||
"fields": {
|
||||
"estimate_sent_approval": "",
|
||||
"estimate_approved": "",
|
||||
"active_tasks": "",
|
||||
"actual_completion": "Achèvement réel",
|
||||
"actual_delivery": "Livraison réelle",
|
||||
@@ -1778,6 +1791,8 @@
|
||||
"est_ct_ln": "Nom de l'évaluateur",
|
||||
"est_ea": "Courriel de l'évaluateur",
|
||||
"est_ph1": "Numéro de téléphone de l'évaluateur",
|
||||
"estimate_approved": "",
|
||||
"estimate_sent_approval": "",
|
||||
"federal_tax_payable": "Impôt fédéral à payer",
|
||||
"federal_tax_rate": "",
|
||||
"flat_rate_ats": "",
|
||||
@@ -1961,8 +1976,6 @@
|
||||
"scheddates": ""
|
||||
},
|
||||
"labels": {
|
||||
"sent": "",
|
||||
"approved": "",
|
||||
"accountsreceivable": "",
|
||||
"act_price_ppc": "",
|
||||
"actual_completion_inferred": "",
|
||||
@@ -1977,6 +1990,7 @@
|
||||
"alreadyaddedtoscoreboard": "",
|
||||
"alreadyclosed": "",
|
||||
"appointmentconfirmation": "Envoyer une confirmation au client?",
|
||||
"approved": "",
|
||||
"associationwarning": "",
|
||||
"audit": "",
|
||||
"available": "",
|
||||
@@ -2167,6 +2181,7 @@
|
||||
"sales": "",
|
||||
"savebeforeconversion": "",
|
||||
"scheduledinchange": "",
|
||||
"sent": "",
|
||||
"specialcoveragepolicy": "",
|
||||
"state_tax_amt": "",
|
||||
"subletsnotcompleted": "",
|
||||
@@ -2383,15 +2398,16 @@
|
||||
},
|
||||
"errors": {
|
||||
"invalidphone": "",
|
||||
"no_consent": "",
|
||||
"noattachedjobs": "",
|
||||
"updatinglabel": "",
|
||||
"no_consent": ""
|
||||
"updatinglabel": ""
|
||||
},
|
||||
"labels": {
|
||||
"addlabel": "",
|
||||
"archive": "",
|
||||
"maxtenimages": "",
|
||||
"messaging": "Messagerie",
|
||||
"no_consent": "",
|
||||
"noallowtxt": "",
|
||||
"nojobs": "",
|
||||
"nopush": "",
|
||||
@@ -2401,8 +2417,7 @@
|
||||
"selectmedia": "",
|
||||
"sentby": "",
|
||||
"typeamessage": "Envoyer un message...",
|
||||
"unarchive": "",
|
||||
"no_consent": ""
|
||||
"unarchive": ""
|
||||
},
|
||||
"render": {
|
||||
"conversation_list": ""
|
||||
@@ -2422,6 +2437,7 @@
|
||||
"fields": {
|
||||
"createdby": "Créé par",
|
||||
"critical": "Critique",
|
||||
"pinned": "",
|
||||
"private": "privé",
|
||||
"text": "Contenu",
|
||||
"type": "",
|
||||
@@ -2440,6 +2456,7 @@
|
||||
"addtorelatedro": "",
|
||||
"newnoteplaceholder": "Ajouter une note...",
|
||||
"notetoadd": "",
|
||||
"pinned_note": "",
|
||||
"systemnotes": "",
|
||||
"usernotes": ""
|
||||
},
|
||||
@@ -2462,13 +2479,15 @@
|
||||
"fcm": ""
|
||||
},
|
||||
"labels": {
|
||||
"auto-add-on": "",
|
||||
"auto-add-off": "",
|
||||
"auto-add-success": "",
|
||||
"auto-add-failure": "",
|
||||
"auto-add-description": "",
|
||||
"add-watchers": "",
|
||||
"add-watchers-team": "",
|
||||
"auto-add": "",
|
||||
"auto-add-description": "",
|
||||
"auto-add-failure": "",
|
||||
"auto-add-off": "",
|
||||
"auto-add-on": "",
|
||||
"auto-add-success": "",
|
||||
"employee-notification": "",
|
||||
"employee-search": "",
|
||||
"mark-all-read": "",
|
||||
"new-notification-title": "",
|
||||
@@ -2485,8 +2504,7 @@
|
||||
"teams-search": "",
|
||||
"unwatch": "",
|
||||
"watch": "",
|
||||
"watching-issue": "",
|
||||
"employee-notification": ""
|
||||
"watching-issue": ""
|
||||
},
|
||||
"scenarios": {
|
||||
"alternate-transport-changed": "",
|
||||
@@ -3296,17 +3314,10 @@
|
||||
"updated": ""
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": ""
|
||||
},
|
||||
"tasks": {
|
||||
"labels": {
|
||||
"my_tasks_center": "",
|
||||
"go_to_job": "",
|
||||
"overdue": "",
|
||||
"due_today": "",
|
||||
"upcoming": "",
|
||||
"no_due_date": "",
|
||||
"ro-number": "",
|
||||
"no_tasks": ""
|
||||
},
|
||||
"actions": {
|
||||
"edit": "",
|
||||
"new": "",
|
||||
@@ -3321,9 +3332,6 @@
|
||||
"myTasks": "",
|
||||
"refresh": ""
|
||||
},
|
||||
"errors": {
|
||||
"load_failure": ""
|
||||
},
|
||||
"date_presets": {
|
||||
"completion": "",
|
||||
"day": "",
|
||||
@@ -3337,6 +3345,9 @@
|
||||
"tomorrow": "",
|
||||
"two_weeks": ""
|
||||
},
|
||||
"errors": {
|
||||
"load_failure": ""
|
||||
},
|
||||
"failures": {
|
||||
"completed": "",
|
||||
"created": "",
|
||||
@@ -3371,6 +3382,16 @@
|
||||
"remind_at": "",
|
||||
"title": ""
|
||||
},
|
||||
"labels": {
|
||||
"due_today": "",
|
||||
"go_to_job": "",
|
||||
"my_tasks_center": "",
|
||||
"no_due_date": "",
|
||||
"no_tasks": "",
|
||||
"overdue": "",
|
||||
"ro-number": "",
|
||||
"upcoming": ""
|
||||
},
|
||||
"placeholders": {
|
||||
"assigned_to": "",
|
||||
"billid": "",
|
||||
@@ -3870,6 +3891,7 @@
|
||||
"state": "Etat / Province",
|
||||
"street1": "rue",
|
||||
"street2": "Adresse 2 ",
|
||||
"tags": "",
|
||||
"taxid": "Identifiant de taxe",
|
||||
"terms": "Modalités de paiement",
|
||||
"zip": "Zip / code postal"
|
||||
@@ -3886,18 +3908,6 @@
|
||||
"validation": {
|
||||
"unique_vendor_name": ""
|
||||
}
|
||||
},
|
||||
"consent": {
|
||||
"phone_number": "Phone Number",
|
||||
"associated_owners": "Associated Owners",
|
||||
"created_at": "Opt-Out Date",
|
||||
"no_owners": "No Associated Owners",
|
||||
"phone_1": "Phone 1",
|
||||
"phone_2": "Phone 2",
|
||||
"text_body": ""
|
||||
},
|
||||
"settings": {
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,24 +48,24 @@ export default async function RenderTemplate(
|
||||
...(renderAsHtml
|
||||
? {}
|
||||
: {
|
||||
recipe: "chrome-pdf",
|
||||
...(!ignoreCustomMargins && {
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
recipe: "chrome-pdf",
|
||||
...(!ignoreCustomMargins && {
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px"
|
||||
}
|
||||
})
|
||||
}),
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px"
|
||||
}
|
||||
})
|
||||
}),
|
||||
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
|
||||
...(renderAsText ? { recipe: "text" } : {})
|
||||
},
|
||||
@@ -100,14 +100,14 @@ export default async function RenderTemplate(
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px"
|
||||
}
|
||||
@@ -182,22 +182,22 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
|
||||
...(renderAsHtml
|
||||
? {}
|
||||
: {
|
||||
recipe: "chrome-pdf",
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
recipe: "chrome-pdf",
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px"
|
||||
}
|
||||
}),
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px"
|
||||
}
|
||||
}),
|
||||
pdfOperations: [
|
||||
{
|
||||
template: {
|
||||
@@ -213,14 +213,14 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
|
||||
chrome: {
|
||||
marginTop:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
bodyshop.logo_img_path.headerMargin &&
|
||||
bodyshop.logo_img_path.headerMargin > 36
|
||||
? bodyshop.logo_img_path.headerMargin
|
||||
: "36px",
|
||||
marginBottom:
|
||||
bodyshop.logo_img_path &&
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
bodyshop.logo_img_path.footerMargin &&
|
||||
bodyshop.logo_img_path.footerMargin > 50
|
||||
? bodyshop.logo_img_path.footerMargin
|
||||
: "50px"
|
||||
},
|
||||
@@ -302,7 +302,6 @@ export const fetchFilterData = async ({ name }) => {
|
||||
const jsReportFilters = await cleanAxios.get(`${server}/odata/assets?$filter=name eq '${name}.filters'`, {
|
||||
headers: { Authorization: jsrAuth }
|
||||
});
|
||||
console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
|
||||
|
||||
let parsedFilterData;
|
||||
let useShopSpecificTemplate = false;
|
||||
|
||||
@@ -4909,6 +4909,7 @@
|
||||
- critical
|
||||
- id
|
||||
- jobid
|
||||
- pinned
|
||||
- private
|
||||
- text
|
||||
- type
|
||||
@@ -4923,6 +4924,7 @@
|
||||
- critical
|
||||
- id
|
||||
- jobid
|
||||
- pinned
|
||||
- private
|
||||
- text
|
||||
- type
|
||||
@@ -4947,6 +4949,7 @@
|
||||
- critical
|
||||
- id
|
||||
- jobid
|
||||
- pinned
|
||||
- private
|
||||
- text
|
||||
- type
|
||||
@@ -7120,6 +7123,7 @@
|
||||
- state
|
||||
- street1
|
||||
- street2
|
||||
- tags
|
||||
- updated_at
|
||||
- zip
|
||||
select_permissions:
|
||||
@@ -7143,6 +7147,7 @@
|
||||
- state
|
||||
- street1
|
||||
- street2
|
||||
- tags
|
||||
- updated_at
|
||||
- zip
|
||||
filter:
|
||||
@@ -7176,6 +7181,7 @@
|
||||
- state
|
||||
- street1
|
||||
- street2
|
||||
- tags
|
||||
- updated_at
|
||||
- zip
|
||||
filter:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."notes" add column "pinned" boolean
|
||||
-- not null default 'false';
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."notes" add column "pinned" boolean
|
||||
not null default 'false';
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."vendors" add column "tags" jsonb
|
||||
-- not null default jsonb_build_array();
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."vendors" add column "tags" jsonb
|
||||
not null default jsonb_build_array();
|
||||
@@ -19,7 +19,7 @@ async function JobCosting(req, res) {
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
//Uncomment for further testing
|
||||
// logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
|
||||
logger.log("job-costing-start", "DEBUG", req.user.email, jobid, null);
|
||||
|
||||
try {
|
||||
const resp = await client.setHeaders({ Authorization: BearerToken }).request(queries.QUERY_JOB_COSTING_DETAILS, {
|
||||
@@ -47,9 +47,9 @@ async function JobCostingMulti(req, res) {
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
//Uncomment for further testing
|
||||
// logger.log("job-costing-multi-start", "DEBUG", req?.user?.email, null, {
|
||||
// jobids
|
||||
// });
|
||||
logger.log("job-costing-multi-start", "DEBUG", req?.user?.email, null, {
|
||||
jobids
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await client
|
||||
@@ -589,7 +589,7 @@ function GenerateCostingData(job) {
|
||||
amount: Math.round((job.storage_payable || 0) * 100)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//Is it a DMS Setup?
|
||||
const selectedDmsAllocationConfig =
|
||||
(job.bodyshop.md_responsibility_centers.dms_defaults &&
|
||||
|
||||
@@ -8,6 +8,7 @@ const getLifecycleStatusColor = require("../utils/getLifecycleStatusColor");
|
||||
const jobLifecycle = async (req, res) => {
|
||||
// Grab the jobids and statuses from the request body
|
||||
const { jobids, statuses } = req.body;
|
||||
const { logger } = req;
|
||||
|
||||
if (!jobids) {
|
||||
return res.status(400).json({
|
||||
@@ -16,102 +17,118 @@ const jobLifecycle = async (req, res) => {
|
||||
}
|
||||
|
||||
const jobIDs = _.isArray(jobids) ? jobids : [jobids];
|
||||
const client = req.userGraphQLClient;
|
||||
const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, { jobids: jobIDs });
|
||||
|
||||
const transitions = resp.transitions;
|
||||
logger.log("job-lifecycle-start", "DEBUG", req?.user?.email, null, {
|
||||
jobids: jobIDs
|
||||
});
|
||||
|
||||
try {
|
||||
const client = req.userGraphQLClient;
|
||||
const resp = await client.request(queries.QUERY_TRANSITIONS_BY_JOBID, { jobids: jobIDs });
|
||||
|
||||
const transitions = resp.transitions;
|
||||
|
||||
if (!transitions) {
|
||||
return res.status(200).json({
|
||||
jobIDs,
|
||||
transitions: []
|
||||
});
|
||||
}
|
||||
|
||||
const transitionsByJobId = _.groupBy(resp.transitions, "jobid");
|
||||
|
||||
const groupedTransitions = {};
|
||||
const allDurations = [];
|
||||
|
||||
for (let jobId in transitionsByJobId) {
|
||||
let lifecycle = transitionsByJobId[jobId].map((transition) => {
|
||||
transition.start_readable = transition.start ? moment(transition.start).fromNow() : "N/A";
|
||||
transition.end_readable = transition.end ? moment(transition.end).fromNow() : "N/A";
|
||||
|
||||
if (transition.duration) {
|
||||
transition.duration_seconds = Math.round(transition.duration / 1000);
|
||||
transition.duration_minutes = Math.round(transition.duration_seconds / 60);
|
||||
let duration = moment.duration(transition.duration);
|
||||
transition.duration_readable = durationToHumanReadable(duration);
|
||||
} else {
|
||||
transition.duration_seconds = 0;
|
||||
transition.duration_minutes = 0;
|
||||
transition.duration_readable = "N/A";
|
||||
}
|
||||
return transition;
|
||||
});
|
||||
|
||||
const durations = calculateStatusDuration(lifecycle, statuses);
|
||||
|
||||
groupedTransitions[jobId] = {
|
||||
lifecycle,
|
||||
durations
|
||||
};
|
||||
|
||||
if (durations?.summations) {
|
||||
allDurations.push(durations.summations);
|
||||
}
|
||||
}
|
||||
|
||||
const finalSummations = [];
|
||||
const flatGroupedAllDurations = _.groupBy(allDurations.flat(), "status");
|
||||
|
||||
const finalStatusCounts = Object.keys(flatGroupedAllDurations).reduce((acc, status) => {
|
||||
acc[status] = flatGroupedAllDurations[status].length;
|
||||
return acc;
|
||||
}, {});
|
||||
// Calculate total value of all statuses
|
||||
const finalTotal = Object.values(flatGroupedAllDurations).reduce((total, statusArr) => {
|
||||
return total + statusArr.reduce((acc, curr) => acc + curr.value, 0);
|
||||
}, 0);
|
||||
|
||||
Object.keys(flatGroupedAllDurations).forEach((status) => {
|
||||
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
|
||||
const humanReadable = durationToHumanReadable(moment.duration(value));
|
||||
const percentage = finalTotal > 0 ? (value / finalTotal) * 100 : 0;
|
||||
const color = getLifecycleStatusColor(status);
|
||||
const roundedPercentage = `${Math.round(percentage)}%`;
|
||||
const averageValue = _.size(jobIDs) > 0 ? value / jobIDs.length : 0;
|
||||
const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue));
|
||||
finalSummations.push({
|
||||
status,
|
||||
value,
|
||||
humanReadable,
|
||||
percentage,
|
||||
color,
|
||||
roundedPercentage,
|
||||
averageValue,
|
||||
averageHumanReadable
|
||||
});
|
||||
});
|
||||
|
||||
if (!transitions) {
|
||||
return res.status(200).json({
|
||||
jobIDs,
|
||||
transitions: []
|
||||
});
|
||||
}
|
||||
|
||||
const transitionsByJobId = _.groupBy(resp.transitions, "jobid");
|
||||
|
||||
const groupedTransitions = {};
|
||||
const allDurations = [];
|
||||
|
||||
for (let jobId in transitionsByJobId) {
|
||||
let lifecycle = transitionsByJobId[jobId].map((transition) => {
|
||||
transition.start_readable = transition.start ? moment(transition.start).fromNow() : "N/A";
|
||||
transition.end_readable = transition.end ? moment(transition.end).fromNow() : "N/A";
|
||||
|
||||
if (transition.duration) {
|
||||
transition.duration_seconds = Math.round(transition.duration / 1000);
|
||||
transition.duration_minutes = Math.round(transition.duration_seconds / 60);
|
||||
let duration = moment.duration(transition.duration);
|
||||
transition.duration_readable = durationToHumanReadable(duration);
|
||||
} else {
|
||||
transition.duration_seconds = 0;
|
||||
transition.duration_minutes = 0;
|
||||
transition.duration_readable = "N/A";
|
||||
transition: groupedTransitions,
|
||||
durations: {
|
||||
jobs: jobIDs.length,
|
||||
summations: finalSummations,
|
||||
totalStatuses: finalSummations.length,
|
||||
total: finalTotal,
|
||||
statusCounts: finalStatusCounts,
|
||||
humanReadable: durationToHumanReadable(moment.duration(finalTotal)),
|
||||
averageValue: _.size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0,
|
||||
averageHumanReadable:
|
||||
_.size(jobIDs) > 0
|
||||
? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length))
|
||||
: durationToHumanReadable(moment.duration(0))
|
||||
}
|
||||
return transition;
|
||||
});
|
||||
|
||||
const durations = calculateStatusDuration(lifecycle, statuses);
|
||||
|
||||
groupedTransitions[jobId] = {
|
||||
lifecycle,
|
||||
durations
|
||||
};
|
||||
|
||||
if (durations?.summations) {
|
||||
allDurations.push(durations.summations);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("job-lifecycle-error", "ERROR", req?.user?.email, null, {
|
||||
jobids: jobIDs,
|
||||
statuses: statuses ? JSON.stringify(statuses) : "N/A",
|
||||
error: error.message
|
||||
});
|
||||
return res.status(500).json({
|
||||
error: "Internal server error"
|
||||
});
|
||||
}
|
||||
|
||||
const finalSummations = [];
|
||||
const flatGroupedAllDurations = _.groupBy(allDurations.flat(), "status");
|
||||
|
||||
const finalStatusCounts = Object.keys(flatGroupedAllDurations).reduce((acc, status) => {
|
||||
acc[status] = flatGroupedAllDurations[status].length;
|
||||
return acc;
|
||||
}, {});
|
||||
// Calculate total value of all statuses
|
||||
const finalTotal = Object.values(flatGroupedAllDurations).reduce((total, statusArr) => {
|
||||
return total + statusArr.reduce((acc, curr) => acc + curr.value, 0);
|
||||
}, 0);
|
||||
|
||||
Object.keys(flatGroupedAllDurations).forEach((status) => {
|
||||
const value = flatGroupedAllDurations[status].reduce((acc, curr) => acc + curr.value, 0);
|
||||
const humanReadable = durationToHumanReadable(moment.duration(value));
|
||||
const percentage = finalTotal > 0 ? (value / finalTotal) * 100 : 0;
|
||||
const color = getLifecycleStatusColor(status);
|
||||
const roundedPercentage = `${Math.round(percentage)}%`;
|
||||
const averageValue = _.size(jobIDs) > 0 ? value / jobIDs.length : 0;
|
||||
const averageHumanReadable = durationToHumanReadable(moment.duration(averageValue));
|
||||
finalSummations.push({
|
||||
status,
|
||||
value,
|
||||
humanReadable,
|
||||
percentage,
|
||||
color,
|
||||
roundedPercentage,
|
||||
averageValue,
|
||||
averageHumanReadable
|
||||
});
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
jobIDs,
|
||||
transition: groupedTransitions,
|
||||
durations: {
|
||||
jobs: jobIDs.length,
|
||||
summations: finalSummations,
|
||||
totalStatuses: finalSummations.length,
|
||||
total: finalTotal,
|
||||
statusCounts: finalStatusCounts,
|
||||
humanReadable: durationToHumanReadable(moment.duration(finalTotal)),
|
||||
averageValue: _.size(jobIDs) > 0 ? finalTotal / jobIDs.length : 0,
|
||||
averageHumanReadable:
|
||||
_.size(jobIDs) > 0
|
||||
? durationToHumanReadable(moment.duration(finalTotal / jobIDs.length))
|
||||
: durationToHumanReadable(moment.duration(0))
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = jobLifecycle;
|
||||
|
||||
@@ -50,7 +50,12 @@ const autoAddWatchers = async (req) => {
|
||||
try {
|
||||
// Fetch bodyshop data from Redis
|
||||
const bodyshopData = await getBodyshopFromRedis(shopId);
|
||||
const notificationFollowers = bodyshopData?.notification_followers || [];
|
||||
let notificationFollowers = bodyshopData?.notification_followers;
|
||||
|
||||
// Bail if notification_followers is missing or not an array
|
||||
if (!notificationFollowers || !Array.isArray(notificationFollowers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute queries in parallel
|
||||
const [notificationData, existingWatchersData] = await Promise.all([
|
||||
|
||||
Reference in New Issue
Block a user