+
{lifecycleData.durations.summations.map((key) => (
-
+
{key.status} (
diff --git a/client/src/components/jobs-documents-imgproxy-gallery/jobs-document-imgproxy-gallery.download.component.jsx b/client/src/components/jobs-documents-imgproxy-gallery/jobs-document-imgproxy-gallery.download.component.jsx
index b9f8266ff..6590498aa 100644
--- a/client/src/components/jobs-documents-imgproxy-gallery/jobs-document-imgproxy-gallery.download.component.jsx
+++ b/client/src/components/jobs-documents-imgproxy-gallery/jobs-document-imgproxy-gallery.download.component.jsx
@@ -35,16 +35,14 @@ export function JobsDocumentsImgproxyDownloadButton({ galleryImages, identifier,
...galleryImages.other.filter((image) => image.isSelected)
];
- function downloadProgress(progressEvent) {
- setDownload((currentDownloadState) => {
- return {
- downloaded: progressEvent.loaded || 0,
- speed: (progressEvent.loaded || 0) - ((currentDownloadState && currentDownloadState.downloaded) || 0)
- };
- });
- }
+ const downloadProgress = ({ loaded }) => {
+ setDownload((currentDownloadState) => ({
+ downloaded: loaded ?? 0,
+ speed: (loaded ?? 0) - (currentDownloadState?.downloaded ?? 0)
+ }));
+ };
- function standardMediaDownload(bufferData) {
+ const standardMediaDownload = (bufferData) => {
try {
const a = document.createElement("a");
const url = window.URL.createObjectURL(new Blob([bufferData]));
@@ -55,29 +53,26 @@ export function JobsDocumentsImgproxyDownloadButton({ galleryImages, identifier,
setLoading(false);
setDownload(null);
}
- }
+ };
const handleDownload = async () => {
logImEXEvent("jobs_documents_download");
setLoading(true);
try {
- const response = await axios({
+ const { data } = await axios({
url: "/media/imgproxy/download",
method: "POST",
responseType: "blob",
data: { jobId, documentids: imagesToDownload.map((_) => _.id) },
onDownloadProgress: downloadProgress
});
-
- setLoading(false);
- setDownload(null);
-
// Use the response data (Blob) to trigger download
- standardMediaDownload(response.data);
+ standardMediaDownload(data);
} catch {
+ // handle error (optional)
+ } finally {
setLoading(false);
setDownload(null);
- // handle error (optional)
}
};
diff --git a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx
index 803505436..1c14eb486 100644
--- a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx
+++ b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.component.jsx
@@ -76,14 +76,14 @@ function JobsDocumentsImgproxyComponent({
+ {!billId && (
+
+ )}
- {!billId && (
-
- )}
{!hasMediaAccess && (
diff --git a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.delete.component.jsx b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.delete.component.jsx
index 4701bca67..c11059b35 100644
--- a/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.delete.component.jsx
+++ b/client/src/components/jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.delete.component.jsx
@@ -67,7 +67,7 @@ export default function JobsDocumentsImgproxyDeleteButton({ galleryImages, delet
okButtonProps={{ danger: true }}
cancelText={t("general.actions.cancel")}
>
-
);
return (
-
+ {overlay}}
+ trigger="click"
+ getPopupContainer={() => document.querySelector('#time-ticket-modal')}
+ >
{t("timetickets.actions.clockout")}
diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js
index c508bf96e..12533865c 100644
--- a/client/src/redux/user/user.sagas.js
+++ b/client/src/redux/user/user.sagas.js
@@ -50,7 +50,7 @@ import {
} from "./user.actions";
import UserActionTypes from "./user.types";
//import * as amplitude from '@amplitude/analytics-browser';
-import posthog from 'posthog-js';
+import posthog from "posthog-js";
const fpPromise = FingerprintJS.load();
@@ -269,11 +269,11 @@ export function* signInSuccessSaga({ payload }) {
instanceSeg,
...(isParts
? [
- InstanceRenderManager({
- imex: "ImexPartsManagement",
- rome: "RomePartsManagement"
- })
- ]
+ InstanceRenderManager({
+ imex: "ImexPartsManagement",
+ rome: "RomePartsManagement"
+ })
+ ]
: [])
];
window.$crisp.push(["set", "session:segments", [segs]]);
@@ -375,17 +375,31 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
const isParts = yield select((state) => state.application.isPartsEntry === true);
const instanceSeg = InstanceRenderManager({ imex: "imex", rome: "rome" });
- let featureSegments;
- if (payload.features?.allAccess === true) {
- featureSegments = ["allAccess"];
- } else {
- const featureKeys = Object.keys(payload.features).filter(
- (key) =>
- payload.features[key] === true ||
- (typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key])))
- );
- featureSegments = ["basic", ...featureKeys];
- }
+ const featureSegments =
+ payload.features?.allAccess === true
+ ? ["allAccess"]
+ : [
+ "basic",
+ ...Object.keys(payload.features).filter(
+ (key) =>
+ payload.features[key] === true ||
+ (typeof payload.features[key] === "string" && !isNaN(Date.parse(payload.features[key])))
+ )
+ ];
+
+ const additionalSegments = [
+ payload.cdk_dealerid && "CDK",
+ payload.pbs_serialnumber && "PBS",
+ // payload.rr_dealerid && "Reynolds",
+ payload.accountingconfig.qbo === true && "QBO",
+ payload.accountingconfig.qbo === false &&
+ !payload.cdk_dealerid &&
+ !payload.pbs_serialnumber &&
+ // !payload.rr_dealerid &&
+ "QBD"
+ ].filter(Boolean);
+
+ featureSegments.push(...additionalSegments);
const regionSeg = payload.region_config ? `region:${payload.region_config}` : null;
const segments = [instanceSeg, ...(regionSeg ? [regionSeg] : []), ...featureSegments];
diff --git a/server/data/carfax-rps.js b/server/data/carfax-rps.js
index de267d7ac..fd8f634a1 100644
--- a/server/data/carfax-rps.js
+++ b/server/data/carfax-rps.js
@@ -234,11 +234,10 @@ const CreateRepairOrderTag = (job, errorCallback) => {
const ret = {
ro_number: crypto.createHash("md5").update(job.id, "utf8").digest("hex"),
v_vin: job.v_vin || "",
- v_year: job.v_model_yr
- ? parseInt(job.v_model_yr.match(/\d/g))
- ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
- : ""
- : "",
+ v_year: (() => {
+ const y = parseInt(job.v_model_yr);
+ return isNaN(y) ? null : y < 100 ? y + (y >= (new Date().getFullYear() + 1) % 100 ? 1900 : 2000) : y;
+ })(),
v_make: job.v_makedesc || "",
v_model: job.v_model || "",
diff --git a/server/data/carfax.js b/server/data/carfax.js
index aaa7d0dde..1424dea4f 100644
--- a/server/data/carfax.js
+++ b/server/data/carfax.js
@@ -286,11 +286,10 @@ const CreateRepairOrderTag = (job, errorCallback) => {
const ret = {
ro_number: crypto.createHash("md5").update(job.ro_number, "utf8").digest("hex"),
v_vin: job.v_vin || "",
- v_year: job.v_model_yr
- ? parseInt(job.v_model_yr.match(/\d/g))
- ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
- : ""
- : "",
+ v_year: (() => {
+ const y = parseInt(job.v_model_yr);
+ return isNaN(y) ? null : y < 100 ? y + (y >= (new Date().getFullYear() + 1) % 100 ? 1900 : 2000) : y;
+ })(),
v_make: job.v_make_desc || "",
v_model: job.v_model_desc || "",
diff --git a/server/data/usageReport.js b/server/data/usageReport.js
index cd03bbda2..ff51c3e5a 100644
--- a/server/data/usageReport.js
+++ b/server/data/usageReport.js
@@ -55,7 +55,9 @@ exports.default = async (req, res) => {
"patrick.fic@convenient-brands.com",
"bradley.rhoades@convenient-brands.com",
"jrome@rometech.com",
- "ivana@imexsystems.ca"
+ "ivana@imexsystems.ca",
+ "support@imexsystems.ca",
+ "sarah@rometech.com"
],
subject: `RO Usage Report - ${moment().format("MM/DD/YYYY")}`,
text: `
diff --git a/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js b/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js
index 66ef19b59..5f626e3d9 100644
--- a/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js
+++ b/server/integrations/partsManagement/endpoints/partsManagementProvisioning.js
@@ -134,13 +134,16 @@ const insertUserAssociation = async (uid, email, shopId) => {
/**
* PATCH handler for updating bodyshop fields.
- * Allows patching: shopname, address1, address2, city, state, zip_post, country, email, timezone, phone, logo_img_path
+ * Allows patching: shopname, address1, address2, city, state, zip_post, country, email, timezone, phone
+ * Also allows updating logo_img_path via a simple logoUrl string, which is expanded to the full object.
* @param req
* @param res
* @returns {Promise
}
*/
const patchPartsManagementProvisioning = async (req, res) => {
const { id } = req.params;
+
+ // Fields that can be directly patched 1:1
const allowedFields = [
"shopname",
"address1",
@@ -151,31 +154,58 @@ const patchPartsManagementProvisioning = async (req, res) => {
"country",
"email",
"timezone",
- "phone",
- "logo_img_path"
+ "phone"
+ // NOTE: logo_img_path is handled separately via logoUrl
];
+
const updateFields = {};
+
+ // Copy over simple scalar fields if present
for (const field of allowedFields) {
if (req.body[field] !== undefined) {
updateFields[field] = req.body[field];
}
}
+
+ // Handle logo update via a simple href string, same behavior as provision route
+ if (typeof req.body.logo_img_path === "string") {
+ const trimmed = req.body.logo_img_path.trim();
+ if (trimmed) {
+ updateFields.logo_img_path = {
+ src: trimmed,
+ width: "",
+ height: "",
+ headerMargin: DefaultNewShop.logo_img_path.headerMargin
+ };
+ }
+ }
+
if (Object.keys(updateFields).length === 0) {
return res.status(400).json({ error: "No valid fields provided for update." });
}
+
// Check that the bodyshop has an external_shop_id before allowing patch
try {
- // Fetch the bodyshop by id
const shopResp = await client.request(
- `query GetBodyshop($id: uuid!) { bodyshops_by_pk(id: $id) { id external_shop_id } }`,
+ `query GetBodyshop($id: uuid!) {
+ bodyshops_by_pk(id: $id) {
+ id
+ external_shop_id
+ }
+ }`,
{ id }
);
+
if (!shopResp.bodyshops_by_pk?.external_shop_id) {
return res.status(400).json({ error: "Cannot patch: bodyshop does not have an external_shop_id." });
}
} catch (err) {
- return res.status(500).json({ error: "Failed to validate bodyshop external_shop_id.", detail: err });
+ return res.status(500).json({
+ error: "Failed to validate bodyshop external_shop_id.",
+ detail: err
+ });
}
+
try {
const resp = await client.request(UPDATE_BODYSHOP_BY_ID, { id, fields: updateFields });
if (!resp.update_bodyshops_by_pk) {
diff --git a/server/notifications/scenarioBuilders.js b/server/notifications/scenarioBuilders.js
index d1bdb22a0..ec24e1804 100644
--- a/server/notifications/scenarioBuilders.js
+++ b/server/notifications/scenarioBuilders.js
@@ -81,8 +81,8 @@ const alternateTransportChangedBuilder = (data) => {
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
*/
const billPostedBuilder = (data) => {
- const facing = data?.data?.isinhouse ? "in-house" : "vendor";
- const body = `An ${facing} ${data?.data?.is_credit_memo ? "credit memo" : "bill"} has been posted.`.trim();
+ const facing = data?.data?.isinhouse ? "An In House" : "A Vendor";
+ const body = `${facing} ${data?.data?.is_credit_memo ? "credit memo" : "bill"} has been posted.`.trim();
return buildNotification(data, "notifications.job.billPosted", body, {
isInHouse: data?.data?.isinhouse,