@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.
- package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +1 -1
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +1 -1
- package/lib/components/base/TMDataGrid.d.ts +2 -0
- package/lib/components/base/TMDataGrid.js +68 -4
- package/lib/components/base/TMModal.d.ts +4 -0
- package/lib/components/base/TMModal.js +42 -11
- package/lib/components/features/documents/TMDcmtForm.d.ts +7 -0
- package/lib/components/features/documents/TMDcmtForm.js +94 -40
- package/lib/components/features/documents/TMMasterDetailDcmts.js +88 -30
- package/lib/components/features/documents/TMMasterInfoFields.d.ts +19 -0
- package/lib/components/features/documents/TMMasterInfoFields.js +172 -0
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.d.ts +40 -0
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.js +256 -0
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.d.ts +352 -0
- package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.js +221 -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/TMSearchResult.d.ts +8 -0
- package/lib/components/features/search/TMSearchResult.js +131 -23
- package/lib/components/layout/panelManager/TMPanelManagerContainer.d.ts +1 -0
- package/lib/components/layout/panelManager/TMPanelManagerContainer.js +24 -14
- package/lib/components/layout/panelManager/TMPanelWrapper.js +2 -2
- package/lib/components/layout/panelManager/types.d.ts +3 -0
- package/lib/helper/SDKUI_Globals.d.ts +34 -0
- package/lib/helper/SDKUI_Globals.js +37 -0
- package/lib/helper/SDKUI_Localizator.d.ts +18 -0
- package/lib/helper/SDKUI_Localizator.js +180 -0
- package/lib/helper/TMIcons.d.ts +1 -0
- package/lib/helper/TMIcons.js +3 -0
- package/lib/helper/dcmtsHelper.d.ts +20 -0
- package/lib/helper/dcmtsHelper.js +58 -1
- package/lib/helper/helpers.d.ts +9 -0
- package/lib/helper/helpers.js +8 -0
- package/lib/hooks/useArchiveListForm.d.ts +39 -0
- package/lib/hooks/useArchiveListForm.js +46 -0
- package/lib/hooks/useDocumentOperations.d.ts +1 -0
- package/lib/hooks/useDocumentOperations.js +16 -4
- package/lib/hooks/useMultiMasterDetailDcmts.d.ts +97 -0
- package/lib/hooks/useMultiMasterDetailDcmts.js +555 -0
- package/lib/hooks/useRelatedDocuments.js +2 -26
- 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
|
+
};
|