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 });