@topconsultnpm/sdkui-react 6.22.0-dev2.2 → 6.22.0-dev2.21

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