@topconsultnpm/sdkui-react 6.22.0-dev2.20 → 6.22.0-dev2.22

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.
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useCallback, useMemo, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
3
  import { LayoutModes } from "@topconsultnpm/sdk-ts";
4
4
  import { LoadIndicator } from "devextreme-react/load-indicator";
5
5
  import { SearchResultContext } from "../../../ts";
6
- import { getFullFileExtension, getMultiDetailPanelStates, hasMultiDetailPanelStates, hoverStyle, IconArchive, IconDelete, IconDuplicate, IconEdit, IconEraser, IconMenuVertical, IconRefresh, IconSearch, IconSearchCheck, saveMultiDetailPanelStates, SDKUI_Localizator } from "../../../helper";
6
+ import { getFullFileExtension, getMultiDetailPanelStates, hasMultiDetailPanelStates, hoverStyle, IconArchive, IconDelete, IconDuplicate, IconEdit, IconEraser, IconInfo, IconMenuVertical, IconRefresh, IconSearch, IconSearchCheck, saveMultiDetailPanelStates, SDKUI_Localizator } from "../../../helper";
7
7
  import { TMColors } from "../../../utils/theme";
8
+ import { DeviceType, useDeviceType } from "../../base/TMDeviceProvider";
8
9
  import { TMLayoutWaitingContainer } from "../../base/TMWaitPanel";
9
10
  import TMModal from "../../base/TMModal";
10
11
  import TMDataGrid from "../../base/TMDataGrid";
@@ -41,9 +42,20 @@ const styles = {
41
42
  archiveButton: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: '8px', height: '38px', padding: '0 22px', border: 'none', borderRadius: '10px', fontSize: '14px', fontWeight: 600, letterSpacing: '0.2px', color: '#ffffff', cursor: 'pointer', transition: 'transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease' },
42
43
  checkbox: { width: '16px', height: '16px', accentColor: TMColors.primary, cursor: 'pointer', margin: 0 },
43
44
  };
45
+ /** Sezione mostrata per prima su mobile, dove i pannelli si vedono uno alla volta */
46
+ const MOBILE_SECTION = 'archiveList';
44
47
  const TMMultiMasterDetailDcmtsForm = (props) => {
45
48
  const { dtd, masterDcmt, onClose, allTasks, getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers, showDcmtFormSidebar, showTodoDcmtForm, openFileUploaderPdfEditor, graphometricManager, editPdfForm, openS4TViewer, showToppyDraggableHelpCenter, toppyHelpCenterUsePortal, fetchRemoteCertificates, onOpenS4TViewerRequest, onOpenPdfEditorRequest, onReferenceClick, onTaskCreateRequest, onRefreshAfterAddDcmtToFavs, onFileOpened, passToArchiveCallback, openWGsCopyMoveForm } = props;
46
- const { isLoading, isSummaryReady, phase, setPhase, detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes, selectRelation, backToTypeSelection, isBackLocked, closeModal, sectionsOpen, setSectionOpen, clearLayout, layoutResetToken, canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken, isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync, archivedDetailResults, archivedDetailsCount, isLoadingArchivedDetails, refreshArchivedDetailsAsync, detailColumns, detailSupportsFile, archiveRows, currentRows, focusedRowKey, setFocusedRowKey, selectedRowKeys, setSelectedRowKeys, isOpenAddForm, editingRowId, editingRow, formInputMids, openAddForm, openEditForm, closeAddForm, deleteRow, duplicateRow, upsertRow, updateRowValues, stopOnFirstError, setStopOnFirstError, isArchiving, archiveAllAsync, waitPanel, abortController, } = useMultiMasterDetailDcmts({ dtd, masterDcmt, onClose });
49
+ const { isLoading, isSummaryReady, phase, setPhase, detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes, selectRelation, backToTypeSelection, isBackLocked, closeModal, sectionsOpen, setSectionOpen, openAllSections, clearLayout, layoutResetToken, canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken, isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync, archivedDetailResults, archivedDetailsCount, isLoadingArchivedDetails, refreshArchivedDetailsAsync, detailColumns, detailSupportsFile, archiveRows, currentRows, focusedRowKey, setFocusedRowKey, selectedRowKeys, setSelectedRowKeys, isOpenAddForm, editingRowId, editingRow, formInputMids, openAddForm, openEditForm, closeAddForm, deleteRow, duplicateRow, upsertRow, updateRowValues, stopOnFirstError, setStopOnFirstError, isArchiving, archiveAllAsync, waitPanel, abortController, } = useMultiMasterDetailDcmts({ dtd, masterDcmt, onClose });
50
+ const deviceType = useDeviceType();
51
+ // Su mobile il panel manager mostra un pannello alla volta: le sezioni si raggiungono dalla sua barra
52
+ const isMobile = useMemo(() => deviceType === DeviceType.MOBILE, [deviceType]);
53
+ // Un pannello alla volta non lascia spazio da cedere: le sezioni chiuse nella vista impilata si
54
+ // aprono qui, altrimenti il pannello a schermo mostrerebbe la sola testata
55
+ useEffect(() => {
56
+ if (isMobile && isSummaryReady)
57
+ openAllSections();
58
+ }, [isMobile, isSummaryReady]);
47
59
  // Ricerca nella griglia dei dettagli da archiviare, come in TMSearch (vedi useDocumentOperations)
48
60
  const [showGridSearch, setShowGridSearch] = useState(false);
49
61
  const toggleGridSearchItem = {
@@ -131,9 +143,10 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
131
143
  const optionsMenuItems = useMemo(() => [
132
144
  { name: 'Cancella layout', icon: _jsx(IconEraser, { fontSize: 18, color: TMColors.error }), onClick: clearLayout },
133
145
  ], [clearLayout]);
134
- // Le opzioni riguardano i pannelli del riepilogo: fuori da quella fase non hanno effetto
146
+ // Le opzioni riguardano i pannelli del riepilogo: fuori da quella fase non hanno effetto, e su mobile
147
+ // non c'è un layout da cancellare (i pannelli si vedono uno alla volta, a tutta altezza)
135
148
  const renderOptionsMenu = () => {
136
- if (!isSummaryPhase)
149
+ if (!isSummaryPhase || isMobile)
137
150
  return undefined;
138
151
  return (_jsx(ContextMenu, { trigger: 'left', items: optionsMenuItems, children: _jsx("span", { style: styles.titleMenuIcon, title: SDKUI_Localizator.Options, "aria-label": SDKUI_Localizator.Options, children: _jsx(IconMenuVertical, { fontSize: 16, color: "white" }) }) }));
139
152
  };
@@ -191,13 +204,13 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
191
204
  // ----- Pannelli del riepilogo, uno per sezione ----------------------------------------------
192
205
  const renderSectionsInPanels = () => {
193
206
  const definitions = [
194
- { section: 'masterInfo', name: `Master: ${getDtdDisplayName(dtd)}`, render: renderMasterInfo },
207
+ { section: 'masterInfo', name: `Master: ${getDtdDisplayName(dtd)}`, icon: _jsx(IconInfo, { fontSize: 22 }), count: masterFieldsCount, render: renderMasterInfo },
195
208
  ...(selectedRelation?.detailTID !== undefined
196
- ? [{ section: 'archivedDetails', name: SDKUI_Localizator.ArchivedDetails, render: renderArchivedDetails }]
209
+ ? [{ section: 'archivedDetails', name: SDKUI_Localizator.ArchivedDetails, icon: _jsx(IconSearchCheck, { fontSize: 22 }), count: archivedDetailsCount, render: renderArchivedDetails }]
197
210
  : []),
198
- { section: 'archiveList', name: SDKUI_Localizator.DetailsToArchive, render: renderArchiveList },
211
+ { section: 'archiveList', name: SDKUI_Localizator.DetailsToArchive, icon: _jsx(IconArchive, { fontSize: 22 }), count: currentRows.length, render: renderArchiveList },
199
212
  ];
200
- const panels = definitions.map(definition => ({
213
+ const panels = definitions.map((definition, index) => ({
201
214
  id: definition.section,
202
215
  name: definition.name,
203
216
  currentGroupDirection: 'vertical',
@@ -209,6 +222,8 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
209
222
  // La sezione collassata è alta quanto la sua testata: i separatori non la ingrandiscono
210
223
  isCollapsed: !sectionsOpen[definition.section],
211
224
  },
225
+ // Barra dei pannelli: su mobile è l'unico modo per passare da una sezione all'altra
226
+ toolbarOptions: { icon: definition.icon, visible: true, isActive: true, orderNumber: index + 1, count: definition.count, tooltip: definition.name },
212
227
  }));
213
228
  const sections = panels.map(panel => panel.id);
214
229
  // Le altezze salvate sono il punto di partenza, ma è il collasso corrente a decidere le quote
@@ -220,13 +235,19 @@ const TMMultiMasterDetailDcmtsForm = (props) => {
220
235
  return acc;
221
236
  }, {})
222
237
  : undefined;
223
- const initialDimensions = sectionPanelHeights(sections, sectionsOpen, savedDimensions);
224
- const initialVisibility = sections.reduce((acc, section) => { acc[section] = true; return acc; }, {});
238
+ // Con un pannello alla volta non ci sono quote da spartire: quello a schermo prende tutta l'altezza.
239
+ // Le quote proporzionali (e il layout salvato) valgono solo per i pannelli impilati
240
+ const fullHeights = sections.reduce((acc, section) => { acc[section] = { width: '100%', height: '100%' }; return acc; }, {});
241
+ const initialDimensions = isMobile ? fullHeights : sectionPanelHeights(sections, sectionsOpen, savedDimensions);
242
+ const initialVisibility = sections.reduce((acc, section) => { acc[section] = isMobile ? section === MOBILE_SECTION : true; return acc; }, {});
225
243
  return (_jsx("div", { style: styles.panelsHost, children: _jsx("div", { style: styles.panelsHostFill, children: _jsxs(TMPanelManagerWithPersistenceProvider
226
- // Le dimensioni sono lette solo al montaggio: dopo la cancellazione del layout i pannelli vanno rimontati
227
- , { panels: panels, initialVisibility: initialVisibility, defaultDimensions: initialDimensions, initialDimensions: initialDimensions, initialMobilePanelId: 'archiveList', isPersistenceEnabled: true, persistPanelStates: saveMultiDetailPanelStates, children: [_jsx(TMSectionPanelsHeights, { sections: sections, sectionsOpen: sectionsOpen }), _jsx(TMPanelManagerContainer, { panels: panels, direction: 'vertical', showToolbar: false, minPanelSizePx: SECTION_HEADER_HEIGHT_PX, gutterSizePx: PANEL_GUTTER_PX })] }, layoutResetToken) }) }));
244
+ // Le dimensioni sono lette solo al montaggio: dopo la cancellazione del layout, e al
245
+ // passaggio fra impilato e un pannello alla volta, i pannelli vanno rimontati
246
+ , { panels: panels, initialVisibility: initialVisibility, defaultDimensions: initialDimensions, initialDimensions: initialDimensions, initialMobilePanelId: MOBILE_SECTION,
247
+ // Su mobile il layout è imposto dal dispositivo: salvarlo sovrascriverebbe quello dell'utente
248
+ isPersistenceEnabled: !isMobile, persistPanelStates: !isMobile ? saveMultiDetailPanelStates : undefined, children: [_jsx(TMSectionPanelsHeights, { sections: sections, sectionsOpen: sectionsOpen }), _jsx(TMPanelManagerContainer, { panels: panels, direction: 'vertical', showToolbar: isMobile, minPanelSizePx: SECTION_HEADER_HEIGHT_PX, gutterSizePx: isMobile ? undefined : PANEL_GUTTER_PX })] }, `${layoutResetToken}-${isMobile}`) }) }));
228
249
  };
229
- const renderArchiveSummary = () => (_jsxs("div", { style: styles.summaryContainer, children: [renderSectionsInPanels(), _jsxs("div", { style: styles.footer, children: [_jsxs("label", { style: styles.footerLabel, ...hoverStyle({ borderColor: TMColors.primary, color: TMColors.text_normal }, { borderColor: TMColors.card_background, color: TMColors.label_normal }), children: [_jsx("input", { type: "checkbox", checked: stopOnFirstError, onChange: (e) => setStopOnFirstError(e.target.checked), style: styles.checkbox }), "Ferma l'archiviazione al primo errore"] }), _jsxs("button", { type: "button", onClick: archiveAllAsync, disabled: isArchiveDisabled, style: {
250
+ const renderArchiveSummary = () => (_jsxs("div", { style: styles.summaryContainer, children: [renderSectionsInPanels(), _jsxs("div", { style: styles.footer, children: [_jsxs("label", { style: styles.footerLabel, ...hoverStyle({ borderColor: TMColors.primary, color: TMColors.text_normal }, { borderColor: TMColors.card_background, color: TMColors.label_normal }), children: [_jsx("input", { type: "checkbox", checked: stopOnFirstError, onChange: (e) => setStopOnFirstError(e.target.checked), style: styles.checkbox }), SDKUI_Localizator.StopOnFirstError] }), _jsxs("button", { type: "button", onClick: archiveAllAsync, disabled: isArchiveDisabled, style: {
230
251
  ...styles.archiveButton,
231
252
  background: isArchiveDisabled ? TMColors.disabled : TMColors.success,
232
253
  boxShadow: isArchiveDisabled ? 'none' : `0 4px 14px ${TMColors.success}55`,
@@ -19,6 +19,8 @@ export type AccordionSection = 'masterInfo' | 'archivedDetails' | 'archiveList';
19
19
  */
20
20
  export type SectionPanelState = {
21
21
  isMaximized: boolean;
22
+ canMaximize: boolean;
23
+ canCollapse: boolean;
22
24
  toggleMaximize: () => void;
23
25
  };
24
26
  /** Valore di un metadato letto da un documento: una cella del risultato di ricerca */
@@ -44,13 +46,14 @@ export interface ArchiveRow {
44
46
  * stringhe, quindi il `value` generico di DevExtreme si restringe
45
47
  */
46
48
  export type ArchiveRowCell = Omit<DataGridTypes.ColumnCellTemplateData<ArchiveRow, number>, 'value'> & {
47
- readonly value?: string;
49
+ readonly value?: string | number;
48
50
  };
49
51
  /**
50
52
  * Modifiche di una riga in arrivo dalla griglia (mid -> nuovo valore): in modifica diretta ci sono
51
- * solo i metadati testuali (vedi isInlineEditableMetadata) e l'editor svuotato restituisce null
53
+ * i metadati testuali e numerici (vedi isInlineEditableMetadata), quindi stringhe e numeri,
54
+ * e l'editor svuotato restituisce null
52
55
  */
53
- export type ArchiveRowChanges = Record<string, string | null | undefined>;
56
+ export type ArchiveRowChanges = Record<string, string | number | null | undefined>;
54
57
  /** Aggiornamento di una riga della griglia, con le sole modifiche possibili in linea */
55
58
  export type ArchiveRowUpdatingEvent = Omit<DataGridTypes.RowUpdatingEvent<ArchiveRow, number>, 'newData'> & {
56
59
  newData: ArchiveRowChanges;
@@ -68,10 +71,15 @@ export declare const canArchive: (dtd?: DcmtTypeDescriptor) => boolean;
68
71
  */
69
72
  export declare const INLINE_EDITABLE_CELL_CLASS = "tm-cell-inline-editable";
70
73
  /**
71
- * Modifica diretta in griglia: solo il testo libero. Liste dati, calcolati, numeratori e speciali
72
- * hanno editor e vincoli propri, date e numeri una formattazione dedicata: restano al form
74
+ * Modifica diretta in griglia: testo libero e numeri. Liste dati, calcolati, numeratori e speciali
75
+ * hanno editor e vincoli propri, le date una formattazione dedicata: restano al form
73
76
  */
74
77
  export declare const isInlineEditableMetadata: (md: MetadataDescriptor) => boolean;
78
+ /**
79
+ * Valore di una cella numerica: l'editor della griglia vuole un numero, non la stringa che
80
+ * la riga porta in `__values` per l'archiviazione. Vuoto o non numerico: cella vuota
81
+ */
82
+ export declare const toGridNumber: (value: unknown) => number | undefined;
75
83
  /** Colonne della griglia di riepilogo dai metadati (utente) del tipo documento */
76
84
  export declare const buildColumnsFromDtd: (dtd: DcmtTypeDescriptor | undefined, renderDataListCell: (value: string | Date | number | undefined, dataListID: number, viewMode: DataListViewModes) => React.ReactElement) => Array<IColumnProps>;
77
85
  export declare const getDetailDtdAsync: (detailTID: number | undefined) => Promise<DcmtTypeDescriptor | undefined>;
@@ -6,6 +6,7 @@ import { formatScalarValue, getUserMetadataSorted } from '../../../helper/dcmtsH
6
6
  import { TMColors } from '../../../utils/theme';
7
7
  import { TMDcmtTypeIcon } from '../../viewers/TMTidViewer';
8
8
  import TMTooltip from '../../base/TMTooltip';
9
+ import { DeviceType, useDeviceType } from '../../base/TMDeviceProvider';
9
10
  import { useTMPanelManagerContext } from '../../layout/panelManager/TMPanelManagerContext';
10
11
  // l'elenco da archiviare è l'informazione più importante, quindi prende la fetta maggiore
11
12
  export const SECTION_WEIGHTS = { masterInfo: 25, archivedDetails: 30, archiveList: 45 };
@@ -26,12 +27,22 @@ export const canArchive = (dtd) => dtd?.perm?.canArchive === AccessLevelsEx.Yes
26
27
  */
27
28
  export const INLINE_EDITABLE_CELL_CLASS = 'tm-cell-inline-editable';
28
29
  /**
29
- * Modifica diretta in griglia: solo il testo libero. Liste dati, calcolati, numeratori e speciali
30
- * hanno editor e vincoli propri, date e numeri una formattazione dedicata: restano al form
30
+ * Modifica diretta in griglia: testo libero e numeri. Liste dati, calcolati, numeratori e speciali
31
+ * hanno editor e vincoli propri, le date una formattazione dedicata: restano al form
31
32
  */
32
- export const isInlineEditableMetadata = (md) => md.dataType === MetadataDataTypes.Varchar
33
+ export const isInlineEditableMetadata = (md) => (md.dataType === MetadataDataTypes.Varchar || md.dataType === MetadataDataTypes.Number)
33
34
  && (md.dataDomain === undefined || md.dataDomain === MetadataDataDomains.None)
34
35
  && md.isSystem !== 1 && md.isSystemDerived !== 1;
36
+ /**
37
+ * Valore di una cella numerica: l'editor della griglia vuole un numero, non la stringa che
38
+ * la riga porta in `__values` per l'archiviazione. Vuoto o non numerico: cella vuota
39
+ */
40
+ export const toGridNumber = (value) => {
41
+ if (value === undefined || value === null || value === '')
42
+ return undefined;
43
+ const parsed = typeof value === 'number' ? value : Number(value);
44
+ return Number.isFinite(parsed) ? parsed : undefined;
45
+ };
35
46
  /** Colonne della griglia di riepilogo dai metadati (utente) del tipo documento */
36
47
  export const buildColumnsFromDtd = (dtd, renderDataListCell) => getUserMetadataSorted(dtd).map(md => {
37
48
  const dataListID = md.dataListID ?? 0;
@@ -40,21 +51,25 @@ export const buildColumnsFromDtd = (dtd, renderDataListCell) => getUserMetadataS
40
51
  const dataType = md.dataType;
41
52
  const format = md.format?.format;
42
53
  const formatCulture = md.format?.formatCulture;
43
- // Date e numeri usano la formattazione scalare; le liste dati il render comune
44
- const needsScalarFormat = !isDataList && (dataType === MetadataDataTypes.DateTime || dataType === MetadataDataTypes.Number);
45
54
  const isInlineEditable = isInlineEditableMetadata(md);
55
+ const isNumber = dataType === MetadataDataTypes.Number;
56
+ // Date e numeri usano la formattazione scalare; le liste dati il render comune. La cella
57
+ // modificabile mostra sempre l'editor, quindi non ha un proprio render
58
+ const needsScalarFormat = !isDataList && !isInlineEditable && (dataType === MetadataDataTypes.DateTime || isNumber);
46
59
  return {
47
60
  dataField: String(md.id),
48
61
  caption: (SDK_Globals.useLocalizedName ? md.nameLoc : md.name) ?? md.name ?? '',
49
62
  // La colonna modificabile dichiara il tipo: senza valori DevExtreme non saprebbe che editor usare
50
63
  ...(isInlineEditable
51
64
  ? {
52
- dataType: 'string',
65
+ // I numeri arrivano in cella già come numeri (vedi buildArchiveRow): l'editor è la casella numerica
66
+ dataType: isNumber ? 'number' : 'string',
53
67
  allowEditing: true,
54
68
  showEditorAlways: true,
55
69
  cssClass: INLINE_EDITABLE_CELL_CLASS,
56
- // Lunghezza e obbligatorietà del metadato valgono anche in griglia
57
- editorOptions: md.length ? { maxLength: md.length } : undefined,
70
+ // Lunghezza e obbligatorietà del metadato valgono anche in griglia; sui numeri
71
+ // i limiti restano all'API, in cella basta che sia un numero
72
+ editorOptions: !isNumber && md.length ? { maxLength: md.length } : undefined,
58
73
  validationRules: String(md.isRequired) === '1' ? [{ type: 'required', message: SDKUI_Localizator.RequiredField }] : undefined,
59
74
  }
60
75
  : { allowEditing: false }),
@@ -164,16 +179,19 @@ export const renderSelectDcmtType = ({ detailRelationItems, selectedRelation, on
164
179
  }) })] }));
165
180
  export const renderAccordionHeader = ({ section, isOpen, tooltip, count, title, actions, panel, onToggleOpen }) => {
166
181
  const restBackground = isOpen ? TMColors.toolbar_background : 'transparent';
167
- // Un pannello ingrandito non si collassa: resterebbe la sola testata al posto di tutto il riepilogo
168
- const canToggle = !panel.isMaximized;
182
+ // Un pannello ingrandito non si collassa: resterebbe la sola testata al posto di tutto il riepilogo.
183
+ // Dove i pannelli si vedono uno alla volta (mobile) il collasso è precluso per lo stesso motivo
184
+ const canToggle = panel.canCollapse && !panel.isMaximized;
185
+ // Con un pannello alla volta l'ingrandimento non ha nulla da nascondere
186
+ const canMaximize = panel.canMaximize;
169
187
  const onToggle = () => onToggleOpen(section, !isOpen);
170
188
  const expandTooltip = panel.isMaximized ? SDKUI_Localizator.Minimize : SDKUI_Localizator.ZoomIn;
171
189
  return (_jsxs("div", { role: canToggle ? 'button' : undefined, tabIndex: canToggle ? 0 : undefined, "aria-expanded": canToggle ? isOpen : undefined, style: { ...accordionStyles.accordionHeader, background: restBackground, ...(canToggle ? {} : { cursor: 'default' }) }, onClick: canToggle ? onToggle : undefined,
172
190
  // Doppio click sulla testata: ingrandisce e riduce, come nei pannelli del prodotto
173
- onDoubleClick: () => panel.toggleMaximize(), onKeyDown: canToggle ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
191
+ onDoubleClick: canMaximize ? () => panel.toggleMaximize() : undefined, onKeyDown: canToggle ? (e) => { if (e.key === 'Enter' || e.key === ' ') {
174
192
  e.preventDefault();
175
193
  onToggle();
176
- } } : undefined, title: canToggle ? tooltip : undefined, ...(canToggle ? hoverStyle({ background: TMColors.primary_container }, { background: restBackground }) : {}), children: [_jsxs("span", { style: accordionStyles.accordionTitle, children: [title, _jsx("span", { style: accordionStyles.accordionBadge, children: count })] }), actions, _jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); panel.toggleMaximize(); }, style: accordionStyles.accordionExpandBtn, title: expandTooltip, "aria-label": expandTooltip, ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: panel.isMaximized ? _jsx(IconWindowMinimize, { fontSize: 14 }) : _jsx(IconWindowMaximize, { fontSize: 14 }) }), canToggle && (_jsx(IconChevronRight, { fontSize: 16, color: TMColors.label_normal, style: { transform: isOpen ? 'rotate(90deg)' : 'none', transition: 'transform 0.2s ease', flexShrink: 0 } }))] }));
194
+ } } : undefined, title: canToggle ? tooltip : undefined, ...(canToggle ? hoverStyle({ background: TMColors.primary_container }, { background: restBackground }) : {}), children: [_jsxs("span", { style: accordionStyles.accordionTitle, children: [title, _jsx("span", { style: accordionStyles.accordionBadge, children: count })] }), actions, canMaximize && (_jsx("button", { type: 'button', onClick: (e) => { e.stopPropagation(); panel.toggleMaximize(); }, style: accordionStyles.accordionExpandBtn, title: expandTooltip, "aria-label": expandTooltip, ...hoverStyle({ background: TMColors.primary_container, color: TMColors.primary }, { background: 'transparent', color: TMColors.label_normal }), children: panel.isMaximized ? _jsx(IconWindowMinimize, { fontSize: 14 }) : _jsx(IconWindowMaximize, { fontSize: 14 }) })), canToggle && (_jsx(IconChevronRight, { fontSize: 16, color: TMColors.label_normal, style: { transform: isOpen ? 'rotate(90deg)' : 'none', transition: 'transform 0.2s ease', flexShrink: 0 } }))] }));
177
195
  };
178
196
  /**
179
197
  * Contenuto di un pannello: il contesto del panel manager è leggibile solo dentro l'albero del
@@ -182,6 +200,10 @@ export const renderAccordionHeader = ({ section, isOpen, tooltip, count, title,
182
200
  export const TMSummarySectionSlot = ({ section, render, onBeforeMaximize }) => {
183
201
  const { maximizedPanels, toggleMaximize } = useTMPanelManagerContext();
184
202
  const isMaximized = maximizedPanels.includes(section);
203
+ // Su mobile il panel manager mostra un pannello alla volta: la sezione è già a tutto schermo, e
204
+ // ingrandirla nasconderebbe la barra con cui si passa da una sezione all'altra; chiuderla lascerebbe
205
+ // a schermo la sola testata, perché non ci sono altre sezioni a cui cedere lo spazio
206
+ const isMobile = useDeviceType() === DeviceType.MOBILE;
185
207
  // Una sezione collassata non ha corpo: ingrandirla mostrerebbe un pannello vuoto, senza
186
208
  // nemmeno poterla riaprire (da ingrandita il collasso è bloccato), quindi la si apre prima
187
209
  const toggleMaximizeSection = () => {
@@ -189,7 +211,7 @@ export const TMSummarySectionSlot = ({ section, render, onBeforeMaximize }) => {
189
211
  onBeforeMaximize?.(section);
190
212
  toggleMaximize(section);
191
213
  };
192
- return _jsx(_Fragment, { children: render({ isMaximized, toggleMaximize: toggleMaximizeSection }) });
214
+ return _jsx(_Fragment, { children: render({ isMaximized, canMaximize: !isMobile, canCollapse: !isMobile, toggleMaximize: toggleMaximizeSection }) });
193
215
  };
194
216
  /**
195
217
  * Tiene allineate le altezze dei pannelli al collasso delle sezioni: il panel manager non
@@ -197,7 +219,7 @@ export const TMSummarySectionSlot = ({ section, render, onBeforeMaximize }) => {
197
219
  * Vive dentro il provider perché il contesto è leggibile solo lì
198
220
  */
199
221
  export const TMSectionPanelsHeights = ({ sections, sectionsOpen }) => {
200
- const { panelDimensions, setPanelDimensionsById, maximizedPanels } = useTMPanelManagerContext();
222
+ const { panelDimensions, panelVisibility, setPanelDimensionsById, maximizedPanels } = useTMPanelManagerContext();
201
223
  // Con un pannello ingrandito le quote sono quelle dell'ingrandimento (100% e 0%): riallinearle al
202
224
  // collasso lo svuoterebbe, quindi si aspetta il ritorno alla vista normale per rifare i conti
203
225
  const hasMaximizedPanel = maximizedPanels.length > 0;
@@ -214,8 +236,13 @@ export const TMSectionPanelsHeights = ({ sections, sectionsOpen }) => {
214
236
  }
215
237
  if (hasMaximizedPanel)
216
238
  return;
217
- const heights = sectionPanelHeights(sections, sectionsOpen, dimensionsRef.current);
218
- sections.forEach(section => setPanelDimensionsById(section, heights[section].width, heights[section].height));
219
- }, [sectionsOpen, hasMaximizedPanel]);
239
+ // Solo i pannelli a schermo si spartiscono l'altezza: contando anche i nascosti (su mobile ne
240
+ // resta visibile uno solo) al pannello a schermo toccherebbe la sua sola quota nominale,
241
+ // lasciando vuoto lo spazio dei pannelli che non ci sono. Chi è fuori schermo resta alle proprie
242
+ // quote, che sono quelle da cui il panel manager riparte quando lo si torna a mostrare
243
+ const visibleSections = sections.filter(section => panelVisibility[section] !== false);
244
+ const heights = sectionPanelHeights(visibleSections, sectionsOpen, dimensionsRef.current);
245
+ visibleSections.forEach(section => setPanelDimensionsById(section, heights[section].width, heights[section].height));
246
+ }, [sectionsOpen, hasMaximizedPanel, panelVisibility]);
220
247
  return null;
221
248
  };
@@ -820,6 +820,7 @@ export declare class SDKUI_Localizator {
820
820
  static get Statistics(): "Statistiken" | "Statistics" | "Estadística" | "Statistiques" | "Estatísticas" | "Statistiche";
821
821
  static get Status(): "Status" | "Estado" | "Statut" | "Stato";
822
822
  static get StatusAndAnswer(): "Status und Antwort" | "Status and answer" | "Estado y respuesta" | "Statut et réponse" | "Estado e resposta" | "Stato e risposta";
823
+ static get StopOnFirstError(): "Beim ersten Fehler abbrechen" | "Stop at the first error" | "Detener en el primer error" | "Arrêter à la première erreur" | "Parar no primeiro erro" | "Interrompi al primo errore";
823
824
  static get Subject(): "Betreff" | "Subject" | "Asunto" | "Objet" | "Assunto" | "Oggetto";
824
825
  static get Summary(): "Zusammenfassung" | "Summary" | "Resumen" | "Résumé" | "Resumo" | "Riepilogo";
825
826
  static get SwitchUser(): "Benutzer wechseln" | "Switch user" | "Cambiar usuario" | "Changer d'utilisateur" | "Mudar de usuário" | "Cambia utente";
@@ -8180,6 +8180,16 @@ export class SDKUI_Localizator {
8180
8180
  default: return "Stato e risposta";
8181
8181
  }
8182
8182
  }
8183
+ static get StopOnFirstError() {
8184
+ switch (this._cultureID) {
8185
+ case CultureIDs.De_DE: return "Beim ersten Fehler abbrechen";
8186
+ case CultureIDs.En_US: return "Stop at the first error";
8187
+ case CultureIDs.Es_ES: return "Detener en el primer error";
8188
+ case CultureIDs.Fr_FR: return "Arrêter à la première erreur";
8189
+ case CultureIDs.Pt_PT: return "Parar no primeiro erro";
8190
+ default: return "Interrompi al primo errore";
8191
+ }
8192
+ }
8183
8193
  static get Subject() {
8184
8194
  switch (this._cultureID) {
8185
8195
  case CultureIDs.De_DE: return "Betreff";
@@ -56,6 +56,7 @@ export declare const useMultiMasterDetailDcmts: ({ dtd, masterDcmt, onClose }: U
56
56
  closeModal: () => void;
57
57
  sectionsOpen: Record<AccordionSection, boolean>;
58
58
  setSectionOpen: (section: AccordionSection, isOpen: boolean) => void;
59
+ openAllSections: () => void;
59
60
  clearLayout: () => void;
60
61
  layoutResetToken: number;
61
62
  canUpdateMaster: boolean;
@@ -8,7 +8,7 @@ import TMSpinner from '../components/base/TMSpinner';
8
8
  import ShowAlert from '../components/base/TMAlert';
9
9
  import { TMResultManager } from '../components/forms/TMResultDialog';
10
10
  import { useDataListItem } from './useDataListItem';
11
- import { buildColumnsFromDtd, canArchive, canUpdateMasterCallback, getDetailDtdAsync } from '../components/features/documents/TMMultiMasterDetailDcmtsUtils';
11
+ import { buildColumnsFromDtd, canArchive, canUpdateMasterCallback, getDetailDtdAsync, toGridNumber } from '../components/features/documents/TMMultiMasterDetailDcmtsUtils';
12
12
  /** Fasi del flusso di archiviazione multipla dei dettagli */
13
13
  export var MultiDetailArchivePhase;
14
14
  (function (MultiDetailArchivePhase) {
@@ -44,6 +44,8 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
44
44
  // Struttura del tipo documento selezionato
45
45
  const [detailColumns, setDetailColumns] = useState([]);
46
46
  const [detailSupportsFile, setDetailSupportsFile] = useState(false);
47
+ // Mid numerici del tipo di dettaglio: in griglia le loro celle portano il numero (vedi buildArchiveRow)
48
+ const numericMidsRef = useRef(new Set());
47
49
  // Dettagli già archiviati del tipo selezionato, collegati al master
48
50
  const [archivedDetailResults, setArchivedDetailResults] = useState([]);
49
51
  const [isLoadingArchivedDetails, setIsLoadingArchivedDetails] = useState(false);
@@ -146,6 +148,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
146
148
  if (!selectedRelation?.detailTID) {
147
149
  setDetailColumns([]);
148
150
  setDetailSupportsFile(false);
151
+ numericMidsRef.current = new Set();
149
152
  return;
150
153
  }
151
154
  try {
@@ -153,16 +156,21 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
153
156
  setDetailSupportsFile(detailDtd?.archiveConstraint !== ArchiveConstraints.OnlyMetadata);
154
157
  // Liste dati referenziate dai metadati, servono al render delle celle
155
158
  const dataListIDs = new Set();
159
+ const numericMids = new Set();
156
160
  (detailDtd?.metadata ?? []).forEach(md => {
157
161
  if (md.dataDomain === MetadataDataDomains.DataList && md.dataListID)
158
162
  dataListIDs.add(md.dataListID);
163
+ if (md.dataType === MetadataDataTypes.Number && md.id !== undefined)
164
+ numericMids.add(md.id);
159
165
  });
166
+ numericMidsRef.current = numericMids;
160
167
  await loadDataListsAsync(dataListIDs);
161
168
  setDetailColumns(buildColumnsFromDtd(detailDtd, renderDataListCell));
162
169
  }
163
170
  catch {
164
171
  setDetailColumns([]);
165
172
  setDetailSupportsFile(false);
173
+ numericMidsRef.current = new Set();
166
174
  }
167
175
  };
168
176
  loadDetailStructureAsync();
@@ -218,6 +226,18 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
218
226
  if (section === 'archivedDetails' && isOpen && !areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
219
227
  refreshArchivedDetailsAsync();
220
228
  };
229
+ /**
230
+ * Apre tutte le sezioni senza salvare: serve dove il collasso non è una scelta dell'utente ma una
231
+ * conseguenza del layout (su mobile si vede un pannello alla volta, e una sezione chiusa lascerebbe
232
+ * a schermo la sola testata). Non si persiste per non sovrascrivere le sezioni che l'utente ha
233
+ * chiuso nella vista a pannelli impilati.
234
+ */
235
+ const openAllSections = () => {
236
+ setSectionsOpen(prev => Object.values(prev).every(Boolean) ? prev : { masterInfo: true, archivedDetails: true, archiveList: true });
237
+ // I dettagli già archiviati si caricano come alla prima apertura della sezione
238
+ if (!areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
239
+ refreshArchivedDetailsAsync();
240
+ };
221
241
  /**
222
242
  * Cancella il layout configurato dall'utente (altezze dei pannelli e sezioni collassate), previa
223
243
  * conferma. Il token cambia per rimontare il panel manager, che legge le dimensioni solo all'avvio.
@@ -303,11 +323,13 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
303
323
  },
304
324
  });
305
325
  }, []);
326
+ // I valori del form sono stringhe: le celle numeriche vogliono il numero, richiesto dal loro editor
306
327
  const buildArchiveRow = useCallback((id, tid, values, file) => {
307
328
  const row = { id, __tid: tid, __values: values, __file: file };
308
329
  for (const v of values) {
309
- if (v.mid !== undefined)
310
- row[String(v.mid)] = v.value ?? '';
330
+ if (v.mid === undefined)
331
+ continue;
332
+ row[String(v.mid)] = numericMidsRef.current.has(v.mid) ? toGridNumber(v.value) : (v.value ?? '');
311
333
  }
312
334
  return row;
313
335
  }, []);
@@ -326,7 +348,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
326
348
  closeAddForm();
327
349
  };
328
350
  /**
329
- * Modifica diretta in griglia di uno o più metadati testuali (changes: mid -> valore).
351
+ * Modifica diretta in griglia di uno o più metadati testuali o numerici (changes: mid -> valore).
330
352
  * La riga e i suoi __values sono ricreati: il duplicato, che condivide __values per
331
353
  * riferimento, resta invariato
332
354
  */
@@ -334,9 +356,10 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
334
356
  const changedMids = Object.keys(changes);
335
357
  if (changedMids.length === 0)
336
358
  return;
337
- // La griglia restituisce null sull'editor svuotato: a sistema il metadato va vuoto, non nullo
359
+ // I __values vanno a sistema come stringhe, anche quelli che in griglia sono numeri. La griglia
360
+ // restituisce null sull'editor svuotato: a sistema il metadato va vuoto, non nullo
338
361
  const normalized = {};
339
- changedMids.forEach(mid => { normalized[mid] = changes[mid] ?? ''; });
362
+ changedMids.forEach(mid => { normalized[mid] = changes[mid] === null || changes[mid] === undefined ? '' : String(changes[mid]); });
340
363
  setArchiveRows(prev => prev.map(r => {
341
364
  if (r.id !== rowId)
342
365
  return r;
@@ -352,7 +375,8 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
352
375
  values.push(added);
353
376
  });
354
377
  const updated = { ...r, __values: values };
355
- changedMids.forEach(mid => { updated[mid] = normalized[mid]; });
378
+ // In cella il numero resta un numero, come lo vuole il suo editor
379
+ changedMids.forEach(mid => { updated[mid] = numericMidsRef.current.has(Number(mid)) ? toGridNumber(normalized[mid]) : normalized[mid]; });
356
380
  return updated;
357
381
  }));
358
382
  }, []);
@@ -537,7 +561,7 @@ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
537
561
  detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes,
538
562
  selectRelation, backToTypeSelection, isBackLocked, closeModal,
539
563
  // Sezioni
540
- sectionsOpen, setSectionOpen, clearLayout, layoutResetToken,
564
+ sectionsOpen, setSectionOpen, openAllSections, clearLayout, layoutResetToken,
541
565
  // Master
542
566
  canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken,
543
567
  isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.22.0-dev2.20",
3
+ "version": "6.22.0-dev2.22",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",