@openg2p/registry-widgets 1.1.0-dev.1 → 1.1.0-dev.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -245,24 +245,27 @@ const getValidationPattern = (validationType) => {
245
245
  };
246
246
 
247
247
  /**
248
- * Validate value against validation rules
248
+ * Validate value against validation rules.
249
+ *
250
+ * @param skipRequired - When true, required-field checks are skipped (used by
251
+ * per-section Save/Next buttons so the user can move between sections without
252
+ * filling every mandatory field; only format/range checks still run).
249
253
  */
250
- const validateWidget = (value, validation, required = false) => {
254
+ const validateWidget = (value, validation, required = false, skipRequired = false) => {
251
255
  const errors = [];
252
256
  if (!validation && !required) {
253
257
  return errors;
254
258
  }
255
- // Check required
256
- const isRequired = validation?.required ?? required;
259
+ // Check required (skipped when navigating between sections)
260
+ const isRequired = !skipRequired && (validation?.required ?? required);
257
261
  // For boolean, false is a valid value, so only check for null/undefined/empty string
258
262
  const isEmpty = value === null || value === undefined || value === '';
259
263
  if (isRequired && isEmpty) {
260
264
  errors.push('This field is required');
261
265
  return errors; // Return early if required field is empty
262
266
  }
263
- // Skip other validations if value is empty and not required
264
- // Note: For boolean, false is a valid value, so we only skip if truly empty
265
- if (isEmpty && !isRequired) {
267
+ // Skip format/range validations if value is empty
268
+ if (isEmpty) {
266
269
  return errors;
267
270
  }
268
271
  if (!validation) {
@@ -1004,6 +1007,19 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1004
1007
  // If not found and doesn't contain dots, try as widget-id
1005
1008
  if ((depValue === null || depValue === undefined) && !dataSource.dependsOn.includes('.')) {
1006
1009
  depValue = allValues[dataSource.dependsOn];
1010
+ // Smart resolution: If not found at top level, try to find the dependency in the same nested object
1011
+ // by looking for other keys in allValues that might contain the dependency.
1012
+ // We look for objects that contain both the current widget's path (if we can guess it) and the dependency.
1013
+ // But since we don't know the current widget's path here, we search for any object that has this dependency key.
1014
+ if (depValue === null || depValue === undefined || depValue === '') {
1015
+ for (const val of Object.values(allValues)) {
1016
+ if (val && typeof val === 'object' && !Array.isArray(val) && dataSource.dependsOn in val) {
1017
+ depValue = val[dataSource.dependsOn];
1018
+ if (depValue !== null && depValue !== undefined && depValue !== '')
1019
+ break;
1020
+ }
1021
+ }
1022
+ }
1007
1023
  }
1008
1024
  if (depValue === null || depValue === undefined || depValue === '') {
1009
1025
  // If dependency is empty, return empty array
@@ -1047,8 +1063,8 @@ const getApiDataSource = async (dataSource, allValues, dataSourceRequestHandler,
1047
1063
  }
1048
1064
  }
1049
1065
  else if (staticParams.level_id) {
1050
- // First level has no parent
1051
- requestParams.parent_level_value_id = null;
1066
+ // First level has no parent, send empty string as many OpenG2P APIs expect it
1067
+ requestParams.parent_level_value_id = "";
1052
1068
  }
1053
1069
  // Get service mnemonic and endpoint (required)
1054
1070
  const service = dataSource.service;
@@ -1117,10 +1133,12 @@ const transformDataSourceOptions = (data, valueKey, labelKey) => {
1117
1133
  return { value: item, label: String(item) };
1118
1134
  });
1119
1135
  }
1120
- return data.map((item) => ({
1121
- value: item[valueKey],
1122
- label: item[labelKey] || String(item[valueKey]),
1123
- }));
1136
+ return data.map((item) => {
1137
+ const value = item[valueKey];
1138
+ // Try multiple common label keys if the primary one is missing
1139
+ const label = item[labelKey] || item.name || item.label || item.mnemonic || item.level_value_mnemonic || String(value);
1140
+ return { value, label };
1141
+ });
1124
1142
  };
1125
1143
 
1126
1144
  const WidgetEventBusContext = React.createContext(null);
@@ -1898,46 +1916,71 @@ const useBaseWidget = (options) => {
1898
1916
  const userHasSetValueRef = useRef(false);
1899
1917
  // Use ref for values to avoid stale closures in handleChange
1900
1918
  const valuesRef = useRef(values);
1919
+ const loadingRef = useRef(loading);
1920
+ const dataSourceOptionsRef = useRef(dataSourceOptions);
1901
1921
  useEffect(() => {
1902
1922
  valuesRef.current = values;
1903
- }, [values]);
1923
+ loadingRef.current = loading;
1924
+ dataSourceOptionsRef.current = dataSourceOptions;
1925
+ }, [values, loading, dataSourceOptions]);
1904
1926
  // Track last dispatched value to prevent duplicate dispatches
1905
1927
  const lastDispatchedValueRef = useRef(null);
1906
- // Get current value
1907
- const currentValue = useMemo(() => {
1908
- if (isLayoutWidget) {
1909
- return undefined; // Layout widgets don't have values
1928
+ // Helper to extract displayable value from object (especially geo hierarchy objects)
1929
+ const extractValueFromObject = useCallback((obj) => {
1930
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
1931
+ return obj;
1910
1932
  }
1911
- // Helper to extract displayable value from object (especially geo hierarchy objects)
1912
- const extractValueFromObject = (obj) => {
1913
- if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
1914
- return obj;
1915
- }
1916
- // Check for geo hierarchy structure first
1917
- if ('geo_code_hierarchy_json' in obj || 'geo_lowest_level_value_id' in obj) {
1918
- if ('geo_lowest_level_value_id' in obj) {
1919
- return obj.geo_lowest_level_value_id;
1933
+ // Check for geo hierarchy structure first
1934
+ const geoConfig = config['widget-geo-config'];
1935
+ if (geoConfig) {
1936
+ // If we have a geo hierarchy object, extract the value for this specific level
1937
+ const hierarchy = obj.hierarchy || obj.geo_code_hierarchy_json?.hierarchy;
1938
+ if (Array.isArray(hierarchy)) {
1939
+ const levelData = hierarchy.find((l) => l.level === geoConfig.level);
1940
+ if (levelData) {
1941
+ return levelData.level_value_id;
1920
1942
  }
1921
- // If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
1922
- return undefined;
1923
1943
  }
1924
- // Try common value fields
1925
- if ('value' in obj) {
1926
- return obj.value;
1944
+ }
1945
+ if ('geo_code_hierarchy_json' in obj || 'geo_lowest_level_value_id' in obj || 'hierarchy' in obj) {
1946
+ if ('geo_lowest_level_value_id' in obj) {
1947
+ return obj.geo_lowest_level_value_id;
1927
1948
  }
1928
- if ('id' in obj) {
1929
- return obj.id;
1949
+ if ('lowest_level_value_id' in obj) {
1950
+ return obj.lowest_level_value_id;
1930
1951
  }
1931
- if ('label' in obj) {
1932
- return obj.label;
1952
+ // Fallback for nested geo_code_hierarchy_json
1953
+ if (obj.geo_code_hierarchy_json?.lowest_level_value_id) {
1954
+ return obj.geo_code_hierarchy_json.lowest_level_value_id;
1933
1955
  }
1934
- if ('name' in obj) {
1935
- return obj.name;
1956
+ if (obj.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
1957
+ return obj.geo_code_hierarchy_json.geo_lowest_level_value_id;
1936
1958
  }
1937
- // If no extractable value found, return undefined to avoid rendering object as React child
1938
- // This prevents "Objects are not valid as a React child" errors
1959
+ // If it's a geo hierarchy object but no extractable ID, return undefined to avoid rendering object
1939
1960
  return undefined;
1940
- };
1961
+ }
1962
+ // Try common value fields
1963
+ if ('value' in obj) {
1964
+ return obj.value;
1965
+ }
1966
+ if ('id' in obj) {
1967
+ return obj.id;
1968
+ }
1969
+ if ('label' in obj) {
1970
+ return obj.label;
1971
+ }
1972
+ if ('name' in obj) {
1973
+ return obj.name;
1974
+ }
1975
+ // If no extractable value found, return undefined to avoid rendering object as React child
1976
+ // This prevents "Objects are not valid as a React child" errors
1977
+ return undefined;
1978
+ }, [config]);
1979
+ // Get current value
1980
+ const currentValue = useMemo(() => {
1981
+ if (isLayoutWidget) {
1982
+ return undefined; // Layout widgets don't have values
1983
+ }
1941
1984
  // Try to get value from widgetId first (this should have the actual selected value)
1942
1985
  // For geo widgets with dataPath, widgetId stores the actual ID, while dataPath stores the hierarchy object
1943
1986
  let value = values[widgetId];
@@ -1978,6 +2021,33 @@ const useBaseWidget = (options) => {
1978
2021
  }
1979
2022
  return value !== undefined ? value : config['widget-data-default'];
1980
2023
  }, [values, config, widgetId, isLayoutWidget]);
2024
+ // Track the last value we attempted to mirror to prevent infinite loops
2025
+ const lastMirroredValueRef = useRef(null);
2026
+ // Mirror value from dataPath to widgetId in Redux state if it's not already there.
2027
+ // This is essential for widgets that depend on this widget via 'dependsOn' using its widgetId,
2028
+ // especially when the actual data is stored in a nested path.
2029
+ // CRITICAL: This ensures that dependencies are resolved correctly when entering Edit mode.
2030
+ useEffect(() => {
2031
+ if (isLayoutWidget || !config['widget-data-path']) {
2032
+ return;
2033
+ }
2034
+ const rawValue = getWidgetValue(values, config['widget-data-path'], widgetId);
2035
+ if (rawValue !== undefined && rawValue !== null) {
2036
+ const extractedValue = extractValueFromObject(rawValue);
2037
+ // Only mirror if:
2038
+ // 1. The top-level value is undefined (initial load or entering edit mode)
2039
+ // 2. We haven't already tried to mirror this specific value (prevents loops if dispatch is ignored or delayed)
2040
+ // 3. The extracted value is valid
2041
+ if (values[widgetId] === undefined &&
2042
+ extractedValue !== undefined &&
2043
+ extractedValue !== null &&
2044
+ lastMirroredValueRef.current !== extractedValue) {
2045
+ lastMirroredValueRef.current = extractedValue;
2046
+ dispatch(setValue({ widgetId, value: extractedValue }));
2047
+ }
2048
+ }
2049
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2050
+ }, [values, config['widget-data-path'], widgetId, isLayoutWidget]);
1981
2051
  // Initialize default value only once on mount (skip for layout widgets)
1982
2052
  useEffect(() => {
1983
2053
  if (isLayoutWidget) {
@@ -2000,6 +2070,20 @@ const useBaseWidget = (options) => {
2000
2070
  if (currentValue === newValue) {
2001
2071
  return;
2002
2072
  }
2073
+ // CRITICAL FIX: Ignore auto-clears (empty string or undefined) from UI components
2074
+ // when the widget's data source is currently loading OR if options are empty.
2075
+ // This prevents data disappearance when switching to Edit mode and components
2076
+ // incorrectly clear values before options load or if handler is temporarily missing.
2077
+ if (newValue === '' || newValue === null || newValue === undefined) {
2078
+ if (loadingRef.current) {
2079
+ console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because data source is loading`);
2080
+ return;
2081
+ }
2082
+ if (config['widget-data-source']?.type === 'api' && dataSourceOptionsRef.current.length === 0) {
2083
+ console.warn(`[useBaseWidget] Ignoring empty value for ${widgetId} because API options are empty`);
2084
+ return;
2085
+ }
2086
+ }
2003
2087
  // Mark that user has set a value (unless this is the default initialization)
2004
2088
  if (newValue !== config['widget-data-default'] || userHasSetValueRef.current) {
2005
2089
  userHasSetValueRef.current = true;
@@ -2017,6 +2101,13 @@ const useBaseWidget = (options) => {
2017
2101
  }
2018
2102
  else {
2019
2103
  // Has dataPath: update both widgetId and dataPath
2104
+ // CRITICAL: For geo widgets, we do NOT want to overwrite the shared hierarchy dataPath
2105
+ // with a primitive value (the selected ID). The hierarchy object is managed by useGeoWidgetCascade.
2106
+ if (config['widget-geo-config']) {
2107
+ dispatch(setValue({ widgetId, value: newValue }));
2108
+ return;
2109
+ }
2110
+ // For non-geo widgets, update both widgetId and dataPath
2020
2111
  // CRITICAL: Create updated values object with newValue already set
2021
2112
  // This prevents setWidgetValue from reading stale values
2022
2113
  const currentValuesWithUpdate = {
@@ -2111,6 +2202,17 @@ const useBaseWidget = (options) => {
2111
2202
  const apiService = dataSource?.type === 'api' ? dataSource.service : '';
2112
2203
  const apiEndpoint = dataSource?.type === 'api' ? dataSource.endpoint : '';
2113
2204
  const configKey = `${widgetId}-${isReadonly}-${dataSource?.type || 'none'}-${apiService}-${apiEndpoint}`;
2205
+ // Extract dependency value using a granular selector to prevent unnecessary re-renders
2206
+ // and infinite loops when other unrelated values in the state change.
2207
+ const dependencyValue = useSelector((state) => {
2208
+ if (dataSource?.type !== 'api' || !dataSource.dependsOn) {
2209
+ return null;
2210
+ }
2211
+ if (dataSource.dependsOn.includes('.')) {
2212
+ return getWidgetValue(state.widget.values, dataSource.dependsOn, '');
2213
+ }
2214
+ return state.widget.values[dataSource.dependsOn];
2215
+ });
2114
2216
  // Handle data source loading
2115
2217
  useEffect(() => {
2116
2218
  if (!dataSource) {
@@ -2131,6 +2233,17 @@ const useBaseWidget = (options) => {
2131
2233
  }
2132
2234
  else {
2133
2235
  depValue = values[dataSource.dependsOn];
2236
+ // Smart resolution: If not found at top level, and current widget has a nested dataPath,
2237
+ // try to find the dependency in the same nested object.
2238
+ if ((depValue === undefined || depValue === null || depValue === '') &&
2239
+ typeof config['widget-data-path'] === 'string' &&
2240
+ config['widget-data-path'].includes('.')) {
2241
+ const pathParts = config['widget-data-path'].split('.');
2242
+ pathParts.pop(); // Remove current field name
2243
+ const prefix = pathParts.join('.');
2244
+ const tryPath = `${prefix}.${dataSource.dependsOn}`;
2245
+ depValue = getWidgetValue(values, tryPath, '');
2246
+ }
2134
2247
  }
2135
2248
  // If dependency is empty, don't load (will load when dependency has value)
2136
2249
  if (depValue === null || depValue === undefined || depValue === '') {
@@ -2164,7 +2277,7 @@ const useBaseWidget = (options) => {
2164
2277
  }
2165
2278
  // Extract level_id from widget-geo-config.level if available
2166
2279
  const levelId = geoConfig?.level;
2167
- data = await getApiDataSource(dataSource, values, currentHandler, levelId);
2280
+ data = await getApiDataSource(dataSource, valuesRef.current, currentHandler, levelId);
2168
2281
  }
2169
2282
  else if (dataSource.type === 'schema') {
2170
2283
  data = getSchemaDataSource(dataSource, schemaData || {});
@@ -2199,9 +2312,9 @@ const useBaseWidget = (options) => {
2199
2312
  }
2200
2313
  };
2201
2314
  loadDataSource();
2202
- // Use configKey to ensure effect runs when readonly state changes
2315
+ // Use configKey and dependencyValue to ensure effect runs only when relevant state changes
2203
2316
  // eslint-disable-next-line react-hooks/exhaustive-deps
2204
- }, [configKey, values, dataSourceRequestHandler, schemaData, widgetId, dispatch]);
2317
+ }, [configKey, dependencyValue, dataSourceRequestHandler, schemaData, widgetId, dispatch]);
2205
2318
  return {
2206
2319
  widgetId,
2207
2320
  value: currentValue,
@@ -2402,6 +2515,9 @@ const useGeoWidgetCascade = (options) => {
2402
2515
  const geoConfig = config['widget-geo-config'];
2403
2516
  const dataSource = config['widget-data-source'];
2404
2517
  const dataPath = config['widget-data-path'];
2518
+ const groupId = typeof dataPath === 'string' && dataPath.includes('.')
2519
+ ? dataPath.split('.').slice(0, -1).join('.')
2520
+ : 'default';
2405
2521
  const valuesRef = useRef(values);
2406
2522
  const handlerRef = useRef(dataSourceRequestHandler);
2407
2523
  // Keep refs updated
@@ -2411,10 +2527,36 @@ const useGeoWidgetCascade = (options) => {
2411
2527
  }, [values, dataSourceRequestHandler]);
2412
2528
  // Get current value and data source options
2413
2529
  const currentValue = useSelector((state) => {
2414
- if (!dataPath) {
2415
- return state.widget.values[widgetId];
2530
+ // Try to get value from widgetId first (most recent selection)
2531
+ let value = state.widget.values[widgetId];
2532
+ // If not found in widgetId, try dataPath
2533
+ if (value === undefined && dataPath) {
2534
+ value = getWidgetValue(state.widget.values, dataPath, widgetId);
2535
+ }
2536
+ // Extract value if it's a geo hierarchy object
2537
+ if (value && typeof value === 'object' && !Array.isArray(value) && geoConfig) {
2538
+ const hierarchy = value.hierarchy || value.geo_code_hierarchy_json?.hierarchy;
2539
+ if (Array.isArray(hierarchy)) {
2540
+ const levelData = hierarchy.find((l) => l.level === geoConfig.level);
2541
+ if (levelData) {
2542
+ return levelData.level_value_id;
2543
+ }
2544
+ }
2545
+ // Extended fallbacks (matching useBaseWidget)
2546
+ if ('geo_lowest_level_value_id' in value) {
2547
+ return value.geo_lowest_level_value_id;
2548
+ }
2549
+ if ('lowest_level_value_id' in value) {
2550
+ return value.lowest_level_value_id;
2551
+ }
2552
+ if (value.geo_code_hierarchy_json?.lowest_level_value_id) {
2553
+ return value.geo_code_hierarchy_json.lowest_level_value_id;
2554
+ }
2555
+ if (value.geo_code_hierarchy_json?.geo_lowest_level_value_id) {
2556
+ return value.geo_code_hierarchy_json.geo_lowest_level_value_id;
2557
+ }
2416
2558
  }
2417
- return getWidgetValue(state.widget.values, dataPath, widgetId);
2559
+ return value;
2418
2560
  });
2419
2561
  // Memoize selector to avoid returning new array reference
2420
2562
  const dataSourceOptions = useSelector((state) => state.widget.dataSources[widgetId] ?? EMPTY_DATA_SOURCE);
@@ -2434,11 +2576,17 @@ const useGeoWidgetCascade = (options) => {
2434
2576
  await new Promise(resolve => setTimeout(resolve, 0));
2435
2577
  const currentValues = valuesRef.current;
2436
2578
  const currentHandler = handlerRef.current;
2437
- // CRITICAL: Get the parent value from Redux state, not from the event
2438
- // The event.value might be stale, but Redux state is always current
2439
- const parentValue = currentValues[parentWidgetId];
2579
+ // CRITICAL: Try to get parent value from event first, then from Redux
2580
+ let parentValue = event.value;
2581
+ if (parentValue === undefined || parentValue === null) {
2582
+ parentValue = currentValues[parentWidgetId];
2583
+ // If not found in top-level values, try to find it via dataPath or dependsOn
2584
+ if (parentValue === undefined && dataSource.dependsOn) {
2585
+ parentValue = getWidgetValue(currentValues, dataSource.dependsOn, '');
2586
+ }
2587
+ }
2440
2588
  // Remove this level and all below from hierarchy
2441
- geoHierarchyBuilder.removeLevelAndBelow(level);
2589
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2442
2590
  // Clear this widget's value
2443
2591
  // CRITICAL: Only dispatch setValue for THIS widget, not for parent or other widgets
2444
2592
  // setWidgetValue returns the entire updated state, but we only want to update this widget
@@ -2493,7 +2641,8 @@ const useGeoWidgetCascade = (options) => {
2493
2641
  }
2494
2642
  }
2495
2643
  else {
2496
- // If parent value is cleared, clear the data source
2644
+ // If parent value is cleared, clear the data source and hierarchy
2645
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2497
2646
  dispatch(setDataSource({ widgetId, data: [] }));
2498
2647
  }
2499
2648
  };
@@ -2508,25 +2657,49 @@ const useGeoWidgetCascade = (options) => {
2508
2657
  if (!geoConfig) {
2509
2658
  return;
2510
2659
  }
2511
- // Skip if value is empty/null (but allow 0 and false)
2512
- if (currentValue === null || currentValue === undefined || currentValue === '') {
2513
- // If value was cleared, remove this level and below from hierarchy
2660
+ // Skip if value is undefined (it might still be loading or rehydrating)
2661
+ // ONLY clear hierarchy if the value is explicitly null or empty string (user action)
2662
+ if (currentValue === null || currentValue === '') {
2514
2663
  const { level } = geoConfig;
2515
- geoHierarchyBuilder.removeLevelAndBelow(level);
2664
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2665
+ // If we have a dataPath, we need to update Redux with the cleared hierarchy
2666
+ if (dataPath) {
2667
+ const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2668
+ let finalUpdatedValues = valuesRef.current;
2669
+ // Use logic similar to the build section below to update the dataPath
2670
+ if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2671
+ const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2672
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2673
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson?.geo_lowest_level_value_id);
2674
+ }
2675
+ else {
2676
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson?.geo_code_hierarchy_json);
2677
+ }
2678
+ dispatch(setValues(finalUpdatedValues));
2679
+ }
2516
2680
  return;
2517
2681
  }
2682
+ if (currentValue === undefined) {
2683
+ return; // Skip if undefined (still initializing)
2684
+ }
2518
2685
  const { level, isLastLevel } = geoConfig;
2519
- // For last level, check if hierarchy is already built to prevent endless loops
2520
- if (isLastLevel && dataPath) {
2686
+ // Check if hierarchy is already built to prevent endless loops
2687
+ if (dataPath) {
2521
2688
  const currentHierarchy = getWidgetValue(valuesRef.current, dataPath, widgetId);
2522
2689
  // If hierarchy JSON is already set and matches current value, skip rebuilding
2523
- if (currentHierarchy && typeof currentHierarchy === 'object' && currentHierarchy.geo_code_hierarchy_json) {
2524
- // Check if the lowest level value matches
2525
- const currentLevelValue = typeof currentValue === 'object'
2526
- ? (currentValue.level_value_id || currentValue.id || currentValue.value)
2527
- : currentValue;
2528
- if (currentHierarchy.geo_lowest_level_value_id === currentLevelValue) {
2529
- return; // Hierarchy already built for this value, skip
2690
+ if (currentHierarchy && typeof currentHierarchy === 'object') {
2691
+ // Check if this specific level's value matches the hierarchy
2692
+ const hierarchyArray = currentHierarchy.hierarchy || currentHierarchy.geo_code_hierarchy_json?.hierarchy;
2693
+ if (Array.isArray(hierarchyArray)) {
2694
+ const currentLevelValue = typeof currentValue === 'object'
2695
+ ? (currentValue.level_value_id || currentValue.id || currentValue.value)
2696
+ : currentValue;
2697
+ const levelData = hierarchyArray.find((l) => l.level === geoConfig.level);
2698
+ // If this level is already correctly represented in the hierarchy, skip rebuilding
2699
+ // String conversion ensures comparison works for mixed types
2700
+ if (levelData && String(levelData.level_value_id) === String(currentLevelValue)) {
2701
+ return;
2702
+ }
2530
2703
  }
2531
2704
  }
2532
2705
  }
@@ -2549,23 +2722,31 @@ const useGeoWidgetCascade = (options) => {
2549
2722
  return;
2550
2723
  }
2551
2724
  // When a widget's own value changes, remove this level and all below from hierarchy first
2552
- // This ensures that when level 1 changes, we clear the hierarchy and rebuild from scratch
2553
- // The addLevel method already handles removing existing levels, but we explicitly clear to be safe
2554
- geoHierarchyBuilder.removeLevelAndBelow(level);
2725
+ geoHierarchyBuilder.removeLevelAndBelow(level, groupId);
2555
2726
  // Add level to hierarchy
2556
- geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic);
2557
- // If this is the last level, build and store hierarchy JSON
2558
- if (isLastLevel && dataPath) {
2559
- const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson();
2727
+ geoHierarchyBuilder.addLevel(level, level_value_id, level_value_mnemonic, groupId);
2728
+ // Build and store hierarchy JSON on every change
2729
+ if (dataPath) {
2730
+ const hierarchyJson = geoHierarchyBuilder.buildHierarchyJson(groupId);
2560
2731
  if (hierarchyJson) {
2561
- // Store both geo_lowest_level_value_id and geo_code_hierarchy_json
2562
- const updatedValues = setWidgetValue(valuesRef.current, dataPath, widgetId, {
2563
- geo_lowest_level_value_id: hierarchyJson.geo_lowest_level_value_id,
2564
- geo_code_hierarchy_json: hierarchyJson.geo_code_hierarchy_json,
2565
- });
2566
- Object.entries(updatedValues).forEach(([key, value]) => {
2567
- dispatch(setValue({ widgetId: key, value }));
2568
- });
2732
+ // Fix: Avoid double nesting of geo_code_hierarchy_json
2733
+ // If dataPath ends with .geo_code_hierarchy_json, we want to save the content directly to it
2734
+ // and save the lowest level ID as a sibling
2735
+ let finalUpdatedValues = valuesRef.current;
2736
+ if (typeof dataPath === 'string' && dataPath.endsWith('.geo_code_hierarchy_json')) {
2737
+ const prefix = dataPath.substring(0, dataPath.lastIndexOf('.'));
2738
+ // Save hierarchy JSON content directly to dataPath (avoiding double nesting)
2739
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2740
+ // Save lowest level ID as sibling
2741
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, `${prefix}.geo_lowest_level_value_id`, widgetId, hierarchyJson.geo_lowest_level_value_id);
2742
+ }
2743
+ else {
2744
+ // Fallback if path doesn't follow the naming convention
2745
+ finalUpdatedValues = setWidgetValue(finalUpdatedValues, dataPath, widgetId, hierarchyJson.geo_code_hierarchy_json);
2746
+ }
2747
+ // CRITICAL: Use setValues for deep merge instead of replacing root keys with setValue
2748
+ // setWidgetValue returns the complete updated state object with all keys preserved
2749
+ dispatch(setValues(finalUpdatedValues));
2569
2750
  }
2570
2751
  }
2571
2752
  }, [geoConfig, currentValue, widgetId, dataPath, dispatch, dataSourceOptions]);
@@ -3628,7 +3809,15 @@ const collectWidgets = (panels) => {
3628
3809
  });
3629
3810
  return widgets;
3630
3811
  };
3631
- const sectionValidate = (section, currentSchemaData, dispatch) => {
3812
+ /**
3813
+ * Validate all widgets in a section and dispatch errors to the store.
3814
+ *
3815
+ * @param skipRequired - When true, required-field checks (widget-required,
3816
+ * validation.required, document-required) are skipped. Use this for
3817
+ * per-section Save/Next navigation so the user can advance without filling
3818
+ * every mandatory field; only format/range errors are reported.
3819
+ */
3820
+ const sectionValidate = (section, currentSchemaData, dispatch, skipRequired = false) => {
3632
3821
  const allWidgets = collectWidgets(section.panels);
3633
3822
  let isValid = true;
3634
3823
  for (const widget of allWidgets) {
@@ -3637,7 +3826,7 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
3637
3826
  continue;
3638
3827
  const widgetId = widget['widget-id'];
3639
3828
  const value = getWidgetValue(currentSchemaData, widget['widget-data-path'], widgetId);
3640
- const errors = validateWidget(value, widget['widget-data-validation'], widget['widget-required']);
3829
+ const errors = validateWidget(value, widget['widget-data-validation'], widget['widget-required'], skipRequired);
3641
3830
  if (errors.length > 0) {
3642
3831
  isValid = false;
3643
3832
  dispatch(setTouched({ widgetId, touched: true }));
@@ -3648,10 +3837,10 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
3648
3837
  dispatch(setError({ widgetId, errors: [] }));
3649
3838
  }
3650
3839
  }
3651
- // Supporting Documents
3840
+ // Supporting Documents — only check required when not skipping required validation
3652
3841
  section['section-supporting-documents']?.forEach((doc, index) => {
3653
3842
  const widgetId = `supporting-doc-${section['section-id']}-${index}`;
3654
- if (doc['document-required']) {
3843
+ if (!skipRequired && doc['document-required']) {
3655
3844
  const file = getValueByPath(currentSchemaData, doc['document-data-path']);
3656
3845
  if (!file) {
3657
3846
  isValid = false;
@@ -3670,6 +3859,85 @@ const sectionValidate = (section, currentSchemaData, dispatch) => {
3670
3859
  return isValid;
3671
3860
  };
3672
3861
 
3862
+ /** Table-style widgets that bind to an array path in the store / schema. */
3863
+ function isTableLikeWidget(widget) {
3864
+ const w = widget.widget;
3865
+ /** widget-type union in types omits legacy values like simple-table still used at runtime */
3866
+ const t = widget['widget-type'];
3867
+ return (w === 'table' ||
3868
+ w === 'dialog-table' ||
3869
+ w === 'simple-table' ||
3870
+ t === 'table' ||
3871
+ t === 'simple-table');
3872
+ }
3873
+ /**
3874
+ * Resolve `records` for section save payloads.
3875
+ * - Back-compat: path ending in `.records` (e.g. `regId.records`)
3876
+ * - Else: first array snapshot at a string `widget-data-path` on a table-like widget
3877
+ * (e.g. `household.members` for dialog-table)
3878
+ */
3879
+ function extractTableRecordsFromSnapshot(snapshot, sectionWidgets) {
3880
+ const convention = Object.entries(snapshot).find(([key, value]) => key.endsWith('.records') && Array.isArray(value));
3881
+ if (convention) {
3882
+ return convention[1];
3883
+ }
3884
+ const tablePaths = [];
3885
+ sectionWidgets.forEach((widget) => {
3886
+ if (!isTableLikeWidget(widget))
3887
+ return;
3888
+ const p = widget['widget-data-path'];
3889
+ if (typeof p === 'string' && p.length > 0) {
3890
+ tablePaths.push(p);
3891
+ }
3892
+ else if (p && typeof p === 'object') {
3893
+ Object.values(p).forEach((sub) => {
3894
+ if (typeof sub === 'string' && sub.length > 0)
3895
+ tablePaths.push(sub);
3896
+ });
3897
+ }
3898
+ });
3899
+ for (const path of tablePaths) {
3900
+ const val = snapshot[path];
3901
+ if (Array.isArray(val)) {
3902
+ return val;
3903
+ }
3904
+ }
3905
+ return [];
3906
+ }
3907
+
3908
+ /** Root class on readonly label/value rows; SectionRenderer scopes overflow/ellipsis rules here. */
3909
+ const READONLY_VALUE_ROW_ROOT_CLASSES = [
3910
+ 'TextDisplayWidget',
3911
+ 'TextAreaDisplayWidget',
3912
+ 'SelectDisplayWidget',
3913
+ 'PhoneDisplayWidget',
3914
+ 'NumberDisplayWidget',
3915
+ 'CurrencyDisplayWidget',
3916
+ 'RadioDisplayWidget',
3917
+ 'DateDisplayWidget',
3918
+ 'DateTimeDisplayWidget',
3919
+ 'CheckboxDisplayWidget',
3920
+ 'BooleanDisplayWidget',
3921
+ 'FileDisplayWidget',
3922
+ 'DisplayFieldWidget',
3923
+ ];
3924
+ /** Rows whose value is one line in .flex-1 > .text-gray-900 (ellipsis; full string via title on the element). */
3925
+ const READONLY_SINGLE_LINE_VALUE_ROW_CLASSES = [
3926
+ 'TextDisplayWidget',
3927
+ 'SelectDisplayWidget',
3928
+ 'PhoneDisplayWidget',
3929
+ 'NumberDisplayWidget',
3930
+ 'CurrencyDisplayWidget',
3931
+ 'RadioDisplayWidget',
3932
+ 'DateDisplayWidget',
3933
+ 'DateTimeDisplayWidget',
3934
+ 'CheckboxDisplayWidget',
3935
+ 'BooleanDisplayWidget',
3936
+ 'DisplayFieldWidget',
3937
+ ];
3938
+ function scopedClassSelectors(sectionClassId, classNames) {
3939
+ return classNames.map((c) => `.${sectionClassId} .${c}`).join(',\n ');
3940
+ }
3673
3941
  /**
3674
3942
  * Renders a section with its panels
3675
3943
  *
@@ -3698,41 +3966,22 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
3698
3966
  }
3699
3967
  return section;
3700
3968
  }, [section, namespace]);
3701
- // Create namespaced schemaData if namespace is provided
3702
- // This ensures widgets can read initial values from schemaData at namespaced paths
3969
+ // Create namespaced schemaData if namespace is provided.
3970
+ // Widgets with namespaced data-paths (e.g. "rv-section-0.a1a4d25a.birth_date")
3971
+ // need a nested object at values[namespace] so getValueByPath can traverse it.
3703
3972
  const namespacedSchemaData = useMemo(() => {
3704
3973
  if (!namespace || !currentSchemaData) {
3705
3974
  return schemaData;
3706
3975
  }
3707
- // Create a namespaced version of schemaData by copying values to namespaced paths
3708
- const namespaced = { ...currentSchemaData };
3709
- // Copy all top-level keys to namespaced paths
3710
- Object.keys(currentSchemaData).forEach(key => {
3711
- const namespacedKey = `${namespace}.${key}`;
3712
- if (!(namespacedKey in namespaced)) {
3713
- namespaced[namespacedKey] = currentSchemaData[key];
3714
- }
3715
- });
3716
- // Also handle nested objects - copy nested values to namespaced paths
3717
- const copyNestedValues = (obj, prefix = '') => {
3718
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
3719
- Object.keys(obj).forEach(key => {
3720
- const fullPath = prefix ? `${prefix}.${key}` : key;
3721
- const namespacedPath = `${namespace}.${fullPath}`;
3722
- if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
3723
- copyNestedValues(obj[key], fullPath);
3724
- // Also set the nested object at the namespaced path
3725
- setValueByPath(namespaced, namespacedPath, obj[key]);
3726
- }
3727
- else {
3728
- setValueByPath(namespaced, namespacedPath, obj[key]);
3729
- }
3730
- });
3731
- }
3732
- };
3733
- copyNestedValues(currentSchemaData);
3734
- return namespaced;
3976
+ return { ...currentSchemaData, [namespace]: currentSchemaData };
3735
3977
  }, [namespace, schemaData, currentSchemaData]);
3978
+ // Populate the store with namespaced schema data so that namespaced widgets
3979
+ // can read their initial values via getValueByPath on the namespaced paths.
3980
+ useEffect(() => {
3981
+ if (namespace && namespacedSchemaData) {
3982
+ dispatch(setValues(namespacedSchemaData));
3983
+ }
3984
+ }, [namespace, namespacedSchemaData, dispatch]);
3736
3985
  const crViewData = useMemo(() => {
3737
3986
  if (mode !== 'CRView')
3738
3987
  return null;
@@ -3755,21 +4004,25 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
3755
4004
  const sectionId = sectionToRender['section-id'];
3756
4005
  const gridId = `section-panels-${sectionId}`;
3757
4006
  const sectionClassId = `section-${sectionId}`;
4007
+ const readonlyValueRowRootsCss = useMemo(() => scopedClassSelectors(sectionClassId, READONLY_VALUE_ROW_ROOT_CLASSES), [sectionClassId]);
4008
+ const readonlyValueRowFlex1Css = useMemo(() => READONLY_VALUE_ROW_ROOT_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1`).join(',\n '), [sectionClassId]);
4009
+ const readonlySingleLineValueTextCss = useMemo(() => READONLY_SINGLE_LINE_VALUE_ROW_CLASSES.map((c) => `.${sectionClassId} .${c} > .flex-1 > .text-gray-900`).join(',\n '), [sectionClassId]);
3758
4010
  // IntakeForm mode: accordion expand/collapse state (supports toggle)
3759
4011
  const [standaloneExpanded, setStandaloneExpanded] = useState(true); // For sectionIndex undefined (standalone use)
3760
4012
  const isExpandedFromContainer = typeof sectionIndex === 'number' && expandedSectionIndex === sectionIndex;
3761
4013
  const isExpandedStandalone = sectionIndex === undefined && standaloneExpanded;
3762
4014
  const isExpanded = mode === 'IntakeForm' && (isExpandedFromContainer || isExpandedStandalone);
4015
+ // When the section is managed by SectionsContainer (sectionIndex is set), header clicks are
4016
+ // disabled — navigation is only allowed via the Next / Previous buttons.
4017
+ // Standalone usage (sectionIndex undefined) keeps the classic toggle behaviour.
3763
4018
  const handleAccordionToggle = useCallback(() => {
3764
4019
  if (mode !== 'IntakeForm')
3765
4020
  return;
3766
- if (typeof sectionIndex === 'number' && onExpandSection) {
3767
- onExpandSection(sectionIndex);
3768
- }
3769
- else if (sectionIndex === undefined) {
4021
+ if (sectionIndex === undefined) {
3770
4022
  setStandaloneExpanded(prev => !prev);
3771
4023
  }
3772
- }, [mode, sectionIndex, onExpandSection]);
4024
+ // Intentionally no-op when sectionIndex is set (managed by SectionsContainer)
4025
+ }, [mode, sectionIndex]);
3773
4026
  // Recursively count all vertical panels, especially those nested inside horizontal panels
3774
4027
  // Typically: horizontal panels at first level contain vertical panels at second level
3775
4028
  // Accounts for panel-column-span: a panel with column-span 3 counts as 3 columns
@@ -4018,9 +4271,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4018
4271
  const widgetPath = widget['widget-data-path'];
4019
4272
  if (!widgetPath)
4020
4273
  return;
4021
- if (widget['widget-type'] === 'table' ||
4022
- widget['widget-type'] === 'simple-table' ||
4023
- widget['widget'] === 'table') {
4274
+ if (isTableLikeWidget(widget)) {
4024
4275
  hasTable = true;
4025
4276
  }
4026
4277
  if (typeof widgetPath === 'object') {
@@ -4055,8 +4306,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4055
4306
  },
4056
4307
  ];
4057
4308
  }
4058
- const tableEntry = Object.entries(snapshot).find(([key, value]) => key.endsWith('.records') && Array.isArray(value));
4059
- return tableEntry ? tableEntry[1] : [];
4309
+ return extractTableRecordsFromSnapshot(snapshot, widgets);
4060
4310
  };
4061
4311
  // Get original section (without namespace) for building snapshots
4062
4312
  // This ensures we use the original data paths when saving
@@ -4107,6 +4357,8 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4107
4357
  const baselineSnapshotRef = useRef(null);
4108
4358
  // IntakeForm only: increment when baseline is updated after save - forces badge to update (refs don't trigger re-renders)
4109
4359
  const [intakeFormBaselineTrigger, setIntakeFormBaselineTrigger] = useState(0);
4360
+ // IntakeForm only: tracks whether the user has actually saved this section (prevents "Saved" badge on initial load)
4361
+ const [hasBeenSavedByUser, setHasBeenSavedByUser] = useState(false);
4110
4362
  // IntakeForm: treat as edit mode for dirty tracking when isDraft. RegistryView: use isEditMode.
4111
4363
  const effectiveEditModeForDirty = mode === 'IntakeForm' ? (isDraft !== false) : isEditMode;
4112
4364
  // Compute isDirty: compare current store state to baseline (only when in edit mode)
@@ -4149,18 +4401,68 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4149
4401
  const intakeFormSectionStatus = useMemo(() => {
4150
4402
  if (mode !== 'IntakeForm' || isDraft === false)
4151
4403
  return null;
4152
- const hasValue = (v) => v !== undefined && v !== null && (typeof v !== 'string' || v.trim().length > 0);
4153
- const currentSnapshot = buildSectionSnapshot(storeValues, namespace);
4154
- const record = currentSnapshot.records?.[0];
4155
- const hasData = record &&
4156
- typeof record === 'object' &&
4157
- Object.values(record).some((v) => hasValue(v));
4158
4404
  if (isDirty)
4159
4405
  return 'modified';
4160
- if (hasData)
4406
+ if (hasBeenSavedByUser)
4161
4407
  return 'saved';
4162
4408
  return null;
4163
- }, [mode, isDirty, storeValues, namespace, buildSectionSnapshot]);
4409
+ }, [mode, isDirty, hasBeenSavedByUser]);
4410
+ // Revert store values to the original schemaData for this section's widgets.
4411
+ // Used by both handleSave (RegistryView raises a CR, so values should not persist)
4412
+ // and handleCancel.
4413
+ const revertToOriginalValues = useCallback(() => {
4414
+ const sectionWidgets = collectWidgets(originalSection.panels);
4415
+ const oldSchemaData = schemaData || contextSchemaData;
4416
+ const currentStoreValues = store.getState().widget.values;
4417
+ let newStoreValues = currentStoreValues;
4418
+ sectionWidgets.forEach(widget => {
4419
+ const originalWidgetId = widget['widget-id'];
4420
+ const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
4421
+ const widgetId = namespacedWidgetId;
4422
+ const originalDataPath = widget['widget-data-path'];
4423
+ const storeDataPath = namespace && originalDataPath
4424
+ ? (typeof originalDataPath === 'string'
4425
+ ? `${namespace}.${originalDataPath}`
4426
+ : Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
4427
+ : originalDataPath;
4428
+ if (widgetId && originalDataPath) {
4429
+ let oldValue;
4430
+ if (typeof originalDataPath === 'object') {
4431
+ oldValue = {};
4432
+ Object.entries(originalDataPath).forEach(([key, path]) => {
4433
+ if (typeof path === 'string') {
4434
+ oldValue[key] = getValueByPath(oldSchemaData, path);
4435
+ }
4436
+ });
4437
+ }
4438
+ else if (typeof originalDataPath === 'string') {
4439
+ oldValue = getValueByPath(oldSchemaData, originalDataPath);
4440
+ }
4441
+ if (oldValue !== undefined) {
4442
+ newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4443
+ // Also revert the widgetId-based entry — useBaseWidget.handleChange
4444
+ // sets values[widgetId] during editing, and useBaseWidget.currentValue
4445
+ // reads values[widgetId] first before falling through to the dataPath.
4446
+ newStoreValues = { ...newStoreValues, [widgetId]: oldValue };
4447
+ }
4448
+ }
4449
+ });
4450
+ if (hasSupportingDocuments) {
4451
+ const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
4452
+ originalSupportingDocuments.forEach((doc, index) => {
4453
+ const widgetId = `supporting-doc-${sectionId}-${index}`;
4454
+ const originalDataPath = doc['document-data-path'];
4455
+ const storeDataPath = namespace && originalDataPath
4456
+ ? `${namespace}.${originalDataPath}`
4457
+ : originalDataPath;
4458
+ const oldValue = getValueByPath(oldSchemaData, originalDataPath);
4459
+ newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4460
+ });
4461
+ }
4462
+ if (newStoreValues !== currentStoreValues) {
4463
+ dispatch(setValues(newStoreValues));
4464
+ }
4465
+ }, [originalSection, schemaData, contextSchemaData, store, namespace, hasSupportingDocuments, sectionId, dispatch]);
4164
4466
  // Handle save button click
4165
4467
  const handleSave = async () => {
4166
4468
  if (!store || !onSectionSave) {
@@ -4174,7 +4476,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4174
4476
  const sectionWidgets = collectWidgets(originalSection.panels);
4175
4477
  const currentState = store.getState().widget;
4176
4478
  const currentSchemaData = currentState.values || {};
4177
- const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch);
4479
+ const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch, true);
4178
4480
  if (!isSectionValid) {
4179
4481
  return;
4180
4482
  }
@@ -4197,12 +4499,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4197
4499
  });
4198
4500
  }
4199
4501
  if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
4502
+ let profileImage = null;
4503
+ for (const record of newSchemaData) {
4504
+ if (typeof record === 'object' && record !== null) {
4505
+ for (const [key, value] of Object.entries(record)) {
4506
+ if (value instanceof File) {
4507
+ profileImage = value;
4508
+ record[key] = '';
4509
+ }
4510
+ }
4511
+ }
4512
+ }
4200
4513
  try {
4201
4514
  const sectionchanges = {
4202
4515
  section_id: dbSectionId ?? originalSection['section-id'],
4203
4516
  section_register_id: sectionRegisterId,
4204
4517
  records: [...newSchemaData],
4205
- files: [...sectionFiles]
4518
+ files: [...sectionFiles],
4519
+ ...(profileImage ? { image: profileImage } : {}),
4206
4520
  };
4207
4521
  await onSectionSave(sectionchanges);
4208
4522
  }
@@ -4210,6 +4524,12 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4210
4524
  console.error('Section Changes Save failed', error);
4211
4525
  }
4212
4526
  }
4527
+ // In RegistryView, save raises a CR — the actual data update follows a
4528
+ // separate approval workflow, so revert the displayed values to the
4529
+ // originals so the view doesn't show unapproved edits.
4530
+ if (mode === 'RegistryView') {
4531
+ revertToOriginalValues();
4532
+ }
4213
4533
  setIsEditMode(false);
4214
4534
  onEditModeChange?.(originalSectionId, false);
4215
4535
  };
@@ -4222,7 +4542,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4222
4542
  const sectionWidgets = collectWidgets(originalSection.panels);
4223
4543
  const currentState = store.getState().widget;
4224
4544
  const currentSchemaData = currentState.values || {};
4225
- const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch);
4545
+ const isSectionValid = sectionValidate(originalSection, currentSchemaData, dispatch, true);
4226
4546
  if (!isSectionValid)
4227
4547
  return;
4228
4548
  const oldSchemaData = schemaData || contextSchemaData;
@@ -4239,12 +4559,24 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4239
4559
  });
4240
4560
  }
4241
4561
  if (JSON.stringify(oldSchemaData) !== JSON.stringify(newSchemaData)) {
4562
+ let profileImage = null;
4563
+ for (const record of newSchemaData) {
4564
+ if (typeof record === 'object' && record !== null) {
4565
+ for (const [key, value] of Object.entries(record)) {
4566
+ if (value instanceof File) {
4567
+ profileImage = value;
4568
+ record[key] = '';
4569
+ }
4570
+ }
4571
+ }
4572
+ }
4242
4573
  try {
4243
4574
  await onSectionSave({
4244
4575
  section_id: dbSectionId ?? originalSection['section-id'],
4245
4576
  section_register_id: sectionRegisterId,
4246
4577
  records: [...newSchemaData],
4247
4578
  files: [...sectionFiles],
4579
+ ...(profileImage ? { image: profileImage } : {}),
4248
4580
  });
4249
4581
  }
4250
4582
  catch (error) {
@@ -4255,6 +4587,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4255
4587
  if (mode === 'IntakeForm') {
4256
4588
  baselineSnapshotRef.current = buildSectionSnapshot(currentSchemaData, namespace);
4257
4589
  setIntakeFormBaselineTrigger((prev) => prev + 1);
4590
+ setHasBeenSavedByUser(true);
4258
4591
  }
4259
4592
  onSectionDirtyChange?.(sectionId, false);
4260
4593
  }
@@ -4263,64 +4596,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4263
4596
  }, [store, onSectionSave, onSectionSaveSuccess, sectionIndex, originalSection, schemaData, contextSchemaData, namespace, hasSupportingDocuments, dbSectionId, sectionRegisterId, dispatch, buildSectionSnapshot, sectionId, onSectionDirtyChange, mode, isDraft]);
4264
4597
  // Handle cancel button click
4265
4598
  const handleCancel = () => {
4266
- // Revert values in store to original schema data
4267
- // Use original section (without namespace) for collecting widgets
4268
- const sectionWidgets = collectWidgets(originalSection.panels);
4269
- const oldSchemaData = schemaData || contextSchemaData;
4270
- const currentStoreValues = store.getState().widget.values;
4271
- let newStoreValues = currentStoreValues;
4272
- sectionWidgets.forEach(widget => {
4273
- const originalWidgetId = widget['widget-id'];
4274
- // If namespace was used, we need to use namespaced widget ID and data path
4275
- const namespacedWidgetId = namespace ? `${namespace}__${originalWidgetId}` : originalWidgetId;
4276
- const widgetId = namespacedWidgetId;
4277
- const originalDataPath = widget['widget-data-path'];
4278
- // If namespace was used, data path in store is namespaced, but we read from original schema using original path
4279
- const storeDataPath = namespace && originalDataPath
4280
- ? (typeof originalDataPath === 'string'
4281
- ? `${namespace}.${originalDataPath}`
4282
- : Object.fromEntries(Object.entries(originalDataPath).map(([key, path]) => [key, `${namespace}.${path}`])))
4283
- : originalDataPath;
4284
- if (widgetId && originalDataPath) {
4285
- // Handle multi-path (object) or single path (string)
4286
- // Read from original schema data using original paths
4287
- let oldValue;
4288
- if (typeof originalDataPath === 'object') {
4289
- // Multi-path: get values for each path
4290
- oldValue = {};
4291
- Object.entries(originalDataPath).forEach(([key, path]) => {
4292
- if (typeof path === 'string') {
4293
- oldValue[key] = getValueByPath(oldSchemaData, path);
4294
- }
4295
- });
4296
- }
4297
- else if (typeof originalDataPath === 'string') {
4298
- oldValue = getValueByPath(oldSchemaData, originalDataPath);
4299
- }
4300
- // Set in store using namespaced data path (if namespace was used)
4301
- if (oldValue !== undefined) {
4302
- newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4303
- }
4304
- }
4305
- });
4306
- // Also revert supporting documents if any
4307
- if (hasSupportingDocuments) {
4308
- // Use original section's supporting documents to get original data paths
4309
- const originalSupportingDocuments = originalSection['section-supporting-documents'] || [];
4310
- originalSupportingDocuments.forEach((doc, index) => {
4311
- const widgetId = `supporting-doc-${sectionId}-${index}`;
4312
- const originalDataPath = doc['document-data-path'];
4313
- // If namespace was used, data path in store is namespaced
4314
- const storeDataPath = namespace && originalDataPath
4315
- ? `${namespace}.${originalDataPath}`
4316
- : originalDataPath;
4317
- const oldValue = getValueByPath(oldSchemaData, originalDataPath);
4318
- newStoreValues = setWidgetValue(newStoreValues, storeDataPath, widgetId, oldValue);
4319
- });
4320
- }
4321
- if (newStoreValues !== currentStoreValues) {
4322
- dispatch(setValues(newStoreValues));
4323
- }
4599
+ revertToOriginalValues();
4324
4600
  setIsEditMode(false);
4325
4601
  onEditModeChange?.(originalSectionId, false);
4326
4602
  };
@@ -4371,20 +4647,27 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4371
4647
  white-space: nowrap !important;
4372
4648
  }
4373
4649
  /* Readonly: prevent flex row from overflowing panel */
4374
- .${sectionClassId} .TextDisplayWidget {
4650
+ ${readonlyValueRowRootsCss} {
4375
4651
  min-width: 0 !important;
4376
4652
  overflow: hidden !important;
4377
4653
  }
4378
- .${sectionClassId} .TextDisplayWidget > .flex-1 {
4654
+ ${readonlyValueRowFlex1Css} {
4379
4655
  min-width: 0 !important;
4380
4656
  overflow: hidden !important;
4381
4657
  }
4382
- /* Readonly value text truncation */
4383
- .${sectionClassId} .TextDisplayWidget > .flex-1 > .text-gray-900 {
4658
+ /* Readonly value: single-line ellipsis; full value via title on the value node */
4659
+ ${readonlySingleLineValueTextCss} {
4384
4660
  overflow: hidden;
4385
4661
  text-overflow: ellipsis;
4386
4662
  white-space: nowrap;
4387
4663
  }
4664
+ /* Readonly textarea: break unbroken long tokens; title on pre keeps full text on hover */
4665
+ .${sectionClassId} .TextAreaDisplayWidget > .flex-1 > pre {
4666
+ min-width: 0;
4667
+ max-width: 100%;
4668
+ overflow-wrap: anywhere;
4669
+ word-break: break-word;
4670
+ }
4388
4671
 
4389
4672
  /* Only apply fixed height when in edit mode */
4390
4673
  .${sectionClassId}[data-edit-mode="true"] {
@@ -4508,6 +4791,10 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4508
4791
  gap: 0.5rem;
4509
4792
  }
4510
4793
 
4794
+
4795
+
4796
+
4797
+
4511
4798
  /* IntakeForm accordion */
4512
4799
  .${sectionClassId}.intake-form-accordion-item {
4513
4800
  border-color: var(--owt-color-border-light, #E4E4E4);
@@ -4522,13 +4809,17 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4522
4809
  .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header h2 {
4523
4810
  color: var(--owt-color-primary-dark, #F07B1A);
4524
4811
  }
4525
- .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header:hover {
4812
+ /* Hover / focus only shown when the header is actually interactive (standalone mode) */
4813
+ .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:hover {
4526
4814
  opacity: 0.85;
4527
4815
  }
4528
- .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header:focus-visible {
4816
+ .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="true"]:focus-visible {
4529
4817
  outline: 2px solid var(--owt-color-primary, #F5BB1A);
4530
4818
  outline-offset: 2px;
4531
4819
  }
4820
+ .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-header[data-interactive="false"]:focus-visible {
4821
+ outline: none;
4822
+ }
4532
4823
  .${sectionClassId}.intake-form-accordion-item .intake-form-accordion-content {
4533
4824
  padding-top: 8px;
4534
4825
  padding-bottom: 0px;
@@ -4571,7 +4862,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4571
4862
  }),
4572
4863
  }, children: mode === 'IntakeForm' ? (
4573
4864
  /* IntakeForm: accordion layout - header always visible, content only when expanded */
4574
- jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("button", { type: "button", id: `intake-form-accordion-header-${sectionId}`, className: "intake-form-accordion-header", onClick: handleAccordionToggle, "aria-expanded": isExpanded, "aria-controls": isExpanded ? `intake-form-accordion-content-${sectionId}` : undefined, style: {
4865
+ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("button", { type: "button", id: `intake-form-accordion-header-${sectionId}`, className: "intake-form-accordion-header", onClick: handleAccordionToggle, "aria-expanded": isExpanded, "aria-controls": isExpanded ? `intake-form-accordion-content-${sectionId}` : undefined, "data-interactive": sectionIndex === undefined ? 'true' : 'false', style: {
4575
4866
  width: '100%',
4576
4867
  display: 'flex',
4577
4868
  alignItems: 'flex-start',
@@ -4581,7 +4872,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4581
4872
  marginBottom: 0,
4582
4873
  background: 'none',
4583
4874
  border: 'none',
4584
- cursor: 'pointer',
4875
+ cursor: sectionIndex === undefined ? 'pointer' : 'default',
4585
4876
  textAlign: 'left',
4586
4877
  fontFamily: 'Roboto, sans-serif',
4587
4878
  }, children: [jsxRuntimeExports.jsxs("div", { style: { flex: 1, display: 'flex', alignItems: 'center', gap: '12px', minWidth: 0 }, children: [jsxRuntimeExports.jsx("h2", { className: "text-xl font-semibold", style: { margin: 0 }, children: sectionToRender['section-title']
@@ -4712,7 +5003,7 @@ const SectionRenderer = ({ section, dataSourceRequestHandler: propDataSourceRequ
4712
5003
  }, children: crViewData.approvedDate }))] })] })] })), mode === 'RegistryView' && !hideEditButton && (jsxRuntimeExports.jsx("hr", { className: "w-full", style: { height: '1px', marginTop: !isEditMode ? '10px' : 0, marginBottom: '14px', border: 'none', backgroundColor: 'var(--owt-color-border, #C4C4C4)' } })), mode === 'RegistryView' && !isEditMode && !hideEditButton && (jsxRuntimeExports.jsx("div", { className: "flex justify-center items-center", style: { marginBottom: '20px' }, children: jsxRuntimeExports.jsxs("button", { onClick: handleEdit, className: "font-normal inline-flex items-center gap-2 bg-transparent border-0 p-0 cursor-pointer hover:opacity-80", style: {
4713
5004
  fontFamily: 'Roboto, sans-serif',
4714
5005
  fontSize: '16px',
4715
- color: 'var(--owt-color-text-muted, #727474)'
5006
+ color: 'var(--owt-color-text-muted, #727474)',
4716
5007
  }, children: [translate('common.editDetails') || 'Edit Details', jsxRuntimeExports.jsx("img", { src: img$8, alt: "right-arrow", className: "w-3.5 h-3.5 brightness-0 opacity-50" })] }) }))] })] })) })] }));
4717
5008
  };
4718
5009
 
@@ -4729,10 +5020,7 @@ function buildSectionChanges(section, storeValues, namespace, options) {
4729
5020
  const widgetPath = widget['widget-data-path'];
4730
5021
  if (!widgetPath)
4731
5022
  return;
4732
- const widgetType = widget['widget-type'];
4733
- if (widgetType === 'table' ||
4734
- widgetType === 'simple-table' ||
4735
- widget.widget === 'table') {
5023
+ if (isTableLikeWidget(widget)) {
4736
5024
  hasTable = true;
4737
5025
  }
4738
5026
  if (typeof widgetPath === 'object') {
@@ -4770,8 +5058,7 @@ function buildSectionChanges(section, storeValues, namespace, options) {
4770
5058
  ];
4771
5059
  }
4772
5060
  else {
4773
- const tableEntry = Object.entries(snapshot).find(([key, value]) => key.endsWith('.records') && Array.isArray(value));
4774
- records = tableEntry ? tableEntry[1] : [];
5061
+ records = extractTableRecordsFromSnapshot(snapshot, sectionWidgets);
4775
5062
  }
4776
5063
  const files = [];
4777
5064
  const supportingDocs = section['section-supporting-documents'] || [];
@@ -4797,7 +5084,9 @@ const hasTableWidget = (panels) => {
4797
5084
  // Check widgets in this panel
4798
5085
  if (panel.widgets) {
4799
5086
  for (const widget of panel.widgets) {
4800
- if (widget.widget === 'table' || widget['widget-type'] === 'table') {
5087
+ if (widget.widget === 'table' ||
5088
+ widget.widget === 'dialog-table' ||
5089
+ widget['widget-type'] === 'table') {
4801
5090
  return true;
4802
5091
  }
4803
5092
  }
@@ -4819,7 +5108,9 @@ const getTableWidgetColumnSpan = (panels) => {
4819
5108
  // Check widgets in this panel
4820
5109
  if (panel.widgets) {
4821
5110
  for (const widget of panel.widgets) {
4822
- if (widget.widget === 'table' || widget['widget-type'] === 'table') {
5111
+ if (widget.widget === 'table' ||
5112
+ widget.widget === 'dialog-table' ||
5113
+ widget['widget-type'] === 'table') {
4823
5114
  // Return the widget's column span if specified, otherwise null
4824
5115
  return widget['widget-column-span'] || null;
4825
5116
  }
@@ -4886,6 +5177,13 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4886
5177
  }, []);
4887
5178
  const safeSections = sections ?? [];
4888
5179
  const prevSectionsLengthRef = useRef(safeSections.length);
5180
+ // Stable ref for namespace so formHandle useMemo doesn't depend on the (possibly inline) function identity
5181
+ const namespaceRef = useRef(namespace);
5182
+ namespaceRef.current = namespace;
5183
+ // Stable refs so formHandle closure can access current mode and accordion setter without stale captures
5184
+ const modeRef = useRef(mode);
5185
+ modeRef.current = mode;
5186
+ const setExpandedSectionIndexRef = useRef(setExpandedSectionIndex);
4889
5187
  // Track dirty (unsaved changes) per section for form handle validation
4890
5188
  const sectionDirtyMapRef = useRef({});
4891
5189
  const handleSectionDirtyChange = useCallback((sectionId, isDirty) => {
@@ -4931,11 +5229,14 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4931
5229
  // Form handle for onFormReady - allows host to validate and get all section data from its own Submit button
4932
5230
  const formHandle = useMemo(() => {
4933
5231
  const getValues = () => store.getState().widget?.values || {};
4934
- const getNamespace = (section, index) => namespace
4935
- ? typeof namespace === 'string'
4936
- ? namespace
4937
- : namespace(section['section-id'], index)
4938
- : undefined;
5232
+ const getNamespace = (section, index) => {
5233
+ const ns = namespaceRef.current;
5234
+ return ns
5235
+ ? typeof ns === 'string'
5236
+ ? ns
5237
+ : ns(section['section-id'], index)
5238
+ : undefined;
5239
+ };
4939
5240
  const checkNoUnsavedChanges = () => {
4940
5241
  const hasDirty = Object.values(sectionDirtyMapRef.current).some(Boolean);
4941
5242
  if (hasDirty) {
@@ -4944,33 +5245,52 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4944
5245
  };
4945
5246
  return {
4946
5247
  validate: async () => {
4947
- checkNoUnsavedChanges();
5248
+ if (modeRef.current !== 'IntakeForm')
5249
+ checkNoUnsavedChanges();
4948
5250
  const values = getValues();
4949
5251
  let allValid = true;
5252
+ let firstInvalidIndex = null;
4950
5253
  for (let i = 0; i < safeSections.length; i++) {
4951
5254
  const section = safeSections[i];
4952
5255
  const ns = getNamespace(section, i);
4953
5256
  const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
4954
5257
  const valid = sectionValidate(sectionToValidate, values, dispatch);
4955
- if (!valid)
5258
+ if (!valid) {
5259
+ if (firstInvalidIndex === null)
5260
+ firstInvalidIndex = i;
4956
5261
  allValid = false;
5262
+ }
5263
+ }
5264
+ if (!allValid && modeRef.current === 'IntakeForm' && firstInvalidIndex !== null) {
5265
+ setExpandedSectionIndexRef.current(firstInvalidIndex);
4957
5266
  }
4958
5267
  return allValid;
4959
5268
  },
4960
5269
  getFormData: () => getValues(),
4961
5270
  validateAndGetData: async () => {
4962
- checkNoUnsavedChanges();
5271
+ if (modeRef.current !== 'IntakeForm')
5272
+ checkNoUnsavedChanges();
4963
5273
  const values = getValues();
4964
5274
  const results = [];
5275
+ let firstInvalidIndex = null;
4965
5276
  for (let i = 0; i < safeSections.length; i++) {
4966
5277
  const section = safeSections[i];
4967
5278
  const ns = getNamespace(section, i);
4968
5279
  const sectionToValidate = ns ? namespaceSectionConfig(section, ns) : section;
4969
5280
  const valid = sectionValidate(sectionToValidate, values, dispatch);
4970
5281
  if (!valid) {
4971
- throw new Error('Validation failed');
5282
+ if (firstInvalidIndex === null)
5283
+ firstInvalidIndex = i;
4972
5284
  }
4973
- results.push(buildSectionChanges(section, values, ns));
5285
+ else {
5286
+ results.push(buildSectionChanges(section, values, ns));
5287
+ }
5288
+ }
5289
+ if (firstInvalidIndex !== null) {
5290
+ if (modeRef.current === 'IntakeForm') {
5291
+ setExpandedSectionIndexRef.current(firstInvalidIndex);
5292
+ }
5293
+ throw new Error('Validation failed. Please fix the errors and try again.');
4974
5294
  }
4975
5295
  return results;
4976
5296
  },
@@ -4985,7 +5305,7 @@ const SectionsContainer = ({ sections, dataSourceRequestHandler: propDataSourceR
4985
5305
  return results;
4986
5306
  },
4987
5307
  };
4988
- }, [store, dispatch, safeSections, namespace]);
5308
+ }, [store, dispatch, safeSections]);
4989
5309
  // Call onFormReady when form is ready (sections loaded)
4990
5310
  useEffect(() => {
4991
5311
  if (onFormReady && safeSections.length > 0) {
@@ -8269,10 +8589,10 @@ const DisplayWidget = ({ config }) => {
8269
8589
  const label = translateConfig(widgetConfig['widget-label']);
8270
8590
  // If no label, render as paragraph text
8271
8591
  if (!label || label.trim() === '') {
8272
- return (jsxRuntimeExports.jsx("div", { className: "mb-3 text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
8592
+ return (jsxRuntimeExports.jsx("div", { className: "DisplayFieldWidget mb-3 min-w-0 w-full overflow-hidden text-ellipsis whitespace-nowrap text-base text-gray-700", title: String(displayValue ?? ''), children: displayValue }));
8273
8593
  }
8274
- // With label, render as key-value pair
8275
- return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("div", { className: "text-base text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: label, children: [label, ":"] }), jsxRuntimeExports.jsx("div", { className: "flex-1 text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue })] }));
8594
+ // With label, render as key-value pair (structure matches other readonly widgets for SectionRenderer ellipsis)
8595
+ return (jsxRuntimeExports.jsxs("div", { className: "mb-[10px] DisplayFieldWidget flex flex-col sm:flex-row sm:items-start", children: [jsxRuntimeExports.jsxs("div", { className: "text-base text-gray-600 font-medium md:min-w-[120px] sm:pr-4 mb-1 sm:mb-0", style: { fontFamily: 'Roboto, sans-serif' }, title: label, children: [label, ":"] }), jsxRuntimeExports.jsx("div", { className: "flex-1 min-w-0", children: jsxRuntimeExports.jsx("div", { className: "text-base text-gray-900 font-medium", title: String(displayValue ?? ''), children: displayValue }) })] }));
8276
8596
  };
8277
8597
 
8278
8598
  const TableCellSelect = ({ config, value, onValueChange }) => {
@@ -8286,7 +8606,7 @@ const TableCellSelect = ({ config, value, onValueChange }) => {
8286
8606
  backgroundColor: isReadonly || loading ? 'var(--owt-color-bg-alt, #F6F6F6)' : 'var(--owt-color-bg, #FFFFFF)',
8287
8607
  }, children: [jsxRuntimeExports.jsx("option", { value: "", children: translate('common.select') || 'Select' }), dataSourceOptions.map((option) => (jsxRuntimeExports.jsx("option", { value: option.value, children: option.label }, option.value)))] }));
8288
8608
  };
8289
- const SelectDisplayValue = ({ config, value }) => {
8609
+ const SelectDisplayValue$1 = ({ config, value }) => {
8290
8610
  const { dataSourceOptions, loading } = useBaseWidget({ config });
8291
8611
  if (loading) {
8292
8612
  return jsxRuntimeExports.jsx("span", { children: "-" });
@@ -8780,7 +9100,7 @@ const TableWidget = ({ config }) => {
8780
9100
  'widget-readonly': true,
8781
9101
  'widget-data-path': undefined,
8782
9102
  };
8783
- return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: jsxRuntimeExports.jsx(SelectDisplayValue, { config: cellConfig, value: cellValue }) }));
9103
+ return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: jsxRuntimeExports.jsx(SelectDisplayValue$1, { config: cellConfig, value: cellValue }) }));
8784
9104
  }
8785
9105
  return (jsxRuntimeExports.jsx("div", { className: "text-sm", style: getCellStyle(), children: displayValue }));
8786
9106
  }
@@ -8919,38 +9239,297 @@ const TableWidget = ({ config }) => {
8919
9239
  }, children: translate('common.cancel') || 'Cancel' })] }) })] }))] })] }) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-sm mt-1", style: { color: 'var(--owt-widget-error-color, #B91C1C)' }, children: error[0] }))] })] }));
8920
9240
  };
8921
9241
 
8922
- const ProfileWidget = ({ config }) => {
8923
- const { value, config: widgetConfig, getFieldValue, } = useBaseWidget({ config });
8924
- const { translateConfig } = useWidgetTranslation();
8925
- // Get schemaData from context as fallback
8926
- const { schemaData } = useWidgetContext();
8927
- // Get values from Redux store
8928
- const values = useSelector((state) => state.widget.values);
8929
- // Support two approaches for data paths:
8930
- // 1. Multi-path binding via widget-data-path (object) - RECOMMENDED (Approach 2)
8931
- // 2. Individual path properties (widget-image-path, widget-name-path, widget-id-path) - Fallback
8932
- let imageUrl = null;
8933
- let displayName = '';
8934
- let idValue = '';
8935
- const dataPath = widgetConfig['widget-data-path'];
8936
- const imagePath = widgetConfig['widget-image-path'];
8937
- const namePath = widgetConfig['widget-name-path'];
8938
- const idPath = widgetConfig['widget-id-path'];
8939
- // Prioritize multi-path data binding (Approach 2 - Recommended)
8940
- if (dataPath && typeof dataPath === 'object') {
8941
- // Multi-path data binding - preferred approach (Approach 2)
8942
- // Always fetch each path individually using getFieldValue for reliability
8943
- // The values in the dataPath object are the actual data paths to fetch
8944
- const imagePathValue = dataPath.image || dataPath.photo || dataPath.avatar;
8945
- const namePathValue = dataPath.name || dataPath.displayName;
8946
- const idPathValue = dataPath.id || dataPath.identifier;
8947
- // Helper function to search for a path within all top-level objects
8948
- const findValueInNestedObjects = (path, searchIn) => {
8949
- if (!searchIn)
8950
- return undefined;
8951
- // First try direct path (in case it's at root level)
8952
- let value = getValueByPath(searchIn, path);
8953
- if (value !== undefined)
9242
+ // Display select value label in view mode
9243
+ const SelectDisplayValue = ({ config, value }) => {
9244
+ const { dataSourceOptions, loading } = useBaseWidget({ config });
9245
+ if (loading)
9246
+ return jsxRuntimeExports.jsx("span", { children: "-" });
9247
+ if (value === null || value === undefined || value === '')
9248
+ return jsxRuntimeExports.jsx("span", { children: "-" });
9249
+ const selectedOption = dataSourceOptions.find((option) => option.value === value);
9250
+ return jsxRuntimeExports.jsx("span", { children: selectedOption ? selectedOption.label : String(value) });
9251
+ };
9252
+ /**
9253
+ * Dialog table widget:
9254
+ * - Table displays a subset of columns (n out of x)
9255
+ * - Add/Edit happens in a modal dialog that shows ALL columns as a form
9256
+ *
9257
+ * Usage in schema:
9258
+ * {
9259
+ * "widget": "dialog-table",
9260
+ * "widget-type": "table",
9261
+ * "widget-label": "Household Members",
9262
+ * "widget-id": "householdMembers",
9263
+ * "widget-data-path": "household.members",
9264
+ * "widget-data-columns": [ ...all columns... ],
9265
+ * "widget-data-visible-columns": ["firstName", "lastName", "dob"], // optional override; default = by column flag
9266
+ * // Per-column control (recommended): set "column-visible-in-table": false to hide it in the table
9267
+ * "widget-data-operations": { "add": true, "edit": true, "remove": true }
9268
+ * }
9269
+ */
9270
+ const DialogTableWidget = ({ config }) => {
9271
+ const { value, error, touched, isEnabled, onChange, config: widgetConfig } = useBaseWidget({ config });
9272
+ const { translate, translateConfig } = useWidgetTranslation();
9273
+ const dispatch = useDispatch();
9274
+ const storeValues = useSelector((state) => state.widget?.values ?? {});
9275
+ const rows = Array.isArray(value) ? value : [];
9276
+ const columns = widgetConfig['widget-data-columns'] || [];
9277
+ const operations = widgetConfig['widget-data-operations'] || {};
9278
+ const isReadonly = widgetConfig['widget-readonly'] || false;
9279
+ const visibleColumnKeys = widgetConfig['widget-data-visible-columns'];
9280
+ const visibleColumns = useMemo(() => {
9281
+ // 1) If explicit list provided, it wins
9282
+ if (Array.isArray(visibleColumnKeys) && visibleColumnKeys.length > 0) {
9283
+ const keySet = new Set(visibleColumnKeys);
9284
+ return columns.filter((c) => keySet.has(c['column-key']));
9285
+ }
9286
+ // 2) Otherwise decide per column (default = visible)
9287
+ return columns.filter((c) => c['column-visible-in-table'] !== false);
9288
+ }, [columns, visibleColumnKeys]);
9289
+ const [dialogOpen, setDialogOpen] = useState(false);
9290
+ const [dialogMode, setDialogMode] = useState('add');
9291
+ const [activeRowIndex, setActiveRowIndex] = useState(null);
9292
+ const [formData, setFormData] = useState({});
9293
+ /** Unique per dialog open so Redux widget ids don't reuse stale values across rows/add sessions */
9294
+ const dialogSessionRef = useRef(0);
9295
+ const [dialogSessionId, setDialogSessionId] = useState(0);
9296
+ const addDialogTitle = translateConfig(widgetConfig['widget-data-dialog-title-add']) ||
9297
+ translate('table.addRecordDialog') ||
9298
+ 'Add record';
9299
+ const editDialogTitle = translateConfig(widgetConfig['widget-data-dialog-title-edit']) ||
9300
+ translate('table.editRecordDialog') ||
9301
+ 'Edit record';
9302
+ const buildEmptyRow = useCallback(() => {
9303
+ const emptyRow = {};
9304
+ columns.forEach((col) => {
9305
+ const key = col['column-key'];
9306
+ emptyRow[key] = col['widget-data-default'] ?? '';
9307
+ });
9308
+ return emptyRow;
9309
+ }, [columns]);
9310
+ const dialogFieldWidgetId = useCallback((columnKey) => `${widgetConfig['widget-id']}-dlg-${dialogSessionId}-${columnKey}`, [widgetConfig, dialogSessionId]);
9311
+ const resetDialogWidgets = useCallback((sessionId) => {
9312
+ if (sessionId <= 0)
9313
+ return;
9314
+ columns.forEach((col) => {
9315
+ const wid = `${widgetConfig['widget-id']}-dlg-${sessionId}-${col['column-key']}`;
9316
+ dispatch(resetWidget(wid));
9317
+ });
9318
+ }, [columns, widgetConfig, dispatch]);
9319
+ const beginDialogSession = useCallback(() => {
9320
+ dialogSessionRef.current += 1;
9321
+ const nextSession = dialogSessionRef.current;
9322
+ setDialogSessionId(nextSession);
9323
+ return nextSession;
9324
+ }, []);
9325
+ const openAddDialog = useCallback(() => {
9326
+ resetDialogWidgets(dialogSessionId);
9327
+ beginDialogSession();
9328
+ setDialogMode('add');
9329
+ setActiveRowIndex(null);
9330
+ setFormData(buildEmptyRow());
9331
+ setDialogOpen(true);
9332
+ }, [buildEmptyRow, beginDialogSession, resetDialogWidgets, dialogSessionId]);
9333
+ const openEditDialog = useCallback((rowIndex) => {
9334
+ resetDialogWidgets(dialogSessionId);
9335
+ beginDialogSession();
9336
+ const row = rows[rowIndex] || {};
9337
+ const nextFormData = buildEmptyRow();
9338
+ columns.forEach((col) => {
9339
+ const key = col['column-key'];
9340
+ if (row[key] !== undefined)
9341
+ nextFormData[key] = row[key];
9342
+ });
9343
+ setDialogMode('edit');
9344
+ setActiveRowIndex(rowIndex);
9345
+ setFormData(nextFormData);
9346
+ setDialogOpen(true);
9347
+ }, [rows, columns, buildEmptyRow, resetDialogWidgets, dialogSessionId, beginDialogSession]);
9348
+ const closeDialog = useCallback(() => {
9349
+ const sessionToClear = dialogSessionId;
9350
+ setDialogOpen(false);
9351
+ setActiveRowIndex(null);
9352
+ setFormData({});
9353
+ resetDialogWidgets(sessionToClear);
9354
+ setDialogSessionId(0);
9355
+ }, [dialogSessionId, resetDialogWidgets]);
9356
+ const updateField = useCallback((columnKey, newValue) => {
9357
+ setFormData((prev) => ({ ...prev, [columnKey]: newValue }));
9358
+ }, []);
9359
+ const collectMergedRowPayload = useCallback(() => {
9360
+ const merged = { ...formData };
9361
+ columns.forEach((col) => {
9362
+ const k = col['column-key'];
9363
+ const wid = dialogFieldWidgetId(k);
9364
+ const fromStore = storeValues[wid];
9365
+ if (fromStore !== undefined)
9366
+ merged[k] = fromStore;
9367
+ });
9368
+ return merged;
9369
+ }, [formData, columns, storeValues, dialogFieldWidgetId]);
9370
+ const saveDialog = useCallback(() => {
9371
+ const payload = collectMergedRowPayload();
9372
+ if (dialogMode === 'add') {
9373
+ const savedRow = { ...payload, edit_action: 'ADD' };
9374
+ onChange([...rows, savedRow]);
9375
+ closeDialog();
9376
+ return;
9377
+ }
9378
+ if (dialogMode === 'edit' && activeRowIndex !== null) {
9379
+ const newRows = [...rows];
9380
+ const currentRow = newRows[activeRowIndex] || {};
9381
+ const wasDeleted = currentRow.edit_action === 'DELETE';
9382
+ const editAction = wasDeleted ? 'UPDATE' : (currentRow.edit_action ?? 'UPDATE');
9383
+ newRows[activeRowIndex] = { ...currentRow, ...payload, edit_action: editAction };
9384
+ onChange(newRows);
9385
+ closeDialog();
9386
+ }
9387
+ }, [collectMergedRowPayload, dialogMode, onChange, rows, closeDialog, activeRowIndex]);
9388
+ const deleteRow = useCallback((rowIndex) => {
9389
+ const newRows = rows.filter((_, i) => i !== rowIndex);
9390
+ onChange(newRows);
9391
+ }, [rows, onChange]);
9392
+ const getDisplayValue = useCallback((rowIndex, column) => {
9393
+ const key = column['column-key'];
9394
+ const cellValue = rows[rowIndex]?.[key];
9395
+ const widgetType = column.widget || 'text';
9396
+ if (cellValue === null || cellValue === undefined || cellValue === '')
9397
+ return '-';
9398
+ if (widgetType === 'select')
9399
+ return null; // handled by SelectDisplayValue
9400
+ if (column['widget-data-format'])
9401
+ return formatValue(cellValue, column['widget-data-format'], column.widget);
9402
+ return String(cellValue);
9403
+ }, [rows]);
9404
+ const tableWidgetId = `dialog-table-widget-${widgetConfig['widget-id']}`;
9405
+ const columnSpan = widgetConfig['widget-column-span'] || 2;
9406
+ const minWidth = columnSpan * 200;
9407
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
9408
+ .${tableWidgetId} {
9409
+ width: 100%;
9410
+ min-width: ${minWidth}px;
9411
+ }
9412
+
9413
+ .widget-container[data-widget-id="${widgetConfig['widget-id']}"] {
9414
+ min-width: ${minWidth}px;
9415
+ width: 100%;
9416
+ flex: none;
9417
+ }
9418
+
9419
+ .panel-horizontal .widget-container[data-widget-id="${widgetConfig['widget-id']}"],
9420
+ [data-panel-orientation="horizontal"] .widget-container[data-widget-id="${widgetConfig['widget-id']}"] {
9421
+ grid-column: span ${columnSpan};
9422
+ }
9423
+ ` }), jsxRuntimeExports.jsxs("div", { className: `table-widget-container ${tableWidgetId}`, children: [operations.add && !isReadonly && isEnabled && (jsxRuntimeExports.jsx("div", { className: "flex justify-end mb-2", children: jsxRuntimeExports.jsx("button", { type: "button", onClick: openAddDialog, className: "px-3 py-1 text-sm disabled:opacity-50 disabled:cursor-not-allowed", style: {
9424
+ borderRadius: 'var(--owt-btn-border-radius, 10px)',
9425
+ border: '1px solid var(--owt-btn-primary-border, #F07B1A)',
9426
+ backgroundColor: 'var(--owt-color-primary, #F5BB1A)',
9427
+ color: 'var(--owt-color-bg, #FFFFFF)',
9428
+ }, children: translate('table.addRecord') || 'Add New Record' }) })), jsxRuntimeExports.jsx("div", { className: "overflow-x-auto border", style: {
9429
+ borderRadius: 'var(--owt-widget-table-border-radius, 15px)',
9430
+ borderColor: 'var(--owt-widget-table-border-color, #C4C4C4)',
9431
+ }, children: jsxRuntimeExports.jsxs("table", { className: "min-w-full", style: { borderCollapse: 'separate', borderSpacing: 0 }, children: [jsxRuntimeExports.jsx("thead", { style: { backgroundColor: 'var(--owt-widget-table-header-bg, #F6F6F6)' }, children: jsxRuntimeExports.jsxs("tr", { style: { borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)' }, children: [visibleColumns.map((col) => (jsxRuntimeExports.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium uppercase tracking-wider", style: { color: 'var(--owt-widget-table-header-color, #727474)' }, children: translateConfig(col['widget-label']) }, col['column-key']))), ((operations.edit || operations.remove) && !isReadonly) && (jsxRuntimeExports.jsx("th", { className: "px-4 py-3 text-left text-xs font-medium uppercase tracking-wider", style: { color: 'var(--owt-widget-table-header-color, #727474)' }, children: translate('common.actions') || 'Actions' }))] }) }), jsxRuntimeExports.jsxs("tbody", { style: { backgroundColor: 'var(--owt-widget-table-body-bg, #FFFFFF)' }, children: [rows.length === 0 && (jsxRuntimeExports.jsx("tr", { children: jsxRuntimeExports.jsxs("td", { colSpan: visibleColumns.length + (((operations.edit || operations.remove) && !isReadonly) ? 1 : 0), className: "px-4 py-6 text-center text-sm", style: { color: 'var(--owt-widget-table-empty-color, #727474)' }, children: [translate('table.noData') || 'No records available.', operations.add && !isReadonly && ` ${translate('table.clickToAdd') || 'Click "Add New Record" to add one.'}`] }) })), rows.map((row, rowIndex) => (jsxRuntimeExports.jsxs("tr", { style: {
9432
+ borderBottom: '1px solid var(--owt-widget-table-row-divider, #E4E4E4)',
9433
+ backgroundColor: row?.edit_action === 'DELETE'
9434
+ ? 'var(--owt-widget-table-deleted-row-bg, #FEE2E2)'
9435
+ : undefined,
9436
+ }, children: [visibleColumns.map((col) => {
9437
+ const key = col['column-key'];
9438
+ const widgetType = col.widget || 'text';
9439
+ const displayValue = getDisplayValue(rowIndex, col);
9440
+ if (widgetType === 'select' && displayValue === null) {
9441
+ const displayConfig = {
9442
+ ...col,
9443
+ 'widget-id': `${widgetConfig['widget-id']}-view-row-${rowIndex}-col-${key}`,
9444
+ 'widget-label': '',
9445
+ 'widget-readonly': true,
9446
+ 'widget-data-path': undefined,
9447
+ };
9448
+ return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: jsxRuntimeExports.jsx("div", { className: "text-sm", children: jsxRuntimeExports.jsx(SelectDisplayValue, { config: displayConfig, value: row?.[key] }) }) }, key));
9449
+ }
9450
+ return (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", children: jsxRuntimeExports.jsx("div", { className: "text-sm", children: displayValue }) }, key));
9451
+ }), ((operations.edit || operations.remove) && !isReadonly) && (jsxRuntimeExports.jsx("td", { className: "px-4 py-3 whitespace-nowrap", style: { minWidth: '120px' }, children: jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [operations.edit && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => openEditDialog(rowIndex), disabled: !isEnabled, className: "px-3 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed", style: {
9452
+ borderRadius: 'var(--owt-btn-border-radius, 10px)',
9453
+ color: 'var(--owt-color-primary-dark, #F07B1A)',
9454
+ backgroundColor: 'transparent',
9455
+ border: 'none',
9456
+ }, children: translate('common.edit') || 'Edit' })), operations.remove && (jsxRuntimeExports.jsx("button", { type: "button", onClick: () => deleteRow(rowIndex), disabled: !isEnabled, className: "px-3 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed", style: {
9457
+ borderRadius: 'var(--owt-btn-border-radius, 10px)',
9458
+ color: 'var(--owt-color-error, #B91C1C)',
9459
+ backgroundColor: 'transparent',
9460
+ border: 'none',
9461
+ }, children: translate('common.remove') || 'Delete' }))] }) }))] }, rowIndex)))] })] }) }), touched && error.length > 0 && (jsxRuntimeExports.jsx("p", { className: "text-sm mt-1", style: { color: 'var(--owt-widget-error-color, #B91C1C)' }, children: error[0] }))] }), dialogOpen && (jsxRuntimeExports.jsx("div", { className: "fixed inset-0 flex items-center justify-center z-50", style: { backgroundColor: 'rgba(0,0,0,0.5)' }, children: jsxRuntimeExports.jsxs("div", { className: "rounded-lg p-6 w-full mx-4", style: {
9462
+ maxWidth: '900px',
9463
+ backgroundColor: 'var(--owt-color-bg, #FFFFFF)',
9464
+ borderRadius: 'var(--owt-widget-card-border-radius, 20px)',
9465
+ }, children: [jsxRuntimeExports.jsxs("div", { className: "flex items-start justify-between gap-4 mb-4", children: [jsxRuntimeExports.jsx("h3", { className: "text-lg font-semibold", style: { color: 'var(--owt-color-text, #011627)' }, children: dialogMode === 'add' ? addDialogTitle : editDialogTitle }), jsxRuntimeExports.jsx("button", { type: "button", onClick: closeDialog, style: {
9466
+ border: 'none',
9467
+ background: 'transparent',
9468
+ color: 'var(--owt-color-text-muted, #727474)',
9469
+ cursor: 'pointer',
9470
+ fontSize: '20px',
9471
+ lineHeight: 1,
9472
+ }, "aria-label": "Close", children: "\u00D7" })] }), jsxRuntimeExports.jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", style: { maxHeight: '70vh', overflow: 'auto' }, children: columns.map((col) => {
9473
+ const key = col['column-key'];
9474
+ const widgetType = col.widget || 'text';
9475
+ const cellWidgetId = dialogFieldWidgetId(key);
9476
+ const initialValue = formData[key] ?? col['widget-data-default'] ?? '';
9477
+ const fieldConfig = {
9478
+ ...col,
9479
+ widget: widgetType,
9480
+ 'widget-type': col['widget-type'] || 'input',
9481
+ 'widget-id': cellWidgetId,
9482
+ 'widget-label': col['widget-label'],
9483
+ 'widget-readonly': isReadonly || col['widget-readonly'] === true,
9484
+ 'widget-data-path': undefined,
9485
+ 'widget-data-default': initialValue,
9486
+ };
9487
+ return (jsxRuntimeExports.jsx("div", { className: "min-w-0", children: jsxRuntimeExports.jsx(WidgetRenderer, { config: fieldConfig, schemaData: { [cellWidgetId]: initialValue }, onValueChange: (_widgetId, newValue) => updateField(key, newValue) }) }, `${dialogSessionId}-${key}`));
9488
+ }) }, `dialog-fields-${dialogSessionId}`), jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-3 mt-6", children: [jsxRuntimeExports.jsx("button", { type: "button", onClick: closeDialog, className: "px-4 py-2 text-sm font-medium", style: {
9489
+ borderRadius: 'var(--owt-btn-border-radius, 10px)',
9490
+ border: '1px solid var(--owt-btn-secondary-border, #C4C4C4)',
9491
+ backgroundColor: 'var(--owt-btn-secondary-bg, #FFFFFF)',
9492
+ color: 'var(--owt-btn-secondary-color, #011627)',
9493
+ }, children: translate('common.cancel') || 'Cancel' }), jsxRuntimeExports.jsx("button", { type: "button", onClick: saveDialog, disabled: isReadonly || !isEnabled, className: "px-4 py-2 text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed", style: {
9494
+ borderRadius: 'var(--owt-btn-border-radius, 10px)',
9495
+ border: '1px solid var(--owt-btn-primary-border, #F07B1A)',
9496
+ backgroundColor: 'var(--owt-color-primary, #F5BB1A)',
9497
+ color: 'var(--owt-color-bg, #FFFFFF)',
9498
+ }, children: translate('common.save') || 'Save' })] })] }) }))] }));
9499
+ };
9500
+
9501
+ const ProfileWidget = ({ config }) => {
9502
+ const { value, config: widgetConfig, getFieldValue, } = useBaseWidget({ config });
9503
+ const { translateConfig } = useWidgetTranslation();
9504
+ // Get schemaData from context as fallback
9505
+ const { schemaData } = useWidgetContext();
9506
+ // Get values from Redux store
9507
+ const values = useSelector((state) => state.widget.values);
9508
+ // Support two approaches for data paths:
9509
+ // 1. Multi-path binding via widget-data-path (object) - RECOMMENDED (Approach 2)
9510
+ // 2. Individual path properties (widget-image-path, widget-name-path, widget-id-path) - Fallback
9511
+ let imageUrl = null;
9512
+ let displayName = '';
9513
+ let idValue = '';
9514
+ const dataPath = widgetConfig['widget-data-path'];
9515
+ const imagePath = widgetConfig['widget-image-path'];
9516
+ const namePath = widgetConfig['widget-name-path'];
9517
+ const idPath = widgetConfig['widget-id-path'];
9518
+ // Prioritize multi-path data binding (Approach 2 - Recommended)
9519
+ if (dataPath && typeof dataPath === 'object') {
9520
+ // Multi-path data binding - preferred approach (Approach 2)
9521
+ // Always fetch each path individually using getFieldValue for reliability
9522
+ // The values in the dataPath object are the actual data paths to fetch
9523
+ const imagePathValue = dataPath.image || dataPath.photo || dataPath.avatar;
9524
+ const namePathValue = dataPath.name || dataPath.displayName;
9525
+ const idPathValue = dataPath.id || dataPath.identifier;
9526
+ // Helper function to search for a path within all top-level objects
9527
+ const findValueInNestedObjects = (path, searchIn) => {
9528
+ if (!searchIn)
9529
+ return undefined;
9530
+ // First try direct path (in case it's at root level)
9531
+ let value = getValueByPath(searchIn, path);
9532
+ if (value !== undefined)
8954
9533
  return value;
8955
9534
  // If not found, search within each top-level object
8956
9535
  for (const [key, obj] of Object.entries(searchIn)) {
@@ -9310,15 +9889,107 @@ const HeaderSectionWidget = ({ config }) => {
9310
9889
  result = searchIn(schemaData);
9311
9890
  return result;
9312
9891
  }, [paths, values, schemaData]);
9313
- const imageUrl = findValue('image') || null;
9892
+ const imageVal = findValue('image');
9893
+ const imageUrlVal = findValue('imageUrl');
9894
+ const [previewUrl, setPreviewUrl] = useState(null);
9895
+ useEffect(() => {
9896
+ if (imageVal instanceof File) {
9897
+ const url = URL.createObjectURL(imageVal);
9898
+ setPreviewUrl(url);
9899
+ return () => URL.revokeObjectURL(url);
9900
+ }
9901
+ setPreviewUrl(null);
9902
+ }, [imageVal]);
9903
+ const displayImageUrl = previewUrl || (typeof imageUrlVal === 'string' && imageUrlVal ? imageUrlVal : null);
9314
9904
  const displayName = findValue('name') || '';
9315
9905
  const functionalId = findValue('functionalId') || '';
9316
9906
  const statusValue = findValue('status') || '';
9317
9907
  const statusReason = findValue('statusReason') || '';
9908
+ const completionScoreRaw = findValue('completionScore');
9909
+ const idealScoreRaw = findValue('idealScore');
9318
9910
  const createdBy = findValue('createdBy') || '';
9319
9911
  const createdAt = findValue('createdAt') || '';
9320
9912
  const lastApprovedBy = findValue('lastApprovedBy') || '';
9321
9913
  const lastApprovedAt = findValue('lastApprovedAt') || '';
9914
+ // ── Validation: status change requires reason ──────────────────
9915
+ // Behavior:
9916
+ // - When status changes away from its initial value, clear reason and require it.
9917
+ // - When status returns to initial value (or a parent "Cancel" restores it), restore initial reason.
9918
+ const initialStatusRef = useRef(null);
9919
+ const initialReasonRef = useRef(null);
9920
+ const prevStatusRef = useRef(null);
9921
+ const [showReasonRequired, setShowReasonRequired] = useState(false);
9922
+ useEffect(() => {
9923
+ // Capture initial status once when it becomes available.
9924
+ if (initialStatusRef.current === null) {
9925
+ const v = statusValue === undefined || statusValue === null ? '' : String(statusValue);
9926
+ initialStatusRef.current = v;
9927
+ }
9928
+ }, [statusValue]);
9929
+ useEffect(() => {
9930
+ // Capture initial reason once when it becomes available.
9931
+ if (initialReasonRef.current === null) {
9932
+ const v = statusReason === undefined || statusReason === null ? '' : String(statusReason);
9933
+ initialReasonRef.current = v;
9934
+ }
9935
+ }, [statusReason]);
9936
+ const isStatusChanged = useMemo(() => {
9937
+ const initial = initialStatusRef.current;
9938
+ if (initial === null)
9939
+ return false;
9940
+ return String(statusValue) !== initial;
9941
+ }, [statusValue]);
9942
+ const isReasonMissing = useMemo(() => {
9943
+ if (!isStatusChanged)
9944
+ return false;
9945
+ return String(statusReason || '').trim().length === 0;
9946
+ }, [isStatusChanged, statusReason]);
9947
+ useEffect(() => {
9948
+ // When status changes:
9949
+ // - If moved away from initial → clear reason.
9950
+ // - If returned to initial → restore initial reason.
9951
+ if (isReadonly)
9952
+ return;
9953
+ if (initialStatusRef.current === null)
9954
+ return;
9955
+ const currentStatus = String(statusValue || '');
9956
+ if (prevStatusRef.current === currentStatus)
9957
+ return;
9958
+ prevStatusRef.current = currentStatus;
9959
+ const initialStatus = initialStatusRef.current;
9960
+ const initialReason = initialReasonRef.current ?? '';
9961
+ if (currentStatus === initialStatus) {
9962
+ // Reverted / cancelled back to original
9963
+ if (String(statusReason || '') !== String(initialReason || '')) {
9964
+ updateFieldValue('statusReason', initialReason);
9965
+ }
9966
+ setShowReasonRequired(false);
9967
+ return;
9968
+ }
9969
+ // Status changed to a new value: clear reason (so user must re-enter)
9970
+ if (String(statusReason || '').trim().length > 0) {
9971
+ updateFieldValue('statusReason', '');
9972
+ }
9973
+ setShowReasonRequired(true);
9974
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9975
+ }, [statusValue, isReadonly]);
9976
+ const score = useMemo(() => {
9977
+ const toNum = (v) => {
9978
+ if (v === null || v === undefined || String(v).trim() === '')
9979
+ return null;
9980
+ const n = typeof v === 'number' ? v : Number(String(v));
9981
+ return Number.isFinite(n) ? n : null;
9982
+ };
9983
+ const completion = toNum(completionScoreRaw);
9984
+ const ideal = toNum(idealScoreRaw);
9985
+ if (completion === null || ideal === null || ideal <= 0)
9986
+ return null;
9987
+ const ratio = completion / ideal;
9988
+ const percent = Math.max(0, Math.min(100, Math.round(ratio * 100)));
9989
+ const completionDisplay = Number.isInteger(completion) ? completion : Math.round(completion);
9990
+ const idealDisplay = Number.isInteger(ideal) ? ideal : Math.round(ideal);
9991
+ return { completion, ideal, completionDisplay, idealDisplay, percent };
9992
+ }, [completionScoreRaw, idealScoreRaw]);
9322
9993
  // ── Format options ────────────────────────────────────────────
9323
9994
  const format = (widgetConfig['widget-data-format'] || {});
9324
9995
  const imageSize = format.imageSize || 120;
@@ -9343,18 +10014,20 @@ const HeaderSectionWidget = ({ config }) => {
9343
10014
  return opt ? opt.label : String(statusValue);
9344
10015
  }, [statusValue, statusOptions]);
9345
10016
  const statusColor = statusColors[String(statusValue).toLowerCase()] || 'var(--owt-color-text-muted, #6B7280)';
10017
+ // ── Image edit helpers ───────────────────────────────────────
10018
+ const fileInputRef = useRef(null);
10019
+ const handleImageUpload = useCallback((e) => {
10020
+ const file = e.target.files?.[0];
10021
+ if (!file)
10022
+ return;
10023
+ updateFieldValue('image', file);
10024
+ e.target.value = '';
10025
+ }, [updateFieldValue]);
10026
+ const handleImageDelete = useCallback(() => {
10027
+ updateFieldValue('image', '');
10028
+ }, [updateFieldValue]);
9346
10029
  // ── Scoped class for CSS isolation ────────────────────────────
9347
10030
  const cls = `header-section-widget-${widgetConfig['widget-id']}`;
9348
- // ── Indicator dot component ───────────────────────────────────
9349
- const Dot = ({ color }) => (jsxRuntimeExports.jsx("span", { style: {
9350
- display: 'inline-block',
9351
- width: 8,
9352
- height: 8,
9353
- borderRadius: '50%',
9354
- backgroundColor: color,
9355
- flexShrink: 0,
9356
- marginTop: 6,
9357
- } }));
9358
10031
  // ── RENDER ────────────────────────────────────────────────────
9359
10032
  return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
9360
10033
  .${cls} {
@@ -9383,6 +10056,58 @@ const HeaderSectionWidget = ({ config }) => {
9383
10056
  min-width: 220px;
9384
10057
  }
9385
10058
 
10059
+ .${cls} .hdr-right-top {
10060
+ display: flex;
10061
+ align-items: flex-start;
10062
+ justify-content: space-between;
10063
+ gap: 14px;
10064
+ width: 100%;
10065
+ }
10066
+
10067
+ .${cls} .hdr-meta-col {
10068
+ display: flex;
10069
+ flex-direction: column;
10070
+ gap: 0.5rem;
10071
+ flex: 1 1 auto;
10072
+ min-width: 0;
10073
+ }
10074
+
10075
+ .${cls} .hdr-score-ring {
10076
+ --ring-size: 54px;
10077
+ --ring-thickness: 7px;
10078
+ --ring-color: var(--owt-color-primary-dark, #F07B1A);
10079
+ --ring-track: rgba(2, 6, 23, 0.10);
10080
+ width: var(--ring-size);
10081
+ height: var(--ring-size);
10082
+ border-radius: 50%;
10083
+ background: conic-gradient(
10084
+ var(--ring-color) calc(var(--pct) * 1%),
10085
+ var(--ring-track) 0
10086
+ );
10087
+ position: relative;
10088
+ flex: 0 0 auto;
10089
+ }
10090
+
10091
+ .${cls} .hdr-score-ring::before {
10092
+ content: "";
10093
+ position: absolute;
10094
+ inset: var(--ring-thickness);
10095
+ border-radius: 50%;
10096
+ background: var(--owt-color-bg, #FFFFFF);
10097
+ }
10098
+
10099
+ .${cls} .hdr-score-value {
10100
+ position: absolute;
10101
+ inset: 0;
10102
+ display: flex;
10103
+ align-items: center;
10104
+ justify-content: center;
10105
+ font-size: 20px;
10106
+ font-weight: 700;
10107
+ color: var(--owt-color-text, #011627);
10108
+ font-family: Roboto, sans-serif;
10109
+ }
10110
+
9386
10111
  .${cls} .hdr-avatar {
9387
10112
  width: ${imageSize}px;
9388
10113
  height: ${imageSize}px;
@@ -9413,6 +10138,56 @@ const HeaderSectionWidget = ({ config }) => {
9413
10138
  border-radius: 8px;
9414
10139
  }
9415
10140
 
10141
+ .${cls} .hdr-avatar-wrapper {
10142
+ position: relative;
10143
+ width: ${imageSize}px;
10144
+ height: ${imageSize}px;
10145
+ flex-shrink: 0;
10146
+ }
10147
+
10148
+ .${cls} .hdr-avatar-overlay {
10149
+ position: absolute;
10150
+ inset: 0;
10151
+ border-radius: 8px;
10152
+ background: rgba(0, 0, 0, 0.55);
10153
+ display: flex;
10154
+ flex-direction: column;
10155
+ align-items: center;
10156
+ justify-content: center;
10157
+ gap: 6px;
10158
+ opacity: 0;
10159
+ transition: opacity 0.2s;
10160
+ }
10161
+
10162
+ .${cls} .hdr-avatar-wrapper:hover .hdr-avatar-overlay {
10163
+ opacity: 1;
10164
+ }
10165
+
10166
+ .${cls} .hdr-avatar-action {
10167
+ display: flex;
10168
+ align-items: center;
10169
+ gap: 5px;
10170
+ padding: 5px 14px;
10171
+ border: none;
10172
+ border-radius: 4px;
10173
+ background: rgba(255, 255, 255, 0.92);
10174
+ color: #374151;
10175
+ font-size: 0.7rem;
10176
+ font-weight: 500;
10177
+ cursor: pointer;
10178
+ font-family: Roboto, sans-serif;
10179
+ transition: background 0.15s;
10180
+ white-space: nowrap;
10181
+ }
10182
+
10183
+ .${cls} .hdr-avatar-action:hover {
10184
+ background: #fff;
10185
+ }
10186
+
10187
+ .${cls} .hdr-avatar-action--delete {
10188
+ color: #DC2626;
10189
+ }
10190
+
9416
10191
  .${cls} .hdr-info {
9417
10192
  display: flex;
9418
10193
  flex-direction: column;
@@ -9509,6 +10284,19 @@ const HeaderSectionWidget = ({ config }) => {
9509
10284
  box-shadow: 0 0 0 2px rgba(237, 124, 34, 0.15);
9510
10285
  }
9511
10286
 
10287
+ .${cls} .hdr-input--error {
10288
+ border-color: var(--owt-color-danger, #DC2626);
10289
+ box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.12);
10290
+ }
10291
+
10292
+ .${cls} .hdr-error-text {
10293
+ margin-left: calc(0px);
10294
+ color: var(--owt-color-danger, #DC2626);
10295
+ font-size: 0.75rem;
10296
+ line-height: 1.2;
10297
+ font-weight: 500;
10298
+ }
10299
+
9512
10300
  @media (max-width: 768px) {
9513
10301
  .${cls} {
9514
10302
  flex-direction: column;
@@ -9517,13 +10305,21 @@ const HeaderSectionWidget = ({ config }) => {
9517
10305
  min-width: 0;
9518
10306
  }
9519
10307
  }
9520
- ` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { children: [imageUrl ? (jsxRuntimeExports.jsx("img", { src: imageUrl, alt: displayName || 'Profile', className: "hdr-avatar", onError: (e) => {
10308
+ ` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [jsxRuntimeExports.jsxs("div", { className: "hdr-left", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-avatar-wrapper", children: [displayImageUrl ? (jsxRuntimeExports.jsx("img", { src: displayImageUrl, alt: displayName || 'Profile', className: "hdr-avatar", onError: (e) => {
9521
10309
  e.target.style.display = 'none';
9522
10310
  const placeholder = e.target
9523
10311
  .parentElement?.querySelector('.hdr-avatar-placeholder');
9524
10312
  if (placeholder)
9525
10313
  placeholder.style.display = 'flex';
9526
- } })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display: imageUrl ? 'none' : 'flex' }, children: jsxRuntimeExports.jsx("img", { src: img$f, alt: "Profile Placeholder" }) })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-info", children: [displayName && jsxRuntimeExports.jsx("div", { className: "hdr-name", children: displayName }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsx(Dot, { color: "#9CA3AF" }), jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('functionalId'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: functionalId || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsx(Dot, { color: isReadonly ? statusColor : '#F59E0B' }), jsxRuntimeExports.jsx("span", { className: "hdr-field-label", children: getLabel('status') }), isReadonly ? (statusLabel ? (jsxRuntimeExports.jsx("span", { className: "hdr-status-badge", style: { backgroundColor: statusColor }, children: statusLabel })) : (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: "-" }))) : (jsxRuntimeExports.jsxs("select", { className: "hdr-select", value: statusValue, onChange: (e) => updateFieldValue('status', e.target.value), children: [jsxRuntimeExports.jsx("option", { value: "", children: getLabel('select') }), statusOptions.map((opt) => (jsxRuntimeExports.jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }))] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsx(Dot, { color: isReadonly ? '#9CA3AF' : '#F59E0B' }), jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('statusReason'), " :"] }), isReadonly ? (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: statusReason || '-' })) : (jsxRuntimeExports.jsx("input", { type: "text", className: "hdr-input", value: statusReason, placeholder: getLabel('enterReason'), onChange: (e) => updateFieldValue('statusReason', e.target.value) }))] })] })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-right", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('createdBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: createdBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('createdAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: createdAt || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('lastApprovedBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: lastApprovedBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('lastApprovedAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: lastApprovedAt || '-' })] })] })] })] }));
10314
+ } })) : null, jsxRuntimeExports.jsx("div", { className: "hdr-avatar-placeholder", style: { display: displayImageUrl ? 'none' : 'flex' }, children: jsxRuntimeExports.jsx("img", { src: img$f, alt: "Profile Placeholder" }) }), !isReadonly && (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsxs("div", { className: "hdr-avatar-overlay", children: [jsxRuntimeExports.jsxs("button", { type: "button", className: "hdr-avatar-action", onClick: () => fileInputRef.current?.click(), children: [jsxRuntimeExports.jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), jsxRuntimeExports.jsx("polyline", { points: "17 8 12 3 7 8" }), jsxRuntimeExports.jsx("line", { x1: "12", y1: "3", x2: "12", y2: "15" })] }), "Upload"] }), jsxRuntimeExports.jsxs("button", { type: "button", className: "hdr-avatar-action hdr-avatar-action--delete", onClick: handleImageDelete, children: [jsxRuntimeExports.jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [jsxRuntimeExports.jsx("polyline", { points: "3 6 5 6 21 6" }), jsxRuntimeExports.jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" })] }), "Delete"] })] }), jsxRuntimeExports.jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", style: { display: 'none' }, onChange: handleImageUpload })] }))] }), jsxRuntimeExports.jsxs("div", { className: "hdr-info", children: [displayName && jsxRuntimeExports.jsx("div", { className: "hdr-name", children: displayName }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('functionalId'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: functionalId || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsx("span", { className: "hdr-field-label", children: getLabel('status') }), isReadonly ? (statusLabel ? (jsxRuntimeExports.jsx("span", { className: "hdr-status-badge", style: { backgroundColor: statusColor }, children: statusLabel })) : (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: "-" }))) : (jsxRuntimeExports.jsxs("select", { className: "hdr-select", value: statusValue, onChange: (e) => updateFieldValue('status', e.target.value), children: [jsxRuntimeExports.jsx("option", { value: "", children: getLabel('select') }), statusOptions.map((opt) => (jsxRuntimeExports.jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }))] }), jsxRuntimeExports.jsxs("div", { className: "hdr-field-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-field-label", children: [getLabel('statusReason'), " :"] }), isReadonly ? (jsxRuntimeExports.jsx("span", { className: "hdr-field-value", children: statusReason || '-' })) : (jsxRuntimeExports.jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [jsxRuntimeExports.jsx("input", { type: "text", className: `hdr-input ${(!isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing) ? 'hdr-input--error' : ''}`, value: statusReason, placeholder: getLabel('enterReason'), required: isStatusChanged, "aria-required": isStatusChanged, "aria-invalid": !isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing, onBlur: () => {
10315
+ if (isReasonMissing)
10316
+ setShowReasonRequired(true);
10317
+ }, onChange: (e) => {
10318
+ updateFieldValue('statusReason', e.target.value);
10319
+ if (showReasonRequired && String(e.target.value || '').trim().length > 0) {
10320
+ setShowReasonRequired(false);
10321
+ }
10322
+ } }), !isReadonly && (showReasonRequired || isReasonMissing) && isReasonMissing ? (jsxRuntimeExports.jsx("div", { className: "hdr-error-text", children: getLabel('enterReason') })) : null] }))] })] })] }), jsxRuntimeExports.jsx("div", { className: "hdr-right", children: jsxRuntimeExports.jsxs("div", { className: "hdr-right-top", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-meta-col", children: [jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('createdBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: createdBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('createdAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: createdAt || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('lastApprovedBy'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: lastApprovedBy || '-' })] }), jsxRuntimeExports.jsxs("div", { className: "hdr-meta-row", children: [jsxRuntimeExports.jsxs("span", { className: "hdr-meta-label", children: [getLabel('lastApprovedAt'), " :"] }), jsxRuntimeExports.jsx("span", { className: "hdr-meta-value", children: lastApprovedAt || '-' })] })] }), score ? (jsxRuntimeExports.jsx("div", { className: "hdr-score-ring", style: { ['--pct']: score.percent }, "aria-label": `Completion score ${score.completionDisplay} of ${score.idealDisplay} (${score.percent}%)`, title: `${score.completionDisplay} / ${score.idealDisplay} (${score.percent}%)`, children: jsxRuntimeExports.jsx("div", { className: "hdr-score-value", children: String(score.completionDisplay) }) })) : null] }) })] })] }));
9527
10323
  };
9528
10324
 
9529
10325
  function getValueByPathOrKey(obj, path) {
@@ -9533,7 +10329,7 @@ function getValueByPathOrKey(obj, path) {
9533
10329
  return obj[path];
9534
10330
  return getValueByPath(obj, path);
9535
10331
  }
9536
- function tryFormatDateTime(value) {
10332
+ function tryFormatDateTime$1(value) {
9537
10333
  if (typeof value !== 'string' || !value)
9538
10334
  return value ? String(value) : '-';
9539
10335
  const d = new Date(value);
@@ -9727,12 +10523,539 @@ const ScoresDisplayWidget = ({ config, schemaData: propSchemaData, }) => {
9727
10523
  String(s.computed_score) !== ''
9728
10524
  ? String(s.computed_score)
9729
10525
  : '-';
9730
- const computedAt = tryFormatDateTime(s?.computed_at);
10526
+ const computedAt = tryFormatDateTime$1(s?.computed_at);
9731
10527
  const key = `${scoreType}-${String(s?.computed_at || '')}-${idx}`;
9732
10528
  return (jsxRuntimeExports.jsxs("div", { className: "scores-card", "aria-live": idx === 0 ? 'polite' : undefined, children: [jsxRuntimeExports.jsx("div", { className: "scores-type", children: scoreType }), jsxRuntimeExports.jsx("div", { className: "scores-value", children: scoreValue }), jsxRuntimeExports.jsx("hr", { className: "scores-separator" }), jsxRuntimeExports.jsx("div", { className: "scores-meta", children: jsxRuntimeExports.jsxs("div", { className: "scores-meta-line", children: ["Computed at: ", jsxRuntimeExports.jsx("strong", { children: computedAt })] }) })] }, key));
9733
10529
  }) })) })] }));
9734
10530
  };
9735
10531
 
10532
+ function tryFormatDateTime(value) {
10533
+ if (typeof value !== 'string' || !value)
10534
+ return value ? String(value) : '-';
10535
+ const d = new Date(value);
10536
+ if (Number.isNaN(d.getTime()))
10537
+ return value;
10538
+ try {
10539
+ return d.toLocaleString(undefined, {
10540
+ year: 'numeric',
10541
+ month: 'short',
10542
+ day: '2-digit',
10543
+ hour: '2-digit',
10544
+ minute: '2-digit',
10545
+ });
10546
+ }
10547
+ catch {
10548
+ return value;
10549
+ }
10550
+ }
10551
+ function tryFormatDate(value) {
10552
+ if (typeof value !== 'string' || !value)
10553
+ return value ? String(value) : '-';
10554
+ const d = new Date(value);
10555
+ if (Number.isNaN(d.getTime()))
10556
+ return value;
10557
+ try {
10558
+ return d.toLocaleDateString(undefined, {
10559
+ year: 'numeric',
10560
+ month: 'short',
10561
+ day: '2-digit',
10562
+ });
10563
+ }
10564
+ catch {
10565
+ return value;
10566
+ }
10567
+ }
10568
+ function displayText(value) {
10569
+ if (value === null || value === undefined || String(value).trim() === '')
10570
+ return '-';
10571
+ return String(value);
10572
+ }
10573
+ function normalizeStatus(raw) {
10574
+ if (raw === null || raw === undefined || String(raw).trim() === '')
10575
+ return 'unknown';
10576
+ const v = String(raw).trim().toLowerCase();
10577
+ if (v === 'success' || v === 'succeeded' || v === 'ok')
10578
+ return 'success';
10579
+ if (v === 'failure' || v === 'failed' || v === 'error')
10580
+ return 'failure';
10581
+ if (v === 'not done' || v === 'not_done' || v === 'not-done' || v === 'pending')
10582
+ return 'not_done';
10583
+ return 'unknown';
10584
+ }
10585
+ /** Large enough for eSignet / OIDC login; clamped so it always fits the current screen. */
10586
+ function getCenteredPopupFeatures(width, height) {
10587
+ const dualScreenLeft = window.screenLeft ?? window.screenX ?? 0;
10588
+ const dualScreenTop = window.screenTop ?? window.screenY ?? 0;
10589
+ const viewportWidth = window.innerWidth || document.documentElement.clientWidth || (typeof screen !== 'undefined' ? screen.width : width);
10590
+ const viewportHeight = window.innerHeight || document.documentElement.clientHeight || (typeof screen !== 'undefined' ? screen.height : height);
10591
+ const maxW = Math.max(320, Math.floor(viewportWidth * 0.92));
10592
+ const maxH = Math.max(400, Math.floor(viewportHeight * 0.92));
10593
+ const w = Math.max(320, Math.min(width, maxW));
10594
+ const h = Math.max(400, Math.min(height, maxH));
10595
+ const left = Math.max(0, Math.floor(viewportWidth / 2 - w / 2 + dualScreenLeft));
10596
+ const top = Math.max(0, Math.floor(viewportHeight / 2 - h / 2 + dualScreenTop));
10597
+ return [
10598
+ 'popup=yes',
10599
+ 'noopener=yes',
10600
+ 'noreferrer=yes',
10601
+ `width=${w}`,
10602
+ `height=${h}`,
10603
+ `left=${left}`,
10604
+ `top=${top}`,
10605
+ 'scrollbars=yes',
10606
+ 'resizable=yes',
10607
+ ].join(',');
10608
+ }
10609
+ function pickAuthorizationUrl(resp, explicitKey) {
10610
+ if (!resp)
10611
+ return null;
10612
+ const tryKey = (k) => {
10613
+ const v = resp?.[k];
10614
+ if (typeof v === 'string' && v)
10615
+ return v;
10616
+ return null;
10617
+ };
10618
+ if (explicitKey) {
10619
+ const v = tryKey(explicitKey);
10620
+ if (v)
10621
+ return v;
10622
+ }
10623
+ return (tryKey('authentication_url') ||
10624
+ tryKey('authorization_url') ||
10625
+ tryKey('authorizationUrl') ||
10626
+ tryKey('auth_url') ||
10627
+ tryKey('authUrl') ||
10628
+ tryKey('url') ||
10629
+ null);
10630
+ }
10631
+ function resolveValueFromSources(path, values, schemaData) {
10632
+ if (!path)
10633
+ return undefined;
10634
+ const fromValues = getValueByPath(values, path);
10635
+ if (fromValues !== undefined)
10636
+ return fromValues;
10637
+ return getValueByPath(schemaData, path);
10638
+ }
10639
+ function unwrapPayload(response) {
10640
+ if (response && typeof response === 'object') {
10641
+ if (response.response_body?.response_payload !== undefined)
10642
+ return response.response_body.response_payload;
10643
+ if (response.response_payload !== undefined)
10644
+ return response.response_payload;
10645
+ }
10646
+ return response;
10647
+ }
10648
+ const IdAuthenticationWidget = ({ config, schemaData: propSchemaData }) => {
10649
+ const { dataSourceRequestHandler, schemaData: ctxSchemaData } = useWidgetContext();
10650
+ const values = useSelector((state) => state.widget.values);
10651
+ const schemaData = (propSchemaData || ctxSchemaData || {});
10652
+ const widgetId = config['widget-id'];
10653
+ const dataPath = config['widget-data-path'];
10654
+ const paths = useMemo(() => {
10655
+ if (!dataPath || typeof dataPath !== 'object')
10656
+ return {};
10657
+ return dataPath;
10658
+ }, [dataPath]);
10659
+ const authConfig = config['widget-auth-config'];
10660
+ const registerId = authConfig?.registerId ?? undefined;
10661
+ const internalRecordId = resolveValueFromSources(paths.internalRecordId, values, schemaData);
10662
+ const initiatedByStaffId = resolveValueFromSources(paths.initiatedByStaffId, values, schemaData);
10663
+ const providerId = authConfig?.providerId;
10664
+ const providerName = authConfig?.providerName;
10665
+ const foundationalId = resolveValueFromSources(paths.foundationalId, values, schemaData);
10666
+ const lastAuthenticatedOn = resolveValueFromSources(paths.lastAuthenticatedOn, values, schemaData);
10667
+ const lastAuthStatusRaw = resolveValueFromSources(paths.lastAuthenticationStatus, values, schemaData);
10668
+ const expiryDate = resolveValueFromSources(paths.expiryDate, values, schemaData);
10669
+ const psut = resolveValueFromSources(paths.authenticationToken, values, schemaData);
10670
+ const status = useMemo(() => normalizeStatus(lastAuthStatusRaw), [lastAuthStatusRaw]);
10671
+ /** URL from prefetch (or default); used when opening the OIDC / eSignet popup */
10672
+ const [resolvedAuthUrl, setResolvedAuthUrl] = useState(null);
10673
+ const [authActionLoading, setAuthActionLoading] = useState(false);
10674
+ const [authError, setAuthError] = useState(null);
10675
+ const popupRef = useRef(null);
10676
+ const pollTimerRef = useRef(null);
10677
+ const [overlayUrl, setOverlayUrl] = useState(null);
10678
+ const emitHostEvent = useCallback((detail) => {
10679
+ if (typeof window === 'undefined')
10680
+ return;
10681
+ window.dispatchEvent(new CustomEvent('openg2p:id-authentication', {
10682
+ detail: {
10683
+ widgetId,
10684
+ ...detail,
10685
+ },
10686
+ }));
10687
+ }, [widgetId]);
10688
+ const cleanupPopup = useCallback(() => {
10689
+ if (pollTimerRef.current) {
10690
+ window.clearInterval(pollTimerRef.current);
10691
+ pollTimerRef.current = null;
10692
+ }
10693
+ popupRef.current = null;
10694
+ }, []);
10695
+ useEffect(() => {
10696
+ return () => {
10697
+ cleanupPopup();
10698
+ try {
10699
+ popupRef.current?.close?.();
10700
+ }
10701
+ catch {
10702
+ // ignore
10703
+ }
10704
+ };
10705
+ }, [cleanupPopup]);
10706
+ // Provider details are supplied by host; clear any previous resolved URL on provider change.
10707
+ useEffect(() => {
10708
+ setResolvedAuthUrl(null);
10709
+ }, [providerId, providerName]);
10710
+ const openAuthPopup = useCallback((authUrl) => {
10711
+ if (authConfig?.useIframeOverlay === true) {
10712
+ setOverlayUrl(authUrl);
10713
+ emitHostEvent({ type: 'overlay_opened' });
10714
+ return;
10715
+ }
10716
+ const pw = authConfig?.popupWidth ?? 1024;
10717
+ const ph = authConfig?.popupHeight ?? 800;
10718
+ const features = getCenteredPopupFeatures(pw, ph);
10719
+ const popup = window.open(authUrl, `${widgetId}-oidc`, features);
10720
+ if (!popup) {
10721
+ setAuthError('Popup blocked. Please allow popups and try again.');
10722
+ return;
10723
+ }
10724
+ popupRef.current = popup;
10725
+ popup.focus?.();
10726
+ setAuthError(null);
10727
+ emitHostEvent({ type: 'popup_opened' });
10728
+ if (pollTimerRef.current) {
10729
+ window.clearInterval(pollTimerRef.current);
10730
+ pollTimerRef.current = null;
10731
+ }
10732
+ pollTimerRef.current = window.setInterval(() => {
10733
+ try {
10734
+ const closed = !popupRef.current || popupRef.current.closed;
10735
+ if (closed) {
10736
+ cleanupPopup();
10737
+ emitHostEvent({ type: 'popup_closed' });
10738
+ }
10739
+ }
10740
+ catch {
10741
+ // ignore
10742
+ }
10743
+ }, 500);
10744
+ }, [authConfig, cleanupPopup, emitHostEvent, widgetId]);
10745
+ const onAuthenticate = useCallback(async () => {
10746
+ setAuthError(null);
10747
+ if (!authConfig) {
10748
+ setAuthError('Missing widget-auth-config.');
10749
+ return;
10750
+ }
10751
+ const canCallAuthApi = Boolean(dataSourceRequestHandler && authConfig.service && authConfig.authenticateEndpoint);
10752
+ let url = resolvedAuthUrl;
10753
+ if (!url && canCallAuthApi) {
10754
+ setAuthActionLoading(true);
10755
+ try {
10756
+ const basePayload = {
10757
+ register_id: registerId,
10758
+ internal_record_id: internalRecordId,
10759
+ provider_id: providerId,
10760
+ initiated_by_staff_id: initiatedByStaffId,
10761
+ };
10762
+ const requestParams = basePayload;
10763
+ // eslint-disable-next-line no-console
10764
+ console.log('[IdAuthenticationWidget] authenticate_registrant params', requestParams);
10765
+ const resp = await dataSourceRequestHandler(authConfig.service, authConfig.authenticateEndpoint, authConfig.authenticateMethod || 'POST', requestParams);
10766
+ const payload = unwrapPayload(resp);
10767
+ const authUrl = pickAuthorizationUrl(payload, authConfig.authorizationUrlKey);
10768
+ url = authUrl || null;
10769
+ if (authUrl)
10770
+ setResolvedAuthUrl(authUrl);
10771
+ }
10772
+ catch (e) {
10773
+ if (!url) {
10774
+ setAuthError(e?.message || 'Could not load provider URL.');
10775
+ return;
10776
+ }
10777
+ }
10778
+ finally {
10779
+ setAuthActionLoading(false);
10780
+ }
10781
+ }
10782
+ if (!url) {
10783
+ setAuthError('No authorization URL returned from authenticate_registrant.');
10784
+ return;
10785
+ }
10786
+ openAuthPopup(url);
10787
+ }, [
10788
+ authConfig,
10789
+ dataSourceRequestHandler,
10790
+ openAuthPopup,
10791
+ registerId,
10792
+ internalRecordId,
10793
+ initiatedByStaffId,
10794
+ resolvedAuthUrl,
10795
+ ]);
10796
+ useEffect(() => {
10797
+ const successType = authConfig?.successMessageType || 'openg2p:oidc:success';
10798
+ const handler = (event) => {
10799
+ const data = event?.data;
10800
+ if (!data || typeof data !== 'object')
10801
+ return;
10802
+ if (data.type !== successType)
10803
+ return;
10804
+ if (data.widgetId && data.widgetId !== widgetId)
10805
+ return;
10806
+ emitHostEvent({ type: 'authenticated', payload: data });
10807
+ try {
10808
+ popupRef.current?.close?.();
10809
+ }
10810
+ catch {
10811
+ // ignore
10812
+ }
10813
+ cleanupPopup();
10814
+ if (authConfig?.reloadOnSuccess) {
10815
+ window.location.reload();
10816
+ }
10817
+ };
10818
+ window.addEventListener('message', handler);
10819
+ return () => window.removeEventListener('message', handler);
10820
+ }, [authConfig?.reloadOnSuccess, authConfig?.successMessageType, cleanupPopup, emitHostEvent, widgetId]);
10821
+ const cls = `id-auth-widget-${widgetId}`;
10822
+ const statusLabel = useMemo(() => {
10823
+ if (status === 'success')
10824
+ return 'Success';
10825
+ if (status === 'failure')
10826
+ return 'Failure';
10827
+ if (status === 'not_done')
10828
+ return 'Not done';
10829
+ return 'Unknown';
10830
+ }, [status]);
10831
+ const statusColor = useMemo(() => {
10832
+ if (status === 'success')
10833
+ return 'var(--owt-color-success, #16A34A)';
10834
+ if (status === 'failure')
10835
+ return 'var(--owt-color-danger, #DC2626)';
10836
+ if (status === 'not_done')
10837
+ return 'var(--owt-color-warning, #D97706)';
10838
+ return 'var(--owt-color-text-muted, #6B7280)';
10839
+ }, [status]);
10840
+ const buttonBusy = authActionLoading;
10841
+ const buttonDisabled = !authConfig || buttonBusy;
10842
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("style", { children: `
10843
+ .${cls} {
10844
+ width: 100%;
10845
+ font-family: Roboto, sans-serif;
10846
+ }
10847
+
10848
+ /* Two-column field grid; primary action in a bottom band (matches section save/edit pattern). */
10849
+ .${cls} .auth-content {
10850
+ display: flex;
10851
+ flex-direction: column;
10852
+ gap: 0;
10853
+ min-width: 0;
10854
+ }
10855
+
10856
+ .${cls} .auth-grid {
10857
+ display: grid;
10858
+ grid-template-columns: repeat(2, minmax(0, 1fr));
10859
+ gap: 16px 24px;
10860
+ min-width: 0;
10861
+ }
10862
+
10863
+ /* Each field: label (left) + value (right), same as DisplayWidget readonly */
10864
+ .${cls} .auth-cell {
10865
+ display: flex;
10866
+ flex-direction: row;
10867
+ align-items: flex-start;
10868
+ gap: 12px 16px;
10869
+ min-width: 0;
10870
+ }
10871
+
10872
+ .${cls} .auth-cell.auth-cell--full {
10873
+ grid-column: 1 / -1;
10874
+ }
10875
+
10876
+ /* Action cell: no left label spacer, align button to column start */
10877
+ .${cls} .auth-cell.auth-cell--action .auth-label {
10878
+ display: none;
10879
+ }
10880
+ .${cls} .auth-cell.auth-cell--action .auth-value {
10881
+ flex: 1 1 auto;
10882
+ }
10883
+
10884
+ .${cls} .auth-label {
10885
+ flex: 0 0 auto;
10886
+ min-width: 200px;
10887
+ max-width: 40%;
10888
+ font-size: 16px;
10889
+ color: rgba(0, 0, 0, 0.6);
10890
+ font-weight: 500;
10891
+ line-height: 1.45;
10892
+ margin: 0;
10893
+ word-break: break-word;
10894
+ }
10895
+
10896
+ .${cls} .auth-value {
10897
+ flex: 1 1 auto;
10898
+ min-width: 0;
10899
+ font-size: 16px;
10900
+ color: var(--owt-color-text, #111827);
10901
+ font-weight: 500;
10902
+ line-height: 1.45;
10903
+ word-break: break-word;
10904
+ }
10905
+
10906
+ .${cls} .auth-value--foundational {
10907
+ font-size: 18px;
10908
+ font-weight: 700;
10909
+ color: var(--owt-color-primary-dark, #F07B1A);
10910
+ letter-spacing: 0.1px;
10911
+ }
10912
+
10913
+ /* Button is placed inside the grid (next to PSUT) */
10914
+
10915
+ .${cls} .auth-status {
10916
+ display: inline-flex;
10917
+ align-items: center;
10918
+ gap: 8px;
10919
+ width: fit-content;
10920
+ padding: 4px 10px;
10921
+ border-radius: 999px;
10922
+ background: rgba(2, 6, 23, 0.04);
10923
+ border: 1px solid rgba(2, 6, 23, 0.08);
10924
+ font-size: 13px;
10925
+ font-weight: 700;
10926
+ color: var(--owt-color-text, #011627);
10927
+ }
10928
+
10929
+ .${cls} .auth-dot {
10930
+ width: 8px;
10931
+ height: 8px;
10932
+ border-radius: 50%;
10933
+ background: ${statusColor};
10934
+ }
10935
+
10936
+ .${cls} .auth-token {
10937
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
10938
+ font-size: 14px;
10939
+ font-weight: 500;
10940
+ color: var(--owt-color-text, #011627);
10941
+ background: transparent;
10942
+ border: none;
10943
+ border-radius: 0;
10944
+ padding: 0;
10945
+ word-break: break-all;
10946
+ }
10947
+
10948
+ .${cls} .auth-button {
10949
+ /* Match SectionRegistryView Save CTA (SectionRenderer) */
10950
+ font-size: 14px;
10951
+ font-weight: 500;
10952
+ padding: 8px 24px;
10953
+ line-height: 1.5;
10954
+ border-radius: var(--owt-btn-border-radius, 10px);
10955
+ border: 1px solid rgb(237, 124, 34);
10956
+ background-color: rgb(237, 124, 34);
10957
+ color: var(--owt-color-bg, #FFFFFF);
10958
+ font-family: Roboto, sans-serif;
10959
+ cursor: pointer;
10960
+ transition: opacity 0.15s ease;
10961
+ }
10962
+
10963
+ .${cls} .auth-button:disabled {
10964
+ opacity: 0.5;
10965
+ cursor: not-allowed;
10966
+ }
10967
+
10968
+ .${cls} .auth-error {
10969
+ font-size: 12px;
10970
+ color: var(--owt-color-danger, #DC2626);
10971
+ font-weight: 700;
10972
+ line-height: 1.3;
10973
+ text-align: left;
10974
+ max-width: 100%;
10975
+ }
10976
+
10977
+ .${cls} .overlay-backdrop {
10978
+ position: fixed;
10979
+ inset: 0;
10980
+ background: rgba(17, 24, 39, 0.55);
10981
+ z-index: 9999;
10982
+ display: flex;
10983
+ align-items: center;
10984
+ justify-content: center;
10985
+ padding: 24px;
10986
+ }
10987
+
10988
+ .${cls} .overlay-panel {
10989
+ width: min(1100px, 92vw);
10990
+ height: min(820px, 92vh);
10991
+ background: var(--owt-color-bg, #FFFFFF);
10992
+ border-radius: 12px;
10993
+ box-shadow: 0 10px 30px rgba(0,0,0,0.25);
10994
+ overflow: hidden;
10995
+ display: flex;
10996
+ flex-direction: column;
10997
+ }
10998
+
10999
+ .${cls} .overlay-header {
11000
+ display: flex;
11001
+ align-items: center;
11002
+ justify-content: space-between;
11003
+ padding: 10px 14px;
11004
+ border-bottom: 1px solid var(--owt-color-border-light, #E4E4E4);
11005
+ font-family: Roboto, sans-serif;
11006
+ }
11007
+
11008
+ .${cls} .overlay-title {
11009
+ font-size: 14px;
11010
+ color: var(--owt-color-text, #011627);
11011
+ font-weight: 600;
11012
+ min-width: 0;
11013
+ overflow: hidden;
11014
+ text-overflow: ellipsis;
11015
+ white-space: nowrap;
11016
+ }
11017
+
11018
+ .${cls} .overlay-close {
11019
+ border: 1px solid var(--owt-btn-secondary-border, #C4C4C4);
11020
+ background: var(--owt-btn-secondary-bg, #FFFFFF);
11021
+ color: var(--owt-btn-secondary-color, #011627);
11022
+ border-radius: var(--owt-btn-border-radius, 10px);
11023
+ padding: 6px 10px;
11024
+ font-size: 12px;
11025
+ cursor: pointer;
11026
+ }
11027
+
11028
+ .${cls} .overlay-iframe {
11029
+ flex: 1 1 auto;
11030
+ width: 100%;
11031
+ border: none;
11032
+ }
11033
+
11034
+ @media (max-width: 640px) {
11035
+ .${cls} .auth-grid {
11036
+ grid-template-columns: 1fr;
11037
+ }
11038
+ .${cls} .auth-cell {
11039
+ flex-direction: column;
11040
+ align-items: stretch;
11041
+ gap: 4px 0;
11042
+ }
11043
+ .${cls} .auth-label {
11044
+ min-width: 0;
11045
+ max-width: none;
11046
+ }
11047
+ }
11048
+ ` }), jsxRuntimeExports.jsxs("div", { className: cls, children: [overlayUrl ? (jsxRuntimeExports.jsx("div", { className: "overlay-backdrop", role: "dialog", "aria-modal": "true", "aria-label": "Authentication", onClick: (e) => {
11049
+ if (e.target === e.currentTarget) {
11050
+ setOverlayUrl(null);
11051
+ emitHostEvent({ type: 'overlay_closed' });
11052
+ }
11053
+ }, children: jsxRuntimeExports.jsxs("div", { className: "overlay-panel", children: [jsxRuntimeExports.jsxs("div", { className: "overlay-header", children: [jsxRuntimeExports.jsx("div", { className: "overlay-title", children: providerName ? `Authenticate via ${providerName}` : 'Authenticate' }), jsxRuntimeExports.jsx("button", { type: "button", className: "overlay-close", onClick: () => {
11054
+ setOverlayUrl(null);
11055
+ emitHostEvent({ type: 'overlay_closed' });
11056
+ }, children: "Close" })] }), jsxRuntimeExports.jsx("iframe", { className: "overlay-iframe", src: overlayUrl, title: "Authentication" })] }) })) : null, jsxRuntimeExports.jsx("div", { className: "auth-content", children: jsxRuntimeExports.jsxs("div", { className: "auth-grid", children: [jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Foundational ID:" }), jsxRuntimeExports.jsx("div", { className: "auth-value auth-value--foundational", children: displayText(foundationalId) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Last authenticated on:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: tryFormatDateTime(lastAuthenticatedOn) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Expiry date:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: tryFormatDate(expiryDate) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Last authentication status:" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: jsxRuntimeExports.jsxs("div", { className: "auth-status", "aria-label": `Authentication status: ${statusLabel}`, children: [jsxRuntimeExports.jsx("span", { className: "auth-dot" }), jsxRuntimeExports.jsx("span", { children: statusLabel })] }) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", children: "Authentication token (PSUT):" }), jsxRuntimeExports.jsx("div", { className: "auth-value", children: jsxRuntimeExports.jsx("div", { className: "auth-token", children: psut ? String(psut) : '-' }) })] }), jsxRuntimeExports.jsxs("div", { className: "auth-cell auth-cell--action", children: [jsxRuntimeExports.jsx("div", { className: "auth-label", "aria-hidden": true }), jsxRuntimeExports.jsxs("div", { className: "auth-value", children: [jsxRuntimeExports.jsx("button", { type: "button", className: "auth-button", onClick: onAuthenticate, disabled: buttonDisabled, children: buttonBusy ? 'Loading…' : 'Authenticate' }), authError ? (jsxRuntimeExports.jsx("div", { className: "auth-error", style: { marginTop: 8 }, children: authError })) : null] })] })] }) })] })] }));
11057
+ };
11058
+
9736
11059
  /**
9737
11060
  * Register all default/generic widgets
9738
11061
  * This is called automatically when the package is imported
@@ -9762,6 +11085,8 @@ const registerDefaultWidgets = () => {
9762
11085
  widgetRegistry.register({ widget: 'simple-table', component: SimpleTableWidget });
9763
11086
  // Table widget with record-level editing
9764
11087
  widgetRegistry.register({ widget: 'table', component: TableWidget });
11088
+ // Table widget with add/edit popup dialog
11089
+ widgetRegistry.register({ widget: 'dialog-table', component: DialogTableWidget });
9765
11090
  // Group widgets
9766
11091
  widgetRegistry.register({ widget: 'array-widget', component: ArrayWidget });
9767
11092
  widgetRegistry.register({ widget: 'iterable-accordion', component: IterableAccordionWidget });
@@ -9776,6 +11101,8 @@ const registerDefaultWidgets = () => {
9776
11101
  widgetRegistry.register({ widget: 'header-section', component: HeaderSectionWidget });
9777
11102
  // Scores display widget for full-width computed scores display (view-only)
9778
11103
  widgetRegistry.register({ widget: 'scores-display', component: ScoresDisplayWidget });
11104
+ // ID Authentication widget for OIDC-based foundational ID authentication (view-only + action)
11105
+ widgetRegistry.register({ widget: 'id-authentication', component: IdAuthenticationWidget });
9779
11106
  };
9780
11107
  // Auto-register on import
9781
11108
  registerDefaultWidgets();
@@ -10137,5 +11464,5 @@ const translateUISchema = (schema, translate) => {
10137
11464
  };
10138
11465
  };
10139
11466
 
10140
- export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DisplayWidget, FileInputWidget, HeaderSectionWidget, IterableAccordionWidget, JSONEditorPanel, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, PropertyEditor, RadioWidget, ScoresDisplayWidget, SectionBuilder, SectionRenderer, SectionTree, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, VisualBuilderPanel, WidgetEventBus, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, defaultTheme, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, resolveTheme, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTheme, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
11467
+ export { ArrayWidget, BooleanWidget, CheckboxWidget, CurrencyInputWidget, DateInputWidget, DateTimeInputWidget, DialogTableWidget, DisplayWidget, FileInputWidget, HeaderSectionWidget, IdAuthenticationWidget, IterableAccordionWidget, JSONEditorPanel, NumberInputWidget, PanelRenderer, PhoneInputWidget, ProfileWidget, PropertyEditor, RadioWidget, ScoresDisplayWidget, SectionBuilder, SectionRenderer, SectionTree, SectionsContainer, SelectWidget, SimpleTableWidget, TableWidget, TextAreaWidget, TextInputWidget, VisualBuilderPanel, WidgetEventBus, WidgetProvider, WidgetRenderer, applyCaseControl, applyDecimalPrecision, applyMask, createWidgetStore, createZodSchema, defaultTheme, evaluateCondition, filterByCharacterType, formatCurrency, formatDate, formatNumber, formatPhone, formatValue, geoHierarchyBuilder, getApiDataSource, getFormattedNumberLength, getSchemaDataSource, getStaticDataSource, getValueByPath, getWidgetValue, initI18n, isAllowedKey, parseDataPath, parseNumber, registerDefaultWidgets, removeMask, resetAll, resetWidget, resolveTheme, setDataSource, setError, setLoading, setTouched, setValue, setValueByPath, setValues, setWidgetValue, shouldEnableWidget, shouldShowWidget, transformDataSourceOptions, translatePanelConfig, translateUISchema, translateWidgetConfig, useBaseWidget, useGeoWidgetCascade, useWidgetCascade, useWidgetContext, useWidgetEventBus, useWidgetTheme, useWidgetTranslation, validateNumericValue, validateWidget, widgetRegistry };
10141
11468
  //# sourceMappingURL=index.esm.js.map