@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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +10 -0
  3. package/README.md +233 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +2 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/lib/components/gantt-task-viewer.component.d.ts +44 -0
  9. package/dist/lib/components/gantt-task-viewer.component.d.ts.map +1 -0
  10. package/dist/lib/components/gantt-task-viewer.component.js +461 -0
  11. package/dist/lib/components/gantt-task-viewer.component.js.map +1 -0
  12. package/dist/lib/components/simple-task-viewer.component.d.ts +36 -0
  13. package/dist/lib/components/simple-task-viewer.component.d.ts.map +1 -0
  14. package/dist/lib/components/simple-task-viewer.component.js +275 -0
  15. package/dist/lib/components/simple-task-viewer.component.js.map +1 -0
  16. package/dist/lib/components/task-detail-panel.component.d.ts +26 -0
  17. package/dist/lib/components/task-detail-panel.component.d.ts.map +1 -0
  18. package/dist/lib/components/task-detail-panel.component.js +294 -0
  19. package/dist/lib/components/task-detail-panel.component.js.map +1 -0
  20. package/dist/lib/components/task.component.d.ts +34 -0
  21. package/dist/lib/components/task.component.d.ts.map +1 -0
  22. package/dist/lib/components/task.component.js +201 -0
  23. package/dist/lib/components/task.component.js.map +1 -0
  24. package/dist/lib/models/task-view.models.d.ts +17 -0
  25. package/dist/lib/models/task-view.models.d.ts.map +1 -0
  26. package/dist/lib/models/task-view.models.js +2 -0
  27. package/dist/lib/models/task-view.models.js.map +1 -0
  28. package/dist/lib/ng-tasks.module.d.ts +14 -0
  29. package/dist/lib/ng-tasks.module.d.ts.map +1 -0
  30. package/dist/lib/ng-tasks.module.js +37 -0
  31. package/dist/lib/ng-tasks.module.js.map +1 -0
  32. package/dist/public-api.d.ts +9 -0
  33. package/dist/public-api.d.ts.map +1 -0
  34. package/dist/public-api.js +12 -0
  35. package/dist/public-api.js.map +1 -0
  36. package/package.json +31 -0
  37. package/src/index.ts +1 -0
  38. package/src/lib/components/gantt-task-viewer.component.ts +524 -0
  39. package/src/lib/components/simple-task-viewer.component.ts +356 -0
  40. package/src/lib/components/task-detail-panel.component.ts +304 -0
  41. package/src/lib/components/task.component.ts +175 -0
  42. package/src/lib/models/task-view.models.ts +19 -0
  43. package/src/lib/ng-tasks.module.ts +22 -0
  44. package/src/lib/types/frappe-gantt.d.ts +32 -0
  45. package/src/public-api.ts +14 -0
  46. 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
+ }