Compare commits

...

21 Commits

Author SHA1 Message Date
Allan Carr
6bae0b8406 IO-3330 CARFAX Bodyshop Query Adjustment
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-08-27 23:41:16 -07:00
Dave Richer
35cbb921d2 Merged in feature/IO-3255-simplified-part-management (pull request #2516)
feature/IO-3255-simplified-parts-management - Change Parts Status logic for Parts Management
2025-08-27 18:19:40 +00:00
Dave
6b926401d0 feature/IO-3255-simplified-parts-management - Change Parts Status logic for Parts Management 2025-08-27 14:17:23 -04:00
Dave Richer
914946c264 Merged in feature/IO-3255-simplified-part-management (pull request #2514)
feature/IO-3255-simplified-parts-management - Change Parts Status logic for Parts Management
2025-08-27 16:37:27 +00:00
Dave
2939b5795b feature/IO-3255-simplified-parts-management - Change Parts Status logic for Parts Management 2025-08-27 12:35:53 -04:00
Dave Richer
531f968ce6 Merged in feature/IO-3255-simplified-part-management (pull request #2512)
feature/IO-3255-simplified-parts-management - Change From Claim to JobID (chgrq)
2025-08-27 15:37:47 +00:00
Dave
24cc3fa6a4 feature/IO-3255-simplified-parts-management - Change From Claim to JobID (chgrq) 2025-08-27 11:36:39 -04:00
Dave Richer
a6f17e7db5 Merged in feature/IO-3255-simplified-part-management (pull request #2510)
feature/IO-3255-simplified-parts-management - Typo in chg request route
2025-08-26 14:51:18 +00:00
Dave
29c904feea feature/IO-3255-simplified-parts-management - Typo in chg request route 2025-08-26 10:50:44 -04:00
Dave Richer
0743048e75 Merged in feature/IO-3255-simplified-part-management (pull request #2509)
Feature/IO-3255 simplified part management
2025-08-25 16:58:43 +00:00
Dave
e2f1758378 feature/IO-3255-simplified-parts-management - Simplified Parts print center stacks on top of each other, not beside each other 2025-08-25 11:33:12 -04:00
Dave
b6dc7a4d92 feature/IO-3255-simplified-parts-management - Notes are appended if they are not duplicates 2025-08-25 11:30:18 -04:00
Patrick Fic
52f62126e1 Merged in feature/IO-3340-imgproxy-pdf-contenttype (pull request #2507)
IO-3340 Change application type on PDFs uploaded via imgproxy to allow inline display.
2025-08-22 19:02:44 +00:00
Patrick Fic
cfdfb8110b IO-3340 Change application type on PDFs uploaded via imgproxy to allow inline display. 2025-08-22 12:01:16 -07:00
Allan Carr
eb04a8b6c5 Merged in feature/IO-3349-Chart-Enqueue-and-Label-Required (pull request #2505)
IO-3349 Chart Enqueue and Label Required

Approved-by: Dave Richer
2025-08-22 18:55:58 +00:00
Allan Carr
141b05f558 IO-3349 Chart Enqueue and Label Required
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-08-22 11:29:33 -07:00
Dave Richer
e9ea36fdad Merged in feature/IO-3255-simplified-part-management (pull request #2504)
feature/IO-3255-simplified-parts-management - Bug changes / Socket close cleanup
2025-08-22 18:26:38 +00:00
Dave
e990781ff7 feature/IO-3255-simplified-parts-management - Bug changes / Socket close cleanup 2025-08-22 14:24:03 -04:00
Patrick Fic
e24237e010 Merged in feature/IO-3325-amplitude (pull request #2502)
Feature/IO-3325 amplitude
2025-08-22 18:00:37 +00:00
Patrick Fic
fabe1508ac Merged in feature/IO-3325-amplitude (pull request #2500)
IO-3325 Add amplitude tracking.
2025-08-22 17:40:33 +00:00
Dave Richer
8c9ef375be Merged in hotfix/background-colors-dark-mode (pull request #2485)
Hotfix/background colors dark mode
2025-08-18 18:39:42 +00:00
15 changed files with 199 additions and 161 deletions

View File

@@ -1,20 +1,20 @@
import { ApolloProvider } from "@apollo/client";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
import { useTranslation } from "react-i18next";
import { connect, useSelector } from "react-redux";
import { createStructuredSelector } from "reselect";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { setDarkMode } from "../redux/application/application.actions";
import { selectDarkMode } from "../redux/application/application.selectors";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
import getTheme from "./themeProvider";
import { CookiesProvider } from "react-cookie";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import { selectDarkMode } from "../redux/application/application.selectors";
import { setDarkMode } from "../redux/application/application.actions";
// Base Split configuration
const config = {

View File

@@ -8,19 +8,20 @@ import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectIsPartsEntry, selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly
jobRO: selectJobReadOnly,
isPartsEntry: selectIsPartsEntry
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail, isPartsEntry }) {
const { t } = useTranslation();
const [availableStatuses, setAvailableStatuses] = useState([]);
@@ -45,25 +46,43 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
});
};
// Updates available statuses based on job and bodyshop context
useEffect(() => {
//Figure out what scenario were in, populate accodingly
if (job && bodyshop) {
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) {
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses);
} else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) {
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses);
} else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) {
setAvailableStatuses(
bodyshop.md_ro_statuses.post_production_statuses.filter(
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported
)
);
} else {
console.log("Status didn't match any restrictions. Allowing all status changes.");
setAvailableStatuses(bodyshop.md_ro_statuses.statuses);
}
if (!job || !bodyshop) return;
const { md_ro_statuses } = bodyshop;
const {
parts_statuses,
pre_production_statuses,
production_statuses,
post_production_statuses,
statuses,
default_invoiced,
default_exported
} = md_ro_statuses;
if (isPartsEntry) {
// Set parts-specific statuses for parts entry scenario
setAvailableStatuses(parts_statuses);
return;
}
}, [job, setAvailableStatuses, bodyshop]);
// Handle non-parts entry scenarios based on job status
if (pre_production_statuses.includes(job.status)) {
setAvailableStatuses(pre_production_statuses);
} else if (production_statuses.includes(job.status)) {
setAvailableStatuses(production_statuses);
} else if (post_production_statuses.includes(job.status)) {
// Filter out invoiced and exported statuses for post-production
setAvailableStatuses(
post_production_statuses.filter((status) => status !== default_invoiced && status !== default_exported)
);
} else {
// Default to all statuses if no specific restrictions apply
console.log("Status didn't match any restrictions. Allowing all status changes.");
setAvailableStatuses(statuses);
}
}, [job, bodyshop, isPartsEntry, setAvailableStatuses]);
const statusMenu = {
items: [

View File

@@ -37,7 +37,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
.filter(
(temp) =>
(!temp.regions ||
(temp.regions && temp.regions[bodyshop.region_config]) ||
temp.regions?.[bodyshop.region_config] ||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)) &&
(!temp.dms || temp.dms === false)
)
@@ -46,7 +46,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
.filter(
(temp) =>
!temp.regions ||
(temp.regions && temp.regions[bodyshop.region_config]) ||
temp.regions?.[bodyshop.region_config] ||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
);
@@ -82,7 +82,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
variables: { id: jobId }
},
{
to: job && job.ownr_ea,
to: job?.ownr_ea,
subject: cards.find((c) => c.key === key)?.subject
},
"e",
@@ -129,7 +129,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
const columns = `repeat(${actions.length}, 1fr)`;
return (
<Col key={item.key} xs={24} sm={12}>
<Col key={item.key} xs={24} sm={24} md={24} lg={24} xl={24}>
<Card hoverable style={{ minHeight: 100 }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ flex: "1 1 70%", minWidth: 0 }}>

View File

@@ -64,7 +64,7 @@ export default function ShopInfoContainer() {
onFinish={handleFinish}
initialValues={
data
? data.bodyshops[0].accountingconfig.ClosingPeriod
? data?.bodyshops?.[0]?.accountingconfig?.ClosingPeriod
? {
...data.bodyshops[0],
accountingconfig: {

View File

@@ -96,13 +96,15 @@ export function SimplifiedPartsJobsListComponent({
key: "status",
ellipsis: true,
sorter: search?.search ? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses) : true,
sorter: search?.search
? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.parts_active_statuses)
: true,
sortOrder: sortcolumn === "status" && sortorder,
render: (text, record) => {
return record.status || t("general.labels.na");
},
filteredValue: filter?.status || null,
filters: bodyshop.md_ro_statuses.statuses.map((s) => {
filters: bodyshop.md_ro_statuses.parts_statuses.map((s) => {
return { text: s, value: [s] };
}),
onFilter: (value, record) => value.includes(record.status)

View File

@@ -167,7 +167,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
socketRef.current = socketInstance;
const handleBodyshopMessage = (message) => {
if (!message || !message.type) return;
if (!message?.type) return;
switch (message.type) {
case "alert-update":
store.dispatch(addAlerts(message.payload));
@@ -512,21 +512,20 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
};
const unsubscribe = auth.onIdTokenChanged(async (user) => {
if (user) {
const token = await user.getIdToken();
if (socketRef.current) {
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id });
} else {
initializeSocket(token).catch((err) =>
console.error(`Something went wrong Initializing Sockets: ${err?.message || ""}`)
);
}
if (!user) {
socketRef.current?.disconnect();
socketRef.current = null;
setIsConnected(false);
return;
}
const token = await user.getIdToken();
if (socketRef.current) {
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id });
} else {
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
setIsConnected(false);
}
initializeSocket(token).catch((err) =>
console.error("Something went wrong Initializing Sockets:", err?.message || "")
);
}
});

View File

@@ -1854,7 +1854,7 @@ exports.GET_CHATTER_SHOPS = `query GET_CHATTER_SHOPS {
}`;
exports.GET_CARFAX_SHOPS = `query GET_CARFAX_SHOPS {
bodyshops{
bodyshops(where: {external_shop_id: {_is_null: true}}){
id
shopname
imexshopid

View File

@@ -35,6 +35,11 @@
"headerMargin": "135"
},
"md_ro_statuses": {
"parts_statuses": [
"Open",
"In Progress",
"Completed"
],
"statuses": [
"Open",
"Scheduled",
@@ -54,6 +59,10 @@
"Void"
],
"default_void": "Void",
"parts_active_statuses": [
"Open",
"In Progress"
],
"active_statuses": [
"Open",
"Scheduled",

View File

@@ -181,7 +181,7 @@ const partsManagementProvisioning = async (req, res) => {
headerMargin: DefaultNewShop.logo_img_path.headerMargin
},
features: {
allAccess: true, // TODO: should be false?
allAccess: false,
partsManagementOnly: true
},
md_ro_statuses: DefaultNewShop.md_ro_statuses,
@@ -191,14 +191,14 @@ const partsManagementProvisioning = async (req, res) => {
md_messaging_presets: DefaultNewShop.md_messaging_presets,
md_rbac: DefaultNewShop.md_rbac,
md_classes: DefaultNewShop.md_classes,
md_ins_cos: DefaultNewShop.md_ins_cos, // TODO need?
md_categories: DefaultNewShop.md_categories, // TODO need?
md_labor_rates: DefaultNewShop.md_labor_rates, // TODO need?
md_payment_types: DefaultNewShop.md_payment_types, // TODO need?
md_hour_split: DefaultNewShop.md_hour_split, // TODO need?
md_ccc_rates: DefaultNewShop.md_ccc_rates, // TODO need?
appt_alt_transport: DefaultNewShop.appt_alt_transport, // TODO need?
md_jobline_presets: DefaultNewShop.md_jobline_presets, // TODO need?
md_ins_cos: DefaultNewShop.md_ins_cos,
md_categories: DefaultNewShop.md_categories,
md_labor_rates: DefaultNewShop.md_labor_rates,
md_payment_types: DefaultNewShop.md_payment_types,
md_hour_split: DefaultNewShop.md_hour_split,
md_ccc_rates: DefaultNewShop.md_ccc_rates,
appt_alt_transport: DefaultNewShop.appt_alt_transport,
md_jobline_presets: DefaultNewShop.md_jobline_presets,
vendors: {
data: p.vendors.map((v) => ({
name: v.name,

View File

@@ -14,7 +14,7 @@ const {
} = require("../partsManagement.queries");
// Defaults
const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN";
const FALLBACK_DEFAULT_ORDER_STATUS = "Open";
/**
* Fetches the default order status for a bodyshop.
@@ -81,7 +81,8 @@ const extractJobData = (rq) => {
scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null,
scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null,
clm_no: ci.ClaimNum || null,
status: ci.ClaimStatus || 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,
ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0)
};

View File

@@ -6,22 +6,23 @@ const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
const {
GET_JOB_BY_CLAIM,
GET_JOB_BY_ID,
UPDATE_JOB_BY_ID,
SOFT_DELETE_JOBLINES_BY_IDS,
INSERT_JOBLINES
INSERT_JOBLINES,
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ
} = require("../partsManagement.queries");
/**
* Finds a job by shop ID and claim number.
* @param shopId
* @param claimNum
* @param jobId
* @param logger
* @returns {Promise<*|null>}
*/
const findJob = async (shopId, claimNum, logger) => {
const findJob = async (shopId, jobId, logger) => {
try {
const { jobs } = await client.request(GET_JOB_BY_CLAIM, { shopid: shopId, clm_no: claimNum });
const { jobs } = await client.request(GET_JOB_BY_ID, { shopid: shopId, jobid: jobId });
return jobs?.[0] || null;
} catch (err) {
logger.log("parts-job-lookup-failed", "error", null, null, { error: err });
@@ -60,8 +61,9 @@ const extractUpdatedJobData = (rq) => {
* - Keep part and labor on the same jobline
* - Aggregate RefinishLabor into secondary labor fields and add its amount to lbr_amt
* - SUBLET-only lines become PAS part_type with act_price = SubletAmount
* Accepts currentJobLineNotes map for notes merging.
*/
const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {}) => {
const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}];
const coerceManual = (val) =>
@@ -83,12 +85,33 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
status: line.LineStatusCode || null,
line_desc: line.LineDesc || null,
notes: line.LineMemo || null,
// notes will be set below
manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
};
const lineOut = { ...base };
// --- Notes merge logic ---
const unqSeq = lineOut.unq_seq;
const currentNotes = currentJobLineNotes?.[unqSeq] || null;
const newNotes = line.LineMemo || null;
if (newNotes && currentNotes) {
if (currentNotes === newNotes) {
lineOut.notes = currentNotes;
} else if (currentNotes.includes(newNotes)) {
lineOut.notes = currentNotes;
} else {
lineOut.notes = `${currentNotes} | ${newNotes}`;
}
} else if (newNotes) {
lineOut.notes = newNotes;
} else if (currentNotes) {
lineOut.notes = currentNotes;
} else {
lineOut.notes = null;
}
// --- End notes merge logic ---
const hasPart = Object.keys(partInfo).length > 0;
const hasSublet = Object.keys(subletInfo).length > 0;
@@ -191,24 +214,38 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
if (!rq) return res.status(400).send("Missing <VehicleDamageEstimateChgRq>");
const shopId = rq.ShopID;
const claimNum = rq.ClaimInfo?.ClaimNum;
const jobId = rq.JobID;
if (!shopId || !claimNum) return res.status(400).send("Missing ShopID or ClaimNum");
if (!shopId || !jobId) return res.status(400).send("Missing ShopID or JobID");
const job = await findJob(shopId, jobId, logger);
const job = await findJob(shopId, claimNum, logger);
if (!job) return res.status(404).send("Job not found");
// --- Get updated lines and their unq_seq ---
const linesIn = Array.isArray(rq.AddsChgs?.DamageLineInfo)
? rq.AddsChgs.DamageLineInfo
: [rq.AddsChgs?.DamageLineInfo || {}];
const updatedSeqs = Array.from(
new Set((linesIn || []).map((l) => parseInt(l?.UniqueSequenceNum || 0, 10)).filter((v) => Number.isInteger(v)))
);
let currentJobLineNotes = {};
if (updatedSeqs.length > 0) {
const resp = await client.request(GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ, { jobid: job.id, unqSeqs: updatedSeqs });
if (resp?.joblines) {
for (const jl of resp.joblines) {
currentJobLineNotes[jl.unq_seq] = jl.notes;
}
}
}
// --- End fetch current notes ---
const updatedJobData = extractUpdatedJobData(rq);
const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id);
const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id, currentJobLineNotes);
const deletedLineIds = extractDeletions(rq.Deletions);
await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
// Build a set of unq_seq that will be updated (replaced). We delete them first to avoid duplicates.
const updatedSeqs = Array.from(
new Set((updatedLines || []).map((l) => l?.unq_seq).filter((v) => Number.isInteger(v)))
);
if (deletedLineIds?.length || updatedSeqs?.length) {
const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])]));
if (allToDelete.length) {

View File

@@ -44,60 +44,22 @@ const GET_JOB_BY_CLAIM = `
}
`;
const UPDATE_JOB_BY_ID = `
mutation UpdateJobById($id: uuid!, $job: jobs_set_input!) {
update_jobs_by_pk(pk_columns: { id: $id }, _set: $job) {
const GET_JOB_BY_ID = `
query GetJobByID($shopid: uuid!, $jobid: uuid!) {
jobs(
where: { shopid: { _eq: $shopid }, id: { _eq: $jobid } }
order_by: { created_at: desc }
limit: 1
) {
id
}
}
`;
const UPSERT_JOBLINES = `
mutation UpsertJoblines($joblines: [joblines_insert_input!]!) {
insert_joblines(
objects: $joblines
on_conflict: {
constraint: joblines_pkey
update_columns: [
jobid
status
line_desc
part_type
part_qty
oem_partno
db_price
act_price
mod_lbr_ty
mod_lb_hrs
lbr_op
lbr_amt
notes
manual_line
]
}
) {
affected_rows
}
}
`;
const DELETE_JOBLINES_BY_JOBID = `
mutation DeleteJoblinesByJobId($jobid: uuid!) {
delete_joblines(where: { jobid: { _eq: $jobid } }) {
affected_rows
}
}
`;
const DELETE_JOBLINES_BY_IDS = `
mutation DeleteJoblinesByIds($jobid: uuid!, $unqSeqs: [Int!]!) {
delete_joblines(
where: {
jobid: { _eq: $jobid },
unq_seq: { _in: $unqSeqs }
}
) {
affected_rows
const UPDATE_JOB_BY_ID = `
mutation UpdateJobById($id: uuid!, $job: jobs_set_input!) {
update_jobs_by_pk(pk_columns: { id: $id }, _set: $job) {
id
}
}
`;
@@ -245,6 +207,15 @@ const DELETE_AUDIT_TRAIL_BY_SHOP = `
}
`;
const GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ = `
query GetJoblinesNotesByJobIdUnqSeq($jobid: uuid!, $unqSeqs: [Int!]!) {
joblines(where: { jobid: { _eq: $jobid }, unq_seq: { _in: $unqSeqs }, removed: { _neq: true } }) {
unq_seq
notes
}
}
`;
module.exports = {
GET_BODYSHOP_STATUS,
GET_VEHICLE_BY_SHOP_VIN,
@@ -252,9 +223,6 @@ module.exports = {
INSERT_JOB_WITH_LINES,
GET_JOB_BY_CLAIM,
UPDATE_JOB_BY_ID,
UPSERT_JOBLINES,
DELETE_JOBLINES_BY_JOBID,
DELETE_JOBLINES_BY_IDS,
SOFT_DELETE_JOBLINES_BY_IDS,
INSERT_JOBLINES,
CHECK_EXTERNAL_SHOP_ID,
@@ -271,5 +239,7 @@ module.exports = {
GET_JOBS_BY_SHOP,
DELETE_JOBLINES_BY_JOB_IDS,
DELETE_JOBS_BY_IDS,
DELETE_AUDIT_TRAIL_BY_SHOP
DELETE_AUDIT_TRAIL_BY_SHOP,
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ,
GET_JOB_BY_ID
};

View File

@@ -44,11 +44,20 @@ const generateSignedUploadUrls = async (req, res) => {
for (const filename of filenames) {
const key = filename;
const client = new S3Client({ region: InstanceRegion() });
const command = new PutObjectCommand({
// Check if filename indicates PDF and set content type accordingly
const isPdf = filename.toLowerCase().endsWith('.pdf');
const commandParams = {
Bucket: imgproxyDestinationBucket,
Key: key,
StorageClass: "INTELLIGENT_TIERING"
});
};
if (isPdf) {
commandParams.ContentType = "application/pdf";
}
const command = new PutObjectCommand(commandParams);
const presignedUrl = await getSignedUrl(client, command, { expiresIn: 360 });
signedUrls.push({ filename, presignedUrl, key });
}

View File

@@ -73,37 +73,23 @@ const processCanvasRequest = async (req, res) => {
// Default width and height
const width = isNumber(w) && w > 0 ? w : 500;
const height = isNumber(h) && h > 0 ? h : 275;
const configuration = getChartConfiguration(keys, values, override);
let canvas = null;
let ctx = null;
let chart = null;
let chartImage = null;
try {
// Create the canvas
canvas = new Canvas(width, height);
ctx = canvas.getContext("2d");
const canvas = new Canvas(width, height);
const ctx = canvas.getContext("2d");
// Render the chart
chart = new Chart(ctx, configuration);
// Generate and send the image
chartImage = (await canvas.toBuffer("image/png")).toString("base64");
const chartImage = (await canvas.toBuffer("image/png")).toString("base64");
res.status(200).send(`data:image/png;base64,${chartImage}`);
} catch (error) {
// Log the error and send the response
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
res.status(500).send("Failed to generate canvas.");
res.status(500).send("Error generating canvas");
} finally {
// Cleanup resources
if (chart) {
chart.destroy();
}
ctx = null;
canvas = null;
chartImage = null;
chart?.destroy();
}
};
@@ -118,15 +104,18 @@ const enqueueRequest = (req, res) => {
};
const processNextInQueue = async () => {
while (requestQueue.length > 0) {
const { req, res } = requestQueue.shift();
try {
await processCanvasRequest(req, res);
} catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
try {
while (requestQueue.length > 0) {
const { req, res } = requestQueue.shift();
try {
await processCanvasRequest(req, res);
} catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
}
}
} finally {
isProcessing = false;
}
isProcessing = false;
};
exports.canvastest = function (req, res) {
@@ -134,7 +123,10 @@ exports.canvastest = function (req, res) {
};
exports.canvas = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
if (!enqueueRequest(req, res)) return;
if (!isProcessing) {
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
}
};

View File

@@ -38,7 +38,7 @@ if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_
* Route to handle Vehicle Damage Estimate Change Request
*/
router.post(
"/parts-management/VehicleDamageEstimateChqRq",
"/parts-management/VehicleDamageEstimateChgRq",
express.raw({ type: "application/xml", limit: XML_BODY_LIMIT }), // Parse XML body
partsManagementIntegrationMiddleware,
partsManagementVehicleDamageEstimateChqRq