Merge remote-tracking branch 'origin/release/2025-09-12' into feature/IO-3255-simplified-part-management

This commit is contained in:
Dave
2025-09-10 15:58:06 -04:00
15 changed files with 142 additions and 154 deletions

View File

@@ -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 } : {}
} }
}; };

View File

@@ -20,35 +20,27 @@ export function JobTotalsCashDiscount({ bodyshop, amountDinero }) {
const notification = useNotification(); const notification = useNotification();
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
if (amountDinero && bodyshop) { if (!amountDinero || !bodyshop) return;
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 (response?.data?.error) { setLoading(true);
notification.open({ const errorMessage = "Error encountered when contacting IntelliPay service to determine cash discounted price.";
type: "error",
message: try {
response.data?.error || const { id, imexshopid, state } = bodyshop;
"Error encountered when contacting IntelliPay service to determine cash discounted price." const { data } = await axios.post("/intellipay/checkfee", {
}); bodyshop: { id, imexshopid, state },
} else { amount: Dinero(amountDinero).toUnit()
setFee(response.data?.fee || 0); });
}
} catch (error) { if (data?.error) {
notification.open({ notification.open({ type: "error", message: data.error || errorMessage });
type: "error", } else {
message: setFee(data?.fee ?? 0);
error.response?.data?.error ||
"Error encountered when contacting IntelliPay service to determine cash discounted price."
});
} finally {
setLoading(false);
} }
} catch (error) {
notification.open({ type: "error", message: error.response?.data?.error || errorMessage });
} finally {
setLoading(false);
} }
}, [amountDinero, bodyshop, notification]); }, [amountDinero, bodyshop, notification]);

View File

@@ -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)

View File

@@ -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 />;

View File

@@ -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"],

View File

@@ -180,7 +180,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
<Form.Item <Form.Item
name="tags" name="tags"
label={t("vendor.fields.tags")} label={t("vendors.fields.tags")}
rules={[ rules={[
{ {
//message: t("general.validation.required"), //message: t("general.validation.required"),

View File

@@ -959,6 +959,7 @@
- enforce_referral - enforce_referral
- entegral_configuration - entegral_configuration
- entegral_id - entegral_id
- external_shop_id
- features - features
- federal_tax_id - federal_tax_id
- id - id
@@ -1012,6 +1013,8 @@
- prodtargethrs - prodtargethrs
- production_config - production_config
- region_config - region_config
- rr_configuration
- rr_dealerid
- schedule_end_time - schedule_end_time
- schedule_start_time - schedule_start_time
- scoreboard_target - scoreboard_target
@@ -1035,7 +1038,6 @@
- use_fippa - use_fippa
- use_paint_scale_data - use_paint_scale_data
- uselocalmediaserver - uselocalmediaserver
- external_shop_id
- website - website
- workingdays - workingdays
- zip_post - zip_post
@@ -1068,6 +1070,7 @@
- enforce_conversion_category - enforce_conversion_category
- enforce_conversion_csr - enforce_conversion_csr
- enforce_referral - enforce_referral
- external_shop_id
- federal_tax_id - federal_tax_id
- id - id
- inhousevendorid - inhousevendorid
@@ -1113,6 +1116,7 @@
- phone - phone
- prodtargethrs - prodtargethrs
- production_config - production_config
- rr_configuration
- schedule_end_time - schedule_end_time
- schedule_start_time - schedule_start_time
- scoreboard_target - scoreboard_target
@@ -1131,7 +1135,6 @@
- use_fippa - use_fippa
- use_paint_scale_data - use_paint_scale_data
- uselocalmediaserver - uselocalmediaserver
- external_shop_id
- website - website
- workingdays - workingdays
- zip_post - zip_post

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "rr_configuration" jsonb
-- null default jsonb_build_object();

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "rr_configuration" jsonb
null default jsonb_build_object();

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."bodyshops" add column "rr_dealierid" text
-- null;

View File

@@ -0,0 +1,2 @@
alter table "public"."bodyshops" add column "rr_dealierid" text
null;

View File

@@ -0,0 +1 @@
alter table "public"."bodyshops" rename column "rr_dealerid" to "rr_dealierid";

View File

@@ -0,0 +1 @@
alter table "public"."bodyshops" rename column "rr_dealierid" to "rr_dealerid";

View File

@@ -252,35 +252,27 @@ const generatePaymentUrl = async (req, res) => {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
const checkFee = async (req, res) => { const checkFee = async (req, res) => {
const logResponseMeta = { const { bodyshop = {}, amount } = req.body || {};
bodyshop: { const { id, imexshopid, shopname, state } = bodyshop;
id: req.body?.bodyshop?.id, const logResponseMeta = { bodyshop: { id, imexshopid, name: shopname, state }, amount };
imexshopid: req.body?.bodyshop?.imexshopid,
name: req.body?.bodyshop?.shopname,
state: req.body?.bodyshop?.state
},
amount: req.body?.amount
};
logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, logResponseMeta); 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, { logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, {
message: "Amount is zero or undefined, skipping fee check.", message: "Amount is zero or undefined, skipping fee check.",
...logResponseMeta ...logResponseMeta
}); });
return res.json({ fee: 0 }); return res.json({ fee: 0 });
} }
const shopCredentials = await getShopCredentials(req.body.bodyshop); const shopCredentials = await getShopCredentials(bodyshop);
if (shopCredentials?.error) { if (shopCredentials?.error) {
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, { logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
message: shopCredentials.error?.message, message: shopCredentials.error?.message,
...logResponseMeta ...logResponseMeta
}); });
return res.status(400).json({ error: 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", method: "fee",
...shopCredentials, ...shopCredentials,
amount: req.body.amount, amount: String(amount), // Type cast to string as required by API
paymenttype: `CC`, paymenttype: "CC",
cardnum: "4111111111111111", // Required for compatibility with API cardnum: "4111111111111111", // Required for compatibility with API
state: state: state?.toUpperCase() || "ZZ"
req.body.bodyshop?.state && req.body.bodyshop.state.length === 2
? req.body.bodyshop.state.toUpperCase()
: "ZZ"
}, },
{ sort: false } // Ensure query string order is preserved { sort: false } // Ensure query string order is preserved
), ),
@@ -310,46 +299,24 @@ const checkFee = async (req, res) => {
...logResponseMeta ...logResponseMeta
}); });
const response = await axios(options); const { data } = await axios(options);
if (response.data?.error) { if (data?.error || data < 0) {
logger.log("intellipay-checkfee-api-error", "ERROR", req.user?.email, null, { const errorType = data?.error ? "intellipay-checkfee-api-error" : "intellipay-checkfee-negative-fee";
message: response.data?.error, const errorMessage = data?.error
...logResponseMeta ? 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({ return res.status(400).json({ error: errorMessage, type: errorType, data, ...logResponseMeta });
error: response.data?.error,
type: "intellipay-checkfee-api-error",
...logResponseMeta
});
} }
if (response.data < 0) { logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, { fee: data, ...logResponseMeta });
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, { return res.json({ fee: data, ...logResponseMeta });
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 });
} catch (error) { } catch (error) {
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, { logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
message: error?.message, message: error?.message,
...logResponseMeta ...logResponseMeta
}); });
return res.status(500).json({ error: error?.message, logResponseMeta }); return res.status(500).json({ error: error?.message, logResponseMeta });
} }
}; };

View File

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