diff --git a/client/src/components/shop-info/shop-info.container.jsx b/client/src/components/shop-info/shop-info.container.jsx index 915325ea9..7102397ed 100644 --- a/client/src/components/shop-info/shop-info.container.jsx +++ b/client/src/components/shop-info/shop-info.container.jsx @@ -1,15 +1,16 @@ import { useMutation, useQuery } from "@apollo/client"; import { Form } from "antd"; -import dayjs from "../../utils/day"; -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { QUERY_BODYSHOP, UPDATE_SHOP } from "../../graphql/bodyshop.queries"; +import dayjs from "../../utils/day"; import AlertComponent from "../alert/alert.component"; import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import ShopInfoComponent from "./shop-info.component"; -import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { FEATURE_CONFIGS, useFormDataPreservation } from "./useFormDataPreservation"; export default function ShopInfoContainer() { const [form] = Form.useForm(); @@ -22,16 +23,24 @@ export default function ShopInfoContainer() { }); const notification = useNotification(); - const handleFinish = (values) => { + const combinedFeatureConfig = { + ...FEATURE_CONFIGS.general, + ...FEATURE_CONFIGS.responsibilitycenters + }; + + // Use form data preservation for all shop-info features + const { createSubmissionHandler } = useFormDataPreservation(form, data?.bodyshops[0], combinedFeatureConfig); + + const handleFinish = createSubmissionHandler((values) => { setSaveLoading(true); logImEXEvent("shop_update"); updateBodyshop({ variables: { id: data.bodyshops[0].id, shop: values } }) - .then((r) => { + .then(() => { notification["success"]({ message: t("bodyshop.successes.save") }); - refetch().then((_) => form.resetFields()); + refetch().then(() => form.resetFields()); }) .catch((error) => { notification["error"]({ @@ -39,7 +48,7 @@ export default function ShopInfoContainer() { }); }); setSaveLoading(false); - }; + }); useEffect(() => { if (data) form.resetFields(); diff --git a/client/src/components/shop-info/useFormDataPreservation.js b/client/src/components/shop-info/useFormDataPreservation.js new file mode 100644 index 000000000..ffd605e00 --- /dev/null +++ b/client/src/components/shop-info/useFormDataPreservation.js @@ -0,0 +1,140 @@ +import { useEffect } from "react"; +import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component"; + +/** + * Custom hook to preserve form data for conditionally hidden fields based on feature access + * @param {Object} form - Ant Design form instance + * @param {Object} bodyshop - Bodyshop data for feature access checks (also contains existing database values) + * @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); + }; + + 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]; + }, obj); + + parent[lastKey] = value; + }; + + const preserveHiddenFormData = () => { + const preservationData = {}; + let hasDataToPreserve = false; + + Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => { + const hasAccess = HasFeatureAccess({ featureName, bodyshop }); + + if (!hasAccess) { + fieldPaths.forEach((fieldPath) => { + const currentValues = form.getFieldsValue(); + let value = getNestedValue(currentValues, fieldPath); + + if (value === undefined || value === null) { + value = getNestedValue(bodyshop, fieldPath); + } + + if (value !== undefined && value !== null) { + setNestedValue(preservationData, fieldPath, value); + hasDataToPreserve = true; + } + }); + } + }); + + if (hasDataToPreserve) { + form.setFieldsValue(preservationData); + } + }; + + const getCompleteFormValues = () => { + const currentFormValues = form.getFieldsValue(); + const completeValues = { ...currentFormValues }; + + 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); + } + }); + } + }); + + return completeValues; + }; + + const createSubmissionHandler = (originalHandler) => { + return () => { + const completeValues = getCompleteFormValues(); + + // Call the original handler with complete values including hidden data + return originalHandler(completeValues); + }; + }; + + useEffect(() => { + preserveHiddenFormData(); + }, [bodyshop]); + + return { preserveHiddenFormData, getCompleteFormValues, createSubmissionHandler }; +}; + +/** + * Predefined feature configurations for common shop-info components + */ +export const FEATURE_CONFIGS = { + responsibilitycenters: { + export: [ + ["md_responsibility_centers", "costs"], + ["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", "ar"], + ["md_responsibility_centers", "refund"], + ["md_responsibility_centers", "sales_tax_codes"], + ["md_responsibility_centers", "ttl_adjustment"], + ["md_responsibility_centers", "ttl_tax_adjustment"] + ] + }, + general: { + export: [ + ["accountingconfig", "qbo"], + ["accountingconfig", "qbo_usa"], + ["accountingconfig", "qbo_departmentid"], + ["accountingconfig", "tiers"], + ["accountingconfig", "twotierpref"], + ["accountingconfig", "printlater"], + ["accountingconfig", "emaillater"], + ["accountingconfig", "ReceivableCustomField1"], + ["accountingconfig", "ReceivableCustomField2"], + ["accountingconfig", "ReceivableCustomField3"], + ["md_classes"], + ["enforce_class"], + ["accountingconfig", "ClosingPeriod"], + ["accountingconfig", "companyCode"], + ["accountingconfig", "batchID"] + ], + bills: [ + ["bill_tax_rates", "federal_tax_rate"], + ["bill_tax_rates", "state_tax_rate"], + ["bill_tax_rates", "local_tax_rate"] + ], + timetickets: [["tt_allow_post_to_invoiced"], ["tt_enforce_hours_for_tech_console"], ["bill_allow_post_to_closed"]] + } +};