@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.
- package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +1 -1
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +1 -1
- package/lib/components/base/TMButton.d.ts +13 -0
- package/lib/components/base/TMButton.js +137 -5
- package/lib/components/base/TMDataGrid.d.ts +2 -0
- package/lib/components/base/TMDataGrid.js +81 -8
- package/lib/components/base/TMEditorBase.d.ts +2 -0
- package/lib/components/base/TMFileManager.js +5 -3
- package/lib/components/base/TMFileManagerUtils.d.ts +1 -0
- package/lib/components/base/TMFileManagerUtils.js +16 -0
- package/lib/components/base/TMModal.d.ts +11 -0
- package/lib/components/base/TMModal.js +47 -13
- package/lib/components/base/TMPanel.d.ts +1 -0
- package/lib/components/base/TMPanel.js +79 -20
- package/lib/components/base/TMPopUp.js +3 -0
- package/lib/components/base/TMTooltip.d.ts +2 -1
- package/lib/components/base/TMTooltip.js +23 -6
- package/lib/components/base/TMTreeView.js +108 -67
- package/lib/components/choosers/TMDcmtTypeChooser.js +4 -3
- package/lib/components/choosers/TMDistinctValues.d.ts +9 -7
- package/lib/components/choosers/TMDistinctValues.js +149 -142
- package/lib/components/choosers/TMMetadataChooser.js +1 -1
- package/lib/components/choosers/TMQuickSearchInfo.d.ts +12 -0
- package/lib/components/choosers/TMQuickSearchInfo.js +279 -0
- package/lib/components/choosers/TMQuickSearchSettingsForm.d.ts +16 -0
- package/lib/components/choosers/TMQuickSearchSettingsForm.js +94 -0
- package/lib/components/choosers/TMSelectedValuesSummary.d.ts +13 -0
- package/lib/components/choosers/TMSelectedValuesSummary.js +91 -0
- package/lib/components/editors/TMDropDown.d.ts +3 -0
- package/lib/components/editors/TMDropDown.js +197 -5
- package/lib/components/editors/TMMetadataEditor.d.ts +5 -1
- package/lib/components/editors/TMMetadataEditor.js +8 -3
- package/lib/components/editors/TMTextBox.js +2 -2
- package/lib/components/features/documents/TMBatchUpdateForm.js +19 -5
- package/lib/components/features/documents/TMDcmtBlog.d.ts +3 -0
- package/lib/components/features/documents/TMDcmtBlog.js +4 -4
- package/lib/components/features/documents/TMDcmtForm.d.ts +7 -0
- package/lib/components/features/documents/TMDcmtForm.js +112 -45
- package/lib/components/features/documents/TMDcmtFormActionButtons.d.ts +11 -0
- package/lib/components/features/documents/TMDcmtFormActionButtons.js +74 -7
- package/lib/components/features/documents/TMDcmtPreview.js +10 -6
- package/lib/components/features/documents/TMMasterDetailDcmts.js +95 -33
- package/lib/components/features/documents/TMMasterInfoFields.d.ts +19 -0
- package/lib/components/features/documents/TMMasterInfoFields.js +186 -0
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.d.ts +45 -0
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.js +323 -0
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.d.ts +370 -0
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.js +283 -0
- package/lib/components/features/documents/TMRelationViewer.d.ts +14 -1
- package/lib/components/features/documents/TMRelationViewer.js +69 -20
- package/lib/components/features/search/TMSearch.js +31 -18
- package/lib/components/features/search/TMSearchQueryEditor.js +5 -3
- package/lib/components/features/search/TMSearchQueryPanel.d.ts +2 -1
- package/lib/components/features/search/TMSearchQueryPanel.js +45 -9
- package/lib/components/features/search/TMSearchResult.d.ts +15 -0
- package/lib/components/features/search/TMSearchResult.js +509 -187
- package/lib/components/features/search/TMTreeSelector.js +18 -5
- package/lib/components/features/workflow/TMWorkflowPopup.d.ts +10 -0
- package/lib/components/features/workflow/TMWorkflowPopup.js +8 -4
- package/lib/components/layout/panelManager/TMPanelManagerContainer.d.ts +1 -0
- package/lib/components/layout/panelManager/TMPanelManagerContainer.js +24 -14
- package/lib/components/layout/panelManager/TMPanelManagerContext.js +68 -93
- package/lib/components/layout/panelManager/TMPanelWrapper.js +2 -2
- package/lib/components/layout/panelManager/types.d.ts +3 -0
- package/lib/components/layout/panelManager/utils.js +13 -11
- package/lib/components/viewers/TMMidViewer.js +4 -1
- package/lib/components/viewers/TMTidViewer.js +25 -11
- package/lib/devextreme-license.d.ts +1 -0
- package/lib/devextreme-license.js +3 -0
- package/lib/helper/SDKUI_Globals.d.ts +109 -0
- package/lib/helper/SDKUI_Globals.js +150 -0
- package/lib/helper/SDKUI_Localizator.d.ts +34 -0
- package/lib/helper/SDKUI_Localizator.js +340 -0
- package/lib/helper/TMIcons.d.ts +1 -0
- package/lib/helper/TMIcons.js +3 -0
- package/lib/helper/dataGridSearchHelper.d.ts +28 -0
- package/lib/helper/dataGridSearchHelper.js +51 -0
- package/lib/helper/dcmtsHelper.d.ts +20 -0
- package/lib/helper/dcmtsHelper.js +58 -1
- package/lib/helper/helpers.d.ts +20 -0
- package/lib/helper/helpers.js +55 -1
- package/lib/helper/index.d.ts +1 -0
- package/lib/helper/index.js +1 -0
- package/lib/helper/queryHelper.d.ts +2 -0
- package/lib/helper/queryHelper.js +3 -1
- package/lib/hooks/tmDistinctValuesGridHelper.d.ts +16 -0
- package/lib/hooks/tmDistinctValuesGridHelper.js +31 -0
- package/lib/hooks/useArchiveListForm.d.ts +39 -0
- package/lib/hooks/useArchiveListForm.js +46 -0
- package/lib/hooks/useCaseFlowApprove.d.ts +56 -0
- package/lib/hooks/useCaseFlowApprove.js +157 -0
- package/lib/hooks/useDataUserIdItem.d.ts +1 -0
- package/lib/hooks/useDataUserIdItem.js +1 -0
- package/lib/hooks/useDocumentOperations.d.ts +4 -0
- package/lib/hooks/useDocumentOperations.js +41 -7
- package/lib/hooks/useMultiMasterDetailDcmts.d.ts +113 -0
- package/lib/hooks/useMultiMasterDetailDcmts.js +760 -0
- package/lib/hooks/useRelatedDocuments.js +2 -26
- package/lib/hooks/useShowDossiersV5.d.ts +12 -0
- package/lib/hooks/useShowDossiersV5.js +37 -0
- package/lib/hooks/useTMDistinctValuesMetadataDisplay.d.ts +18 -0
- package/lib/hooks/useTMDistinctValuesMetadataDisplay.js +103 -0
- package/lib/hooks/useTMDistinctValuesQuickSearch.d.ts +49 -0
- package/lib/hooks/useTMDistinctValuesQuickSearch.js +307 -0
- package/lib/hooks/useTMDistinctValuesSelection.d.ts +42 -0
- package/lib/hooks/useTMDistinctValuesSelection.js +103 -0
- package/lib/hooks/useTMDistinctValuesSource.d.ts +29 -0
- package/lib/hooks/useTMDistinctValuesSource.js +105 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +5 -2
- package/lib/services/caseflow/CaseFlowService.d.ts +236 -0
- package/lib/services/caseflow/CaseFlowService.js +693 -0
- package/lib/services/caseflow/CaseFlowTemplateCacheService.d.ts +28 -0
- package/lib/services/caseflow/CaseFlowTemplateCacheService.js +121 -0
- package/lib/services/caseflow/caseFlowNameCache.d.ts +32 -0
- package/lib/services/caseflow/caseFlowNameCache.js +116 -0
- package/lib/services/caseflow/caseFlowWorkItemUtils.d.ts +61 -0
- package/lib/services/caseflow/caseFlowWorkItemUtils.js +113 -0
- package/lib/services/index.d.ts +4 -0
- package/lib/services/index.js +4 -0
- package/lib/services/platform_services.d.ts +1 -1
- 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
|
+
};
|