@@ -67,11 +67,14 @@ export const uploadToS3 = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Key should be same as we provided to maintain backwards compatibility.
|
//Key should be same as we provided to maintain backwards compatibility.
|
||||||
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key } = signedURLResponse.data.signedUrls[0];
|
const { presignedUrl: preSignedUploadUrlToS3, key: s3Key, contentType } = signedURLResponse.data.signedUrls[0];
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
onUploadProgress: (e) => {
|
onUploadProgress: (e) => {
|
||||||
if (onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
if (onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
...contentType ? { "Content-Type": fileType } : {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,13 +12,15 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|||||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
technician: selectTechnician
|
technician: selectTechnician,
|
||||||
|
isPartsEntry: selectIsPartsEntry
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = () => ({});
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
|
export function JobLinesPartPriceChange({ job, line, refetch, technician, isPartsEntry }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
|
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
@@ -64,6 +66,7 @@ export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
|
|||||||
|
|
||||||
const popcontent =
|
const popcontent =
|
||||||
!technician &&
|
!technician &&
|
||||||
|
!isPartsEntry &&
|
||||||
InstanceRenderManager({
|
InstanceRenderManager({
|
||||||
imex: null,
|
imex: null,
|
||||||
rome: (
|
rome: (
|
||||||
|
|||||||
@@ -481,48 +481,50 @@ export function JobLinesComponent({
|
|||||||
{Enhanced_Payroll.treatment === "on" && (
|
{Enhanced_Payroll.treatment === "on" && (
|
||||||
<JobLineBulkAssignComponent selectedLines={selectedLines} setSelectedLines={setSelectedLines} job={job} />
|
<JobLineBulkAssignComponent selectedLines={selectedLines} setSelectedLines={setSelectedLines} job={job} />
|
||||||
)}
|
)}
|
||||||
<Button
|
{!isPartsEntry && (
|
||||||
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
<Button
|
||||||
onClick={() => {
|
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
||||||
setBillEnterContext({
|
onClick={() => {
|
||||||
actions: { refetch: refetch },
|
setBillEnterContext({
|
||||||
context: {
|
actions: { refetch: refetch },
|
||||||
disableInvNumber: true,
|
context: {
|
||||||
job: { id: job.id },
|
disableInvNumber: true,
|
||||||
bill: {
|
job: { id: job.id },
|
||||||
vendorid: bodyshop.inhousevendorid,
|
bill: {
|
||||||
invoice_number: "ih",
|
vendorid: bodyshop.inhousevendorid,
|
||||||
isinhouse: true,
|
invoice_number: "ih",
|
||||||
date: dayjs(),
|
isinhouse: true,
|
||||||
total: 0,
|
date: dayjs(),
|
||||||
billlines: selectedLines.map((p) => {
|
total: 0,
|
||||||
return {
|
billlines: selectedLines.map((p) => {
|
||||||
joblineid: p.id,
|
return {
|
||||||
actual_price: p.act_price,
|
joblineid: p.id,
|
||||||
actual_cost: 0, //p.act_price,
|
actual_price: p.act_price,
|
||||||
line_desc: p.line_desc,
|
actual_cost: 0, //p.act_price,
|
||||||
line_remarks: p.line_remarks,
|
line_desc: p.line_desc,
|
||||||
part_type: p.part_type,
|
line_remarks: p.line_remarks,
|
||||||
quantity: p.quantity || 1,
|
part_type: p.part_type,
|
||||||
applicable_taxes: {
|
quantity: p.quantity || 1,
|
||||||
local: false,
|
applicable_taxes: {
|
||||||
state: false,
|
local: false,
|
||||||
federal: false
|
state: false,
|
||||||
}
|
federal: false
|
||||||
};
|
}
|
||||||
})
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
//Clear out the selected lines. IO-785
|
//Clear out the selected lines. IO-785
|
||||||
setSelectedLines([]);
|
setSelectedLines([]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HomeOutlined />
|
<HomeOutlined />
|
||||||
{t("parts.actions.orderinhouse")}
|
{t("parts.actions.orderinhouse")}
|
||||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
id="job-lines-order-parts-button"
|
id="job-lines-order-parts-button"
|
||||||
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
||||||
@@ -578,7 +580,8 @@ export function JobLinesComponent({
|
|||||||
{t("joblines.actions.new")}
|
{t("joblines.actions.new")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
{!isPartsEntry &&
|
||||||
|
InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
||||||
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
|
|||||||
@@ -139,17 +139,18 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
|||||||
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||||
<ProductionListColumnComment record={job} />
|
<ProductionListColumnComment record={job} />
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
|
{!isPartsEntry && <DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>}
|
||||||
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||||
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
||||||
{job.po_number}
|
{job.po_number}
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
{!isPartsEntry && (
|
||||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||||
</DataLabel>
|
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||||
|
</DataLabel>
|
||||||
|
)}
|
||||||
{!isPartsEntry && (
|
{!isPartsEntry && (
|
||||||
<>
|
<>
|
||||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||||
|
|||||||
@@ -40,27 +40,26 @@ export function ScheduleCalendarWrapperComponent({
|
|||||||
const currentView = search.view || defaultView || "week";
|
const currentView = search.view || defaultView || "week";
|
||||||
|
|
||||||
const handleEventPropStyles = (event) => {
|
const handleEventPropStyles = (event) => {
|
||||||
const hasColor = Boolean(event?.color?.hex || event?.color);
|
const { color, block, arrived } = event ?? {};
|
||||||
|
const hasColor = Boolean(color?.hex || color);
|
||||||
const useBg = currentView !== "agenda";
|
const useBg = currentView !== "agenda";
|
||||||
|
|
||||||
// Prioritize explicit blocked-day background to ensure red in all themes
|
// Prioritize explicit blocked-day background to ensure red in all themes
|
||||||
let bg;
|
let bg;
|
||||||
if (useBg) {
|
if (useBg) {
|
||||||
if (event?.block) {
|
bg = block
|
||||||
bg = "var(--event-block-bg)";
|
? "var(--event-block-bg)"
|
||||||
} else if (hasColor) {
|
: arrived
|
||||||
bg = event?.color?.hex ?? event?.color;
|
? "var(--event-arrived-bg)"
|
||||||
} else {
|
: (color?.hex ?? color ?? "var(--event-bg-fallback)");
|
||||||
bg = "var(--event-bg-fallback)";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const usedFallback = !hasColor && !event?.block; // only mark as fallback when not blocked
|
const usedFallback = !hasColor && !block && !arrived; // only mark as fallback when not blocked or arrived
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
"imex-event",
|
"imex-event",
|
||||||
event.arrived && "imex-event-arrived",
|
arrived && "imex-event-arrived",
|
||||||
event.block && "imex-event-block",
|
block && "imex-event-block",
|
||||||
usedFallback && "imex-event-fallback"
|
usedFallback && "imex-event-fallback"
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|||||||
@@ -23,13 +23,24 @@ export default function ShopInfoContainer() {
|
|||||||
});
|
});
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
|
||||||
const combinedFeatureConfig = {
|
const combineFeatureConfigs = (...configs) =>
|
||||||
...FEATURE_CONFIGS.general,
|
(configs || [])
|
||||||
...FEATURE_CONFIGS.responsibilitycenters
|
.filter(Boolean)
|
||||||
};
|
.flatMap((cfg) => Object.entries(cfg))
|
||||||
|
.reduce((acc, [featureName, fieldPaths]) => {
|
||||||
|
if (!Array.isArray(fieldPaths)) return acc;
|
||||||
|
acc[featureName] = [...(acc[featureName] ?? []), ...fieldPaths];
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const combinedFeatureConfig = combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters);
|
||||||
|
|
||||||
// Use form data preservation for all shop-info features
|
// Use form data preservation for all shop-info features
|
||||||
const { createSubmissionHandler } = useFormDataPreservation(form, data?.bodyshops[0], combinedFeatureConfig);
|
const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation(
|
||||||
|
form,
|
||||||
|
data?.bodyshops[0],
|
||||||
|
combinedFeatureConfig
|
||||||
|
);
|
||||||
|
|
||||||
const handleFinish = createSubmissionHandler((values) => {
|
const handleFinish = createSubmissionHandler((values) => {
|
||||||
setSaveLoading(true);
|
setSaveLoading(true);
|
||||||
@@ -51,8 +62,11 @@ export default function ShopInfoContainer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) form.resetFields();
|
if (!data) return;
|
||||||
}, [form, data]);
|
form.resetFields();
|
||||||
|
// After reset, re-apply hidden field preservation so values aren't wiped
|
||||||
|
preserveHiddenFormData();
|
||||||
|
}, [data, form, preserveHiddenFormData]);
|
||||||
|
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,73 +8,57 @@ import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component
|
|||||||
* @param {Object} featureConfig - Configuration object defining which features and their associated fields to preserve
|
* @param {Object} featureConfig - Configuration object defining which features and their associated fields to preserve
|
||||||
*/
|
*/
|
||||||
export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
|
export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
|
||||||
const getNestedValue = (obj, path) => {
|
// Safe nested getters/setters using path arrays
|
||||||
return path.reduce((current, key) => current?.[key], obj);
|
const getNestedValue = (obj, path) => path?.reduce((acc, key) => acc?.[key], obj);
|
||||||
};
|
|
||||||
|
|
||||||
const setNestedValue = (obj, path, value) => {
|
const setNestedValue = (obj, path, value) => {
|
||||||
const lastKey = path[path.length - 1];
|
const lastKey = path[path.length - 1];
|
||||||
const parentPath = path.slice(0, -1);
|
const parent = path.slice(0, -1).reduce((curr, key) => {
|
||||||
|
if (!curr[key] || typeof curr[key] !== "object") curr[key] = {};
|
||||||
const parent = parentPath.reduce((current, key) => {
|
return curr[key];
|
||||||
if (!current[key]) current[key] = {};
|
|
||||||
return current[key];
|
|
||||||
}, obj);
|
}, obj);
|
||||||
|
|
||||||
parent[lastKey] = value;
|
parent[lastKey] = value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const preserveHiddenFormData = useCallback(() => {
|
// Paths for features that are NOT accessible
|
||||||
const preservationData = {};
|
const disabledPaths = useMemo(() => {
|
||||||
let hasDataToPreserve = false;
|
const result = [];
|
||||||
|
if (!featureConfig) return result;
|
||||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||||
|
if (hasAccess || !Array.isArray(fieldPaths)) return;
|
||||||
|
fieldPaths.forEach((p) => Array.isArray(p) && p.length && result.push(p));
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}, [featureConfig, bodyshop]);
|
||||||
|
|
||||||
if (!hasAccess) {
|
const preserveHiddenFormData = useCallback(() => {
|
||||||
fieldPaths.forEach((fieldPath) => {
|
const currentValues = form.getFieldsValue();
|
||||||
const currentValues = form.getFieldsValue();
|
const preservationData = {};
|
||||||
let value = getNestedValue(currentValues, fieldPath);
|
let hasAny = false;
|
||||||
|
|
||||||
if (value === undefined || value === null) {
|
disabledPaths.forEach((path) => {
|
||||||
value = getNestedValue(bodyshop, fieldPath);
|
let value = getNestedValue(currentValues, path);
|
||||||
}
|
if (value == null) value = getNestedValue(bodyshop, path);
|
||||||
|
if (value != null) {
|
||||||
if (value !== undefined && value !== null) {
|
setNestedValue(preservationData, path, value);
|
||||||
setNestedValue(preservationData, fieldPath, value);
|
hasAny = true;
|
||||||
hasDataToPreserve = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasDataToPreserve) {
|
if (hasAny) form.setFieldsValue(preservationData);
|
||||||
form.setFieldsValue(preservationData);
|
}, [form, bodyshop, disabledPaths]);
|
||||||
}
|
|
||||||
}, [form, featureConfig, bodyshop]);
|
|
||||||
|
|
||||||
const getCompleteFormValues = () => {
|
const getCompleteFormValues = () => {
|
||||||
const currentFormValues = form.getFieldsValue();
|
const currentValues = form.getFieldsValue();
|
||||||
const completeValues = { ...currentFormValues };
|
const complete = { ...currentValues };
|
||||||
|
|
||||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
disabledPaths.forEach((path) => {
|
||||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
let value = getNestedValue(currentValues, path);
|
||||||
|
if (value == null) value = getNestedValue(bodyshop, path);
|
||||||
if (!hasAccess) {
|
if (value != null) setNestedValue(complete, path, value);
|
||||||
fieldPaths.forEach((fieldPath) => {
|
|
||||||
let value = getNestedValue(currentFormValues, fieldPath);
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
value = getNestedValue(bodyshop, fieldPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== undefined && value !== null) {
|
|
||||||
setNestedValue(completeValues, fieldPath, value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return completeValues;
|
return complete;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createSubmissionHandler = (originalHandler) => {
|
const createSubmissionHandler = (originalHandler) => {
|
||||||
@@ -103,8 +87,8 @@ export const FEATURE_CONFIGS = {
|
|||||||
["md_responsibility_centers", "profits"],
|
["md_responsibility_centers", "profits"],
|
||||||
["md_responsibility_centers", "defaults"],
|
["md_responsibility_centers", "defaults"],
|
||||||
["md_responsibility_centers", "dms_defaults"],
|
["md_responsibility_centers", "dms_defaults"],
|
||||||
["md_responsibility_centers", "taxes", "itemexemptcode"],
|
["md_responsibility_centers", "taxes"],
|
||||||
["md_responsibility_centers", "taxes", "invoiceexemptcode"],
|
["md_responsibility_centers", "cieca_pfl"],
|
||||||
["md_responsibility_centers", "ar"],
|
["md_responsibility_centers", "ar"],
|
||||||
["md_responsibility_centers", "refund"],
|
["md_responsibility_centers", "refund"],
|
||||||
["md_responsibility_centers", "sales_tax_codes"],
|
["md_responsibility_centers", "sales_tax_codes"],
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { connect } from "react-redux";
|
|||||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
|
||||||
import { pageLimit } from "../../utils/config";
|
import { pageLimit } from "../../utils/config";
|
||||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
@@ -144,26 +143,6 @@ export function SimplifiedPartsJobsListComponent({
|
|||||||
sortOrder: sortcolumn === "clm_no" && sortorder,
|
sortOrder: sortcolumn === "clm_no" && sortorder,
|
||||||
render: (text, record) => `${record.clm_no || ""}${record.po_number ? ` (PO: ${record.po_number})` : ""}`
|
render: (text, record) => `${record.clm_no || ""}${record.po_number ? ` (PO: ${record.po_number})` : ""}`
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t("jobs.fields.ins_co_nm"),
|
|
||||||
dataIndex: "ins_co_nm",
|
|
||||||
key: "ins_co_nm",
|
|
||||||
ellipsis: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("jobs.fields.clm_total"),
|
|
||||||
dataIndex: "clm_total",
|
|
||||||
key: "clm_total",
|
|
||||||
sorter: search?.search ? (a, b) => a.clm_total - b.clm_total : true,
|
|
||||||
sortOrder: sortcolumn === "clm_total" && sortorder,
|
|
||||||
render: (text, record) => {
|
|
||||||
return record.clm_total ? (
|
|
||||||
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
|
||||||
) : (
|
|
||||||
t("general.labels.unknown")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t("jobs.fields.partsstatus"),
|
title: t("jobs.fields.partsstatus"),
|
||||||
dataIndex: "partsstatus",
|
dataIndex: "partsstatus",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import "./translations/i18n";
|
|||||||
import "./utils/CleanAxios";
|
import "./utils/CleanAxios";
|
||||||
import * as amplitude from "@amplitude/analytics-browser";
|
import * as amplitude from "@amplitude/analytics-browser";
|
||||||
import { PostHogProvider } from "posthog-js/react";
|
import { PostHogProvider } from "posthog-js/react";
|
||||||
|
import posthog from "posthog-js";
|
||||||
|
|
||||||
window.global ||= window;
|
window.global ||= window;
|
||||||
|
|
||||||
@@ -25,7 +26,29 @@ registerSW({ immediate: true });
|
|||||||
// Dinero.globalLocale = "en-CA";
|
// Dinero.globalLocale = "en-CA";
|
||||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||||
|
|
||||||
amplitude.init("6228a598e57cd66875cfd41604f1f891", {});
|
amplitude.init("6228a598e57cd66875cfd41604f1f891", {
|
||||||
|
defaultTracking: true
|
||||||
|
// {
|
||||||
|
// 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,
|
||||||
|
capture_exceptions: true,
|
||||||
|
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST
|
||||||
|
});
|
||||||
|
|
||||||
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
||||||
|
|
||||||
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
|
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
|
||||||
@@ -39,10 +62,7 @@ function App() {
|
|||||||
return (
|
return (
|
||||||
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
|
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<PostHogProvider
|
<PostHogProvider client={posthog}>
|
||||||
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
|
|
||||||
options={{ autocapture: false, capture_exceptions: true }}
|
|
||||||
>
|
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</PostHogProvider>
|
</PostHogProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ export function* signInSuccessSaga({ payload }) {
|
|||||||
|
|
||||||
setUserId(analytics, payload.email);
|
setUserId(analytics, payload.email);
|
||||||
setUserProperties(analytics, payload);
|
setUserProperties(analytics, payload);
|
||||||
|
yield;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function* onSendPasswordResetStart() {
|
export function* onSendPasswordResetStart() {
|
||||||
|
|||||||
@@ -181,7 +181,10 @@ const cache = new InMemoryCache({
|
|||||||
const client = new ApolloClient({
|
const client = new ApolloClient({
|
||||||
link: ApolloLink.from(middlewares),
|
link: ApolloLink.from(middlewares),
|
||||||
cache,
|
cache,
|
||||||
connectToDevTools: import.meta.env.DEV,
|
devtools: {
|
||||||
|
name: "Imex Client",
|
||||||
|
enabled: import.meta.env.DEV
|
||||||
|
},
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
watchQuery: {
|
watchQuery: {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
|
|||||||
2565
package-lock.json
generated
2565
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
26
package.json
26
package.json
@@ -18,14 +18,14 @@
|
|||||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.873.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.876.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.872.0",
|
"@aws-sdk/client-elasticache": "^3.876.0",
|
||||||
"@aws-sdk/client-s3": "^3.872.0",
|
"@aws-sdk/client-s3": "^3.876.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.872.0",
|
"@aws-sdk/client-secrets-manager": "^3.876.0",
|
||||||
"@aws-sdk/client-ses": "^3.872.0",
|
"@aws-sdk/client-ses": "^3.876.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.873.0",
|
"@aws-sdk/credential-provider-node": "^3.876.0",
|
||||||
"@aws-sdk/lib-storage": "^3.872.0",
|
"@aws-sdk/lib-storage": "^3.876.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.872.0",
|
"@aws-sdk/s3-request-presigner": "^3.876.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
@@ -34,14 +34,14 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"axios-curlirize": "^2.0.0",
|
"axios-curlirize": "^2.0.0",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bullmq": "^5.58.0",
|
"bullmq": "^5.58.2",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
"cloudinary": "^2.7.0",
|
"cloudinary": "^2.7.0",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"dd-trace": "^5.63.3",
|
"dd-trace": "^5.64.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^17.2.1",
|
"dotenv": "^17.2.1",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"query-string": "7.1.3",
|
"query-string": "7.1.3",
|
||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"skia-canvas": "^3.0.3",
|
"skia-canvas": "^3.0.4",
|
||||||
"soap": "^1.3.0",
|
"soap": "^1.3.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
@@ -77,8 +77,8 @@
|
|||||||
"yazl": "^3.3.1"
|
"yazl": "^3.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.34.0",
|
||||||
"eslint": "^9.33.0",
|
"eslint": "^9.34.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"mock-require": "^3.0.3",
|
"mock-require": "^3.0.3",
|
||||||
|
|||||||
@@ -1854,7 +1854,7 @@ exports.GET_CHATTER_SHOPS = `query GET_CHATTER_SHOPS {
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
exports.GET_CARFAX_SHOPS = `query GET_CARFAX_SHOPS {
|
exports.GET_CARFAX_SHOPS = `query GET_CARFAX_SHOPS {
|
||||||
bodyshops{
|
bodyshops(where: {external_shop_id: {_is_null: true}}){
|
||||||
id
|
id
|
||||||
shopname
|
shopname
|
||||||
imexshopid
|
imexshopid
|
||||||
|
|||||||
@@ -775,6 +775,7 @@
|
|||||||
"csi:page": 11,
|
"csi:page": 11,
|
||||||
"jobs:void": 80,
|
"jobs:void": 80,
|
||||||
"shop:rbac": 99,
|
"shop:rbac": 99,
|
||||||
|
"shop:responsibilitycenter": 99,
|
||||||
"bills:list": 11,
|
"bills:list": 11,
|
||||||
"bills:view": 11,
|
"bills:view": 11,
|
||||||
"csi:export": 11,
|
"csi:export": 11,
|
||||||
|
|||||||
@@ -23,10 +23,16 @@ const KNOWN_PART_RATE_TYPES = [
|
|||||||
* @returns {object} The parts tax rates object.
|
* @returns {object} The parts tax rates object.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
//TODO: Major validation would be required on this - EMS files are inconsistent with things like 5% being passed as 5.0 or .05.
|
||||||
const extractPartsTaxRates = (profile = {}) => {
|
const extractPartsTaxRates = (profile = {}) => {
|
||||||
const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}];
|
const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}];
|
||||||
const partsTaxRates = {};
|
const partsTaxRates = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this context, r.RateType._ accesses the property named _ on the RateType object.
|
||||||
|
* This pattern is common when handling data parsed from XML, where element values are stored under the _ key. So,
|
||||||
|
* _ aligns to the actual value/content of the RateType field when RateType is an object (not a string).
|
||||||
|
*/
|
||||||
for (const r of rateInfos) {
|
for (const r of rateInfos) {
|
||||||
const rateTypeRaw =
|
const rateTypeRaw =
|
||||||
typeof r?.RateType === "string"
|
typeof r?.RateType === "string"
|
||||||
|
|||||||
197
server/integrations/partsManagement/endpoints/lib/opCodes.json
Normal file
197
server/integrations/partsManagement/endpoints/lib/opCodes.json
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
{
|
||||||
|
"OP0": {
|
||||||
|
"desc": "REMOVE / REPLACE PARTIAL",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAA"
|
||||||
|
},
|
||||||
|
"OP1": {
|
||||||
|
"desc": "REFINISH / REPAIR",
|
||||||
|
"opcode": "OP1",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP2": {
|
||||||
|
"desc": "REMOVE / INSTALL",
|
||||||
|
"opcode": "OP2",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP3": {
|
||||||
|
"desc": "ADDITIONAL LABOR",
|
||||||
|
"opcode": "OP9",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP4": {
|
||||||
|
"desc": "ALIGNMENT",
|
||||||
|
"opcode": "OP4",
|
||||||
|
"partcode": "PAS"
|
||||||
|
},
|
||||||
|
"OP5": {
|
||||||
|
"desc": "OVERHAUL",
|
||||||
|
"opcode": "OP5",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP6": {
|
||||||
|
"desc": "REFINISH",
|
||||||
|
"opcode": "OP6",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP7": {
|
||||||
|
"desc": "INSPECT",
|
||||||
|
"opcode": "OP7",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP8": {
|
||||||
|
"desc": "CHECK / ADJUST",
|
||||||
|
"opcode": "OP8",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP9": {
|
||||||
|
"desc": "REPAIR",
|
||||||
|
"opcode": "OP9",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP10": {
|
||||||
|
"desc": "REPAIR , PARTIAL",
|
||||||
|
"opcode": "OP9",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP11": {
|
||||||
|
"desc": "REMOVE / REPLACE",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAN"
|
||||||
|
},
|
||||||
|
"OP12": {
|
||||||
|
"desc": "REMOVE / REPLACE PARTIAL",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAN"
|
||||||
|
},
|
||||||
|
"OP13": {
|
||||||
|
"desc": "ADDITIONAL COSTS",
|
||||||
|
"opcode": "OP13",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP14": {
|
||||||
|
"desc": "ADDITIONAL OPERATIONS",
|
||||||
|
"opcode": "OP14",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP15": {
|
||||||
|
"desc": "BLEND",
|
||||||
|
"opcode": "OP15",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP16": {
|
||||||
|
"desc": "SUBLET",
|
||||||
|
"opcode": "OP16",
|
||||||
|
"partcode": "PAS"
|
||||||
|
},
|
||||||
|
"OP17": {
|
||||||
|
"desc": "POLICY LIMIT ADJUSTMENT",
|
||||||
|
"opcode": "OP9",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP18": {
|
||||||
|
"desc": "APPEAR ALLOWANCE",
|
||||||
|
"opcode": "OP7",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP20": {
|
||||||
|
"desc": "REMOVE AND REINSTALL",
|
||||||
|
"opcode": "OP20",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP24": {
|
||||||
|
"desc": "CHIPGUARD",
|
||||||
|
"opcode": "OP6",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP25": {
|
||||||
|
"desc": "TWO TONE",
|
||||||
|
"opcode": "OP6",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP26": {
|
||||||
|
"desc": "PAINTLESS DENT REPAIR",
|
||||||
|
"opcode": "OP16",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP100": {
|
||||||
|
"desc": "REPLACE PRE-PRICED",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAA"
|
||||||
|
},
|
||||||
|
"OP101": {
|
||||||
|
"desc": "REMOVE/REPLACE RECYCLED PART",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAL"
|
||||||
|
},
|
||||||
|
"OP103": {
|
||||||
|
"desc": "REMOVE / REPLACE PARTIAL",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAA"
|
||||||
|
},
|
||||||
|
"OP104": {
|
||||||
|
"desc": "REMOVE / REPLACE PARTIAL LABOUR",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAA"
|
||||||
|
},
|
||||||
|
"OP105": {
|
||||||
|
"desc": "!!ADJUST MANUALLY!!",
|
||||||
|
"opcode": "OP99",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP106": {
|
||||||
|
"desc": "REPAIR , PARTIAL",
|
||||||
|
"opcode": "OP9",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP107": {
|
||||||
|
"desc": "CHIPGUARD",
|
||||||
|
"opcode": "OP6",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP108": {
|
||||||
|
"desc": "MULTI TONE",
|
||||||
|
"opcode": "OP6",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP109": {
|
||||||
|
"desc": "REPLACE PRE-PRICED",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAA"
|
||||||
|
},
|
||||||
|
"OP110": {
|
||||||
|
"desc": "REFINISH / REPAIR",
|
||||||
|
"opcode": "OP1",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP111": {
|
||||||
|
"desc": "REMOVE / REPLACE",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAN"
|
||||||
|
},
|
||||||
|
"OP112": {
|
||||||
|
"desc": "REMOVE / REPLACE",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAA"
|
||||||
|
},
|
||||||
|
"OP113": {
|
||||||
|
"desc": "REPLACE PRE-PRICED",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAA"
|
||||||
|
},
|
||||||
|
"OP114": {
|
||||||
|
"desc": "REPLACE PRE-PRICED",
|
||||||
|
"opcode": "OP11",
|
||||||
|
"partcode": "PAA"
|
||||||
|
},
|
||||||
|
"OP120": {
|
||||||
|
"desc": "REPAIR , PARTIAL",
|
||||||
|
"opcode": "OP9",
|
||||||
|
"partcode": "PAE"
|
||||||
|
},
|
||||||
|
"OP260": {
|
||||||
|
"desc": "SUBLET",
|
||||||
|
"opcode": "OP16",
|
||||||
|
"partcode": "PAE"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
const admin = require("firebase-admin");
|
const admin = require("firebase-admin");
|
||||||
const client = require("../../../graphql-client/graphql-client").client;
|
const client = require("../../../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
DELETE_SHOP,
|
DELETE_SHOP,
|
||||||
DELETE_VENDORS_BY_SHOP,
|
DELETE_VENDORS_BY_SHOP,
|
||||||
@@ -18,143 +17,154 @@ const {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a Firebase user by UID.
|
* Deletes a Firebase user by UID.
|
||||||
* @param uid
|
* @param {string} uid - The Firebase user ID
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const deleteFirebaseUser = async (uid) => {
|
const deleteFirebaseUser = async (uid) => {
|
||||||
|
if (!uid) throw new Error("User UID is required");
|
||||||
return admin.auth().deleteUser(uid);
|
return admin.auth().deleteUser(uid);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all vendors associated with a shop.
|
* Deletes all vendors associated with a shop.
|
||||||
* @param shopId
|
* @param {string} shopId - The shop ID
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const deleteVendorsByShop = async (shopId) => {
|
const deleteVendorsByShop = async (shopId) => {
|
||||||
|
if (!shopId) throw new Error("Shop ID is required");
|
||||||
await client.request(DELETE_VENDORS_BY_SHOP, { shopId });
|
await client.request(DELETE_VENDORS_BY_SHOP, { shopId });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a bodyshop from the database.
|
* Deletes a bodyshop from the database.
|
||||||
* @param shopId
|
* @param {string} shopId - The shop ID
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const deleteBodyshop = async (shopId) => {
|
const deleteBodyshop = async (shopId) => {
|
||||||
|
if (!shopId) throw new Error("Shop ID is required");
|
||||||
await client.request(DELETE_SHOP, { id: shopId });
|
await client.request(DELETE_SHOP, { id: shopId });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch job ids for a given shop
|
* Fetch job ids for a given shop
|
||||||
* @param shopId
|
* @param {string} shopId - The shop ID
|
||||||
* @returns {Promise<string[]>}
|
* @returns {Promise<string[]>}
|
||||||
*/
|
*/
|
||||||
const getJobIdsForShop = async (shopId) => {
|
const getJobIdsForShop = async (shopId) => {
|
||||||
|
if (!shopId) throw new Error("Shop ID is required");
|
||||||
const resp = await client.request(GET_JOBS_BY_SHOP, { shopId });
|
const resp = await client.request(GET_JOBS_BY_SHOP, { shopId });
|
||||||
return resp.jobs.map((j) => j.id);
|
return resp.jobs?.map((j) => j.id) || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete joblines for the given job ids
|
* Delete joblines for the given job ids
|
||||||
* @param jobIds {string[]}
|
* @param {string[]} jobIds - Array of job IDs
|
||||||
* @returns {Promise<number>} affected rows
|
* @returns {Promise<number>} affected rows
|
||||||
*/
|
*/
|
||||||
const deleteJoblinesForJobs = async (jobIds) => {
|
const deleteJoblinesForJobs = async (jobIds) => {
|
||||||
if (!jobIds.length) return 0;
|
if (!jobIds?.length) return 0;
|
||||||
const resp = await client.request(DELETE_JOBLINES_BY_JOB_IDS, { jobIds });
|
const resp = await client.request(DELETE_JOBLINES_BY_JOB_IDS, { jobIds });
|
||||||
return resp.delete_joblines.affected_rows;
|
return resp.delete_joblines?.affected_rows || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete jobs for the given job ids
|
* Delete jobs for the given job ids
|
||||||
* @param jobIds {string[]}
|
* @param {string[]} jobIds - Array of job IDs
|
||||||
* @returns {Promise<number>} affected rows
|
* @returns {Promise<number>} affected rows
|
||||||
*/
|
*/
|
||||||
const deleteJobsByIds = async (jobIds) => {
|
const deleteJobsByIds = async (jobIds) => {
|
||||||
if (!jobIds.length) return 0;
|
if (!jobIds?.length) return 0;
|
||||||
const resp = await client.request(DELETE_JOBS_BY_IDS, { jobIds });
|
const resp = await client.request(DELETE_JOBS_BY_IDS, { jobIds });
|
||||||
return resp.delete_jobs.affected_rows;
|
return resp.delete_jobs?.affected_rows || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles deprovisioning a shop for parts management.
|
* Handles deprovisioning a shop for parts management.
|
||||||
* @param req
|
* @param {Object} req - Express request object
|
||||||
* @param res
|
* @param {Object} res - Express response object
|
||||||
* @returns {Promise<*>}
|
* @returns {Promise<*>}
|
||||||
*/
|
*/
|
||||||
const partsManagementDeprovisioning = async (req, res) => {
|
const partsManagementDeprovisioning = async (req, res) => {
|
||||||
const { logger } = req;
|
const { logger } = req;
|
||||||
const p = req.body;
|
const { shopId } = req.body;
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production") {
|
if (process.env.NODE_ENV === "production") {
|
||||||
return res.status(403).json({ error: "Deprovisioning not allowed in production environment." });
|
return res.status(403).json({ error: "Deprovisioning not allowed in production environment." });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!p.shopId) {
|
if (!shopId) {
|
||||||
throw { status: 400, message: "shopId is required." };
|
throw { status: 400, message: "shopId is required." };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch bodyshop and check external_shop_id
|
// Fetch bodyshop and check external_shop_id
|
||||||
const shopResp = await client.request(GET_BODYSHOP, { id: p.shopId });
|
const shopResp = await client.request(GET_BODYSHOP, { id: shopId });
|
||||||
const shop = shopResp.bodyshops_by_pk;
|
const shop = shopResp.bodyshops_by_pk;
|
||||||
if (!shop) {
|
if (!shop) {
|
||||||
throw { status: 404, message: `Bodyshop with id ${p.shopId} not found.` };
|
throw { status: 404, message: `Bodyshop with id ${shopId} not found.` };
|
||||||
}
|
}
|
||||||
if (!shop.external_shop_id) {
|
if (!shop.external_shop_id) {
|
||||||
throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." };
|
throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." };
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("admin-delete-shop", "debug", null, null, {
|
logger.log("admin-delete-shop", "debug", null, null, {
|
||||||
shopId: p.shopId,
|
shopId,
|
||||||
shopname: shop.shopname,
|
shopname: shop.shopname,
|
||||||
ioadmin: true
|
ioadmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get vendors
|
// Get vendors
|
||||||
const vendorsResp = await client.request(GET_VENDORS, { shopId: p.shopId });
|
const vendorsResp = await client.request(GET_VENDORS, { shopId });
|
||||||
const deletedVendors = vendorsResp.vendors.map((v) => v.name);
|
const deletedVendors = vendorsResp.vendors?.map((v) => v.name) || [];
|
||||||
|
|
||||||
// Get associated users
|
// Get associated users
|
||||||
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: p.shopId });
|
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId });
|
||||||
const associatedUsers = assocResp.associations.map((assoc) => ({
|
const associatedUsers =
|
||||||
authId: assoc.user.authid,
|
assocResp.associations?.map((assoc) => ({
|
||||||
email: assoc.user.email
|
authId: assoc.user?.authid,
|
||||||
}));
|
email: assoc.user?.email
|
||||||
|
})) || [];
|
||||||
|
|
||||||
// Delete associations for the shop
|
// Delete associations for the shop
|
||||||
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: p.shopId });
|
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId });
|
||||||
const associationsDeleted = assocDeleteResp.delete_associations.affected_rows;
|
const associationsDeleted = assocDeleteResp.delete_associations?.affected_rows || 0;
|
||||||
|
|
||||||
// For each user, check if they have remaining associations; if not, delete user and Firebase account
|
// Delete users with no remaining associations
|
||||||
const deletedUsers = [];
|
const deletedUsers = [];
|
||||||
for (const user of associatedUsers) {
|
for (const user of associatedUsers) {
|
||||||
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
|
if (!user.email || !user.authId) continue;
|
||||||
const assocCount = countResp.associations_aggregate.aggregate.count;
|
try {
|
||||||
if (assocCount === 0) {
|
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
|
||||||
await client.request(DELETE_USER, { email: user.email });
|
const assocCount = countResp.associations_aggregate?.aggregate?.count || 0;
|
||||||
await deleteFirebaseUser(user.authId);
|
if (assocCount === 0) {
|
||||||
deletedUsers.push(user.email);
|
await client.request(DELETE_USER, { email: user.email });
|
||||||
|
await deleteFirebaseUser(user.authId);
|
||||||
|
deletedUsers.push(user.email);
|
||||||
|
}
|
||||||
|
} catch (userError) {
|
||||||
|
logger.log("admin-delete-user-error", "warn", null, null, {
|
||||||
|
email: user.email,
|
||||||
|
error: userError.message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all job ids for this shop, then delete joblines and jobs (joblines first)
|
// Delete jobs and joblines
|
||||||
const jobIds = await getJobIdsForShop(p.shopId);
|
const jobIds = await getJobIdsForShop(shopId);
|
||||||
const joblinesDeleted = await deleteJoblinesForJobs(jobIds);
|
const joblinesDeleted = await deleteJoblinesForJobs(jobIds);
|
||||||
const jobsDeleted = await deleteJobsByIds(jobIds);
|
const jobsDeleted = await deleteJobsByIds(jobIds);
|
||||||
|
|
||||||
// Delete any audit trail entries tied to this bodyshop to avoid FK violations
|
// Delete audit trail
|
||||||
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: p.shopId });
|
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId });
|
||||||
const auditDeleted = auditResp.delete_audit_trail.affected_rows;
|
const auditDeleted = auditResp.delete_audit_trail?.affected_rows || 0;
|
||||||
|
|
||||||
// Delete vendors
|
// Delete vendors and shop
|
||||||
await deleteVendorsByShop(p.shopId);
|
await deleteVendorsByShop(shopId);
|
||||||
|
await deleteBodyshop(shopId);
|
||||||
// Delete shop
|
|
||||||
await deleteBodyshop(p.shopId);
|
|
||||||
|
|
||||||
// Summary log
|
// Summary log
|
||||||
logger.log("admin-delete-shop-summary", "info", null, null, {
|
logger.log("admin-delete-shop-summary", "info", null, null, {
|
||||||
shopId: p.shopId,
|
shopId,
|
||||||
shopname: shop.shopname,
|
shopname: shop.shopname,
|
||||||
associationsDeleted,
|
associationsDeleted,
|
||||||
deletedUsers,
|
deletedUsers,
|
||||||
@@ -165,11 +175,11 @@ const partsManagementDeprovisioning = async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
message: `Bodyshop ${p.shopId} and associated resources deleted successfully.`,
|
message: `Bodyshop ${shopId} and associated resources deleted successfully.`,
|
||||||
deletedShop: { id: p.shopId, name: shop.shopname },
|
deletedShop: { id: shopId, name: shop.shopname },
|
||||||
deletedAssociationsCount: associationsDeleted,
|
deletedAssociationsCount: associationsDeleted,
|
||||||
deletedUsers: deletedUsers,
|
deletedUsers,
|
||||||
deletedVendors: deletedVendors,
|
deletedVendors,
|
||||||
deletedJoblinesCount: joblinesDeleted,
|
deletedJoblinesCount: joblinesDeleted,
|
||||||
deletedJobsCount: jobsDeleted,
|
deletedJobsCount: jobsDeleted,
|
||||||
deletedAuditTrailCount: auditDeleted
|
deletedAuditTrailCount: auditDeleted
|
||||||
@@ -177,9 +187,8 @@ const partsManagementDeprovisioning = async (req, res) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log("admin-delete-shop-error", "error", null, null, {
|
logger.log("admin-delete-shop-error", "error", null, null, {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
detail: err.detail || err
|
detail: err.detail || err.stack || err
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(err.status || 500).json({ error: err.message || "Internal server error" });
|
return res.status(err.status || 500).json({ error: err.message || "Internal server error" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const insertUserAssociation = async (uid, email, shopId) => {
|
|||||||
authid: uid,
|
authid: uid,
|
||||||
validemail: true,
|
validemail: true,
|
||||||
associations: {
|
associations: {
|
||||||
data: [{ shopid: shopId, authlevel: 80, active: true }]
|
data: [{ shopid: shopId, authlevel: 99, active: true }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -139,11 +139,11 @@ const insertUserAssociation = async (uid, email, shopId) => {
|
|||||||
*/
|
*/
|
||||||
const partsManagementProvisioning = async (req, res) => {
|
const partsManagementProvisioning = async (req, res) => {
|
||||||
const { logger } = req;
|
const { logger } = req;
|
||||||
const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
|
const body = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureEmailNotRegistered(p.userEmail);
|
await ensureEmailNotRegistered(body.userEmail);
|
||||||
requireFields(p, [
|
requireFields(body, [
|
||||||
"external_shop_id",
|
"external_shop_id",
|
||||||
"shopname",
|
"shopname",
|
||||||
"address1",
|
"address1",
|
||||||
@@ -155,27 +155,27 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
"phone",
|
"phone",
|
||||||
"userEmail"
|
"userEmail"
|
||||||
]);
|
]);
|
||||||
await ensureExternalIdUnique(p.external_shop_id);
|
await ensureExternalIdUnique(body.external_shop_id);
|
||||||
|
|
||||||
logger.log("admin-create-shop-user", "debug", p.userEmail, null, {
|
logger.log("admin-create-shop-user", "debug", body.userEmail, null, {
|
||||||
request: req.body,
|
request: req.body,
|
||||||
ioadmin: true
|
ioadmin: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const shopInput = {
|
const shopInput = {
|
||||||
shopname: p.shopname,
|
shopname: body.shopname,
|
||||||
address1: p.address1,
|
address1: body.address1,
|
||||||
address2: p.address2 || null,
|
address2: body.address2 || null,
|
||||||
city: p.city,
|
city: body.city,
|
||||||
state: p.state,
|
state: body.state,
|
||||||
zip_post: p.zip_post,
|
zip_post: body.zip_post,
|
||||||
country: p.country,
|
country: body.country,
|
||||||
email: p.email,
|
email: body.email,
|
||||||
external_shop_id: p.external_shop_id,
|
external_shop_id: body.external_shop_id,
|
||||||
timezone: p.timezone || DefaultNewShop.timezone,
|
timezone: body.timezone || DefaultNewShop.timezone,
|
||||||
phone: p.phone,
|
phone: body.phone,
|
||||||
logo_img_path: {
|
logo_img_path: {
|
||||||
src: p.logoUrl,
|
src: body.logoUrl,
|
||||||
width: "",
|
width: "",
|
||||||
height: "",
|
height: "",
|
||||||
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
||||||
@@ -200,7 +200,7 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
appt_alt_transport: DefaultNewShop.appt_alt_transport,
|
appt_alt_transport: DefaultNewShop.appt_alt_transport,
|
||||||
md_jobline_presets: DefaultNewShop.md_jobline_presets,
|
md_jobline_presets: DefaultNewShop.md_jobline_presets,
|
||||||
vendors: {
|
vendors: {
|
||||||
data: p.vendors.map((v) => ({
|
data: body.vendors.map((v) => ({
|
||||||
name: v.name,
|
name: v.name,
|
||||||
street1: v.street1 || null,
|
street1: v.street1 || null,
|
||||||
street2: v.street2 || null,
|
street2: v.street2 || null,
|
||||||
@@ -221,14 +221,14 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const newShopId = await insertBodyshop(shopInput);
|
const newShopId = await insertBodyshop(shopInput);
|
||||||
const userRecord = await createFirebaseUser(p.userEmail, p.userPassword);
|
const userRecord = await createFirebaseUser(body.userEmail, body.userPassword);
|
||||||
let resetLink = null;
|
let resetLink = null;
|
||||||
if (!p.userPassword) resetLink = await generateResetLink(p.userEmail);
|
if (!body.userPassword) resetLink = await generateResetLink(body.userEmail);
|
||||||
|
|
||||||
const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId);
|
const createdUser = await insertUserAssociation(userRecord.uid, body.userEmail, newShopId);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
shop: { id: newShopId, shopname: p.shopname },
|
shop: { id: newShopId, shopname: body.shopname },
|
||||||
user: {
|
user: {
|
||||||
id: createdUser.id,
|
id: createdUser.id,
|
||||||
email: createdUser.email,
|
email: createdUser.email,
|
||||||
@@ -236,7 +236,7 @@ const partsManagementProvisioning = async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log("admin-create-shop-user-error", "error", p.userEmail, null, {
|
logger.log("admin-create-shop-user-error", "error", body.userEmail, null, {
|
||||||
message: err.message,
|
message: err.message,
|
||||||
detail: err.detail || err
|
detail: err.detail || err
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const {
|
|||||||
} = require("../partsManagement.queries");
|
} = require("../partsManagement.queries");
|
||||||
|
|
||||||
// Defaults
|
// Defaults
|
||||||
const FALLBACK_DEFAULT_ORDER_STATUS = "Open";
|
const FALLBACK_DEFAULT_JOB_STATUS = "Open";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the default order status for a bodyshop.
|
* Fetches the default order status for a bodyshop.
|
||||||
@@ -22,13 +22,13 @@ const FALLBACK_DEFAULT_ORDER_STATUS = "Open";
|
|||||||
* @param {object} logger - The logger instance.
|
* @param {object} logger - The logger instance.
|
||||||
* @returns {Promise<string>} The default status or fallback.
|
* @returns {Promise<string>} The default status or fallback.
|
||||||
*/
|
*/
|
||||||
const getDefaultOrderStatus = async (shopId, logger) => {
|
const getDefaultJobStatus = async (shopId, logger) => {
|
||||||
try {
|
try {
|
||||||
const { bodyshop_by_pk } = await client.request(GET_BODYSHOP_STATUS, { id: shopId });
|
const { bodyshop_by_pk } = await client.request(GET_BODYSHOP_STATUS, { id: shopId });
|
||||||
return bodyshop_by_pk?.md_order_statuses?.default_open || FALLBACK_DEFAULT_ORDER_STATUS;
|
return bodyshop_by_pk?.md_ro_statuses?.default_imported || FALLBACK_DEFAULT_JOB_STATUS;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.log("parts-bodyshop-fetch-failed", "warn", shopId, null, { error: err });
|
logger.log("parts-bodyshop-fetch-failed", "warn", shopId, null, { error: err });
|
||||||
return FALLBACK_DEFAULT_ORDER_STATUS;
|
return FALLBACK_DEFAULT_JOB_STATUS;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
@@ -66,6 +66,7 @@ const extractJobData = (rq) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
shopId: rq.ShopID || rq.shopId,
|
shopId: rq.ShopID || rq.shopId,
|
||||||
|
// status: ci.ClaimStatus || null, Proper, setting it default for now
|
||||||
refClaimNum: rq.RefClaimNum,
|
refClaimNum: rq.RefClaimNum,
|
||||||
ciecaid: rq.RqUID || null,
|
ciecaid: rq.RqUID || null,
|
||||||
// Pull Cieca_ttl from ClaimInfo per schema/sample
|
// Pull Cieca_ttl from ClaimInfo per schema/sample
|
||||||
@@ -81,8 +82,6 @@ const extractJobData = (rq) => {
|
|||||||
scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null,
|
scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null,
|
||||||
scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null,
|
scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null,
|
||||||
clm_no: ci.ClaimNum || null,
|
clm_no: ci.ClaimNum || null,
|
||||||
// status: ci.ClaimStatus || null, Proper, setting it default for now
|
|
||||||
status: FALLBACK_DEFAULT_ORDER_STATUS,
|
|
||||||
policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null,
|
policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null,
|
||||||
ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0)
|
ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0)
|
||||||
};
|
};
|
||||||
@@ -101,17 +100,19 @@ const extractOwnerData = (rq, shopId) => {
|
|||||||
const personName = personInfo.PersonName || {};
|
const personName = personInfo.PersonName || {};
|
||||||
const address = personInfo.Communications?.Address || {};
|
const address = personInfo.Communications?.Address || {};
|
||||||
|
|
||||||
let ownr_ph1, ownr_ph2, ownr_ea, ownr_alt_ph;
|
let ownr_ph1, ownr_ph2, ownr_ea;
|
||||||
|
|
||||||
const comms = Array.isArray(ownerOrClaimant.ContactInfo?.Communications)
|
const comms = Array.isArray(ownerOrClaimant.ContactInfo?.Communications)
|
||||||
? ownerOrClaimant.ContactInfo.Communications
|
? ownerOrClaimant.ContactInfo.Communications
|
||||||
: [ownerOrClaimant.ContactInfo?.Communications || {}];
|
: [ownerOrClaimant.ContactInfo?.Communications || {}];
|
||||||
|
|
||||||
for (const c of comms) {
|
for (const c of comms) {
|
||||||
|
// TODO: Should document this logic. 1 and 2 don't
|
||||||
|
// typically indicate type in EMS. This makes sense, but good to document.
|
||||||
if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone;
|
if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone;
|
||||||
if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone;
|
if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone;
|
||||||
if (c.CommQualifier === "EM") ownr_ea = c.CommEmail;
|
if (c.CommQualifier === "EM") ownr_ea = c.CommEmail;
|
||||||
if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone;
|
// if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -127,8 +128,8 @@ const extractOwnerData = (rq, shopId) => {
|
|||||||
ownr_ctry: address.Country || null,
|
ownr_ctry: address.Country || null,
|
||||||
ownr_ph1,
|
ownr_ph1,
|
||||||
ownr_ph2,
|
ownr_ph2,
|
||||||
ownr_ea,
|
ownr_ea
|
||||||
ownr_alt_ph
|
// ownr_alt_ph
|
||||||
// ownr_id_qualifier: ownerOrClaimant.IDInfo?.IDQualifierCode || null // New
|
// ownr_id_qualifier: ownerOrClaimant.IDInfo?.IDQualifierCode || null // New
|
||||||
// ownr_id_num: ownerOrClaimant.IDInfo?.IDNum || null, // New
|
// ownr_id_num: ownerOrClaimant.IDInfo?.IDNum || null, // New
|
||||||
// ownr_preferred_contact: ownerOrClaimant.PreferredContactMethod || null // New
|
// ownr_preferred_contact: ownerOrClaimant.PreferredContactMethod || null // New
|
||||||
@@ -159,37 +160,40 @@ const extractEstimatorData = (rq) => {
|
|||||||
* @param {object} rq - The VehicleDamageEstimateAddRq object.
|
* @param {object} rq - The VehicleDamageEstimateAddRq object.
|
||||||
* @returns {object} Adjuster data.
|
* @returns {object} Adjuster data.
|
||||||
*/
|
*/
|
||||||
const extractAdjusterData = (rq) => {
|
// const extractAdjusterData = (rq) => {
|
||||||
const adjParty = rq.AdminInfo?.Adjuster?.Party || {};
|
// const adjParty = rq.AdminInfo?.Adjuster?.Party || {};
|
||||||
const adjComms = Array.isArray(adjParty.ContactInfo?.Communications)
|
// const adjComms = Array.isArray(adjParty.ContactInfo?.Communications)
|
||||||
? adjParty.ContactInfo.Communications
|
// ? adjParty.ContactInfo.Communications
|
||||||
: [adjParty.ContactInfo?.Communications || {}];
|
// : [adjParty.ContactInfo?.Communications || {}];
|
||||||
|
//
|
||||||
return {
|
// return {
|
||||||
agt_ct_fn: adjParty.PersonInfo?.PersonName?.FirstName || null,
|
// //TODO: I dont think we display agt_ct_* fields in app. Have they typically been sending data here?
|
||||||
agt_ct_ln: adjParty.PersonInfo?.PersonName?.LastName || null,
|
// agt_ct_fn: adjParty.PersonInfo?.PersonName?.FirstName || null,
|
||||||
agt_ct_ph: adjComms.find((c) => c.CommQualifier === "CP")?.CommPhone || null,
|
// agt_ct_ln: adjParty.PersonInfo?.PersonName?.LastName || null,
|
||||||
agt_ea: adjComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null
|
// agt_ct_ph: adjComms.find((c) => c.CommQualifier === "CP")?.CommPhone || null,
|
||||||
};
|
// agt_ea: adjComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null
|
||||||
};
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts repair facility data from the XML request.
|
* Extracts repair facility data from the XML request.
|
||||||
* @param {object} rq - The VehicleDamageEstimateAddRq object.
|
* @param {object} rq - The VehicleDamageEstimateAddRq object.
|
||||||
* @returns {object} Repair facility data.
|
* @returns {object} Repair facility data.
|
||||||
*/
|
*/
|
||||||
const extractRepairFacilityData = (rq) => {
|
// const extractRepairFacilityData = (rq) => {
|
||||||
const rfParty = rq.AdminInfo?.RepairFacility?.Party || {};
|
// const rfParty = rq.AdminInfo?.RepairFacility?.Party || {};
|
||||||
const rfComms = Array.isArray(rfParty.ContactInfo?.Communications)
|
// const rfComms = Array.isArray(rfParty.ContactInfo?.Communications)
|
||||||
? rfParty.ContactInfo.Communications
|
// ? rfParty.ContactInfo.Communications
|
||||||
: [rfParty.ContactInfo?.Communications || {}];
|
// : [rfParty.ContactInfo?.Communications || {}];
|
||||||
|
//
|
||||||
return {
|
// return {
|
||||||
servicing_dealer: rfParty.OrgInfo?.CompanyName || null,
|
// servicing_dealer: rfParty.OrgInfo?.CompanyName || null,
|
||||||
servicing_dealer_contact:
|
// // TODO: The servicing dealer fields are a relic from synergy for a few folks
|
||||||
rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null
|
// // TODO: I suspect RF data could be ignored since they are the RF.
|
||||||
};
|
// servicing_dealer_contact:
|
||||||
};
|
// rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts loss information from the XML request.
|
* Extracts loss information from the XML request.
|
||||||
@@ -203,10 +207,12 @@ const extractLossInfo = (rq) => {
|
|||||||
loss_date: loss.LossDateTime || null,
|
loss_date: loss.LossDateTime || null,
|
||||||
loss_type: custom.LossTypeCode || null,
|
loss_type: custom.LossTypeCode || null,
|
||||||
loss_desc: custom.LossTypeDesc || null
|
loss_desc: custom.LossTypeDesc || null
|
||||||
// primary_poi: loss.PrimaryPOI?.POICode || null,
|
// area_of_impact: {
|
||||||
// secondary_poi: loss.SecondaryPOI?.POICode || null,
|
// impact_1: loss.PrimaryPOI?.POICode || null,
|
||||||
|
// imact_2 :loss.SecondaryPOI?.POICode || null,
|
||||||
|
// },
|
||||||
|
// tlosind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null,
|
||||||
// damage_memo: loss.DamageMemo || null, //(maybe ins_memo)
|
// damage_memo: loss.DamageMemo || null, //(maybe ins_memo)
|
||||||
// total_loss_ind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null // New
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -288,8 +294,10 @@ const extractVehicleData = (rq, shopId) => {
|
|||||||
v_color: exterior.Color?.ColorName || null,
|
v_color: exterior.Color?.ColorName || null,
|
||||||
v_bstyle: desc.BodyStyle || null,
|
v_bstyle: desc.BodyStyle || null,
|
||||||
v_engine: desc.EngineDesc || null,
|
v_engine: desc.EngineDesc || null,
|
||||||
|
// TODO Need to confirm with exact data, but this is typically a list of options. Not used AFAIK.
|
||||||
v_options: desc.SubModelDesc || null,
|
v_options: desc.SubModelDesc || null,
|
||||||
v_type: desc.FuelType || null,
|
v_type: desc.FuelType || null,
|
||||||
|
// TODO there is a separate driveable flag on the job.
|
||||||
v_cond: rq.VehicleInfo?.Condition?.DrivableInd,
|
v_cond: rq.VehicleInfo?.Condition?.DrivableInd,
|
||||||
v_trimcode: desc.TrimCode || null,
|
v_trimcode: desc.TrimCode || null,
|
||||||
v_tone: exterior.Tone || null,
|
v_tone: exterior.Tone || null,
|
||||||
@@ -342,6 +350,7 @@ const extractJobLines = (rq) => {
|
|||||||
line.ManualLineInd === true ||
|
line.ManualLineInd === true ||
|
||||||
line.ManualLineInd === 1 ||
|
line.ManualLineInd === 1 ||
|
||||||
line.ManualLineInd === "1" ||
|
line.ManualLineInd === "1" ||
|
||||||
|
// TODO: manual line tracks manual in IO or not, this woudl presumably always be false
|
||||||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y");
|
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y");
|
||||||
} else {
|
} else {
|
||||||
lineOut.manual_line = null;
|
lineOut.manual_line = null;
|
||||||
@@ -355,7 +364,9 @@ const extractJobLines = (rq) => {
|
|||||||
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
|
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
|
||||||
lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
|
lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
|
||||||
lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
|
lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
|
||||||
|
//TODO: if aftermarket part, we have alt_part_no to capture.
|
||||||
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
|
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
|
||||||
|
//TODO: the Db and act price often are different. These should map back to their EMS equivalents.
|
||||||
lineOut.db_price = isNaN(price) ? 0 : price;
|
lineOut.db_price = isNaN(price) ? 0 : price;
|
||||||
lineOut.act_price = isNaN(price) ? 0 : price;
|
lineOut.act_price = isNaN(price) ? 0 : price;
|
||||||
|
|
||||||
@@ -372,7 +383,10 @@ const extractJobLines = (rq) => {
|
|||||||
partInfo.TaxableInd === "1" ||
|
partInfo.TaxableInd === "1" ||
|
||||||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
|
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
|
||||||
}
|
}
|
||||||
} else if (hasSublet) {
|
}
|
||||||
|
//TODO: Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically
|
||||||
|
// could.May require additional discussion.
|
||||||
|
else if (hasSublet) {
|
||||||
const amt = parseFloat(subletInfo.SubletAmount || 0);
|
const amt = parseFloat(subletInfo.SubletAmount || 0);
|
||||||
lineOut.part_type = "PAS"; // Sublet as parts-as-service
|
lineOut.part_type = "PAS"; // Sublet as parts-as-service
|
||||||
lineOut.part_qty = 1;
|
lineOut.part_qty = 1;
|
||||||
@@ -389,10 +403,14 @@ const extractJobLines = (rq) => {
|
|||||||
if (hasLabor) {
|
if (hasLabor) {
|
||||||
lineOut.mod_lbr_ty = laborInfo.LaborType || null;
|
lineOut.mod_lbr_ty = laborInfo.LaborType || null;
|
||||||
lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
|
lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
|
||||||
|
//TODO: can add lbr_op_desc according to mapping available in new partner.
|
||||||
lineOut.lbr_op = laborInfo.LaborOperation || null;
|
lineOut.lbr_op = laborInfo.LaborOperation || null;
|
||||||
lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
|
lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: what's the BMS logic for this? Body and refinish operations can often happen to the same part,
|
||||||
|
// but most systems output a second line for the refinish labor.
|
||||||
|
//TODO: 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC.
|
||||||
// Refinish labor (if present) recorded on the same line using secondary labor fields
|
// Refinish labor (if present) recorded on the same line using secondary labor fields
|
||||||
const rHrs = parseFloat(refinishInfo.LaborHours || 0);
|
const rHrs = parseFloat(refinishInfo.LaborHours || 0);
|
||||||
const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
|
const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
|
||||||
@@ -403,9 +421,9 @@ const extractJobLines = (rq) => {
|
|||||||
!isNaN(rAmt) ||
|
!isNaN(rAmt) ||
|
||||||
!!refinishInfo.LaborOperation);
|
!!refinishInfo.LaborOperation);
|
||||||
if (hasRefinish) {
|
if (hasRefinish) {
|
||||||
lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR";
|
lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR"; //TODO: _j fields indicate judgement, and are bool type.
|
||||||
lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;
|
lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs; //TODO: _j fields indicate judgement, and are bool type.
|
||||||
lineOut.lbr_op_j = refinishInfo.LaborOperation || null;
|
lineOut.lbr_op_j = refinishInfo.LaborOperation || null; //TODO: _j fields indicate judgement, and are bool type.
|
||||||
// Aggregate refinish labor amount into the total labor amount for the line
|
// Aggregate refinish labor amount into the total labor amount for the line
|
||||||
if (!isNaN(rAmt)) {
|
if (!isNaN(rAmt)) {
|
||||||
lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
|
lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
|
||||||
@@ -421,26 +439,26 @@ const extractJobLines = (rq) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper to extract a GRAND TOTAL amount from RepairTotalsInfo
|
// Helper to extract a GRAND TOTAL amount from RepairTotalsInfo
|
||||||
const extractGrandTotal = (rq) => {
|
// const extractGrandTotal = (rq) => {
|
||||||
const rti = rq.RepairTotalsInfo;
|
// const rti = rq.RepairTotalsInfo;
|
||||||
const groups = Array.isArray(rti) ? rti : rti ? [rti] : [];
|
// const groups = Array.isArray(rti) ? rti : rti ? [rti] : [];
|
||||||
for (const grp of groups) {
|
// for (const grp of groups) {
|
||||||
const sums = Array.isArray(grp.SummaryTotalsInfo)
|
// const sums = Array.isArray(grp.SummaryTotalsInfo)
|
||||||
? grp.SummaryTotalsInfo
|
// ? grp.SummaryTotalsInfo
|
||||||
: grp.SummaryTotalsInfo
|
// : grp.SummaryTotalsInfo
|
||||||
? [grp.SummaryTotalsInfo]
|
// ? [grp.SummaryTotalsInfo]
|
||||||
: [];
|
// : [];
|
||||||
for (const s of sums) {
|
// for (const s of sums) {
|
||||||
const type = (s.TotalType || "").toString().toUpperCase();
|
// const type = (s.TotalType || "").toString().toUpperCase();
|
||||||
const desc = (s.TotalTypeDesc || "").toString().toUpperCase();
|
// const desc = (s.TotalTypeDesc || "").toString().toUpperCase();
|
||||||
if (type.includes("GRAND") || type === "TOTAL" || desc.includes("GRAND")) {
|
// if (type.includes("GRAND") || type === "TOTAL" || desc.includes("GRAND")) {
|
||||||
const amt = parseFloat(s.TotalAmt ?? "NaN");
|
// const amt = parseFloat(s.TotalAmt ?? "NaN");
|
||||||
if (!isNaN(amt)) return amt;
|
// if (!isNaN(amt)) return amt;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
return null;
|
// return null;
|
||||||
};
|
// };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inserts an owner and returns the owner ID.
|
* Inserts an owner and returns the owner ID.
|
||||||
@@ -459,24 +477,26 @@ const insertOwner = async (ownerInput, logger) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fallback: compute a naive total from joblines (parts + sublet + labor amounts)
|
// Fallback: compute a naive total from joblines (parts + sublet + labor amounts)
|
||||||
const computeLinesTotal = (joblines = []) => {
|
// const computeLinesTotal = (joblines = []) => {
|
||||||
let parts = 0;
|
// let parts = 0;
|
||||||
let labor = 0;
|
// let labor = 0;
|
||||||
for (const jl of joblines) {
|
// for (const jl of joblines) {
|
||||||
if (jl?.part_type) {
|
// if (jl?.part_type) {
|
||||||
const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1;
|
// const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1;
|
||||||
const price = Number.isFinite(jl.act_price) ? jl.act_price : 0;
|
// const price = Number.isFinite(jl.act_price) ? jl.act_price : 0;
|
||||||
parts += price * (qty || 1);
|
// parts += price * (qty || 1);
|
||||||
} else if (!jl.part_type && Number.isFinite(jl.act_price)) {
|
// } else if (!jl.part_type && Number.isFinite(jl.act_price)) {
|
||||||
parts += jl.act_price;
|
// parts += jl.act_price;
|
||||||
}
|
// }
|
||||||
if (Number.isFinite(jl.lbr_amt)) {
|
// if (Number.isFinite(jl.lbr_amt)) {
|
||||||
labor += jl.lbr_amt;
|
// labor += jl.lbr_amt;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
const total = parts + labor;
|
// const total = parts + labor;
|
||||||
return Number.isFinite(total) && total > 0 ? total : 0;
|
//
|
||||||
};
|
// //TODO: clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system.
|
||||||
|
// return Number.isFinite(total) && total > 0 ? total : 0;
|
||||||
|
// };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the VehicleDamageEstimateAddRq XML request from parts management.
|
* Handles the VehicleDamageEstimateAddRq XML request from parts management.
|
||||||
@@ -513,9 +533,9 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
|||||||
scheduled_in,
|
scheduled_in,
|
||||||
scheduled_completion,
|
scheduled_completion,
|
||||||
clm_no,
|
clm_no,
|
||||||
status,
|
|
||||||
policy_no,
|
policy_no,
|
||||||
ded_amt
|
ded_amt
|
||||||
|
// status,
|
||||||
} = extractJobData(rq);
|
} = extractJobData(rq);
|
||||||
|
|
||||||
if (!shopId) {
|
if (!shopId) {
|
||||||
@@ -523,22 +543,22 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get default status
|
// Get default status
|
||||||
const defaultStatus = await getDefaultOrderStatus(shopId, logger);
|
const defaultStatus = await getDefaultJobStatus(shopId, logger);
|
||||||
|
|
||||||
// Extract additional data
|
// Extract additional data
|
||||||
const parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
|
const parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
|
||||||
const ownerData = extractOwnerData(rq, shopId);
|
const ownerData = extractOwnerData(rq, shopId);
|
||||||
const estimatorData = extractEstimatorData(rq);
|
const estimatorData = extractEstimatorData(rq);
|
||||||
const adjusterData = extractAdjusterData(rq);
|
// const adjusterData = extractAdjusterData(rq);
|
||||||
const repairFacilityData = extractRepairFacilityData(rq);
|
// const repairFacilityData = extractRepairFacilityData(rq);
|
||||||
const vehicleData = extractVehicleData(rq, shopId);
|
const vehicleData = extractVehicleData(rq, shopId);
|
||||||
const lossInfo = extractLossInfo(rq);
|
const lossInfo = extractLossInfo(rq);
|
||||||
const joblinesData = extractJobLines(rq);
|
const joblinesData = extractJobLines(rq);
|
||||||
const insuranceData = extractInsuranceData(rq);
|
const insuranceData = extractInsuranceData(rq);
|
||||||
|
|
||||||
// Derive clm_total: prefer RepairTotalsInfo SummaryTotals GRAND TOTAL; else sum from lines
|
// Derive clm_total: prefer RepairTotalsInfo SummaryTotals GRAND TOTAL; else sum from lines
|
||||||
const grandTotal = extractGrandTotal(rq);
|
// const grandTotal = extractGrandTotal(rq);
|
||||||
const computedTotal = grandTotal ?? computeLinesTotal(joblinesData);
|
// const computedTotal = grandTotal ?? computeLinesTotal(joblinesData);
|
||||||
|
|
||||||
// Find or create relationships
|
// Find or create relationships
|
||||||
const ownerid = await insertOwner(ownerData, logger);
|
const ownerid = await insertOwner(ownerData, logger);
|
||||||
@@ -557,8 +577,8 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
|||||||
class: classType,
|
class: classType,
|
||||||
parts_tax_rates,
|
parts_tax_rates,
|
||||||
clm_no,
|
clm_no,
|
||||||
status: status || defaultStatus,
|
status: defaultStatus,
|
||||||
clm_total: computedTotal || null,
|
clm_total: 0, // computedTotal || null,
|
||||||
policy_no,
|
policy_no,
|
||||||
ded_amt,
|
ded_amt,
|
||||||
comment,
|
comment,
|
||||||
@@ -573,8 +593,8 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
|||||||
...lossInfo,
|
...lossInfo,
|
||||||
...ownerData,
|
...ownerData,
|
||||||
...estimatorData,
|
...estimatorData,
|
||||||
...adjusterData,
|
// ...adjusterData,
|
||||||
...repairFacilityData,
|
// ...repairFacilityData,
|
||||||
// Inline vehicle data
|
// Inline vehicle data
|
||||||
v_vin: vehicleData.v_vin,
|
v_vin: vehicleData.v_vin,
|
||||||
v_model_yr: vehicleData.v_model_yr,
|
v_model_yr: vehicleData.v_model_yr,
|
||||||
|
|||||||
@@ -38,13 +38,15 @@ const findJob = async (shopId, jobId, logger) => {
|
|||||||
const extractUpdatedJobData = (rq) => {
|
const extractUpdatedJobData = (rq) => {
|
||||||
const doc = rq.DocumentInfo || {};
|
const doc = rq.DocumentInfo || {};
|
||||||
const claim = rq.ClaimInfo || {};
|
const claim = rq.ClaimInfo || {};
|
||||||
|
//TODO: In the full BMS world, much more can change, this will need to be expanded
|
||||||
|
// before it can be considered an generic BMS importer, currently it is bespoke to webest
|
||||||
const policyNo = claim.PolicyInfo?.PolicyInfo?.PolicyNum || claim.PolicyInfo?.PolicyNum || null;
|
const policyNo = claim.PolicyInfo?.PolicyInfo?.PolicyNum || claim.PolicyInfo?.PolicyNum || null;
|
||||||
|
|
||||||
const out = {
|
const out = {
|
||||||
comment: doc.Comment || null,
|
comment: doc.Comment || null,
|
||||||
clm_no: claim.ClaimNum || null,
|
clm_no: claim.ClaimNum || null,
|
||||||
status: claim.ClaimStatus || null,
|
// TODO: Commented out so they do not blow over with 'Auth Cust'
|
||||||
|
// status: claim.ClaimStatus || null,
|
||||||
policy_no: policyNo
|
policy_no: policyNo
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -246,10 +248,13 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
|
|||||||
|
|
||||||
await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
|
await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
|
||||||
|
|
||||||
|
//TODO: for changed lines, are they deleted and then reinserted?
|
||||||
|
//TODO: Updated lines should get an upsert to update things like desc, price, etc.
|
||||||
if (deletedLineIds?.length || updatedSeqs?.length) {
|
if (deletedLineIds?.length || updatedSeqs?.length) {
|
||||||
const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])]));
|
const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])]));
|
||||||
if (allToDelete.length) {
|
if (allToDelete.length) {
|
||||||
await client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete });
|
await client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete });
|
||||||
|
//TODO: appears to soft delete updated lines as well.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
const GET_BODYSHOP_STATUS = `
|
const GET_BODYSHOP_STATUS = `
|
||||||
query GetBodyshopStatus($id: uuid!) {
|
query GetBodyshopStatus($id: uuid!) {
|
||||||
bodyshops_by_pk(id: $id) {
|
bodyshops_by_pk(id: $id) {
|
||||||
md_order_statuses
|
md_ro_statuses
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -216,6 +216,36 @@ const GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ = `
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Clear task links to parts orders for all jobs in a shop to avoid FK violations when deleting parts orders
|
||||||
|
const CLEAR_TASKS_PARTSORDER_LINKS_BY_JOBIDS = `
|
||||||
|
mutation ClearTasksPartsOrderLinks($jobIds: [uuid!]!) {
|
||||||
|
update_tasks(
|
||||||
|
where: { parts_order: { jobid: { _in: $jobIds } } },
|
||||||
|
_set: { partsorderid: null }
|
||||||
|
) {
|
||||||
|
affected_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Delete parts order lines where the parent order belongs to any of the provided job IDs
|
||||||
|
const DELETE_PARTS_ORDER_LINES_BY_JOB_IDS = `
|
||||||
|
mutation DeletePartsOrderLinesByJobIds($jobIds: [uuid!]!) {
|
||||||
|
delete_parts_order_lines(where: { parts_order: { jobid: { _in: $jobIds } } }) {
|
||||||
|
affected_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Delete parts orders for the given job IDs
|
||||||
|
const DELETE_PARTS_ORDERS_BY_JOB_IDS = `
|
||||||
|
mutation DeletePartsOrdersByJobIds($jobIds: [uuid!]!) {
|
||||||
|
delete_parts_orders(where: { jobid: { _in: $jobIds } }) {
|
||||||
|
affected_rows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
GET_BODYSHOP_STATUS,
|
GET_BODYSHOP_STATUS,
|
||||||
GET_VEHICLE_BY_SHOP_VIN,
|
GET_VEHICLE_BY_SHOP_VIN,
|
||||||
@@ -241,5 +271,9 @@ module.exports = {
|
|||||||
DELETE_JOBS_BY_IDS,
|
DELETE_JOBS_BY_IDS,
|
||||||
DELETE_AUDIT_TRAIL_BY_SHOP,
|
DELETE_AUDIT_TRAIL_BY_SHOP,
|
||||||
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ,
|
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ,
|
||||||
GET_JOB_BY_ID
|
GET_JOB_BY_ID,
|
||||||
|
// newly added exports
|
||||||
|
CLEAR_TASKS_PARTSORDER_LINKS_BY_JOBIDS,
|
||||||
|
DELETE_PARTS_ORDER_LINES_BY_JOB_IDS,
|
||||||
|
DELETE_PARTS_ORDERS_BY_JOB_IDS
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,8 +58,20 @@ const generateSignedUploadUrls = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const command = new PutObjectCommand(commandParams);
|
const command = new PutObjectCommand(commandParams);
|
||||||
const presignedUrl = await getSignedUrl(client, command, { expiresIn: 360 });
|
|
||||||
signedUrls.push({ filename, presignedUrl, key });
|
// For PDFs, we need to add conditions to the presigned URL to enforce content type
|
||||||
|
const presignedUrlOptions = { expiresIn: 360 };
|
||||||
|
if (isPdf) {
|
||||||
|
presignedUrlOptions.signableHeaders = new Set(['content-type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const presignedUrl = await getSignedUrl(client, command, presignedUrlOptions);
|
||||||
|
signedUrls.push({
|
||||||
|
filename,
|
||||||
|
presignedUrl,
|
||||||
|
key,
|
||||||
|
...(isPdf && { contentType: "application/pdf" })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("imgproxy-upload-success", "DEBUG", req.user?.email, jobid, { signedUrls });
|
logger.log("imgproxy-upload-success", "DEBUG", req.user?.email, jobid, { signedUrls });
|
||||||
|
|||||||
Reference in New Issue
Block a user