wuic-framework-lib 1.5.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/{wuic-framework-lib-chart-list.component-0J1ghIrS.mjs → wuic-framework-lib-chart-list.component-Dq9IEKsY.mjs} +50 -38
- package/fesm2022/{wuic-framework-lib-code-editor.component-C21mvjyr.mjs → wuic-framework-lib-code-editor.component-B6IpTSo8.mjs} +174 -274
- package/fesm2022/{wuic-framework-lib-designer.component-x10ZzUEy.mjs → wuic-framework-lib-designer.component-D6cbqNFg.mjs} +499 -581
- package/fesm2022/wuic-framework-lib-first-run-wizard.component-D1ZQUl-q.mjs +1630 -0
- package/fesm2022/{wuic-framework-lib-framework-docs.component-xTGLbEnp.mjs → wuic-framework-lib-framework-docs.component-C_o5r7pg.mjs} +4320 -1255
- package/fesm2022/{wuic-framework-lib-pivot-builder.component-BkFFjPm4.mjs → wuic-framework-lib-pivot-builder.component-DkWGzJpr.mjs} +580 -68
- package/fesm2022/{wuic-framework-lib-prompt-dialog-fallback-host.component-Bi-TrJB6.mjs → wuic-framework-lib-prompt-dialog-fallback-host.component-D29go5yA.mjs} +2 -2
- package/fesm2022/wuic-framework-lib-report-designer.component-DcpCWghx.mjs +380 -0
- package/fesm2022/wuic-framework-lib-report-scaffold-dialog.component-CgUjG7Bi.mjs +431 -0
- package/fesm2022/{wuic-framework-lib-report-viewer.component-BR-BfI2r.mjs → wuic-framework-lib-report-viewer.component-C4Opgl_M.mjs} +8 -13
- package/fesm2022/{wuic-framework-lib-scene3d-designer.component-BQzFxSXP.mjs → wuic-framework-lib-scene3d-designer.component-8l3vsyEb.mjs} +323 -88
- package/fesm2022/{wuic-framework-lib-scene3d-light-baker-gpu-0LjqhSNn.mjs → wuic-framework-lib-scene3d-light-baker-gpu-C1w-8Aai.mjs} +2 -2
- package/fesm2022/{wuic-framework-lib-scene3d-shader-effect-CQLWkuh1.mjs → wuic-framework-lib-scene3d-shader-effect-DjHyCAFK.mjs} +612 -32
- package/fesm2022/{wuic-framework-lib-scene3d-viewer.component-imoV3HOs.mjs → wuic-framework-lib-scene3d-viewer.component-nMSl5jbe.mjs} +149 -18
- package/fesm2022/{wuic-framework-lib-scheduler-list.component-xzlTEkoq.mjs → wuic-framework-lib-scheduler-list.component-Cvk3Zg0Q.mjs} +169 -53
- package/fesm2022/{wuic-framework-lib-spreadsheet-list-sf.component-CrS6fOGj.mjs → wuic-framework-lib-spreadsheet-list-sf.component-C1U7JPiq.mjs} +149 -108
- package/fesm2022/{wuic-framework-lib-stimulsoft-license.service-BIyxB4pO.mjs → wuic-framework-lib-stimulsoft-license.service-CsNNSFYs.mjs} +3 -3
- package/fesm2022/wuic-framework-lib-timeline-list.component-cZbPgNh1.mjs +805 -0
- package/fesm2022/{wuic-framework-lib-workflow-designer.component-5xtQ7-6z.mjs → wuic-framework-lib-workflow-designer.component-C_4RQGa0.mjs} +180 -150
- package/fesm2022/{wuic-framework-lib-wuic-framework-lib-3TZ-j-yL.mjs → wuic-framework-lib-wuic-framework-lib-BZZwta8b.mjs} +6785 -2347
- package/fesm2022/wuic-framework-lib.mjs +1 -1
- package/package.json +21 -2
- package/styles/_wuic-tokens.scss +82 -0
- package/styles/styles.scss +160 -0
- package/types/wuic-framework-lib.d.ts +4269 -2691
- package/fesm2022/wuic-framework-lib-first-run-wizard.component-BuSxs_CM.mjs +0 -1316
- 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-BZZwta8b.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-BZZwta8b.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-BZZwta8b.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-cZbPgNh1.mjs.map
|