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

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 (41) 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/TMDataGrid.d.ts +2 -0
  4. package/lib/components/base/TMDataGrid.js +68 -4
  5. package/lib/components/base/TMModal.d.ts +4 -0
  6. package/lib/components/base/TMModal.js +42 -11
  7. package/lib/components/features/documents/TMDcmtForm.d.ts +7 -0
  8. package/lib/components/features/documents/TMDcmtForm.js +94 -40
  9. package/lib/components/features/documents/TMMasterDetailDcmts.js +88 -30
  10. package/lib/components/features/documents/TMMasterInfoFields.d.ts +19 -0
  11. package/lib/components/features/documents/TMMasterInfoFields.js +172 -0
  12. package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.d.ts +40 -0
  13. package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.js +256 -0
  14. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.d.ts +352 -0
  15. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.js +221 -0
  16. package/lib/components/features/documents/TMRelationViewer.d.ts +14 -1
  17. package/lib/components/features/documents/TMRelationViewer.js +69 -20
  18. package/lib/components/features/search/TMSearchResult.d.ts +8 -0
  19. package/lib/components/features/search/TMSearchResult.js +131 -23
  20. package/lib/components/layout/panelManager/TMPanelManagerContainer.d.ts +1 -0
  21. package/lib/components/layout/panelManager/TMPanelManagerContainer.js +24 -14
  22. package/lib/components/layout/panelManager/TMPanelWrapper.js +2 -2
  23. package/lib/components/layout/panelManager/types.d.ts +3 -0
  24. package/lib/helper/SDKUI_Globals.d.ts +34 -0
  25. package/lib/helper/SDKUI_Globals.js +37 -0
  26. package/lib/helper/SDKUI_Localizator.d.ts +18 -0
  27. package/lib/helper/SDKUI_Localizator.js +180 -0
  28. package/lib/helper/TMIcons.d.ts +1 -0
  29. package/lib/helper/TMIcons.js +3 -0
  30. package/lib/helper/dcmtsHelper.d.ts +20 -0
  31. package/lib/helper/dcmtsHelper.js +58 -1
  32. package/lib/helper/helpers.d.ts +9 -0
  33. package/lib/helper/helpers.js +8 -0
  34. package/lib/hooks/useArchiveListForm.d.ts +39 -0
  35. package/lib/hooks/useArchiveListForm.js +46 -0
  36. package/lib/hooks/useDocumentOperations.d.ts +1 -0
  37. package/lib/hooks/useDocumentOperations.js +16 -4
  38. package/lib/hooks/useMultiMasterDetailDcmts.d.ts +97 -0
  39. package/lib/hooks/useMultiMasterDetailDcmts.js +555 -0
  40. package/lib/hooks/useRelatedDocuments.js +2 -26
  41. package/package.json +1 -1
@@ -0,0 +1,97 @@
1
+ import { DcmtTypeDescriptor, RelationDescriptor, SearchResultDescriptor } from '@topconsultnpm/sdk-ts';
2
+ import { IColumnProps } from 'devextreme-react/data-grid';
3
+ import { DcmtInfo, MetadataValueDescriptorEx } from '../ts';
4
+ import { AccordionSection, ArchiveRow, ArchiveRowChanges, DetailRelationItem, MetadataScalarValue } from '../components/features/documents/TMMultiMasterDetailDcmtsUtils';
5
+ /** Fasi del flusso di archiviazione multipla dei dettagli */
6
+ export declare enum MultiDetailArchivePhase {
7
+ /** Scelta del tipo documento */
8
+ SelectDcmtType = 0,
9
+ /** Riepilogo delle archiviazioni */
10
+ ArchiveSummary = 1
11
+ }
12
+ /** Riga di riepilogo: definita con i tipi della feature (vedi TMMultiMasterDetailDcmtsUtils) */
13
+ export type { ArchiveRow };
14
+ /** Coppia mid/valore per pre-compilare il form */
15
+ export type MidValue = {
16
+ mid: number;
17
+ value: string;
18
+ };
19
+ /** Stato del wait panel: le chiavi sono quelle dei props di TMLayoutWaitingContainer */
20
+ export type WaitPanelState = {
21
+ showWaitPanel: boolean;
22
+ waitPanelTitle: string;
23
+ showWaitPanelPrimary: boolean;
24
+ waitPanelTextPrimary: string;
25
+ waitPanelValuePrimary: number;
26
+ waitPanelMaxValuePrimary: number;
27
+ showWaitPanelSecondary: boolean;
28
+ waitPanelTextSecondary: string;
29
+ waitPanelValueSecondary: number;
30
+ waitPanelMaxValueSecondary: number;
31
+ };
32
+ interface UseMultiMasterDetailDcmtsParams {
33
+ /** Tipo documento del master */
34
+ dtd: DcmtTypeDescriptor;
35
+ /** Documento master da cui parte l'archiviazione dei dettagli */
36
+ masterDcmt: DcmtInfo;
37
+ /** Chiusura del modale */
38
+ onClose: () => void;
39
+ }
40
+ /**
41
+ * Logica dell'archiviazione multipla dei dettagli: relazioni, sezioni, righe del riepilogo e
42
+ * archiviazione. Il rendering sta in TMMultiMasterDetailDcmtsForm
43
+ */
44
+ export declare const useMultiMasterDetailDcmts: ({ dtd, masterDcmt, onClose }: UseMultiMasterDetailDcmtsParams) => {
45
+ isLoading: boolean;
46
+ isSummaryReady: boolean;
47
+ phase: MultiDetailArchivePhase;
48
+ setPhase: import("react").Dispatch<import("react").SetStateAction<MultiDetailArchivePhase>>;
49
+ detailRelationItems: DetailRelationItem[];
50
+ selectedRelation: RelationDescriptor | undefined;
51
+ selectedItem: DetailRelationItem | undefined;
52
+ hasMultipleTypes: boolean;
53
+ selectRelation: (item: DetailRelationItem) => void;
54
+ backToTypeSelection: () => void;
55
+ isBackLocked: boolean;
56
+ closeModal: () => void;
57
+ sectionsOpen: Record<AccordionSection, boolean>;
58
+ setSectionOpen: (section: AccordionSection, isOpen: boolean) => void;
59
+ clearLayout: () => void;
60
+ layoutResetToken: number;
61
+ canUpdateMaster: boolean;
62
+ masterFieldsCount: number;
63
+ getMasterMetadataAsync: () => Promise<Map<number, MetadataScalarValue>>;
64
+ masterInfoReloadToken: number;
65
+ isOpenMasterForm: boolean;
66
+ setIsOpenMasterForm: import("react").Dispatch<import("react").SetStateAction<boolean>>;
67
+ handleMasterSavedAsync: () => Promise<void>;
68
+ archivedDetailResults: SearchResultDescriptor[];
69
+ archivedDetailsCount: number;
70
+ isLoadingArchivedDetails: boolean;
71
+ refreshArchivedDetailsAsync: () => Promise<void>;
72
+ detailColumns: IColumnProps[];
73
+ detailSupportsFile: boolean;
74
+ archiveRows: ArchiveRow[];
75
+ currentRows: ArchiveRow[];
76
+ focusedRowKey: number | undefined;
77
+ setFocusedRowKey: import("react").Dispatch<import("react").SetStateAction<number | undefined>>;
78
+ selectedRowKeys: number[];
79
+ setSelectedRowKeys: import("react").Dispatch<import("react").SetStateAction<number[]>>;
80
+ isOpenAddForm: boolean;
81
+ editingRowId: number | undefined;
82
+ editingRow: ArchiveRow | undefined;
83
+ formInputMids: MidValue[];
84
+ openAddForm: () => void;
85
+ openEditForm: (row: ArchiveRow | undefined) => void;
86
+ closeAddForm: () => void;
87
+ deleteRow: (row: ArchiveRow | undefined) => void;
88
+ duplicateRow: (row: ArchiveRow | undefined) => void;
89
+ upsertRow: (values: Array<MetadataValueDescriptorEx>, file: File | undefined) => void;
90
+ updateRowValues: (rowId: number, changes: ArchiveRowChanges) => void;
91
+ stopOnFirstError: boolean;
92
+ setStopOnFirstError: import("react").Dispatch<import("react").SetStateAction<boolean>>;
93
+ isArchiving: boolean;
94
+ archiveAllAsync: () => Promise<void>;
95
+ waitPanel: WaitPanelState;
96
+ abortController: AbortController | undefined;
97
+ };
@@ -0,0 +1,555 @@
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
+ * Cancella il layout configurato dall'utente (altezze dei pannelli e sezioni collassate), previa
223
+ * conferma. Il token cambia per rimontare il panel manager, che legge le dimensioni solo all'avvio.
224
+ */
225
+ const clearLayout = () => {
226
+ TMMessageBoxManager.show({
227
+ title: 'Cancella layout',
228
+ message: 'Vuoi cancellare il layout?',
229
+ buttons: [ButtonNames.YES, ButtonNames.NO],
230
+ onButtonClick: (button) => {
231
+ if (button !== ButtonNames.YES)
232
+ return;
233
+ clearMultiDetailLayout();
234
+ const restored = getMultiDetailSectionsOpen();
235
+ setSectionsOpen(restored);
236
+ setLayoutResetToken(prev => prev + 1);
237
+ // Le sezioni riaperte dal ripristino caricano i dettagli già archiviati come alla prima apertura
238
+ if (restored.archivedDetails && !areArchivedDetailsLoadedRef.current && !isLoadingArchivedDetails)
239
+ refreshArchivedDetailsAsync();
240
+ ShowAlert({ message: 'Layout cancellato', mode: 'success', title: 'Cancella layout', duration: 3000 });
241
+ },
242
+ });
243
+ };
244
+ // =============================================================================================
245
+ // NAVIGAZIONE
246
+ // =============================================================================================
247
+ const selectRelation = (item) => {
248
+ setSelectedRelation(item.relation);
249
+ setPhase(MultiDetailArchivePhase.ArchiveSummary);
250
+ };
251
+ const backToTypeSelection = () => {
252
+ setSelectedRelation(undefined);
253
+ setPhase(MultiDetailArchivePhase.SelectDcmtType);
254
+ };
255
+ // Con righe inserite per il tipo corrente il ritorno alla scelta del tipo è bloccato
256
+ const isBackLocked = currentRows.length > 0;
257
+ const closeModal = () => {
258
+ if (archiveRows.length === 0) {
259
+ onClose();
260
+ return;
261
+ }
262
+ TMMessageBoxManager.show({
263
+ title: SDKUI_Localizator.Archive,
264
+ message: SDKUI_Localizator.ExitWithoutArchiving_Confirm,
265
+ buttons: [ButtonNames.YES, ButtonNames.NO],
266
+ onButtonClick: (button) => { if (button === ButtonNames.YES)
267
+ onClose(); },
268
+ });
269
+ };
270
+ // =============================================================================================
271
+ // RIGHE DEL RIEPILOGO
272
+ // =============================================================================================
273
+ const openAddForm = () => {
274
+ if (!selectedRelation?.detailTID)
275
+ return;
276
+ setEditingRowId(undefined);
277
+ setIsOpenAddForm(true);
278
+ };
279
+ // Identità stabile: la griglia dei dettagli la usa nelle sue callback memoizzate
280
+ const openEditForm = useCallback((row) => {
281
+ if (!row)
282
+ return;
283
+ setEditingRowId(row.id);
284
+ setIsOpenAddForm(true);
285
+ }, []);
286
+ const closeAddForm = () => {
287
+ setEditingRowId(undefined);
288
+ setIsOpenAddForm(false);
289
+ };
290
+ const deleteRow = useCallback((row) => {
291
+ if (!row)
292
+ return;
293
+ TMMessageBoxManager.show({
294
+ title: SDKUI_Localizator.Remove,
295
+ message: SDKUI_Localizator.RemoveDetailFromSummary_Confirm,
296
+ buttons: [ButtonNames.YES, ButtonNames.NO],
297
+ onButtonClick: (button) => {
298
+ if (button !== ButtonNames.YES)
299
+ return;
300
+ setArchiveRows(prev => prev.filter(r => r.id !== row.id));
301
+ setFocusedRowKey(prev => (prev === row.id ? undefined : prev));
302
+ setSelectedRowKeys(prev => prev.filter(k => k !== row.id));
303
+ },
304
+ });
305
+ }, []);
306
+ const buildArchiveRow = useCallback((id, tid, values, file) => {
307
+ const row = { id, __tid: tid, __values: values, __file: file };
308
+ for (const v of values) {
309
+ if (v.mid !== undefined)
310
+ row[String(v.mid)] = v.value ?? '';
311
+ }
312
+ return row;
313
+ }, []);
314
+ // Aggiunge/aggiorna nel riepilogo la riga compilata nel form, senza archiviare a sistema
315
+ const upsertRow = (values, file) => {
316
+ const tid = selectedRelation?.detailTID;
317
+ if (tid === undefined)
318
+ return;
319
+ if (editingRowId !== undefined) {
320
+ setArchiveRows(prev => prev.map(r => (r.id === editingRowId ? buildArchiveRow(editingRowId, tid, values, file) : r)));
321
+ }
322
+ else {
323
+ rowIdRef.current += 1;
324
+ setArchiveRows(prev => [...prev, buildArchiveRow(rowIdRef.current, tid, values, file)]);
325
+ }
326
+ closeAddForm();
327
+ };
328
+ /**
329
+ * Modifica diretta in griglia di uno o più metadati testuali (changes: mid -> valore).
330
+ * La riga e i suoi __values sono ricreati: il duplicato, che condivide __values per
331
+ * riferimento, resta invariato
332
+ */
333
+ const updateRowValues = useCallback((rowId, changes) => {
334
+ const changedMids = Object.keys(changes);
335
+ if (changedMids.length === 0)
336
+ return;
337
+ // La griglia restituisce null sull'editor svuotato: a sistema il metadato va vuoto, non nullo
338
+ const normalized = {};
339
+ changedMids.forEach(mid => { normalized[mid] = changes[mid] ?? ''; });
340
+ setArchiveRows(prev => prev.map(r => {
341
+ if (r.id !== rowId)
342
+ return r;
343
+ const values = r.__values.map(v => (v.mid !== undefined && normalized[String(v.mid)] !== undefined
344
+ ? { ...v, value: normalized[String(v.mid)] }
345
+ : v));
346
+ // Un metadato assente dai valori del form non arriverebbe all'archiviazione
347
+ changedMids.filter(mid => !values.some(v => String(v.mid) === mid)).forEach(mid => {
348
+ const added = new MetadataValueDescriptorEx();
349
+ added.tid = r.__tid;
350
+ added.mid = Number(mid);
351
+ added.value = normalized[mid];
352
+ values.push(added);
353
+ });
354
+ const updated = { ...r, __values: values };
355
+ changedMids.forEach(mid => { updated[mid] = normalized[mid]; });
356
+ return updated;
357
+ }));
358
+ }, []);
359
+ // I __values sono condivisi per riferimento: la modifica sostituisce l'intero array, quindi non intacca il duplicato
360
+ const duplicateRow = useCallback((row) => {
361
+ if (!row)
362
+ return;
363
+ rowIdRef.current += 1;
364
+ setArchiveRows(prev => [...prev, buildArchiveRow(rowIdRef.current, row.__tid, row.__values, row.__file)]);
365
+ }, [buildArchiveRow]);
366
+ // All'apertura del form pre-compila i MID di collegamento valorizzati dal master
367
+ useEffect(() => {
368
+ if (!isOpenAddForm || !selectedRelation) {
369
+ setAssociationInputMids([]);
370
+ return;
371
+ }
372
+ let cancelled = false;
373
+ getMasterAssociationsForDetailAsync(selectedRelation)
374
+ .then(associations => { if (!cancelled)
375
+ setAssociationInputMids(associations); })
376
+ .catch(() => { if (!cancelled)
377
+ setAssociationInputMids([]); });
378
+ return () => { cancelled = true; };
379
+ }, [isOpenAddForm, selectedRelation]);
380
+ // MID pre-compilati: in inserimento i collegamenti dal master, in modifica i valori della riga
381
+ const editingRow = editingRowId !== undefined ? archiveRows.find(r => r.id === editingRowId) : undefined;
382
+ const nextFormInputMids = editingRow
383
+ ? editingRow.__values
384
+ .filter(v => v.mid !== undefined && v.value !== undefined && v.value !== null && v.value !== '')
385
+ .map(v => ({ mid: v.mid, value: String(v.value) }))
386
+ : associationInputMids;
387
+ // Il form riapplica i valori quando cambia il riferimento di inputMids: si mantiene la stessa
388
+ // istanza finché il contenuto non cambia, così le modifiche dell'utente non vengono sovrascritte
389
+ const formInputMidsSignature = nextFormInputMids.map(o => `${o.mid}=${o.value}`).join('|');
390
+ const formInputMidsRef = useRef(nextFormInputMids);
391
+ const formInputMidsSignatureRef = useRef(formInputMidsSignature);
392
+ if (formInputMidsSignatureRef.current !== formInputMidsSignature) {
393
+ formInputMidsSignatureRef.current = formInputMidsSignature;
394
+ formInputMidsRef.current = nextFormInputMids;
395
+ }
396
+ const formInputMids = formInputMidsRef.current;
397
+ // =============================================================================================
398
+ // ARCHIVIAZIONE
399
+ // =============================================================================================
400
+ // Archivia solo le righe del tipo corrente: quelle di altri tipi restano intatte
401
+ const archiveAllAsync = async () => {
402
+ const detailTID = selectedRelation?.detailTID;
403
+ if (detailTID === undefined)
404
+ return;
405
+ const rowsToArchive = archiveRows.filter(r => r.__tid === detailTID);
406
+ if (rowsToArchive.length === 0) {
407
+ ShowAlert({ message: SDKUI_Localizator.NoDataToDisplay, mode: 'warning', title: SDKUI_Localizator.Archive, duration: 3000 });
408
+ return;
409
+ }
410
+ try {
411
+ setIsArchiving(true);
412
+ const detailDtd = await getDetailDtdAsync(detailTID);
413
+ if (!canArchive(detailDtd)) {
414
+ ShowAlert({ message: SDKUI_Localizator.YouDoNotHavePermissionsToArchiveDetailDocumentsOfThisType, mode: 'warning', title: SDKUI_Localizator.Archive, duration: 5000 });
415
+ return;
416
+ }
417
+ const dataTypeByMid = new Map();
418
+ (detailDtd?.metadata ?? []).forEach(md => { if (md.id !== undefined)
419
+ dataTypeByMid.set(md.id, md.dataType); });
420
+ // Collegamenti della relazione valorizzati dal master (prevalgono sui valori del form)
421
+ const associationMids = await getMasterAssociationsForDetailAsync(selectedRelation);
422
+ // Barra primaria: avanzamento documenti. Barra secondaria: upload del file corrente
423
+ const abortController = new AbortController();
424
+ abortControllerRef.current = abortController;
425
+ setWaitPanel({
426
+ ...CLOSED_WAIT_PANEL,
427
+ showWaitPanel: true,
428
+ waitPanelTitle: SDKUI_Localizator.Archiving,
429
+ showWaitPanelPrimary: rowsToArchive.length > 1,
430
+ waitPanelMaxValuePrimary: rowsToArchive.length,
431
+ showWaitPanelSecondary: true,
432
+ });
433
+ const result = [];
434
+ const archivedRowIds = new Set();
435
+ for (let i = 0; i < rowsToArchive.length; i++) {
436
+ if (abortController.signal.aborted) {
437
+ result.push({ rowIndex: i, id1: detailTID, id2: undefined, resultType: ResultTypes.WARNING, description: `Operazione interrotta. Elaborati ${i} documenti` });
438
+ break;
439
+ }
440
+ const rowData = rowsToArchive[i];
441
+ try {
442
+ setWaitPanel(prev => ({ ...prev, waitPanelTextPrimary: `${SDKUI_Localizator.Archiving} ${i + 1} / ${rowsToArchive.length}` }));
443
+ const newDID = await archiveRowAsync(rowData, detailTID, dataTypeByMid, associationMids, abortController.signal);
444
+ setWaitPanel(prev => ({ ...prev, waitPanelValuePrimary: i + 1 }));
445
+ archivedRowIds.add(rowData.id);
446
+ result.push({ rowIndex: i, id1: detailTID, id2: newDID, resultType: ResultTypes.SUCCESS });
447
+ }
448
+ catch (error) {
449
+ const err = error;
450
+ // Annullamento richiesto dall'utente: interrompe senza mostrare l'errore
451
+ if (err?.name === 'CanceledError' || abortController.signal.aborted) {
452
+ result.push({ rowIndex: i, id1: detailTID, id2: undefined, resultType: ResultTypes.WARNING, description: `Operazione interrotta. Elaborati ${i} documenti` });
453
+ break;
454
+ }
455
+ result.push({ rowIndex: i, id1: detailTID, id2: undefined, resultType: ResultTypes.ERROR, description: getExceptionMessage(error) });
456
+ if (stopOnFirstError)
457
+ break;
458
+ }
459
+ }
460
+ // Le righe archiviate escono dal riepilogo
461
+ if (archivedRowIds.size > 0) {
462
+ setArchiveRows(prev => prev.filter(r => !archivedRowIds.has(r.id)));
463
+ setSelectedRowKeys([]);
464
+ setFocusedRowKey(undefined);
465
+ if (areArchivedDetailsLoadedRef.current)
466
+ refreshArchivedDetailsAsync();
467
+ }
468
+ const successCount = result.filter(o => o.resultType === ResultTypes.SUCCESS).length;
469
+ const successMsg = `${successCount} ${successCount === 1 ? 'documento di dettaglio archiviato' : 'documenti di dettaglio archiviati'} con successo`;
470
+ TMResultManager.show(result, SDKUI_Localizator.Archive, 'TID', 'DID', successMsg);
471
+ // Tutte le righe del tipo corrente archiviate senza errori: il modale si chiude
472
+ const errorCount = result.filter(o => o.resultType === ResultTypes.ERROR).length;
473
+ if (errorCount === 0 && archivedRowIds.size === rowsToArchive.length)
474
+ onClose();
475
+ }
476
+ catch (error) {
477
+ TMExceptionBoxManager.show({ exception: error });
478
+ }
479
+ finally {
480
+ abortControllerRef.current = undefined;
481
+ setWaitPanel(CLOSED_WAIT_PANEL);
482
+ setIsArchiving(false);
483
+ }
484
+ };
485
+ // Archiviazione di una singola riga, con avanzamento dell'upload sulla barra secondaria
486
+ const archiveRowAsync = async (row, detailTID, dataTypeByMid, associationMids, signal) => {
487
+ const ae = new ArchiveEngineByID(SDK_Globals.tmSession);
488
+ ae.TID = detailTID;
489
+ ae.Metadata_ClearAll();
490
+ // Valori del form + associazioni con il master (le associazioni prevalgono)
491
+ const valueByMid = new Map();
492
+ for (const v of row.__values) {
493
+ if (v.mid !== undefined && v.value)
494
+ valueByMid.set(v.mid, v.value);
495
+ }
496
+ for (const a of associationMids) {
497
+ if (a.value)
498
+ valueByMid.set(a.mid, a.value);
499
+ }
500
+ valueByMid.forEach((value, mid) => {
501
+ switch (dataTypeByMid.get(mid)) {
502
+ case MetadataDataTypes.DateTime:
503
+ ae.Metadata_AddDateTime(mid, new Date(value));
504
+ break;
505
+ case MetadataDataTypes.Number:
506
+ ae.Metadata_AddNumber(mid, parseFloat(value));
507
+ break;
508
+ default:
509
+ ae.Metadata_AddString(mid, value);
510
+ break;
511
+ }
512
+ });
513
+ if (row.__file)
514
+ ae.ArchivingFile = row.__file;
515
+ let firstBlock = true;
516
+ let maxFileSize = 0;
517
+ return ae.ArchiveAsync(signal, (pd) => {
518
+ if (firstBlock) {
519
+ maxFileSize = pd.ProgressBarMaximum ?? 0;
520
+ setWaitPanel(prev => ({ ...prev, waitPanelMaxValueSecondary: maxFileSize }));
521
+ firstBlock = false;
522
+ }
523
+ setWaitPanel(prev => ({
524
+ ...prev,
525
+ waitPanelValueSecondary: pd.ProgressBarValue,
526
+ waitPanelTextSecondary: `Uploading... ${Globalization.getNumberDisplayValue(pd.ProgressBarValue, true)} / ${Globalization.getNumberDisplayValue(maxFileSize, true)}`,
527
+ }));
528
+ if (pd.ProgressBarValue === pd.ProgressBarMaximum) {
529
+ setWaitPanel(prev => ({ ...prev, waitPanelValueSecondary: 0, waitPanelMaxValueSecondary: 0, waitPanelTextSecondary: '' }));
530
+ firstBlock = true;
531
+ }
532
+ });
533
+ };
534
+ return {
535
+ // Flusso
536
+ isLoading, isSummaryReady, phase, setPhase,
537
+ detailRelationItems, selectedRelation, selectedItem, hasMultipleTypes,
538
+ selectRelation, backToTypeSelection, isBackLocked, closeModal,
539
+ // Sezioni
540
+ sectionsOpen, setSectionOpen, clearLayout, layoutResetToken,
541
+ // Master
542
+ canUpdateMaster, masterFieldsCount, getMasterMetadataAsync, masterInfoReloadToken,
543
+ isOpenMasterForm, setIsOpenMasterForm, handleMasterSavedAsync,
544
+ // Dettagli già archiviati
545
+ archivedDetailResults, archivedDetailsCount, isLoadingArchivedDetails, refreshArchivedDetailsAsync,
546
+ // Dettagli da archiviare
547
+ detailColumns, detailSupportsFile, archiveRows, currentRows,
548
+ focusedRowKey, setFocusedRowKey, selectedRowKeys, setSelectedRowKeys,
549
+ isOpenAddForm, editingRowId, editingRow, formInputMids,
550
+ openAddForm, openEditForm, closeAddForm, deleteRow, duplicateRow, upsertRow, updateRowValues,
551
+ // Archiviazione
552
+ stopOnFirstError, setStopOnFirstError, isArchiving, archiveAllAsync,
553
+ waitPanel, abortController: abortControllerRef.current,
554
+ };
555
+ };