import { useCallback, useEffect, useRef, useState } from 'react'; import { useOutletContext } from 'react-router-dom'; import { Controller, useForm } from 'react-hook-form'; import { Col } from 'react-bootstrap'; // components import CheckboxInput from "../formElements/CheckBoxInput.Controlled"; import Reset from "./Reset"; import Selector from "../formElements/Selector.Controlled"; import TextInput from "../formElements/TextInput"; import TextArea from "../formElements/TextArea"; // hooks import useDebounceLogic from '../../hooks/useDebounceLogic'; // functions import apiLoader from '../../../services/utilities/apiLoader'; // constants const errorNumber = 633; const nowRunning = 'common/display/CustomFields'; function CustomFields({ category, criteria, globalPrompt='make this change globally', setCustomFields, targetItem, }) { // Use a separate form instance for custom fields. This allows all of the React Hook form magic. const { control, getValues, setValue } = useForm(); // Local state for field definitions and loaded flag. const [debouncedCriteria, setDebouncedCriteria] = useState(criteria); const [fieldDefs, setFieldDefs] = useState({}); const [loaded, setLoaded] = useState(false); // Use a ref to store previous lifted values and avoid unnecessary updates. const prevValuesRef = useRef({}); const { appReady, customFieldsData, handleError, setStaticValues } = useOutletContext(); const onReset = useCallback( () => { setStaticValues(prev => ({ ...prev, customFieldsLoaded: false })); }, [setStaticValues] ); const debouncedSetter = useDebounceLogic(setDebouncedCriteria, 1000); useEffect( () => { debouncedSetter(criteria); }, [ criteria, debouncedSetter ] ); useEffect( () => { if (!appReady) return; // New items do not need to run this effect. const context = `${nowRunning}.useEffect`; const runThis = async () => { let currentCustomValues = {}; if (targetItem) { // Load the custom fields data for the target item. This is for existing items. const result = await apiLoader({ api: 'utilities/fields/data/get-item-data', payload: { targetId: targetItem } }); if (!result || result?.data?.failure) { handleError(result?.data?.failure, context, errorNumber, true); return; } currentCustomValues = result?.data.currentCustomValues || {}; } else if (typeof criteria === 'string' && criteria.trim().length >= 3 ) { // Load the most recent recently updated item with the given criteria. let lastItemId = ''; switch(category) { case 2: { // inventory items const result = await apiLoader({ api: 'inventory/items/lookup-last-item', payload: { sku: criteria } }); if (!result || result?.data?.failure) { handleError(result?.data?.failure, context, errorNumber, true); return; } lastItemId = result.data.lastItemId || ''; break; } default: { break; } } if (lastItemId) { const result = await apiLoader({ api: 'utilities/fields/data/get-item-data', payload: { targetId: lastItemId } }); if (!result || result?.data?.failure) { handleError(result?.data?.failure, context, errorNumber, true); return; } currentCustomValues = result?.data.currentCustomValues || {}; } } let updatedFields = {}; // Make sure this is always an object (even if empty) at the end of the switch cases!! switch(category) { case 2: { // inventory items updatedFields = customFieldsData.inventoryCustomFields || {}; break; } default: { break; } } // Filter out inactive fields. updatedFields = Object.fromEntries( Object.entries(updatedFields).filter(([_, field]) => field.active) ); if (Object.keys(updatedFields).length < 1) return; // This category has no active custom fields, so there's nothing to display. // Build initialValues using the API data and the field definitions. NOTE: data is undefined if there is no targetItem (new items). // For each field in updatedFields, if there is an API value, then: // - For guiType 1, 3, or 4 (Selector, TextInput, TextArea): take the first element’s uuidValue (or textValue, as appropriate) // - For guiType 2 (checkbox group): map the array to an array of uuidValues. const initialValues = {}; for (const [fieldId, field] of Object.entries(currentCustomValues)) { const apiValues = currentCustomValues?.[fieldId]; if (apiValues && Array.isArray(apiValues) && apiValues.length > 0) { const guiType = updatedFields[fieldId].guiType; // What we did here was go back to the field metadata to understand the type of field. if (guiType === 1 || guiType === 3 || guiType === 4) { // For single-value fields, use the first value’s uuidValue. initialValues[fieldId] = apiValues[0].uuidValue || ""; } else if (guiType === 2) { // For checkbox groups, use an array of uuidValues. initialValues[fieldId] = apiValues.map(item => item.uuidValue); } } else { // No API value: use default from field definition or a fallback. initialValues[fieldId] = field.defaultValue ? field.defaultValue : (field.guiType === 2 ? [] : ""); } } setCustomFields(initialValues); // Lift initial custom field values up to the parent. // Update local form state with these defaults. if (Object.keys(initialValues).length > 0) { Object.entries(initialValues).forEach(([key, val]) => { setValue(key, val); }); } else { Object.keys(updatedFields).forEach((fieldId) => { const guiType = updatedFields[fieldId]?.guiType; const defaultVal = guiType === 2 ? [] : ""; // checkbox group = [], others = "" setValue(fieldId, defaultVal); }); setValue("global", false); // reset global manually too } // Store the initial lifted values. prevValuesRef.current = initialValues; setFieldDefs(updatedFields); setLoaded(true); }; runThis().catch(e => handleError(e, context, errorNumber)); }, [ appReady, category, criteria, customFieldsData, debouncedCriteria, handleError, setCustomFields, setValue, targetItem ] ); // When the target item changes, reset the global field to false. useEffect( () => { if (targetItem) { setValue("global", false); setCustomFields(prev => ({ ...prev, global: false })); } }, [ setCustomFields, setValue, targetItem ] ); // Helper function to lift values on change if they've actually changed. const liftValues = () => { const newValues = getValues(); if (JSON.stringify(newValues) !== JSON.stringify(prevValuesRef.current)) { setCustomFields(newValues); prevValuesRef.current = newValues; } }; if (!loaded || !appReady) return null; if (Object.keys(fieldDefs).length < 1) return null; return ( <>