@memberjunction/ng-tasks 2.105.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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +10 -0
- package/README.md +233 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/components/gantt-task-viewer.component.d.ts +44 -0
- package/dist/lib/components/gantt-task-viewer.component.d.ts.map +1 -0
- package/dist/lib/components/gantt-task-viewer.component.js +461 -0
- package/dist/lib/components/gantt-task-viewer.component.js.map +1 -0
- package/dist/lib/components/simple-task-viewer.component.d.ts +36 -0
- package/dist/lib/components/simple-task-viewer.component.d.ts.map +1 -0
- package/dist/lib/components/simple-task-viewer.component.js +275 -0
- package/dist/lib/components/simple-task-viewer.component.js.map +1 -0
- package/dist/lib/components/task-detail-panel.component.d.ts +26 -0
- package/dist/lib/components/task-detail-panel.component.d.ts.map +1 -0
- package/dist/lib/components/task-detail-panel.component.js +294 -0
- package/dist/lib/components/task-detail-panel.component.js.map +1 -0
- package/dist/lib/components/task.component.d.ts +34 -0
- package/dist/lib/components/task.component.d.ts.map +1 -0
- package/dist/lib/components/task.component.js +201 -0
- package/dist/lib/components/task.component.js.map +1 -0
- package/dist/lib/models/task-view.models.d.ts +17 -0
- package/dist/lib/models/task-view.models.d.ts.map +1 -0
- package/dist/lib/models/task-view.models.js +2 -0
- package/dist/lib/models/task-view.models.js.map +1 -0
- package/dist/lib/ng-tasks.module.d.ts +14 -0
- package/dist/lib/ng-tasks.module.d.ts.map +1 -0
- package/dist/lib/ng-tasks.module.js +37 -0
- package/dist/lib/ng-tasks.module.js.map +1 -0
- package/dist/public-api.d.ts +9 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +12 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +31 -0
- package/src/index.ts +1 -0
- package/src/lib/components/gantt-task-viewer.component.ts +524 -0
- package/src/lib/components/simple-task-viewer.component.ts +356 -0
- package/src/lib/components/task-detail-panel.component.ts +304 -0
- package/src/lib/components/task.component.ts +175 -0
- package/src/lib/models/task-view.models.ts +19 -0
- package/src/lib/ng-tasks.module.ts +22 -0
- package/src/lib/types/frappe-gantt.d.ts +32 -0
- package/src/public-api.ts +14 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { Component, Input, Output, EventEmitter, OnChanges, AfterViewInit, ElementRef, ViewChild, OnDestroy, HostListener } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { TaskEntity, TaskDependencyEntity } from '@memberjunction/core-entities';
|
|
4
|
+
import { gantt } from 'dhtmlx-gantt';
|
|
5
|
+
import { TaskDetailPanelComponent } from './task-detail-panel.component';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Gantt chart view for tasks using DHTMLX Gantt
|
|
9
|
+
*/
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'mj-gantt-task-viewer',
|
|
12
|
+
standalone: true,
|
|
13
|
+
imports: [CommonModule, TaskDetailPanelComponent],
|
|
14
|
+
template: `
|
|
15
|
+
<div class="gantt-task-viewer">
|
|
16
|
+
<div *ngIf="!tasks || tasks.length === 0" class="no-tasks">
|
|
17
|
+
<i class="fas fa-chart-gantt"></i>
|
|
18
|
+
<p>No tasks to display in Gantt view</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div *ngIf="tasks && tasks.length > 0" class="gantt-layout">
|
|
22
|
+
<div #ganttContainer class="gantt-container"></div>
|
|
23
|
+
|
|
24
|
+
<div *ngIf="selectedTask" class="gantt-resizer"
|
|
25
|
+
(mousedown)="startResize($event)"></div>
|
|
26
|
+
|
|
27
|
+
<div *ngIf="selectedTask" class="task-detail-panel" [style.width.px]="detailPanelWidth">
|
|
28
|
+
<mj-task-detail-panel
|
|
29
|
+
[task]="selectedTask"
|
|
30
|
+
[agentRunId]="getAgentRunId(selectedTask)"
|
|
31
|
+
(closePanel)="closeDetailPanel()"
|
|
32
|
+
(openEntityRecord)="onOpenEntityRecord($event)">
|
|
33
|
+
</mj-task-detail-panel>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
`,
|
|
38
|
+
styles: [`
|
|
39
|
+
.gantt-task-viewer {
|
|
40
|
+
height: 100%;
|
|
41
|
+
background: white;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.gantt-layout {
|
|
48
|
+
display: flex;
|
|
49
|
+
height: 600px;
|
|
50
|
+
position: relative;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.gantt-container {
|
|
54
|
+
flex: 1;
|
|
55
|
+
min-width: 400px;
|
|
56
|
+
height: 100%;
|
|
57
|
+
position: relative;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.gantt-resizer {
|
|
61
|
+
width: 4px;
|
|
62
|
+
background: #E5E7EB;
|
|
63
|
+
cursor: col-resize;
|
|
64
|
+
flex-shrink: 0;
|
|
65
|
+
transition: background 0.2s;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.gantt-resizer:hover {
|
|
69
|
+
background: #3B82F6;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.task-detail-panel {
|
|
73
|
+
min-width: 300px;
|
|
74
|
+
max-width: 600px;
|
|
75
|
+
height: 100%;
|
|
76
|
+
border-left: 1px solid #E5E7EB;
|
|
77
|
+
flex-shrink: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Override DHTMLX Gantt default styles */
|
|
81
|
+
:host ::ng-deep .gantt_container {
|
|
82
|
+
font-family: inherit;
|
|
83
|
+
font-size: 13px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
:host ::ng-deep .gantt_grid_scale,
|
|
87
|
+
:host ::ng-deep .gantt_task_scale {
|
|
88
|
+
background: #F9FAFB;
|
|
89
|
+
border-bottom: 2px solid #E5E7EB;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
:host ::ng-deep .gantt_task .gantt_task_content {
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.no-tasks {
|
|
97
|
+
text-align: center;
|
|
98
|
+
padding: 80px 20px;
|
|
99
|
+
color: #9CA3AF;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.no-tasks i {
|
|
103
|
+
font-size: 64px;
|
|
104
|
+
opacity: 0.3;
|
|
105
|
+
margin-bottom: 16px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.no-tasks p {
|
|
109
|
+
margin: 0;
|
|
110
|
+
font-size: 14px;
|
|
111
|
+
}
|
|
112
|
+
`]
|
|
113
|
+
})
|
|
114
|
+
export class GanttTaskViewerComponent implements OnChanges, AfterViewInit, OnDestroy {
|
|
115
|
+
@Input() tasks: TaskEntity[] = [];
|
|
116
|
+
@Input() taskDependencies: TaskDependencyEntity[] = [];
|
|
117
|
+
@Input() agentRunMap?: Map<string, string>; // Maps TaskID -> AgentRunID
|
|
118
|
+
@Output() taskClicked = new EventEmitter<TaskEntity>();
|
|
119
|
+
@Output() openEntityRecord = new EventEmitter<{ entityName: string; recordId: string }>();
|
|
120
|
+
|
|
121
|
+
@ViewChild('ganttContainer', { static: false }) ganttContainer!: ElementRef<HTMLDivElement>;
|
|
122
|
+
|
|
123
|
+
public selectedTask: TaskEntity | null = null;
|
|
124
|
+
public detailPanelWidth: number = 400;
|
|
125
|
+
|
|
126
|
+
private ganttInitialized = false;
|
|
127
|
+
private isResizing = false;
|
|
128
|
+
private resizeStartX = 0;
|
|
129
|
+
private resizeStartWidth = 0;
|
|
130
|
+
|
|
131
|
+
ngAfterViewInit() {
|
|
132
|
+
console.log('🔧 ngAfterViewInit called', {
|
|
133
|
+
taskCount: this.tasks?.length || 0,
|
|
134
|
+
hasContainer: !!this.ganttContainer
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (this.tasks && this.tasks.length > 0 && this.ganttContainer) {
|
|
138
|
+
this.initGantt();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
ngOnChanges() {
|
|
143
|
+
console.log('🔄 ngOnChanges called', {
|
|
144
|
+
initialized: this.ganttInitialized,
|
|
145
|
+
hasContainer: !!this.ganttContainer,
|
|
146
|
+
taskCount: this.tasks?.length || 0
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (this.ganttInitialized && this.ganttContainer) {
|
|
150
|
+
this.updateGanttData();
|
|
151
|
+
} else if (!this.ganttInitialized && this.ganttContainer && this.tasks && this.tasks.length > 0) {
|
|
152
|
+
// Initialize if we have container and tasks but haven't initialized yet
|
|
153
|
+
console.log('🎨 Late initialization - gantt not initialized but container and tasks available');
|
|
154
|
+
this.initGantt();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
ngOnDestroy() {
|
|
159
|
+
if (this.ganttInitialized) {
|
|
160
|
+
gantt.clearAll();
|
|
161
|
+
this.ganttInitialized = false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private initGantt(): void {
|
|
166
|
+
try {
|
|
167
|
+
console.log('🎨 Initializing DHTMLX Gantt');
|
|
168
|
+
|
|
169
|
+
// IMPORTANT: Clear any previous configuration
|
|
170
|
+
gantt.clearAll();
|
|
171
|
+
|
|
172
|
+
// Configure Gantt layout and appearance
|
|
173
|
+
gantt.config.date_format = '%Y-%m-%d %H:%i';
|
|
174
|
+
gantt.config.scale_unit = 'day';
|
|
175
|
+
gantt.config.date_scale = '%d %M';
|
|
176
|
+
gantt.config.subscales = [];
|
|
177
|
+
gantt.config.show_progress = true;
|
|
178
|
+
gantt.config.show_links = true;
|
|
179
|
+
gantt.config.auto_types = true;
|
|
180
|
+
gantt.config.readonly = true; // Read-only for now
|
|
181
|
+
gantt.config.fit_tasks = true; // Auto-fit timeline to tasks
|
|
182
|
+
|
|
183
|
+
// Disable auto-scheduling - we calculate dates ourselves based on dependencies
|
|
184
|
+
gantt.config.auto_scheduling = false;
|
|
185
|
+
|
|
186
|
+
// Grid configuration
|
|
187
|
+
gantt.config.grid_width = 350;
|
|
188
|
+
gantt.config.row_height = 36;
|
|
189
|
+
gantt.config.scale_height = 0; // Hide date scale - dates are just for positioning
|
|
190
|
+
|
|
191
|
+
// Layout configuration - ensure timeline is visible
|
|
192
|
+
gantt.config.layout = {
|
|
193
|
+
css: "gantt_container",
|
|
194
|
+
rows: [
|
|
195
|
+
{
|
|
196
|
+
cols: [
|
|
197
|
+
{ view: "grid", group: "grids", scrollY: "scrollVer" },
|
|
198
|
+
{ resizer: true, width: 1 },
|
|
199
|
+
{ view: "timeline", scrollX: "scrollHor", scrollY: "scrollVer" },
|
|
200
|
+
{ view: "scrollbar", id: "scrollVer", group: "vertical" }
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
{ view: "scrollbar", id: "scrollHor", group: "horizontal" }
|
|
204
|
+
]
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Column configuration - only show task names, hide dates
|
|
208
|
+
gantt.config.columns = [
|
|
209
|
+
{ name: 'text', label: 'Task name', tree: true, width: '*' }
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
// Initialize Gantt in the container
|
|
213
|
+
gantt.init(this.ganttContainer.nativeElement);
|
|
214
|
+
this.ganttInitialized = true;
|
|
215
|
+
|
|
216
|
+
// Attach click event
|
|
217
|
+
gantt.attachEvent('onTaskClick', (id: string) => {
|
|
218
|
+
const originalTask = this.tasks.find(t => t.ID === id);
|
|
219
|
+
if (originalTask) {
|
|
220
|
+
this.selectedTask = originalTask;
|
|
221
|
+
this.taskClicked.emit(originalTask);
|
|
222
|
+
}
|
|
223
|
+
return true;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Load data
|
|
227
|
+
this.updateGanttData();
|
|
228
|
+
|
|
229
|
+
// Force resize after data load to ensure proper rendering
|
|
230
|
+
setTimeout(() => {
|
|
231
|
+
gantt.setSizes();
|
|
232
|
+
}, 0);
|
|
233
|
+
|
|
234
|
+
// Expand and select after render completes
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
this.expandAllAndSelectRoot();
|
|
237
|
+
}, 100);
|
|
238
|
+
|
|
239
|
+
console.log('✅ DHTMLX Gantt initialized successfully');
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('❌ Error initializing Gantt:', error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private updateGanttData(): void {
|
|
246
|
+
if (!this.ganttInitialized) return;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
console.log('📊 Updating Gantt data with', this.tasks.length, 'tasks');
|
|
250
|
+
|
|
251
|
+
const ganttData = this.convertToGanttFormat(this.tasks);
|
|
252
|
+
gantt.clearAll();
|
|
253
|
+
gantt.parse(ganttData);
|
|
254
|
+
|
|
255
|
+
// Log final parsed data for debugging
|
|
256
|
+
console.log('📋 Gantt data after parse:', {
|
|
257
|
+
tasks: gantt.getTaskByTime(),
|
|
258
|
+
links: gantt.getLinks()
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Force resize after data update
|
|
262
|
+
setTimeout(() => {
|
|
263
|
+
gantt.setSizes();
|
|
264
|
+
}, 0);
|
|
265
|
+
|
|
266
|
+
// Expand and select after render completes
|
|
267
|
+
setTimeout(() => {
|
|
268
|
+
this.expandAllAndSelectRoot();
|
|
269
|
+
}, 100);
|
|
270
|
+
|
|
271
|
+
console.log('✅ Gantt data updated');
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error('❌ Error updating Gantt data:', error);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private convertToGanttFormat(tasks: TaskEntity[]): { data: any[], links: any[] } {
|
|
278
|
+
const data: any[] = [];
|
|
279
|
+
const links: any[] = [];
|
|
280
|
+
|
|
281
|
+
console.log('🔍 Converting tasks:', tasks);
|
|
282
|
+
console.log('🔗 Task dependencies:', this.taskDependencies);
|
|
283
|
+
|
|
284
|
+
// Build a map of task ID to task for quick lookup
|
|
285
|
+
const taskMap = new Map<string, TaskEntity>();
|
|
286
|
+
tasks.forEach(t => taskMap.set(t.ID, t));
|
|
287
|
+
|
|
288
|
+
// Build dependency map: taskId -> array of tasks it depends on
|
|
289
|
+
const dependencyMap = new Map<string, string[]>();
|
|
290
|
+
this.taskDependencies.forEach(dep => {
|
|
291
|
+
if (!dependencyMap.has(dep.TaskID)) {
|
|
292
|
+
dependencyMap.set(dep.TaskID, []);
|
|
293
|
+
}
|
|
294
|
+
dependencyMap.get(dep.TaskID)!.push(dep.DependsOnTaskID);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Calculate start dates based on dependencies
|
|
298
|
+
const taskStartDates = new Map<string, Date>();
|
|
299
|
+
const baseDate = new Date();
|
|
300
|
+
baseDate.setHours(0, 0, 0, 0);
|
|
301
|
+
|
|
302
|
+
// Recursive function to calculate start date for a task
|
|
303
|
+
const calculateStartDate = (taskId: string, visited = new Set<string>()): Date => {
|
|
304
|
+
// Prevent circular dependencies
|
|
305
|
+
if (visited.has(taskId)) {
|
|
306
|
+
return new Date(baseDate);
|
|
307
|
+
}
|
|
308
|
+
visited.add(taskId);
|
|
309
|
+
|
|
310
|
+
// If already calculated, return it
|
|
311
|
+
if (taskStartDates.has(taskId)) {
|
|
312
|
+
return taskStartDates.get(taskId)!;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const task = taskMap.get(taskId);
|
|
316
|
+
if (!task) return new Date(baseDate);
|
|
317
|
+
|
|
318
|
+
const dependencies = dependencyMap.get(taskId) || [];
|
|
319
|
+
|
|
320
|
+
if (dependencies.length === 0) {
|
|
321
|
+
// No dependencies - use base date or task's actual start date
|
|
322
|
+
const startDate = task.StartedAt ? new Date(task.StartedAt) : new Date(baseDate);
|
|
323
|
+
taskStartDates.set(taskId, startDate);
|
|
324
|
+
return startDate;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Has dependencies - start after the latest dependency ends
|
|
328
|
+
let latestEnd = new Date(baseDate);
|
|
329
|
+
for (const depId of dependencies) {
|
|
330
|
+
const depTask = taskMap.get(depId);
|
|
331
|
+
if (depTask) {
|
|
332
|
+
const depStart = calculateStartDate(depId, new Set(visited));
|
|
333
|
+
const depDuration = this.calculateDuration(depTask);
|
|
334
|
+
const depEnd = new Date(depStart);
|
|
335
|
+
depEnd.setDate(depEnd.getDate() + depDuration);
|
|
336
|
+
|
|
337
|
+
if (depEnd > latestEnd) {
|
|
338
|
+
latestEnd = depEnd;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
taskStartDates.set(taskId, latestEnd);
|
|
344
|
+
return latestEnd;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Calculate start dates for all tasks
|
|
348
|
+
tasks.forEach(task => calculateStartDate(task.ID));
|
|
349
|
+
|
|
350
|
+
// Calculate min/max dates for timeline display range
|
|
351
|
+
let minDate: Date | null = null;
|
|
352
|
+
let maxDate: Date | null = null;
|
|
353
|
+
|
|
354
|
+
tasks.forEach(task => {
|
|
355
|
+
const startDate = taskStartDates.get(task.ID);
|
|
356
|
+
if (startDate) {
|
|
357
|
+
const duration = this.calculateDuration(task);
|
|
358
|
+
const endDate = new Date(startDate);
|
|
359
|
+
endDate.setDate(endDate.getDate() + duration);
|
|
360
|
+
|
|
361
|
+
if (!minDate || startDate < minDate) {
|
|
362
|
+
minDate = startDate;
|
|
363
|
+
}
|
|
364
|
+
if (!maxDate || endDate > maxDate) {
|
|
365
|
+
maxDate = endDate;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Add 1 day padding before and after
|
|
371
|
+
if (minDate) {
|
|
372
|
+
const paddedStart = new Date(minDate);
|
|
373
|
+
paddedStart.setDate(paddedStart.getDate() - 1);
|
|
374
|
+
gantt.config.start_date = paddedStart;
|
|
375
|
+
}
|
|
376
|
+
if (maxDate) {
|
|
377
|
+
const paddedEnd = new Date(maxDate);
|
|
378
|
+
paddedEnd.setDate(paddedEnd.getDate() + 1);
|
|
379
|
+
gantt.config.end_date = paddedEnd;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Now create Gantt tasks with calculated dates
|
|
383
|
+
tasks.forEach((task) => {
|
|
384
|
+
console.log('📝 Processing task:', {
|
|
385
|
+
ID: task.ID,
|
|
386
|
+
Name: task.Name,
|
|
387
|
+
ParentID: task.ParentID
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// Calculate progress (0-1 scale for DHTMLX)
|
|
391
|
+
let progress = (task.PercentComplete || 0) / 100;
|
|
392
|
+
if (task.Status === 'Complete') {
|
|
393
|
+
progress = 1;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const duration = this.calculateDuration(task);
|
|
397
|
+
const startDate = taskStartDates.get(task.ID) || new Date(baseDate);
|
|
398
|
+
|
|
399
|
+
const ganttTask: any = {
|
|
400
|
+
id: task.ID,
|
|
401
|
+
text: task.Name || 'Untitled Task',
|
|
402
|
+
start_date: this.formatDateForDHTMLX(startDate),
|
|
403
|
+
duration: duration,
|
|
404
|
+
progress: progress
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// Add parent relationship for tree structure (not dependency)
|
|
408
|
+
if (task.ParentID) {
|
|
409
|
+
ganttTask.parent = task.ParentID;
|
|
410
|
+
} else {
|
|
411
|
+
// Root tasks need parent: 0
|
|
412
|
+
ganttTask.parent = 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
console.log('✅ Created Gantt task:', ganttTask);
|
|
416
|
+
data.push(ganttTask);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Create links from TaskDependencyEntity records
|
|
420
|
+
this.taskDependencies.forEach((dep, index) => {
|
|
421
|
+
links.push({
|
|
422
|
+
id: dep.ID || `link_${index}`,
|
|
423
|
+
source: dep.DependsOnTaskID, // The task being depended on
|
|
424
|
+
target: dep.TaskID, // The task that depends on it
|
|
425
|
+
type: '0' // finish-to-start (DHTMLX type 0)
|
|
426
|
+
});
|
|
427
|
+
console.log(`🔗 Created link: ${dep.DependsOnTaskID} -> ${dep.TaskID}`);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
console.log('📊 Final DHTMLX format:', { data, links });
|
|
431
|
+
return { data, links };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private calculateDuration(task: TaskEntity): number {
|
|
435
|
+
if (task.StartedAt && task.DueAt) {
|
|
436
|
+
const startDate = new Date(task.StartedAt);
|
|
437
|
+
const endDate = new Date(task.DueAt);
|
|
438
|
+
return Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)));
|
|
439
|
+
}
|
|
440
|
+
return 1; // Default to 1 day
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private formatDateForDHTMLX(date: Date): string {
|
|
444
|
+
const year = date.getFullYear();
|
|
445
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
446
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
447
|
+
return `${year}-${month}-${day} 00:00`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
public getAgentRunId(task: TaskEntity): string | null {
|
|
451
|
+
return this.agentRunMap?.get(task.ID) || null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
public closeDetailPanel(): void {
|
|
455
|
+
this.selectedTask = null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
public onOpenEntityRecord(event: { entityName: string; recordId: string }): void {
|
|
459
|
+
this.openEntityRecord.emit(event);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
public startResize(event: MouseEvent): void {
|
|
463
|
+
this.isResizing = true;
|
|
464
|
+
this.resizeStartX = event.clientX;
|
|
465
|
+
this.resizeStartWidth = this.detailPanelWidth;
|
|
466
|
+
event.preventDefault();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@HostListener('document:mousemove', ['$event'])
|
|
470
|
+
handleResize(event: MouseEvent): void {
|
|
471
|
+
if (!this.isResizing) return;
|
|
472
|
+
|
|
473
|
+
const delta = this.resizeStartX - event.clientX;
|
|
474
|
+
const newWidth = this.resizeStartWidth + delta;
|
|
475
|
+
|
|
476
|
+
// Constrain width between min and max
|
|
477
|
+
this.detailPanelWidth = Math.max(300, Math.min(600, newWidth));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
@HostListener('document:mouseup')
|
|
481
|
+
stopResize(): void {
|
|
482
|
+
if (this.isResizing) {
|
|
483
|
+
this.isResizing = false;
|
|
484
|
+
// Resize gantt chart after panel resize completes
|
|
485
|
+
setTimeout(() => {
|
|
486
|
+
if (this.ganttInitialized) {
|
|
487
|
+
gantt.setSizes();
|
|
488
|
+
}
|
|
489
|
+
}, 0);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private expandAllAndSelectRoot(): void {
|
|
494
|
+
try {
|
|
495
|
+
// Expand all tasks
|
|
496
|
+
gantt.eachTask((task: any) => {
|
|
497
|
+
gantt.open(task.id);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
// Find and select the root task (task with parent = 0)
|
|
501
|
+
let rootTask: any = null;
|
|
502
|
+
gantt.eachTask((task: any) => {
|
|
503
|
+
if (task.parent === 0 || task.parent === '0') {
|
|
504
|
+
rootTask = task;
|
|
505
|
+
return false; // Stop iteration
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (rootTask) {
|
|
510
|
+
gantt.selectTask(rootTask.id);
|
|
511
|
+
// Trigger task click event to open detail panel
|
|
512
|
+
const originalTask = this.tasks.find(t => t.ID === rootTask.id);
|
|
513
|
+
if (originalTask) {
|
|
514
|
+
this.selectedTask = originalTask;
|
|
515
|
+
this.taskClicked.emit(originalTask);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
console.log('✅ Expanded all tasks and selected root:', rootTask?.id);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
console.error('❌ Error expanding/selecting tasks:', error);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|