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