@topconsultnpm/sdkui-react 6.22.0-dev2.4 → 6.22.0-dev2.40

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 (122) 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 +11 -0
  12. package/lib/components/base/TMModal.js +47 -13
  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 +45 -0
  46. package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.js +323 -0
  47. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.d.ts +370 -0
  48. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.js +283 -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 +31 -18
  52. package/lib/components/features/search/TMSearchQueryEditor.js +5 -3
  53. package/lib/components/features/search/TMSearchQueryPanel.d.ts +2 -1
  54. package/lib/components/features/search/TMSearchQueryPanel.js +45 -9
  55. package/lib/components/features/search/TMSearchResult.d.ts +15 -0
  56. package/lib/components/features/search/TMSearchResult.js +509 -187
  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/devextreme-license.d.ts +1 -0
  69. package/lib/devextreme-license.js +3 -0
  70. package/lib/helper/SDKUI_Globals.d.ts +109 -0
  71. package/lib/helper/SDKUI_Globals.js +150 -0
  72. package/lib/helper/SDKUI_Localizator.d.ts +34 -0
  73. package/lib/helper/SDKUI_Localizator.js +340 -0
  74. package/lib/helper/TMIcons.d.ts +1 -0
  75. package/lib/helper/TMIcons.js +3 -0
  76. package/lib/helper/dataGridSearchHelper.d.ts +28 -0
  77. package/lib/helper/dataGridSearchHelper.js +51 -0
  78. package/lib/helper/dcmtsHelper.d.ts +20 -0
  79. package/lib/helper/dcmtsHelper.js +58 -1
  80. package/lib/helper/helpers.d.ts +20 -0
  81. package/lib/helper/helpers.js +55 -1
  82. package/lib/helper/index.d.ts +1 -0
  83. package/lib/helper/index.js +1 -0
  84. package/lib/helper/queryHelper.d.ts +2 -0
  85. package/lib/helper/queryHelper.js +3 -1
  86. package/lib/hooks/tmDistinctValuesGridHelper.d.ts +16 -0
  87. package/lib/hooks/tmDistinctValuesGridHelper.js +31 -0
  88. package/lib/hooks/useArchiveListForm.d.ts +39 -0
  89. package/lib/hooks/useArchiveListForm.js +46 -0
  90. package/lib/hooks/useCaseFlowApprove.d.ts +56 -0
  91. package/lib/hooks/useCaseFlowApprove.js +157 -0
  92. package/lib/hooks/useDataUserIdItem.d.ts +1 -0
  93. package/lib/hooks/useDataUserIdItem.js +1 -0
  94. package/lib/hooks/useDocumentOperations.d.ts +4 -0
  95. package/lib/hooks/useDocumentOperations.js +41 -7
  96. package/lib/hooks/useMultiMasterDetailDcmts.d.ts +113 -0
  97. package/lib/hooks/useMultiMasterDetailDcmts.js +760 -0
  98. package/lib/hooks/useRelatedDocuments.js +2 -26
  99. package/lib/hooks/useShowDossiersV5.d.ts +12 -0
  100. package/lib/hooks/useShowDossiersV5.js +37 -0
  101. package/lib/hooks/useTMDistinctValuesMetadataDisplay.d.ts +18 -0
  102. package/lib/hooks/useTMDistinctValuesMetadataDisplay.js +103 -0
  103. package/lib/hooks/useTMDistinctValuesQuickSearch.d.ts +49 -0
  104. package/lib/hooks/useTMDistinctValuesQuickSearch.js +307 -0
  105. package/lib/hooks/useTMDistinctValuesSelection.d.ts +42 -0
  106. package/lib/hooks/useTMDistinctValuesSelection.js +103 -0
  107. package/lib/hooks/useTMDistinctValuesSource.d.ts +29 -0
  108. package/lib/hooks/useTMDistinctValuesSource.js +105 -0
  109. package/lib/index.d.ts +2 -0
  110. package/lib/index.js +5 -2
  111. package/lib/services/caseflow/CaseFlowService.d.ts +236 -0
  112. package/lib/services/caseflow/CaseFlowService.js +693 -0
  113. package/lib/services/caseflow/CaseFlowTemplateCacheService.d.ts +28 -0
  114. package/lib/services/caseflow/CaseFlowTemplateCacheService.js +121 -0
  115. package/lib/services/caseflow/caseFlowNameCache.d.ts +32 -0
  116. package/lib/services/caseflow/caseFlowNameCache.js +116 -0
  117. package/lib/services/caseflow/caseFlowWorkItemUtils.d.ts +61 -0
  118. package/lib/services/caseflow/caseFlowWorkItemUtils.js +113 -0
  119. package/lib/services/index.d.ts +4 -0
  120. package/lib/services/index.js +4 -0
  121. package/lib/services/platform_services.d.ts +1 -1
  122. package/package.json +9 -5
@@ -0,0 +1,760 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { AccessLevels, ArchiveConstraints, ArchiveEngineByID, DcmtOpers, GeneralRetrieveFormats, MetadataDataDomains, MetadataDataTypes, RelationCacheService, RelationTypes, ResultTypes, RetrieveFileOptions, SDK_Globals, SystemMIDsAsNumber } from '@topconsultnpm/sdk-ts';
3
+ import { MetadataValueDescriptorEx } from '../ts';
4
+ import { clearMultiDetailLayout, Globalization, getExceptionMessage, getMultiDetailSectionsOpen, handleArchiveVisibility, 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, toArchiveRowValue, 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, canNavigateMaster, onNavigateMaster }) => {
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
+ // Copia di un dettaglio archiviato in corso: i clic successivi sull'icona vanno ignorati (vedi copyArchivedDetailToRows)
69
+ const isCopyingArchivedDetailRef = useRef(false);
70
+ const [focusedRowKey, setFocusedRowKey] = useState(undefined);
71
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
72
+ // Form di inserimento/modifica di una riga
73
+ const [isOpenAddForm, setIsOpenAddForm] = useState(false);
74
+ const [editingRowId, setEditingRowId] = useState(undefined); // Undefined = inserimento
75
+ const [associationInputMids, setAssociationInputMids] = useState([]); // Associazioni pre-compilate dal master
76
+ // Archiviazione finale
77
+ const [stopOnFirstError, setStopOnFirstError] = useState(true);
78
+ const [isArchiving, setIsArchiving] = useState(false);
79
+ const [waitPanel, setWaitPanel] = useState(CLOSED_WAIT_PANEL);
80
+ const abortControllerRef = useRef(undefined);
81
+ const selectedItem = detailRelationItems.find(item => item.relation === selectedRelation);
82
+ const hasMultipleTypes = detailRelationItems.length > 1;
83
+ const canUpdateMaster = canUpdateMasterCallback(dtd, masterDcmt);
84
+ const masterFieldsCount = useMemo(() => getUserMetadataSorted(dtd).length, [dtd]);
85
+ // Nel riepilogo si mostrano solo le righe del tipo selezionato
86
+ const currentRows = useMemo(() => archiveRows.filter(r => r.__tid === selectedRelation?.detailTID), [archiveRows, selectedRelation?.detailTID]);
87
+ const archivedDetailsCount = archivedDetailResults.reduce((total, r) => total + (r.dcmtsFound ?? r.jsonResult?.length ?? 0), 0);
88
+ // =============================================================================================
89
+ // MASTER
90
+ // =============================================================================================
91
+ /** Identifica il master: cambia quando la navigazione sposta il modale su un altro documento */
92
+ const masterKey = `${masterDcmt?.TID}_${masterDcmt?.DID}`;
93
+ // Metadati del master indicizzati per mid: si leggono una sola volta per documento e la cache
94
+ // si invalida se cambia il master (TID/DID) o dopo un suo salvataggio
95
+ const masterMetadataCacheRef = useRef(undefined);
96
+ const getMasterMetadataAsync = () => {
97
+ if (masterMetadataCacheRef.current?.key !== masterKey) {
98
+ masterMetadataCacheRef.current = { key: masterKey, promise: getDcmtMetadataByMid(masterDcmt) };
99
+ }
100
+ return masterMetadataCacheRef.current.promise;
101
+ };
102
+ const handleMasterSavedAsync = async () => {
103
+ masterMetadataCacheRef.current = undefined;
104
+ setMasterInfoReloadToken(token => token + 1);
105
+ setIsOpenMasterForm(false);
106
+ };
107
+ // La navigazione ha spostato il modale su un altro master: le righe del riepilogo erano del
108
+ // documento precedente e sono già state scartate con conferma (vedi navigateMaster).
109
+ // Il tipo di dettaglio scelto resta: nella lista di provenienza i documenti sono dello stesso tipo
110
+ const previousMasterKeyRef = useRef(masterKey);
111
+ useEffect(() => {
112
+ if (previousMasterKeyRef.current === masterKey)
113
+ return;
114
+ previousMasterKeyRef.current = masterKey;
115
+ setArchiveRows([]);
116
+ setFocusedRowKey(undefined);
117
+ setSelectedRowKeys([]);
118
+ setIsOpenAddForm(false);
119
+ setEditingRowId(undefined);
120
+ setIsOpenMasterForm(false);
121
+ }, [masterKey]);
122
+ // Associazioni della relazione valorizzate dal master, per pre-compilare il dettaglio
123
+ const getMasterAssociationsForDetailAsync = async (relation) => {
124
+ if (!relation)
125
+ return [];
126
+ return mapAssociationsFromMetadata(relation, 'detail', await getMasterMetadataAsync());
127
+ };
128
+ // =============================================================================================
129
+ // RELAZIONI E STRUTTURA DEL DETTAGLIO
130
+ // =============================================================================================
131
+ // Relazioni di dettaglio archiviabili; con una sola relazione si salta la scelta del tipo.
132
+ // Dipende dal tipo del master: la navigazione resta nella stessa lista, quindi di norma non cambia
133
+ useEffect(() => {
134
+ const loadAsync = async () => {
135
+ try {
136
+ setIsLoading(true);
137
+ TMSpinner.show({ description: SDKUI_Localizator.Loading });
138
+ const relations = await RelationCacheService.GetAllAsync(SDK_Globals.tmSession);
139
+ if (!relations)
140
+ throw new Error('Impossibile caricare le relazioni.');
141
+ // Relazioni in cui il tipo corrente è master: i target sono i dettagli archiviabili
142
+ const detailRelations = relations.filter(r => r.masterTID === dtd?.id
143
+ && r.relationType !== RelationTypes.ManyToMany
144
+ && r.associations
145
+ && r.associations.length > 0);
146
+ const items = await Promise.all(detailRelations.map(async (relation) => ({
147
+ relation,
148
+ detailDtd: await getDetailDtdAsync(relation.detailTID),
149
+ })));
150
+ setDetailRelationItems(items);
151
+ if (items.length === 1) {
152
+ setSelectedRelation(items[0].relation);
153
+ setPhase(MultiDetailArchivePhase.ArchiveSummary);
154
+ }
155
+ else {
156
+ setPhase(MultiDetailArchivePhase.SelectDcmtType);
157
+ }
158
+ }
159
+ catch (error) {
160
+ TMExceptionBoxManager.show({ exception: error });
161
+ }
162
+ finally {
163
+ TMSpinner.hide();
164
+ setIsLoading(false);
165
+ }
166
+ };
167
+ loadAsync();
168
+ }, [dtd?.id]);
169
+ // Colonne della griglia di riepilogo e supporto al file del tipo selezionato
170
+ useEffect(() => {
171
+ const loadDetailStructureAsync = async () => {
172
+ if (!selectedRelation?.detailTID) {
173
+ setDetailColumns([]);
174
+ setDetailSupportsFile(false);
175
+ numericMidsRef.current = new Set();
176
+ return;
177
+ }
178
+ try {
179
+ const detailDtd = await getDetailDtdAsync(selectedRelation.detailTID);
180
+ setDetailSupportsFile(detailDtd?.archiveConstraint !== ArchiveConstraints.OnlyMetadata);
181
+ // Liste dati referenziate dai metadati, servono al render delle celle
182
+ const dataListIDs = new Set();
183
+ const numericMids = new Set();
184
+ (detailDtd?.metadata ?? []).forEach(md => {
185
+ if (md.dataDomain === MetadataDataDomains.DataList && md.dataListID)
186
+ dataListIDs.add(md.dataListID);
187
+ if (md.dataType === MetadataDataTypes.Number && md.id !== undefined)
188
+ numericMids.add(md.id);
189
+ });
190
+ numericMidsRef.current = numericMids;
191
+ await loadDataListsAsync(dataListIDs);
192
+ setDetailColumns(buildColumnsFromDtd(detailDtd, renderDataListCell));
193
+ }
194
+ catch {
195
+ setDetailColumns([]);
196
+ setDetailSupportsFile(false);
197
+ numericMidsRef.current = new Set();
198
+ }
199
+ };
200
+ loadDetailStructureAsync();
201
+ }, [selectedRelation?.detailTID]);
202
+ useEffect(() => {
203
+ if (phase !== MultiDetailArchivePhase.ArchiveSummary) {
204
+ setIsSummaryReady(false);
205
+ return;
206
+ }
207
+ setIsSummaryReady(false);
208
+ const rafId = requestAnimationFrame(() => setIsSummaryReady(true));
209
+ return () => cancelAnimationFrame(rafId);
210
+ }, [phase, selectedRelation?.detailTID]);
211
+ // =============================================================================================
212
+ // DETTAGLI GIÀ ARCHIVIATI
213
+ // =============================================================================================
214
+ const refreshArchivedDetailsAsync = async () => {
215
+ const detailTID = selectedRelation?.detailTID;
216
+ if (detailTID === undefined || !masterDcmt?.TID || !masterDcmt?.DID)
217
+ return;
218
+ try {
219
+ setIsLoadingArchivedDetails(true);
220
+ const searchEngine = SDK_Globals.tmSession?.NewSearchEngine();
221
+ const results = await searchEngine?.GetAllDetailDcmtsAsync(masterDcmt.TID, masterDcmt.DID) ?? [];
222
+ let filtered = results.filter(r => selectedRelation?.id !== undefined && r.relationID === selectedRelation.id);
223
+ if (filtered.length === 0)
224
+ filtered = results.filter(r => r.fromTID === detailTID);
225
+ setArchivedDetailResults(filtered);
226
+ areArchivedDetailsLoadedRef.current = true;
227
+ }
228
+ catch (error) {
229
+ TMExceptionBoxManager.show({ exception: error });
230
+ }
231
+ finally {
232
+ setIsLoadingArchivedDetails(false);
233
+ }
234
+ };
235
+ // I risultati seguono il tipo documento selezionato e il master (che cambia con la navigazione), e si
236
+ // caricano anche a sezione chiusa: il contatore nella testata e nella barra dei pannelli deve essere
237
+ // corretto senza dover aprire la sezione
238
+ useEffect(() => {
239
+ setArchivedDetailResults([]);
240
+ areArchivedDetailsLoadedRef.current = false;
241
+ if (selectedRelation?.detailTID !== undefined)
242
+ refreshArchivedDetailsAsync();
243
+ }, [selectedRelation?.detailTID, masterKey]);
244
+ // =============================================================================================
245
+ // SEZIONI
246
+ // =============================================================================================
247
+ const setSectionOpen = (section, isOpen) => {
248
+ const next = { ...sectionsOpen, [section]: isOpen };
249
+ setSectionsOpen(next);
250
+ saveMultiDetailSectionsOpen(next);
251
+ // Riaperte tutte le sezioni il collasso torna ai default: senza altezze salvate non c'è più layout
252
+ setHasSavedLayout(hasMultiDetailLayout());
253
+ // I dettagli già archiviati sono caricati insieme al tipo di dettaglio: qui si riprova solo se quel caricamento è fallito
254
+ if (section === 'archivedDetails' && isOpen && !areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
255
+ refreshArchivedDetailsAsync();
256
+ };
257
+ /**
258
+ * Apre tutte le sezioni senza salvare: serve dove il collasso non è una scelta dell'utente ma una
259
+ * conseguenza del layout (su mobile si vede un pannello alla volta, e una sezione chiusa lascerebbe
260
+ * a schermo la sola testata). Non si persiste per non sovrascrivere le sezioni che l'utente ha
261
+ * chiuso nella vista a pannelli impilati.
262
+ */
263
+ const openAllSections = () => {
264
+ setSectionsOpen(prev => Object.values(prev).every(Boolean) ? prev : { masterInfo: true, archivedDetails: true, archiveList: true });
265
+ // I dettagli già archiviati sono caricati insieme al tipo di dettaglio: qui si riprova solo se quel caricamento è fallito
266
+ if (!areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
267
+ refreshArchivedDetailsAsync();
268
+ };
269
+ /**
270
+ * Cancella il layout configurato dall'utente (altezze dei pannelli e sezioni collassate), previa
271
+ * conferma. Il token cambia per rimontare il panel manager, che legge le dimensioni solo all'avvio.
272
+ */
273
+ const clearLayout = () => {
274
+ TMMessageBoxManager.show({
275
+ title: SDKUI_Localizator.RemoveLayout,
276
+ message: SDKUI_Localizator.RemoveLayout_Confirm,
277
+ buttons: [ButtonNames.YES, ButtonNames.NO],
278
+ onButtonClick: (button) => {
279
+ if (button !== ButtonNames.YES)
280
+ return;
281
+ clearMultiDetailLayout();
282
+ const restored = getMultiDetailSectionsOpen();
283
+ setSectionsOpen(restored);
284
+ setLayoutResetToken(prev => prev + 1);
285
+ setHasSavedLayout(false);
286
+ // I dettagli già archiviati sono caricati insieme al tipo di dettaglio: qui si riprova solo se quel caricamento è fallito
287
+ if (!areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
288
+ refreshArchivedDetailsAsync();
289
+ ShowAlert({ message: SDKUI_Localizator.LayoutRemoved, mode: 'success', title: SDKUI_Localizator.RemoveLayout, duration: 3000 });
290
+ },
291
+ });
292
+ };
293
+ // =============================================================================================
294
+ // NAVIGAZIONE
295
+ // =============================================================================================
296
+ const selectRelation = (item) => {
297
+ setSelectedRelation(item.relation);
298
+ setPhase(MultiDetailArchivePhase.ArchiveSummary);
299
+ };
300
+ const backToTypeSelection = () => {
301
+ setSelectedRelation(undefined);
302
+ setPhase(MultiDetailArchivePhase.SelectDcmtType);
303
+ };
304
+ // Con righe inserite per il tipo corrente il ritorno alla scelta del tipo è bloccato
305
+ const isBackLocked = currentRows.length > 0;
306
+ const closeModal = () => {
307
+ if (archiveRows.length === 0) {
308
+ onClose();
309
+ return;
310
+ }
311
+ TMMessageBoxManager.show({
312
+ title: SDKUI_Localizator.Archive,
313
+ message: SDKUI_Localizator.ExitWithoutArchiving_Confirm,
314
+ buttons: [ButtonNames.YES, ButtonNames.NO],
315
+ onButtonClick: (button) => { if (button === ButtonNames.YES)
316
+ onClose(); },
317
+ });
318
+ };
319
+ // Navigazione del master sulla lista di provenienza: senza la sua callback le frecce non si mostrano
320
+ const canNavigateMasterPrev = !!onNavigateMaster && (canNavigateMaster?.('prev') ?? false);
321
+ const canNavigateMasterNext = !!onNavigateMaster && (canNavigateMaster?.('next') ?? false);
322
+ const isMasterNavigationEnabled = !!onNavigateMaster;
323
+ /**
324
+ * Sposta il master sul documento precedente/successivo della lista di provenienza. Le righe già
325
+ * inserite appartengono al master corrente: si perdono, quindi prima si chiede conferma
326
+ */
327
+ const navigateMaster = (dir) => {
328
+ if (!onNavigateMaster)
329
+ return;
330
+ if (dir === 'prev' ? !canNavigateMasterPrev : !canNavigateMasterNext)
331
+ return;
332
+ if (archiveRows.length === 0) {
333
+ onNavigateMaster(dir);
334
+ return;
335
+ }
336
+ TMMessageBoxManager.show({
337
+ title: SDKUI_Localizator.Archive,
338
+ message: SDKUI_Localizator.ChangeMasterWithoutArchiving_Confirm,
339
+ buttons: [ButtonNames.YES, ButtonNames.NO],
340
+ // Le righe le azzera l'effetto sul cambio di master, non serve svuotarle qui
341
+ onButtonClick: (button) => { if (button === ButtonNames.YES)
342
+ onNavigateMaster(dir); },
343
+ });
344
+ };
345
+ // =============================================================================================
346
+ // RIGHE DEL RIEPILOGO
347
+ // =============================================================================================
348
+ const openAddForm = () => {
349
+ if (!selectedRelation?.detailTID)
350
+ return;
351
+ setEditingRowId(undefined);
352
+ setIsOpenAddForm(true);
353
+ };
354
+ // Identità stabile: la griglia dei dettagli la usa nelle sue callback memoizzate
355
+ const openEditForm = useCallback((row) => {
356
+ if (!row)
357
+ return;
358
+ setEditingRowId(row.id);
359
+ setIsOpenAddForm(true);
360
+ }, []);
361
+ const closeAddForm = () => {
362
+ setEditingRowId(undefined);
363
+ setIsOpenAddForm(false);
364
+ };
365
+ const deleteRow = useCallback((row) => {
366
+ if (!row)
367
+ return;
368
+ TMMessageBoxManager.show({
369
+ title: SDKUI_Localizator.Remove,
370
+ message: SDKUI_Localizator.RemoveDetailFromSummary_Confirm,
371
+ buttons: [ButtonNames.YES, ButtonNames.NO],
372
+ onButtonClick: (button) => {
373
+ if (button !== ButtonNames.YES)
374
+ return;
375
+ setArchiveRows(prev => prev.filter(r => r.id !== row.id));
376
+ setFocusedRowKey(prev => (prev === row.id ? undefined : prev));
377
+ setSelectedRowKeys(prev => prev.filter(k => k !== row.id));
378
+ },
379
+ });
380
+ }, []);
381
+ // I valori del form sono stringhe: le celle numeriche vogliono il numero, richiesto dal loro editor
382
+ const buildArchiveRow = useCallback((id, tid, values, file) => {
383
+ const row = { id, __tid: tid, __values: values, __file: file };
384
+ for (const v of values) {
385
+ if (v.mid === undefined)
386
+ continue;
387
+ row[String(v.mid)] = numericMidsRef.current.has(v.mid) ? toGridNumber(v.value) : (v.value ?? '');
388
+ }
389
+ return row;
390
+ }, []);
391
+ // Aggiunge/aggiorna nel riepilogo la riga compilata nel form, senza archiviare a sistema
392
+ const upsertRow = (values, file) => {
393
+ const tid = selectedRelation?.detailTID;
394
+ if (tid === undefined)
395
+ return;
396
+ if (editingRowId !== undefined) {
397
+ setArchiveRows(prev => prev.map(r => (r.id === editingRowId ? buildArchiveRow(editingRowId, tid, values, file) : r)));
398
+ }
399
+ else {
400
+ rowIdRef.current += 1;
401
+ setArchiveRows(prev => [...prev, buildArchiveRow(rowIdRef.current, tid, values, file)]);
402
+ }
403
+ closeAddForm();
404
+ };
405
+ /**
406
+ * Modifica diretta in griglia di uno o più metadati testuali o numerici (changes: mid -> valore).
407
+ * La riga è aggiornata sul posto e non con setArchiveRows: un nuovo riferimento all'array
408
+ * ricaricherebbe la griglia a ogni cella confermata, perdendo il clic sulla cella successiva.
409
+ * L'array __values è sostituito, non modificato: il duplicato, che lo condivide, resta invariato
410
+ */
411
+ const updateRowValues = useCallback((rowId, changes) => {
412
+ const changedMids = Object.keys(changes);
413
+ if (changedMids.length === 0)
414
+ return;
415
+ const row = archiveRowsRef.current.find(r => r.id === rowId);
416
+ if (!row)
417
+ return;
418
+ // I __values vanno a sistema come stringhe, anche quelli che in griglia sono numeri. La griglia
419
+ // restituisce null sull'editor svuotato: a sistema il metadato va vuoto, non nullo
420
+ const normalized = {};
421
+ changedMids.forEach(mid => { normalized[mid] = changes[mid] === null || changes[mid] === undefined ? '' : String(changes[mid]); });
422
+ const values = row.__values.map(v => (v.mid !== undefined && normalized[String(v.mid)] !== undefined
423
+ ? { ...v, value: normalized[String(v.mid)] }
424
+ : v));
425
+ // Un metadato assente dai valori del form non arriverebbe all'archiviazione
426
+ changedMids.filter(mid => !values.some(v => String(v.mid) === mid)).forEach(mid => {
427
+ const added = new MetadataValueDescriptorEx();
428
+ added.tid = row.__tid;
429
+ added.mid = Number(mid);
430
+ added.value = normalized[mid];
431
+ values.push(added);
432
+ });
433
+ row.__values = values;
434
+ // In cella il numero resta un numero, come lo vuole il suo editor
435
+ changedMids.forEach(mid => { row[mid] = numericMidsRef.current.has(Number(mid)) ? toGridNumber(normalized[mid]) : normalized[mid]; });
436
+ }, []);
437
+ // I __values sono condivisi per riferimento: la modifica sostituisce l'intero array, quindi non intacca il duplicato
438
+ const duplicateRow = useCallback((row) => {
439
+ if (!row)
440
+ return;
441
+ rowIdRef.current += 1;
442
+ setArchiveRows(prev => [...prev, buildArchiveRow(rowIdRef.current, row.__tid, row.__values, row.__file)]);
443
+ }, [buildArchiveRow]);
444
+ /**
445
+ * Copia un dettaglio già archiviato tra le righe da archiviare: i suoi metadati (e il file, se il
446
+ * tipo lo prevede e il documento ne ha uno) diventano una nuova riga del riepilogo. Il documento
447
+ * archiviato non è toccato: quello che si ottiene è un nuovo dettaglio, ancora da archiviare.
448
+ * I numeratori non si copiano, il loro valore lo assegna il sistema all'archiviazione.
449
+ * Ogni motivo che impedisce la copia, o che la rende parziale, arriva all'utente con un avviso:
450
+ * la riga entra nel riepilogo solo se i metadati del dettaglio sono stati letti davvero.
451
+ */
452
+ const copyArchivedDetailToRows = async (dcmt) => {
453
+ // Clic ripetuti sull'icona: una copia alla volta, altrimenti spinner e righe si sovrappongono
454
+ if (isCopyingArchivedDetailRef.current)
455
+ return;
456
+ try {
457
+ const detailTID = selectedRelation?.detailTID;
458
+ // Senza tipo selezionato il riepilogo non ha colonne in cui copiare il dettaglio
459
+ if (detailTID === undefined) {
460
+ ShowAlert({ message: SDKUI_Localizator.SelectDcmtTypeForDetailArchiving, mode: 'warning', title: SDKUI_Localizator.Duplicate, duration: 3000 });
461
+ return;
462
+ }
463
+ // La griglia dei risultati porta i valori di riga come stringhe: TID e DID vanno usati come numeri
464
+ const archivedDcmt = { TID: Number(dcmt?.TID), DID: Number(dcmt?.DID) };
465
+ if (!Number.isFinite(archivedDcmt.TID) || !Number.isFinite(archivedDcmt.DID) || archivedDcmt.TID <= 0 || archivedDcmt.DID <= 0) {
466
+ ShowAlert({ message: SDKUI_Localizator.InvalidDcmt, mode: 'warning', title: SDKUI_Localizator.Duplicate, duration: 3000 });
467
+ return;
468
+ }
469
+ // Le righe del riepilogo sono del tipo selezionato: un dettaglio di altro tipo non ne ha le colonne
470
+ if (archivedDcmt.TID !== detailTID) {
471
+ ShowAlert({ message: SDKUI_Localizator.DetailNotOfSelectedDcmtType, mode: 'warning', title: SDKUI_Localizator.Duplicate, duration: 3000 });
472
+ return;
473
+ }
474
+ isCopyingArchivedDetailRef.current = true;
475
+ TMSpinner.show({ description: SDKUI_Localizator.Loading });
476
+ const [detailDtd, metadataByMid] = await Promise.all([getDetailDtdAsync(detailTID), getDcmtMetadataByMid(archivedDcmt)]);
477
+ // Senza la struttura del tipo non si sa quali metadati copiare: la riga nascerebbe vuota
478
+ if (!detailDtd) {
479
+ ShowAlert({ message: SDKUI_Localizator.DcmtTypeStructureNotRetrieved, mode: 'error', title: SDKUI_Localizator.Duplicate, duration: 5000 });
480
+ return;
481
+ }
482
+ // Il permesso serve comunque all'archiviazione: dirlo qui evita di compilare righe inutili
483
+ if (!canArchive(detailDtd)) {
484
+ ShowAlert({ message: SDKUI_Localizator.YouDoNotHavePermissionsToArchiveDetailDocumentsOfThisType, mode: 'warning', title: SDKUI_Localizator.Duplicate, duration: 5000 });
485
+ return;
486
+ }
487
+ // Metadati non letti (sessione non valida, documento non più visibile): una riga di valori vuoti non serve
488
+ if (metadataByMid.size === 0) {
489
+ ShowAlert({ message: SDKUI_Localizator.DetailMetadataNotRetrieved, mode: 'error', title: SDKUI_Localizator.Duplicate, duration: 5000 });
490
+ return;
491
+ }
492
+ const userMetadata = getUserMetadataSorted(detailDtd);
493
+ const archivableMetadata = userMetadata.filter(md => md.dataDomain !== MetadataDataDomains.Numerator && handleArchiveVisibility(md));
494
+ // Niente da copiare: il tipo non ha metadati utente, o nessuno di questi è archiviabile
495
+ if (archivableMetadata.length === 0) {
496
+ const message = userMetadata.length > 0
497
+ ? SDKUI_Localizator.YouDoNotHavePermissionsToArchiveDetailDocumentsOfThisType
498
+ : SDKUI_Localizator.DetailMetadataNotRetrieved;
499
+ ShowAlert({ message, mode: 'warning', title: SDKUI_Localizator.Duplicate, duration: 5000 });
500
+ return;
501
+ }
502
+ // Gli stessi metadati che compila il form della riga: valori come stringhe, mancanti a vuoto
503
+ const values = archivableMetadata.map(md => {
504
+ const mvd = new MetadataValueDescriptorEx();
505
+ mvd.tid = detailTID;
506
+ mvd.mid = md.id;
507
+ mvd.md = md;
508
+ // Date e numeri nel formato del form: la riga copiata si rilegge e si modifica come le altre
509
+ mvd.value = toArchiveRowValue(metadataByMid.get(md.id), md.dataType);
510
+ mvd.isRequired = md.isRequired?.toString();
511
+ return mvd;
512
+ });
513
+ // Il file segue la riga solo se il tipo lo prevede e il documento ne ha uno; se non si
514
+ // recupera la riga si inserisce comunque, il file si aggiunge dal form della riga
515
+ let file = undefined;
516
+ // Il file c'è se lo dice il metadato di sistema del documento: la colonna della griglia
517
+ // dei risultati è solo un ripiego, la ricerca potrebbe non averla tra i suoi valori
518
+ const fileExt = String(metadataByMid.get(SystemMIDsAsNumber.FileExt) ?? dcmt?.FILEEXT ?? '');
519
+ const hasFile = fileExt !== '';
520
+ const supportsFile = detailDtd.archiveConstraint !== ArchiveConstraints.OnlyMetadata;
521
+ if (hasFile && supportsFile) {
522
+ if (detailDtd.perm?.canRetrieveFile !== AccessLevels.Yes) {
523
+ ShowAlert({ message: `${SDKUI_Localizator.DetailFileNotRetrieved}: ${SDKUI_Localizator.YouDoNotHavePermissionsToRetrieveTheFile}`, mode: 'warning', title: SDKUI_Localizator.Duplicate, duration: 5000 });
524
+ }
525
+ else {
526
+ try {
527
+ const rfo = new RetrieveFileOptions();
528
+ rfo.retrieveReason = DcmtOpers.None;
529
+ rfo.generalRetrieveFormat = GeneralRetrieveFormats.OriginalUnsigned;
530
+ file = await SDK_Globals.tmSession?.NewSearchEngine().RetrieveFileAsync(archivedDcmt.TID, archivedDcmt.DID, rfo);
531
+ // Nessun errore ma nemmeno il file (sessione non valida): l'utente deve saperlo
532
+ if (!file)
533
+ ShowAlert({ message: SDKUI_Localizator.DetailFileNotRetrieved, mode: 'warning', title: SDKUI_Localizator.Duplicate, duration: 5000 });
534
+ }
535
+ catch (error) {
536
+ ShowAlert({ message: `${SDKUI_Localizator.DetailFileNotRetrieved}: ${getExceptionMessage(error)}`, mode: 'warning', title: SDKUI_Localizator.Duplicate, duration: 5000 });
537
+ }
538
+ }
539
+ }
540
+ rowIdRef.current += 1;
541
+ const newRowId = rowIdRef.current;
542
+ setArchiveRows(prev => [...prev, buildArchiveRow(newRowId, detailTID, values, file)]);
543
+ // La riga copiata è quella su cui si lavora: fuoco e selezione la seguono
544
+ setFocusedRowKey(newRowId);
545
+ setSelectedRowKeys([newRowId]);
546
+ // Con la sezione chiusa la riga appena copiata non si vedrebbe
547
+ if (!sectionsOpen.archiveList)
548
+ setSectionOpen('archiveList', true);
549
+ ShowAlert({ message: SDKUI_Localizator.DetailCopiedToArchiveList, mode: 'success', title: SDKUI_Localizator.Duplicate, duration: 3000 });
550
+ // Il tipo pretende il file: senza, la riga non passerebbe l'archiviazione
551
+ if (!file && detailDtd.archiveConstraint === ArchiveConstraints.ContentCompulsory) {
552
+ ShowAlert({ message: SDKUI_Localizator.DetailFileToAddFromRowForm, mode: 'warning', title: SDKUI_Localizator.Duplicate, duration: 5000 });
553
+ }
554
+ }
555
+ catch (error) {
556
+ TMExceptionBoxManager.show({ exception: error });
557
+ }
558
+ finally {
559
+ // Spinner e blocco partono insieme: se la copia non è nemmeno iniziata non c'è niente da chiudere
560
+ if (isCopyingArchivedDetailRef.current) {
561
+ isCopyingArchivedDetailRef.current = false;
562
+ TMSpinner.hide();
563
+ }
564
+ }
565
+ };
566
+ // All'apertura del form pre-compila i MID di collegamento valorizzati dal master
567
+ useEffect(() => {
568
+ if (!isOpenAddForm || !selectedRelation) {
569
+ setAssociationInputMids([]);
570
+ return;
571
+ }
572
+ let cancelled = false;
573
+ getMasterAssociationsForDetailAsync(selectedRelation)
574
+ .then(associations => { if (!cancelled)
575
+ setAssociationInputMids(associations); })
576
+ .catch(() => { if (!cancelled)
577
+ setAssociationInputMids([]); });
578
+ return () => { cancelled = true; };
579
+ }, [isOpenAddForm, selectedRelation]);
580
+ // MID pre-compilati: in inserimento i collegamenti dal master, in modifica i valori della riga
581
+ const editingRow = editingRowId !== undefined ? archiveRows.find(r => r.id === editingRowId) : undefined;
582
+ const nextFormInputMids = editingRow
583
+ ? editingRow.__values
584
+ .filter(v => v.mid !== undefined && v.value !== undefined && v.value !== null && v.value !== '')
585
+ .map(v => ({ mid: v.mid, value: String(v.value) }))
586
+ : associationInputMids;
587
+ // Il form riapplica i valori quando cambia il riferimento di inputMids: si mantiene la stessa
588
+ // istanza finché il contenuto non cambia, così le modifiche dell'utente non vengono sovrascritte
589
+ const formInputMidsSignature = nextFormInputMids.map(o => `${o.mid}=${o.value}`).join('|');
590
+ const formInputMidsRef = useRef(nextFormInputMids);
591
+ const formInputMidsSignatureRef = useRef(formInputMidsSignature);
592
+ if (formInputMidsSignatureRef.current !== formInputMidsSignature) {
593
+ formInputMidsSignatureRef.current = formInputMidsSignature;
594
+ formInputMidsRef.current = nextFormInputMids;
595
+ }
596
+ const formInputMids = formInputMidsRef.current;
597
+ // =============================================================================================
598
+ // ARCHIVIAZIONE
599
+ // =============================================================================================
600
+ // Archivia solo le righe del tipo corrente: quelle di altri tipi restano intatte
601
+ const archiveAllAsync = async () => {
602
+ const detailTID = selectedRelation?.detailTID;
603
+ if (detailTID === undefined)
604
+ return;
605
+ const rowsToArchive = archiveRows.filter(r => r.__tid === detailTID);
606
+ if (rowsToArchive.length === 0) {
607
+ ShowAlert({ message: SDKUI_Localizator.NoDataToDisplay, mode: 'warning', title: SDKUI_Localizator.Archive, duration: 3000 });
608
+ return;
609
+ }
610
+ try {
611
+ setIsArchiving(true);
612
+ const detailDtd = await getDetailDtdAsync(detailTID);
613
+ if (!canArchive(detailDtd)) {
614
+ ShowAlert({ message: SDKUI_Localizator.YouDoNotHavePermissionsToArchiveDetailDocumentsOfThisType, mode: 'warning', title: SDKUI_Localizator.Archive, duration: 5000 });
615
+ return;
616
+ }
617
+ const dataTypeByMid = new Map();
618
+ (detailDtd?.metadata ?? []).forEach(md => { if (md.id !== undefined)
619
+ dataTypeByMid.set(md.id, md.dataType); });
620
+ // Collegamenti della relazione valorizzati dal master (prevalgono sui valori del form)
621
+ const associationMids = await getMasterAssociationsForDetailAsync(selectedRelation);
622
+ // Barra primaria: avanzamento documenti. Barra secondaria: upload del file corrente
623
+ const abortController = new AbortController();
624
+ abortControllerRef.current = abortController;
625
+ setWaitPanel({
626
+ ...CLOSED_WAIT_PANEL,
627
+ showWaitPanel: true,
628
+ waitPanelTitle: SDKUI_Localizator.Archiving,
629
+ showWaitPanelPrimary: rowsToArchive.length > 1,
630
+ waitPanelMaxValuePrimary: rowsToArchive.length,
631
+ showWaitPanelSecondary: true,
632
+ });
633
+ const result = [];
634
+ const archivedRowIds = new Set();
635
+ for (let i = 0; i < rowsToArchive.length; i++) {
636
+ if (abortController.signal.aborted) {
637
+ result.push({ rowIndex: i, id1: detailTID, id2: undefined, resultType: ResultTypes.WARNING, description: `Operazione interrotta. Elaborati ${i} documenti` });
638
+ break;
639
+ }
640
+ const rowData = rowsToArchive[i];
641
+ try {
642
+ setWaitPanel(prev => ({ ...prev, waitPanelTextPrimary: `${SDKUI_Localizator.Archiving} ${i + 1} / ${rowsToArchive.length}` }));
643
+ const newDID = await archiveRowAsync(rowData, detailTID, dataTypeByMid, associationMids, abortController.signal);
644
+ setWaitPanel(prev => ({ ...prev, waitPanelValuePrimary: i + 1 }));
645
+ archivedRowIds.add(rowData.id);
646
+ result.push({ rowIndex: i, id1: detailTID, id2: newDID, resultType: ResultTypes.SUCCESS });
647
+ }
648
+ catch (error) {
649
+ const err = error;
650
+ // Annullamento richiesto dall'utente: interrompe senza mostrare l'errore
651
+ if (err?.name === 'CanceledError' || abortController.signal.aborted) {
652
+ result.push({ rowIndex: i, id1: detailTID, id2: undefined, resultType: ResultTypes.WARNING, description: `Operazione interrotta. Elaborati ${i} documenti` });
653
+ break;
654
+ }
655
+ result.push({ rowIndex: i, id1: detailTID, id2: undefined, resultType: ResultTypes.ERROR, description: getExceptionMessage(error) });
656
+ if (stopOnFirstError)
657
+ break;
658
+ }
659
+ }
660
+ // Le righe archiviate escono dal riepilogo
661
+ if (archivedRowIds.size > 0) {
662
+ setArchiveRows(prev => prev.filter(r => !archivedRowIds.has(r.id)));
663
+ setSelectedRowKeys([]);
664
+ setFocusedRowKey(undefined);
665
+ refreshArchivedDetailsAsync();
666
+ }
667
+ const successCount = result.filter(o => o.resultType === ResultTypes.SUCCESS).length;
668
+ const successMsg = `${successCount} ${successCount === 1 ? 'documento di dettaglio archiviato' : 'documenti di dettaglio archiviati'} con successo`;
669
+ TMResultManager.show(result, SDKUI_Localizator.Archive, 'TID', 'DID', successMsg);
670
+ // Tutte le righe del tipo corrente archiviate senza errori: il modale si chiude
671
+ const errorCount = result.filter(o => o.resultType === ResultTypes.ERROR).length;
672
+ if (errorCount === 0 && archivedRowIds.size === rowsToArchive.length)
673
+ onClose();
674
+ }
675
+ catch (error) {
676
+ TMExceptionBoxManager.show({ exception: error });
677
+ }
678
+ finally {
679
+ abortControllerRef.current = undefined;
680
+ setWaitPanel(CLOSED_WAIT_PANEL);
681
+ setIsArchiving(false);
682
+ }
683
+ };
684
+ // Archiviazione di una singola riga, con avanzamento dell'upload sulla barra secondaria
685
+ const archiveRowAsync = async (row, detailTID, dataTypeByMid, associationMids, signal) => {
686
+ const ae = new ArchiveEngineByID(SDK_Globals.tmSession);
687
+ ae.TID = detailTID;
688
+ ae.Metadata_ClearAll();
689
+ // Valori del form + associazioni con il master (le associazioni prevalgono). I metadati di
690
+ // sistema (TID, DID, FILEEXT...) non si archiviano: li assegna il sistema e mandarli fa fallire
691
+ // l'archiviazione. Il form li esclude già (vedi useArchiveListForm), qui è la rete di sicurezza
692
+ // per le righe che arrivano da altre strade, come la copia di un dettaglio già archiviato
693
+ const valueByMid = new Map();
694
+ for (const v of row.__values) {
695
+ if (v.mid !== undefined && v.mid > 99 && v.value)
696
+ valueByMid.set(v.mid, v.value);
697
+ }
698
+ for (const a of associationMids) {
699
+ if (a.mid > 99 && a.value)
700
+ valueByMid.set(a.mid, a.value);
701
+ }
702
+ valueByMid.forEach((value, mid) => {
703
+ switch (dataTypeByMid.get(mid)) {
704
+ case MetadataDataTypes.DateTime:
705
+ ae.Metadata_AddDateTime(mid, new Date(value));
706
+ break;
707
+ case MetadataDataTypes.Number:
708
+ ae.Metadata_AddNumber(mid, parseFloat(value));
709
+ break;
710
+ default:
711
+ ae.Metadata_AddString(mid, value);
712
+ break;
713
+ }
714
+ });
715
+ if (row.__file)
716
+ ae.ArchivingFile = row.__file;
717
+ let firstBlock = true;
718
+ let maxFileSize = 0;
719
+ return ae.ArchiveAsync(signal, (pd) => {
720
+ if (firstBlock) {
721
+ maxFileSize = pd.ProgressBarMaximum ?? 0;
722
+ setWaitPanel(prev => ({ ...prev, waitPanelMaxValueSecondary: maxFileSize }));
723
+ firstBlock = false;
724
+ }
725
+ setWaitPanel(prev => ({
726
+ ...prev,
727
+ waitPanelValueSecondary: pd.ProgressBarValue,
728
+ waitPanelTextSecondary: `Uploading... ${Globalization.getNumberDisplayValue(pd.ProgressBarValue, true)} / ${Globalization.getNumberDisplayValue(maxFileSize, true)}`,
729
+ }));
730
+ if (pd.ProgressBarValue === pd.ProgressBarMaximum) {
731
+ setWaitPanel(prev => ({ ...prev, waitPanelValueSecondary: 0, waitPanelMaxValueSecondary: 0, waitPanelTextSecondary: '' }));
732
+ firstBlock = true;
733
+ }
734
+ });
735
+ };
736
+ return {
737
+ // Flusso
738
+ isLoading, isSummaryReady, phase, setPhase,
739
+ detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes,
740
+ selectRelation, backToTypeSelection, isBackLocked, closeModal,
741
+ // Navigazione del master sulla lista di provenienza
742
+ isMasterNavigationEnabled, canNavigateMasterPrev, canNavigateMasterNext, navigateMaster,
743
+ // Sezioni
744
+ sectionsOpen, setSectionOpen, openAllSections, clearLayout, layoutResetToken,
745
+ hasSavedLayout, setHasSavedLayout,
746
+ // Master
747
+ canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken,
748
+ isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync,
749
+ // Dettagli già archiviati
750
+ archivedDetailResults, archivedDetailsCount, isLoadingArchivedDetails, refreshArchivedDetailsAsync,
751
+ // Dettagli da archiviare
752
+ detailColumns, detailSupportsFile, archiveRows, currentRows,
753
+ focusedRowKey, setFocusedRowKey, selectedRowKeys, setSelectedRowKeys,
754
+ isOpenAddForm, editingRowId, editingRow, formInputMids,
755
+ openAddForm, openEditForm, closeAddForm, deleteRow, duplicateRow, upsertRow, updateRowValues, copyArchivedDetailToRows,
756
+ // Archiviazione
757
+ stopOnFirstError, setStopOnFirstError, isArchiving, archiveAllAsync,
758
+ waitPanel, abortController: abortControllerRef.current,
759
+ };
760
+ };