Merged in release/2025-08-01 (pull request #2448)

Release/2025 08 01 into master-AIO - IO-2604, IO-3292, IO-3310, IO-3315, IO-3316, IO-3318, IO-3319, IO-3320
This commit is contained in:
Dave Richer
2025-08-02 01:21:40 +00:00
38 changed files with 6771 additions and 5187 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { Card, Checkbox, Input, Space, Table } from "antd"; import { Card, Checkbox, Input, Space, Table } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -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 PayableExportButton from "../payable-export-button/payable-export-button.component";
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component"; import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
import useLocalStorage from "./../../utils/useLocalStorage";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
@@ -31,7 +32,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedBills, setSelectedBills] = useState([]); const [selectedBills, setSelectedBills] = useState([]);
const [transInProgress, setTransInProgress] = useState(false); const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({ const [state, setState] = useLocalStorage("accounting-payables-table-state", {
sortedInfo: {}, sortedInfo: {},
search: "" search: ""
}); });
@@ -181,7 +182,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)), onSelectAll: (selected, selectedRows) => setSelectedBills(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => { onSelect: (record, selected, selectedRows) => {
setSelectedBills(selectedRows.map((i) => i.id)); setSelectedBills(selectedRows.map((i) => i.id));
}, },
getCheckboxProps: (record) => ({ getCheckboxProps: (record) => ({

View File

@@ -1,5 +1,5 @@
import { Card, Input, Space, Table } from "antd"; import { Card, Input, Space, Table } from "antd";
import React, { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -10,6 +10,7 @@ import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { exportPageLimit } from "../../utils/config"; import { exportPageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters"; import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component"; import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import PaymentExportButton from "../payment-export-button/payment-export-button.component"; import PaymentExportButton from "../payment-export-button/payment-export-button.component";
@@ -21,7 +22,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
@@ -31,7 +32,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
const { t } = useTranslation(); const { t } = useTranslation();
const [selectedPayments, setSelectedPayments] = useState([]); const [selectedPayments, setSelectedPayments] = useState([]);
const [transInProgress, setTransInProgress] = useState(false); const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({ const [state, setState] = useLocalStorage("accounting-payments-table-state", {
sortedInfo: {}, sortedInfo: {},
search: "" search: ""
}); });
@@ -194,7 +195,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)), onSelectAll: (selected, selectedRows) => setSelectedPayments(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => { onSelect: (record, selected, selectedRows) => {
setSelectedPayments(selectedRows.map((i) => i.id)); setSelectedPayments(selectedRows.map((i) => i.id));
}, },
getCheckboxProps: (record) => ({ getCheckboxProps: (record) => ({

View File

@@ -1,5 +1,5 @@
import { Button, Card, Input, Space, Table } from "antd"; import { Button, Card, Input, Space, Table } from "antd";
import React, { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -10,6 +10,7 @@ import { exportPageLimit } from "../../utils/config";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort, dateSort, statusSort } from "../../utils/sorters"; 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 ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.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"; 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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent); export default connect(mapStateToProps, mapDispatchToProps)(AccountingReceivablesTableComponent);
@@ -30,7 +31,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
const [selectedJobs, setSelectedJobs] = useState([]); const [selectedJobs, setSelectedJobs] = useState([]);
const [transInProgress, setTransInProgress] = useState(false); const [transInProgress, setTransInProgress] = useState(false);
const [state, setState] = useState({ const [state, setState] = useLocalStorage("accounting-receivables-table-state", {
sortedInfo: {}, sortedInfo: {},
search: "" search: ""
}); });
@@ -207,7 +208,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
onChange={handleTableChange} onChange={handleTableChange}
rowSelection={{ rowSelection={{
onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)), onSelectAll: (selected, selectedRows) => setSelectedJobs(selectedRows.map((i) => i.id)),
onSelect: (record, selected, selectedRows, nativeEvent) => { onSelect: (record, selected, selectedRows) => {
setSelectedJobs(selectedRows.map((i) => i.id)); setSelectedJobs(selectedRows.map((i) => i.id));
}, },
getCheckboxProps: (record) => ({ getCheckboxProps: (record) => ({

View File

@@ -14,7 +14,6 @@ import {
Typography Typography
} from "antd"; } from "antd";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -24,14 +23,14 @@ import i18n from "../../translations/i18n";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component"; import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.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 CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.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({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm); export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
@@ -93,7 +92,9 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
}) })
: "" : ""
}`.slice(0, 239), }`.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> <LayoutFormRow grow>

View File

@@ -1,7 +1,7 @@
import React, { forwardRef } from "react"; import { forwardRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const LaborTypeFormItem = ({ value, onChange }, ref) => { const LaborTypeFormItem = ({ value }) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (!value) return null; if (!value) return null;

View File

@@ -1,11 +1,13 @@
import React, { forwardRef } from "react"; import { forwardRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const PartTypeFormItem = ({ value, onChange }, ref) => { const PartTypeFormItem = ({ value }) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (!value) return null; 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); export default forwardRef(PartTypeFormItem);

View File

@@ -1,6 +1,5 @@
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import React, { forwardRef } from "react"; import { forwardRef } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
@@ -8,23 +7,24 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
const ReadOnlyFormItem = ({ bodyshop, value, type = "text", onChange }, ref) => { const ReadOnlyFormItem = ({ bodyshop, value, type = "text" }) => {
if (!value) return null; if (!value) return null;
switch (type) { switch (type) {
case "employee": case "employee": {
const emp = bodyshop.employees.find((e) => e.id === value); const emp = bodyshop.employees.find((e) => e.id === value);
return `${emp?.first_name} ${emp?.last_name}`; return `${emp?.first_name} ${emp?.last_name}`;
}
case "text": case "text":
return <div>{value}</div>; return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
case "currency": case "currency":
return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>; return <div>{Dinero({ amount: Math.round(value * 100) }).toFormat()}</div>;
default: default:
return <div>{value}</div>; return <div style={{ wordWrap: "break-word", overflowWrap: "break-word" }}>{value}</div>;
} }
}; };

View File

@@ -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;

View File

@@ -1,16 +1,16 @@
import { DownCircleFilled } from "@ant-design/icons"; import { DownCircleFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Dropdown } from "antd"; import { Button, Dropdown } from "antd";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries"; import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -24,7 +24,6 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [availableStatuses, setAvailableStatuses] = useState([]); const [availableStatuses, setAvailableStatuses] = useState([]);
const [otherStages, setOtherStages] = useState([]);
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS); const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
const notification = useNotification(); const notification = useNotification();
@@ -32,7 +31,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
mutationUpdateJobstatus({ mutationUpdateJobstatus({
variables: { jobId: job.id, status: status } variables: { jobId: job.id, status: status }
}) })
.then((r) => { .then(() => {
notification["success"]({ message: t("jobs.successes.save") }); notification["success"]({ message: t("jobs.successes.save") });
insertAuditTrail({ insertAuditTrail({
jobid: job.id, jobid: job.id,
@@ -41,7 +40,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
}); });
// refetch(); // refetch();
}) })
.catch((error) => { .catch(() => {
notification["error"]({ message: t("jobs.errors.saving") }); notification["error"]({ message: t("jobs.errors.saving") });
}); });
}; };
@@ -51,19 +50,14 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
if (job && bodyshop) { if (job && bodyshop) {
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) { if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) {
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses); 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)) { } else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) {
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses); 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)) { } else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) {
setAvailableStatuses( setAvailableStatuses(
bodyshop.md_ro_statuses.post_production_statuses.filter( bodyshop.md_ro_statuses.post_production_statuses.filter(
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported (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 { } else {
console.log("Status didn't match any restrictions. Allowing all status changes."); console.log("Status didn't match any restrictions. Allowing all status changes.");
setAvailableStatuses(bodyshop.md_ro_statuses.statuses); setAvailableStatuses(bodyshop.md_ro_statuses.statuses);
@@ -76,16 +70,7 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
...availableStatuses.map((item) => ({ ...availableStatuses.map((item) => ({
key: item, key: item,
label: item label: item
})), }))
...(job.converted
? [
{ type: "divider" },
...otherStages.map((item) => ({
key: item,
label: item
}))
]
: [])
], ],
onClick: (e) => updateJobStatus(e.key) onClick: (e) => updateJobStatus(e.key)
}; };

View File

@@ -1,5 +1,5 @@
import { WarningOutlined } from "@ant-design/icons";
import { Form, Select, Space, Tooltip } from "antd"; import { Form, Select, Space, Tooltip } from "antd";
import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; 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 LaborTypeFormItem from "../form-items-formatted/labor-type-form-item.component";
import PartTypeFormItem from "../form-items-formatted/part-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 ReadOnlyFormItem from "../form-items-formatted/read-only-form-item.component";
import { WarningOutlined } from "@ant-design/icons";
import "./jobs-close-lines.styles.scss"; import "./jobs-close-lines.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
jobRO: selectJobReadOnly jobRO: selectJobReadOnly
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
@@ -24,7 +23,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
return ( return (
<div> <div>
<Form.List name={["joblines"]}> <Form.List name={["joblines"]}>
{(fields, { add, remove, move }) => { {(fields) => {
return ( return (
<table className="jobs-close-table"> <table className="jobs-close-table">
<thead> <thead>

View File

@@ -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 JobEmployeeAssignments from "../job-employee-assignments/job-employee-assignments.container";
import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component"; import JobsRelatedRos from "../jobs-related-ros/jobs-related-ros.component";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.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 ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component"; import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
@@ -102,254 +103,257 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail })
}; };
return ( return (
<Row gutter={[16, 16]} style={{ alignItems: "stretch" }}> <>
<Col {...colSpan}> <Row gutter={[16, 16]} style={{ alignItems: "stretch" }}>
<Card title={"Job Status"} style={{ height: "100%" }}> <Col {...colSpan}>
<div> <Card title={"Job Status"} style={{ height: "100%" }}>
<DataLabel label={t("jobs.fields.status")}> <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> <Space wrap>
{job.status} {job.special_coverage_policy && (
{job.inproduction && <Tag color="#f50">{t("jobs.labels.inproduction")}</Tag>} <Tag color="tomato">
{job.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />} <Space>
{job.iouparent && ( <WarningFilled />
<Link to={`/manage/jobs/${job.iouparent}`}> <span>{t("jobs.labels.specialcoveragepolicy")}</span>
<Tooltip title={t("jobs.labels.iou")}> </Space>
<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> </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> </Space>
</DataLabel> </div>
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}> </Card>
<ProductionListColumnComment record={job} /> </Col>
</DataLabel> <Col {...colSpan}>
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel> <Card
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel> style={{ height: "100%" }}
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull> title={
{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 ? (
disabled ? ( 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}`}> <Link to={`/manage/owners/${job.owner.id}`}>
{vehicleTitle.length > 0 ? vehicleTitle : t("vehicles.labels.novehinfo")} {ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}
</Link> </Link>
) )
) : ( }
<span></span> >
) <div>
} <DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
> {disabled ? (
<div> <PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
<DataLabel key="2" label={t("vehicles.fields.plate_no")}> ) : (
{`${job.plate_no || t("general.labels.na")} (${`${job.plate_st || t("general.labels.na")}`})`} <ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
</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> </DataLabel>
)} <DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
{job.vehicle && job.vehicle.v_paint_codes && ( {disabled ? (
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}> <PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
<span style={{ whiteSpace: "pre" }}> ) : (
{Object.keys(job.vehicle.v_paint_codes) <ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
.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> </DataLabel>
)} <DataLabel key="3" label={t("owners.fields.address")}>
</div> {`${job.ownr_addr1 || ""} ${job.ownr_addr2 || ""} ${
</Card> job.ownr_city || ""
</Col> } ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
<Col {...colSpan}> </DataLabel>
<Card <DataLabel key="4" label={t("owners.fields.ownr_ea")}>
style={{ height: "100%" }} {disabled ? (
title={<span id="job-employee-assignments-title">{t("jobs.labels.employeeassignments")}</span>} <>{job.ownr_ea || ""}</>
id={"job-employee-assignments"} ) : job.ownr_ea ? (
> <a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
<div> ) : null}
<JobEmployeeAssignments job={job} /> </DataLabel>
<Divider style={{ margin: ".5rem" }} /> {job.owner?.tax_number && (
<DataLabel label={t("jobs.labels.labor_hrs")}> <DataLabel key="5" label={t("owners.fields.tax_number")}>
{bodyHrs.toFixed(1)} / {refinishHrs.toFixed(1)} / {(bodyHrs + refinishHrs).toFixed(1)} {job.owner?.tax_number || ""}
</DataLabel> </DataLabel>
</div> )}
</Card> <DataLabel label={t("owners.fields.note")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
</Col> {job.owner?.note || ""}
</Row> </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} />
</>
); );
} }

View File

@@ -12,6 +12,7 @@ import useLocalStorage from "../../utils/useLocalStorage";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container"; import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import JobNotesPinToggle from "../job-notes-pin-toggle/job-notes-pin-toggle.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly jobRO: selectJobReadOnly
@@ -47,6 +48,9 @@ export function JobNotesComponent({
key: "icons", key: "icons",
width: 80, width: 80,
filteredValue: filter?.icons || null, filteredValue: filter?.icons || null,
defaultSortOrder: "desc",
multiple: 1,
sorter: (a, b) => a.pinned - b.pinned,
filters: [ filters: [
{ {
text: t("notes.labels.usernotes"), text: t("notes.labels.usernotes"),
@@ -63,6 +67,7 @@ export function JobNotesComponent({
{record.critical ? <WarningFilled style={{ margin: 4, color: "red" }} /> : null} {record.critical ? <WarningFilled style={{ margin: 4, color: "red" }} /> : null}
{record.private ? <EyeInvisibleFilled style={{ margin: 4 }} /> : null} {record.private ? <EyeInvisibleFilled style={{ margin: 4 }} /> : null}
{record.audit ? <AuditOutlined style={{ margin: 4 }} /> : null} {record.audit ? <AuditOutlined style={{ margin: 4 }} /> : null}
<JobNotesPinToggle note={record} />
</span> </span>
) )
}, },
@@ -100,6 +105,7 @@ export function JobNotesComponent({
dataIndex: "updated_at", dataIndex: "updated_at",
key: "updated_at", key: "updated_at",
defaultSortOrder: "descend", defaultSortOrder: "descend",
multiple: 2,
width: 200, width: 200,
sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at), sorter: (a, b) => new Date(a.updated_at) - new Date(b.updated_at),
render: (text, record) => <DateTimeFormatter>{record.updated_at}</DateTimeFormatter> render: (text, record) => <DateTimeFormatter>{record.updated_at}</DateTimeFormatter>

View File

@@ -23,17 +23,22 @@ export function NoteUpsertModalComponent({ form, noteUpsertModal }) {
return ( return (
<> <>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col span={8}> <Col span={6}>
<Form.Item label={t("notes.fields.critical")} name="critical" valuePropName="checked"> <Form.Item label={t("notes.fields.critical")} name="critical" valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col span={6}>
<Form.Item label={t("notes.fields.private")} name="private" valuePropName="checked"> <Form.Item label={t("notes.fields.private")} name="private" valuePropName="checked">
<Switch /> <Switch />
</Form.Item> </Form.Item>
</Col> </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"> <Form.Item label={t("notes.fields.type")} name="type" initialValue="general">
<Select <Select
options={[ options={[

View File

@@ -1,10 +1,12 @@
import { useMutation } from "@apollo/client"; import { useApolloClient, useMutation } from "@apollo/client";
import { Form, Modal } from "antd"; import { Form, Modal } from "antd";
import React, { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils"; 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 { INSERT_NEW_NOTE, UPDATE_NOTE } from "../../graphql/notes.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { toggleModalVisible } from "../../redux/modals/modals.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 { selectCurrentUser } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
import NoteUpsertModalComponent from "./note-upsert-modal.component"; import NoteUpsertModalComponent from "./note-upsert-modal.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,
@@ -42,6 +43,8 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
const [form] = Form.useForm(); const [form] = Form.useForm();
const { client } = useApolloClient();
useEffect(() => { useEffect(() => {
//Required to prevent infinite looping. //Required to prevent infinite looping.
if (existingNote && open) { if (existingNote && open) {
@@ -65,8 +68,9 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
variables: { variables: {
noteId: existingNote.id, noteId: existingNote.id,
note: values note: values
} },
}).then((r) => { refetchQueries: ["GET_JOB_BY_PK", "QUERY_JOB_CARD_DETAILS", "QUERY_PARTS_QUEUE_CARD_DETAILS"]
}).then(() => {
notification["success"]({ notification["success"]({
message: t("notes.successes.updated") message: t("notes.successes.updated")
}); });
@@ -86,6 +90,33 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
variables: { variables: {
noteInput: [{ ...values, jobid: jobId, created_by: currentUser.email }] 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"] refetchQueries: ["QUERY_NOTES_BY_JOB_PK"]
}); });

View File

@@ -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;

View File

@@ -59,6 +59,7 @@ const ret = {
"shop:dashboard": 3, "shop:dashboard": 3,
"shop:rbac": 5, "shop:rbac": 5,
"shop:reportcenter": 2, "shop:reportcenter": 2,
"shop:responsibilitycenter": 4, // Updated from "shop:responsibility" to "shop:responsibilitycenter"
"shop:templates": 4, "shop:templates": 4,
"shop:vendors": 2, "shop:vendors": 2,

View File

@@ -1,15 +1,16 @@
import { useMutation, useQuery } from "@apollo/client"; import { useMutation, useQuery } from "@apollo/client";
import { Form } from "antd"; import { Form } from "antd";
import dayjs from "../../utils/day"; import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries"; import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import dayjs from "../../utils/day";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import ShopInfoComponent from "./shop-info.component"; import ShopInfoComponent from "./shop-info.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { FEATURE_CONFIGS, useFormDataPreservation } from "./useFormDataPreservation";
export default function ShopInfoContainer() { export default function ShopInfoContainer() {
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -22,16 +23,24 @@ export default function ShopInfoContainer() {
}); });
const notification = useNotification(); 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); setSaveLoading(true);
logImEXEvent("shop_update"); logImEXEvent("shop_update");
updateBodyshop({ updateBodyshop({
variables: { id: data.bodyshops[0].id, shop: values } variables: { id: data.bodyshops[0].id, shop: values }
}) })
.then((r) => { .then(() => {
notification["success"]({ message: t("bodyshop.successes.save") }); notification["success"]({ message: t("bodyshop.successes.save") });
refetch().then((_) => form.resetFields()); refetch().then(() => form.resetFields());
}) })
.catch((error) => { .catch((error) => {
notification["error"]({ notification["error"]({
@@ -39,7 +48,7 @@ export default function ShopInfoContainer() {
}); });
}); });
setSaveLoading(false); setSaveLoading(false);
}; });
useEffect(() => { useEffect(() => {
if (data) form.resetFields(); if (data) form.resetFields();

File diff suppressed because it is too large Load Diff

View 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"]]
}
};

View File

@@ -52,6 +52,7 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
> >
{label} {label}
</div> </div>
{discount && discount !== 0 ? <Tag color="green">{`${discount * 100}%`}</Tag> : null} {discount && discount !== 0 ? <Tag color="green">{`${discount * 100}%`}</Tag> : null}
</div> </div>
); );
@@ -116,6 +117,11 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref
{o.name} {o.name}
</div> </div>
<Space style={{ marginLeft: "1rem" }}> <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.phone && showPhone && <PhoneNumberFormatter>{o.phone}</PhoneNumberFormatter>}
{o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null} {o.discount && o.discount !== 0 ? <Tag color="green">{`${o.discount * 100}%`}</Tag> : null}
</Space> </Space>

View File

@@ -1,7 +1,7 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client"; import { useApolloClient } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react"; 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 { PageHeader } from "@ant-design/pro-layout";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -179,6 +179,18 @@ export function VendorsFormComponent({
} }
</LayoutFormRow> </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" && ( {DmsAp.treatment === "on" && (
<Form.Item label={t("vendors.fields.dmsid")} name="dmsid"> <Form.Item label={t("vendors.fields.dmsid")} name="dmsid">
<Input /> <Input />

View File

@@ -1,5 +1,5 @@
import { SyncOutlined } from "@ant-design/icons"; 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 queryString from "query-string";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -38,6 +38,18 @@ export default function VendorsListComponent({ handleNewVendor, loading, handleO
title: t("vendors.fields.city"), title: t("vendors.fields.city"),
dataIndex: "city", dataIndex: "city",
key: "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>
)
} }
]; ];

View File

@@ -713,6 +713,19 @@ export const GET_JOB_BY_PK = gql`
v_model_yr v_model_yr
v_model_desc v_model_desc
v_vin 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 { vehicle {
id id
jobs { jobs {
@@ -959,6 +972,8 @@ export const QUERY_JOB_CARD_DETAILS = gql`
critical critical
private private
created_at created_at
pinned
type
} }
updated_at updated_at
clm_total clm_total
@@ -984,6 +999,7 @@ export const QUERY_JOB_CARD_DETAILS = gql`
key key
type type
} }
} }
} }
`; `;
@@ -1048,6 +1064,8 @@ export const QUERY_TECH_JOB_DETAILS = gql`
critical critical
private private
created_at created_at
pinned
type
} }
updated_at updated_at
documents(order_by: { created_at: desc }) { 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` 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) { jobs_by_pk(id: $id) {
actual_completion actual_completion
actual_delivery actual_delivery
@@ -2349,6 +2367,19 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
start start
status 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_no
clm_total clm_total
comment comment

View File

@@ -14,6 +14,7 @@ export const INSERT_NEW_NOTE = gql`
updated_at updated_at
audit audit
type type
pinned
} }
} }
} }
@@ -43,6 +44,7 @@ export const QUERY_NOTES_BY_JOB_PK = gql`
updated_at updated_at
audit audit
type type
pinned
} }
} }
} }
@@ -63,6 +65,7 @@ export const UPDATE_NOTE = gql`
updated_at updated_at
audit audit
type type
pinned
} }
} }
} }

View File

@@ -19,6 +19,7 @@ export const QUERY_VENDOR_BY_ID = gql`
active active
phone phone
dmsid dmsid
tags
} }
} }
`; `;
@@ -54,6 +55,7 @@ export const QUERY_ALL_VENDORS = gql`
city city
phone phone
active active
tags
} }
} }
`; `;
@@ -89,6 +91,7 @@ export const QUERY_ALL_VENDORS_FOR_ORDER = gql`
email email
active active
phone phone
tags
} }
jobs(where: { id: { _eq: $jobId } }) { jobs(where: { id: { _eq: $jobId } }) {
v_make_desc v_make_desc
@@ -105,6 +108,7 @@ export const SEARCH_VENDOR_AUTOCOMPLETE = gql`
cost_center cost_center
active active
favorite favorite
tags
} }
} }
`; `;
@@ -124,6 +128,7 @@ export const SEARCH_VENDOR_AUTOCOMPLETE_WITH_ADDR = gql`
email email
state state
active active
tags
} }
} }
`; `;

View File

@@ -2,7 +2,7 @@ import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd"; import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd";
import axios from "axios"; import axios from "axios";
import queryString from "query-string"; import queryString from "query-string";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom"; 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 CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter"; import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import { alphaSort, dateSort } from "../../utils/sorters";
import { pageLimit } from "../../utils/config"; import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })), 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 [searchLoading, setSearchLoading] = useState(false);
const { page } = search; const { page } = search;
const history = useNavigate(); const history = useNavigate();
const [state, setState] = useState({ const [state, setState] = useLocalStorage("bills_list_sort", {
sortedInfo: {}, sortedInfo: {},
filteredInfo: { text: "" } filteredInfo: { text: "" }
}); });

View File

@@ -271,7 +271,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
{ {
required: true required: true
}, },
({ getFieldValue }) => ({ () => ({
validator(_, value) { validator(_, value) {
if (!bodyshop.cdk_dealerid) return Promise.resolve(); if (!bodyshop.cdk_dealerid) return Promise.resolve();
if (!value || dayjs(value).isSameOrAfter(dayjs(), "day")) { 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"))); return Promise.reject(new Error(t("jobs.labels.dms.invoicedatefuture")));
} }
}), }),
({ getFieldValue }) => ({ () => ({
validator(_, value) { validator(_, value) {
if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) { if (ClosingPeriod.treatment === "on" && bodyshop.accountingconfig.ClosingPeriod) {
if ( if (
@@ -369,8 +369,8 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
<Form.List <Form.List
name={["qb_multiple_payers"]} name={["qb_multiple_payers"]}
rules={[ rules={[
({ getFieldValue }) => ({ () => ({
validator(_, value) { validator() {
let totalAllocated = Dinero(); let totalAllocated = Dinero();
const payers = form.getFieldValue("qb_multiple_payers"); const payers = form.getFieldValue("qb_multiple_payers");
@@ -492,7 +492,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
<Statistic <Statistic
title={t("jobs.labels.pimraryamountpayable")} title={t("jobs.labels.pimraryamountpayable")}
valueStyle={{ valueStyle={{
color: discrep.getAmount() > 0 ? "green" : "red" color: discrep.getAmount() >= 0 ? "green" : "red"
}} }}
value={discrep.toFormat()} value={discrep.toFormat()}
/> />

View File

@@ -426,6 +426,11 @@
"messagingtext": "Messaging Preset Text", "messagingtext": "Messaging Preset Text",
"noteslabel": "Note Label", "noteslabel": "Note Label",
"notestext": "Note Text", "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", "partslocation": "Parts Location",
"phone": "Phone", "phone": "Phone",
"prodtargethrs": "Production Target Hours", "prodtargethrs": "Production Target Hours",
@@ -512,6 +517,7 @@
"dashboard": "Shop -> Dashboard", "dashboard": "Shop -> Dashboard",
"rbac": "Shop -> RBAC", "rbac": "Shop -> RBAC",
"reportcenter": "Shop -> Report Center", "reportcenter": "Shop -> Report Center",
"responsibilitycenter": "Shop -> Responsibility Centers",
"templates": "Shop -> Templates", "templates": "Shop -> Templates",
"vendors": "Shop -> Vendors" "vendors": "Shop -> Vendors"
}, },
@@ -648,15 +654,9 @@
"use_paint_scale_data": "Use Paint Scale Data for Job Costing?", "use_paint_scale_data": "Use Paint Scale Data for Job Costing?",
"uselocalmediaserver": "Use Local Media Server?", "uselocalmediaserver": "Use Local Media Server?",
"website": "Website", "website": "Website",
"zip_post": "Zip/Postal Code", "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."
}
}, },
"labels": { "labels": {
"consent_settings": "Phone Number Opt-Out List",
"2tiername": "Name => RO", "2tiername": "Name => RO",
"2tiersetup": "2 Tier Setup", "2tiersetup": "2 Tier Setup",
"2tiersource": "Source => RO", "2tiersource": "Source => RO",
@@ -667,6 +667,7 @@
"apptcolors": "Appointment Colors", "apptcolors": "Appointment Colors",
"businessinformation": "Business Information", "businessinformation": "Business Information",
"checklists": "Checklists", "checklists": "Checklists",
"consent_settings": "Phone Number Opt-Out List",
"csiq": "CSI Questions", "csiq": "CSI Questions",
"customtemplates": "Custom Templates", "customtemplates": "Custom Templates",
"defaultcostsmapping": "Default Costs Mapping", "defaultcostsmapping": "Default Costs Mapping",
@@ -704,6 +705,9 @@
"messagingpresets": "Messaging Presets", "messagingpresets": "Messaging Presets",
"notemplatesavailable": "No templates available to add.", "notemplatesavailable": "No templates available to add.",
"notespresets": "Notes Presets", "notespresets": "Notes Presets",
"notifications": {
"followers": "Notifications"
},
"orderstatuses": "Order Statuses", "orderstatuses": "Order Statuses",
"partslocations": "Parts Locations", "partslocations": "Parts Locations",
"partsscan": "Parts Scanning", "partsscan": "Parts Scanning",
@@ -734,10 +738,7 @@
"ssbuckets": "Job Size Definitions", "ssbuckets": "Job Size Definitions",
"systemsettings": "System Settings", "systemsettings": "System Settings",
"task-presets": "Task Presets", "task-presets": "Task Presets",
"workingdays": "Working Days", "workingdays": "Working Days"
"notifications": {
"followers": "Notifications"
}
}, },
"operations": { "operations": {
"contains": "Contains", "contains": "Contains",
@@ -783,6 +784,15 @@
"completed": "Job checklist completed." "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": { "contracts": {
"actions": { "actions": {
"changerate": "Change Contract Rates", "changerate": "Change Contract Rates",
@@ -1235,11 +1245,11 @@
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.", "fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
"notfound": "No record was found.", "notfound": "No record was found.",
"sizelimit": "The selected items exceed the size limit.", "sizelimit": "The selected items exceed the size limit.",
"submit-for-testing": "Error submitting Job for testing.",
"sub_status": { "sub_status": {
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.", "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." "trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
} },
"submit-for-testing": "Error submitting Job for testing."
}, },
"itemtypes": { "itemtypes": {
"contract": "CC Contract", "contract": "CC Contract",
@@ -1659,8 +1669,6 @@
"adjustment_bottom_line": "Adjustments", "adjustment_bottom_line": "Adjustments",
"adjustmenthours": "Adjustment Hours", "adjustmenthours": "Adjustment Hours",
"alt_transport": "Alt. Trans.", "alt_transport": "Alt. Trans.",
"estimate_sent_approval": "Estimate Sent for Approval",
"estimate_approved": "Estimate Approved",
"area_of_damage_impact": { "area_of_damage_impact": {
"10": "Left Front Side", "10": "Left Front Side",
"11": "Left Front Corner", "11": "Left Front Corner",
@@ -1783,6 +1791,8 @@
"est_ct_ln": "Estimator Last Name", "est_ct_ln": "Estimator Last Name",
"est_ea": "Estimator Email", "est_ea": "Estimator Email",
"est_ph1": "Estimator Phone #", "est_ph1": "Estimator Phone #",
"estimate_approved": "Estimate Approved",
"estimate_sent_approval": "Estimate Sent for Approval",
"federal_tax_payable": "Federal Tax Payable", "federal_tax_payable": "Federal Tax Payable",
"federal_tax_rate": "Federal Tax Rate", "federal_tax_rate": "Federal Tax Rate",
"flat_rate_ats": "Flat Rate ATS?", "flat_rate_ats": "Flat Rate ATS?",
@@ -1966,8 +1976,6 @@
"scheddates": "Schedule Dates" "scheddates": "Schedule Dates"
}, },
"labels": { "labels": {
"sent": "",
"approved": "",
"accountsreceivable": "Accounts Receivable", "accountsreceivable": "Accounts Receivable",
"act_price_ppc": "New Part Price", "act_price_ppc": "New Part Price",
"actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).", "actual_completion_inferred": "$t(jobs.fields.actual_completion) inferred using $t(jobs.fields.scheduled_completion).",
@@ -1982,6 +1990,7 @@
"alreadyaddedtoscoreboard": "Job has already been added to scoreboard. Saving will update the previous entry.", "alreadyaddedtoscoreboard": "Job has already been added to scoreboard. Saving will update the previous entry.",
"alreadyclosed": "This Job has already been closed.", "alreadyclosed": "This Job has already been closed.",
"appointmentconfirmation": "Send confirmation to customer?", "appointmentconfirmation": "Send confirmation to customer?",
"approved": "",
"associationwarning": "Any changes to associations will require updating the data from the new parent record to the Job.", "associationwarning": "Any changes to associations will require updating the data from the new parent record to the Job.",
"audit": "Audit Trail", "audit": "Audit Trail",
"available": "Available", "available": "Available",
@@ -2172,6 +2181,7 @@
"sales": "Sales", "sales": "Sales",
"savebeforeconversion": "You have unsaved changes on the Job. Please save them before converting it. ", "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. ", "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", "specialcoveragepolicy": "Special Coverage Policy Applies",
"state_tax_amt": "Provincial/State Taxes", "state_tax_amt": "Provincial/State Taxes",
"subletsnotcompleted": "Outstanding Sublets", "subletsnotcompleted": "Outstanding Sublets",
@@ -2388,15 +2398,16 @@
}, },
"errors": { "errors": {
"invalidphone": "The phone number is invalid. Unable to open conversation. ", "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. ", "noattachedjobs": "No Jobs have been associated to this conversation. ",
"updatinglabel": "Error updating label. {{error}}", "updatinglabel": "Error updating label. {{error}}"
"no_consent": "This phone number has opted-out of Messaging."
}, },
"labels": { "labels": {
"addlabel": "Add a label to this conversation.", "addlabel": "Add a label to this conversation.",
"archive": "Archive", "archive": "Archive",
"maxtenimages": "You can only select up to a maximum of 10 images at a time.", "maxtenimages": "You can only select up to a maximum of 10 images at a time.",
"messaging": "Messaging", "messaging": "Messaging",
"no_consent": "Opted-out",
"noallowtxt": "This customer has not indicated their permission to be messaged.", "noallowtxt": "This customer has not indicated their permission to be messaged.",
"nojobs": "Not associated to any Job.", "nojobs": "Not associated to any Job.",
"nopush": "Polling Mode Enabled", "nopush": "Polling Mode Enabled",
@@ -2406,8 +2417,7 @@
"selectmedia": "Select Media", "selectmedia": "Select Media",
"sentby": "Sent by {{by}} at {{time}}", "sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...", "typeamessage": "Send a message...",
"unarchive": "Unarchive", "unarchive": "Unarchive"
"no_consent": "Opted-out"
}, },
"render": { "render": {
"conversation_list": "Conversation List" "conversation_list": "Conversation List"
@@ -2427,6 +2437,7 @@
"fields": { "fields": {
"createdby": "Created By", "createdby": "Created By",
"critical": "Critical", "critical": "Critical",
"pinned": "Pinned",
"private": "Private", "private": "Private",
"text": "Contents", "text": "Contents",
"type": "Type", "type": "Type",
@@ -2445,6 +2456,7 @@
"addtorelatedro": "Add to Related ROs", "addtorelatedro": "Add to Related ROs",
"newnoteplaceholder": "Add a note...", "newnoteplaceholder": "Add a note...",
"notetoadd": "Note to Add", "notetoadd": "Note to Add",
"pinned_note": "Pinned Note",
"systemnotes": "System Notes", "systemnotes": "System Notes",
"usernotes": "User Notes" "usernotes": "User Notes"
}, },
@@ -2467,11 +2479,15 @@
"fcm": "Push" "fcm": "Push"
}, },
"labels": { "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": "Add Watchers",
"add-watchers-team": "Add Team Members", "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", "employee-search": "Search for an Employee",
"mark-all-read": "Mark All Read", "mark-all-read": "Mark All Read",
"new-notification-title": "New Notification:", "new-notification-title": "New Notification:",
@@ -2488,8 +2504,7 @@
"teams-search": "Search for a Team", "teams-search": "Search for a Team",
"unwatch": "Unwatch", "unwatch": "Unwatch",
"watch": "Watch", "watch": "Watch",
"watching-issue": "Watching", "watching-issue": "Watching"
"employee-notification": "Notifications are disabled because you do not have an associated Employee record."
}, },
"scenarios": { "scenarios": {
"alternate-transport-changed": "Alternate Transport Changed", "alternate-transport-changed": "Alternate Transport Changed",
@@ -3299,17 +3314,10 @@
"updated": "Scoreboard updated." "updated": "Scoreboard updated."
} }
}, },
"settings": {
"title": "Phone Number Opt-Out List"
},
"tasks": { "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": { "actions": {
"edit": "Edit Task", "edit": "Edit Task",
"new": "New Task", "new": "New Task",
@@ -3324,9 +3332,6 @@
"myTasks": "Mine", "myTasks": "Mine",
"refresh": "Refresh" "refresh": "Refresh"
}, },
"errors": {
"load_failure": "Failed to load Tasks."
},
"date_presets": { "date_presets": {
"completion": "Completion", "completion": "Completion",
"day": "Day", "day": "Day",
@@ -3340,6 +3345,9 @@
"tomorrow": "Tomorrow", "tomorrow": "Tomorrow",
"two_weeks": "Two Weeks" "two_weeks": "Two Weeks"
}, },
"errors": {
"load_failure": "Failed to load Tasks."
},
"failures": { "failures": {
"completed": "Failed to toggle Task completion.", "completed": "Failed to toggle Task completion.",
"created": "Failed to create Task.", "created": "Failed to create Task.",
@@ -3374,6 +3382,16 @@
"remind_at": "Remind At", "remind_at": "Remind At",
"title": "Title" "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": { "placeholders": {
"assigned_to": "Select an Employee", "assigned_to": "Select an Employee",
"billid": "Select a Bill", "billid": "Select a Bill",
@@ -3873,6 +3891,7 @@
"state": "Province/State", "state": "Province/State",
"street1": "Street", "street1": "Street",
"street2": "Address 2", "street2": "Address 2",
"tags": "Tags",
"taxid": "Tax ID", "taxid": "Tax ID",
"terms": "Payment Terms", "terms": "Payment Terms",
"zip": "Zip/Postal Code" "zip": "Zip/Postal Code"
@@ -3889,18 +3908,6 @@
"validation": { "validation": {
"unique_vendor_name": "You must enter a unique vendor name." "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"
} }
} }
} }

View File

@@ -426,6 +426,11 @@
"messagingtext": "", "messagingtext": "",
"noteslabel": "", "noteslabel": "",
"notestext": "", "notestext": "",
"notifications": {
"description": "",
"invalid_followers": "",
"placeholder": ""
},
"partslocation": "", "partslocation": "",
"phone": "", "phone": "",
"prodtargethrs": "", "prodtargethrs": "",
@@ -512,6 +517,7 @@
"dashboard": "", "dashboard": "",
"rbac": "", "rbac": "",
"reportcenter": "", "reportcenter": "",
"responsibilitycenter": "",
"templates": "", "templates": "",
"vendors": "" "vendors": ""
}, },
@@ -648,15 +654,9 @@
"use_paint_scale_data": "", "use_paint_scale_data": "",
"uselocalmediaserver": "", "uselocalmediaserver": "",
"website": "", "website": "",
"zip_post": "", "zip_post": ""
"notifications": {
"description": "",
"placeholder": "",
"invalid_followers": ""
}
}, },
"labels": { "labels": {
"consent_settings": "",
"2tiername": "", "2tiername": "",
"2tiersetup": "", "2tiersetup": "",
"2tiersource": "", "2tiersource": "",
@@ -667,6 +667,7 @@
"apptcolors": "", "apptcolors": "",
"businessinformation": "", "businessinformation": "",
"checklists": "", "checklists": "",
"consent_settings": "",
"csiq": "", "csiq": "",
"customtemplates": "", "customtemplates": "",
"defaultcostsmapping": "", "defaultcostsmapping": "",
@@ -704,6 +705,9 @@
"messagingpresets": "", "messagingpresets": "",
"notemplatesavailable": "", "notemplatesavailable": "",
"notespresets": "", "notespresets": "",
"notifications": {
"followers": ""
},
"orderstatuses": "", "orderstatuses": "",
"partslocations": "", "partslocations": "",
"partsscan": "", "partsscan": "",
@@ -734,10 +738,7 @@
"ssbuckets": "", "ssbuckets": "",
"systemsettings": "", "systemsettings": "",
"task-presets": "", "task-presets": "",
"workingdays": "", "workingdays": ""
"notifications": {
"followers": ""
}
}, },
"operations": { "operations": {
"contains": "", "contains": "",
@@ -783,6 +784,15 @@
"completed": "" "completed": ""
} }
}, },
"consent": {
"associated_owners": "",
"created_at": "",
"no_owners": "",
"phone_1": "",
"phone_2": "",
"phone_number": "",
"text_body": ""
},
"contracts": { "contracts": {
"actions": { "actions": {
"changerate": "", "changerate": "",
@@ -1235,11 +1245,11 @@
"fcm": "", "fcm": "",
"notfound": "", "notfound": "",
"sizelimit": "", "sizelimit": "",
"submit-for-testing": "",
"sub_status": { "sub_status": {
"expired": "", "expired": "",
"trial-expired": "" "trial-expired": ""
} },
"submit-for-testing": ""
}, },
"itemtypes": { "itemtypes": {
"contract": "", "contract": "",
@@ -1651,8 +1661,6 @@
"voiding": "" "voiding": ""
}, },
"fields": { "fields": {
"estimate_sent_approval": "",
"estimate_approved": "",
"active_tasks": "", "active_tasks": "",
"actual_completion": "Realización real", "actual_completion": "Realización real",
"actual_delivery": "Entrega real", "actual_delivery": "Entrega real",
@@ -1783,6 +1791,8 @@
"est_ct_ln": "Apellido del tasador", "est_ct_ln": "Apellido del tasador",
"est_ea": "Correo electrónico del tasador", "est_ea": "Correo electrónico del tasador",
"est_ph1": "Número de teléfono 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_payable": "Impuesto federal por pagar",
"federal_tax_rate": "", "federal_tax_rate": "",
"flat_rate_ats": "", "flat_rate_ats": "",
@@ -1966,8 +1976,6 @@
"scheddates": "" "scheddates": ""
}, },
"labels": { "labels": {
"sent": "",
"approved": "",
"accountsreceivable": "", "accountsreceivable": "",
"act_price_ppc": "", "act_price_ppc": "",
"actual_completion_inferred": "", "actual_completion_inferred": "",
@@ -1982,6 +1990,7 @@
"alreadyaddedtoscoreboard": "", "alreadyaddedtoscoreboard": "",
"alreadyclosed": "", "alreadyclosed": "",
"appointmentconfirmation": "¿Enviar confirmación al cliente?", "appointmentconfirmation": "¿Enviar confirmación al cliente?",
"approved": "",
"associationwarning": "", "associationwarning": "",
"audit": "", "audit": "",
"available": "", "available": "",
@@ -2172,6 +2181,7 @@
"sales": "", "sales": "",
"savebeforeconversion": "", "savebeforeconversion": "",
"scheduledinchange": "", "scheduledinchange": "",
"sent": "",
"specialcoveragepolicy": "", "specialcoveragepolicy": "",
"state_tax_amt": "", "state_tax_amt": "",
"subletsnotcompleted": "", "subletsnotcompleted": "",
@@ -2388,15 +2398,16 @@
}, },
"errors": { "errors": {
"invalidphone": "", "invalidphone": "",
"no_consent": "",
"noattachedjobs": "", "noattachedjobs": "",
"updatinglabel": "", "updatinglabel": ""
"no_consent": ""
}, },
"labels": { "labels": {
"addlabel": "", "addlabel": "",
"archive": "", "archive": "",
"maxtenimages": "", "maxtenimages": "",
"messaging": "Mensajería", "messaging": "Mensajería",
"no_consent": "",
"noallowtxt": "", "noallowtxt": "",
"nojobs": "", "nojobs": "",
"nopush": "", "nopush": "",
@@ -2406,8 +2417,7 @@
"selectmedia": "", "selectmedia": "",
"sentby": "", "sentby": "",
"typeamessage": "Enviar un mensaje...", "typeamessage": "Enviar un mensaje...",
"unarchive": "", "unarchive": ""
"no_consent": ""
}, },
"render": { "render": {
"conversation_list": "" "conversation_list": ""
@@ -2427,6 +2437,7 @@
"fields": { "fields": {
"createdby": "Creado por", "createdby": "Creado por",
"critical": "Crítico", "critical": "Crítico",
"pinned": "",
"private": "Privado", "private": "Privado",
"text": "Contenido", "text": "Contenido",
"type": "", "type": "",
@@ -2445,6 +2456,7 @@
"addtorelatedro": "", "addtorelatedro": "",
"newnoteplaceholder": "Agrega una nota...", "newnoteplaceholder": "Agrega una nota...",
"notetoadd": "", "notetoadd": "",
"pinned_note": "",
"systemnotes": "", "systemnotes": "",
"usernotes": "" "usernotes": ""
}, },
@@ -2467,13 +2479,15 @@
"fcm": "" "fcm": ""
}, },
"labels": { "labels": {
"auto-add-on": "",
"auto-add-off": "",
"auto-add-success": "",
"auto-add-failure": "",
"auto-add-description": "",
"add-watchers": "", "add-watchers": "",
"add-watchers-team": "", "add-watchers-team": "",
"auto-add": "",
"auto-add-description": "",
"auto-add-failure": "",
"auto-add-off": "",
"auto-add-on": "",
"auto-add-success": "",
"employee-notification": "",
"employee-search": "", "employee-search": "",
"mark-all-read": "", "mark-all-read": "",
"new-notification-title": "", "new-notification-title": "",
@@ -2490,8 +2504,7 @@
"teams-search": "", "teams-search": "",
"unwatch": "", "unwatch": "",
"watch": "", "watch": "",
"watching-issue": "", "watching-issue": ""
"employee-notification": ""
}, },
"scenarios": { "scenarios": {
"alternate-transport-changed": "", "alternate-transport-changed": "",
@@ -3301,17 +3314,10 @@
"updated": "" "updated": ""
} }
}, },
"settings": {
"title": ""
},
"tasks": { "tasks": {
"labels": {
"my_tasks_center": "",
"go_to_job": "",
"overdue": "",
"due_today": "",
"upcoming": "",
"no_due_date": "",
"ro-number": "",
"no_tasks": ""
},
"actions": { "actions": {
"edit": "", "edit": "",
"new": "", "new": "",
@@ -3326,9 +3332,6 @@
"myTasks": "", "myTasks": "",
"refresh": "" "refresh": ""
}, },
"errors": {
"load_failure": ""
},
"date_presets": { "date_presets": {
"completion": "", "completion": "",
"day": "", "day": "",
@@ -3342,6 +3345,9 @@
"tomorrow": "", "tomorrow": "",
"two_weeks": "" "two_weeks": ""
}, },
"errors": {
"load_failure": ""
},
"failures": { "failures": {
"completed": "", "completed": "",
"created": "", "created": "",
@@ -3376,6 +3382,16 @@
"remind_at": "", "remind_at": "",
"title": "" "title": ""
}, },
"labels": {
"due_today": "",
"go_to_job": "",
"my_tasks_center": "",
"no_due_date": "",
"no_tasks": "",
"overdue": "",
"ro-number": "",
"upcoming": ""
},
"placeholders": { "placeholders": {
"assigned_to": "", "assigned_to": "",
"billid": "", "billid": "",
@@ -3875,6 +3891,7 @@
"state": "Provincia del estado", "state": "Provincia del estado",
"street1": "calle", "street1": "calle",
"street2": "Dirección 2", "street2": "Dirección 2",
"tags": "",
"taxid": "Identificación del impuesto", "taxid": "Identificación del impuesto",
"terms": "Términos de pago", "terms": "Términos de pago",
"zip": "código postal" "zip": "código postal"
@@ -3891,18 +3908,6 @@
"validation": { "validation": {
"unique_vendor_name": "" "unique_vendor_name": ""
} }
},
"consent": {
"phone_number": "",
"associated_owners": "",
"created_at": "",
"no_owners": "",
"phone_1": "",
"phone_2": "",
"text_body": ""
},
"settings": {
"title": ""
} }
} }
} }

View File

@@ -426,6 +426,11 @@
"messagingtext": "", "messagingtext": "",
"noteslabel": "", "noteslabel": "",
"notestext": "", "notestext": "",
"notifications": {
"description": "",
"invalid_followers": "",
"placeholder": ""
},
"partslocation": "", "partslocation": "",
"phone": "", "phone": "",
"prodtargethrs": "", "prodtargethrs": "",
@@ -512,6 +517,7 @@
"dashboard": "", "dashboard": "",
"rbac": "", "rbac": "",
"reportcenter": "", "reportcenter": "",
"responsibilitycenter": "",
"templates": "", "templates": "",
"vendors": "" "vendors": ""
}, },
@@ -648,15 +654,9 @@
"use_paint_scale_data": "", "use_paint_scale_data": "",
"uselocalmediaserver": "", "uselocalmediaserver": "",
"website": "", "website": "",
"zip_post": "", "zip_post": ""
"notifications": {
"description": "",
"placeholder": "",
"invalid_followers": ""
}
}, },
"labels": { "labels": {
"consent_settings": "",
"2tiername": "", "2tiername": "",
"2tiersetup": "", "2tiersetup": "",
"2tiersource": "", "2tiersource": "",
@@ -667,6 +667,7 @@
"apptcolors": "", "apptcolors": "",
"businessinformation": "", "businessinformation": "",
"checklists": "", "checklists": "",
"consent_settings": "",
"csiq": "", "csiq": "",
"customtemplates": "", "customtemplates": "",
"defaultcostsmapping": "", "defaultcostsmapping": "",
@@ -704,6 +705,9 @@
"messagingpresets": "", "messagingpresets": "",
"notemplatesavailable": "", "notemplatesavailable": "",
"notespresets": "", "notespresets": "",
"notifications": {
"followers": ""
},
"orderstatuses": "", "orderstatuses": "",
"partslocations": "", "partslocations": "",
"partsscan": "", "partsscan": "",
@@ -734,10 +738,7 @@
"ssbuckets": "", "ssbuckets": "",
"systemsettings": "", "systemsettings": "",
"task-presets": "", "task-presets": "",
"workingdays": "", "workingdays": ""
"notifications": {
"followers": ""
}
}, },
"operations": { "operations": {
"contains": "", "contains": "",
@@ -783,6 +784,15 @@
"completed": "" "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": { "contracts": {
"actions": { "actions": {
"changerate": "", "changerate": "",
@@ -1235,11 +1245,11 @@
"fcm": "", "fcm": "",
"notfound": "", "notfound": "",
"sizelimit": "", "sizelimit": "",
"submit-for-testing": "",
"sub_status": { "sub_status": {
"expired": "", "expired": "",
"trial-expired": "" "trial-expired": ""
} },
"submit-for-testing": ""
}, },
"itemtypes": { "itemtypes": {
"contract": "", "contract": "",
@@ -1651,8 +1661,6 @@
"voiding": "" "voiding": ""
}, },
"fields": { "fields": {
"estimate_sent_approval": "",
"estimate_approved": "",
"active_tasks": "", "active_tasks": "",
"actual_completion": "Achèvement réel", "actual_completion": "Achèvement réel",
"actual_delivery": "Livraison réelle", "actual_delivery": "Livraison réelle",
@@ -1783,6 +1791,8 @@
"est_ct_ln": "Nom de l'évaluateur", "est_ct_ln": "Nom de l'évaluateur",
"est_ea": "Courriel de l'évaluateur", "est_ea": "Courriel de l'évaluateur",
"est_ph1": "Numéro de téléphone 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_payable": "Impôt fédéral à payer",
"federal_tax_rate": "", "federal_tax_rate": "",
"flat_rate_ats": "", "flat_rate_ats": "",
@@ -1966,8 +1976,6 @@
"scheddates": "" "scheddates": ""
}, },
"labels": { "labels": {
"sent": "",
"approved": "",
"accountsreceivable": "", "accountsreceivable": "",
"act_price_ppc": "", "act_price_ppc": "",
"actual_completion_inferred": "", "actual_completion_inferred": "",
@@ -1982,6 +1990,7 @@
"alreadyaddedtoscoreboard": "", "alreadyaddedtoscoreboard": "",
"alreadyclosed": "", "alreadyclosed": "",
"appointmentconfirmation": "Envoyer une confirmation au client?", "appointmentconfirmation": "Envoyer une confirmation au client?",
"approved": "",
"associationwarning": "", "associationwarning": "",
"audit": "", "audit": "",
"available": "", "available": "",
@@ -2172,6 +2181,7 @@
"sales": "", "sales": "",
"savebeforeconversion": "", "savebeforeconversion": "",
"scheduledinchange": "", "scheduledinchange": "",
"sent": "",
"specialcoveragepolicy": "", "specialcoveragepolicy": "",
"state_tax_amt": "", "state_tax_amt": "",
"subletsnotcompleted": "", "subletsnotcompleted": "",
@@ -2388,15 +2398,16 @@
}, },
"errors": { "errors": {
"invalidphone": "", "invalidphone": "",
"no_consent": "",
"noattachedjobs": "", "noattachedjobs": "",
"updatinglabel": "", "updatinglabel": ""
"no_consent": ""
}, },
"labels": { "labels": {
"addlabel": "", "addlabel": "",
"archive": "", "archive": "",
"maxtenimages": "", "maxtenimages": "",
"messaging": "Messagerie", "messaging": "Messagerie",
"no_consent": "",
"noallowtxt": "", "noallowtxt": "",
"nojobs": "", "nojobs": "",
"nopush": "", "nopush": "",
@@ -2406,8 +2417,7 @@
"selectmedia": "", "selectmedia": "",
"sentby": "", "sentby": "",
"typeamessage": "Envoyer un message...", "typeamessage": "Envoyer un message...",
"unarchive": "", "unarchive": ""
"no_consent": ""
}, },
"render": { "render": {
"conversation_list": "" "conversation_list": ""
@@ -2427,6 +2437,7 @@
"fields": { "fields": {
"createdby": "Créé par", "createdby": "Créé par",
"critical": "Critique", "critical": "Critique",
"pinned": "",
"private": "privé", "private": "privé",
"text": "Contenu", "text": "Contenu",
"type": "", "type": "",
@@ -2445,6 +2456,7 @@
"addtorelatedro": "", "addtorelatedro": "",
"newnoteplaceholder": "Ajouter une note...", "newnoteplaceholder": "Ajouter une note...",
"notetoadd": "", "notetoadd": "",
"pinned_note": "",
"systemnotes": "", "systemnotes": "",
"usernotes": "" "usernotes": ""
}, },
@@ -2467,13 +2479,15 @@
"fcm": "" "fcm": ""
}, },
"labels": { "labels": {
"auto-add-on": "",
"auto-add-off": "",
"auto-add-success": "",
"auto-add-failure": "",
"auto-add-description": "",
"add-watchers": "", "add-watchers": "",
"add-watchers-team": "", "add-watchers-team": "",
"auto-add": "",
"auto-add-description": "",
"auto-add-failure": "",
"auto-add-off": "",
"auto-add-on": "",
"auto-add-success": "",
"employee-notification": "",
"employee-search": "", "employee-search": "",
"mark-all-read": "", "mark-all-read": "",
"new-notification-title": "", "new-notification-title": "",
@@ -2490,8 +2504,7 @@
"teams-search": "", "teams-search": "",
"unwatch": "", "unwatch": "",
"watch": "", "watch": "",
"watching-issue": "", "watching-issue": ""
"employee-notification": ""
}, },
"scenarios": { "scenarios": {
"alternate-transport-changed": "", "alternate-transport-changed": "",
@@ -3301,17 +3314,10 @@
"updated": "" "updated": ""
} }
}, },
"settings": {
"title": ""
},
"tasks": { "tasks": {
"labels": {
"my_tasks_center": "",
"go_to_job": "",
"overdue": "",
"due_today": "",
"upcoming": "",
"no_due_date": "",
"ro-number": "",
"no_tasks": ""
},
"actions": { "actions": {
"edit": "", "edit": "",
"new": "", "new": "",
@@ -3326,9 +3332,6 @@
"myTasks": "", "myTasks": "",
"refresh": "" "refresh": ""
}, },
"errors": {
"load_failure": ""
},
"date_presets": { "date_presets": {
"completion": "", "completion": "",
"day": "", "day": "",
@@ -3342,6 +3345,9 @@
"tomorrow": "", "tomorrow": "",
"two_weeks": "" "two_weeks": ""
}, },
"errors": {
"load_failure": ""
},
"failures": { "failures": {
"completed": "", "completed": "",
"created": "", "created": "",
@@ -3376,6 +3382,16 @@
"remind_at": "", "remind_at": "",
"title": "" "title": ""
}, },
"labels": {
"due_today": "",
"go_to_job": "",
"my_tasks_center": "",
"no_due_date": "",
"no_tasks": "",
"overdue": "",
"ro-number": "",
"upcoming": ""
},
"placeholders": { "placeholders": {
"assigned_to": "", "assigned_to": "",
"billid": "", "billid": "",
@@ -3875,6 +3891,7 @@
"state": "Etat / Province", "state": "Etat / Province",
"street1": "rue", "street1": "rue",
"street2": "Adresse 2 ", "street2": "Adresse 2 ",
"tags": "",
"taxid": "Identifiant de taxe", "taxid": "Identifiant de taxe",
"terms": "Modalités de paiement", "terms": "Modalités de paiement",
"zip": "Zip / code postal" "zip": "Zip / code postal"
@@ -3891,18 +3908,6 @@
"validation": { "validation": {
"unique_vendor_name": "" "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": ""
} }
} }
} }

View File

@@ -48,24 +48,24 @@ export default async function RenderTemplate(
...(renderAsHtml ...(renderAsHtml
? {} ? {}
: { : {
recipe: "chrome-pdf", recipe: "chrome-pdf",
...(!ignoreCustomMargins && { ...(!ignoreCustomMargins && {
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px" : "50px"
} }
}) })
}), }),
...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}), ...(renderAsExcel ? { recipe: "html-to-xlsx" } : {}),
...(renderAsText ? { recipe: "text" } : {}) ...(renderAsText ? { recipe: "text" } : {})
}, },
@@ -100,14 +100,14 @@ export default async function RenderTemplate(
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px" : "50px"
} }
@@ -182,22 +182,22 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
...(renderAsHtml ...(renderAsHtml
? {} ? {}
: { : {
recipe: "chrome-pdf", recipe: "chrome-pdf",
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px" : "50px"
} }
}), }),
pdfOperations: [ pdfOperations: [
{ {
template: { template: {
@@ -213,14 +213,14 @@ export async function RenderTemplates(templateObjects, bodyshop, renderAsHtml =
chrome: { chrome: {
marginTop: marginTop:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.headerMargin && bodyshop.logo_img_path.headerMargin &&
bodyshop.logo_img_path.headerMargin > 36 bodyshop.logo_img_path.headerMargin > 36
? bodyshop.logo_img_path.headerMargin ? bodyshop.logo_img_path.headerMargin
: "36px", : "36px",
marginBottom: marginBottom:
bodyshop.logo_img_path && bodyshop.logo_img_path &&
bodyshop.logo_img_path.footerMargin && bodyshop.logo_img_path.footerMargin &&
bodyshop.logo_img_path.footerMargin > 50 bodyshop.logo_img_path.footerMargin > 50
? bodyshop.logo_img_path.footerMargin ? bodyshop.logo_img_path.footerMargin
: "50px" : "50px"
}, },
@@ -302,7 +302,6 @@ export const fetchFilterData = async ({ name }) => {
const jsReportFilters = await cleanAxios.get(`${server}/odata/assets?$filter=name eq '${name}.filters'`, { const jsReportFilters = await cleanAxios.get(`${server}/odata/assets?$filter=name eq '${name}.filters'`, {
headers: { Authorization: jsrAuth } headers: { Authorization: jsrAuth }
}); });
console.log("🚀 ~ fetchFilterData ~ jsReportFilters:", jsReportFilters);
let parsedFilterData; let parsedFilterData;
let useShopSpecificTemplate = false; let useShopSpecificTemplate = false;

View File

@@ -4909,6 +4909,7 @@
- critical - critical
- id - id
- jobid - jobid
- pinned
- private - private
- text - text
- type - type
@@ -4923,6 +4924,7 @@
- critical - critical
- id - id
- jobid - jobid
- pinned
- private - private
- text - text
- type - type
@@ -4947,6 +4949,7 @@
- critical - critical
- id - id
- jobid - jobid
- pinned
- private - private
- text - text
- type - type
@@ -7120,6 +7123,7 @@
- state - state
- street1 - street1
- street2 - street2
- tags
- updated_at - updated_at
- zip - zip
select_permissions: select_permissions:
@@ -7143,6 +7147,7 @@
- state - state
- street1 - street1
- street2 - street2
- tags
- updated_at - updated_at
- zip - zip
filter: filter:
@@ -7176,6 +7181,7 @@
- state - state
- street1 - street1
- street2 - street2
- tags
- updated_at - updated_at
- zip - zip
filter: filter:

View File

@@ -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';

View File

@@ -0,0 +1,2 @@
alter table "public"."notes" add column "pinned" boolean
not null default 'false';

View File

@@ -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();

View File

@@ -0,0 +1,2 @@
alter table "public"."vendors" add column "tags" jsonb
not null default jsonb_build_array();