wuic-framework-lib 1.5.0 → 1.7.0

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 (26) hide show
  1. package/fesm2022/{wuic-framework-lib-chart-list.component-0J1ghIrS.mjs → wuic-framework-lib-chart-list.component-Bl424n7x.mjs} +50 -38
  2. package/fesm2022/{wuic-framework-lib-code-editor.component-C21mvjyr.mjs → wuic-framework-lib-code-editor.component-N6HMl2mf.mjs} +174 -274
  3. package/fesm2022/{wuic-framework-lib-designer.component-x10ZzUEy.mjs → wuic-framework-lib-designer.component-Bd2fwl6H.mjs} +499 -581
  4. package/fesm2022/{wuic-framework-lib-first-run-wizard.component-BuSxs_CM.mjs → wuic-framework-lib-first-run-wizard.component-NN6cWVHL.mjs} +22 -24
  5. package/fesm2022/{wuic-framework-lib-framework-docs.component-xTGLbEnp.mjs → wuic-framework-lib-framework-docs.component-B867JAYe.mjs} +2912 -294
  6. package/fesm2022/{wuic-framework-lib-pivot-builder.component-BkFFjPm4.mjs → wuic-framework-lib-pivot-builder.component-DWukH0L-.mjs} +580 -68
  7. package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-Bi-TrJB6.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-DWO1Rcgv.mjs} +2 -2
  8. package/fesm2022/wuic-framework-lib-report-designer.component-QcrU81m6.mjs +380 -0
  9. package/fesm2022/wuic-framework-lib-report-scaffold-dialog.component-U-zdOoRV.mjs +431 -0
  10. package/fesm2022/{wuic-framework-lib-report-viewer.component-BR-BfI2r.mjs → wuic-framework-lib-report-viewer.component-B4LhA3SL.mjs} +8 -13
  11. package/fesm2022/{wuic-framework-lib-scene3d-designer.component-BQzFxSXP.mjs → wuic-framework-lib-scene3d-designer.component-DjOfzKLo.mjs} +323 -88
  12. package/fesm2022/{wuic-framework-lib-scene3d-light-baker-gpu-0LjqhSNn.mjs → wuic-framework-lib-scene3d-light-baker-gpu-C96T9uIW.mjs} +2 -2
  13. package/fesm2022/{wuic-framework-lib-scene3d-shader-effect-CQLWkuh1.mjs → wuic-framework-lib-scene3d-shader-effect-DjHyCAFK.mjs} +612 -32
  14. package/fesm2022/{wuic-framework-lib-scene3d-viewer.component-imoV3HOs.mjs → wuic-framework-lib-scene3d-viewer.component-Dboz1fjl.mjs} +149 -18
  15. package/fesm2022/{wuic-framework-lib-scheduler-list.component-xzlTEkoq.mjs → wuic-framework-lib-scheduler-list.component--wY21R3o.mjs} +169 -53
  16. package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-CrS6fOGj.mjs → wuic-framework-lib-spreadsheet-list-sf.component-Ek30J8v-.mjs} +149 -108
  17. package/fesm2022/{wuic-framework-lib-stimulsoft-license.service-BIyxB4pO.mjs → wuic-framework-lib-stimulsoft-license.service-CsNNSFYs.mjs} +3 -3
  18. package/fesm2022/wuic-framework-lib-timeline-list.component-aqCH4P0G.mjs +805 -0
  19. package/fesm2022/{wuic-framework-lib-workflow-designer.component-5xtQ7-6z.mjs → wuic-framework-lib-workflow-designer.component-DMM8BeXm.mjs} +180 -150
  20. package/fesm2022/{wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs → wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs} +6761 -2347
  21. package/fesm2022/wuic-framework-lib.mjs +1 -1
  22. package/package.json +4 -2
  23. package/styles/_wuic-tokens.scss +82 -0
  24. package/styles/styles.scss +160 -0
  25. package/types/wuic-framework-lib.d.ts +4264 -2691
  26. package/fesm2022/wuic-framework-lib-report-designer.component-ZvB46-LV.mjs +0 -194
@@ -0,0 +1,805 @@
1
+ import * as i0 from '@angular/core';
2
+ import { EventEmitter, ViewChild, Output, Input, ViewEncapsulation, Component } from '@angular/core';
3
+ import { BehaviorSubject } from 'rxjs';
4
+ import * as i4 from 'primeng/button';
5
+ import { ButtonModule } from 'primeng/button';
6
+ import * as i8 from '@ngx-translate/core';
7
+ import { TranslateModule } from '@ngx-translate/core';
8
+ import Gantt from 'frappe-gantt';
9
+ import { u as TimelineOptions, g as MetadatiColonna, W as WtoolboxService, T as TranslationManagerService } from './wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs';
10
+ import * as i1 from '@angular/router';
11
+
12
+ /**
13
+ * Archetype business `timeline` (alias `gantt`) — pianificazione operativa su
14
+ * asse temporale (plan.timeline-gantt.md). Rendering via frappe-gantt (SVG);
15
+ * config metadata-driven da `md_props_bag.archetypes.timeline`
16
+ * (vedi TimelineOptions). Persistenza drag/resize su syncData del datasource
17
+ * (pristine → __changes, stesso seam anti-concorrenza del kanban), con
18
+ * modalita' immediate (default, rollback su errore) o batch (Save/Discard).
19
+ *
20
+ * Limiti v1 (documentati): dipendenze rese come frecce standard (FS; SS
21
+ * accettata nel dato ma resa uguale); groupByField ordina e prefissa il
22
+ * titolo (frappe-gantt non ha corsie per gruppo); milestone = barra 1 giorno
23
+ * con classe dedicata (diamante via CSS).
24
+ */
25
+ class TimelineListComponent {
26
+ router;
27
+ trslSrv;
28
+ cdr;
29
+ ngZone;
30
+ hardcodedRoute;
31
+ parentRecord;
32
+ parentMetaInfo;
33
+ datasource;
34
+ hardcodedDatasource;
35
+ hideToolbar = false;
36
+ onTimelineDataBound = new EventEmitter();
37
+ onTimelineTaskChange = new EventEmitter();
38
+ onTimelineTaskClick = new EventEmitter();
39
+ onTimelineBatchSave = new EventEmitter();
40
+ onTimelineBatchCancel = new EventEmitter();
41
+ ganttHost;
42
+ metaInfo;
43
+ timelineOptions = new TimelineOptions();
44
+ tasks = [];
45
+ warningMessage = '';
46
+ savingBatch = false;
47
+ viewMode = 'week';
48
+ gantt = null;
49
+ datasourceReadySubscription;
50
+ fetchInfoSubscription;
51
+ langChangeSubscription;
52
+ pendingChanges = new Map();
53
+ taskById = new Map();
54
+ constructor(router, trslSrv, cdr, ngZone) {
55
+ this.router = router;
56
+ this.trslSrv = trslSrv;
57
+ this.cdr = cdr;
58
+ this.ngZone = ngZone;
59
+ }
60
+ t(key, fallback) {
61
+ const translated = this.trslSrv?.instant?.(key);
62
+ return translated && translated !== key ? translated : fallback;
63
+ }
64
+ ngOnInit() {
65
+ // L'header del gantt (mesi/settimane) e' disegnato da frappe-gantt alla
66
+ // costruzione leggendo `language`: al cambio lingua va ricostruito, altrimenti
67
+ // resta sulla lingua "fotografata" al primo render.
68
+ this.langChangeSubscription = this.trslSrv?.onLangChange$?.subscribe(() => {
69
+ this.ngZone.run(() => {
70
+ this.rebuildTimeline();
71
+ this.cdr.detectChanges();
72
+ });
73
+ });
74
+ if (this.hardcodedDatasource) {
75
+ this.datasource = new BehaviorSubject(this.hardcodedDatasource);
76
+ this.subscribeToDatasource();
77
+ return;
78
+ }
79
+ if (this.datasource?.value) {
80
+ this.subscribeToDatasource();
81
+ return;
82
+ }
83
+ this.datasourceReadySubscription?.unsubscribe();
84
+ this.datasourceReadySubscription = this.datasource?.subscribe((ds) => {
85
+ if (ds) {
86
+ this.datasourceReadySubscription?.unsubscribe();
87
+ this.datasourceReadySubscription = undefined;
88
+ this.subscribeToDatasource();
89
+ }
90
+ });
91
+ }
92
+ ngOnDestroy() {
93
+ this.datasourceReadySubscription?.unsubscribe();
94
+ this.fetchInfoSubscription?.unsubscribe();
95
+ this.langChangeSubscription?.unsubscribe();
96
+ this.destroyGantt();
97
+ }
98
+ get hasPendingChanges() {
99
+ return this.pendingChanges.size > 0;
100
+ }
101
+ get pendingCount() {
102
+ return this.pendingChanges.size;
103
+ }
104
+ get isBatchMode() {
105
+ return this.timelineOptions?.persistMode === 'batch';
106
+ }
107
+ // ─── Datasource wiring (stesso pattern zone/CD del kanban) ───────────────
108
+ subscribeToDatasource() {
109
+ this.fetchInfoSubscription?.unsubscribe();
110
+ this.fetchInfoSubscription = this.datasource.value.fetchInfo$.subscribe((info) => {
111
+ const dataSourceRoute = this.datasource?.value?.route?.value || this.hardcodedRoute;
112
+ if (!info?.metaInfo || (dataSourceRoute && info.metaInfo?.tableMetadata?.md_route_name && dataSourceRoute !== info.metaInfo.tableMetadata.md_route_name)) {
113
+ return;
114
+ }
115
+ this.metaInfo = info.metaInfo;
116
+ this.timelineOptions = this.resolveTimelineOptions();
117
+ this.viewMode = (['day', 'week', 'month'].includes(this.timelineOptions.timeScale) ? this.timelineOptions.timeScale : 'week');
118
+ // Differito al prossimo microtask + ngZone.run: vedi i fix documentati in
119
+ // kanban-list.subscribeToDatasource (navigation vs refresh, CD fuori zona).
120
+ Promise.resolve().then(() => {
121
+ this.ngZone.run(() => {
122
+ this.rebuildTimeline();
123
+ this.cdr.detectChanges();
124
+ });
125
+ });
126
+ });
127
+ }
128
+ subscribeToDS() {
129
+ this.subscribeToDatasource();
130
+ }
131
+ resolveTimelineOptions() {
132
+ const options = Object.assign(new TimelineOptions(), this.metaInfo?.tableMetadata?.extraProps?.archetypes?.timeline || {});
133
+ options.startDateField = String(options.startDateField || '').trim();
134
+ options.endDateField = String(options.endDateField || '').trim();
135
+ options.titleField = String(options.titleField || '').trim() || this.pickDefaultTitleField();
136
+ options.progressField = String(options.progressField || '').trim();
137
+ options.dependencyField = String(options.dependencyField || '').trim();
138
+ options.milestoneField = String(options.milestoneField || '').trim();
139
+ options.groupByField = String(options.groupByField || '').trim();
140
+ options.persistMode = String(options.persistMode || 'immediate').trim().toLowerCase() === 'batch' ? 'batch' : 'immediate';
141
+ const scaleRaw = String(options.timeScale || 'week').trim().toLowerCase();
142
+ options.timeScale = ['day', 'week', 'month'].includes(scaleRaw) ? scaleRaw : 'week';
143
+ options.showTodayLine = options.showTodayLine !== false;
144
+ const clickRaw = String(options.clickAction || 'none').trim().toLowerCase();
145
+ options.clickAction = ['detail', 'edit'].includes(clickRaw) ? clickRaw : 'none';
146
+ return options;
147
+ }
148
+ pickDefaultTitleField() {
149
+ const cols = this.metaInfo?.columnMetadata || [];
150
+ const preferred = cols.find((c) => /name|title|descri/i.test(c.mc_nome_colonna) && c.mc_ui_column_type !== 'button');
151
+ return preferred?.mc_nome_colonna || cols.find((c) => c.mc_ui_column_type !== 'button')?.mc_nome_colonna || '';
152
+ }
153
+ // ─── Mapping record → task ───────────────────────────────────────────────
154
+ unwrap(value) {
155
+ return (value && typeof value === 'object' && 'value' in value && typeof value.subscribe === 'function') ? value.value : value;
156
+ }
157
+ /**
158
+ * Formatta una Date nel giorno LOCALE (YYYY-MM-DD). Mai toISOString qui:
159
+ * converte in UTC e per i fusi positivi (Italia +1/+2) una mezzanotte
160
+ * locale scivola al giorno precedente.
161
+ */
162
+ formatLocalDay(d) {
163
+ const pad = (n) => String(n).padStart(2, '0');
164
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
165
+ }
166
+ toIsoDay(value) {
167
+ const raw = this.unwrap(value);
168
+ if (raw === null || raw === undefined || raw === '') {
169
+ return null;
170
+ }
171
+ const d = raw instanceof Date ? raw : new Date(raw);
172
+ return Number.isNaN(d.getTime()) ? null : this.formatLocalDay(d);
173
+ }
174
+ /**
175
+ * Etichetta di gruppo del task.
176
+ *
177
+ * `groupByField` puo' puntare a una colonna testo oppure a un `lookupByID`
178
+ * (es. progetto su tabella dedicata): in quest'ultimo caso il valore grezzo
179
+ * e' l'id, e mostrarlo darebbe "[3] Titolo". Deleghiamo a
180
+ * `MetadatiColonna.formatGridViewValue`, il risolutore usato dalla grid, che
181
+ * conosce sia l'alias denormalizzato del server
182
+ * (`<route>___<textField>__<colonna>`) sia il fallback su `__lookup_obj`
183
+ * quando il record non ha ancora fatto round-trip.
184
+ */
185
+ resolveGroupLabel(row) {
186
+ const field = String(this.timelineOptions?.groupByField || '').trim();
187
+ if (!field) {
188
+ return '';
189
+ }
190
+ const col = this.metaInfo?.columnMetadata?.find((c) => c.mc_nome_colonna === field);
191
+ if (col) {
192
+ try {
193
+ const formatted = MetadatiColonna.formatGridViewValue(col, row);
194
+ if (formatted !== null && formatted !== undefined && String(formatted).trim() !== '') {
195
+ return String(formatted);
196
+ }
197
+ }
198
+ catch { /* fallback al valore grezzo */ }
199
+ }
200
+ return String(this.unwrap(row?.[field]) ?? '');
201
+ }
202
+ primaryKeyField() {
203
+ return this.metaInfo?.columnMetadata?.find((c) => c.mc_is_primary_key)?.mc_nome_colonna || 'id';
204
+ }
205
+ /**
206
+ * Estrae l'id di un predecessore da un elemento di `dependencyField`.
207
+ *
208
+ * Il campo puo' essere alimentato in due modi:
209
+ * - colonna testo con csv di id → elementi scalari;
210
+ * - colonna `multiple_check` (multiselect del framework) → il record raw
211
+ * porta un ARRAY DI OGGETTI del correlato (vedi
212
+ * metadati_colonna_helpers: la grid rende `x[mc_ui_grid_display_field]`),
213
+ * quindi l'id va estratto dal campo dichiarato in
214
+ * `mc_ui_grid_related_id_field`, con fallback sulla pk della route.
215
+ */
216
+ dependencyIdOf(item) {
217
+ const value = this.unwrap(item);
218
+ if (value === null || value === undefined) {
219
+ return '';
220
+ }
221
+ if (typeof value !== 'object') {
222
+ return String(value).trim();
223
+ }
224
+ const depCol = this.metaInfo?.columnMetadata?.find((c) => c.mc_nome_colonna === this.timelineOptions?.dependencyField);
225
+ const idField = String(depCol?.mc_ui_grid_related_id_field || '').trim() || this.primaryKeyField();
226
+ const raw = this.unwrap(value[idField]);
227
+ return raw === null || raw === undefined ? '' : String(raw).trim();
228
+ }
229
+ /**
230
+ * Costruisce i TimelineTask dai record della route. Regole v1:
231
+ * - record senza start valido → skippato (conteggiato nel warning);
232
+ * - end mancante → milestone implicita (end = start);
233
+ * - end < start → clamp a start + warning;
234
+ * - dependencies: csv/array di id predecessori normalizzata in csv.
235
+ */
236
+ buildTasks(rows) {
237
+ const o = this.timelineOptions;
238
+ const pk = this.primaryKeyField();
239
+ const warnings = [];
240
+ let skipped = 0;
241
+ const tasks = [];
242
+ for (const row of rows || []) {
243
+ const id = String(this.unwrap(row?.[pk]) ?? '');
244
+ const start = this.toIsoDay(row?.[o.startDateField]);
245
+ if (!id || !start) {
246
+ skipped++;
247
+ continue;
248
+ }
249
+ let end = this.toIsoDay(row?.[o.endDateField]);
250
+ const milestoneFlag = o.milestoneField ? !!this.unwrap(row?.[o.milestoneField]) : false;
251
+ const isMilestone = milestoneFlag || !end;
252
+ if (!end) {
253
+ end = start;
254
+ }
255
+ if (end < start) {
256
+ warnings.push(this.t('timeline.warning.end_before_start', 'End date before start date on task') + ` ${id}`);
257
+ end = start;
258
+ }
259
+ const progressRaw = o.progressField ? Number(this.unwrap(row?.[o.progressField])) : 0;
260
+ const progress = Number.isFinite(progressRaw) ? Math.min(100, Math.max(0, progressRaw)) : 0;
261
+ let dependencies = '';
262
+ if (o.dependencyField) {
263
+ const depRaw = this.unwrap(row?.[o.dependencyField]);
264
+ if (Array.isArray(depRaw)) {
265
+ dependencies = depRaw.map((x) => this.dependencyIdOf(x)).filter(Boolean).join(', ');
266
+ }
267
+ else if (depRaw !== null && depRaw !== undefined && depRaw !== '') {
268
+ dependencies = String(depRaw).split(/[,;]/).map((x) => x.trim()).filter(Boolean).join(', ');
269
+ }
270
+ }
271
+ const group = this.resolveGroupLabel(row);
272
+ const title = String(this.unwrap(row?.[o.titleField]) ?? id);
273
+ tasks.push({
274
+ id,
275
+ name: group ? `[${group}] ${title}` : title,
276
+ start,
277
+ end,
278
+ progress,
279
+ dependencies,
280
+ custom_class: isMilestone ? 'wuic-timeline-milestone' : undefined,
281
+ record: row,
282
+ isMilestone,
283
+ group
284
+ });
285
+ }
286
+ // Validazione dipendenze: figlio che inizia prima della fine del predecessore.
287
+ const byId = new Map(tasks.map((t2) => [t2.id, t2]));
288
+ for (const task of tasks) {
289
+ for (const depId of task.dependencies.split(',').map((x) => x.trim()).filter(Boolean)) {
290
+ const pred = byId.get(depId);
291
+ if (pred && task.start < pred.end && !task.isMilestone) {
292
+ warnings.push(this.t('timeline.warning.dependency_conflict', 'Task starts before its predecessor ends') + ` (${depId} → ${task.id})`);
293
+ }
294
+ }
295
+ }
296
+ // groupByField: frappe-gantt non ha corsie → ordina per gruppo poi start.
297
+ if (o.groupByField) {
298
+ tasks.sort((a, b) => (a.group || '').localeCompare(b.group || '') || a.start.localeCompare(b.start));
299
+ }
300
+ return { tasks, skipped, warnings };
301
+ }
302
+ // ─── Rendering frappe-gantt ──────────────────────────────────────────────
303
+ rebuildTimeline() {
304
+ const o = this.timelineOptions;
305
+ if (!o.startDateField) {
306
+ this.warningMessage = this.t('timeline.warning.missing_config', 'Timeline non configurata: impostare startDateField (e endDateField) in md_props_bag.archetypes.timeline.');
307
+ this.destroyGantt();
308
+ this.tasks = [];
309
+ return;
310
+ }
311
+ const rows = Array.isArray(this.datasource?.value?.resultInfo?.dato) ? this.datasource.value.resultInfo.dato : [];
312
+ const { tasks, skipped, warnings } = this.buildTasks(rows);
313
+ this.tasks = tasks;
314
+ this.taskById.clear();
315
+ tasks.forEach((task) => this.taskById.set(task.id, task));
316
+ const parts = [...warnings.slice(0, 3)];
317
+ if (skipped > 0) {
318
+ parts.unshift(`${this.t('timeline.warning.skipped_rows', 'Righe senza data di inizio valida (escluse)')}: ${skipped}`);
319
+ }
320
+ this.warningMessage = parts.join(' | ');
321
+ this.destroyGantt();
322
+ if (!tasks.length) {
323
+ this.onTimelineDataBound.emit({ metaInfo: this.metaInfo, tasks, skipped });
324
+ return;
325
+ }
326
+ // frappe-gantt muta l'array: passa una copia shallow senza i campi extra.
327
+ const ganttTasks = tasks.map((task) => ({
328
+ id: task.id,
329
+ name: task.name,
330
+ start: task.start,
331
+ end: task.end,
332
+ progress: task.progress,
333
+ dependencies: task.dependencies,
334
+ custom_class: task.custom_class
335
+ }));
336
+ // Fuori zona: frappe-gantt attacca listener globali (mousemove) che non
337
+ // devono triggerare CD continua.
338
+ this.ngZone.runOutsideAngular(() => {
339
+ this.gantt = new Gantt(this.ganttHost.nativeElement, ganttTasks, {
340
+ view_mode: this.frappeViewMode(),
341
+ language: this.currentLanguageCode(),
342
+ today_button: o.showTodayLine,
343
+ view_mode_select: false,
344
+ readonly_progress: true,
345
+ popup: (ctx) => this.renderPopup(ctx),
346
+ on_click: (task) => this.ngZone.run(() => this.handleTaskClick(task)),
347
+ on_date_change: (task, start, end) => this.ngZone.run(() => { void this.handleDateChange(task, start, end); })
348
+ });
349
+ });
350
+ this.onTimelineDataBound.emit({ metaInfo: this.metaInfo, tasks, skipped });
351
+ }
352
+ /** Parser boolean-like per i flag metadata (`true/1/'yes'` → true). */
353
+ metaFlag(raw) {
354
+ if (raw === undefined || raw === null || String(raw).trim() === '') {
355
+ return false;
356
+ }
357
+ return !['false', '0', 'no', 'n', 'off'].includes(String(raw).trim().toLowerCase());
358
+ }
359
+ /** La route consente l'edit dei record (metadata `md_editable`). */
360
+ get canEditRecords() {
361
+ return this.metaFlag(this.metaInfo?.tableMetadata?.md_editable);
362
+ }
363
+ /** La route consente l'inserimento (metadata `md_insertable`). */
364
+ get canInsertRecords() {
365
+ return this.metaFlag(this.metaInfo?.tableMetadata?.md_insertable);
366
+ }
367
+ /** La route consente la cancellazione (metadata `md_deletable`). */
368
+ get canDeleteRecords() {
369
+ return this.metaFlag(this.metaInfo?.tableMetadata?.md_deletable);
370
+ }
371
+ /**
372
+ * Un task e' "foglia" quando NESSUN altro task lo elenca fra i propri
373
+ * predecessori. Cancellare un task da cui altri dipendono lascerebbe le
374
+ * dipendenze orfane (frecce verso un id inesistente), quindi l'azione di
375
+ * delete viene offerta solo sulle foglie.
376
+ */
377
+ isLeafTask(task) {
378
+ if (!task) {
379
+ return false;
380
+ }
381
+ const id = String(task.id);
382
+ return !this.tasks.some((other) => other.id !== task.id
383
+ && other.dependencies.split(',').map((x) => x.trim()).includes(id));
384
+ }
385
+ /**
386
+ * Popup della barra (frappe `popup`): replica il contenuto di default
387
+ * (titolo, intervallo, avanzamento) e, se la route e' editabile, aggiunge
388
+ * l'azione che apre il record nel parametric-dialog — stesso flusso
389
+ * dell'edit da list-grid (`data-action-button.edit`).
390
+ *
391
+ * `add_action(label, handler)` e' l'API di frappe-gantt per i bottoni del
392
+ * popup: crea un `button.action-btn` dentro `.actions` e ne lega l'onclick.
393
+ */
394
+ renderPopup(ctx) {
395
+ const task = this.taskById.get(String(ctx?.task?.id));
396
+ if (!task) {
397
+ return;
398
+ }
399
+ ctx.set_title?.(task.name);
400
+ ctx.set_subtitle?.(task.isMilestone ? task.start : `${task.start} → ${task.end}`);
401
+ ctx.set_details?.(`${this.t('timeline.popup.progress', 'Avanzamento')}: ${task.progress}%`);
402
+ if (this.canEditRecords) {
403
+ ctx.add_action?.(this.t('timeline.popup.edit', 'Modifica'), () => this.ngZone.run(() => { void this.openEditDialog(task); }));
404
+ }
405
+ if (this.canInsertRecords) {
406
+ ctx.add_action?.(this.t('timeline.popup.add', 'Nuovo'), () => this.ngZone.run(() => { void this.openAddDialog(task); }));
407
+ }
408
+ // Delete solo sulle foglie: vedi isLeafTask.
409
+ if (this.canDeleteRecords && this.isLeafTask(task)) {
410
+ ctx.add_action?.(this.t('timeline.popup.delete', 'Elimina'), () => this.ngZone.run(() => { void this.deleteTask(task); }));
411
+ }
412
+ }
413
+ /**
414
+ * Apre il record del task nel parametric-dialog in modalita' edit.
415
+ *
416
+ * L'import e' DINAMICO di proposito: `ParametricDialogComponent` tira dentro
417
+ * tutti i field editor e vanificherebbe il lazy-loading di questo chunk
418
+ * (che gia' porta frappe-gantt). Viene caricato solo al primo click.
419
+ */
420
+ async openEditDialog(task) {
421
+ const ds = this.datasource?.value;
422
+ if (!ds) {
423
+ return;
424
+ }
425
+ try {
426
+ ds.setCurrent(task.record);
427
+ const { ParametricDialogComponent } = await import('./wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs').then(function (n) { return n.c9; });
428
+ const header = `${this.t('timeline.popup.edit', 'Modifica')} ${this.metaInfo?.tableMetadata?.md_display_string || ''}`.trim();
429
+ const ref = WtoolboxService.dialogService.open(ParametricDialogComponent, {
430
+ data: {
431
+ datasource: this.datasource,
432
+ isEditForm: true
433
+ },
434
+ header,
435
+ styleClass: 'edit-form-content',
436
+ position: 'center',
437
+ closable: true,
438
+ // stacking sopra eventuali dialog gia' aperti (stesso flag della grid)
439
+ duplicate: true
440
+ });
441
+ ref?.onClose?.subscribe((result) => {
442
+ if (result) {
443
+ // Ricarica: fetchData ripubblica su fetchInfo$ e il gantt si ridisegna
444
+ // con le date/avanzamenti aggiornati.
445
+ void ds.fetchData();
446
+ }
447
+ });
448
+ }
449
+ catch (err) {
450
+ WtoolboxService.errorHandler.handleError(err);
451
+ }
452
+ }
453
+ /**
454
+ * Apre il dialog di inserimento con il task cliccato gia' PRESELEZIONATO fra
455
+ * i predecessori del nuovo task.
456
+ *
457
+ * La preselezione richiede due observable coerenti (stesso contratto che il
458
+ * `lookup-editor` produce quando l'utente sceglie a mano, vedi
459
+ * lookup-editor.component `___added`):
460
+ * - `<campo>` → array di id selezionati;
461
+ * - `<campo>__lookup_obj` → array di oggetti correlati, ognuno marcato
462
+ * `___added: true` perche' `syncData` invia proprio questo array come
463
+ * payload m2m e il backend inserisce nella tabella ponte solo le voci
464
+ * marcate come aggiunte.
465
+ * Con `dependencyField` su colonna testo (csv) si valorizza direttamente l'id.
466
+ */
467
+ async openAddDialog(task) {
468
+ const ds = this.datasource?.value;
469
+ if (!ds) {
470
+ return;
471
+ }
472
+ try {
473
+ // Il preset va passato ad addNewRecord (che fa Object.assign sul record
474
+ // PLAIN prima di setCurrent): e' setCurrent→getObservable a derivare gli
475
+ // observable corretti. Scrivere a mano `<campo>` e `<campo>__lookup_obj`
476
+ // dopo la creazione non regge il round-trip
477
+ // getModelFromObservable→setCurrent→getObservable e il valore torna null
478
+ // al salvataggio (il chip resta visibile, ma il payload parte vuoto).
479
+ const record = ds.addNewRecord(this.buildPredecessorPreset(task));
480
+ const { ParametricDialogComponent } = await import('./wuic-framework-lib-wuic-framework-lib-dfLTqam5.mjs').then(function (n) { return n.c9; });
481
+ const header = `${this.t('timeline.popup.add', 'Nuovo')} ${this.metaInfo?.tableMetadata?.md_display_string || ''}`.trim();
482
+ const ref = WtoolboxService.dialogService.open(ParametricDialogComponent, {
483
+ data: {
484
+ record,
485
+ pristine: ds.pristine,
486
+ metaInfo: this.metaInfo,
487
+ datasource: this.datasource,
488
+ isEditForm: true
489
+ },
490
+ header,
491
+ styleClass: 'edit-form-content',
492
+ position: 'center',
493
+ closable: true,
494
+ duplicate: true
495
+ });
496
+ ref?.onClose?.subscribe((added) => {
497
+ if (added) {
498
+ void ds.fetchData();
499
+ }
500
+ });
501
+ }
502
+ catch (err) {
503
+ WtoolboxService.errorHandler.handleError(err);
504
+ }
505
+ }
506
+ /**
507
+ * Costruisce il preset PLAIN da passare ad `addNewRecord`, col task di
508
+ * partenza gia' fra i predecessori.
509
+ *
510
+ * Formato per la multiselect: array di record correlati con i tre flag che
511
+ * il backend stesso emette in lettura (`AttachManyToManyOptionsBatch` →
512
+ * `___selected` / `___added` / `___deleted`). `___added: true` e' cio' che
513
+ * fa scrivere la riga nella tabella ponte in fase di insert; da questo array
514
+ * `getObservable` deriva sia gli id sia `<campo>__lookup_obj`.
515
+ * Con `dependencyField` su colonna testo il preset e' il semplice id.
516
+ */
517
+ buildPredecessorPreset(task) {
518
+ const field = String(this.timelineOptions?.dependencyField || '').trim();
519
+ if (!field) {
520
+ return undefined;
521
+ }
522
+ const depCol = this.metaInfo?.columnMetadata?.find((c) => c.mc_nome_colonna === field);
523
+ if (depCol?.mc_ui_column_type !== 'multiple_check') {
524
+ return { [field]: task.id };
525
+ }
526
+ const idField = String(depCol?.mc_ui_grid_related_id_field || '').trim() || this.primaryKeyField();
527
+ const displayField = String(depCol?.mc_ui_grid_display_field || '').trim()
528
+ || String(this.timelineOptions?.titleField || '').trim();
529
+ const selected = {
530
+ [idField]: task.id,
531
+ ___selected: true,
532
+ ___added: true,
533
+ ___deleted: false
534
+ };
535
+ if (displayField) {
536
+ // Titolo dal record, senza il prefisso di gruppo aggiunto al rendering.
537
+ selected[displayField] = this.unwrap(task.record?.[displayField]) ?? task.name;
538
+ }
539
+ return { [field]: [selected] };
540
+ }
541
+ /**
542
+ * Elimina il task (solo foglie, vedi `isLeafTask`) previa conferma, con lo
543
+ * stesso seam della grid: `syncData(record, record, true)`.
544
+ */
545
+ async deleteTask(task) {
546
+ const ds = this.datasource?.value;
547
+ if (!ds || !this.isLeafTask(task)) {
548
+ return;
549
+ }
550
+ try {
551
+ const confirmed = await WtoolboxService.confirm({
552
+ message: this.t('data_action.confirm_delete_message', 'Confermi l\'eliminazione?'),
553
+ header: this.t('confirmation', 'Conferma')
554
+ });
555
+ if (!confirmed) {
556
+ return;
557
+ }
558
+ const result = await ds.syncData(task.record, task.record, true);
559
+ if (result) {
560
+ void ds.fetchData();
561
+ }
562
+ }
563
+ catch (err) {
564
+ WtoolboxService.errorHandler.handleError(err);
565
+ }
566
+ }
567
+ /**
568
+ * Codice lingua per l'header del gantt. frappe-gantt non ha dizionari
569
+ * propri: passa questo valore a `Intl.DateTimeFormat`, quindi accetta sia
570
+ * `it` sia `it-IT`. Fallback `en` se la lingua non e' ancora risolta.
571
+ */
572
+ currentLanguageCode() {
573
+ const lang = String(this.trslSrv?.getCurrentLang?.() || '').trim();
574
+ return lang || 'en';
575
+ }
576
+ frappeViewMode() {
577
+ switch (this.viewMode) {
578
+ case 'day': return 'Day';
579
+ case 'month': return 'Month';
580
+ default: return 'Week';
581
+ }
582
+ }
583
+ destroyGantt() {
584
+ if (this.ganttHost?.nativeElement) {
585
+ this.ganttHost.nativeElement.innerHTML = '';
586
+ }
587
+ this.gantt = null;
588
+ }
589
+ setViewMode(mode) {
590
+ this.viewMode = mode;
591
+ if (this.gantt) {
592
+ this.ngZone.runOutsideAngular(() => this.gantt.change_view_mode(this.frappeViewMode()));
593
+ }
594
+ }
595
+ scrollToToday() {
596
+ try {
597
+ this.ngZone.runOutsideAngular(() => this.gantt?.scroll_current?.());
598
+ }
599
+ catch { /* best-effort */ }
600
+ }
601
+ refresh() {
602
+ void this.datasource?.value?.fetchData();
603
+ }
604
+ /**
605
+ * Sopprime il context menu nativo sull'area del gantt. Niente
606
+ * `stopPropagation`: fermiamo solo l'azione di default del browser, senza
607
+ * impedire ad handler applicativi a monte di reagire all'evento.
608
+ */
609
+ onGanttContextMenu(event) {
610
+ event?.preventDefault?.();
611
+ }
612
+ // ─── Interazioni ─────────────────────────────────────────────────────────
613
+ handleTaskClick(ganttTask) {
614
+ const task = this.taskById.get(String(ganttTask?.id));
615
+ if (!task) {
616
+ return;
617
+ }
618
+ this.onTimelineTaskClick.emit({ task });
619
+ const action = this.timelineOptions.clickAction;
620
+ if (action === 'none') {
621
+ return;
622
+ }
623
+ const route = this.datasource?.value?.route?.value || this.hardcodedRoute || this.metaInfo?.tableMetadata?.md_route_name;
624
+ if (route) {
625
+ void this.router.navigateByUrl(`/${route}/${action}/${encodeURIComponent(task.id)}`);
626
+ }
627
+ }
628
+ /** Drag/resize da frappe-gantt: valida, poi persist (immediate) o accoda (batch). */
629
+ async handleDateChange(ganttTask, start, end) {
630
+ const task = this.taskById.get(String(ganttTask?.id));
631
+ if (!task) {
632
+ return;
633
+ }
634
+ const newStart = this.formatLocalDay(start);
635
+ const newEnd = this.formatLocalDay(end);
636
+ if (newEnd < newStart) {
637
+ this.warningMessage = this.t('timeline.warning.end_before_start', 'End date before start date on task') + ` ${task.id}`;
638
+ this.rebuildTimeline();
639
+ return;
640
+ }
641
+ const o = this.timelineOptions;
642
+ const prevStart = this.unwrap(task.record?.[o.startDateField]);
643
+ const prevEnd = this.unwrap(task.record?.[o.endDateField]);
644
+ if (this.isBatchMode) {
645
+ this.registerPendingChange(task, prevStart, prevEnd, newStart, newEnd);
646
+ this.applyDatesToRecord(task, newStart, newEnd);
647
+ task.start = newStart;
648
+ task.end = newEnd;
649
+ this.onTimelineTaskChange.emit({ task, start: newStart, end: newEnd, mode: 'batch' });
650
+ this.cdr.detectChanges();
651
+ return;
652
+ }
653
+ const ok = await this.persistDateChange(task, prevStart, prevEnd, newStart, newEnd);
654
+ if (ok) {
655
+ this.applyDatesToRecord(task, newStart, newEnd);
656
+ task.start = newStart;
657
+ task.end = newEnd;
658
+ this.onTimelineTaskChange.emit({ task, start: newStart, end: newEnd, mode: 'immediate' });
659
+ }
660
+ else {
661
+ // Rollback UI: ridisegna dalle date originali del record.
662
+ this.rebuildTimeline();
663
+ }
664
+ this.cdr.detectChanges();
665
+ }
666
+ applyDatesToRecord(task, newStart, newEnd) {
667
+ const o = this.timelineOptions;
668
+ if (o.startDateField) {
669
+ task.record[o.startDateField] = newStart;
670
+ }
671
+ if (o.endDateField && !task.isMilestone) {
672
+ task.record[o.endDateField] = newEnd;
673
+ }
674
+ }
675
+ registerPendingChange(task, fromStart, fromEnd, toStart, toEnd) {
676
+ const existing = this.pendingChanges.get(task.id);
677
+ if (existing) {
678
+ existing.toStart = toStart;
679
+ existing.toEnd = toEnd;
680
+ return;
681
+ }
682
+ this.pendingChanges.set(task.id, { taskId: task.id, record: task.record, fromStart, fromEnd, toStart, toEnd });
683
+ }
684
+ // ─── Persistenza (seam syncData + pristine, come kanban) ─────────────────
685
+ async persistDateChange(task, prevStart, prevEnd, newStart, newEnd) {
686
+ const ds = this.datasource?.value;
687
+ const o = this.timelineOptions;
688
+ if (!ds || !o.startDateField) {
689
+ return true;
690
+ }
691
+ try {
692
+ // Current-record flow: changeTracking produce il payload __changes.
693
+ ds.setCurrent(task.record);
694
+ const observable = ds.resultInfo?.current || ds.getObservable(task.record);
695
+ const pristine = ds.pristine ? { ...ds.pristine } : this.buildPristineRow(task.record);
696
+ pristine[o.startDateField] = prevStart;
697
+ if (o.endDateField) {
698
+ pristine[o.endDateField] = prevEnd;
699
+ }
700
+ if (observable?.[o.startDateField]) {
701
+ observable[o.startDateField].next(newStart);
702
+ }
703
+ if (o.endDateField && !task.isMilestone && observable?.[o.endDateField]) {
704
+ observable[o.endDateField].next(newEnd);
705
+ }
706
+ const result = await ds.syncData(observable, pristine, false);
707
+ if (!result) {
708
+ throw new Error(this.t('timeline.error.sync_data_null', 'syncData returned null'));
709
+ }
710
+ return true;
711
+ }
712
+ catch (err) {
713
+ WtoolboxService.errorHandler.handleError(err);
714
+ return false;
715
+ }
716
+ }
717
+ buildPristineRow(record) {
718
+ const out = {};
719
+ (this.metaInfo?.columnMetadata || []).forEach((col) => {
720
+ out[col.mc_nome_colonna] = record?.[col.mc_nome_colonna];
721
+ });
722
+ return out;
723
+ }
724
+ // ─── Batch save/discard ──────────────────────────────────────────────────
725
+ async saveBatchChanges() {
726
+ if (!this.pendingChanges.size || this.savingBatch) {
727
+ return;
728
+ }
729
+ this.savingBatch = true;
730
+ let saved = 0;
731
+ try {
732
+ for (const change of [...this.pendingChanges.values()]) {
733
+ const task = this.taskById.get(change.taskId);
734
+ if (!task) {
735
+ this.pendingChanges.delete(change.taskId);
736
+ continue;
737
+ }
738
+ const ok = await this.persistDateChange(task, change.fromStart, change.fromEnd, change.toStart, change.toEnd);
739
+ if (!ok) {
740
+ // Stop al primo errore: i rimanenti restano pending (ritentabili).
741
+ break;
742
+ }
743
+ this.pendingChanges.delete(change.taskId);
744
+ saved++;
745
+ }
746
+ }
747
+ finally {
748
+ this.savingBatch = false;
749
+ this.onTimelineBatchSave.emit({ savedCount: saved });
750
+ this.cdr.detectChanges();
751
+ }
752
+ }
753
+ cancelBatchChanges() {
754
+ let reverted = 0;
755
+ for (const change of [...this.pendingChanges.values()]) {
756
+ const task = this.taskById.get(change.taskId);
757
+ if (task) {
758
+ const o = this.timelineOptions;
759
+ task.record[o.startDateField] = change.fromStart;
760
+ if (o.endDateField) {
761
+ task.record[o.endDateField] = change.fromEnd;
762
+ }
763
+ reverted++;
764
+ }
765
+ }
766
+ this.pendingChanges.clear();
767
+ this.rebuildTimeline();
768
+ this.onTimelineBatchCancel.emit({ revertedCount: reverted });
769
+ this.cdr.detectChanges();
770
+ }
771
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TimelineListComponent, deps: [{ token: i1.Router }, { token: TranslationManagerService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
772
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.9", type: TimelineListComponent, isStandalone: true, selector: "wuic-timeline-list", inputs: { hardcodedRoute: "hardcodedRoute", parentRecord: "parentRecord", parentMetaInfo: "parentMetaInfo", datasource: "datasource", hardcodedDatasource: "hardcodedDatasource", hideToolbar: "hideToolbar" }, outputs: { onTimelineDataBound: "onTimelineDataBound", onTimelineTaskChange: "onTimelineTaskChange", onTimelineTaskClick: "onTimelineTaskClick", onTimelineBatchSave: "onTimelineBatchSave", onTimelineBatchCancel: "onTimelineBatchCancel" }, viewQueries: [{ propertyName: "ganttHost", first: true, predicate: ["ganttHost"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"wuic-timeline-wrapper\">\n @if (!hideToolbar) {\n <div class=\"wuic-timeline-toolbar\">\n <div class=\"wuic-timeline-toolbar-left\">\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-calendar\" [label]=\"'timeline.toolbar.day' | translate\"\n [styleClass]=\"viewMode === 'day' ? 'wuic-timeline-mode-active' : ''\"\n (onClick)=\"setViewMode('day')\" />\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-calendar\" [label]=\"'timeline.toolbar.week' | translate\"\n [styleClass]=\"viewMode === 'week' ? 'wuic-timeline-mode-active' : ''\"\n (onClick)=\"setViewMode('week')\" />\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-calendar\" [label]=\"'timeline.toolbar.month' | translate\"\n [styleClass]=\"viewMode === 'month' ? 'wuic-timeline-mode-active' : ''\"\n (onClick)=\"setViewMode('month')\" />\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-map-marker\" [label]=\"'timeline.toolbar.today' | translate\"\n (onClick)=\"scrollToToday()\" />\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-refresh\" (onClick)=\"refresh()\" />\n </div>\n @if (isBatchMode) {\n <div class=\"wuic-timeline-toolbar-right\">\n <p-button size=\"small\" icon=\"pi pi-check\" [label]=\"'timeline.toolbar.save' | translate\"\n [disabled]=\"!hasPendingChanges || savingBatch\" [loading]=\"savingBatch\"\n (onClick)=\"saveBatchChanges()\" />\n <p-button size=\"small\" severity=\"secondary\" icon=\"pi pi-times\" [label]=\"'timeline.toolbar.discard' | translate\"\n [disabled]=\"!hasPendingChanges || savingBatch\"\n (onClick)=\"cancelBatchChanges()\" />\n @if (hasPendingChanges) {\n <span class=\"wuic-timeline-pending-badge\">{{ pendingCount }}</span>\n }\n </div>\n }\n </div>\n }\n\n @if (warningMessage) {\n <div class=\"wuic-timeline-warning\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <span>{{ warningMessage }}</span>\n </div>\n }\n\n <!-- Il context menu del browser sull'area di disegno interferisce con\n drag/resize delle barre (un right-click accidentale durante il drag lo\n apre): sopprimiamo l'evento sull'host, non sulla toolbar. -->\n <div class=\"wuic-timeline-gantt-host\" #ganttHost (contextmenu)=\"onGanttContextMenu($event)\"></div>\n\n @if (!tasks.length && !warningMessage) {\n <div class=\"wuic-timeline-empty\">{{ 'timeline.empty' | translate }}</div>\n }\n</div>\n", styles: [":root{--g-arrow-color: #1f2937;--g-bar-color: #fff;--g-bar-border: #fff;--g-tick-color-thick: #ededed;--g-tick-color: #f3f3f3;--g-actions-background: #f3f3f3;--g-border-color: #ebeff2;--g-text-muted: #7c7c7c;--g-text-light: #fff;--g-text-dark: #171717;--g-progress-color: #dbdbdb;--g-handle-color: #37352f;--g-weekend-label-color: #dcdce4;--g-expected-progress: #c4c4e9;--g-header-background: #fff;--g-row-color: #fdfdfd;--g-row-border-color: #c7c7c7;--g-today-highlight: #37352f;--g-popup-actions: #ebeff2;--g-weekend-highlight-color: #f7f7f7}html[data-theme=dark]{--g-arrow-color: #e5e5e5;--g-bar-color: #2a2a2a;--g-bar-border: #3a3a3a;--g-tick-color-thick: #1a1a1a;--g-tick-color: #0a0a0a;--g-actions-background: #1a1a1a;--g-border-color: #2a2a2a;--g-text-muted: #999999;--g-text-light: #ffffff;--g-text-dark: #f5f5f5;--g-progress-color: #4a4a4a;--g-handle-color: #d0d0d0;--g-weekend-label-color: #3a3a3a;--g-expected-progress: #5a5a5a;--g-header-background: #0a0a0a;--g-row-color: #141414;--g-row-border-color: #2a2a2a;--g-today-highlight: #999999;--g-popup-actions: #1a1a1a;--g-weekend-highlight-color: #0f0f0f}.gantt-container{line-height:14.5px;position:relative;overflow:auto;font-size:12px;height:var(--gv-grid-height);width:100%;border-radius:8px;isolation:isolate}.gantt-container .popup-wrapper{position:absolute;top:0;left:0;background:var(--g-header-background);box-shadow:0 10px 24px -3px #0003;padding:10px;border-radius:5px;width:max-content;z-index:1000}.gantt-container .popup-wrapper .title{margin-bottom:2px;color:var(--g-text-dark);font-size:.85rem;font-weight:650;line-height:15px}.gantt-container .popup-wrapper .subtitle{color:var(--g-text-dark);font-size:.8rem;margin-bottom:5px}.gantt-container .popup-wrapper .details{color:var(--g-text-muted);font-size:.7rem}.gantt-container .popup-wrapper .actions{margin-top:10px;margin-left:3px}.gantt-container .popup-wrapper .action-btn{border:none;padding:5px 8px;background-color:var(--g-popup-actions);border-right:1px solid var(--g-border-color)}.gantt-container .popup-wrapper .action-btn:hover{filter:brightness(97%)}.gantt-container .popup-wrapper .action-btn:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px}.gantt-container .popup-wrapper .action-btn:last-child{border-right:none;border-top-right-radius:4px;border-bottom-right-radius:4px}.gantt-container .grid-header{height:calc(var(--gv-lower-header-height) + var(--gv-upper-header-height) + 10px);background-color:var(--g-header-background);position:sticky;top:0;left:0;border-bottom:1px solid var(--g-row-border-color);z-index:1000}.gantt-container .lower-text,.gantt-container .upper-text{text-anchor:middle}.gantt-container .upper-header{height:var(--gv-upper-header-height)}.gantt-container .lower-header{height:var(--gv-lower-header-height)}.gantt-container .lower-text{font-size:12px;position:absolute;width:calc(var(--gv-column-width) * .8);height:calc(var(--gv-lower-header-height) * .8);margin:0 calc(var(--gv-column-width) * .1);align-content:center;text-align:center;color:var(--g-text-muted)}.gantt-container .upper-text{position:absolute;width:fit-content;font-weight:500;font-size:14px;color:var(--g-text-dark);height:calc(var(--gv-lower-header-height) * .66)}.gantt-container .current-upper{position:sticky;left:0!important;padding-left:17px;background:var(--g-header-background)}.gantt-container .side-header{position:sticky;top:0;right:0;float:right;z-index:1000;line-height:20px;font-weight:400;width:max-content;margin-left:auto;padding-right:10px;padding-top:10px;background:var(--g-header-background);display:flex}.gantt-container .side-header *{transition-property:background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:var(--g-actions-background);border-radius:.5rem;border:none;padding:5px 8px;color:var(--g-text-dark);font-size:14px;letter-spacing:.02em;font-weight:420;box-sizing:content-box;margin-right:5px}.gantt-container .side-header *:last-child{margin-right:0}.gantt-container .side-header *:hover{filter:brightness(97.5%)}.gantt-container .side-header select{width:60px;padding-top:2px;padding-bottom:2px}.gantt-container .side-header select:focus{outline:none}.gantt-container .date-range-highlight{background-color:var(--g-progress-color);border-radius:12px;height:calc(var(--gv-lower-header-height) - 6px);top:calc(var(--gv-upper-header-height) + 5px);position:absolute}.gantt-container .current-highlight{position:absolute;background:var(--g-today-highlight);width:1px;z-index:999}.gantt-container .current-ball-highlight{position:absolute;background:var(--g-today-highlight);z-index:1001;border-radius:50%}.gantt-container .current-date-highlight{background:var(--g-today-highlight);color:var(--g-text-light);border-radius:5px}.gantt-container .holiday-label{position:absolute;top:0;left:0;opacity:0;z-index:1000;background:var(--g-weekend-label-color);border-radius:5px;padding:2px 5px}.gantt-container .holiday-label.show{opacity:100}.gantt-container .extras{position:sticky;left:0}.gantt-container .extras .adjust{position:absolute;left:8px;top:calc(var(--gv-grid-height) - 60px);background-color:var(--g-actions-background);color:var(--g-text-light);border:none;padding:8px;border-radius:3px}.gantt-container .hide{display:none}.gantt{user-select:none;-webkit-user-select:none;position:absolute}.gantt .grid-background{fill:none}.gantt .grid-row{fill:var(--g-row-color)}.gantt .row-line{stroke:var(--g-border-color)}.gantt .tick{stroke:var(--g-tick-color);stroke-width:.4}.gantt .tick.thick{stroke:var(--g-tick-color-thick);stroke-width:.7}.gantt .arrow{fill:none;stroke:var(--g-arrow-color);stroke-width:1.5}.gantt .bar-wrapper .bar{fill:var(--g-bar-color);stroke:var(--g-bar-border);stroke-width:0;transition:stroke-width .3s ease}.gantt .bar-progress{fill:var(--g-progress-color);border-radius:4px}.gantt .bar-expected-progress{fill:var(--g-expected-progress)}.gantt .bar-invalid{fill:transparent;stroke:var(--g-bar-border);stroke-width:1;stroke-dasharray:5}:is(.gantt .bar-invalid)~.bar-label{fill:var(--g-text-light)}.gantt .bar-label{fill:var(--g-text-dark);dominant-baseline:central;font-family:Helvetica;font-size:13px;font-weight:400}.gantt .bar-label.big{fill:var(--g-text-dark);text-anchor:start}.gantt .handle{fill:var(--g-handle-color);opacity:0;transition:opacity .3s ease}.gantt .handle.active,.gantt .handle.visible{cursor:ew-resize;opacity:1}.gantt .handle.progress{fill:var(--g-text-muted)}.gantt .bar-wrapper{cursor:pointer}.gantt .bar-wrapper .bar{outline:1px solid var(--g-row-border-color);border-radius:3px}.gantt .bar-wrapper:hover .bar{transition:transform .3s ease}.gantt .bar-wrapper:hover .date-range-highlight{display:block}.gantt .grid-column{fill:transparent;pointer-events:all}.gantt .grid-column:hover{fill:var(--g-weekend-highlight-color);transition:fill .1s ease}wuic-timeline-list{display:flex;flex:1 1 auto;width:100%;height:100%;min-height:0}wuic-timeline-list .wuic-timeline-wrapper{display:flex;flex-direction:column;flex:1 1 auto;width:100%;min-height:0}wuic-timeline-list .wuic-timeline-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.25rem .5rem;flex:0 0 auto}wuic-timeline-list .wuic-timeline-toolbar-left,wuic-timeline-list .wuic-timeline-toolbar-right{display:flex;align-items:center;gap:.25rem}wuic-timeline-list .wuic-timeline-mode-active{font-weight:700;text-decoration:underline}wuic-timeline-list .wuic-timeline-pending-badge{background:var(--p-primary-color, #3b82f6);color:var(--p-primary-contrast-color, #fff);border-radius:999px;font-size:.75rem;padding:.1rem .5rem}wuic-timeline-list .wuic-timeline-warning{display:flex;align-items:center;gap:.5rem;padding:.25rem .75rem;color:#92400e;background:#fef3c7;border-radius:4px;margin:0 .5rem .25rem;font-size:.85rem;flex:0 0 auto}wuic-timeline-list .wuic-timeline-gantt-host{flex:1 1 auto;min-height:0;overflow:auto}wuic-timeline-list .wuic-timeline-empty{padding:1.5rem;text-align:center;opacity:.7}wuic-timeline-list .gantt .bar-wrapper.wuic-timeline-milestone .bar{fill:#7c3aed;transform-box:fill-box;transform-origin:center;transform:rotate(45deg) scale(.72)}wuic-timeline-list .gantt .bar-wrapper.wuic-timeline-milestone .bar-progress{display:none}wuic-timeline-list .gantt .bar-wrapper.wuic-timeline-milestone .handle{display:none}\n"], dependencies: [{ kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i4.Button, selector: "p-button", inputs: ["hostName", "type", "badge", "disabled", "raised", "rounded", "text", "plain", "outlined", "link", "tabindex", "size", "variant", "style", "styleClass", "badgeClass", "badgeSeverity", "ariaLabel", "autofocus", "iconPos", "icon", "label", "loading", "loadingIcon", "severity", "buttonProps", "fluid"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: TranslateModule }, { kind: "pipe", type: i8.TranslatePipe, name: "translate" }], encapsulation: i0.ViewEncapsulation.None });
773
+ }
774
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: TimelineListComponent, decorators: [{
775
+ type: Component,
776
+ args: [{ selector: 'wuic-timeline-list', standalone: true, imports: [ButtonModule, TranslateModule], encapsulation: ViewEncapsulation.None, template: "<div class=\"wuic-timeline-wrapper\">\n @if (!hideToolbar) {\n <div class=\"wuic-timeline-toolbar\">\n <div class=\"wuic-timeline-toolbar-left\">\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-calendar\" [label]=\"'timeline.toolbar.day' | translate\"\n [styleClass]=\"viewMode === 'day' ? 'wuic-timeline-mode-active' : ''\"\n (onClick)=\"setViewMode('day')\" />\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-calendar\" [label]=\"'timeline.toolbar.week' | translate\"\n [styleClass]=\"viewMode === 'week' ? 'wuic-timeline-mode-active' : ''\"\n (onClick)=\"setViewMode('week')\" />\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-calendar\" [label]=\"'timeline.toolbar.month' | translate\"\n [styleClass]=\"viewMode === 'month' ? 'wuic-timeline-mode-active' : ''\"\n (onClick)=\"setViewMode('month')\" />\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-map-marker\" [label]=\"'timeline.toolbar.today' | translate\"\n (onClick)=\"scrollToToday()\" />\n <p-button [text]=\"true\" size=\"small\" icon=\"pi pi-refresh\" (onClick)=\"refresh()\" />\n </div>\n @if (isBatchMode) {\n <div class=\"wuic-timeline-toolbar-right\">\n <p-button size=\"small\" icon=\"pi pi-check\" [label]=\"'timeline.toolbar.save' | translate\"\n [disabled]=\"!hasPendingChanges || savingBatch\" [loading]=\"savingBatch\"\n (onClick)=\"saveBatchChanges()\" />\n <p-button size=\"small\" severity=\"secondary\" icon=\"pi pi-times\" [label]=\"'timeline.toolbar.discard' | translate\"\n [disabled]=\"!hasPendingChanges || savingBatch\"\n (onClick)=\"cancelBatchChanges()\" />\n @if (hasPendingChanges) {\n <span class=\"wuic-timeline-pending-badge\">{{ pendingCount }}</span>\n }\n </div>\n }\n </div>\n }\n\n @if (warningMessage) {\n <div class=\"wuic-timeline-warning\">\n <i class=\"pi pi-exclamation-triangle\"></i>\n <span>{{ warningMessage }}</span>\n </div>\n }\n\n <!-- Il context menu del browser sull'area di disegno interferisce con\n drag/resize delle barre (un right-click accidentale durante il drag lo\n apre): sopprimiamo l'evento sull'host, non sulla toolbar. -->\n <div class=\"wuic-timeline-gantt-host\" #ganttHost (contextmenu)=\"onGanttContextMenu($event)\"></div>\n\n @if (!tasks.length && !warningMessage) {\n <div class=\"wuic-timeline-empty\">{{ 'timeline.empty' | translate }}</div>\n }\n</div>\n", styles: [":root{--g-arrow-color: #1f2937;--g-bar-color: #fff;--g-bar-border: #fff;--g-tick-color-thick: #ededed;--g-tick-color: #f3f3f3;--g-actions-background: #f3f3f3;--g-border-color: #ebeff2;--g-text-muted: #7c7c7c;--g-text-light: #fff;--g-text-dark: #171717;--g-progress-color: #dbdbdb;--g-handle-color: #37352f;--g-weekend-label-color: #dcdce4;--g-expected-progress: #c4c4e9;--g-header-background: #fff;--g-row-color: #fdfdfd;--g-row-border-color: #c7c7c7;--g-today-highlight: #37352f;--g-popup-actions: #ebeff2;--g-weekend-highlight-color: #f7f7f7}html[data-theme=dark]{--g-arrow-color: #e5e5e5;--g-bar-color: #2a2a2a;--g-bar-border: #3a3a3a;--g-tick-color-thick: #1a1a1a;--g-tick-color: #0a0a0a;--g-actions-background: #1a1a1a;--g-border-color: #2a2a2a;--g-text-muted: #999999;--g-text-light: #ffffff;--g-text-dark: #f5f5f5;--g-progress-color: #4a4a4a;--g-handle-color: #d0d0d0;--g-weekend-label-color: #3a3a3a;--g-expected-progress: #5a5a5a;--g-header-background: #0a0a0a;--g-row-color: #141414;--g-row-border-color: #2a2a2a;--g-today-highlight: #999999;--g-popup-actions: #1a1a1a;--g-weekend-highlight-color: #0f0f0f}.gantt-container{line-height:14.5px;position:relative;overflow:auto;font-size:12px;height:var(--gv-grid-height);width:100%;border-radius:8px;isolation:isolate}.gantt-container .popup-wrapper{position:absolute;top:0;left:0;background:var(--g-header-background);box-shadow:0 10px 24px -3px #0003;padding:10px;border-radius:5px;width:max-content;z-index:1000}.gantt-container .popup-wrapper .title{margin-bottom:2px;color:var(--g-text-dark);font-size:.85rem;font-weight:650;line-height:15px}.gantt-container .popup-wrapper .subtitle{color:var(--g-text-dark);font-size:.8rem;margin-bottom:5px}.gantt-container .popup-wrapper .details{color:var(--g-text-muted);font-size:.7rem}.gantt-container .popup-wrapper .actions{margin-top:10px;margin-left:3px}.gantt-container .popup-wrapper .action-btn{border:none;padding:5px 8px;background-color:var(--g-popup-actions);border-right:1px solid var(--g-border-color)}.gantt-container .popup-wrapper .action-btn:hover{filter:brightness(97%)}.gantt-container .popup-wrapper .action-btn:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px}.gantt-container .popup-wrapper .action-btn:last-child{border-right:none;border-top-right-radius:4px;border-bottom-right-radius:4px}.gantt-container .grid-header{height:calc(var(--gv-lower-header-height) + var(--gv-upper-header-height) + 10px);background-color:var(--g-header-background);position:sticky;top:0;left:0;border-bottom:1px solid var(--g-row-border-color);z-index:1000}.gantt-container .lower-text,.gantt-container .upper-text{text-anchor:middle}.gantt-container .upper-header{height:var(--gv-upper-header-height)}.gantt-container .lower-header{height:var(--gv-lower-header-height)}.gantt-container .lower-text{font-size:12px;position:absolute;width:calc(var(--gv-column-width) * .8);height:calc(var(--gv-lower-header-height) * .8);margin:0 calc(var(--gv-column-width) * .1);align-content:center;text-align:center;color:var(--g-text-muted)}.gantt-container .upper-text{position:absolute;width:fit-content;font-weight:500;font-size:14px;color:var(--g-text-dark);height:calc(var(--gv-lower-header-height) * .66)}.gantt-container .current-upper{position:sticky;left:0!important;padding-left:17px;background:var(--g-header-background)}.gantt-container .side-header{position:sticky;top:0;right:0;float:right;z-index:1000;line-height:20px;font-weight:400;width:max-content;margin-left:auto;padding-right:10px;padding-top:10px;background:var(--g-header-background);display:flex}.gantt-container .side-header *{transition-property:background-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:var(--g-actions-background);border-radius:.5rem;border:none;padding:5px 8px;color:var(--g-text-dark);font-size:14px;letter-spacing:.02em;font-weight:420;box-sizing:content-box;margin-right:5px}.gantt-container .side-header *:last-child{margin-right:0}.gantt-container .side-header *:hover{filter:brightness(97.5%)}.gantt-container .side-header select{width:60px;padding-top:2px;padding-bottom:2px}.gantt-container .side-header select:focus{outline:none}.gantt-container .date-range-highlight{background-color:var(--g-progress-color);border-radius:12px;height:calc(var(--gv-lower-header-height) - 6px);top:calc(var(--gv-upper-header-height) + 5px);position:absolute}.gantt-container .current-highlight{position:absolute;background:var(--g-today-highlight);width:1px;z-index:999}.gantt-container .current-ball-highlight{position:absolute;background:var(--g-today-highlight);z-index:1001;border-radius:50%}.gantt-container .current-date-highlight{background:var(--g-today-highlight);color:var(--g-text-light);border-radius:5px}.gantt-container .holiday-label{position:absolute;top:0;left:0;opacity:0;z-index:1000;background:var(--g-weekend-label-color);border-radius:5px;padding:2px 5px}.gantt-container .holiday-label.show{opacity:100}.gantt-container .extras{position:sticky;left:0}.gantt-container .extras .adjust{position:absolute;left:8px;top:calc(var(--gv-grid-height) - 60px);background-color:var(--g-actions-background);color:var(--g-text-light);border:none;padding:8px;border-radius:3px}.gantt-container .hide{display:none}.gantt{user-select:none;-webkit-user-select:none;position:absolute}.gantt .grid-background{fill:none}.gantt .grid-row{fill:var(--g-row-color)}.gantt .row-line{stroke:var(--g-border-color)}.gantt .tick{stroke:var(--g-tick-color);stroke-width:.4}.gantt .tick.thick{stroke:var(--g-tick-color-thick);stroke-width:.7}.gantt .arrow{fill:none;stroke:var(--g-arrow-color);stroke-width:1.5}.gantt .bar-wrapper .bar{fill:var(--g-bar-color);stroke:var(--g-bar-border);stroke-width:0;transition:stroke-width .3s ease}.gantt .bar-progress{fill:var(--g-progress-color);border-radius:4px}.gantt .bar-expected-progress{fill:var(--g-expected-progress)}.gantt .bar-invalid{fill:transparent;stroke:var(--g-bar-border);stroke-width:1;stroke-dasharray:5}:is(.gantt .bar-invalid)~.bar-label{fill:var(--g-text-light)}.gantt .bar-label{fill:var(--g-text-dark);dominant-baseline:central;font-family:Helvetica;font-size:13px;font-weight:400}.gantt .bar-label.big{fill:var(--g-text-dark);text-anchor:start}.gantt .handle{fill:var(--g-handle-color);opacity:0;transition:opacity .3s ease}.gantt .handle.active,.gantt .handle.visible{cursor:ew-resize;opacity:1}.gantt .handle.progress{fill:var(--g-text-muted)}.gantt .bar-wrapper{cursor:pointer}.gantt .bar-wrapper .bar{outline:1px solid var(--g-row-border-color);border-radius:3px}.gantt .bar-wrapper:hover .bar{transition:transform .3s ease}.gantt .bar-wrapper:hover .date-range-highlight{display:block}.gantt .grid-column{fill:transparent;pointer-events:all}.gantt .grid-column:hover{fill:var(--g-weekend-highlight-color);transition:fill .1s ease}wuic-timeline-list{display:flex;flex:1 1 auto;width:100%;height:100%;min-height:0}wuic-timeline-list .wuic-timeline-wrapper{display:flex;flex-direction:column;flex:1 1 auto;width:100%;min-height:0}wuic-timeline-list .wuic-timeline-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.25rem .5rem;flex:0 0 auto}wuic-timeline-list .wuic-timeline-toolbar-left,wuic-timeline-list .wuic-timeline-toolbar-right{display:flex;align-items:center;gap:.25rem}wuic-timeline-list .wuic-timeline-mode-active{font-weight:700;text-decoration:underline}wuic-timeline-list .wuic-timeline-pending-badge{background:var(--p-primary-color, #3b82f6);color:var(--p-primary-contrast-color, #fff);border-radius:999px;font-size:.75rem;padding:.1rem .5rem}wuic-timeline-list .wuic-timeline-warning{display:flex;align-items:center;gap:.5rem;padding:.25rem .75rem;color:#92400e;background:#fef3c7;border-radius:4px;margin:0 .5rem .25rem;font-size:.85rem;flex:0 0 auto}wuic-timeline-list .wuic-timeline-gantt-host{flex:1 1 auto;min-height:0;overflow:auto}wuic-timeline-list .wuic-timeline-empty{padding:1.5rem;text-align:center;opacity:.7}wuic-timeline-list .gantt .bar-wrapper.wuic-timeline-milestone .bar{fill:#7c3aed;transform-box:fill-box;transform-origin:center;transform:rotate(45deg) scale(.72)}wuic-timeline-list .gantt .bar-wrapper.wuic-timeline-milestone .bar-progress{display:none}wuic-timeline-list .gantt .bar-wrapper.wuic-timeline-milestone .handle{display:none}\n"] }]
777
+ }], ctorParameters: () => [{ type: i1.Router }, { type: TranslationManagerService }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }], propDecorators: { hardcodedRoute: [{
778
+ type: Input
779
+ }], parentRecord: [{
780
+ type: Input
781
+ }], parentMetaInfo: [{
782
+ type: Input
783
+ }], datasource: [{
784
+ type: Input
785
+ }], hardcodedDatasource: [{
786
+ type: Input
787
+ }], hideToolbar: [{
788
+ type: Input
789
+ }], onTimelineDataBound: [{
790
+ type: Output
791
+ }], onTimelineTaskChange: [{
792
+ type: Output
793
+ }], onTimelineTaskClick: [{
794
+ type: Output
795
+ }], onTimelineBatchSave: [{
796
+ type: Output
797
+ }], onTimelineBatchCancel: [{
798
+ type: Output
799
+ }], ganttHost: [{
800
+ type: ViewChild,
801
+ args: ['ganttHost', { static: true }]
802
+ }] } });
803
+
804
+ export { TimelineListComponent };
805
+ //# sourceMappingURL=wuic-framework-lib-timeline-list.component-aqCH4P0G.mjs.map