Merge remote-tracking branch 'origin/feature/IO-2776-cdk-fortellis' into feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -113,6 +113,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 &&
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -318,7 +318,7 @@ export function JobLinesComponent({
|
||||
.filter(onlyUnique)
|
||||
.map((s) => {
|
||||
return {
|
||||
text: s || "No Status*",
|
||||
text: s || t("dashboard.errors.status"),
|
||||
value: [s]
|
||||
};
|
||||
})) ||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
})) ||
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
}) || [],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -425,7 +425,15 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
]
|
||||
: [])
|
||||
]
|
||||
: [])
|
||||
: []),
|
||||
<Form.Item
|
||||
key="accumulatePayableLines"
|
||||
name={["accountingconfig", "accumulatePayableLines"]}
|
||||
label={t("bodyshop.fields.accumulatePayableLines")}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
]}
|
||||
</LayoutFormRow>
|
||||
<FeatureWrapper featureName="scoreboard" noauth={() => null}>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
})) ||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
|
||||
import { getFirestore } from "@firebase/firestore";
|
||||
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
|
||||
import { store } from "../redux/store";
|
||||
import * as amplitude from '@amplitude/analytics-browser';
|
||||
//import * as amplitude from '@amplitude/analytics-browser';
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||
@@ -91,14 +91,14 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
// dbevent: false,
|
||||
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
|
||||
// });
|
||||
console.log(
|
||||
"%c[Analytics]",
|
||||
"background-color: green ;font-weight:bold;",
|
||||
eventName,
|
||||
eventParams
|
||||
);
|
||||
// console.log(
|
||||
// "%c[Analytics]",
|
||||
// "background-color: green ;font-weight:bold;",
|
||||
// eventName,
|
||||
// eventParams
|
||||
// );
|
||||
logEvent(analytics, eventName, eventParams);
|
||||
amplitude.track(eventName, eventParams);
|
||||
//amplitude.track(eventName, eventParams);
|
||||
posthog.capture(eventName, eventParams);
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -42,7 +42,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
|
||||
ro_number
|
||||
}
|
||||
}
|
||||
bills_aggregate {
|
||||
bills_aggregate(where: $where) {
|
||||
aggregate {
|
||||
count(distinct: true)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { persistor, store } from "./redux/store";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import "./translations/i18n";
|
||||
import "./utils/CleanAxios";
|
||||
import * as amplitude from "@amplitude/analytics-browser";
|
||||
//import * as amplitude from "@amplitude/analytics-browser";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import posthog from "posthog-js";
|
||||
|
||||
@@ -26,23 +26,23 @@ registerSW({ immediate: true });
|
||||
// Dinero.globalLocale = "en-CA";
|
||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||
|
||||
amplitude.init(import.meta.env.VITE_APP_AMP_KEY, {
|
||||
defaultTracking: true,
|
||||
serverUrl: import.meta.env.VITE_APP_AMP_URL
|
||||
// {
|
||||
// attribution: {
|
||||
// excludeReferrers: true,
|
||||
// initialEmptyValue: true,
|
||||
// resetSessionOnNewCampaign: true,
|
||||
// },
|
||||
// fileDownloads: true,
|
||||
// formInteractions: true,
|
||||
// pageViews: {
|
||||
// trackHistoryChanges: 'all'
|
||||
// },
|
||||
// sessions: true
|
||||
// }
|
||||
});
|
||||
// amplitude.init(import.meta.env.VITE_APP_AMP_KEY, {
|
||||
// defaultTracking: true,
|
||||
// serverUrl: import.meta.env.VITE_APP_AMP_URL
|
||||
// // {
|
||||
// // attribution: {
|
||||
// // excludeReferrers: true,
|
||||
// // initialEmptyValue: true,
|
||||
// // resetSessionOnNewCampaign: true,
|
||||
// // },
|
||||
// // fileDownloads: true,
|
||||
// // formInteractions: true,
|
||||
// // pageViews: {
|
||||
// // trackHistoryChanges: 'all'
|
||||
// // },
|
||||
// // sessions: true
|
||||
// // }
|
||||
// });
|
||||
|
||||
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
|
||||
autocapture: false,
|
||||
|
||||
@@ -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() !== "") {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -50,7 +50,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);
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
})) ||
|
||||
|
||||
@@ -49,7 +49,7 @@ import {
|
||||
validatePasswordResetSuccess
|
||||
} from "./user.actions";
|
||||
import UserActionTypes from "./user.types";
|
||||
import * as amplitude from '@amplitude/analytics-browser';
|
||||
//import * as amplitude from '@amplitude/analytics-browser';
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
const fpPromise = FingerprintJS.load();
|
||||
@@ -92,7 +92,7 @@ export function* isUserAuthenticated() {
|
||||
}
|
||||
|
||||
LogRocket.identify(user.email);
|
||||
amplitude.setUserId(user.email);
|
||||
//amplitude.setUserId(user.email);
|
||||
posthog.identify(user.email);
|
||||
|
||||
const eulaQuery = yield client.query({
|
||||
@@ -139,7 +139,7 @@ export function* signOutStart() {
|
||||
imexshopid: state.user.bodyshop.imexshopid,
|
||||
type: "messaging"
|
||||
});
|
||||
amplitude.reset();
|
||||
//amplitude.reset();
|
||||
} catch {
|
||||
console.log("No FCM token. Skipping unsubscribe.");
|
||||
}
|
||||
@@ -365,7 +365,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
}
|
||||
|
||||
try {
|
||||
amplitude.setGroup('Shop', payload.shopname);
|
||||
//amplitude.setGroup('Shop', payload.shopname);
|
||||
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||
|
||||
@@ -281,6 +281,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
|
||||
"accumulatePayableLines": "Accumulate Payable Lines",
|
||||
"address1": "Address 1",
|
||||
"address2": "Address 2",
|
||||
"appt_alt_transport": "Appointment Alternative Transportation Options",
|
||||
@@ -321,6 +322,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 +998,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": {
|
||||
|
||||
@@ -281,6 +281,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "",
|
||||
"accumulatePayableLines": "",
|
||||
"address1": "",
|
||||
"address2": "",
|
||||
"appt_alt_transport": "",
|
||||
@@ -321,6 +322,7 @@
|
||||
"itc_local": "",
|
||||
"itc_state": "",
|
||||
"mappingname": "",
|
||||
"ro_posting": "",
|
||||
"sendmaterialscosting": "",
|
||||
"srcco": ""
|
||||
},
|
||||
@@ -996,6 +998,7 @@
|
||||
"insco": "",
|
||||
"refreshrequired": "",
|
||||
"status": "",
|
||||
"status_normal": "",
|
||||
"updatinglayout": ""
|
||||
},
|
||||
"labels": {
|
||||
|
||||
@@ -281,6 +281,7 @@
|
||||
},
|
||||
"fields": {
|
||||
"ReceivableCustomField": "",
|
||||
"accumulatePayableLines": "",
|
||||
"address1": "",
|
||||
"address2": "",
|
||||
"appt_alt_transport": "",
|
||||
@@ -321,6 +322,7 @@
|
||||
"itc_local": "",
|
||||
"itc_state": "",
|
||||
"mappingname": "",
|
||||
"ro_posting": "",
|
||||
"sendmaterialscosting": "",
|
||||
"srcco": ""
|
||||
},
|
||||
@@ -996,6 +998,7 @@
|
||||
"insco": "",
|
||||
"refreshrequired": "",
|
||||
"status": "",
|
||||
"status_normal": "",
|
||||
"updatinglayout": ""
|
||||
},
|
||||
"labels": {
|
||||
|
||||
@@ -207,6 +207,9 @@ services:
|
||||
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
|
||||
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
"
|
||||
|
||||
networks:
|
||||
|
||||
@@ -120,6 +120,8 @@ services:
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
"
|
||||
# Node App: The Main IMEX API
|
||||
node-app:
|
||||
|
||||
@@ -15,6 +15,15 @@
|
||||
- name: x-imex-auth
|
||||
value_from_env: DATAPUMP_AUTH
|
||||
comment: Project Mexico
|
||||
- name: CARFAX RPS Data Pump
|
||||
webhook: '{{HASURA_API_URL}}/data/carfaxrps'
|
||||
schedule: 15 7 * * 0
|
||||
include_in_metadata: true
|
||||
payload: {}
|
||||
headers:
|
||||
- name: x-imex-auth
|
||||
value_from_env: DATAPUMP_AUTH
|
||||
comment: Project Mexico
|
||||
- name: Chatter Data Pump
|
||||
webhook: '{{HASURA_API_URL}}/data/chatter'
|
||||
schedule: 45 5 * * *
|
||||
|
||||
@@ -177,11 +177,9 @@ exports.PbsCalculateAllocationsAp = PbsCalculateAllocationsAp;
|
||||
async function QueryBillData(socket, billids) {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
const currentToken =
|
||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${currentToken}` })
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.GET_PBS_AP_ALLOCATIONS, { billids: billids });
|
||||
|
||||
WsLogger.createLogEvent(socket, "SILLY", `Bill data query result ${JSON.stringify(result, null, 2)}`);
|
||||
@@ -201,11 +199,9 @@ function getCostAccount(billline, respcenters) {
|
||||
async function MarkApExported(socket, billids) {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
const currentToken =
|
||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${currentToken}` })
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.MARK_BILLS_EXPORTED, {
|
||||
billids,
|
||||
bill: {
|
||||
|
||||
@@ -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`,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,8 @@ const AxiosLib = require("axios").default;
|
||||
const queries = require("../../graphql-client/queries");
|
||||
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
|
||||
const WsLogger = require("../../web-sockets/createLogEvent");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
//const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
|
||||
const CalculateAllocations = require("../../cdk/cdk-calculate-allocations").default;
|
||||
@@ -22,9 +24,9 @@ axios.interceptors.request.use((x) => {
|
||||
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
|
||||
x.url
|
||||
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
|
||||
//console.log(printable);
|
||||
//logRequestToFile(printable);
|
||||
|
||||
WsLogger.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
|
||||
WsLogger.createJsonEvent(socket, "DEBUG", `Raw Request: ${printable}`, x.data);
|
||||
|
||||
return x;
|
||||
});
|
||||
@@ -32,23 +34,36 @@ 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);
|
||||
WsLogger.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
|
||||
const printable = `${new Date()} | Response: ${x.status} ${x.statusText} |${JSON.stringify(x.data)}`;
|
||||
//logRequestToFile(printable);
|
||||
WsLogger.createJsonEvent(socket, "DEBUG", `Raw Response: ${printable}`, x.data);
|
||||
|
||||
return x;
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultHandler = async (socket, { txEnvelope, jobid }) => {
|
||||
socket.logEvents = [];
|
||||
socket.recordid = jobid;
|
||||
socket.txEnvelope = txEnvelope;
|
||||
try {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Received Job export request for id ${jobid}`);
|
||||
|
||||
const JobData = await QueryJobData(socket, jobid);
|
||||
socket.JobData = JobData;
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
|
||||
WsLogger.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.
|
||||
@@ -71,42 +86,53 @@ exports.default = defaultHandler;
|
||||
|
||||
exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selectedCustomerId) {
|
||||
try {
|
||||
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
|
||||
socket.selectedCustomerId = selectedCustomerId;
|
||||
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle !== true) {
|
||||
WsLogger.createLogEvent(socket, "INFO", `User selected customer ${selectedCustomerId || "NEW"}`);
|
||||
|
||||
//Upsert the contact information as per Wafaa's Email.
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"DEBUG",
|
||||
"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);
|
||||
socket.ownerRef = ownerRef;
|
||||
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
|
||||
await UpsertVehicleData(socket, ownerRef.ReferenceId);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
|
||||
const vehicleRef = await UpsertVehicleData(socket, ownerRef.ReferenceId);
|
||||
socket.vehicleRef = vehicleRef;
|
||||
} else {
|
||||
WsLogger.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;
|
||||
}
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Inserting account data.`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Inserting account posting data...`);
|
||||
const insertResponse = await InsertAccountPostingData(socket);
|
||||
|
||||
if (insertResponse.WasSuccessful) {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
|
||||
if (socket.JobData.bodyshop.pbs_configuration.ro_posting) {
|
||||
await CreateRepairOrderInPBS(socket, socket.ownerRef, socket.vehicleRef);
|
||||
}
|
||||
WsLogger.createLogEvent(socket, "INFO", `Marking job as exported.`);
|
||||
await MarkJobExported(socket, socket.JobData.id);
|
||||
|
||||
socket.emit("export-success", socket.JobData.id);
|
||||
} else {
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||
}
|
||||
} catch (error) {
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
|
||||
await InsertFailedExportLog(socket, error);
|
||||
}
|
||||
};
|
||||
@@ -114,26 +140,24 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
|
||||
// Was Successful
|
||||
async function CheckForErrors(socket, response) {
|
||||
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Successful response from DMS. ${response.Message || ""}`);
|
||||
} else {
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
|
||||
WsLogger.createLogEvent(socket, "SILLY", `Error received from DMS: ${JSON.stringify(response)}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Error received from DMS: ${JSON.stringify(response)}`);
|
||||
}
|
||||
}
|
||||
|
||||
exports.CheckForErrors = CheckForErrors;
|
||||
|
||||
async function QueryJobData(socket, jobid) {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Querying job data for id ${jobid}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
const currentToken =
|
||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${currentToken}` })
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.QUERY_JOBS_FOR_PBS_EXPORT, { id: jobid });
|
||||
|
||||
WsLogger.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Job data query result ${JSON.stringify(result, null, 2)}`);
|
||||
return result.jobs_by_pk;
|
||||
}
|
||||
|
||||
@@ -338,7 +362,7 @@ async function UpsertVehicleData(socket, ownerRef) {
|
||||
//FleetNumber: "String",
|
||||
//Status: "String",
|
||||
OwnerRef: ownerRef, // "00000000000000000000000000000000",
|
||||
ModelNumber: socket.JobData.vehicle?.v_makecode,
|
||||
// ModelNumber: socket.JobData.vehicle?.v_makecode,
|
||||
Make: socket.JobData.v_make_desc,
|
||||
Model: socket.JobData.v_model_desc,
|
||||
Trim: socket.JobData.vehicle?.v_trimcode,
|
||||
@@ -346,7 +370,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" },
|
||||
@@ -476,6 +500,56 @@ 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 {
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in Getting Vehicle Data - ${Vehicles.length} vehicle(s) found`);
|
||||
}
|
||||
} catch (error) {
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function InsertAccountPostingData(socket) {
|
||||
try {
|
||||
const allocations = await CalculateAllocations(socket, socket.JobData.id);
|
||||
@@ -579,13 +653,11 @@ async function InsertAccountPostingData(socket) {
|
||||
}
|
||||
|
||||
async function MarkJobExported(socket, jobid) {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Marking job as exported for id ${jobid}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
const currentToken =
|
||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${currentToken}` })
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.MARK_JOB_EXPORTED, {
|
||||
jobId: jobid,
|
||||
job: {
|
||||
@@ -611,11 +683,9 @@ async function MarkJobExported(socket, jobid) {
|
||||
async function InsertFailedExportLog(socket, error) {
|
||||
try {
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
const currentToken =
|
||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${currentToken}` })
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.INSERT_EXPORT_LOG, {
|
||||
log: {
|
||||
bodyshopid: socket.JobData.bodyshop.id,
|
||||
@@ -631,3 +701,161 @@ async function InsertFailedExportLog(socket, error) {
|
||||
WsLogger.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;
|
||||
WsLogger.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) {
|
||||
WsLogger.createLogEvent(socket, "INFO", `ROs found for vehicle, but none match. Inserting record.`);
|
||||
const InsertedRepairOrder = await RepairOrderChange(socket);
|
||||
socket.InsertedRepairOrder = InsertedRepairOrder;
|
||||
} else {
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"WARN",
|
||||
`Repair order appears to already exist in PBS. ${matchingRo.RepairOrderNumber}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
WsLogger.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) {
|
||||
WsLogger.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) {
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,21 +205,49 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
||||
|
||||
async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) {
|
||||
const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, bill.job.shopid);
|
||||
let lines;
|
||||
if (bodyshop.accountingconfig.accumulatePayableLines === true) {
|
||||
lines = Object.values(
|
||||
bill.billlines.reduce((acc, il) => {
|
||||
const { cost_center, actual_cost, quantity = 1 } = il;
|
||||
|
||||
const lines = bill.billlines.map((il) =>
|
||||
generateBillLine(
|
||||
il,
|
||||
accounts,
|
||||
bill.job.class,
|
||||
bodyshop.md_responsibility_centers.sales_tax_codes,
|
||||
classes,
|
||||
taxCodes,
|
||||
bodyshop.md_responsibility_centers.costs,
|
||||
bodyshop.accountingconfig,
|
||||
bodyshop.region_config
|
||||
)
|
||||
);
|
||||
if (!acc[cost_center]) {
|
||||
acc[cost_center] = { ...il, actual_cost: 0, quantity: 1 };
|
||||
}
|
||||
|
||||
acc[cost_center].actual_cost += Math.round(actual_cost * quantity * 100);
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
).map((il) => {
|
||||
il.actual_cost /= 100;
|
||||
return generateBillLine(
|
||||
il,
|
||||
accounts,
|
||||
bill.job.class,
|
||||
bodyshop.md_responsibility_centers.sales_tax_codes,
|
||||
classes,
|
||||
taxCodes,
|
||||
bodyshop.md_responsibility_centers.costs,
|
||||
bodyshop.accountingconfig,
|
||||
bodyshop.region_config
|
||||
);
|
||||
});
|
||||
} else {
|
||||
lines = bill.billlines.map((il) =>
|
||||
generateBillLine(
|
||||
il,
|
||||
accounts,
|
||||
bill.job.class,
|
||||
bodyshop.md_responsibility_centers.sales_tax_codes,
|
||||
classes,
|
||||
taxCodes,
|
||||
bodyshop.md_responsibility_centers.costs,
|
||||
bodyshop.accountingconfig,
|
||||
bodyshop.region_config
|
||||
)
|
||||
);
|
||||
}
|
||||
//QB USA with GST
|
||||
//This was required for the No. 1 Collision Group.
|
||||
if (
|
||||
@@ -241,7 +269,7 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
||||
Amount: Dinero({
|
||||
amount: Math.round(
|
||||
bill.billlines.reduce((acc, val) => {
|
||||
return acc + (val.applicable_taxes?.federal ? (val.actual_cost * val.quantity ?? 0) : 0);
|
||||
return acc + (val.applicable_taxes?.federal ? val.actual_cost * val.quantity || 0 : 0);
|
||||
}, 0) * 100
|
||||
)
|
||||
})
|
||||
|
||||
@@ -46,6 +46,28 @@ exports.default = async (req, res) => {
|
||||
};
|
||||
|
||||
const generateBill = (bill, bodyshop) => {
|
||||
let lines;
|
||||
if (bodyshop.accountingconfig.accumulatePayableLines === true) {
|
||||
lines = Object.values(
|
||||
bill.billlines.reduce((acc, il) => {
|
||||
const { cost_center, actual_cost, quantity = 1 } = il;
|
||||
|
||||
if (!acc[cost_center]) {
|
||||
acc[cost_center] = { ...il, actual_cost: 0, quantity: 1 };
|
||||
}
|
||||
|
||||
acc[cost_center].actual_cost += Math.round(actual_cost * quantity * 100);
|
||||
|
||||
return acc;
|
||||
}, {})
|
||||
).map((il) => {
|
||||
il.actual_cost /= 100;
|
||||
return generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class);
|
||||
});
|
||||
} else {
|
||||
lines = bill.billlines.map((il) => generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class));
|
||||
}
|
||||
|
||||
const billQbxmlObj = {
|
||||
QBXML: {
|
||||
QBXMLMsgsRq: {
|
||||
@@ -67,9 +89,7 @@ const generateBill = (bill, bodyshop) => {
|
||||
}),
|
||||
RefNumber: bill.invoice_number,
|
||||
Memo: `RO ${bill.job.ro_number || ""}`,
|
||||
ExpenseLineAdd: bill.billlines.map((il) =>
|
||||
generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class)
|
||||
)
|
||||
ExpenseLineAdd: lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
441
server/data/carfax-rps.js
Normal file
441
server/data/carfax-rps.js
Normal file
@@ -0,0 +1,441 @@
|
||||
const queries = require("../graphql-client/queries");
|
||||
const moment = require("moment-timezone");
|
||||
const logger = require("../utils/logger");
|
||||
const fs = require("fs");
|
||||
const client = require("../graphql-client/graphql-client").rpsClient;
|
||||
const { sendServerEmail, sendMexicoBillingEmail } = require("../email/sendemail");
|
||||
const crypto = require("crypto");
|
||||
const { ftpSetup, uploadToS3 } = require("./carfax");
|
||||
let Client = require("ssh2-sftp-client");
|
||||
|
||||
const AHDateFormat = "YYYY-MM-DD";
|
||||
|
||||
const NON_ASCII_REGEX = /[^\x20-\x7E]/g;
|
||||
|
||||
const S3_BUCKET_NAME = "rps-carfax-uploads";
|
||||
|
||||
const carfaxExportRps = async (req, res) => {
|
||||
// Only process if in production environment.
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return res.sendStatus(403);
|
||||
}
|
||||
// Only process if the appropriate token is provided.
|
||||
if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
|
||||
return res.sendStatus(401);
|
||||
}
|
||||
|
||||
// Send immediate response and continue processing.
|
||||
res.status(202).json({
|
||||
success: true,
|
||||
message: "Processing request ...",
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
logger.log("CARFAX-RPS-start", "DEBUG", "api", null, null);
|
||||
const allJSONResults = [];
|
||||
const allErrors = [];
|
||||
|
||||
const { bodyshops } = await client.request(queries.GET_CARFAX_RPS_SHOPS); //Query for the List of Bodyshop Clients.
|
||||
const specificShopIds = req.body.bodyshopIds; // ['uuid];
|
||||
const { start, end, skipUpload, ignoreDateFilter } = req.body; //YYYY-MM-DD
|
||||
|
||||
const shopsToProcess =
|
||||
specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
|
||||
logger.log("CARFAX-RPS-shopsToProcess-generated", "DEBUG", "api", null, null);
|
||||
|
||||
if (shopsToProcess.length === 0) {
|
||||
logger.log("CARFAX-RPS-shopsToProcess-empty", "DEBUG", "api", null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors);
|
||||
|
||||
await sendServerEmail({
|
||||
subject: `Project Mexico RPS Report ${moment().format("MM-DD-YY")}`,
|
||||
text: `Total Count: ${allJSONResults.reduce((a, v) => a + v.count, 0)}\nErrors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||
allJSONResults.map((x) => ({
|
||||
imexshopid: x.imexshopid,
|
||||
filename: x.filename,
|
||||
count: x.count,
|
||||
result: x.result
|
||||
})),
|
||||
null,
|
||||
2
|
||||
)}`,
|
||||
to: ["bradley.rhoades@convenient-brands.com"]
|
||||
});
|
||||
|
||||
logger.log("CARFAX-RPS-end", "DEBUG", "api", null, null);
|
||||
} catch (error) {
|
||||
logger.log("CARFAX-RPS-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
||||
}
|
||||
};
|
||||
|
||||
async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors) {
|
||||
for (const bodyshop of shopsToProcess) {
|
||||
const shopid = bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
||||
const erroredJobs = [];
|
||||
try {
|
||||
logger.log("CARFAX-RPS-start-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||
shopname: bodyshop.shopname
|
||||
});
|
||||
|
||||
const { jobs, bodyshops_by_pk } = await client.request(queries.CARFAX_RPS_QUERY, {
|
||||
bodyshopid: bodyshop.id,
|
||||
...(ignoreDateFilter
|
||||
? {}
|
||||
: {
|
||||
starttz: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"),
|
||||
...(end && { endtz: moment(end).endOf("day") }),
|
||||
start: start
|
||||
? moment(start).startOf("day").format(AHDateFormat)
|
||||
: moment().subtract(7, "days").startOf("day").format(AHDateFormat),
|
||||
...(end && { endtz: moment(end).endOf("day").format(AHDateFormat) })
|
||||
})
|
||||
});
|
||||
|
||||
const carfaxObject = {
|
||||
shopid: shopid,
|
||||
shop_name: bodyshop.shopname,
|
||||
job: jobs.map((j) =>
|
||||
CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
|
||||
erroredJobs.push({ job: job, error: error.toString() });
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
if (erroredJobs.length > 0) {
|
||||
logger.log("CARFAX-RPS-failed-jobs", "ERROR", "api", bodyshop.id, {
|
||||
count: erroredJobs.length,
|
||||
jobs: JSON.stringify(erroredJobs.map((j) => j.job.id))
|
||||
});
|
||||
}
|
||||
|
||||
const jsonObj = {
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
json: JSON.stringify(carfaxObject, null, 2),
|
||||
filename: `${shopid}_${moment().format("DDMMYYYY_HHMMss")}.json`,
|
||||
count: carfaxObject.job.length
|
||||
};
|
||||
|
||||
if (skipUpload) {
|
||||
fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json);
|
||||
uploadToS3(jsonObj, S3_BUCKET_NAME);
|
||||
} else {
|
||||
await uploadViaSFTP(jsonObj);
|
||||
|
||||
await sendMexicoBillingEmail({
|
||||
subject: `${shopid.replace(/_/g, "").toUpperCase()}_MexicoRPS_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
|
||||
text: `Errors:\n${JSON.stringify(
|
||||
erroredJobs.map((ej) => ({
|
||||
jobid: ej.job?.id,
|
||||
error: ej.error
|
||||
})),
|
||||
null,
|
||||
2
|
||||
)}\n\nUploaded:\n${JSON.stringify(
|
||||
{
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
count: jsonObj.count,
|
||||
filename: jsonObj.filename,
|
||||
result: jsonObj.result
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
});
|
||||
}
|
||||
|
||||
allJSONResults.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
count: jsonObj.count,
|
||||
filename: jsonObj.filename,
|
||||
result: jsonObj.result
|
||||
});
|
||||
|
||||
logger.log("CARFAX-RPS-end-shop-extract", "DEBUG", "api", bodyshop.id, {
|
||||
shopname: bodyshop.shopname
|
||||
});
|
||||
} catch (error) {
|
||||
//Error at the shop level.
|
||||
logger.log("CARFAX-RPS-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
|
||||
|
||||
allErrors.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
CARFAXid: bodyshop.CARFAXid,
|
||||
fatal: true,
|
||||
errors: [error.toString()]
|
||||
});
|
||||
} finally {
|
||||
allErrors.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
CARFAXid: bodyshop.CARFAXid,
|
||||
errors: erroredJobs.map((ej) => ({
|
||||
jobid: ej.job?.id,
|
||||
error: ej.error
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadViaSFTP(jsonObj) {
|
||||
const sftp = new Client();
|
||||
sftp.on("error", (errors) =>
|
||||
logger.log("CARFAX-RPS-sftp-connection-error", "ERROR", "api", jsonObj.bodyshopid, {
|
||||
error: errors.message,
|
||||
stack: errors.stack
|
||||
})
|
||||
);
|
||||
try {
|
||||
// Upload to S3 first.
|
||||
uploadToS3(jsonObj, S3_BUCKET_NAME);
|
||||
|
||||
//Connect to the FTP and upload all.
|
||||
await sftp.connect(ftpSetup);
|
||||
|
||||
try {
|
||||
jsonObj.result = await sftp.put(Buffer.from(jsonObj.json), `${jsonObj.filename}`);
|
||||
logger.log("CARFAX-RPS-sftp-upload", "DEBUG", "api", jsonObj.bodyshopid, {
|
||||
imexshopid: jsonObj.imexshopid,
|
||||
filename: jsonObj.filename,
|
||||
result: jsonObj.result
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log("CARFAX-RPS-sftp-upload-error", "ERROR", "api", jsonObj.bodyshopid, {
|
||||
filename: jsonObj.filename,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("CARFAX-RPS-sftp-error", "ERROR", "api", jsonObj.bodyshopid, {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
sftp.end();
|
||||
}
|
||||
}
|
||||
|
||||
const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
try {
|
||||
const subtotalEntry = job.totals.find((total) => total.TTL_TYPECD === "");
|
||||
const subtotal = subtotalEntry ? subtotalEntry.T_AMT : 0;
|
||||
|
||||
const ret = {
|
||||
ro_number: crypto.createHash("md5").update(job.id, "utf8").digest("hex"),
|
||||
v_vin: job.v_vin || "",
|
||||
v_year: job.v_model_yr
|
||||
? parseInt(job.v_model_yr.match(/\d/g))
|
||||
? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
|
||||
: ""
|
||||
: "",
|
||||
v_make: job.v_makedesc || "",
|
||||
v_model: job.v_model || "",
|
||||
|
||||
date_estimated: moment(job.created_at).tz("America/Winnipeg").format(AHDateFormat) || "",
|
||||
data_opened: moment(job.created_at).tz("America/Winnipeg").format(AHDateFormat) || "",
|
||||
date_invoiced: [job.close_date, job.created_at].find((date) => date)
|
||||
? moment([job.close_date, job.created_at].find((date) => date))
|
||||
.tz("America/Winnipeg")
|
||||
.format(AHDateFormat)
|
||||
: "",
|
||||
loss_date: job.loss_date ? moment(job.loss_date).format(AHDateFormat) : "",
|
||||
|
||||
ins_co_nm: job.ins_co_nm || "",
|
||||
loss_desc: job.loss_desc || "",
|
||||
theft_ind: job.theft_ind,
|
||||
tloss_ind: job.tlos_ind,
|
||||
|
||||
subtotal: subtotal,
|
||||
|
||||
areaofdamage: {
|
||||
impact1: generateAreaOfDamage(job.impact_1 || ""),
|
||||
impact2: generateAreaOfDamage(job.impact_2 || "")
|
||||
},
|
||||
|
||||
jobLines: job.joblines.length > 0 ? job.joblines.map((jl) => GenerateDetailLines(jl)) : [generateNullDetailLine()]
|
||||
};
|
||||
return ret;
|
||||
} catch (error) {
|
||||
logger.log("CARFAX-RPS-job-data-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
|
||||
errorCallback({ jobid: job.id, error });
|
||||
}
|
||||
};
|
||||
|
||||
const GenerateDetailLines = (line) => {
|
||||
const ret = {
|
||||
line_desc: line.line_desc ? line.line_desc.replace(NON_ASCII_REGEX, "") : null,
|
||||
oem_partno: line.oem_partno ? line.oem_partno.replace(NON_ASCII_REGEX, "") : null,
|
||||
alt_partno: line.alt_partno ? line.alt_partno.replace(NON_ASCII_REGEX, "") : null,
|
||||
op_code_desc: generateOpCodeDescription(line.lbr_op),
|
||||
lbr_ty: generateLaborType(line.mod_lbr_ty),
|
||||
lbr_hrs: line.mod_lb_hrs || 0,
|
||||
part_qty: line.part_qty || 0,
|
||||
part_type: generatePartType(line.part_type),
|
||||
act_price: line.act_price || 0
|
||||
};
|
||||
return ret;
|
||||
};
|
||||
|
||||
const generateNullDetailLine = () => {
|
||||
return {
|
||||
line_desc: null,
|
||||
oem_partno: null,
|
||||
alt_partno: null,
|
||||
lbr_ty: null,
|
||||
part_qty: 0,
|
||||
part_type: null,
|
||||
act_price: 0
|
||||
};
|
||||
};
|
||||
|
||||
const generateAreaOfDamage = (loc) => {
|
||||
const areaMap = {
|
||||
"01": "Right Front Corner",
|
||||
"02": "Right Front Side",
|
||||
"03": "Right Side",
|
||||
"04": "Right Rear Side",
|
||||
"05": "Right Rear Corner",
|
||||
"06": "Rear",
|
||||
"07": "Left Rear Corner",
|
||||
"08": "Left Rear Side",
|
||||
"09": "Left Side",
|
||||
10: "Left Front Side",
|
||||
11: "Left Front Corner",
|
||||
12: "Front",
|
||||
13: "Rollover",
|
||||
14: "Uknown",
|
||||
15: "Total Loss",
|
||||
16: "Non-Collision",
|
||||
19: "All Over",
|
||||
25: "Hood",
|
||||
26: "Deck Lid",
|
||||
27: "Roof",
|
||||
28: "Undercarriage",
|
||||
34: "All Over"
|
||||
};
|
||||
return areaMap[loc] || null;
|
||||
};
|
||||
|
||||
const generateLaborType = (type) => {
|
||||
const laborTypeMap = {
|
||||
laa: "Aluminum",
|
||||
lab: "Body",
|
||||
lad: "Diagnostic",
|
||||
lae: "Electrical",
|
||||
laf: "Frame",
|
||||
lag: "Glass",
|
||||
lam: "Mechanical",
|
||||
lar: "Refinish",
|
||||
las: "Structural",
|
||||
lau: "Other - LAU",
|
||||
la1: "Other - LA1",
|
||||
la2: "Other - LA2",
|
||||
la3: "Other - LA3",
|
||||
la4: "Other - LA4",
|
||||
null: "Other",
|
||||
mapa: "Paint Materials",
|
||||
mash: "Shop Materials",
|
||||
rates_subtotal: "Labor Total",
|
||||
"timetickets.labels.shift": "Shift",
|
||||
"timetickets.labels.amshift": "Morning Shift",
|
||||
"timetickets.labels.ambreak": "Morning Break",
|
||||
"timetickets.labels.pmshift": "Afternoon Shift",
|
||||
"timetickets.labels.pmbreak": "Afternoon Break",
|
||||
"timetickets.labels.lunch": "Lunch"
|
||||
};
|
||||
|
||||
return laborTypeMap[type?.toLowerCase()] || null;
|
||||
};
|
||||
|
||||
const generatePartType = (type) => {
|
||||
const partTypeMap = {
|
||||
paa: "Aftermarket",
|
||||
pae: "Existing",
|
||||
pag: "Glass",
|
||||
pal: "LKQ",
|
||||
pan: "OEM",
|
||||
pao: "Other",
|
||||
pas: "Sublet",
|
||||
pasl: "Sublet",
|
||||
ccc: "CC Cleaning",
|
||||
ccd: "CC Damage Waiver",
|
||||
ccdr: "CC Daily Rate",
|
||||
ccf: "CC Refuel",
|
||||
ccm: "CC Mileage",
|
||||
prt_dsmk_total: "Line Item Adjustment"
|
||||
};
|
||||
|
||||
return partTypeMap[type?.toLowerCase()] || null;
|
||||
};
|
||||
|
||||
const generateOpCodeDescription = (type) => {
|
||||
const opCodeMap = {
|
||||
OP0: "REMOVE / REPLACE PARTIAL",
|
||||
OP1: "REFINISH / REPAIR",
|
||||
OP10: "REPAIR , PARTIAL",
|
||||
OP100: "REPLACE PRE-PRICED",
|
||||
OP101: "REMOVE/REPLACE RECYCLED PART",
|
||||
OP103: "REMOVE / REPLACE PARTIAL",
|
||||
OP104: "REMOVE / REPLACE PARTIAL LABOUR",
|
||||
OP105: "!!ADJUST MANUALLY!!",
|
||||
OP106: "REPAIR , PARTIAL",
|
||||
OP107: "CHIPGUARD",
|
||||
OP108: "MULTI TONE",
|
||||
OP109: "REPLACE PRE-PRICED",
|
||||
OP11: "REMOVE / REPLACE",
|
||||
OP110: "REFINISH / REPAIR",
|
||||
OP111: "REMOVE / REPLACE",
|
||||
OP112: "REMOVE / REPLACE",
|
||||
OP113: "REPLACE PRE-PRICED",
|
||||
OP114: "REPLACE PRE-PRICED",
|
||||
OP12: "REMOVE / REPLACE PARTIAL",
|
||||
OP120: "REPAIR , PARTIAL",
|
||||
OP13: "ADDITIONAL COSTS",
|
||||
OP14: "ADDITIONAL OPERATIONS",
|
||||
OP15: "BLEND",
|
||||
OP16: "SUBLET",
|
||||
OP17: "POLICY LIMIT ADJUSTMENT",
|
||||
OP18: "APPEAR ALLOWANCE",
|
||||
OP2: "REMOVE / INSTALL",
|
||||
OP24: "CHIPGUARD",
|
||||
OP25: "TWO TONE",
|
||||
OP26: "PAINTLESS DENT REPAIR",
|
||||
OP260: "SUBLET",
|
||||
OP3: "ADDITIONAL LABOR",
|
||||
OP4: "ALIGNMENT",
|
||||
OP5: "OVERHAUL",
|
||||
OP6: "REFINISH",
|
||||
OP7: "INSPECT",
|
||||
OP8: "CHECK / ADJUST",
|
||||
OP9: "REPAIR"
|
||||
};
|
||||
|
||||
return opCodeMap[type?.toUpperCase()] || null;
|
||||
};
|
||||
|
||||
const errorCode = ({ count, filename, results }) => {
|
||||
if (count === 0) return 1;
|
||||
if (!filename) return 3;
|
||||
const sftpErrorCode = results?.sftpError?.code;
|
||||
if (sftpErrorCode && ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "ECONNRESET"].includes(sftpErrorCode)) {
|
||||
return 4;
|
||||
}
|
||||
if (sftpErrorCode) return 7;
|
||||
return 0;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
default: carfaxExportRps,
|
||||
ftpSetup
|
||||
};
|
||||
@@ -37,12 +37,12 @@ const S3_BUCKET_NAME = InstanceManager({
|
||||
const region = InstanceManager.InstanceRegion;
|
||||
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
|
||||
|
||||
const uploadToS3 = (jsonObj) => {
|
||||
const uploadToS3 = (jsonObj, bucketName = S3_BUCKET_NAME) => {
|
||||
const webPath = isLocal
|
||||
? `https://${S3_BUCKET_NAME}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}`
|
||||
: `https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/${jsonObj.filename}`;
|
||||
? `https://${bucketName}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}`
|
||||
: `https://${bucketName}.s3.${region}.amazonaws.com/${jsonObj.filename}`;
|
||||
|
||||
uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: jsonObj.filename, content: jsonObj.json })
|
||||
uploadFileToS3({ bucketName: bucketName, key: jsonObj.filename, content: jsonObj.json })
|
||||
.then(() => {
|
||||
logger.log("CARFAX-s3-upload", "DEBUG", "api", jsonObj.bodyshopid, {
|
||||
imexshopid: jsonObj.imexshopid,
|
||||
@@ -61,7 +61,7 @@ const uploadToS3 = (jsonObj) => {
|
||||
});
|
||||
};
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
const carfaxExport = async (req, res) => {
|
||||
// Only process if in production environment.
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return res.sendStatus(403);
|
||||
@@ -80,7 +80,7 @@ exports.default = async (req, res) => {
|
||||
|
||||
try {
|
||||
logger.log("CARFAX-start", "DEBUG", "api", null, null);
|
||||
const allXMLResults = [];
|
||||
const allJSONResults = [];
|
||||
const allErrors = [];
|
||||
|
||||
const { bodyshops } = await client.request(queries.GET_CARFAX_SHOPS); //Query for the List of Bodyshop Clients.
|
||||
@@ -96,12 +96,12 @@ exports.default = async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors);
|
||||
await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors);
|
||||
|
||||
await sendServerEmail({
|
||||
subject: `Project Mexico Report ${moment().format("MM-DD-YY")}`,
|
||||
text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||
allXMLResults.map((x) => ({
|
||||
text: `Total Count: ${allJSONResults.reduce((a, v) => a + v.count, 0)}\nErrors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
|
||||
allJSONResults.map((x) => ({
|
||||
imexshopid: x.imexshopid,
|
||||
filename: x.filename,
|
||||
count: x.count,
|
||||
@@ -109,7 +109,8 @@ exports.default = async (req, res) => {
|
||||
})),
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
)}`,
|
||||
to: ["bradley.rhoades@convenient-brands.com"]
|
||||
});
|
||||
|
||||
logger.log("CARFAX-end", "DEBUG", "api", null, null);
|
||||
@@ -118,7 +119,7 @@ exports.default = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors) {
|
||||
async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors) {
|
||||
for (const bodyshop of shopsToProcess) {
|
||||
const shopid = bodyshop.imexshopid?.toLowerCase() || bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
||||
const erroredJobs = [];
|
||||
@@ -195,7 +196,7 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat
|
||||
});
|
||||
}
|
||||
|
||||
allXMLResults.push({
|
||||
allJSONResults.push({
|
||||
bodyshopid: bodyshop.id,
|
||||
imexshopid: shopid,
|
||||
count: jsonObj.count,
|
||||
@@ -447,3 +448,9 @@ const errorCode = ({ count, filename, results }) => {
|
||||
if (sftpErrorCode) return 7;
|
||||
return 0;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
default: carfaxExport,
|
||||
ftpSetup,
|
||||
uploadToS3
|
||||
};
|
||||
|
||||
@@ -7,4 +7,5 @@ exports.usageReport = require("./usageReport").default;
|
||||
exports.podium = require("./podium").default;
|
||||
exports.emsUpload = require("./emsUpload").default;
|
||||
exports.carfax = require("./carfax").default;
|
||||
exports.carfaxRps = require("./carfax-rps").default;
|
||||
exports.vehicletype = require("./vehicletype/vehicletype").default;
|
||||
@@ -44,8 +44,14 @@ const logEmail = async (req, email) => {
|
||||
}
|
||||
};
|
||||
|
||||
const sendServerEmail = async ({ subject, text }) => {
|
||||
const sendServerEmail = async ({ subject, text, to = [] }) => {
|
||||
if (process.env.NODE_ENV === undefined) return;
|
||||
|
||||
let sentTo = ["support@imexsystems.ca"];
|
||||
if (to?.length) {
|
||||
sentTo = [...sentTo, ...to];
|
||||
}
|
||||
|
||||
try {
|
||||
mailer.sendMail(
|
||||
{
|
||||
@@ -53,7 +59,7 @@ const sendServerEmail = async ({ subject, text }) => {
|
||||
imex: `ImEX Online API - ${process.env.NODE_ENV} <noreply@imex.online>`,
|
||||
rome: `Rome Online API - ${process.env.NODE_ENV} <noreply@romeonline.io>`
|
||||
}),
|
||||
to: ["support@thinkimex.com"],
|
||||
to: sentTo,
|
||||
subject: subject,
|
||||
text: text,
|
||||
ses: {
|
||||
@@ -68,7 +74,7 @@ const sendServerEmail = async ({ subject, text }) => {
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(err, info) => {
|
||||
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
|
||||
logger.log("server-email-send", err ? "error" : "debug", null, null, {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
});
|
||||
@@ -103,7 +109,7 @@ const sendMexicoBillingEmail = async ({ subject, text }) => {
|
||||
},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(err, info) => {
|
||||
logger.log("server-email-failure", err ? "error" : "debug", null, null, {
|
||||
logger.log("server-email-send", err ? "error" : "debug", null, null, {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
});
|
||||
@@ -258,7 +264,10 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
(err, info) => {
|
||||
// (message, type, user, record, meta
|
||||
logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack });
|
||||
logger.log("server-email-send", err ? "error" : "debug", null, null, {
|
||||
message: err?.message,
|
||||
stack: err?.stack
|
||||
});
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||
|
||||
//New bug introduced with Graphql Request.
|
||||
@@ -11,9 +13,24 @@ const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
|
||||
}
|
||||
});
|
||||
|
||||
const rpsClient =
|
||||
process.env.RPS_GRAPHQL_ENDPOINT && process.env.RPS_HASURA_ADMIN_SECRET ?
|
||||
new GraphQLClient(process.env.RPS_GRAPHQL_ENDPOINT, {
|
||||
headers: {
|
||||
"x-hasura-admin-secret": process.env.RPS_HASURA_ADMIN_SECRET
|
||||
}
|
||||
}) : null;
|
||||
|
||||
if (!rpsClient) {
|
||||
//System log to disable RPS functions
|
||||
logger.log(`RPS secrets are not set. Client is not configured.`, "WARN", "redis", "api", {
|
||||
});
|
||||
}
|
||||
|
||||
const unauthorizedClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT);
|
||||
|
||||
module.exports = {
|
||||
client,
|
||||
rpsClient,
|
||||
unauthorizedClient
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -920,6 +922,41 @@ exports.CARFAX_QUERY = `query CARFAX_EXPORT($start: timestamptz, $bodyshopid: uu
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.CARFAX_RPS_QUERY = `query CARFAX_RPS_EXPORT($starttz: timestamptz, $endtz: timestamptz,$start: date, $end: date, $bodyshopid: uuid!) {
|
||||
bodyshops_by_pk(id: $bodyshopid) {
|
||||
id
|
||||
shopname
|
||||
}
|
||||
jobs(where: {_and: [{_or: [{close_date: {_gt: $start, _lte: $end}}, {created_at: {_gt: $starttz, _lte: $endtz}, close_date: {_is_null: true}}]}, {_not: {_and: [{close_date: {_is_null: true}}, {created_at: {_is_null: true}}]}}, {bodyshopid: {_eq: $bodyshopid}}, {v_vin: {_is_null: false}}]}) {
|
||||
close_date
|
||||
created_at
|
||||
id
|
||||
ins_co_nm
|
||||
impact_1
|
||||
impact_2
|
||||
joblines {
|
||||
act_price
|
||||
alt_partno
|
||||
line_desc
|
||||
mod_lb_hrs
|
||||
mod_lbr_ty
|
||||
oem_partno
|
||||
lbr_op
|
||||
part_type
|
||||
part_qty
|
||||
}
|
||||
loss_date
|
||||
loss_desc
|
||||
theft_ind
|
||||
tlos_ind
|
||||
totals
|
||||
v_makedesc
|
||||
v_model
|
||||
v_model_yr
|
||||
v_vin
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
|
||||
bodyshops_by_pk(id: $bodyshopid){
|
||||
id
|
||||
@@ -1870,6 +1907,13 @@ exports.GET_CARFAX_SHOPS = `query GET_CARFAX_SHOPS {
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.GET_CARFAX_RPS_SHOPS = `query GET_CARFAX_RPS_SHOPS {
|
||||
bodyshops(where: {carfax_exclude: {_neq: "true"}}){
|
||||
id
|
||||
shopname
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS {
|
||||
bodyshops(where: {claimscorpid: {_is_null: false}, _or: {claimscorpid: {_neq: ""}}}){
|
||||
id
|
||||
@@ -2164,18 +2208,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
|
||||
}`
|
||||
: ""
|
||||
: ""
|
||||
}
|
||||
}`;
|
||||
|
||||
|
||||
@@ -82,22 +82,35 @@ exports.partsScan = async function (req, res) {
|
||||
criticalIds.add(jobline.id);
|
||||
}
|
||||
if (update_field && update_value) {
|
||||
const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.UPDATE_JOB_LINE, {
|
||||
lineId: jobline.id,
|
||||
line: { [update_field]: update_value, manual_line: true }
|
||||
});
|
||||
|
||||
const auditResult = await client
|
||||
.setHeaders({ Authorization: BearerToken })
|
||||
.request(queries.INSERT_AUDIT_TRAIL, {
|
||||
auditObj: {
|
||||
bodyshopid: data.jobs_by_pk.bodyshop.id,
|
||||
jobid,
|
||||
operation: `Jobline (#${jobline.line_no} ${jobline.line_desc}/${jobline.id}) ${update_field} updated from ${jobline[update_field]} to ${update_value}. Lined marked as manual line.`,
|
||||
type: "partscanupdate",
|
||||
useremail: req.user.email
|
||||
let actualUpdateField = update_field;
|
||||
if (update_field === "part_number") {
|
||||
// Determine which part number field to update based on the match
|
||||
if (!jobline.oem_partno) {
|
||||
actualUpdateField = "oem_partno";
|
||||
} else {
|
||||
if (regex) {
|
||||
actualUpdateField = regex.test(jobline.oem_partno || "") ? "oem_partno" : "alt_partno";
|
||||
} else {
|
||||
actualUpdateField = jobline.oem_partno === value ? "oem_partno" : "alt_partno";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (actualUpdateField) {
|
||||
await client.setHeaders({ Authorization: BearerToken }).request(queries.UPDATE_JOB_LINE, {
|
||||
lineId: jobline.id,
|
||||
line: { [actualUpdateField]: update_value, manual_line: true }
|
||||
});
|
||||
}
|
||||
|
||||
await client.setHeaders({ Authorization: BearerToken }).request(queries.INSERT_AUDIT_TRAIL, {
|
||||
auditObj: {
|
||||
bodyshopid: data.jobs_by_pk.bodyshop.id,
|
||||
jobid,
|
||||
operation: `Jobline (#${jobline.line_no} ${jobline.line_desc}/${jobline.id}) ${update_field} updated from ${jobline[update_field]} to ${update_value}. Lined marked as manual line.`,
|
||||
type: "partscanupdate",
|
||||
useremail: req.user.email
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//break; // No need to evaluate further rules for this jobline
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const { autohouse, claimscorp, chatter, kaizen, usageReport, podium, carfax } = require("../data/data");
|
||||
const { autohouse, claimscorp, chatter, kaizen, usageReport, podium, carfax, carfaxRps } = require("../data/data");
|
||||
|
||||
router.post("/ah", autohouse);
|
||||
router.post("/cc", claimscorp);
|
||||
@@ -9,5 +9,6 @@ router.post("/kaizen", kaizen);
|
||||
router.post("/usagereport", usageReport);
|
||||
router.post("/podium", podium);
|
||||
router.post("/carfax", carfax);
|
||||
router.post("/carfaxrps", carfaxRps);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user