@topconsultnpm/sdkui-react 6.21.0-dev2.35 → 6.21.0-dev2.36

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 (30) hide show
  1. package/lib/components/editors/TMTextBox.d.ts +2 -0
  2. package/lib/components/editors/TMTextBox.js +3 -3
  3. package/lib/components/features/documents/TMCopyToFolderForm.d.ts +16 -0
  4. package/lib/components/features/documents/TMCopyToFolderForm.js +306 -0
  5. package/lib/components/features/documents/TMDownloadRelationViewerSection.d.ts +15 -0
  6. package/lib/components/features/documents/TMDownloadRelationViewerSection.js +155 -0
  7. package/lib/components/features/documents/TMMasterDetailDcmts.d.ts +4 -0
  8. package/lib/components/features/documents/TMMasterDetailDcmts.js +6 -5
  9. package/lib/components/features/documents/TMMergeToPdfForm.d.ts +18 -0
  10. package/lib/components/features/documents/TMMergeToPdfForm.js +164 -0
  11. package/lib/components/features/documents/TMRelationViewer.d.ts +13 -0
  12. package/lib/components/features/documents/TMRelationViewer.js +75 -6
  13. package/lib/components/features/documents/copyAndMergeDcmtsShared.d.ts +53 -0
  14. package/lib/components/features/documents/copyAndMergeDcmtsShared.js +262 -0
  15. package/lib/components/features/search/TMSearch.d.ts +2 -0
  16. package/lib/components/features/search/TMSearch.js +2 -2
  17. package/lib/components/features/search/TMSearchResult.d.ts +2 -0
  18. package/lib/components/features/search/TMSearchResult.js +3 -2
  19. package/lib/helper/SDKUI_Globals.d.ts +15 -0
  20. package/lib/helper/SDKUI_Globals.js +15 -1
  21. package/lib/helper/TMUtils.d.ts +19 -0
  22. package/lib/helper/ZipManager.d.ts +56 -0
  23. package/lib/helper/ZipManager.js +104 -0
  24. package/lib/helper/index.d.ts +1 -0
  25. package/lib/helper/index.js +1 -0
  26. package/lib/hooks/useDcmtOperations.d.ts +8 -2
  27. package/lib/hooks/useDcmtOperations.js +29 -20
  28. package/lib/hooks/useDocumentOperations.d.ts +4 -0
  29. package/lib/hooks/useDocumentOperations.js +75 -7
  30. package/package.json +1 -1
@@ -16,7 +16,7 @@ import TMDcmtForm from './TMDcmtForm';
16
16
  import { TMNothingToShow } from './TMDcmtPreview';
17
17
  import { Spinner, TMButton } from '../..';
18
18
  import { useDocumentOperations } from '../../../hooks/useDocumentOperations';
19
- const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers, deviceType, inputDcmts, isForMaster, showCurrentDcmtIndicator = true, allowNavigation, canNext, canPrev, onNext, onPrev, onBack, appendMasterDcmts, onTaskCreateRequest, onRefreshAfterAddDcmtToFavs, editPdfForm, openS4TViewer, onOpenS4TViewerRequest, onOpenPdfEditorRequest, datagridUtility, dcmtUtility }) => {
19
+ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers, deviceType, inputDcmts, isForMaster, showCurrentDcmtIndicator = true, allowNavigation, canNext, canPrev, onNext, onPrev, onBack, appendMasterDcmts, onTaskCreateRequest, onRefreshAfterAddDcmtToFavs, editPdfForm, openS4TViewer, onOpenS4TViewerRequest, onOpenPdfEditorRequest, datagridUtility, dcmtUtility, mergePdfManager }) => {
20
20
  const floatingBarContainerRef = useRef(null);
21
21
  const [focusedItem, setFocusedItem] = useState();
22
22
  const [selectedItems, setSelectedItems] = useState([]);
@@ -117,7 +117,7 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
117
117
  createTaskFromDocumentOrWorkItem();
118
118
  }
119
119
  };
120
- const { operationItems, renderFloatingBar, renderDcmtOperations, features } = useDocumentOperations({
120
+ const { operationItems, renderFloatingBar, renderDcmtOperations, features, } = useDocumentOperations({
121
121
  context: SearchResultContext.MASTER_DETAIL,
122
122
  documentData: {
123
123
  dtd: dtdFocused,
@@ -192,6 +192,7 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
192
192
  openTaskFormHandler,
193
193
  onRefreshAfterAddDcmtToFavs,
194
194
  },
195
+ mergePdfManager: mergePdfManager
195
196
  });
196
197
  // Load dtdMaster when inputDcmts changes
197
198
  useEffect(() => {
@@ -285,7 +286,7 @@ const TMMasterDetailDcmts = ({ allTasks = [], getAllTasks, deleteTaskByIdsCallba
285
286
  position: contextMenuPosition,
286
287
  onClose: () => setContextMenuVisible(false)
287
288
  } })] }) }), [inputDcmts, isForMaster, showCurrentDcmtIndicator, showZeroDcmts, allowMultipleSelection, focusedItem, selectedItems, handleFocusedItemChanged, handleSelectedItemsChanged, handleNoRelationsFound, onItemContextMenu, contextMenuVisible, contextMenuPosition, refreshKey, focusedItemFormData]);
288
- const tmFormOrResult = useMemo(() => _jsx(TMFormOrResultWrapper, { refreshKey: refreshKeyFormOrResult, deviceType: deviceType, focusedItem: focusedItem, onTaskCreateRequest: onTaskCreateRequest, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers, onRefreshAfterAddDcmtToFavs: onRefreshAfterAddDcmtToFavs, editPdfForm: editPdfForm, openS4TViewer: openS4TViewer, onOpenS4TViewerRequest: onOpenS4TViewerRequest, onOpenPdfEditorRequest: onOpenPdfEditorRequest, onRefreshSearchResults: onRefreshAllPanels }), [focusedItem, deviceType, allTasks, handleNavigateToWGs, handleNavigateToDossiers, editPdfForm, openS4TViewer, onOpenS4TViewerRequest, onOpenPdfEditorRequest, onRefreshAfterAddDcmtToFavs, refreshKeyFormOrResult]);
289
+ const tmFormOrResult = useMemo(() => _jsx(TMFormOrResultWrapper, { refreshKey: refreshKeyFormOrResult, deviceType: deviceType, focusedItem: focusedItem, onTaskCreateRequest: onTaskCreateRequest, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers, onRefreshAfterAddDcmtToFavs: onRefreshAfterAddDcmtToFavs, editPdfForm: editPdfForm, openS4TViewer: openS4TViewer, onOpenS4TViewerRequest: onOpenS4TViewerRequest, onOpenPdfEditorRequest: onOpenPdfEditorRequest, onRefreshSearchResults: onRefreshAllPanels, mergePdfManager: mergePdfManager }), [focusedItem, deviceType, allTasks, handleNavigateToWGs, handleNavigateToDossiers, editPdfForm, openS4TViewer, onOpenS4TViewerRequest, onOpenPdfEditorRequest, onRefreshAfterAddDcmtToFavs, refreshKeyFormOrResult]);
289
290
  const initialPanelDimensions = {
290
291
  'tmTreeView': { width: '50%', height: '100%' },
291
292
  'tmFormOrResult': { width: '50%', height: '100%' },
@@ -404,11 +405,11 @@ const TMRelationViewerWrapper = ({ refreshKey, inputDcmts, isForMaster, showCurr
404
405
  }, [onItemContextMenu, handleFocusedItemChanged]);
405
406
  return (_jsx(TMRelationViewer, { inputDcmts: inputDcmts, isForMaster: isForMaster, showCurrentDcmtIndicator: showCurrentDcmtIndicator, initialShowZeroDcmts: showZeroDcmts, customItemRender: customItemRender, allowMultipleSelection: allowMultipleSelection, focusedItem: focusedItem, selectedItems: selectedItems, onFocusedItemChanged: handleFocusedItemChanged, onSelectedItemsChanged: onSelectedItemsChanged, maxDepthLevel: 1, invertMasterNavigation: false, onNoRelationsFound: onNoRelationsFound, onItemContextMenu: onContextMenu, focusedItemFormData: focusedItemFormData }, refreshKey));
406
407
  };
407
- const TMFormOrResultWrapper = ({ refreshKey, deviceType, focusedItem, onTaskCreateRequest, allTasks = [], getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers, onRefreshAfterAddDcmtToFavs, editPdfForm, openS4TViewer, onOpenS4TViewerRequest, onOpenPdfEditorRequest, onRefreshSearchAsyncDatagrid, onRefreshSearchResults }) => {
408
+ const TMFormOrResultWrapper = ({ refreshKey, deviceType, focusedItem, onTaskCreateRequest, allTasks = [], mergePdfManager, getAllTasks, deleteTaskByIdsCallback, addTaskCallback, editTaskCallback, handleNavigateToWGs, handleNavigateToDossiers, onRefreshAfterAddDcmtToFavs, editPdfForm, openS4TViewer, onOpenS4TViewerRequest, onOpenPdfEditorRequest, onRefreshSearchAsyncDatagrid, onRefreshSearchResults }) => {
408
409
  const { setPanelVisibilityById } = useTMPanelManagerContext();
409
410
  return (_jsx(_Fragment, { children: focusedItem?.isDcmt ?
410
411
  _jsx(TMDcmtForm, { groupId: 'tmFormOrResult', TID: focusedItem?.tid, DID: focusedItem.did, allowButtonsRefs: true, isClosable: deviceType !== DeviceType.MOBILE, allowNavigation: false, allowRelations: deviceType !== DeviceType.MOBILE, showDcmtFormSidebar: false, onClose: () => { setPanelVisibilityById('tmTreeView', true); }, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers, moreInfoTasks: getMoreInfoTasksForDocument(allTasks, focusedItem?.tid, focusedItem?.did), openS4TViewer: openS4TViewer, onOpenS4TViewerRequest: onOpenS4TViewerRequest, onOpenPdfEditorRequest: onOpenPdfEditorRequest, datagridUtility: {
411
412
  onRefreshSearchAsyncDatagrid,
412
413
  } }, refreshKey) :
413
- _jsx(TMSearchResult, { groupId: 'tmFormOrResult', isClosable: deviceType !== DeviceType.MOBILE, context: SearchResultContext.METADATA_SEARCH, allowFloatingBar: false, allowRelations: false, openDcmtFormAsModal: true, searchResults: focusedItem?.searchResult ?? [], showSearchResultSidebar: false, showDcmtFormSidebar: false, onTaskCreateRequest: onTaskCreateRequest, onClose: () => { setPanelVisibilityById('tmTreeView', true); }, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers, editPdfForm: editPdfForm, onOpenPdfEditorRequest: onOpenPdfEditorRequest, openS4TViewer: openS4TViewer, onOpenS4TViewerRequest: onOpenS4TViewerRequest, enablePinIcons: false, onRefreshAfterAddDcmtToFavs: onRefreshAfterAddDcmtToFavs, showBackButton: false, onRefreshSearchAsyncDatagrid: onRefreshSearchResults }, refreshKey) }));
414
+ _jsx(TMSearchResult, { groupId: 'tmFormOrResult', isClosable: deviceType !== DeviceType.MOBILE, context: SearchResultContext.METADATA_SEARCH, allowFloatingBar: false, allowRelations: false, openDcmtFormAsModal: true, searchResults: focusedItem?.searchResult ?? [], showSearchResultSidebar: false, showDcmtFormSidebar: false, onTaskCreateRequest: onTaskCreateRequest, onClose: () => { setPanelVisibilityById('tmTreeView', true); }, allTasks: allTasks, getAllTasks: getAllTasks, deleteTaskByIdsCallback: deleteTaskByIdsCallback, addTaskCallback: addTaskCallback, editTaskCallback: editTaskCallback, handleNavigateToWGs: handleNavigateToWGs, handleNavigateToDossiers: handleNavigateToDossiers, editPdfForm: editPdfForm, onOpenPdfEditorRequest: onOpenPdfEditorRequest, openS4TViewer: openS4TViewer, onOpenS4TViewerRequest: onOpenS4TViewerRequest, enablePinIcons: false, onRefreshAfterAddDcmtToFavs: onRefreshAfterAddDcmtToFavs, showBackButton: false, onRefreshSearchAsyncDatagrid: onRefreshSearchResults, mergePdfManager: mergePdfManager }, refreshKey) }));
414
415
  };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { DcmtInfo } from '../../../ts';
3
+ import { MergePdfManagerType } from '../../../helper';
4
+ import { TMCopyToFolderMode } from '../../../hooks/useDocumentOperations';
5
+ interface ITMMergeToPdfFormProps {
6
+ mode: TMCopyToFolderMode;
7
+ selectedDcmtInfos: Array<DcmtInfo>;
8
+ onClose: () => void;
9
+ showTMRelationViewer: boolean;
10
+ mergePdfManager?: MergePdfManagerType;
11
+ }
12
+ /**
13
+ * Form per l'unione di più documenti PDF in un singolo file.
14
+ * Condivide TMDownloadRelationViewerSection e gli helper in copyAndMergeDcmtsShared
15
+ * con TMCopyToFolderForm.
16
+ */
17
+ declare const TMMergeToPdfForm: React.FC<ITMMergeToPdfFormProps>;
18
+ export default TMMergeToPdfForm;
@@ -0,0 +1,164 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { DownloadTypes } from '../../../ts';
4
+ import { calcResponsiveSizes, IconPlay, SDKUI_Globals, SDKUI_Localizator, } from '../../../helper';
5
+ import { DcmtOpers, GeneralRetrieveFormats, ResultTypes, RetrieveFileOptions, ValidationItem, } from '@topconsultnpm/sdk-ts';
6
+ import TMModal from '../../base/TMModal';
7
+ import { useDeviceType } from '../../base/TMDeviceProvider';
8
+ import TMTextBox from '../../editors/TMTextBox';
9
+ import TMButton from '../../base/TMButton';
10
+ import { ButtonNames, TMMessageBoxManager } from '../../base/TMPopUp';
11
+ import { useDcmtOperations } from '../../../hooks/useDcmtOperations';
12
+ import { TMLayoutWaitingContainer } from '../../base/TMWaitPanel';
13
+ import { TMSplitterLayout } from '../../base/TMLayout';
14
+ import { TMColors } from '../../../utils/theme';
15
+ import TMDownloadRelationViewerSection from './TMDownloadRelationViewerSection';
16
+ import { getDcmtInfosToDownload, getFloatingLabelStyle, isPdfExt, MIN_PDF_FOR_MERGE, SPLITTER_MIN, SPLITTER_START_65_35, } from './copyAndMergeDcmtsShared';
17
+ /**
18
+ * Form per l'unione di più documenti PDF in un singolo file.
19
+ * Condivide TMDownloadRelationViewerSection e gli helper in copyAndMergeDcmtsShared
20
+ * con TMCopyToFolderForm.
21
+ */
22
+ const TMMergeToPdfForm = ({ mode, selectedDcmtInfos, onClose, showTMRelationViewer, mergePdfManager }) => {
23
+ const { abortController, showWaitPanel, waitPanelTitle, showPrimary, waitPanelTextPrimary, waitPanelValuePrimary, waitPanelMaxValuePrimary, showSecondary, waitPanelTextSecondary, waitPanelValueSecondary, waitPanelMaxValueSecondary, downloadDcmtsAsync, } = useDcmtOperations();
24
+ const deviceType = useDeviceType();
25
+ const [pdfFileName, setPdfFileName] = useState('');
26
+ const [selectedItemsRelationViewer, setSelectedItemsRelationViewer] = useState([]);
27
+ const getTitle = () => {
28
+ const count = ` (${selectedDcmtInfos.length})`;
29
+ const modeLabelMap = {
30
+ onlySelected: 'Solo i documenti selezionati',
31
+ customized: 'Documenti di primo livello e i correlati selezionati',
32
+ };
33
+ const modeLabel = modeLabelMap[mode] || modeLabelMap.onlySelected;
34
+ return `Unisci in un file PDF - ${modeLabel}${count}`;
35
+ };
36
+ // ---- Validazione ----
37
+ const pdfValidationItems = [];
38
+ if (!pdfFileName.trim()) {
39
+ pdfValidationItems.push(new ValidationItem(ResultTypes.ERROR, 'Nome file PDF', SDKUI_Localizator.RequiredField));
40
+ }
41
+ // ---- Statistiche selezione PDF / non-PDF ----
42
+ const nonPdfSelectedItems = (() => {
43
+ if (showTMRelationViewer) {
44
+ return selectedItemsRelationViewer
45
+ .filter(i => i.isDcmt && !isPdfExt(i.fileExt))
46
+ .map(i => ({ key: `${i.tid}_${i.did}`, ext: i.fileExt ?? undefined }));
47
+ }
48
+ return selectedDcmtInfos
49
+ .filter(d => !isPdfExt(d.FILEEXT))
50
+ .map(d => ({ key: `${d.TID}_${d.DID}`, ext: d.FILEEXT }));
51
+ })();
52
+ const hasNonPdfSelected = nonPdfSelectedItems.length > 0;
53
+ const pdfSelectedCount = (() => {
54
+ if (showTMRelationViewer) {
55
+ return selectedItemsRelationViewer.filter(i => i.isDcmt && isPdfExt(i.fileExt)).length;
56
+ }
57
+ return selectedDcmtInfos.filter(d => isPdfExt(d.FILEEXT)).length;
58
+ })();
59
+ const hasEnoughPdfForMerge = pdfSelectedCount >= MIN_PDF_FOR_MERGE;
60
+ const isFormValid = () => pdfValidationItems.length === 0;
61
+ // ---- Recupero dei file PDF da unire (condiviso tra Esegui e Anteprima) ----
62
+ const collectPdfFilesToMerge = async () => {
63
+ const dcmtInfosToDownload = getDcmtInfosToDownload(selectedDcmtInfos, selectedItemsRelationViewer, showTMRelationViewer);
64
+ if (dcmtInfosToDownload.length === 0) {
65
+ TMMessageBoxManager.show({ message: 'Nessun documento selezionato.', buttons: [ButtonNames.OK] });
66
+ return null;
67
+ }
68
+ const nonPdfKeys = new Set(nonPdfSelectedItems.map(i => i.key));
69
+ const pdfDcmtInfosToDownload = dcmtInfosToDownload.filter(d => !nonPdfKeys.has(`${d.TID}_${d.DID}`));
70
+ if (pdfDcmtInfosToDownload.length === 0) {
71
+ TMMessageBoxManager.show({
72
+ message: 'Tra i documenti selezionati non è presente alcun file PDF: l\'unione non può essere eseguita.',
73
+ buttons: [ButtonNames.OK]
74
+ });
75
+ return null;
76
+ }
77
+ if (pdfDcmtInfosToDownload.length < MIN_PDF_FOR_MERGE) {
78
+ TMMessageBoxManager.show({
79
+ message: `Tra i documenti selezionati è presente un solo file PDF: per l'unione ne servono almeno ${MIN_PDF_FOR_MERGE}.`,
80
+ buttons: [ButtonNames.OK]
81
+ });
82
+ return null;
83
+ }
84
+ const pdfFiles = [];
85
+ const retrieveOptions = new RetrieveFileOptions();
86
+ retrieveOptions.retrieveReason = DcmtOpers.None;
87
+ retrieveOptions.generalRetrieveFormat = GeneralRetrieveFormats.Original;
88
+ retrieveOptions.invoiceRetrieveFormat = SDKUI_Globals.userSettings.searchSettings.invoiceRetrieveFormat;
89
+ retrieveOptions.orderRetrieveFormat = SDKUI_Globals.userSettings.searchSettings.orderRetrieveFormat;
90
+ const collectFileForMerge = async (file, _dcmtInfo) => {
91
+ pdfFiles.push(file);
92
+ };
93
+ await downloadDcmtsAsync(pdfDcmtInfosToDownload, DownloadTypes.Dcmt, 'download', collectFileForMerge, undefined, true, retrieveOptions, false);
94
+ if (pdfFiles.length === 0) {
95
+ TMMessageBoxManager.show({ message: 'Nessun file disponibile per l\'unione.', buttons: [ButtonNames.OK] });
96
+ return null;
97
+ }
98
+ return pdfFiles;
99
+ };
100
+ // ---- Esecuzione: scarica i PDF e li unisce ----
101
+ const run = async () => {
102
+ if (pdfValidationItems.length > 0) {
103
+ TMMessageBoxManager.show({ message: 'Per favore, correggi gli errori prima di procedere.', buttons: [ButtonNames.OK] });
104
+ return;
105
+ }
106
+ if (!mergePdfManager) {
107
+ TMMessageBoxManager.show({ message: 'La funzionalità di unione PDF non è disponibile.', buttons: [ButtonNames.OK] });
108
+ return;
109
+ }
110
+ const pdfFiles = await collectPdfFilesToMerge();
111
+ if (!pdfFiles)
112
+ return;
113
+ const finalName = pdfFileName.trim().toLowerCase().endsWith('.pdf')
114
+ ? pdfFileName.trim()
115
+ : pdfFileName.trim() + '.pdf';
116
+ try {
117
+ await mergePdfManager.mergeAndDownload(pdfFiles, finalName);
118
+ }
119
+ catch (err) {
120
+ console.error('Errore durante l\'unione dei PDF:', err);
121
+ TMMessageBoxManager.show({ message: 'Errore durante l\'unione dei PDF.', buttons: [ButtonNames.OK] });
122
+ return;
123
+ }
124
+ onClose();
125
+ };
126
+ // ---- Render della sezione configurazione (solo PDF file name + warning) ----
127
+ const configSection = (_jsxs("div", { style: {
128
+ position: 'relative',
129
+ border: `1px solid ${TMColors.border_normal}`,
130
+ borderRadius: '8px',
131
+ padding: '8px',
132
+ height: showTMRelationViewer ? '100%' : undefined,
133
+ flex: showTMRelationViewer ? undefined : 1,
134
+ minHeight: 0,
135
+ marginTop: showTMRelationViewer ? '10px' : undefined,
136
+ display: 'flex',
137
+ flexDirection: 'column',
138
+ boxSizing: 'border-box'
139
+ }, children: [_jsx("span", { style: getFloatingLabelStyle(), title: "Parametri di configurazione (Unisci in PDF)", children: "Parametri di configurazione" }), _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: '12px', overflowY: 'auto', flex: 1 }, children: [_jsx(TMTextBox, { label: "Nome file PDF", value: pdfFileName, validationItems: pdfValidationItems, autoComplete: "one-time-code", placeHolder: "merged.pdf", onValueChanged: (e) => setPdfFileName(e.target.value) }), (!hasEnoughPdfForMerge || hasNonPdfSelected) && (_jsxs("div", { style: {
140
+ display: 'flex',
141
+ flexDirection: 'column',
142
+ gap: '6px',
143
+ padding: '8px 12px',
144
+ border: '1px solid #f0c36d',
145
+ borderRadius: '5px',
146
+ backgroundColor: '#fdf5e2',
147
+ color: '#7a5d00',
148
+ fontSize: '0.85rem'
149
+ }, children: [_jsx("strong", { children: "Attenzione" }), _jsxs("ul", { style: { margin: 0, paddingLeft: '18px', display: 'flex', flexDirection: 'column', gap: '4px' }, children: [!hasEnoughPdfForMerge && (_jsx("li", { children: pdfSelectedCount === 0
150
+ ? `Nessun PDF tra i selezionati: ne servono almeno ${MIN_PDF_FOR_MERGE}.`
151
+ : `Solo 1 PDF tra i selezionati: ne servono almeno ${MIN_PDF_FOR_MERGE}.` })), hasNonPdfSelected && (_jsx("li", { children: (() => {
152
+ const extSet = Array.from(new Set(nonPdfSelectedItems.map(i => (i.ext ?? '').toString().trim().toLowerCase().replace(/^\./, '')).filter(e => e.length > 0)));
153
+ const extLabel = extSet.length > 0
154
+ ? ` (${extSet.map(e => '.' + e).join(', ')})`
155
+ : '';
156
+ return nonPdfSelectedItems.length === 1
157
+ ? `Un documento non è in formato PDF${extLabel} e sarà escluso dall'unione.`
158
+ : `${nonPdfSelectedItems.length} documenti non sono in formato PDF${extLabel} e saranno esclusi dall'unione.`;
159
+ })() }))] })] }))] })] }));
160
+ return (_jsx(TMModal, { width: calcResponsiveSizes(deviceType, showTMRelationViewer ? '90%' : '480px', showTMRelationViewer ? '90%' : '480px', '95%'), height: calcResponsiveSizes(deviceType, showTMRelationViewer ? '90%' : 'auto', showTMRelationViewer ? '90%' : 'auto', showTMRelationViewer ? '95%' : 'auto'), title: getTitle(), onClose: onClose, showCloseButton: true, children: _jsx(TMLayoutWaitingContainer, { direction: 'vertical', showWaitPanel: showWaitPanel, showWaitPanelPrimary: showPrimary, showWaitPanelSecondary: showSecondary, waitPanelTitle: waitPanelTitle, waitPanelTextPrimary: waitPanelTextPrimary, waitPanelValuePrimary: waitPanelValuePrimary, waitPanelMaxValuePrimary: waitPanelMaxValuePrimary, waitPanelTextSecondary: waitPanelTextSecondary, waitPanelValueSecondary: waitPanelValueSecondary, waitPanelMaxValueSecondary: waitPanelMaxValueSecondary, isCancelable: true, abortController: abortController, children: _jsxs("div", { onContextMenu: (e) => e.preventDefault(), style: { display: 'flex', flexDirection: 'column', gap: '16px', padding: '16px', width: '100%', height: '100%', boxSizing: 'border-box', overflow: 'hidden' }, children: [showTMRelationViewer ? (_jsx("div", { style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }, children: _jsxs(TMSplitterLayout, { direction: 'vertical', showSeparator: true, separatorSize: 8, separatorColor: 'transparent', separatorActiveColor: 'transparent', overflow: 'visible', min: SPLITTER_MIN, start: SPLITTER_START_65_35, children: [_jsx(TMDownloadRelationViewerSection, { selectedDcmtInfos: selectedDcmtInfos, onSelectionChanged: setSelectedItemsRelationViewer }), configSection] }, "TMMergeToPdf-relation-config") })) : (configSection), _jsx("div", { style: { display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '12px', flexShrink: 0 }, children: _jsx(TMButton, { caption: pdfSelectedCount === 0 ? 'Unisci in PDF' : `Unisci ${pdfSelectedCount} file in PDF`, icon: _jsx(IconPlay, {}), color: 'success', showTooltip: false, disabled: !isFormValid()
161
+ || (showTMRelationViewer && selectedItemsRelationViewer.filter(i => i.isDcmt).length === 0)
162
+ || !hasEnoughPdfForMerge, onClick: run }) })] }) }) }));
163
+ };
164
+ export default TMMergeToPdfForm;
@@ -50,6 +50,8 @@ export interface TMRelationViewerProps {
50
50
  onFocusedItemChanged?: (item: RelationTreeItem | null) => void;
51
51
  /** Callback when selected items change */
52
52
  onSelectedItemsChanged?: (items: RelationTreeItem[]) => void;
53
+ /** Callback fired when the internal tree data changes, providing a flattened list of all items currently rendered */
54
+ onAllItemsChanged?: (items: RelationTreeItem[]) => void;
53
55
  /** Callback when a document is double-clicked */
54
56
  onDocumentDoubleClick?: (tid: number, did: number, name?: string) => void;
55
57
  /** Custom item renderer (full control). Return undefined to use default renderer. */
@@ -109,6 +111,17 @@ export interface TMRelationViewerProps {
109
111
  * (used in master-detail context)
110
112
  */
111
113
  focusedItemFormData?: MetadataValueDescriptorEx[];
114
+ /**
115
+ * If true, shows a toolbar button to expand/collapse all tree nodes.
116
+ * Default: false
117
+ */
118
+ showExpandAllButton?: boolean;
119
+ /**
120
+ * If true, all tree nodes are expanded by default on initial load.
121
+ * If false (default), uses the standard initial expansion behavior
122
+ * (root container + first document + first correlation folder).
123
+ */
124
+ defaultExpandAll?: boolean;
112
125
  }
113
126
  /**
114
127
  * Check if document type has detail relations
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
3
3
  import { DcmtTypeListCacheService, SDK_Globals, DataColumnTypes, MetadataFormats, SystemMIDs, MetadataDataDomains, RelationCacheService, RelationTypes, UserListCacheService } from "@topconsultnpm/sdk-ts";
4
- import { genUniqueId, IconFolder, IconBackhandIndexPointingRight, IconCircleInfo, getDcmtCicoStatus } from '../../../helper';
4
+ import { genUniqueId, IconFolder, IconBackhandIndexPointingRight, IconCircleInfo, getDcmtCicoStatus, IconChevronDown, IconChevronRight } from '../../../helper';
5
5
  import { TMColors } from '../../../utils/theme';
6
- import { StyledDivHorizontal, StyledBadge } from '../../base/Styled';
6
+ import { StyledDivHorizontal, StyledBadge, StyledToolbarForm } from '../../base/Styled';
7
7
  import TMTreeView from '../../base/TMTreeView';
8
8
  import { TMWaitPanel } from '../../base/TMWaitPanel';
9
9
  import TMDataListItemViewer from '../../viewers/TMDataListItemViewer';
@@ -136,12 +136,14 @@ export const searchResultToDataSource = async (searchResult, hideSysMetadata) =>
136
136
  }
137
137
  return output;
138
138
  };
139
- const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndicator = true, allowShowZeroDcmts = true, initialShowZeroDcmts = false, allowedTIDs, allowMultipleSelection = false, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged, onDocumentDoubleClick, customItemRender, customDocumentStyle, customMainContainerContent, customDocumentContent, showMetadataNames = false, maxDepthLevel = 2, invertMasterNavigation = true, additionalStaticItems, showMainDocument = true, labelMainContainer, onNoRelationsFound, onItemContextMenu, focusedItemFormData = [] }) => {
139
+ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndicator = true, allowShowZeroDcmts = true, initialShowZeroDcmts = false, allowedTIDs, allowMultipleSelection = false, focusedItem, selectedItems, onFocusedItemChanged, onSelectedItemsChanged, onAllItemsChanged, onDocumentDoubleClick, customItemRender, customDocumentStyle, customMainContainerContent, customDocumentContent, showMetadataNames = false, maxDepthLevel = 2, invertMasterNavigation = true, additionalStaticItems, showMainDocument = true, labelMainContainer, onNoRelationsFound, onItemContextMenu, focusedItemFormData = [], showExpandAllButton = false, defaultExpandAll = false }) => {
140
140
  // State
141
141
  const [dcmtTypes, setDcmtTypes] = useState([]);
142
142
  const [treeData, setTreeData] = useState([]);
143
143
  const [showZeroDcmts, setShowZeroDcmts] = useState(initialShowZeroDcmts);
144
144
  const [staticItemsState, setStaticItemsState] = useState([]);
145
+ const [allExpanded, setAllExpanded] = useState(defaultExpandAll);
146
+ const initialExpandAllAppliedRef = React.useRef(false);
145
147
  const [showWaitPanel, setShowWaitPanel] = useState(false);
146
148
  const [waitPanelTextPrimary, setWaitPanelTextPrimary] = useState('');
147
149
  const [waitPanelValuePrimary, setWaitPanelValuePrimary] = useState(0);
@@ -271,7 +273,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
271
273
  console.error('❌ Error loading detail documents:', error);
272
274
  }
273
275
  return items;
274
- }, [allowedTIDs, focusedItemFormData, focusedItem?.dtd?.id]);
276
+ }, [allowedTIDs]);
275
277
  /**
276
278
  * Recursively retrieve master documents
277
279
  */
@@ -734,6 +736,24 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
734
736
  // and may contain custom properties that would be lost by the spread operator
735
737
  return [...treeData, ...itemsToMerge];
736
738
  }, [treeData, additionalStaticItems, staticItemsState]);
739
+ /**
740
+ * Notify parent with a flattened list of all items whenever the merged tree data changes.
741
+ */
742
+ useEffect(() => {
743
+ if (!onAllItemsChanged)
744
+ return;
745
+ const flatten = (items) => {
746
+ const out = [];
747
+ for (const it of items) {
748
+ out.push(it);
749
+ if (it.items && Array.isArray(it.items)) {
750
+ out.push(...flatten(it.items));
751
+ }
752
+ }
753
+ return out;
754
+ };
755
+ onAllItemsChanged(flatten(mergedTreeData));
756
+ }, [mergedTreeData, onAllItemsChanged]);
737
757
  /**
738
758
  * Load data when inputs change
739
759
  */
@@ -1118,7 +1138,7 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1118
1138
  }
1119
1139
  }
1120
1140
  return (_jsxs("div", { onDoubleClick: handleDoubleClick, style: documentStyle, children: [item.did && item.tid && showCurrentDcmtIndicator && inputDcmts?.some(d => d.DID === item.did && d.TID === item.tid) ? _jsx(IconBackhandIndexPointingRight, { fontSize: 22, overflow: 'visible' }) : _jsx(_Fragment, {}), item.values && (_jsx(TMDcmtIcon, { tid: item.values?.TID?.value, did: item.values?.DID?.value, fileExtension: item.values?.FILEEXT?.value, fileCount: item.values?.FILECOUNT?.value, isLexProt: item.values?.IsLexProt?.value, isMail: item.values?.ISMAIL?.value, isShared: item.values?.ISSHARED?.value, isSigned: item.values?.ISSIGNED?.value, downloadMode: 'openInNewWindow' })), checkoutStatusIcon, metadataContent] }));
1121
- }, [onDocumentDoubleClick, showCurrentDcmtIndicator, inputDcmts, customMainContainerContent, customDocumentStyle, customDocumentContent, showMetadataNames, showMainDocument, focusedItemFormData, focusedItem?.dtd, allUsers]);
1141
+ }, [onDocumentDoubleClick, showCurrentDcmtIndicator, inputDcmts, customMainContainerContent, customDocumentStyle, customDocumentContent, showMetadataNames, showMainDocument, allUsers]);
1122
1142
  /**
1123
1143
  * Wrapper renderer that handles custom rendering if provided
1124
1144
  */
@@ -1148,6 +1168,42 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1148
1168
  const handleSelectedItemsChanged = useCallback((items) => {
1149
1169
  onSelectedItemsChanged?.(items);
1150
1170
  }, [onSelectedItemsChanged]);
1171
+ /**
1172
+ * Recursively set the `expanded` property on every node that has children loaded.
1173
+ * Nodes without `items` are left untouched (will be loaded lazily on user click).
1174
+ */
1175
+ const setExpandedRecursively = useCallback((nodes, expanded) => {
1176
+ if (!nodes || !Array.isArray(nodes))
1177
+ return [];
1178
+ return nodes.map(node => {
1179
+ const hasChildren = Array.isArray(node.items) && node.items.length > 0;
1180
+ const newItems = hasChildren ? setExpandedRecursively(node.items, expanded) : node.items;
1181
+ // Only toggle expansion if the node is expandable (container/doc with children or marked expandible)
1182
+ const canExpand = hasChildren || node.isExpandible;
1183
+ return { ...node, expanded: canExpand ? expanded : node.expanded, items: newItems };
1184
+ });
1185
+ }, []);
1186
+ const handleToggleExpandAll = useCallback(() => {
1187
+ const next = !allExpanded;
1188
+ setAllExpanded(next);
1189
+ setTreeData(prev => setExpandedRecursively(prev, next));
1190
+ setStaticItemsState(prev => setExpandedRecursively(prev, next));
1191
+ userInteractedWithStaticItemsRef.current = true;
1192
+ }, [allExpanded, setExpandedRecursively]);
1193
+ /**
1194
+ * Apply defaultExpandAll once on initial tree load.
1195
+ */
1196
+ useEffect(() => {
1197
+ if (!defaultExpandAll)
1198
+ return;
1199
+ if (initialExpandAllAppliedRef.current)
1200
+ return;
1201
+ if (!treeData || treeData.length === 0)
1202
+ return;
1203
+ initialExpandAllAppliedRef.current = true;
1204
+ setTreeData(prev => setExpandedRecursively(prev, true));
1205
+ setStaticItemsState(prev => setExpandedRecursively(prev, true));
1206
+ }, [defaultExpandAll, treeData, setExpandedRecursively]);
1151
1207
  /**
1152
1208
  * Handle data changed - separate static items from dynamic tree data
1153
1209
  */
@@ -1169,7 +1225,20 @@ const TMRelationViewer = ({ inputDcmts, isForMaster = false, showCurrentDcmtIndi
1169
1225
  return null;
1170
1226
  return _jsx("div", { style: { padding: '20px', textAlign: 'center', color: '#666' }, children: "Nessuna relazione disponibile." });
1171
1227
  }
1172
- return (_jsxs(_Fragment, { children: [_jsx(TMTreeView, { dataSource: mergedTreeData, itemRender: finalItemRender, calculateItemsForNode: calculateItemsForNode, onDataChanged: handleDataChanged, focusedItem: focusedItem, onFocusedItemChanged: handleFocusedItemChanged, allowMultipleSelection: allowMultipleSelection, selectedItems: selectedItems, itemsPerPage: 100, onSelectionChanged: handleSelectedItemsChanged, onItemContextMenu: onItemContextMenu }), showExpansionWaitPanel && (_jsx(TMWaitPanel, { title: isForMaster ? 'Caricamento documenti master' : 'Caricamento documenti dettaglio', showPrimary: true, textPrimary: expansionWaitPanelText, valuePrimary: expansionWaitPanelValue, maxValuePrimary: expansionWaitPanelMaxValue, isCancelable: true, abortController: expansionAbortController, onAbortClick: (abortController) => {
1228
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0, width: '100%' }, children: [showExpandAllButton && (_jsx(StyledToolbarForm, { style: { flexShrink: 0 }, children: _jsxs("button", { type: "button", onClick: handleToggleExpandAll, title: allExpanded ? 'Comprimi tutto' : 'Espandi tutto', style: {
1229
+ display: 'inline-flex',
1230
+ alignItems: 'center',
1231
+ gap: '4px',
1232
+ padding: '4px 8px',
1233
+ fontSize: '0.8rem',
1234
+ color: TMColors.primary,
1235
+ background: 'transparent',
1236
+ border: `1px solid ${TMColors.border_normal}`,
1237
+ borderRadius: '4px',
1238
+ cursor: 'pointer',
1239
+ lineHeight: 1,
1240
+ whiteSpace: 'nowrap'
1241
+ }, children: [allExpanded ? _jsx(IconChevronRight, { fontSize: 14 }) : _jsx(IconChevronDown, { fontSize: 14 }), _jsx("span", { children: allExpanded ? 'Comprimi tutto' : 'Espandi tutto' })] }) })), _jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'auto' }, children: _jsx(TMTreeView, { dataSource: mergedTreeData, itemRender: finalItemRender, calculateItemsForNode: calculateItemsForNode, onDataChanged: handleDataChanged, focusedItem: focusedItem, onFocusedItemChanged: handleFocusedItemChanged, allowMultipleSelection: allowMultipleSelection, selectedItems: selectedItems, itemsPerPage: 100, onSelectionChanged: handleSelectedItemsChanged, onItemContextMenu: onItemContextMenu }) }), showExpansionWaitPanel && (_jsx(TMWaitPanel, { title: isForMaster ? 'Caricamento documenti master' : 'Caricamento documenti dettaglio', showPrimary: true, textPrimary: expansionWaitPanelText, valuePrimary: expansionWaitPanelValue, maxValuePrimary: expansionWaitPanelMaxValue, isCancelable: true, abortController: expansionAbortController, onAbortClick: (abortController) => {
1173
1242
  setTimeout(() => {
1174
1243
  abortController?.abort();
1175
1244
  }, 100);
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import { DcmtInfo } from '../../../ts';
3
+ import { DocumentDownloadSettings, FileNamingMode } from '../../../helper';
4
+ import { IRelatedDcmt } from './TMMasterDetailDcmts';
5
+ export declare const SPLITTER_MIN: string[];
6
+ export declare const SPLITTER_START_HALF: string[];
7
+ export declare const SPLITTER_START_65_35: string[];
8
+ /** Numero minimo di file PDF necessari per poterli unire in un unico documento. */
9
+ export declare const MIN_PDF_FOR_MERGE = 2;
10
+ /** Stile per le etichette "floating" posizionate sopra i bordi dei container */
11
+ export declare const getFloatingLabelStyle: () => React.CSSProperties;
12
+ /** Chiave univoca per un IRelatedDcmt basata sulla coppia tid+did */
13
+ export declare const getDcmtKey: (item: IRelatedDcmt) => string;
14
+ /** Deduplica gli IRelatedDcmt in base alla coppia tid+did (mantiene la prima occorrenza) */
15
+ export declare const dedupeByTidDid: (items: ReadonlyArray<IRelatedDcmt>) => Array<IRelatedDcmt>;
16
+ /** Verifica se showDirectoryPicker è supportato (Chrome, Edge) */
17
+ export declare const isDirectoryPickerSupported: () => boolean;
18
+ /**
19
+ * Verifica se un'estensione (con o senza punto iniziale, case-insensitive) è "pdf".
20
+ */
21
+ export declare const isPdfExt: (ext: string | null | undefined) => boolean;
22
+ /** Sanitizza il nome file per Windows (rimuove caratteri illegali e limita la lunghezza) */
23
+ export declare const sanitizeFileName: (fileName: string, fallbackName: string, maxLength?: number) => string;
24
+ /** Verifica se un file esiste nella cartella (FileSystemDirectoryHandle) */
25
+ export declare const fileExists: (dirHandle: FileSystemDirectoryHandle, fileName: string) => Promise<boolean>;
26
+ /** Genera un nome file univoco aggiungendo un suffisso numerico se già esistente */
27
+ export declare const generateUniqueFileName: (dirHandle: FileSystemDirectoryHandle, originalName: string) => Promise<string>;
28
+ /** Recupera il nome leggibile del tipo documento (cache) */
29
+ export declare const getTypeName: (tid: number | undefined) => Promise<string>;
30
+ /** Formatta un valore convertendo le date in formato dd-MM-yyyy [HH-mm-ss] */
31
+ export declare const formatMetadataValue: (value: string) => string;
32
+ /** Recupera i metadati filtrati (primi 5 con mid > 100 e valore presente), concatenati con separatorChar */
33
+ export declare const getFilteredMetadata: (tid: number, did: number, separatorChar: string) => Promise<string | null>;
34
+ /** Opzioni di naming necessarie per generare il nome del file di destinazione */
35
+ export interface IFileNamingOptions {
36
+ fileNamingMode: FileNamingMode;
37
+ separatorChar: string;
38
+ }
39
+ /** Genera il nome file di destinazione in base alle impostazioni di naming */
40
+ export declare const generateTargetFileName: (file: File, dcmtInfo: DcmtInfo, options: IFileNamingOptions) => Promise<string>;
41
+ /**
42
+ * Restituisce i documenti da scaricare:
43
+ * - se è visibile il TMRelationViewer, usa gli isDcmt selezionati al suo interno
44
+ * - altrimenti restituisce i selectedDcmtInfos passati come prop
45
+ */
46
+ export declare const getDcmtInfosToDownload: (selectedDcmtInfos: Array<DcmtInfo>, selectedItemsRelationViewer: Array<IRelatedDcmt>, showTMRelationViewer: boolean) => Array<DcmtInfo>;
47
+ /**
48
+ * Costruisce lo stato iniziale di DocumentDownloadSettings combinando:
49
+ * - i valori salvati in userSettings (se presenti)
50
+ * - i default della classe
51
+ * Nota: destinationFolder e zipPassword vengono sempre resettati per sicurezza.
52
+ */
53
+ export declare const buildInitialDownloadSettings: (saved: DocumentDownloadSettings | undefined, defaultInvoiceFormat: DocumentDownloadSettings["invoiceFormat"], defaultOrderFormat: DocumentDownloadSettings["orderFormat"]) => DocumentDownloadSettings;