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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +1 -1
  2. package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +1 -1
  3. package/lib/components/base/TMButton.d.ts +13 -0
  4. package/lib/components/base/TMButton.js +137 -5
  5. package/lib/components/base/TMDataGrid.d.ts +2 -0
  6. package/lib/components/base/TMDataGrid.js +68 -4
  7. package/lib/components/base/TMEditorBase.d.ts +2 -0
  8. package/lib/components/base/TMModal.d.ts +4 -0
  9. package/lib/components/base/TMModal.js +42 -11
  10. package/lib/components/base/TMPanel.d.ts +1 -0
  11. package/lib/components/base/TMPanel.js +79 -20
  12. package/lib/components/base/TMTooltip.d.ts +2 -1
  13. package/lib/components/base/TMTooltip.js +23 -6
  14. package/lib/components/base/TMTreeView.js +108 -67
  15. package/lib/components/choosers/TMDcmtTypeChooser.js +4 -3
  16. package/lib/components/choosers/TMDistinctValues.d.ts +9 -7
  17. package/lib/components/choosers/TMDistinctValues.js +149 -142
  18. package/lib/components/choosers/TMMetadataChooser.js +1 -1
  19. package/lib/components/choosers/TMQuickSearchInfo.d.ts +12 -0
  20. package/lib/components/choosers/TMQuickSearchInfo.js +279 -0
  21. package/lib/components/choosers/TMQuickSearchSettingsForm.d.ts +16 -0
  22. package/lib/components/choosers/TMQuickSearchSettingsForm.js +94 -0
  23. package/lib/components/choosers/TMSelectedValuesSummary.d.ts +13 -0
  24. package/lib/components/choosers/TMSelectedValuesSummary.js +91 -0
  25. package/lib/components/editors/TMDropDown.d.ts +3 -0
  26. package/lib/components/editors/TMDropDown.js +197 -5
  27. package/lib/components/editors/TMMetadataEditor.d.ts +5 -1
  28. package/lib/components/editors/TMMetadataEditor.js +8 -3
  29. package/lib/components/editors/TMTextBox.js +15 -15
  30. package/lib/components/features/documents/TMBatchUpdateForm.js +19 -5
  31. package/lib/components/features/documents/TMDcmtForm.d.ts +7 -0
  32. package/lib/components/features/documents/TMDcmtForm.js +104 -42
  33. package/lib/components/features/documents/TMDcmtFormActionButtons.d.ts +11 -0
  34. package/lib/components/features/documents/TMDcmtFormActionButtons.js +74 -7
  35. package/lib/components/features/documents/TMMasterDetailDcmts.js +95 -33
  36. package/lib/components/features/documents/TMMasterInfoFields.d.ts +19 -0
  37. package/lib/components/features/documents/TMMasterInfoFields.js +172 -0
  38. package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.d.ts +40 -0
  39. package/lib/components/features/documents/TMMultiMasterDetailDcmtsForm.js +277 -0
  40. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.d.ts +354 -0
  41. package/lib/components/features/documents/TMMultiMasterDetailDcmtsUtils.js +234 -0
  42. package/lib/components/features/documents/TMRelationViewer.d.ts +14 -1
  43. package/lib/components/features/documents/TMRelationViewer.js +69 -20
  44. package/lib/components/features/search/TMSearch.js +29 -16
  45. package/lib/components/features/search/TMSearchQueryEditor.js +5 -3
  46. package/lib/components/features/search/TMSearchQueryPanel.d.ts +1 -1
  47. package/lib/components/features/search/TMSearchQueryPanel.js +28 -7
  48. package/lib/components/features/search/TMSearchResult.d.ts +9 -0
  49. package/lib/components/features/search/TMSearchResult.js +323 -92
  50. package/lib/components/features/search/TMTreeSelector.js +18 -5
  51. package/lib/components/features/workflow/TMWorkflowPopup.d.ts +10 -0
  52. package/lib/components/features/workflow/TMWorkflowPopup.js +8 -4
  53. package/lib/components/layout/panelManager/TMPanelManagerContainer.d.ts +1 -0
  54. package/lib/components/layout/panelManager/TMPanelManagerContainer.js +24 -14
  55. package/lib/components/layout/panelManager/TMPanelWrapper.js +2 -2
  56. package/lib/components/layout/panelManager/types.d.ts +3 -0
  57. package/lib/components/viewers/TMMidViewer.js +4 -1
  58. package/lib/components/viewers/TMTidViewer.js +25 -11
  59. package/lib/helper/SDKUI_Globals.d.ts +55 -0
  60. package/lib/helper/SDKUI_Globals.js +82 -0
  61. package/lib/helper/SDKUI_Localizator.d.ts +24 -0
  62. package/lib/helper/SDKUI_Localizator.js +240 -0
  63. package/lib/helper/TMIcons.d.ts +1 -0
  64. package/lib/helper/TMIcons.js +3 -0
  65. package/lib/helper/dataGridSearchHelper.d.ts +28 -0
  66. package/lib/helper/dataGridSearchHelper.js +51 -0
  67. package/lib/helper/dcmtsHelper.d.ts +20 -0
  68. package/lib/helper/dcmtsHelper.js +58 -1
  69. package/lib/helper/helpers.d.ts +20 -0
  70. package/lib/helper/helpers.js +40 -0
  71. package/lib/helper/index.d.ts +1 -0
  72. package/lib/helper/index.js +1 -0
  73. package/lib/helper/queryHelper.d.ts +2 -0
  74. package/lib/helper/queryHelper.js +3 -1
  75. package/lib/hooks/tmDistinctValuesGridHelper.d.ts +16 -0
  76. package/lib/hooks/tmDistinctValuesGridHelper.js +31 -0
  77. package/lib/hooks/useArchiveListForm.d.ts +39 -0
  78. package/lib/hooks/useArchiveListForm.js +46 -0
  79. package/lib/hooks/useCaseFlowApprove.d.ts +56 -0
  80. package/lib/hooks/useCaseFlowApprove.js +157 -0
  81. package/lib/hooks/useDataUserIdItem.d.ts +1 -0
  82. package/lib/hooks/useDataUserIdItem.js +1 -0
  83. package/lib/hooks/useDocumentOperations.d.ts +2 -0
  84. package/lib/hooks/useDocumentOperations.js +28 -5
  85. package/lib/hooks/useMultiMasterDetailDcmts.d.ts +98 -0
  86. package/lib/hooks/useMultiMasterDetailDcmts.js +567 -0
  87. package/lib/hooks/useRelatedDocuments.js +2 -26
  88. package/lib/hooks/useTMDistinctValuesMetadataDisplay.d.ts +18 -0
  89. package/lib/hooks/useTMDistinctValuesMetadataDisplay.js +103 -0
  90. package/lib/hooks/useTMDistinctValuesQuickSearch.d.ts +49 -0
  91. package/lib/hooks/useTMDistinctValuesQuickSearch.js +307 -0
  92. package/lib/hooks/useTMDistinctValuesSelection.d.ts +42 -0
  93. package/lib/hooks/useTMDistinctValuesSelection.js +103 -0
  94. package/lib/hooks/useTMDistinctValuesSource.d.ts +29 -0
  95. package/lib/hooks/useTMDistinctValuesSource.js +105 -0
  96. package/lib/index.d.ts +1 -0
  97. package/lib/index.js +1 -0
  98. package/lib/services/caseflow/CaseFlowService.d.ts +236 -0
  99. package/lib/services/caseflow/CaseFlowService.js +693 -0
  100. package/lib/services/caseflow/CaseFlowTemplateCacheService.d.ts +28 -0
  101. package/lib/services/caseflow/CaseFlowTemplateCacheService.js +121 -0
  102. package/lib/services/caseflow/caseFlowNameCache.d.ts +32 -0
  103. package/lib/services/caseflow/caseFlowNameCache.js +116 -0
  104. package/lib/services/caseflow/caseFlowWorkItemUtils.d.ts +61 -0
  105. package/lib/services/caseflow/caseFlowWorkItemUtils.js +113 -0
  106. package/lib/services/index.d.ts +4 -0
  107. package/lib/services/index.js +4 -0
  108. package/lib/services/platform_services.d.ts +1 -1
  109. package/package.json +66 -66
@@ -0,0 +1,693 @@
1
+ import { SDK_Globals, BlogPost, CaseFlowDcmt, CaseFlowInstanceDescriptor, CaseFlowParticipant, CaseFlowWIOperationDescriptor, MetadataValueDescriptor, CF_ParticipantRoles, CF_States, CF_WIOperations, CF_WorkItemCategories, CF_WorkItemStates, UserListCacheService, DcmtTypeListCacheService, } from '@topconsultnpm/sdk-ts';
2
+ import { CaseFlowTemplateCacheService } from './CaseFlowTemplateCacheService';
3
+ import { getTemplateNameSync, primeTemplateName, primeTemplateNames, primeActionNames } from './caseFlowNameCache';
4
+ export class CaseFlowService {
5
+ // ----- Template / tipi documento (invariati: cache condivise) ------------
6
+ /** Elenco dei template reali (via CaseFlowTemplateCacheService). */
7
+ static async getTemplates() {
8
+ const templates = await CaseFlowTemplateCacheService.GetAllAsync();
9
+ // Popola la cache dei nomi template, così i componenti possono risolvere
10
+ // `templateName` dal `cftid` senza augmentare i descrittori del SDK.
11
+ primeTemplateNames(templates);
12
+ return templates;
13
+ }
14
+ /**
15
+ * Singolo template per id. Usa GetAsync (non un find su GetAllAsync): così i
16
+ * template di cui l'utente NON può creare istanze — ma di cui è destinatario
17
+ * di un workitem (es. Completamento dati) — vengono risolti on-demand via
18
+ * RetrieveAsync mirata invece di restituire undefined.
19
+ */
20
+ static async getTemplate(id) {
21
+ const template = await CaseFlowTemplateCacheService.GetAsync(id);
22
+ if (template)
23
+ primeTemplateNames([template]);
24
+ return template;
25
+ }
26
+ /** Invalida la cache dei template (dopo creazione/modifica/eliminazione). */
27
+ static invalidateTemplatesCache() {
28
+ CaseFlowTemplateCacheService.Invalidate();
29
+ }
30
+ /** DcmtTypeDescriptor (copertina) per il TID indicato. */
31
+ static async getDcmtTypeAsync(tid) {
32
+ try {
33
+ return await DcmtTypeListCacheService.GetAsync(tid, true);
34
+ }
35
+ catch (e) {
36
+ console.warn(`[CaseFlow] DcmtType ${tid} non disponibile:`, e);
37
+ return undefined;
38
+ }
39
+ }
40
+ // ----- Lettura istanze ---------------------------------------------------
41
+ /** Tutte le istanze visibili all'utente (unione di tutti i template). */
42
+ static async getCaseflows() {
43
+ return this.loadAllInstances();
44
+ }
45
+ /**
46
+ * Singola istanza COMPLETA per id. `RetrieveAllAsync` (usata dalla dashboard)
47
+ * porta i dati necessari alle card, ma per il dettaglio completo
48
+ * (documenti/bacheca/timeline) serve comunque una `RetrieveAsync(cftid, id)`
49
+ * che idrata l'istanza. Il `cftid` va passato quando noto (evita una
50
+ * scansione); in mancanza viene risolto dalla lista.
51
+ *
52
+ * ATTENZIONE sui `workItems` della RetrieveAll: contengono i soli workitem
53
+ * ANCORA APERTI, non lo storico. Una pratica conclusa arriva con
54
+ * `workItems: []`, perché non ha più nulla di pendente. Chi deve ricostruire
55
+ * la STORIA di una pratica (chi ha fatto cosa e quando) non può quindi basarsi
56
+ * sull'elenco: serve la Retrieve completa, e la fonte dei passaggi già
57
+ * avvenuti sono i messaggi di sistema della bacheca (`blogPosts` con
58
+ * allegato di riferimento all'azione).
59
+ */
60
+ static async getCaseflow(id, cftid) {
61
+ try {
62
+ return await this.retrieveCaseflow(id, cftid);
63
+ }
64
+ catch (e) {
65
+ console.warn(`[CaseFlow] RetrieveAsync istanza id=${id} (cftid=${cftid}) fallita:`, e);
66
+ return undefined;
67
+ }
68
+ }
69
+ /**
70
+ * Come {@link getCaseflow} ma PROPAGA l'errore invece di inghiottirlo.
71
+ * Da usare nei punti in cui l'utente apre esplicitamente l'istanza (apertura
72
+ * scheda, deep link): il fallimento della Retrieve deve essere notificato,
73
+ * non mascherato da una vista incompleta ("non sei un partecipante").
74
+ */
75
+ static async retrieveCaseflow(id, cftid) {
76
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
77
+ if (!engine)
78
+ return undefined;
79
+ let resolvedCftid = cftid;
80
+ if (resolvedCftid == null) {
81
+ const all = await this.loadAllInstances();
82
+ resolvedCftid = all.find(cf => cf.id === id)?.cftid;
83
+ }
84
+ if (resolvedCftid == null) {
85
+ console.warn(`[CaseFlow] impossibile risolvere il cftid per l'istanza id=${id}.`);
86
+ return undefined;
87
+ }
88
+ const full = await engine.RetrieveAsync(resolvedCftid, id);
89
+ if (!full)
90
+ return undefined;
91
+ if (full.cftid == null)
92
+ full.cftid = resolvedCftid;
93
+ // Risolvi il template (best-effort, on-demand se non creabile dall'utente)
94
+ // per l'etichetta del modello e i nomi azione.
95
+ const tpl = await CaseFlowTemplateCacheService.GetAsync(full.cftid);
96
+ if (tpl) {
97
+ primeTemplateName(tpl.id, tpl.name);
98
+ primeActionNames(tpl);
99
+ }
100
+ await this.resolveUserNames([full]);
101
+ return full;
102
+ }
103
+ /** Istanze di cui l'utente indicato è proprietario. */
104
+ static async getMyCaseflows(userId) {
105
+ const all = await this.loadAllInstances();
106
+ return all.filter(cf => cf.ownerID === userId);
107
+ }
108
+ /** Ricerca avanzata sulle istanze applicando i filtri indicati. */
109
+ static async searchCaseflows(criteria) {
110
+ const all = await this.loadAllInstances();
111
+ const lowerIncludes = (haystack, needle) => (haystack ?? '').toLowerCase().includes(needle.toLowerCase());
112
+ return all.filter(cf => {
113
+ if (criteria.templateId != null && cf.cftid !== criteria.templateId)
114
+ return false;
115
+ if (criteria.name && !lowerIncludes(cf.name, criteria.name))
116
+ return false;
117
+ if (criteria.description && !lowerIncludes(cf.description, criteria.description))
118
+ return false;
119
+ if (criteria.state && cf.state !== criteria.state)
120
+ return false;
121
+ if (criteria.dateFrom) {
122
+ const from = new Date(criteria.dateFrom);
123
+ if (cf.creationTime && cf.creationTime < from)
124
+ return false;
125
+ }
126
+ if (criteria.dateTo) {
127
+ const to = new Date(criteria.dateTo);
128
+ if (cf.creationTime && cf.creationTime > to)
129
+ return false;
130
+ }
131
+ if (criteria.metadataFilters && criteria.metadataFilters.length > 0) {
132
+ for (const mf of criteria.metadataFilters) {
133
+ if (!mf.value.trim())
134
+ continue;
135
+ const cfMeta = (cf.cover ?? []).find(m => m.mid === mf.mid);
136
+ if (!cfMeta || !lowerIncludes(cfMeta.value, mf.value))
137
+ return false;
138
+ }
139
+ }
140
+ if (criteria.freeTextMetadata && criteria.freeTextMetadata.trim()) {
141
+ const needle = criteria.freeTextMetadata.trim();
142
+ const matchesCommon = lowerIncludes(cf.name, needle)
143
+ || lowerIncludes(cf.description, needle)
144
+ || lowerIncludes(cf.ownerName, needle)
145
+ || lowerIncludes(getTemplateNameSync(cf.cftid), needle);
146
+ const matchesMeta = (cf.cover ?? []).some(m => lowerIncludes(m.value, needle) || lowerIncludes(m.metadataName, needle));
147
+ if (!matchesCommon && !matchesMeta)
148
+ return false;
149
+ }
150
+ return true;
151
+ });
152
+ }
153
+ /** WorkItem pendenti assegnati all'utente, raggruppati per istanza. */
154
+ static async getMyPendingWorkItems(userId) {
155
+ return this.collectWorkItemsByState(userId, CF_WorkItemStates.Pending);
156
+ }
157
+ /**
158
+ * Richieste di "maggiori informazioni" INVIATE dall'utente e ancora in
159
+ * attesa di risposta, raggruppate per istanza.
160
+ *
161
+ * Non basta filtrare `toUserID === userId && state === MoreInfo` (il solo
162
+ * workitem "padre" bloccato intestato a me): a seconda di come il backend
163
+ * materializza la richiesta, l'unico workitem visibile può essere quello
164
+ * INVIATO al destinatario (`fromUserID === userId`, categoria MoreInfo,
165
+ * ancora Pending). In quel caso il filtro su `toUserID` non lo intercetta e
166
+ * l'istanza sparisce da TUTTE le sezioni di "Per te" (non è più Pending per
167
+ * me, non è MoreInfo intestato a me, e non ne sono owner). Consideriamo
168
+ * quindi entrambe le rappresentazioni, preferendo il figlio inviato (che
169
+ * porta il destinatario) ed evitando di mostrare padre + figlio della
170
+ * stessa richiesta.
171
+ */
172
+ static async getMyMoreInfoRequests(userId) {
173
+ const all = await this.loadAllInstances();
174
+ const result = [];
175
+ for (const cf of all) {
176
+ const wis = cf.workItems ?? [];
177
+ // Richieste che HO inviato e attendono risposta da un altro utente.
178
+ const sent = wis.filter(wi => wi.fromUserID === userId
179
+ && wi.workItemCategory === CF_WorkItemCategories.MoreInfo
180
+ && wi.state === CF_WorkItemStates.Pending);
181
+ // Mia attività messa "in attesa" (workitem padre intestato a me).
182
+ const waitingParents = wis.filter(wi => wi.toUserID === userId && wi.state === CF_WorkItemStates.MoreInfo);
183
+ const parentsWithoutChild = waitingParents.filter(p => !sent.some(s => s.parentWorkItemID === p.id));
184
+ const workItems = [...sent, ...parentsWithoutChild];
185
+ if (workItems.length > 0)
186
+ result.push({ caseflow: cf, workItems });
187
+ }
188
+ return result;
189
+ }
190
+ static async collectWorkItemsByState(userId, state) {
191
+ const all = await this.loadAllInstances();
192
+ const result = [];
193
+ for (const cf of all) {
194
+ const workItems = (cf.workItems ?? []).filter(wi => wi.toUserID === userId && wi.state === state);
195
+ if (workItems.length > 0)
196
+ result.push({ caseflow: cf, workItems });
197
+ }
198
+ return result;
199
+ }
200
+ // ----- Creazione / eliminazione / aggiornamento istanza ------------------
201
+ /**
202
+ * Crea una nuova istanza a partire da un template tramite CreateAsync.
203
+ * I metadati di copertina (metadataValues: mid -> valore) vengono passati
204
+ * nel campo `cover`. Il backend materializza azioni/stati/partecipanti dal
205
+ * template identificato da `cftid`.
206
+ * @returns id della nuova istanza.
207
+ */
208
+ static async CreateCaseFlowInstance(template, instanceName, instanceDescription, metadataValues) {
209
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
210
+ if (!engine)
211
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
212
+ if (template.id == null)
213
+ throw new Error('Template privo di id (cftid) per la creazione dell\'istanza.');
214
+ const d = new CaseFlowInstanceDescriptor();
215
+ d.cftid = template.id;
216
+ d.name = instanceName;
217
+ d.description = instanceDescription ?? '';
218
+ d.state = CF_States.New;
219
+ d.progress = 0;
220
+ d.cover = Object.entries(metadataValues).map(([mid, value]) => MetadataValueDescriptor.fromJS({ mid: Number(mid), value }));
221
+ const id = await engine.CreateAsync(d);
222
+ return id ?? 0;
223
+ }
224
+ /**
225
+ * Elimina un'istanza tramite DeleteAsync(cftid, id). Il cftid è preso dal
226
+ * template fornito (se disponibile) oppure risolto dall'istanza.
227
+ */
228
+ static async DeleteCaseFlowInstance(instanceId, template) {
229
+ if (!instanceId)
230
+ throw new Error('instanceId non valido per la cancellazione.');
231
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
232
+ if (!engine)
233
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
234
+ const cftid = template?.id ?? (await this.getCaseflow(instanceId))?.cftid;
235
+ if (cftid == null)
236
+ throw new Error(`Impossibile risolvere il template (cftid) per l'istanza id=${instanceId}.`);
237
+ await engine.DeleteAsync(cftid, instanceId);
238
+ return { instanceDID: instanceId, complete: true, orphaned: [] };
239
+ }
240
+ /** Aggiorna un'istanza esistente tramite UpdateAsync. */
241
+ static async saveCaseflow(caseflow) {
242
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
243
+ if (!engine)
244
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
245
+ await engine.UpdateAsync(caseflow);
246
+ }
247
+ // ----- Bacheca (blog dell'istanza) ---------------------------------------
248
+ /**
249
+ * Aggiunge un post in bacheca tramite BlogPostAddAsync(cftid, id, bp). Il
250
+ * cftid è passato quando noto (evita una Retrieve) oppure risolto dall'istanza.
251
+ * @returns il post creato (transitorio: la ricarica leggerà quelli reali).
252
+ */
253
+ static async addBlogPost(caseFlowId, text, authorId, authorName, attachments, cftid) {
254
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
255
+ if (!engine)
256
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
257
+ const resolvedCftid = cftid ?? (await this.getCaseflow(caseFlowId))?.cftid;
258
+ if (resolvedCftid == null)
259
+ throw new Error(`Impossibile risolvere il template (cftid) per l'istanza id=${caseFlowId}.`);
260
+ const attachmentsJson = attachments?.map(a => a.toJSON()) ?? [];
261
+ const bp = BlogPost.fromJS({
262
+ ownerID: authorId,
263
+ ownerName: authorName,
264
+ description: text,
265
+ isDel: 0,
266
+ isSys: 0,
267
+ attachments: attachmentsJson,
268
+ });
269
+ const newId = await engine.BlogPostAddAsync(resolvedCftid, caseFlowId, bp);
270
+ // Post transitorio: rispecchia quanto inviato in attesa della ricarica.
271
+ return BlogPost.fromJS({
272
+ id: newId ?? 0,
273
+ ownerID: authorId,
274
+ ownerName: authorName,
275
+ description: text,
276
+ creationTime: new Date().toISOString(),
277
+ isDel: 0,
278
+ isSys: 0,
279
+ attachments: attachmentsJson,
280
+ });
281
+ }
282
+ /**
283
+ * Elimina (o ripristina) un post in bacheca tramite
284
+ * BlogPostDeleteOrUndeleteAsync(cftid, id, postID, del).
285
+ */
286
+ static async deleteOrUndeleteBlogPost(caseFlowId, postId, del, cftid) {
287
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
288
+ if (!engine)
289
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
290
+ const resolvedCftid = cftid ?? (await this.getCaseflow(caseFlowId))?.cftid;
291
+ if (resolvedCftid == null)
292
+ throw new Error(`Impossibile risolvere il template (cftid) per l'istanza id=${caseFlowId}.`);
293
+ await engine.BlogPostDeleteOrUndeleteAsync(resolvedCftid, caseFlowId, postId, del);
294
+ }
295
+ // ----- Documenti della pratica --------------------------------------------
296
+ //
297
+ // Come per i partecipanti, l'engine espone UNA sola API,
298
+ // DcmtAddOrRemoveAsync(cftid, id, tid, did, remove): con remove=false aggancia
299
+ // il documento all'istanza, con remove=true lo sgancia.
300
+ /**
301
+ * Aggancia un documento (nuovo archiviato o già presente in archivio)
302
+ * all'istanza tramite DcmtAddOrRemoveAsync(remove=false).
303
+ * @returns descrittore transitorio del documento agganciato (la ricarica
304
+ * dell'istanza leggerà quello reale).
305
+ */
306
+ static async addDcmtToInstance(caseFlowId, tid, did, cftid) {
307
+ await this.dcmtAddOrRemove(caseFlowId, tid, did, false, cftid);
308
+ return CaseFlowDcmt.fromJS({ tid, did, creationTime: new Date().toISOString() });
309
+ }
310
+ /** Sgancia un documento dall'istanza tramite DcmtAddOrRemoveAsync(remove=true). */
311
+ static async removeDcmtFromInstance(caseFlowId, tid, did, cftid) {
312
+ await this.dcmtAddOrRemove(caseFlowId, tid, did, true, cftid);
313
+ }
314
+ // ----- Regole documenti attesi dell'istanza (dcmtRules) -------------------
315
+ //
316
+ // Finché l'istanza non ha regole proprie (dcmtRules assente o vuota) i
317
+ // documenti attesi — con min e max — sono i dcmts del template. Un
318
+ // supervisor può personalizzarle per la SOLA istanza tramite l'unica API
319
+ // DcmtRuleAddOrRemoveAsync(cftid, id, tid, min, max, remove): con
320
+ // remove=false funge da UPSERT della regola per il TID, con remove=true la
321
+ // rimuove. Rimuovendo tutte le regole l'istanza torna a seguire il template.
322
+ /** Aggiunge o aggiorna (upsert) la regola documenti del TID sulla sola istanza. */
323
+ static async saveDcmtRule(caseFlowId, tid, min, max, cftid) {
324
+ await this.dcmtRuleAddOrRemove(caseFlowId, tid, min, max, false, cftid);
325
+ }
326
+ /** Rimuove la regola documenti del TID dalla sola istanza. */
327
+ static async removeDcmtRule(caseFlowId, tid, cftid) {
328
+ await this.dcmtRuleAddOrRemove(caseFlowId, tid, 0, 0, true, cftid);
329
+ }
330
+ /**
331
+ * Chiama DcmtRuleAddOrRemoveAsync(cftid, id, tid, min, max, remove)
332
+ * risolvendo il cftid quando non passato (come per bacheca e partecipanti).
333
+ */
334
+ static async dcmtRuleAddOrRemove(caseFlowId, tid, min, max, remove, cftid) {
335
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
336
+ if (!engine)
337
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
338
+ const resolvedCftid = cftid ?? (await this.getCaseflow(caseFlowId))?.cftid;
339
+ if (resolvedCftid == null)
340
+ throw new Error(`Impossibile risolvere il template (cftid) per l'istanza id=${caseFlowId}.`);
341
+ await engine.DcmtRuleAddOrRemoveAsync(resolvedCftid, caseFlowId, tid, min, max, remove);
342
+ }
343
+ /**
344
+ * Chiama DcmtAddOrRemoveAsync(cftid, id, tid, did, remove) risolvendo il
345
+ * cftid quando non passato (come per bacheca e partecipanti).
346
+ */
347
+ static async dcmtAddOrRemove(caseFlowId, tid, did, remove, cftid) {
348
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
349
+ if (!engine)
350
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
351
+ const resolvedCftid = cftid ?? (await this.getCaseflow(caseFlowId))?.cftid;
352
+ if (resolvedCftid == null)
353
+ throw new Error(`Impossibile risolvere il template (cftid) per l'istanza id=${caseFlowId}.`);
354
+ await engine.DcmtAddOrRemoveAsync(resolvedCftid, caseFlowId, tid, did, remove);
355
+ }
356
+ // ----- Partecipanti ------------------------------------------------------
357
+ //
358
+ // Per scelta del backend l'engine espone UNA sola API,
359
+ // ParticipantsAddOrRemoveAsync(cftid, id, d, remove): con remove=false funge
360
+ // da UPSERT (aggiunge il partecipante oppure ne aggiorna ruolo/permessi Guest
361
+ // se già presente), con remove=true lo rimuove. Non esiste — né esisterà — un
362
+ // metodo di Update dedicato: modifica ruolo e permessi passano dallo stesso
363
+ // upsert. Il partecipante identifica l'utente col campo `id` (ex `userId`).
364
+ /**
365
+ * Aggiunge un partecipante all'istanza (o ne aggiorna ruolo/permessi se già
366
+ * presente) tramite ParticipantsAddOrRemoveAsync(remove=false).
367
+ */
368
+ static async addParticipant(caseFlowId, userId, userName, role, guestPermissions, cftid) {
369
+ const participant = CaseFlowParticipant.fromJS({
370
+ id: userId,
371
+ name: userName,
372
+ role,
373
+ guestPermissions: guestPermissions?.toJSON(),
374
+ });
375
+ await this.participantsAddOrRemove(caseFlowId, participant, false, cftid);
376
+ return participant;
377
+ }
378
+ /** Rimuove un partecipante dall'istanza tramite ParticipantsAddOrRemoveAsync(remove=true). */
379
+ static async removeParticipant(caseFlowId, userId, cftid) {
380
+ const participant = CaseFlowParticipant.fromJS({ id: userId });
381
+ await this.participantsAddOrRemove(caseFlowId, participant, true, cftid);
382
+ }
383
+ /**
384
+ * Cambia il ruolo di un partecipante ri-applicando l'upsert col nuovo ruolo
385
+ * (non esiste un'Update dedicata). Passando a Guest si conservano/impostano i
386
+ * relativi permessi.
387
+ */
388
+ static async changeParticipantRole(caseFlowId, userId, role, guestPermissions, cftid) {
389
+ const participant = CaseFlowParticipant.fromJS({
390
+ id: userId,
391
+ role,
392
+ guestPermissions: role === CF_ParticipantRoles.Guest ? guestPermissions?.toJSON() : undefined,
393
+ });
394
+ await this.participantsAddOrRemove(caseFlowId, participant, false, cftid);
395
+ }
396
+ /** Aggiorna i permessi Guest di un partecipante (upsert col ruolo Guest). */
397
+ static async updateParticipantGuestPermissions(caseFlowId, userId, permissions, cftid) {
398
+ const participant = CaseFlowParticipant.fromJS({
399
+ id: userId,
400
+ role: CF_ParticipantRoles.Guest,
401
+ guestPermissions: permissions.toJSON(),
402
+ });
403
+ await this.participantsAddOrRemove(caseFlowId, participant, false, cftid);
404
+ }
405
+ /**
406
+ * Chiama ParticipantsAddOrRemoveAsync(cftid, id, d, remove) risolvendo il
407
+ * cftid quando non passato (come per la bacheca).
408
+ */
409
+ static async participantsAddOrRemove(caseFlowId, participant, remove, cftid) {
410
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
411
+ if (!engine)
412
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
413
+ const resolvedCftid = cftid ?? (await this.getCaseflow(caseFlowId))?.cftid;
414
+ if (resolvedCftid == null)
415
+ throw new Error(`Impossibile risolvere il template (cftid) per l'istanza id=${caseFlowId}.`);
416
+ await engine.ParticipantsAddOrRemoveAsync(resolvedCftid, caseFlowId, participant, remove);
417
+ }
418
+ // ----- Azioni personali dell'utente (Preferiti / Segui) -------------------
419
+ //
420
+ // Riguardano la relazione fra UTENTE e istanza, non il ciclo di vita della
421
+ // pratica. Come per documenti e partecipanti, l'engine espone un'unica API
422
+ // add-or-remove per ciascuna delle due.
423
+ //
424
+ // ATTENZIONE all'asimmetria: dei Preferiti si può leggere l'elenco
425
+ // (FavoritesRetrieveAllAsync), del Segui NO. Nessuna API dice se l'utente
426
+ // corrente segue una pratica: le `FollowAddOrRemove` sono di sola scrittura,
427
+ // qui come per pratiche-dossier e gruppi di lavoro. Finché il backend non
428
+ // esporrà `IsFollow` sul descrittore d'istanza, lo stato del comando Segui
429
+ // resta a carico del chiamante.
430
+ /** Istanze marcate come preferite dall'utente corrente. */
431
+ static async getFavorites() {
432
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
433
+ if (!engine)
434
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
435
+ return await engine.FavoritesRetrieveAllAsync() ?? [];
436
+ }
437
+ /**
438
+ * Aggiunge o rimuove l'istanza dai preferiti dell'utente corrente tramite
439
+ * FavoritesAddOrRemoveAsync(cftid, id, remove).
440
+ */
441
+ static async setFavorite(caseFlowId, favorite, cftid) {
442
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
443
+ if (!engine)
444
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
445
+ const resolvedCftid = cftid ?? (await this.getCaseflow(caseFlowId))?.cftid;
446
+ if (resolvedCftid == null)
447
+ throw new Error(`Impossibile risolvere il template (cftid) per l'istanza id=${caseFlowId}.`);
448
+ await engine.FavoritesAddOrRemoveAsync(resolvedCftid, caseFlowId, !favorite);
449
+ }
450
+ /**
451
+ * Inizia o smette di seguire l'istanza tramite
452
+ * FollowAddOrRemoveAsync(cftid, id, remove).
453
+ */
454
+ static async setFollow(caseFlowId, follow, cftid) {
455
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
456
+ if (!engine)
457
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
458
+ const resolvedCftid = cftid ?? (await this.getCaseflow(caseFlowId))?.cftid;
459
+ if (resolvedCftid == null)
460
+ throw new Error(`Impossibile risolvere il template (cftid) per l'istanza id=${caseFlowId}.`);
461
+ await engine.FollowAddOrRemoveAsync(resolvedCftid, caseFlowId, !follow);
462
+ }
463
+ /**
464
+ * Stato "l'utente corrente segue questa pratica" letto dal descrittore.
465
+ *
466
+ * Restituisce `undefined` quando il descrittore non porta l'informazione —
467
+ * cioè SEMPRE, finché il backend non aggiunge `IsFollow` in output a
468
+ * `usp_Cfi_RetrieveAll`/`usp_Cfi_Retrieve` e la relativa proprietà al
469
+ * contratto. Il tipo è tollerante perché la colonna è un BIT/NUMBER e può
470
+ * arrivare come booleano, come 0/1 o come stringa.
471
+ *
472
+ * Da NON dedurre dalla bacheca Home come fanno pratiche-dossier e gruppi di
473
+ * lavoro (filtro su `HomeBlogPost.classID`): quella query prende `TOP 30`
474
+ * righe e il solo ultimo post non di sistema per oggetto, quindi chi segue
475
+ * un oggetto senza post risulta "non seguace"; per le istanze di CaseFlow,
476
+ * poi, non esiste nemmeno un ramo nella UNION.
477
+ */
478
+ static readFollowFlag(cf) {
479
+ const raw = cf.isFollow;
480
+ if (typeof raw === 'boolean')
481
+ return raw;
482
+ if (typeof raw === 'number')
483
+ return raw !== 0;
484
+ if (raw === '0' || raw === '1')
485
+ return raw === '1';
486
+ return undefined;
487
+ }
488
+ // ----- Workitem: movimentazione del flusso -------------------------------
489
+ //
490
+ // L'engine espone UNA sola API per la movimentazione dei workitem,
491
+ // ExecuteWorkItemOperationAsync(cftid, id, operation): il tipo di operazione
492
+ // (Complete, Reject, RequestMoreInfo, RespondMoreInfo, Assign, ForceComplete,
493
+ // ForceReject, ...) è indicato in `operation.operation` e i soli dati pertinenti
494
+ // (wiid, actionID, toUserID, comment, metadataValues) vengono valorizzati in
495
+ // base all'operazione. I messaggi in bacheca (nota di completamento, motivo del
496
+ // rifiuto, richiesta informazioni) sono inseriti DIRETTAMENTE dal backend a
497
+ // partire dal `comment`: la UI non deve più creare post di bacheca a mano.
498
+ /** Completa (approva) un workitem. La nota facoltativa viene pubblicata in bacheca dal backend. */
499
+ static async completeWorkItem(caseFlowId, workItemId, response, cftid) {
500
+ await this.executeWorkItemOperation(caseFlowId, CaseFlowWIOperationDescriptor.fromJS({
501
+ operation: CF_WIOperations.Complete,
502
+ wiid: workItemId,
503
+ comment: response,
504
+ }), cftid);
505
+ }
506
+ /**
507
+ * Completa un workitem di "Completamento dati": i metadati valorizzati
508
+ * (mid -> valore) viaggiano in `metadataValues`; il backend li applica al
509
+ * documento master dell'istanza e completa il workitem.
510
+ */
511
+ static async completeDataCompletionWorkItem(caseFlowId, workItemId, metadataValues, response, cftid) {
512
+ await this.executeWorkItemOperation(caseFlowId, CaseFlowWIOperationDescriptor.fromJS({
513
+ operation: CF_WIOperations.Complete,
514
+ wiid: workItemId,
515
+ comment: response,
516
+ metadataValues: Object.entries(metadataValues).map(([mid, value]) => ({ mid: Number(mid), value })),
517
+ }), cftid);
518
+ }
519
+ /** Rifiuta un workitem. Il motivo viene pubblicato in bacheca dal backend. */
520
+ static async rejectWorkItem(caseFlowId, workItemId, response, cftid) {
521
+ await this.executeWorkItemOperation(caseFlowId, CaseFlowWIOperationDescriptor.fromJS({
522
+ operation: CF_WIOperations.Reject,
523
+ wiid: workItemId,
524
+ comment: response,
525
+ }), cftid);
526
+ }
527
+ /** Assegna un'azione a un utente creando il relativo workitem (operazione Assign). */
528
+ static async createWorkItemForAction(caseFlowId, actionID, toUserID, description, cftid) {
529
+ await this.executeWorkItemOperation(caseFlowId, CaseFlowWIOperationDescriptor.fromJS({
530
+ operation: CF_WIOperations.Assign,
531
+ actionID,
532
+ toUserID,
533
+ comment: description,
534
+ }), cftid);
535
+ }
536
+ /** Forza il completamento di un'azione (supervisor). */
537
+ static async forceCompleteAction(caseFlowId, actionID, reason, cftid) {
538
+ await this.executeWorkItemOperation(caseFlowId, CaseFlowWIOperationDescriptor.fromJS({
539
+ operation: CF_WIOperations.ForceComplete,
540
+ actionID,
541
+ comment: reason,
542
+ }), cftid);
543
+ }
544
+ /** Forza il rifiuto di un'azione (supervisor). Il motivo è obbligatorio. */
545
+ static async forceRejectAction(caseFlowId, actionID, reason, cftid) {
546
+ await this.executeWorkItemOperation(caseFlowId, CaseFlowWIOperationDescriptor.fromJS({
547
+ operation: CF_WIOperations.ForceReject,
548
+ actionID,
549
+ comment: reason,
550
+ }), cftid);
551
+ }
552
+ /**
553
+ * Richiede maggiori informazioni a un altro utente: il backend crea il
554
+ * workitem MoreInfo figlio assegnato a `targetUserId` e registra la richiesta.
555
+ */
556
+ static async requestMoreInfo(caseFlowId, workItemId, targetUserId, message, cftid) {
557
+ await this.executeWorkItemOperation(caseFlowId, CaseFlowWIOperationDescriptor.fromJS({
558
+ operation: CF_WIOperations.RequestMoreInfo,
559
+ wiid: workItemId,
560
+ toUserID: targetUserId,
561
+ comment: message,
562
+ }), cftid);
563
+ }
564
+ /** Risponde a una richiesta di informazioni completando il workitem MoreInfo. */
565
+ static async respondToMoreInfo(caseFlowId, moreInfoWorkItemId, response, cftid) {
566
+ await this.executeWorkItemOperation(caseFlowId, CaseFlowWIOperationDescriptor.fromJS({
567
+ operation: CF_WIOperations.RespondMoreInfo,
568
+ wiid: moreInfoWorkItemId,
569
+ comment: response,
570
+ }), cftid);
571
+ }
572
+ /**
573
+ * Chiama ExecuteWorkItemOperationAsync(cftid, id, operation) risolvendo il
574
+ * cftid quando non passato (come per bacheca, documenti e partecipanti).
575
+ */
576
+ static async executeWorkItemOperation(caseFlowId, operation, cftid) {
577
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
578
+ if (!engine)
579
+ throw new Error('Motore delle istanze CaseFlow non disponibile.');
580
+ const resolvedCftid = cftid ?? (await this.getCaseflow(caseFlowId))?.cftid;
581
+ if (resolvedCftid == null)
582
+ throw new Error(`Impossibile risolvere il template (cftid) per l'istanza id=${caseFlowId}.`);
583
+ await engine.ExecuteWorkItemOperationAsync(resolvedCftid, caseFlowId, operation);
584
+ }
585
+ static loadAllInstances() {
586
+ if (this._inFlightInstances)
587
+ return this._inFlightInstances;
588
+ const p = this.doLoadAllInstances().finally(() => {
589
+ if (this._inFlightInstances === p)
590
+ this._inFlightInstances = undefined;
591
+ });
592
+ this._inFlightInstances = p;
593
+ return p;
594
+ }
595
+ static async doLoadAllInstances() {
596
+ const engine = SDK_Globals.tmSession?.NewCaseFlowInstanceEngine();
597
+ if (!engine)
598
+ return [];
599
+ // cftid=0 => TUTTE le istanze visibili all'utente in un'unica chiamata,
600
+ // cross-template (owner, partecipante di template o di singola istanza),
601
+ // incluse quelle di template NON visibili all'utente.
602
+ //
603
+ // TRANSIZIONE: finché il backend non supporta `cftid=0`, la chiamata può
604
+ // fallire o tornare vuota. In quel caso si ripiega sull'enumerazione
605
+ // per-template (copre gli utenti che vedono i template). Rimuovere il
606
+ // fallback quando `cftid=0` sarà stabile lato backend.
607
+ let list = [];
608
+ try {
609
+ list = (await engine.RetrieveAllAsync(0)) ?? [];
610
+ }
611
+ catch (e) {
612
+ console.warn('[CaseFlow] RetrieveAll(0) non disponibile, ripiego per-template:', e);
613
+ }
614
+ if (list.length === 0) {
615
+ list = await this.loadInstancesPerTemplate(engine);
616
+ }
617
+ const instances = [];
618
+ const seen = new Set();
619
+ for (const inst of list) {
620
+ // Salvaguardia anti-duplicati per id.
621
+ if (inst.id != null) {
622
+ if (seen.has(inst.id))
623
+ continue;
624
+ seen.add(inst.id);
625
+ }
626
+ instances.push(inst);
627
+ }
628
+ // Risolvi i template di TUTTI i cftid presenti per popolare le cache di
629
+ // nome template e nome azione (usate in rendering). GetAsync risolve
630
+ // on-demand anche i template non creabili dall'utente (di cui però
631
+ // partecipa a un'istanza), che altrimenti la cache non conoscerebbe.
632
+ // Best-effort: se un template non è risolvibile, l'etichetta degrada su
633
+ // `Modello #<cftid>` senza nascondere l'istanza.
634
+ const cftids = [...new Set(instances.map(i => i.cftid).filter((c) => c != null))];
635
+ await Promise.all(cftids.map(async (cftid) => {
636
+ const tpl = await CaseFlowTemplateCacheService.GetAsync(cftid);
637
+ if (tpl) {
638
+ primeTemplateName(tpl.id, tpl.name);
639
+ primeActionNames(tpl);
640
+ }
641
+ }));
642
+ await this.resolveUserNames(instances);
643
+ return instances;
644
+ }
645
+ /**
646
+ * Fallback di TRANSIZIONE: enumera le istanze template per template usando
647
+ * l'elenco dei template visibili all'utente. Non copre le istanze di template
648
+ * NON visibili (partecipante a livello di istanza): per quelle serve la
649
+ * RetrieveAll cross-template (cftid=0). Da rimuovere quando cftid=0 è stabile.
650
+ */
651
+ static async loadInstancesPerTemplate(engine) {
652
+ const templates = await this.getTemplates();
653
+ const collected = [];
654
+ await Promise.all(templates.map(async (t) => {
655
+ if (t.id == null)
656
+ return;
657
+ try {
658
+ const l = await engine.RetrieveAllAsync(t.id);
659
+ for (const inst of l ?? []) {
660
+ if (inst.cftid == null)
661
+ inst.cftid = t.id;
662
+ collected.push(inst);
663
+ }
664
+ }
665
+ catch (e) {
666
+ console.warn(`[CaseFlow] RetrieveAll istanze per template ${t.id} fallito:`, e);
667
+ }
668
+ }));
669
+ return collected;
670
+ }
671
+ /** Valorizza `ownerName` sulle istanze (best-effort, via cache utenti). */
672
+ static async resolveUserNames(instances) {
673
+ const ids = new Set();
674
+ for (const inst of instances) {
675
+ if (inst.ownerID != null)
676
+ ids.add(inst.ownerID);
677
+ }
678
+ const nameById = new Map();
679
+ await Promise.all([...ids].map(async (id) => {
680
+ try {
681
+ const u = await UserListCacheService.GetAsync(id);
682
+ nameById.set(id, u?.name ?? `Utente ${id}`);
683
+ }
684
+ catch {
685
+ nameById.set(id, `Utente ${id}`);
686
+ }
687
+ }));
688
+ for (const inst of instances) {
689
+ if (inst.ownerID != null && !inst.ownerName)
690
+ inst.ownerName = nameById.get(inst.ownerID);
691
+ }
692
+ }
693
+ }