@topconsultnpm/sdkui-react 6.22.0-dev2.3 → 6.22.0-dev2.30

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.
Files changed (118) hide show
  1. package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +1 -1
  2. package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +1 -1
  3. package/lib/components/base/TMButton.d.ts +13 -0
  4. package/lib/components/base/TMButton.js +137 -5
  5. package/lib/components/base/TMDataGrid.d.ts +2 -0
  6. package/lib/components/base/TMDataGrid.js +81 -8
  7. package/lib/components/base/TMEditorBase.d.ts +2 -0
  8. package/lib/components/base/TMFileManager.js +5 -3
  9. package/lib/components/base/TMFileManagerUtils.d.ts +1 -0
  10. package/lib/components/base/TMFileManagerUtils.js +16 -0
  11. package/lib/components/base/TMModal.d.ts +4 -0
  12. package/lib/components/base/TMModal.js +42 -11
  13. package/lib/components/base/TMPanel.d.ts +1 -0
  14. package/lib/components/base/TMPanel.js +79 -20
  15. package/lib/components/base/TMPopUp.js +3 -0
  16. package/lib/components/base/TMTooltip.d.ts +2 -1
  17. package/lib/components/base/TMTooltip.js +23 -6
  18. package/lib/components/base/TMTreeView.js +108 -67
  19. package/lib/components/choosers/TMDcmtTypeChooser.js +4 -3
  20. package/lib/components/choosers/TMDistinctValues.d.ts +9 -7
  21. package/lib/components/choosers/TMDistinctValues.js +149 -142
  22. package/lib/components/choosers/TMMetadataChooser.js +1 -1
  23. package/lib/components/choosers/TMQuickSearchInfo.d.ts +12 -0
  24. package/lib/components/choosers/TMQuickSearchInfo.js +279 -0
  25. package/lib/components/choosers/TMQuickSearchSettingsForm.d.ts +16 -0
  26. package/lib/components/choosers/TMQuickSearchSettingsForm.js +94 -0
  27. package/lib/components/choosers/TMSelectedValuesSummary.d.ts +13 -0
  28. package/lib/components/choosers/TMSelectedValuesSummary.js +91 -0
  29. package/lib/components/editors/TMDropDown.d.ts +3 -0
  30. package/lib/components/editors/TMDropDown.js +197 -5
  31. package/lib/components/editors/TMMetadataEditor.d.ts +5 -1
  32. package/lib/components/editors/TMMetadataEditor.js +8 -3
  33. package/lib/components/editors/TMTextBox.js +2 -2
  34. package/lib/components/features/documents/TMBatchUpdateForm.js +19 -5
  35. package/lib/components/features/documents/TMDcmtBlog.d.ts +3 -0
  36. package/lib/components/features/documents/TMDcmtBlog.js +4 -4
  37. package/lib/components/features/documents/TMDcmtForm.d.ts +7 -0
  38. package/lib/components/features/documents/TMDcmtForm.js +112 -45
  39. package/lib/components/features/documents/TMDcmtFormActionButtons.d.ts +11 -0
  40. package/lib/components/features/documents/TMDcmtFormActionButtons.js +74 -7
  41. package/lib/components/features/documents/TMDcmtPreview.js +10 -6
  42. package/lib/components/features/documents/TMMasterDetailDcmts.js +95 -33
  43. package/lib/components/features/documents/TMMasterInfoFields.d.ts +19 -0
  44. package/lib/components/features/documents/TMMasterInfoFields.js +186 -0
  45. package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.d.ts +40 -0
  46. package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.js +296 -0
  47. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.d.ts +363 -0
  48. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.js +259 -0
  49. package/lib/components/features/documents/TMRelationViewer.d.ts +14 -1
  50. package/lib/components/features/documents/TMRelationViewer.js +69 -20
  51. package/lib/components/features/search/TMSearch.js +29 -16
  52. package/lib/components/features/search/TMSearchQueryEditor.js +5 -3
  53. package/lib/components/features/search/TMSearchQueryPanel.d.ts +1 -1
  54. package/lib/components/features/search/TMSearchQueryPanel.js +28 -7
  55. package/lib/components/features/search/TMSearchResult.d.ts +9 -0
  56. package/lib/components/features/search/TMSearchResult.js +419 -108
  57. package/lib/components/features/search/TMTreeSelector.js +18 -5
  58. package/lib/components/features/workflow/TMWorkflowPopup.d.ts +10 -0
  59. package/lib/components/features/workflow/TMWorkflowPopup.js +8 -4
  60. package/lib/components/layout/panelManager/TMPanelManagerContainer.d.ts +1 -0
  61. package/lib/components/layout/panelManager/TMPanelManagerContainer.js +24 -14
  62. package/lib/components/layout/panelManager/TMPanelManagerContext.js +68 -93
  63. package/lib/components/layout/panelManager/TMPanelWrapper.js +2 -2
  64. package/lib/components/layout/panelManager/types.d.ts +3 -0
  65. package/lib/components/layout/panelManager/utils.js +13 -11
  66. package/lib/components/viewers/TMMidViewer.js +4 -1
  67. package/lib/components/viewers/TMTidViewer.js +25 -11
  68. package/lib/helper/SDKUI_Globals.d.ts +82 -0
  69. package/lib/helper/SDKUI_Globals.js +119 -0
  70. package/lib/helper/SDKUI_Localizator.d.ts +24 -0
  71. package/lib/helper/SDKUI_Localizator.js +240 -0
  72. package/lib/helper/TMIcons.d.ts +1 -0
  73. package/lib/helper/TMIcons.js +3 -0
  74. package/lib/helper/dataGridSearchHelper.d.ts +28 -0
  75. package/lib/helper/dataGridSearchHelper.js +51 -0
  76. package/lib/helper/dcmtsHelper.d.ts +20 -0
  77. package/lib/helper/dcmtsHelper.js +58 -1
  78. package/lib/helper/helpers.d.ts +20 -0
  79. package/lib/helper/helpers.js +55 -1
  80. package/lib/helper/index.d.ts +1 -0
  81. package/lib/helper/index.js +1 -0
  82. package/lib/helper/queryHelper.d.ts +2 -0
  83. package/lib/helper/queryHelper.js +3 -1
  84. package/lib/hooks/tmDistinctValuesGridHelper.d.ts +16 -0
  85. package/lib/hooks/tmDistinctValuesGridHelper.js +31 -0
  86. package/lib/hooks/useArchiveListForm.d.ts +39 -0
  87. package/lib/hooks/useArchiveListForm.js +46 -0
  88. package/lib/hooks/useCaseFlowApprove.d.ts +56 -0
  89. package/lib/hooks/useCaseFlowApprove.js +157 -0
  90. package/lib/hooks/useDataUserIdItem.d.ts +1 -0
  91. package/lib/hooks/useDataUserIdItem.js +1 -0
  92. package/lib/hooks/useDocumentOperations.d.ts +2 -0
  93. package/lib/hooks/useDocumentOperations.js +28 -5
  94. package/lib/hooks/useMultiMasterDetailDcmts.d.ts +100 -0
  95. package/lib/hooks/useMultiMasterDetailDcmts.js +587 -0
  96. package/lib/hooks/useRelatedDocuments.js +2 -26
  97. package/lib/hooks/useTMDistinctValuesMetadataDisplay.d.ts +18 -0
  98. package/lib/hooks/useTMDistinctValuesMetadataDisplay.js +103 -0
  99. package/lib/hooks/useTMDistinctValuesQuickSearch.d.ts +49 -0
  100. package/lib/hooks/useTMDistinctValuesQuickSearch.js +307 -0
  101. package/lib/hooks/useTMDistinctValuesSelection.d.ts +42 -0
  102. package/lib/hooks/useTMDistinctValuesSelection.js +103 -0
  103. package/lib/hooks/useTMDistinctValuesSource.d.ts +29 -0
  104. package/lib/hooks/useTMDistinctValuesSource.js +105 -0
  105. package/lib/index.d.ts +1 -0
  106. package/lib/index.js +1 -0
  107. package/lib/services/caseflow/CaseFlowService.d.ts +236 -0
  108. package/lib/services/caseflow/CaseFlowService.js +693 -0
  109. package/lib/services/caseflow/CaseFlowTemplateCacheService.d.ts +28 -0
  110. package/lib/services/caseflow/CaseFlowTemplateCacheService.js +121 -0
  111. package/lib/services/caseflow/caseFlowNameCache.d.ts +32 -0
  112. package/lib/services/caseflow/caseFlowNameCache.js +116 -0
  113. package/lib/services/caseflow/caseFlowWorkItemUtils.d.ts +61 -0
  114. package/lib/services/caseflow/caseFlowWorkItemUtils.js +113 -0
  115. package/lib/services/index.d.ts +4 -0
  116. package/lib/services/index.js +4 -0
  117. package/lib/services/platform_services.d.ts +1 -1
  118. package/package.json +66 -66
@@ -0,0 +1,587 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { ArchiveConstraints, ArchiveEngineByID, MetadataDataDomains, MetadataDataTypes, RelationCacheService, RelationTypes, ResultTypes, SDK_Globals } from '@topconsultnpm/sdk-ts';
3
+ import { MetadataValueDescriptorEx } from '../ts';
4
+ import { clearMultiDetailLayout, Globalization, getExceptionMessage, getMultiDetailSectionsOpen, hasMultiDetailLayout, saveMultiDetailSectionsOpen, SDKUI_Localizator } from '../helper';
5
+ import { getDcmtMetadataByMid, getUserMetadataSorted, mapAssociationsFromMetadata } from '../helper/dcmtsHelper';
6
+ import { ButtonNames, TMExceptionBoxManager, TMMessageBoxManager } from '../components/base/TMPopUp';
7
+ import TMSpinner from '../components/base/TMSpinner';
8
+ import ShowAlert from '../components/base/TMAlert';
9
+ import { TMResultManager } from '../components/forms/TMResultDialog';
10
+ import { useDataListItem } from './useDataListItem';
11
+ import { buildColumnsFromDtd, canArchive, canUpdateMasterCallback, getDetailDtdAsync, toGridNumber } from '../components/features/documents/TMMultiMasterDetailDcmtsUtils';
12
+ /** Fasi del flusso di archiviazione multipla dei dettagli */
13
+ export var MultiDetailArchivePhase;
14
+ (function (MultiDetailArchivePhase) {
15
+ /** Scelta del tipo documento */
16
+ MultiDetailArchivePhase[MultiDetailArchivePhase["SelectDcmtType"] = 0] = "SelectDcmtType";
17
+ /** Riepilogo delle archiviazioni */
18
+ MultiDetailArchivePhase[MultiDetailArchivePhase["ArchiveSummary"] = 1] = "ArchiveSummary";
19
+ })(MultiDetailArchivePhase || (MultiDetailArchivePhase = {}));
20
+ const CLOSED_WAIT_PANEL = {
21
+ showWaitPanel: false,
22
+ waitPanelTitle: '',
23
+ showWaitPanelPrimary: false,
24
+ waitPanelTextPrimary: '',
25
+ waitPanelValuePrimary: 0,
26
+ waitPanelMaxValuePrimary: 0,
27
+ showWaitPanelSecondary: false,
28
+ waitPanelTextSecondary: '',
29
+ waitPanelValueSecondary: 0,
30
+ waitPanelMaxValueSecondary: 0,
31
+ };
32
+ /**
33
+ * Logica dell'archiviazione multipla dei dettagli: relazioni, sezioni, righe del riepilogo e
34
+ * archiviazione. Il rendering sta in TMMultiMasterDetailDcmtsForm
35
+ */
36
+ export const useMultiMasterDetailDcmts = ({ dtd, masterDcmt, onClose }) => {
37
+ const { renderDataListCell, loadDataListsAsync } = useDataListItem();
38
+ const [isLoading, setIsLoading] = useState(true);
39
+ const [phase, setPhase] = useState(MultiDetailArchivePhase.SelectDcmtType);
40
+ const [detailRelationItems, setDetailRelationItems] = useState([]);
41
+ const [selectedRelation, setSelectedRelation] = useState(undefined);
42
+ // Il mount della griglia è differito di un frame, così stepper e titolo si dipingono subito
43
+ const [isSummaryReady, setIsSummaryReady] = useState(false);
44
+ // Struttura del tipo documento selezionato
45
+ const [detailColumns, setDetailColumns] = useState([]);
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());
49
+ // Dettagli già archiviati del tipo selezionato, collegati al master
50
+ const [archivedDetailResults, setArchivedDetailResults] = useState([]);
51
+ const [isLoadingArchivedDetails, setIsLoadingArchivedDetails] = useState(false);
52
+ const areArchivedDetailsLoadedRef = useRef(false);
53
+ // Sezioni aperte/collassate, come le ha lasciate l'utente
54
+ const [sectionsOpen, setSectionsOpen] = useState(getMultiDetailSectionsOpen());
55
+ // Cambia a ogni cancellazione del layout, per rimontare i pannelli sulle dimensioni di default
56
+ const [layoutResetToken, setLayoutResetToken] = useState(0);
57
+ // Layout dell'utente nel local storage: senza non c'è niente da cancellare (è uno stato perché le altezze le salva il panel manager)
58
+ const [hasSavedLayout, setHasSavedLayout] = useState(hasMultiDetailLayout());
59
+ // Master
60
+ const [isOpenMasterForm, setIsOpenMasterForm] = useState(false);
61
+ const [masterInfoReloadToken, setMasterInfoReloadToken] = useState(0);
62
+ // Righe del riepilogo
63
+ const [archiveRows, setArchiveRows] = useState([]);
64
+ // La modifica in cella aggiorna la riga fuori dallo stato (vedi updateRowValues)
65
+ const archiveRowsRef = useRef(archiveRows);
66
+ archiveRowsRef.current = archiveRows;
67
+ const rowIdRef = useRef(0);
68
+ const [focusedRowKey, setFocusedRowKey] = useState(undefined);
69
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
70
+ // Form di inserimento/modifica di una riga
71
+ const [isOpenAddForm, setIsOpenAddForm] = useState(false);
72
+ const [editingRowId, setEditingRowId] = useState(undefined); // Undefined = inserimento
73
+ const [associationInputMids, setAssociationInputMids] = useState([]); // Associazioni pre-compilate dal master
74
+ // Archiviazione finale
75
+ const [stopOnFirstError, setStopOnFirstError] = useState(true);
76
+ const [isArchiving, setIsArchiving] = useState(false);
77
+ const [waitPanel, setWaitPanel] = useState(CLOSED_WAIT_PANEL);
78
+ const abortControllerRef = useRef(undefined);
79
+ const selectedItem = detailRelationItems.find(item => item.relation === selectedRelation);
80
+ const hasMultipleTypes = detailRelationItems.length > 1;
81
+ const canUpdateMaster = canUpdateMasterCallback(dtd, masterDcmt);
82
+ const masterFieldsCount = useMemo(() => getUserMetadataSorted(dtd).length, [dtd]);
83
+ // Nel riepilogo si mostrano solo le righe del tipo selezionato
84
+ const currentRows = useMemo(() => archiveRows.filter(r => r.__tid === selectedRelation?.detailTID), [archiveRows, selectedRelation?.detailTID]);
85
+ const archivedDetailsCount = archivedDetailResults.reduce((total, r) => total + (r.dcmtsFound ?? r.jsonResult?.length ?? 0), 0);
86
+ // =============================================================================================
87
+ // MASTER
88
+ // =============================================================================================
89
+ // Metadati del master indicizzati per mid: il master è fisso, quindi si legge una sola volta
90
+ // e la cache si invalida solo se cambia il documento (TID/DID) o dopo un salvataggio
91
+ const masterMetadataCacheRef = useRef(undefined);
92
+ const getMasterMetadataAsync = () => {
93
+ const key = `${masterDcmt?.TID}_${masterDcmt?.DID}`;
94
+ if (masterMetadataCacheRef.current?.key !== key) {
95
+ masterMetadataCacheRef.current = { key, promise: getDcmtMetadataByMid(masterDcmt) };
96
+ }
97
+ return masterMetadataCacheRef.current.promise;
98
+ };
99
+ const handleMasterSavedAsync = async () => {
100
+ masterMetadataCacheRef.current = undefined;
101
+ setMasterInfoReloadToken(token => token + 1);
102
+ setIsOpenMasterForm(false);
103
+ };
104
+ // Associazioni della relazione valorizzate dal master, per pre-compilare il dettaglio
105
+ const getMasterAssociationsForDetailAsync = async (relation) => {
106
+ if (!relation)
107
+ return [];
108
+ return mapAssociationsFromMetadata(relation, 'detail', await getMasterMetadataAsync());
109
+ };
110
+ // =============================================================================================
111
+ // RELAZIONI E STRUTTURA DEL DETTAGLIO
112
+ // =============================================================================================
113
+ // Relazioni di dettaglio archiviabili; con una sola relazione si salta la scelta del tipo
114
+ useEffect(() => {
115
+ const loadAsync = async () => {
116
+ try {
117
+ setIsLoading(true);
118
+ TMSpinner.show({ description: SDKUI_Localizator.Loading });
119
+ const relations = await RelationCacheService.GetAllAsync(SDK_Globals.tmSession);
120
+ if (!relations)
121
+ throw new Error('Impossibile caricare le relazioni.');
122
+ // Relazioni in cui il tipo corrente è master: i target sono i dettagli archiviabili
123
+ const detailRelations = relations.filter(r => r.masterTID === dtd?.id
124
+ && r.relationType !== RelationTypes.ManyToMany
125
+ && r.associations
126
+ && r.associations.length > 0);
127
+ const items = await Promise.all(detailRelations.map(async (relation) => ({
128
+ relation,
129
+ detailDtd: await getDetailDtdAsync(relation.detailTID),
130
+ })));
131
+ setDetailRelationItems(items);
132
+ if (items.length === 1) {
133
+ setSelectedRelation(items[0].relation);
134
+ setPhase(MultiDetailArchivePhase.ArchiveSummary);
135
+ }
136
+ else {
137
+ setPhase(MultiDetailArchivePhase.SelectDcmtType);
138
+ }
139
+ }
140
+ catch (error) {
141
+ TMExceptionBoxManager.show({ exception: error });
142
+ }
143
+ finally {
144
+ TMSpinner.hide();
145
+ setIsLoading(false);
146
+ }
147
+ };
148
+ loadAsync();
149
+ }, []);
150
+ // Colonne della griglia di riepilogo e supporto al file del tipo selezionato
151
+ useEffect(() => {
152
+ const loadDetailStructureAsync = async () => {
153
+ if (!selectedRelation?.detailTID) {
154
+ setDetailColumns([]);
155
+ setDetailSupportsFile(false);
156
+ numericMidsRef.current = new Set();
157
+ return;
158
+ }
159
+ try {
160
+ const detailDtd = await getDetailDtdAsync(selectedRelation.detailTID);
161
+ setDetailSupportsFile(detailDtd?.archiveConstraint !== ArchiveConstraints.OnlyMetadata);
162
+ // Liste dati referenziate dai metadati, servono al render delle celle
163
+ const dataListIDs = new Set();
164
+ const numericMids = new Set();
165
+ (detailDtd?.metadata ?? []).forEach(md => {
166
+ if (md.dataDomain === MetadataDataDomains.DataList && md.dataListID)
167
+ dataListIDs.add(md.dataListID);
168
+ if (md.dataType === MetadataDataTypes.Number && md.id !== undefined)
169
+ numericMids.add(md.id);
170
+ });
171
+ numericMidsRef.current = numericMids;
172
+ await loadDataListsAsync(dataListIDs);
173
+ setDetailColumns(buildColumnsFromDtd(detailDtd, renderDataListCell));
174
+ }
175
+ catch {
176
+ setDetailColumns([]);
177
+ setDetailSupportsFile(false);
178
+ numericMidsRef.current = new Set();
179
+ }
180
+ };
181
+ loadDetailStructureAsync();
182
+ }, [selectedRelation?.detailTID]);
183
+ useEffect(() => {
184
+ if (phase !== MultiDetailArchivePhase.ArchiveSummary) {
185
+ setIsSummaryReady(false);
186
+ return;
187
+ }
188
+ setIsSummaryReady(false);
189
+ const rafId = requestAnimationFrame(() => setIsSummaryReady(true));
190
+ return () => cancelAnimationFrame(rafId);
191
+ }, [phase, selectedRelation?.detailTID]);
192
+ // =============================================================================================
193
+ // DETTAGLI GIÀ ARCHIVIATI
194
+ // =============================================================================================
195
+ const refreshArchivedDetailsAsync = async () => {
196
+ const detailTID = selectedRelation?.detailTID;
197
+ if (detailTID === undefined || !masterDcmt?.TID || !masterDcmt?.DID)
198
+ return;
199
+ try {
200
+ setIsLoadingArchivedDetails(true);
201
+ const searchEngine = SDK_Globals.tmSession?.NewSearchEngine();
202
+ const results = await searchEngine?.GetAllDetailDcmtsAsync(masterDcmt.TID, masterDcmt.DID) ?? [];
203
+ let filtered = results.filter(r => selectedRelation?.id !== undefined && r.relationID === selectedRelation.id);
204
+ if (filtered.length === 0)
205
+ filtered = results.filter(r => r.fromTID === detailTID);
206
+ setArchivedDetailResults(filtered);
207
+ areArchivedDetailsLoadedRef.current = true;
208
+ }
209
+ catch (error) {
210
+ TMExceptionBoxManager.show({ exception: error });
211
+ }
212
+ finally {
213
+ setIsLoadingArchivedDetails(false);
214
+ }
215
+ };
216
+ // I risultati seguono il tipo documento selezionato
217
+ useEffect(() => {
218
+ setArchivedDetailResults([]);
219
+ areArchivedDetailsLoadedRef.current = false;
220
+ if (selectedRelation?.detailTID !== undefined && sectionsOpen.archivedDetails)
221
+ refreshArchivedDetailsAsync();
222
+ }, [selectedRelation?.detailTID]);
223
+ // =============================================================================================
224
+ // SEZIONI
225
+ // =============================================================================================
226
+ const setSectionOpen = (section, isOpen) => {
227
+ const next = { ...sectionsOpen, [section]: isOpen };
228
+ setSectionsOpen(next);
229
+ saveMultiDetailSectionsOpen(next);
230
+ // Riaperte tutte le sezioni il collasso torna ai default: senza altezze salvate non c'è più layout
231
+ setHasSavedLayout(hasMultiDetailLayout());
232
+ // I dettagli già archiviati si caricano alla prima apertura della sezione
233
+ if (section === 'archivedDetails' && isOpen && !areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
234
+ refreshArchivedDetailsAsync();
235
+ };
236
+ /**
237
+ * Apre tutte le sezioni senza salvare: serve dove il collasso non è una scelta dell'utente ma una
238
+ * conseguenza del layout (su mobile si vede un pannello alla volta, e una sezione chiusa lascerebbe
239
+ * a schermo la sola testata). Non si persiste per non sovrascrivere le sezioni che l'utente ha
240
+ * chiuso nella vista a pannelli impilati.
241
+ */
242
+ const openAllSections = () => {
243
+ setSectionsOpen(prev => Object.values(prev).every(Boolean) ? prev : { masterInfo: true, archivedDetails: true, archiveList: true });
244
+ // I dettagli già archiviati si caricano come alla prima apertura della sezione
245
+ if (!areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
246
+ refreshArchivedDetailsAsync();
247
+ };
248
+ /**
249
+ * Cancella il layout configurato dall'utente (altezze dei pannelli e sezioni collassate), previa
250
+ * conferma. Il token cambia per rimontare il panel manager, che legge le dimensioni solo all'avvio.
251
+ */
252
+ const clearLayout = () => {
253
+ TMMessageBoxManager.show({
254
+ title: 'Cancella layout',
255
+ message: 'Vuoi cancellare il layout?',
256
+ buttons: [ButtonNames.YES, ButtonNames.NO],
257
+ onButtonClick: (button) => {
258
+ if (button !== ButtonNames.YES)
259
+ return;
260
+ clearMultiDetailLayout();
261
+ const restored = getMultiDetailSectionsOpen();
262
+ setSectionsOpen(restored);
263
+ setLayoutResetToken(prev => prev + 1);
264
+ setHasSavedLayout(false);
265
+ // Le sezioni riaperte dal ripristino caricano i dettagli già archiviati come alla prima apertura
266
+ if (restored.archivedDetails && !areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
267
+ refreshArchivedDetailsAsync();
268
+ ShowAlert({ message: 'Layout cancellato', mode: 'success', title: 'Cancella layout', duration: 3000 });
269
+ },
270
+ });
271
+ };
272
+ // =============================================================================================
273
+ // NAVIGAZIONE
274
+ // =============================================================================================
275
+ const selectRelation = (item) => {
276
+ setSelectedRelation(item.relation);
277
+ setPhase(MultiDetailArchivePhase.ArchiveSummary);
278
+ };
279
+ const backToTypeSelection = () => {
280
+ setSelectedRelation(undefined);
281
+ setPhase(MultiDetailArchivePhase.SelectDcmtType);
282
+ };
283
+ // Con righe inserite per il tipo corrente il ritorno alla scelta del tipo è bloccato
284
+ const isBackLocked = currentRows.length > 0;
285
+ const closeModal = () => {
286
+ if (archiveRows.length === 0) {
287
+ onClose();
288
+ return;
289
+ }
290
+ TMMessageBoxManager.show({
291
+ title: SDKUI_Localizator.Archive,
292
+ message: SDKUI_Localizator.ExitWithoutArchiving_Confirm,
293
+ buttons: [ButtonNames.YES, ButtonNames.NO],
294
+ onButtonClick: (button) => { if (button === ButtonNames.YES)
295
+ onClose(); },
296
+ });
297
+ };
298
+ // =============================================================================================
299
+ // RIGHE DEL RIEPILOGO
300
+ // =============================================================================================
301
+ const openAddForm = () => {
302
+ if (!selectedRelation?.detailTID)
303
+ return;
304
+ setEditingRowId(undefined);
305
+ setIsOpenAddForm(true);
306
+ };
307
+ // Identità stabile: la griglia dei dettagli la usa nelle sue callback memoizzate
308
+ const openEditForm = useCallback((row) => {
309
+ if (!row)
310
+ return;
311
+ setEditingRowId(row.id);
312
+ setIsOpenAddForm(true);
313
+ }, []);
314
+ const closeAddForm = () => {
315
+ setEditingRowId(undefined);
316
+ setIsOpenAddForm(false);
317
+ };
318
+ const deleteRow = useCallback((row) => {
319
+ if (!row)
320
+ return;
321
+ TMMessageBoxManager.show({
322
+ title: SDKUI_Localizator.Remove,
323
+ message: SDKUI_Localizator.RemoveDetailFromSummary_Confirm,
324
+ buttons: [ButtonNames.YES, ButtonNames.NO],
325
+ onButtonClick: (button) => {
326
+ if (button !== ButtonNames.YES)
327
+ return;
328
+ setArchiveRows(prev => prev.filter(r => r.id !== row.id));
329
+ setFocusedRowKey(prev => (prev === row.id ? undefined : prev));
330
+ setSelectedRowKeys(prev => prev.filter(k => k !== row.id));
331
+ },
332
+ });
333
+ }, []);
334
+ // I valori del form sono stringhe: le celle numeriche vogliono il numero, richiesto dal loro editor
335
+ const buildArchiveRow = useCallback((id, tid, values, file) => {
336
+ const row = { id, __tid: tid, __values: values, __file: file };
337
+ for (const v of values) {
338
+ if (v.mid === undefined)
339
+ continue;
340
+ row[String(v.mid)] = numericMidsRef.current.has(v.mid) ? toGridNumber(v.value) : (v.value ?? '');
341
+ }
342
+ return row;
343
+ }, []);
344
+ // Aggiunge/aggiorna nel riepilogo la riga compilata nel form, senza archiviare a sistema
345
+ const upsertRow = (values, file) => {
346
+ const tid = selectedRelation?.detailTID;
347
+ if (tid === undefined)
348
+ return;
349
+ if (editingRowId !== undefined) {
350
+ setArchiveRows(prev => prev.map(r => (r.id === editingRowId ? buildArchiveRow(editingRowId, tid, values, file) : r)));
351
+ }
352
+ else {
353
+ rowIdRef.current += 1;
354
+ setArchiveRows(prev => [...prev, buildArchiveRow(rowIdRef.current, tid, values, file)]);
355
+ }
356
+ closeAddForm();
357
+ };
358
+ /**
359
+ * Modifica diretta in griglia di uno o più metadati testuali o numerici (changes: mid -> valore).
360
+ * La riga è aggiornata sul posto e non con setArchiveRows: un nuovo riferimento all'array
361
+ * ricaricherebbe la griglia a ogni cella confermata, perdendo il clic sulla cella successiva.
362
+ * L'array __values è sostituito, non modificato: il duplicato, che lo condivide, resta invariato
363
+ */
364
+ const updateRowValues = useCallback((rowId, changes) => {
365
+ const changedMids = Object.keys(changes);
366
+ if (changedMids.length === 0)
367
+ return;
368
+ const row = archiveRowsRef.current.find(r => r.id === rowId);
369
+ if (!row)
370
+ return;
371
+ // I __values vanno a sistema come stringhe, anche quelli che in griglia sono numeri. La griglia
372
+ // restituisce null sull'editor svuotato: a sistema il metadato va vuoto, non nullo
373
+ const normalized = {};
374
+ changedMids.forEach(mid => { normalized[mid] = changes[mid] === null || changes[mid] === undefined ? '' : String(changes[mid]); });
375
+ const values = row.__values.map(v => (v.mid !== undefined && normalized[String(v.mid)] !== undefined
376
+ ? { ...v, value: normalized[String(v.mid)] }
377
+ : v));
378
+ // Un metadato assente dai valori del form non arriverebbe all'archiviazione
379
+ changedMids.filter(mid => !values.some(v => String(v.mid) === mid)).forEach(mid => {
380
+ const added = new MetadataValueDescriptorEx();
381
+ added.tid = row.__tid;
382
+ added.mid = Number(mid);
383
+ added.value = normalized[mid];
384
+ values.push(added);
385
+ });
386
+ row.__values = values;
387
+ // In cella il numero resta un numero, come lo vuole il suo editor
388
+ changedMids.forEach(mid => { row[mid] = numericMidsRef.current.has(Number(mid)) ? toGridNumber(normalized[mid]) : normalized[mid]; });
389
+ }, []);
390
+ // I __values sono condivisi per riferimento: la modifica sostituisce l'intero array, quindi non intacca il duplicato
391
+ const duplicateRow = useCallback((row) => {
392
+ if (!row)
393
+ return;
394
+ rowIdRef.current += 1;
395
+ setArchiveRows(prev => [...prev, buildArchiveRow(rowIdRef.current, row.__tid, row.__values, row.__file)]);
396
+ }, [buildArchiveRow]);
397
+ // All'apertura del form pre-compila i MID di collegamento valorizzati dal master
398
+ useEffect(() => {
399
+ if (!isOpenAddForm || !selectedRelation) {
400
+ setAssociationInputMids([]);
401
+ return;
402
+ }
403
+ let cancelled = false;
404
+ getMasterAssociationsForDetailAsync(selectedRelation)
405
+ .then(associations => { if (!cancelled)
406
+ setAssociationInputMids(associations); })
407
+ .catch(() => { if (!cancelled)
408
+ setAssociationInputMids([]); });
409
+ return () => { cancelled = true; };
410
+ }, [isOpenAddForm, selectedRelation]);
411
+ // MID pre-compilati: in inserimento i collegamenti dal master, in modifica i valori della riga
412
+ const editingRow = editingRowId !== undefined ? archiveRows.find(r => r.id === editingRowId) : undefined;
413
+ const nextFormInputMids = editingRow
414
+ ? editingRow.__values
415
+ .filter(v => v.mid !== undefined && v.value !== undefined && v.value !== null && v.value !== '')
416
+ .map(v => ({ mid: v.mid, value: String(v.value) }))
417
+ : associationInputMids;
418
+ // Il form riapplica i valori quando cambia il riferimento di inputMids: si mantiene la stessa
419
+ // istanza finché il contenuto non cambia, così le modifiche dell'utente non vengono sovrascritte
420
+ const formInputMidsSignature = nextFormInputMids.map(o => `${o.mid}=${o.value}`).join('|');
421
+ const formInputMidsRef = useRef(nextFormInputMids);
422
+ const formInputMidsSignatureRef = useRef(formInputMidsSignature);
423
+ if (formInputMidsSignatureRef.current !== formInputMidsSignature) {
424
+ formInputMidsSignatureRef.current = formInputMidsSignature;
425
+ formInputMidsRef.current = nextFormInputMids;
426
+ }
427
+ const formInputMids = formInputMidsRef.current;
428
+ // =============================================================================================
429
+ // ARCHIVIAZIONE
430
+ // =============================================================================================
431
+ // Archivia solo le righe del tipo corrente: quelle di altri tipi restano intatte
432
+ const archiveAllAsync = async () => {
433
+ const detailTID = selectedRelation?.detailTID;
434
+ if (detailTID === undefined)
435
+ return;
436
+ const rowsToArchive = archiveRows.filter(r => r.__tid === detailTID);
437
+ if (rowsToArchive.length === 0) {
438
+ ShowAlert({ message: SDKUI_Localizator.NoDataToDisplay, mode: 'warning', title: SDKUI_Localizator.Archive, duration: 3000 });
439
+ return;
440
+ }
441
+ try {
442
+ setIsArchiving(true);
443
+ const detailDtd = await getDetailDtdAsync(detailTID);
444
+ if (!canArchive(detailDtd)) {
445
+ ShowAlert({ message: SDKUI_Localizator.YouDoNotHavePermissionsToArchiveDetailDocumentsOfThisType, mode: 'warning', title: SDKUI_Localizator.Archive, duration: 5000 });
446
+ return;
447
+ }
448
+ const dataTypeByMid = new Map();
449
+ (detailDtd?.metadata ?? []).forEach(md => { if (md.id !== undefined)
450
+ dataTypeByMid.set(md.id, md.dataType); });
451
+ // Collegamenti della relazione valorizzati dal master (prevalgono sui valori del form)
452
+ const associationMids = await getMasterAssociationsForDetailAsync(selectedRelation);
453
+ // Barra primaria: avanzamento documenti. Barra secondaria: upload del file corrente
454
+ const abortController = new AbortController();
455
+ abortControllerRef.current = abortController;
456
+ setWaitPanel({
457
+ ...CLOSED_WAIT_PANEL,
458
+ showWaitPanel: true,
459
+ waitPanelTitle: SDKUI_Localizator.Archiving,
460
+ showWaitPanelPrimary: rowsToArchive.length > 1,
461
+ waitPanelMaxValuePrimary: rowsToArchive.length,
462
+ showWaitPanelSecondary: true,
463
+ });
464
+ const result = [];
465
+ const archivedRowIds = new Set();
466
+ for (let i = 0; i < rowsToArchive.length; i++) {
467
+ if (abortController.signal.aborted) {
468
+ result.push({ rowIndex: i, id1: detailTID, id2: undefined, resultType: ResultTypes.WARNING, description: `Operazione interrotta. Elaborati ${i} documenti` });
469
+ break;
470
+ }
471
+ const rowData = rowsToArchive[i];
472
+ try {
473
+ setWaitPanel(prev => ({ ...prev, waitPanelTextPrimary: `${SDKUI_Localizator.Archiving} ${i + 1} / ${rowsToArchive.length}` }));
474
+ const newDID = await archiveRowAsync(rowData, detailTID, dataTypeByMid, associationMids, abortController.signal);
475
+ setWaitPanel(prev => ({ ...prev, waitPanelValuePrimary: i + 1 }));
476
+ archivedRowIds.add(rowData.id);
477
+ result.push({ rowIndex: i, id1: detailTID, id2: newDID, resultType: ResultTypes.SUCCESS });
478
+ }
479
+ catch (error) {
480
+ const err = error;
481
+ // Annullamento richiesto dall'utente: interrompe senza mostrare l'errore
482
+ if (err?.name === 'CanceledError' || abortController.signal.aborted) {
483
+ result.push({ rowIndex: i, id1: detailTID, id2: undefined, resultType: ResultTypes.WARNING, description: `Operazione interrotta. Elaborati ${i} documenti` });
484
+ break;
485
+ }
486
+ result.push({ rowIndex: i, id1: detailTID, id2: undefined, resultType: ResultTypes.ERROR, description: getExceptionMessage(error) });
487
+ if (stopOnFirstError)
488
+ break;
489
+ }
490
+ }
491
+ // Le righe archiviate escono dal riepilogo
492
+ if (archivedRowIds.size > 0) {
493
+ setArchiveRows(prev => prev.filter(r => !archivedRowIds.has(r.id)));
494
+ setSelectedRowKeys([]);
495
+ setFocusedRowKey(undefined);
496
+ if (areArchivedDetailsLoadedRef.current)
497
+ refreshArchivedDetailsAsync();
498
+ }
499
+ const successCount = result.filter(o => o.resultType === ResultTypes.SUCCESS).length;
500
+ const successMsg = `${successCount} ${successCount === 1 ? 'documento di dettaglio archiviato' : 'documenti di dettaglio archiviati'} con successo`;
501
+ TMResultManager.show(result, SDKUI_Localizator.Archive, 'TID', 'DID', successMsg);
502
+ // Tutte le righe del tipo corrente archiviate senza errori: il modale si chiude
503
+ const errorCount = result.filter(o => o.resultType === ResultTypes.ERROR).length;
504
+ if (errorCount === 0 && archivedRowIds.size === rowsToArchive.length)
505
+ onClose();
506
+ }
507
+ catch (error) {
508
+ TMExceptionBoxManager.show({ exception: error });
509
+ }
510
+ finally {
511
+ abortControllerRef.current = undefined;
512
+ setWaitPanel(CLOSED_WAIT_PANEL);
513
+ setIsArchiving(false);
514
+ }
515
+ };
516
+ // Archiviazione di una singola riga, con avanzamento dell'upload sulla barra secondaria
517
+ const archiveRowAsync = async (row, detailTID, dataTypeByMid, associationMids, signal) => {
518
+ const ae = new ArchiveEngineByID(SDK_Globals.tmSession);
519
+ ae.TID = detailTID;
520
+ ae.Metadata_ClearAll();
521
+ // Valori del form + associazioni con il master (le associazioni prevalgono)
522
+ const valueByMid = new Map();
523
+ for (const v of row.__values) {
524
+ if (v.mid !== undefined && v.value)
525
+ valueByMid.set(v.mid, v.value);
526
+ }
527
+ for (const a of associationMids) {
528
+ if (a.value)
529
+ valueByMid.set(a.mid, a.value);
530
+ }
531
+ valueByMid.forEach((value, mid) => {
532
+ switch (dataTypeByMid.get(mid)) {
533
+ case MetadataDataTypes.DateTime:
534
+ ae.Metadata_AddDateTime(mid, new Date(value));
535
+ break;
536
+ case MetadataDataTypes.Number:
537
+ ae.Metadata_AddNumber(mid, parseFloat(value));
538
+ break;
539
+ default:
540
+ ae.Metadata_AddString(mid, value);
541
+ break;
542
+ }
543
+ });
544
+ if (row.__file)
545
+ ae.ArchivingFile = row.__file;
546
+ let firstBlock = true;
547
+ let maxFileSize = 0;
548
+ return ae.ArchiveAsync(signal, (pd) => {
549
+ if (firstBlock) {
550
+ maxFileSize = pd.ProgressBarMaximum ?? 0;
551
+ setWaitPanel(prev => ({ ...prev, waitPanelMaxValueSecondary: maxFileSize }));
552
+ firstBlock = false;
553
+ }
554
+ setWaitPanel(prev => ({
555
+ ...prev,
556
+ waitPanelValueSecondary: pd.ProgressBarValue,
557
+ waitPanelTextSecondary: `Uploading... ${Globalization.getNumberDisplayValue(pd.ProgressBarValue, true)} / ${Globalization.getNumberDisplayValue(maxFileSize, true)}`,
558
+ }));
559
+ if (pd.ProgressBarValue === pd.ProgressBarMaximum) {
560
+ setWaitPanel(prev => ({ ...prev, waitPanelValueSecondary: 0, waitPanelMaxValueSecondary: 0, waitPanelTextSecondary: '' }));
561
+ firstBlock = true;
562
+ }
563
+ });
564
+ };
565
+ return {
566
+ // Flusso
567
+ isLoading, isSummaryReady, phase, setPhase,
568
+ detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes,
569
+ selectRelation, backToTypeSelection, isBackLocked, closeModal,
570
+ // Sezioni
571
+ sectionsOpen, setSectionOpen, openAllSections, clearLayout, layoutResetToken,
572
+ hasSavedLayout, setHasSavedLayout,
573
+ // Master
574
+ canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken,
575
+ isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync,
576
+ // Dettagli già archiviati
577
+ archivedDetailResults, archivedDetailsCount, isLoadingArchivedDetails, refreshArchivedDetailsAsync,
578
+ // Dettagli da archiviare
579
+ detailColumns, detailSupportsFile, archiveRows, currentRows,
580
+ focusedRowKey, setFocusedRowKey, selectedRowKeys, setSelectedRowKeys,
581
+ isOpenAddForm, editingRowId, editingRow, formInputMids,
582
+ openAddForm, openEditForm, closeAddForm, deleteRow, duplicateRow, upsertRow, updateRowValues,
583
+ // Archiviazione
584
+ stopOnFirstError, setStopOnFirstError, isArchiving, archiveAllAsync,
585
+ waitPanel, abortController: abortControllerRef.current,
586
+ };
587
+ };
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useState, useCallback, useMemo, useEffect } from 'react';
3
3
  import { SDKUI_Localizator } from '../helper/SDKUI_Localizator';
4
4
  import { IconPair, IconUnpair } from '../helper/TMIcons';
5
- import { hasDetailRelations, hasMasterRelations } from '../helper/dcmtsHelper';
5
+ import { hasDetailRelations, hasMasterRelations, mapRelationAssociationsToMids } from '../helper/dcmtsHelper';
6
6
  import { SDK_Globals, SearchEngine, RelationCacheService, RelationTypes, DcmtTypeListCacheService, SystemMIDsAsNumber, AccessLevelsEx } from '@topconsultnpm/sdk-ts';
7
7
  import { TMSpinner, TMExceptionBoxManager, ShowAlert } from '../components';
8
8
  export const useRelatedDocuments = ({ selectedSearchResult, focusedItem, currentSearchResults }) => {
@@ -224,31 +224,7 @@ export const useRelatedDocuments = ({ selectedSearchResult, focusedItem, current
224
224
  };
225
225
  //This ensures archive master/detail works correctly regardless of output metadata configuration.
226
226
  const mapAssociationsToMidsAsync = useCallback(async (relation, type) => {
227
- if (!relation.associations || relation.associations.length === 0)
228
- return [];
229
- if (!focusedItem?.TID || !focusedItem?.DID)
230
- return [];
231
- const searchEngine = SDK_Globals.tmSession?.NewSearchEngine();
232
- if (!searchEngine)
233
- return [];
234
- const allMetadata = await searchEngine.GetMetadataAsync(focusedItem.TID, focusedItem.DID, false);
235
- if (!allMetadata?.dtdResult?.rows?.[0] || !allMetadata.selectMIDs)
236
- return [];
237
- const row = allMetadata.dtdResult.rows[0];
238
- const mids = allMetadata.selectMIDs;
239
- const metadataMap = new Map();
240
- mids.forEach((mid, index) => {
241
- const value = row[index];
242
- if (value !== undefined && value !== null) {
243
- metadataMap.set(mid, value.toString());
244
- }
245
- });
246
- return relation.associations.map(assoc => {
247
- const sourceMid = type === 'detail' ? (assoc.item1 ?? 0) : (assoc.item2 ?? 0);
248
- const targetMid = type === 'detail' ? (assoc.item2 ?? 0) : (assoc.item1 ?? 0);
249
- const value = metadataMap.get(sourceMid) ?? '';
250
- return { mid: targetMid, value };
251
- });
227
+ return mapRelationAssociationsToMids(relation, type, focusedItem);
252
228
  }, [focusedItem]);
253
229
  const archiveRelatedDcmtHandler = useCallback(async (relation, type) => {
254
230
  const targetTID = type === 'detail' ? relation.detailTID : relation.masterTID;
@@ -0,0 +1,18 @@
1
+ import { MetadataDescriptor } from '@topconsultnpm/sdk-ts';
2
+ import { IColumnProps } from 'devextreme-react/data-grid';
3
+ export declare const needsDisplayValue: (columnMd: MetadataDescriptor | undefined) => boolean;
4
+ /**
5
+ * Resa dei valori nelle griglie di TMDistinctValues: al posto della chiave archiviata mostra la descrizione
6
+ * presa dalle cache di liste dati e utenti, tenendole allineate alle colonne effettivamente visualizzate.
7
+ *
8
+ * Vale per entrambe le sorgenti del pannello (valori distinti e ricerca rapida, che può essere su un altro
9
+ * tipo documento): le chiavi delle due sorgenti vengono accumulate e ricaricate sempre insieme, altrimenti
10
+ * un caricamento farebbe perdere le descrizioni dell'altra sorgente.
11
+ */
12
+ export declare const useTMDistinctValuesMetadataDisplay: () => {
13
+ loadDisplayCachesAsync: (columnsMd: Array<MetadataDescriptor | undefined>, rows: string[][] | undefined) => Promise<void>;
14
+ getMetadataDisplayValue: (columnMd: MetadataDescriptor | undefined, value: any) => any;
15
+ displayValueColumnProps: (columnMd: MetadataDescriptor | undefined, dataField: string) => Partial<IColumnProps>;
16
+ };
17
+ /** Tipo di ritorno dell'hook: le sorgenti di TMDistinctValues lo ricevono come dipendenza */
18
+ export type TMDistinctValuesMetadataDisplay = ReturnType<typeof useTMDistinctValuesMetadataDisplay>;