Compare commits

...

21 Commits

Author SHA1 Message Date
Allan Carr
31529fad80 IO-3365 Bills Filtering Server Side
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-10-02 23:43:25 -07:00
Patrick Fic
46be1fa889 Merged in feature/IO-3356-pbs-ro-posting (pull request #2577)
Feature/IO-3356 pbs ro posting

Approved-by: Dave Richer
2025-10-02 20:27:48 +00:00
Patrick Fic
40a8036472 Adjust logging statements. 2025-10-02 13:26:12 -07:00
Patrick Fic
22970ac149 Remove additional RO posting. 2025-10-02 13:20:15 -07:00
Dave Richer
63eeab888f Merged in feature/IO-3388-parts-queue-fix (pull request #2603)
feature/IO-3388 - Parts Queue Fix
2025-10-02 16:56:37 +00:00
Dave
367ced88d8 feature/IO-3388 - Parts Queue Fix 2025-10-02 12:55:50 -04:00
Allan Carr
7f67b01c87 Merged in feature/IO-3387-Mitchell-OP14-Import-USA (pull request #2599)
IO-3384 Mitchell OP14 Import USA

Approved-by: Dave Richer
2025-10-01 23:33:56 +00:00
Patrick Fic
93e137c84e Final changes for pbs. 2025-10-01 13:59:11 -07:00
Allan Carr
36d4a02be0 Merged in feature/IO-3330-CARFAX-Adjustments (pull request #2598)
IO-3330 CARFAX Adjustment

Approved-by: Dave Richer
2025-10-01 18:23:01 +00:00
Allan Carr
31367f278e IO-3330 CARFAX Adjustment
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-09-29 10:08:11 -07:00
Patrick Fic
9ff3311579 Add vehicle query. 2025-09-26 08:19:57 -07:00
Patrick Fic
05385fca6d Reoslve DMS allocation scroll. 2025-09-18 14:26:23 -07:00
Patrick Fic
1355d79fa4 Comments and cleanup. 2025-09-18 14:23:51 -07:00
Patrick Fic
8805538706 Merge branch 'master-AIO' into feature/IO-3356-pbs-ro-posting 2025-09-18 14:06:24 -07:00
Patrick Fic
9e35b0f123 Add and remove logging. 2025-09-18 14:05:14 -07:00
Patrick Fic
fd229d5d09 Debug level changes, vehicle upsert correction, and logic updates. 2025-09-12 13:14:38 -07:00
Patrick Fic
1bb66a5378 Merge branch 'master-AIO' into feature/IO-3356-pbs-ro-posting 2025-09-11 14:40:08 -07:00
Patrick Fic
01fc0cbc08 IO-3356 PBS posting initial working commit. 2025-09-05 10:45:59 -07:00
Patrick Fic
9ab3d8c81a Merge branch 'master-AIO' into feature/IO-XXXX-pbs-ro-posting 2025-09-02 15:24:15 -07:00
Patrick Fic
83c0696a3e Reformat translations. 2025-09-02 15:24:01 -07:00
Patrick Fic
5552c73721 WIP PBS RO 2025-08-22 08:21:29 -07:00
25 changed files with 487 additions and 155 deletions

View File

@@ -5305,6 +5305,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>ro_posting</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>sendmaterialscosting</name>
<definition_loaded>false</definition_loaded>

View File

@@ -92,6 +92,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
rowKey="center"
dataSource={allocationsSummary}
locale={{ emptyText: t("dms.labels.refreshallocations") }}
scroll={{ x: true }}
summary={() => {
const totals =
allocationsSummary &&

View File

@@ -90,7 +90,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||
@@ -103,7 +103,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
<div>
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
<PartsStatusPie joblines_status={joblines_status} />
<Table key="id" columns={columns} dataSource={filteredJobLines ? filteredJobLines : []} />
<Table key="id" columns={columns} dataSource={filteredJobLines || []} />
</CardTemplate>
</div>
);

View File

@@ -318,7 +318,7 @@ export function JobLinesComponent({
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||

View File

@@ -1,8 +1,9 @@
import { useMemo } from "react";
import { Col, Row, Tag, Tooltip } from "antd";
import { Tag, Tooltip } from "antd";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -11,65 +12,67 @@ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export const DEFAULT_COL_LAYOUT = { xs: 24, sm: 24, md: 8, lg: 4, xl: 4, xxl: 4 };
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
export function JobPartsQueueCount({ bodyshop, parts, defaultColLayout = DEFAULT_COL_LAYOUT }) {
export function JobPartsQueueCount({ bodyshop, parts }) {
const { t } = useTranslation();
const partsStatus = useMemo(() => {
if (!parts) return null;
const statusKeys = ["default_bo", "default_ordered", "default_received", "default_returned"];
return parts.reduce(
(acc, val) => {
if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
acc.total = acc.total + val.count;
acc[val.status] = acc[val.status] + val.count;
return acc;
},
{
total: 0,
null: 0,
[bodyshop.md_order_statuses.default_bo]: 0,
[bodyshop.md_order_statuses.default_ordered]: 0,
[bodyshop.md_order_statuses.default_received]: 0,
[bodyshop.md_order_statuses.default_returned]: 0
...Object.fromEntries(statusKeys.map((key) => [bodyshop.md_order_statuses[key], 0]))
}
);
}, [bodyshop, parts]);
if (!parts) return null;
return (
<Row>
<Col {...defaultColLayout}>
<Tooltip title="Total">
<Tag>{partsStatus.total}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title="No Status">
<Tag color="gold">{partsStatus["null"]}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
<Tag color="blue">{partsStatus[bodyshop.md_order_statuses.default_ordered]}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_received}>
<Tag color="green">{partsStatus[bodyshop.md_order_statuses.default_received]}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange">{partsStatus[bodyshop.md_order_statuses.default_returned]}</Tag>
</Tooltip>
</Col>
<Col {...defaultColLayout}>
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
<Tag color="red">{partsStatus[bodyshop.md_order_statuses.default_bo]}</Tag>
</Tooltip>
</Col>
</Row>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(40px, 1fr))",
gap: "8px",
width: "100%",
justifyItems: "start"
}}
>
<Tooltip title="Total">
<Tag style={{ minWidth: "40px", textAlign: "center" }}>{partsStatus.total}</Tag>
</Tooltip>
<Tooltip title={t("dashboard.errors.status_normal")}>
<Tag color="gold" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus["null"]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
<Tag color="red" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_bo]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
<Tag color="blue" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_ordered]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_received}>
<Tag color="green" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_received]}
</Tag>
</Tooltip>
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
<Tag color="orange" style={{ minWidth: "40px", textAlign: "center" }}>
{partsStatus[bodyshop.md_order_statuses.default_returned]}
</Tag>
</Tooltip>
</div>
);
}

View File

@@ -166,7 +166,7 @@ export function JobsList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})

View File

@@ -165,7 +165,7 @@ export function JobsReadyList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})

View File

@@ -145,7 +145,7 @@ export function PartsQueueJobLinesComponent({ loading, jobLines }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||

View File

@@ -171,7 +171,7 @@ export function PartsQueueListComponent({ bodyshop }) {
filters:
bodyshop.md_ro_statuses.active_statuses.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
}) || [],

View File

@@ -34,8 +34,9 @@ const getEmployeeName = (employeeId, employees) => {
return employee ? `${employee.first_name} ${employee.last_name}` : "";
};
const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
const productionListColumnsData = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
const { Enhanced_Payroll } = treatments;
return [
{
title: i18n.t("jobs.actions.viewdetail"),
@@ -313,7 +314,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
activeStatuses
?.map((s) => {
return {
text: s || "No Status*",
text: s || i18n.t("dashboard.errors.status"),
value: [s]
};
})
@@ -584,4 +585,4 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
}
];
};
export default r;
export default productionListColumnsData;

View File

@@ -138,6 +138,15 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
<Switch />
</Form.Item>
)}
{bodyshop.pbs_serialnumber && (
<Form.Item
label={t("bodyshop.fields.dms.ro_posting")}
valuePropName="checked"
name={["pbs_configuration", "ro_posting"]}
>
<Switch />
</Form.Item>
)}
{bodyshop.pbs_serialnumber && (
<Form.Item
label={t("bodyshop.fields.dms.appostingaccount")}

View File

@@ -103,7 +103,7 @@ export function SimplifiedPartsJobsListComponent({
return record.status || t("general.labels.na");
},
filteredValue: filter?.status || null,
filters: bodyshop.md_ro_statuses?.parts_statuses.map((s) => {
filters: bodyshop?.md_ro_statuses?.parts_statuses?.map((s) => {
return { text: s, value: [s] };
}),
onFilter: (value, record) => value.includes(record.status)

View File

@@ -111,7 +111,7 @@ export function TechLookupJobsList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||

View File

@@ -42,7 +42,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
ro_number
}
}
bills_aggregate {
bills_aggregate(where: $where) {
aggregate {
count(distinct: true)
}

View File

@@ -16,7 +16,6 @@ import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { logImEXEvent } from "../../firebase/firebase.utils";
@@ -24,16 +23,12 @@ const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" }))
});
export function BillsListPage({ loading, data, refetch, total, setBillEnterContext }) {
export function BillsListPage({ loading, data, refetch, total, setBillEnterContext, handleTableChange, sortedInfo }) {
const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page } = search;
const history = useNavigate();
const [state, setState] = useLocalStorage("bills_list_sort", {
sortedInfo: {},
filteredInfo: { vendorname: [] }
});
const Templates = TemplateList("bill");
const { t } = useTranslation();
@@ -50,7 +45,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
}),
filters: (vendorsData?.vendors || []).map((v) => ({ text: v.name, value: v.id })),
filteredValue: search.vendorIds ? search.vendorIds.split(",") : null,
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
sortOrder: sortedInfo.columnKey === "vendorname" && sortedInfo.order,
render: (text, record) => <span>{record.vendor.name}</span>
},
{
@@ -58,7 +53,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "invoice_number",
key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order
sortOrder: sortedInfo.columnKey === "invoice_number" && sortedInfo.order
},
{
title: t("jobs.fields.ro_number"),
@@ -68,7 +63,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
sortObject: (order) => ({
job: { ro_number: order === "descend" ? "desc" : "asc" }
}),
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
sortOrder: sortedInfo.columnKey === "ro_number" && sortedInfo.order,
render: (text, record) => record.job && <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
},
{
@@ -76,7 +71,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
sortOrder: sortedInfo.columnKey === "date" && sortedInfo.order,
render: (text, record) => <DateFormatter>{record.date}</DateFormatter>
},
{
@@ -84,7 +79,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total - b.total,
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
sortOrder: sortedInfo.columnKey === "total" && sortedInfo.order,
render: (text, record) => <CurrencyFormatter>{record.total}</CurrencyFormatter>
},
{
@@ -92,7 +87,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "is_credit_memo",
key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
sortOrder: sortedInfo.columnKey === "is_credit_memo" && sortedInfo.order,
render: (text, record) => <Checkbox checked={record.is_credit_memo} />
},
{
@@ -100,7 +95,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "exported",
key: "exported",
sorter: (a, b) => a.exported - b.exported,
sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
sortOrder: sortedInfo.columnKey === "exported" && sortedInfo.order,
render: (text, record) => <Checkbox checked={record.exported} />
},
{
@@ -164,37 +159,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({
sortedInfo: sorter,
filteredInfo: { ...state.filteredInfo, vendorname: filters.vendorname || [] }
});
search.page = pagination.current;
if (filters.vendorname && filters.vendorname.length) {
search.vendorIds = filters.vendorname.join(",");
} else {
delete search.vendorIds;
}
if (sorter && sorter.column && sorter.column.sortObject) {
search.searchObj = JSON.stringify(sorter.column.sortObject(sorter.order));
delete search.sortcolumn;
delete search.sortorder;
} else {
delete search.searchObj;
search.sortcolumn = sorter.order ? sorter.columnKey : null;
search.sortorder = sorter.order;
}
history({ search: queryString.stringify(search) });
logImEXEvent("bills_list_sort_filter", { pagination, filters, sorter });
};
useEffect(() => {
if (!search.vendorIds && state.filteredInfo.vendorname && state.filteredInfo.vendorname.length) {
search.vendorIds = state.filteredInfo.vendorname.join(",");
history({ search: queryString.stringify(search) });
}
}, []);
// (State & URL handling moved to container - Option C)
useEffect(() => {
if (search.search && search.search.trim() !== "") {

View File

@@ -3,13 +3,14 @@ import queryString from "query-string";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import AlertComponent from "../../components/alert/alert.component";
import BillDetailEditContainer from "../../components/bill-detail-edit/bill-detail-edit.container";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { QUERY_ALL_BILLS_PAGINATED } from "../../graphql/bills.queries";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import BillsPageComponent from "./bills.page.component";
import useLocalStorage from "../../utils/useLocalStorage";
import { pageLimit } from "../../utils/config";
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
@@ -23,7 +24,9 @@ const mapDispatchToProps = (dispatch) => ({
export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
const searchParams = queryString.parse(useLocation().search);
const location = useLocation();
const history = useNavigate();
const searchParams = queryString.parse(location.search);
const { page, sortcolumn, sortorder, searchObj } = searchParams;
useEffect(() => {
@@ -37,6 +40,12 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
setBreadcrumbs([{ link: "/manage/bills", label: t("titles.bc.bills-list") }]);
}, [t, setBreadcrumbs, setSelectedHeader]);
// Persisted table state (sorting & filtering)
const [persistState, setPersistState] = useLocalStorage("bills_list_sort", {
sortedInfo: {},
filteredInfo: { vendorname: [] }
});
const { loading, error, data, refetch } = useQuery(QUERY_ALL_BILLS_PAGINATED, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
@@ -54,6 +63,90 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
}
});
const handleTableChange = (pagination, filters, sorter) => {
const search = queryString.parse(location.search);
const vendorArr = filters?.vendorname ?? [];
const newVendorIds = vendorArr.length ? vendorArr.join(",") : undefined;
const vendorFilterChanged = search.vendorIds !== newVendorIds;
search.page = vendorFilterChanged || !search.page ? 1 : pagination.current;
newVendorIds ? (search.vendorIds = newVendorIds) : delete search.vendorIds;
const { columnKey, order, column } = sorter || {};
if (column?.sortObject) {
search.searchObj = JSON.stringify(column.sortObject(order));
delete search.sortcolumn;
delete search.sortorder;
} else {
delete search.searchObj;
search.sortcolumn = order ? columnKey : null;
search.sortorder = order ?? null; // keep explicit null to mirror prior behavior
}
setPersistState({
sortedInfo: sorter || {},
filteredInfo: { vendorname: vendorArr }
});
history({ search: queryString.stringify(search) });
};
useEffect(() => {
const search = queryString.parse(location.search);
let changed = false;
const vendorPersisted = persistState.filteredInfo.vendorname || [];
if (!search.vendorIds && vendorPersisted.length) {
search.vendorIds = vendorPersisted.join(",");
search.page = 1; // reset page when injecting filter
changed = true;
}
const { sortedInfo } = persistState;
if (!search.searchObj && !search.sortcolumn && sortedInfo?.order) {
const { columnKey, order } = sortedInfo;
if (columnKey) {
const dir = order === "descend" ? "desc" : "asc";
if (columnKey === "vendorname") {
search.searchObj = JSON.stringify({ vendor: { name: dir } });
} else if (columnKey === "ro_number") {
search.searchObj = JSON.stringify({ job: { ro_number: dir } });
} else {
search.sortcolumn = columnKey;
search.sortorder = order;
}
changed = true;
}
}
if (changed) {
history({ search: queryString.stringify(search) });
return;
}
const hasPersistSort = !!sortedInfo?.order;
const hasUrlSort = !!(search.searchObj || (search.sortcolumn && search.sortorder));
if (!hasPersistSort && hasUrlSort) {
let derived = {};
if (search.searchObj) {
try {
const o = JSON.parse(search.searchObj);
if (o.vendor?.name) {
derived = { columnKey: "vendorname", order: o.vendor.name === "desc" ? "descend" : "ascend" };
} else if (o.job?.ro_number) {
derived = { columnKey: "ro_number", order: o.job.ro_number === "desc" ? "descend" : "ascend" };
}
} catch {
/* ignore parse errors */
}
} else {
derived = { columnKey: search.sortcolumn, order: search.sortorder };
}
if (derived.order) setPersistState((prev) => ({ ...prev, sortedInfo: derived }));
}
}, [location.search]);
if (error) return <AlertComponent message={error.message} type="error" />;
return (
<FeatureWrapperComponent
@@ -71,6 +164,8 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
loading={loading}
refetch={refetch}
total={data ? data.bills_aggregate.aggregate.count : 0}
handleTableChange={handleTableChange}
sortedInfo={persistState.sortedInfo || {}}
/>
<BillDetailEditContainer />

View File

@@ -48,7 +48,7 @@ export const socket = SocketIO(
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
const { t } = useTranslation();
const [logLevel, setLogLevel] = useState("DEBUG");
const [logLevel, setLogLevel] = useState(determineDmsType(bodyshop) === "pbs" ? "INFO" : "DEBUG");
const history = useNavigate();
const [logs, setLogs] = useState([]);
const search = queryString.parse(useLocation().search);

View File

@@ -97,7 +97,7 @@ export function TechAssignedProdJobs({ setTimeTicketTaskContext, technician, bod
.filter(onlyUnique)
.map((s) => {
return {
text: s || "No Status*",
text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||

View File

@@ -321,6 +321,7 @@
"itc_local": "Local Tax is ITC?",
"itc_state": "State Tax is ITC?",
"mappingname": "DMS Mapping Name",
"ro_posting": "Create $0 RO?",
"sendmaterialscosting": "Materials Cost as % of Sale",
"srcco": "Source Company #/Dealer #"
},
@@ -996,6 +997,7 @@
"insco": "No Ins. Co.*",
"refreshrequired": "You must refresh the dashboard data to see this component.",
"status": "No Status*",
"status_normal": "No Status",
"updatinglayout": "Error saving updated layout {{message}}"
},
"labels": {

View File

@@ -321,6 +321,7 @@
"itc_local": "",
"itc_state": "",
"mappingname": "",
"ro_posting": "",
"sendmaterialscosting": "",
"srcco": ""
},
@@ -996,6 +997,7 @@
"insco": "",
"refreshrequired": "",
"status": "",
"status_normal": "",
"updatinglayout": ""
},
"labels": {

View File

@@ -321,6 +321,7 @@
"itc_local": "",
"itc_state": "",
"mappingname": "",
"ro_posting": "",
"sendmaterialscosting": "",
"srcco": ""
},
@@ -996,6 +997,7 @@
"insco": "",
"refreshrequired": "",
"status": "",
"status_normal": "",
"updatinglayout": ""
},
"labels": {

View File

@@ -8,7 +8,7 @@
value_from_env: DATAPUMP_AUTH
- name: CARFAX Data Pump
webhook: '{{HASURA_API_URL}}/data/carfax'
schedule: 0 7 * * 6
schedule: 0 7 * * 0
include_in_metadata: true
payload: {}
headers:

View File

@@ -6,10 +6,6 @@ const PBS_CREDENTIALS = {
};
exports.PBS_CREDENTIALS = PBS_CREDENTIALS;
// const cdkDomain =
// process.env.NODE_ENV === "production"
// ? "https://3pa.dmotorworks.com"
// : "https://uat-3pa.dmotorworks.com";
const pbsDomain = `https://partnerhub.pbsdealers.com/json/reply`;
exports.PBS_ENDPOINTS = {
@@ -18,5 +14,9 @@ exports.PBS_ENDPOINTS = {
VehicleGet: `${pbsDomain}/VehicleGet`,
AccountingPostingChange: `${pbsDomain}/AccountingPostingChange`,
ContactChange: `${pbsDomain}/ContactChange`,
VehicleChange: `${pbsDomain}/VehicleChange`
VehicleChange: `${pbsDomain}/VehicleChange`,
RepairOrderChange: `${pbsDomain}/RepairOrderChange`, //TODO: Verify that this is correct. Docs have /reply/ in path.
RepairOrderGet: `${pbsDomain}/RepairOrderGet`,
RepairOrderContactVehicleGet: `${pbsDomain}/RepairOrderContactVehicleGet`,
RepairOrderContactVehicleChange: `${pbsDomain}/RepairOrderContactVehicleChange`,
};

View File

@@ -19,12 +19,11 @@ axios.interceptors.request.use((x) => {
...x.headers[x.method],
...x.headers
};
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
//console.log(printable);
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
//logRequestToFile(printable);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
CdkBase.createJsonEvent(socket, "DEBUG", `Raw Request: ${printable}`, x.data);
return x;
});
@@ -32,23 +31,39 @@ axios.interceptors.request.use((x) => {
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
//console.log(printable);
CdkBase.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
const printable = `${new Date()} | Response: ${x.status} ${x.statusText} |${JSON.stringify(x.data)}`;
//logRequestToFile(printable);
CdkBase.createJsonEvent(socket, "DEBUG", `Raw Response: ${printable}`, x.data);
return x;
});
const fs = require('fs');
const path = require("path");
function logRequestToFile(printable) {
try {
const logDir = path.join(process.cwd(), "logs");
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, "pbs-http.log");
fs.appendFileSync(logFile, `${printable}\n`);
} catch (err) {
console.error("Unexpected error in logRequestToFile:", err);
}
}
exports.default = async function (socket, { txEnvelope, jobid }) {
socket.logEvents = [];
socket.recordid = jobid;
socket.txEnvelope = txEnvelope;
try {
CdkBase.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
CdkBase.createLogEvent(socket, "INFO", `Received Job export request for id ${jobid}`);
const JobData = await QueryJobData(socket, jobid);
socket.JobData = JobData;
CdkBase.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
CdkBase.createLogEvent(socket, "INFO", `Querying the DMS for the Vehicle Record.`);
//Query for the Vehicle record to get the associated customer.
socket.DmsVeh = await QueryVehicleFromDms(socket);
//Todo: Need to validate the lines and methods below.
@@ -69,42 +84,52 @@ exports.default = async function (socket, { txEnvelope, jobid }) {
exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selectedCustomerId) {
try {
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
CdkBase.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
socket.selectedCustomerId = selectedCustomerId;
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle !== true) {
CdkBase.createLogEvent(socket, "INFO", `User selected customer ${selectedCustomerId || "NEW"}`);
//Upsert the contact information as per Wafaa's Email.
CdkBase.createLogEvent(
socket,
"DEBUG",
`Upserting contact information to DMS for ${
socket.JobData.ownr_fn || ""
"INFO",
`Upserting contact information to DMS for ${socket.JobData.ownr_fn || ""
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
);
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
CdkBase.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
await UpsertVehicleData(socket, ownerRef.ReferenceId);
socket.ownerRef = ownerRef;
CdkBase.createLogEvent(socket, "INFO", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
const vehicleRef = await UpsertVehicleData(socket, ownerRef.ReferenceId);
socket.vehicleRef = vehicleRef;
} else {
CdkBase.createLogEvent(
socket,
"DEBUG",
`Contact and Vehicle updates disabled. Skipping to accounting data insert.`
"INFO",
`Contact and Vehicle updates disabled. Querying data and skipping to accounting data insert.`
);
//Must query for records to insert $0 RO.
if (!socket.ownerRef) {
const ownerRef = (await QueryCustomerBycodeFromDms(socket, selectedCustomerId))?.[0];
socket.ownerRef = ownerRef;
}
const vehicleRef = await GetVehicleData(socket, socket.ownerRef?.ReferenceId || socket.selectedCustomerId);
socket.vehicleRef = vehicleRef;
}
CdkBase.createLogEvent(socket, "DEBUG", `Inserting account data.`);
CdkBase.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
CdkBase.createLogEvent(socket, "INFO", `Inserting accounting posting data..`);
const insertResponse = await InsertAccountPostingData(socket);
if (insertResponse.WasSuccessful) {
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
await MarkJobExported(socket, socket.JobData.id);
if (socket.JobData.bodyshop.pbs_configuration.ro_posting) {
await CreateRepairOrderInPBS(socket, socket.ownerRef, socket.vehicleRef)
}
CdkBase.createLogEvent(socket, "INFO", `Marking job as exported.`);
await MarkJobExported(socket, socket.JobData.id);
socket.emit("export-success", socket.JobData.id);
} else {
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
}
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
await InsertFailedExportLog(socket, error);
}
};
@@ -112,22 +137,22 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
// Was Successful
async function CheckForErrors(socket, response) {
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
CdkBase.createLogEvent(socket, "INFO", `Successful response from DMS. ${response.Message || ""}`);
} else {
CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
CdkBase.createLogEvent(socket, "SILLY", `Error received from DMS: ${JSON.stringify(response)}`);
CdkBase.createLogEvent(socket, "DEBUG", `Error received from DMS: ${JSON.stringify(response)}`);
}
}
exports.CheckForErrors = CheckForErrors;
async function QueryJobData(socket, jobid) {
CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
CdkBase.createLogEvent(socket, "INFO", `Querying job data for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.QUERY_JOBS_FOR_PBS_EXPORT, { id: jobid });
CdkBase.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(socket, "DEBUG", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
@@ -247,15 +272,15 @@ async function UpsertContactData(socket, selectedCustomerId) {
Code: socket.JobData.owner.accountingid,
...(socket.JobData.ownr_co_nm
? {
//LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_co_nm,
IsBusiness: true
}
//LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_co_nm,
IsBusiness: true
}
: {
LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_fn,
IsBusiness: false
}),
LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_fn,
IsBusiness: false
}),
//Salutation: "String",
//MiddleName: "String",
@@ -332,7 +357,7 @@ async function UpsertVehicleData(socket, ownerRef) {
//FleetNumber: "String",
//Status: "String",
OwnerRef: ownerRef, // "00000000000000000000000000000000",
ModelNumber: socket.JobData.vehicle && socket.JobData.vehicle.v_makecode,
// ModelNumber: socket.JobData.vehicle && socket.JobData.vehicle.v_makecode,
Make: socket.JobData.v_make_desc,
Model: socket.JobData.v_model_desc,
Trim: socket.JobData.vehicle && socket.JobData.vehicle.v_trimcode,
@@ -340,7 +365,7 @@ async function UpsertVehicleData(socket, ownerRef) {
Year: socket.JobData.v_model_yr,
Odometer: socket.JobData.kmout,
ExteriorColor: {
Code: socket.JobData.v_color,
// Code: socket.JobData.v_color,
Description: socket.JobData.v_color
}
// InteriorColor: { Code: "String", Description: "String" },
@@ -470,6 +495,57 @@ async function UpsertVehicleData(socket, ownerRef) {
}
}
async function GetVehicleData(socket, ownerRef) {
try {
const { data: { Vehicles } } = await axios.post(
PBS_ENDPOINTS.VehicleGet,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
// "VehicleId": "00000000000000000000000000000000",
// "Year": "String",
// "YearFrom": "String",
// "YearTo": "String",
// "Make": "String",
// "Model": "String",
// "Trim": "String",
// "ModelNumber": "String",
// "StockNumber": "String",
VIN: socket.JobData.v_vin,
// "LicenseNumber": "String",
// "Lot": "String",
// "Status": "String",
// "StatusList": ["String"],
// "OwnerRef": "00000000000000000000000000000000",
// "ModifiedSince": "0001-01-01T00:00:00.0000000Z",
// "ModifiedUntil": "0001-01-01T00:00:00.0000000Z",
// "LastSaleSince": "0001-01-01T00:00:00.0000000Z",
// "VehicleIDList": ["00000000000000000000000000000000"],
// "IncludeInactive": false,
// "IncludeBuildVehicles": false,
// "IncludeBlankLot": false,
// "ShortVIN": "String",
// "ResultLimit": 0,
// "LotAccessDivisions": [0],
// "OdometerTo": 0,
// "OdometerFrom": 0
}
,
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, Vehicles);
if (Vehicles.length === 1) {
return Vehicles[0];
} else {
CdkBase.createLogEvent(socket, "ERROR", `Error in Getting Vehicle Data - ${Vehicles.length} vehicle(s) found`);
}
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
throw new Error(error);
}
}
async function InsertAccountPostingData(socket) {
try {
const allocations = await CalculateAllocations(socket, socket.JobData.id);
@@ -572,7 +648,7 @@ async function InsertAccountPostingData(socket) {
}
async function MarkJobExported(socket, jobid) {
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
CdkBase.createLogEvent(socket, "INFO", `Marking job as exported for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
@@ -618,3 +694,158 @@ async function InsertFailedExportLog(socket, error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
}
}
async function CreateRepairOrderInPBS(socket) {
try {
const { RepairOrders } = await RepairOrderGet(socket);
if (RepairOrders.length === 0) {
const InsertedRepairOrder = await RepairOrderChange(socket)
socket.InsertedRepairOrder = InsertedRepairOrder;
CdkBase.createLogEvent(socket, "INFO", `No repair orders found for vehicle. Inserting record.`);
} else if (RepairOrders.length > 0) {
//Find out if it's a matching RO.
//This logic is used because the integration will simply add another line to an open RO if it exists.
const matchingRo = RepairOrders.find(ro => ro.Memo?.toLowerCase()?.includes(socket.JobData.ro_number.toLowerCase()))
if (!matchingRo) {
CdkBase.createLogEvent(socket, "INFO", `ROs found for vehicle, but none match. Inserting record.`);
const InsertedRepairOrder = await RepairOrderChange(socket)
socket.InsertedRepairOrder = InsertedRepairOrder;
} else {
CdkBase.createLogEvent(socket, "WARN", `Repair order appears to already exist in PBS. ${matchingRo.RepairOrderNumber}`);
}
}
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in CreateRepairOrderInPBS - ${error} - ${JSON.stringify(error)}`);
}
}
async function RepairOrderGet(socket) {
try {
const { data: RepairOrderGet } = await axios.post(
PBS_ENDPOINTS.RepairOrderGet,
{
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
//"RepairOrderId": "374728766",
//"RepairOrderNumber": "4" || socket.JobData.ro_number,
//"RawRepairOrderNumber": socket.JobData.ro_number,
// "Tag": "String",
//"ContactRef": socket.contactRef,
// "ContactRefList": ["00000000000000000000000000000000"],
"VehicleRef": socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId,
// "VehicleRefList": ["00000000000000000000000000000000"],
// "Status": "String",
// "CashieredSince": "0001-01-01T00:00:00.0000000Z",
// "CashieredUntil": "0001-01-01T00:00:00.0000000Z",
// "OpenDateSince": "0001-01-01T00:00:00.0000000Z",
// "OpenDateUntil": "0001-01-01T00:00:00.0000000Z",
//"ModifiedSince": "2025-01-01T00:00:00.0000000Z",
// "ModifiedUntil": "0001-01-01T00:00:00.0000000Z",
// "Shop": "String"
},
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, RepairOrderGet);
return RepairOrderGet;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
throw new Error(error);
}
}
async function RepairOrderChange(socket) {
try {
const { data: RepairOrderChangeResponse } = await axios.post(
PBS_ENDPOINTS.RepairOrderChange,
{ //Additional details at https://partnerhub.pbsdealers.com/json/metadata?op=RepairOrderChange
"RepairOrderInfo": {
//"Id": "string/00000000-0000-0000-0000-000000000000",
//"RepairOrderId": "00000000000000000000000000000000",
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
"RepairOrderNumber": "00000000000000000000000000000000", //This helps force a new RO.
"RawRepairOrderNumber": "00000000000000000000000000000000",
// "RepairOrderNumber": socket.JobData.ro_number, //These 2 values are ignored as confirmed by PBS.
// "RawRepairOrderNumber": socket.JobData.ro_number,
"DateOpened": moment(),
// "DateOpenedUTC": "0001-01-01T00:00:00.0000000Z",
// "DateCashiered": "0001-01-01T00:00:00.0000000Z",
// "DateCashieredUTC": "0001-01-01T00:00:00.0000000Z",
"DatePromised": socket.JobData.scheduled_completion,
// "DatePromisedUTC": "0001-01-01T00:00:00.0000000Z",
"DateVehicleCompleted": socket.JobData.actual_completion,
// "DateCustomerNotified": "0001-01-01T00:00:00.0000000Z",
// "CSR": "String",
// "CSRRef": "00000000000000000000000000000000",
// "BookingUser": "String",
// "BookingUserRef": "00000000000000000000000000000000",
"ContactRef": socket.ownerRef?.ReferenceId || socket.ownerRef?.ContactId,
"VehicleRef": socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId,
"MileageIn": socket.JobData.km_in,
"Tag": "BODYSHOP",
//"Status": "CLOSED", //Values here do not impact the status. Confirmed by PBS support.
Requests: [
{
// "RepairOrderRequestRef": "b1842ecad62c4279bbc2fef4f6bf6cde",
// "RepairOrderRequestId": 1,
// "CSR": "PBS",
// "CSRRef": "1ce12ac692564e94bda955d529ee911a",
// "Skill": "GEN",
"RequestCode": "MISC",
"RequestDescription": `VEHICLE REPAIRED AT BODYSHOP. PLEASE REFERENCE IMEX SHOP MANAGEMENT SYSTEM. ${socket.txEnvelope.story}`,
"Status": "Completed",
// "TechRef": "00000000000000000000000000000000",
"AllowedHours": 0,
"EstimateLabour": 0,
"EstimateParts": 0,
"ComeBack": false,
"AddedOperation": true,
"PartLines": [],
"PartRequestLines": [],
"LabourLines": [],
"SubletLines": [],
"TimePunches": [],
"Summary": {
"Labour": 0,
"Parts": 0,
"OilGas": 0,
"SubletTow": 0,
"Misc": 0,
"Environment": 0,
"ShopSupplies": 0,
"Freight": 0,
"WarrantyDeductible": 0,
"Discount": 0,
"SubTotal": 0,
"Tax1": 0,
"Tax2": 0,
"InvoiceTotal": 0,
"CustomerDeductible": 0,
"GrandTotal": 0,
"LabourDiscount": 0,
"PartDiscount": 0,
"ServiceFeeTotal": 0,
"OEMDiscount": 0
},
"LineType": "RequestLine",
},
],
"Memo": socket.txEnvelope.story,
},
"IsAsynchronous": false,
// "UserRequest": "String",
// "UserRef": "00000000000000000000000000000000"
}
,
{ auth: PBS_CREDENTIALS, socket }
);
CheckForErrors(socket, RepairOrderChangeResponse);
return RepairOrderChangeResponse;
} catch (error) {
CdkBase.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
throw new Error(error);
}
}

View File

@@ -420,6 +420,8 @@ query QUERY_JOBS_FOR_PBS_EXPORT($id: uuid!) {
v_make_desc
v_color
ca_customer_gst
scheduled_completion
actual_completion
vehicle {
v_trimcode
v_makecode
@@ -2159,18 +2161,16 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $
exports.INSERT_NEW_TRANSITION = (
includeOldTransition
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${
includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
}) {
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
}) {
insert_transitions_one(object: $newTransition) {
id
}
${
includeOldTransition
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
${includeOldTransition
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
affected_rows
}`
: ""
: ""
}
}`;