diff --git a/client/src/components/documents-upload-imgproxy/documents-upload-imgproxy.utility.js b/client/src/components/documents-upload-imgproxy/documents-upload-imgproxy.utility.js
index 7ecbd50bb..000871efe 100644
--- a/client/src/components/documents-upload-imgproxy/documents-upload-imgproxy.utility.js
+++ b/client/src/components/documents-upload-imgproxy/documents-upload-imgproxy.utility.js
@@ -67,11 +67,14 @@ export const uploadToS3 = async (
}
//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 = {
onUploadProgress: (e) => {
if (onProgress) onProgress({ percent: (e.loaded / e.total) * 100 });
+ },
+ headers: {
+ ...contentType ? { "Content-Type": fileType } : {}
}
};
diff --git a/client/src/components/job-totals-table/jobs-totals.cash-discount-display.component.jsx b/client/src/components/job-totals-table/jobs-totals.cash-discount-display.component.jsx
index e5a89e23c..409e2e5dc 100644
--- a/client/src/components/job-totals-table/jobs-totals.cash-discount-display.component.jsx
+++ b/client/src/components/job-totals-table/jobs-totals.cash-discount-display.component.jsx
@@ -20,35 +20,27 @@ export function JobTotalsCashDiscount({ bodyshop, amountDinero }) {
const notification = useNotification();
const fetchData = useCallback(async () => {
- if (amountDinero && bodyshop) {
- setLoading(true);
- let response;
- try {
- response = await axios.post("/intellipay/checkfee", {
- bodyshop: { id: bodyshop.id, imexshopid: bodyshop.imexshopid, state: bodyshop.state },
- amount: Dinero(amountDinero).toFormat("0.00")
- });
+ if (!amountDinero || !bodyshop) return;
- if (response?.data?.error) {
- notification.open({
- type: "error",
- message:
- response.data?.error ||
- "Error encountered when contacting IntelliPay service to determine cash discounted price."
- });
- } else {
- setFee(response.data?.fee || 0);
- }
- } catch (error) {
- notification.open({
- type: "error",
- message:
- error.response?.data?.error ||
- "Error encountered when contacting IntelliPay service to determine cash discounted price."
- });
- } finally {
- setLoading(false);
+ setLoading(true);
+ const errorMessage = "Error encountered when contacting IntelliPay service to determine cash discounted price.";
+
+ try {
+ const { id, imexshopid, state } = bodyshop;
+ const { data } = await axios.post("/intellipay/checkfee", {
+ bodyshop: { id, imexshopid, state },
+ amount: Dinero(amountDinero).toUnit()
+ });
+
+ if (data?.error) {
+ notification.open({ type: "error", message: data.error || errorMessage });
+ } else {
+ setFee(data?.fee ?? 0);
}
+ } catch (error) {
+ notification.open({ type: "error", message: error.response?.data?.error || errorMessage });
+ } finally {
+ setLoading(false);
}
}, [amountDinero, bodyshop, notification]);
diff --git a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx
index e9e411b41..b2d355434 100644
--- a/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx
+++ b/client/src/components/schedule-calendar-wrapper/scheduler-calendar-wrapper.component.jsx
@@ -40,27 +40,26 @@ export function ScheduleCalendarWrapperComponent({
const currentView = search.view || defaultView || "week";
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";
// Prioritize explicit blocked-day background to ensure red in all themes
let bg;
if (useBg) {
- if (event?.block) {
- bg = "var(--event-block-bg)";
- } else if (hasColor) {
- bg = event?.color?.hex ?? event?.color;
- } else {
- bg = "var(--event-bg-fallback)";
- }
+ bg = block
+ ? "var(--event-block-bg)"
+ : arrived
+ ? "var(--event-arrived-bg)"
+ : (color?.hex ?? color ?? "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 = [
"imex-event",
- event.arrived && "imex-event-arrived",
- event.block && "imex-event-block",
+ arrived && "imex-event-arrived",
+ block && "imex-event-block",
usedFallback && "imex-event-fallback"
]
.filter(Boolean)
diff --git a/client/src/components/shop-info/shop-info.container.jsx b/client/src/components/shop-info/shop-info.container.jsx
index afcd7968d..a820dce69 100644
--- a/client/src/components/shop-info/shop-info.container.jsx
+++ b/client/src/components/shop-info/shop-info.container.jsx
@@ -23,13 +23,24 @@ export default function ShopInfoContainer() {
});
const notification = useNotification();
- const combinedFeatureConfig = {
- ...FEATURE_CONFIGS.general,
- ...FEATURE_CONFIGS.responsibilitycenters
- };
+ const combineFeatureConfigs = (...configs) =>
+ (configs || [])
+ .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
- const { createSubmissionHandler } = useFormDataPreservation(form, data?.bodyshops[0], combinedFeatureConfig);
+ const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation(
+ form,
+ data?.bodyshops[0],
+ combinedFeatureConfig
+ );
const handleFinish = createSubmissionHandler((values) => {
setSaveLoading(true);
@@ -51,8 +62,11 @@ export default function ShopInfoContainer() {
});
useEffect(() => {
- if (data) form.resetFields();
- }, [form, data]);
+ if (!data) return;
+ form.resetFields();
+ // After reset, re-apply hidden field preservation so values aren't wiped
+ preserveHiddenFormData();
+ }, [data, form, preserveHiddenFormData]);
if (error) return ;
if (loading) return ;
diff --git a/client/src/components/shop-info/useFormDataPreservation.js b/client/src/components/shop-info/useFormDataPreservation.js
index 310ff0202..93978962e 100644
--- a/client/src/components/shop-info/useFormDataPreservation.js
+++ b/client/src/components/shop-info/useFormDataPreservation.js
@@ -1,4 +1,4 @@
-import { useCallback, useEffect } from "react";
+import { useCallback, useEffect, useMemo } from "react";
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
*/
export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
- const getNestedValue = (obj, path) => {
- return path.reduce((current, key) => current?.[key], obj);
- };
-
+ // Safe nested getters/setters using path arrays
+ const getNestedValue = (obj, path) => path?.reduce((acc, key) => acc?.[key], obj);
const setNestedValue = (obj, path, value) => {
const lastKey = path[path.length - 1];
- const parentPath = path.slice(0, -1);
-
- const parent = parentPath.reduce((current, key) => {
- if (!current[key]) current[key] = {};
- return current[key];
+ const parent = path.slice(0, -1).reduce((curr, key) => {
+ if (!curr[key] || typeof curr[key] !== "object") curr[key] = {};
+ return curr[key];
}, obj);
-
parent[lastKey] = value;
};
- const preserveHiddenFormData = useCallback(() => {
- const preservationData = {};
- let hasDataToPreserve = false;
-
+ // Paths for features that are NOT accessible
+ const disabledPaths = useMemo(() => {
+ const result = [];
+ if (!featureConfig) return result;
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
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) {
- fieldPaths.forEach((fieldPath) => {
- const currentValues = form.getFieldsValue();
- let value = getNestedValue(currentValues, fieldPath);
+ const preserveHiddenFormData = useCallback(() => {
+ const currentValues = form.getFieldsValue();
+ const preservationData = {};
+ let hasAny = false;
- if (value === undefined || value === null) {
- value = getNestedValue(bodyshop, fieldPath);
- }
-
- if (value !== undefined && value !== null) {
- setNestedValue(preservationData, fieldPath, value);
- hasDataToPreserve = true;
- }
- });
+ disabledPaths.forEach((path) => {
+ let value = getNestedValue(currentValues, path);
+ if (value == null) value = getNestedValue(bodyshop, path);
+ if (value != null) {
+ setNestedValue(preservationData, path, value);
+ hasAny = true;
}
});
- if (hasDataToPreserve) {
- form.setFieldsValue(preservationData);
- }
- }, [form, featureConfig, bodyshop]);
+ if (hasAny) form.setFieldsValue(preservationData);
+ }, [form, bodyshop, disabledPaths]);
const getCompleteFormValues = () => {
- const currentFormValues = form.getFieldsValue();
- const completeValues = { ...currentFormValues };
+ const currentValues = form.getFieldsValue();
+ const complete = { ...currentValues };
- Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
- const hasAccess = HasFeatureAccess({ featureName, bodyshop });
-
- if (!hasAccess) {
- 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);
- }
- });
- }
+ disabledPaths.forEach((path) => {
+ let value = getNestedValue(currentValues, path);
+ if (value == null) value = getNestedValue(bodyshop, path);
+ if (value != null) setNestedValue(complete, path, value);
});
- return completeValues;
+ return complete;
};
const createSubmissionHandler = (originalHandler) => {
@@ -103,8 +87,8 @@ export const FEATURE_CONFIGS = {
["md_responsibility_centers", "profits"],
["md_responsibility_centers", "defaults"],
["md_responsibility_centers", "dms_defaults"],
- ["md_responsibility_centers", "taxes", "itemexemptcode"],
- ["md_responsibility_centers", "taxes", "invoiceexemptcode"],
+ ["md_responsibility_centers", "taxes"],
+ ["md_responsibility_centers", "cieca_pfl"],
["md_responsibility_centers", "ar"],
["md_responsibility_centers", "refund"],
["md_responsibility_centers", "sales_tax_codes"],
diff --git a/client/src/components/vendors-form/vendors-form.component.jsx b/client/src/components/vendors-form/vendors-form.component.jsx
index 5249c9ffe..df93e08d3 100644
--- a/client/src/components/vendors-form/vendors-form.component.jsx
+++ b/client/src/components/vendors-form/vendors-form.component.jsx
@@ -180,7 +180,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
{
* @returns {Promise}
*/
const checkFee = async (req, res) => {
- const logResponseMeta = {
- bodyshop: {
- id: req.body?.bodyshop?.id,
- imexshopid: req.body?.bodyshop?.imexshopid,
- name: req.body?.bodyshop?.shopname,
- state: req.body?.bodyshop?.state
- },
- amount: req.body?.amount
- };
+ const { bodyshop = {}, amount } = req.body || {};
+ const { id, imexshopid, shopname, state } = bodyshop;
+ const logResponseMeta = { bodyshop: { id, imexshopid, name: shopname, state }, amount };
logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, logResponseMeta);
- if (!isNumber(req.body?.amount) || req.body?.amount <= 0) {
+ if (!isNumber(amount) || amount <= 0) {
logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, {
message: "Amount is zero or undefined, skipping fee check.",
...logResponseMeta
});
-
return res.json({ fee: 0 });
}
- const shopCredentials = await getShopCredentials(req.body.bodyshop);
+ const shopCredentials = await getShopCredentials(bodyshop);
if (shopCredentials?.error) {
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
message: shopCredentials.error?.message,
...logResponseMeta
});
-
return res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
}
@@ -292,13 +284,10 @@ const checkFee = async (req, res) => {
{
method: "fee",
...shopCredentials,
- amount: req.body.amount,
- paymenttype: `CC`,
+ amount: String(amount), // Type cast to string as required by API
+ paymenttype: "CC",
cardnum: "4111111111111111", // Required for compatibility with API
- state:
- req.body.bodyshop?.state && req.body.bodyshop.state.length === 2
- ? req.body.bodyshop.state.toUpperCase()
- : "ZZ"
+ state: state?.toUpperCase() || "ZZ"
},
{ sort: false } // Ensure query string order is preserved
),
@@ -310,46 +299,24 @@ const checkFee = async (req, res) => {
...logResponseMeta
});
- const response = await axios(options);
+ const { data } = await axios(options);
- if (response.data?.error) {
- logger.log("intellipay-checkfee-api-error", "ERROR", req.user?.email, null, {
- message: response.data?.error,
- ...logResponseMeta
- });
-
- return res.status(400).json({
- error: response.data?.error,
- type: "intellipay-checkfee-api-error",
- ...logResponseMeta
- });
+ if (data?.error || data < 0) {
+ const errorType = data?.error ? "intellipay-checkfee-api-error" : "intellipay-checkfee-negative-fee";
+ const errorMessage = data?.error
+ ? data?.error
+ : "Fee amount negative. Check API credentials & account configuration.";
+ logger.log(errorType, "ERROR", req.user?.email, null, { message: errorMessage, data, ...logResponseMeta });
+ return res.status(400).json({ error: errorMessage, type: errorType, data, ...logResponseMeta });
}
- if (response.data < 0) {
- logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
- message: "Fee amount returned is negative.",
- ...logResponseMeta
- });
-
- return res.json({
- error: "Fee amount negative. Check API credentials & account configuration.",
- ...logResponseMeta,
- type: "intellipay-checkfee-negative-fee"
- });
- }
-
- logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, {
- fee: response.data,
- ...logResponseMeta
- });
-
- return res.json({ fee: response.data, ...logResponseMeta });
+ logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, { fee: data, ...logResponseMeta });
+ return res.json({ fee: data, ...logResponseMeta });
} catch (error) {
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
message: error?.message,
...logResponseMeta
});
-
return res.status(500).json({ error: error?.message, logResponseMeta });
}
};
diff --git a/server/media/imgproxy-media.js b/server/media/imgproxy-media.js
index 7a2b1ebfb..85bc3393f 100644
--- a/server/media/imgproxy-media.js
+++ b/server/media/imgproxy-media.js
@@ -58,8 +58,20 @@ const generateSignedUploadUrls = async (req, res) => {
}
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 });