@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,356 @@
1
+ import { Component, Input, Output, EventEmitter, OnChanges, HostListener } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { TaskEntity } from '@memberjunction/core-entities';
4
+ import { TaskDetailPanelComponent } from './task-detail-panel.component';
5
+
6
+ /**
7
+ * Simple list view for tasks
8
+ */
9
+ @Component({
10
+ selector: 'mj-simple-task-viewer',
11
+ standalone: true,
12
+ imports: [CommonModule, TaskDetailPanelComponent],
13
+ template: `
14
+ <div class="simple-task-viewer">
15
+ <div class="list-layout">
16
+ <div class="task-list" [class.with-detail]="selectedTask">
17
+ <div *ngFor="let task of tasks"
18
+ class="task-item"
19
+ [class.completed]="task.Status === 'Complete'"
20
+ [class.selected]="selectedTask?.ID === task.ID"
21
+ (click)="onTaskClick(task)">
22
+
23
+ <!-- Status Icon -->
24
+ <div class="status-icon" [class.complete]="task.Status === 'Complete'">
25
+ <i class="fas" [ngClass]="getStatusIcon(task.Status)"></i>
26
+ </div>
27
+
28
+ <!-- Task Content -->
29
+ <div class="task-content">
30
+ <div class="task-header">
31
+ <div class="task-title-row">
32
+ <span class="task-title" [class.completed-text]="task.Status === 'Complete'">
33
+ {{ task.Name }}
34
+ <i *ngIf="task.Status === 'Complete'" class="fas fa-check completed-check"></i>
35
+ </span>
36
+ <!-- Compact progress indicator for all tasks -->
37
+ <div *ngIf="task.PercentComplete != null" class="task-progress-compact">
38
+ <div class="progress-bar-compact">
39
+ <div class="progress-fill-compact"
40
+ [style.width.%]="task.PercentComplete"
41
+ [class.complete]="task.Status === 'Complete'"></div>
42
+ </div>
43
+ <span class="progress-text-compact">{{ task.PercentComplete }}%</span>
44
+ </div>
45
+ </div>
46
+ <span class="task-meta">
47
+ <span *ngIf="task.DueAt" class="due-date">
48
+ <i class="far fa-calendar"></i>
49
+ {{ formatDate(task.DueAt) }}
50
+ </span>
51
+ <span *ngIf="task.User" class="assigned-to">
52
+ <i class="far fa-user"></i>
53
+ {{ task.User }}
54
+ </span>
55
+ </span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ <div *ngIf="!tasks || tasks.length === 0" class="no-tasks">
61
+ <i class="fas fa-tasks"></i>
62
+ <p>No tasks to display</p>
63
+ </div>
64
+ </div>
65
+
66
+ <div *ngIf="selectedTask" class="list-resizer"
67
+ (mousedown)="startResize($event)"></div>
68
+
69
+ <div *ngIf="selectedTask" class="task-detail-panel" [style.width.px]="detailPanelWidth">
70
+ <mj-task-detail-panel
71
+ [task]="selectedTask"
72
+ [agentRunId]="getAgentRunId(selectedTask)"
73
+ (closePanel)="closeDetailPanel()"
74
+ (openEntityRecord)="onOpenEntityRecord($event)">
75
+ </mj-task-detail-panel>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ `,
80
+ styles: [`
81
+ .simple-task-viewer {
82
+ height: 100%;
83
+ background: #FAFAFA;
84
+ overflow: hidden;
85
+ display: flex;
86
+ flex-direction: column;
87
+ }
88
+
89
+ .list-layout {
90
+ display: flex;
91
+ height: 100%;
92
+ position: relative;
93
+ }
94
+
95
+ .task-list {
96
+ flex: 1;
97
+ min-width: 400px;
98
+ padding: 16px;
99
+ overflow-y: auto;
100
+ }
101
+
102
+ .task-list.with-detail {
103
+ border-right: 1px solid #E5E7EB;
104
+ }
105
+
106
+ .list-resizer {
107
+ width: 4px;
108
+ background: #E5E7EB;
109
+ cursor: col-resize;
110
+ flex-shrink: 0;
111
+ transition: background 0.2s;
112
+ }
113
+
114
+ .list-resizer:hover {
115
+ background: #3B82F6;
116
+ }
117
+
118
+ .task-detail-panel {
119
+ min-width: 300px;
120
+ max-width: 600px;
121
+ height: 100%;
122
+ flex-shrink: 0;
123
+ }
124
+
125
+ .task-item {
126
+ display: flex;
127
+ align-items: flex-start;
128
+ gap: 12px;
129
+ padding: 16px;
130
+ background: white;
131
+ border: 1px solid #E5E7EB;
132
+ border-radius: 8px;
133
+ margin-bottom: 8px;
134
+ transition: all 0.2s;
135
+ cursor: pointer;
136
+ }
137
+
138
+ .task-item:hover {
139
+ border-color: #3B82F6;
140
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.1);
141
+ transform: translateY(-1px);
142
+ }
143
+
144
+ .task-item.selected {
145
+ border-color: #3B82F6;
146
+ background: #EFF6FF;
147
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);
148
+ }
149
+
150
+ .task-item.completed {
151
+ background: #F9FAFB;
152
+ border-color: #D1D5DB;
153
+ }
154
+
155
+ .status-icon {
156
+ width: 32px;
157
+ height: 32px;
158
+ border-radius: 6px;
159
+ display: flex;
160
+ align-items: center;
161
+ justify-content: center;
162
+ background: #FEF3C7;
163
+ color: #F59E0B;
164
+ flex-shrink: 0;
165
+ }
166
+
167
+ .status-icon.complete {
168
+ background: #D1FAE5;
169
+ color: #10B981;
170
+ }
171
+
172
+ .task-content {
173
+ flex: 1;
174
+ min-width: 0;
175
+ }
176
+
177
+ .task-header {
178
+ display: flex;
179
+ justify-content: space-between;
180
+ align-items: flex-start;
181
+ gap: 12px;
182
+ margin-bottom: 6px;
183
+ }
184
+
185
+ .task-title-row {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 12px;
189
+ flex: 1;
190
+ min-width: 0;
191
+ }
192
+
193
+ .task-title {
194
+ font-weight: 600;
195
+ color: #111827;
196
+ font-size: 14px;
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 6px;
200
+ }
201
+
202
+ .task-title.completed-text {
203
+ color: #6B7280;
204
+ text-decoration: line-through;
205
+ text-decoration-thickness: 1.5px;
206
+ }
207
+
208
+ .completed-check {
209
+ color: #10B981;
210
+ font-size: 12px;
211
+ }
212
+
213
+ .task-meta {
214
+ display: flex;
215
+ gap: 12px;
216
+ font-size: 12px;
217
+ color: #6B7280;
218
+ white-space: nowrap;
219
+ }
220
+
221
+ .task-meta i {
222
+ margin-right: 4px;
223
+ }
224
+
225
+ .task-description {
226
+ font-size: 13px;
227
+ color: #6B7280;
228
+ margin-bottom: 8px;
229
+ line-height: 1.5;
230
+ }
231
+
232
+ .task-progress-compact {
233
+ display: flex;
234
+ align-items: center;
235
+ gap: 6px;
236
+ flex-shrink: 0;
237
+ }
238
+
239
+ .progress-bar-compact {
240
+ width: 80px;
241
+ height: 4px;
242
+ background: #E5E7EB;
243
+ border-radius: 2px;
244
+ overflow: hidden;
245
+ }
246
+
247
+ .progress-fill-compact {
248
+ height: 100%;
249
+ background: #3B82F6;
250
+ transition: width 0.3s ease;
251
+ }
252
+
253
+ .progress-fill-compact.complete {
254
+ background: #10B981;
255
+ }
256
+
257
+ .progress-text-compact {
258
+ font-size: 10px;
259
+ font-weight: 600;
260
+ color: #6B7280;
261
+ min-width: 30px;
262
+ text-align: right;
263
+ }
264
+
265
+ .no-tasks {
266
+ text-align: center;
267
+ padding: 60px 20px;
268
+ color: #9CA3AF;
269
+ }
270
+
271
+ .no-tasks i {
272
+ font-size: 48px;
273
+ opacity: 0.3;
274
+ margin-bottom: 12px;
275
+ }
276
+
277
+ .no-tasks p {
278
+ margin: 0;
279
+ font-size: 14px;
280
+ }
281
+ `]
282
+ })
283
+ export class SimpleTaskViewerComponent implements OnChanges {
284
+ @Input() tasks: TaskEntity[] = [];
285
+ @Input() agentRunMap?: Map<string, string>; // Maps TaskID -> AgentRunID
286
+ @Output() taskClicked = new EventEmitter<TaskEntity>();
287
+ @Output() openEntityRecord = new EventEmitter<{ entityName: string; recordId: string }>();
288
+
289
+ public selectedTask: TaskEntity | null = null;
290
+ public detailPanelWidth: number = 400;
291
+
292
+ private isResizing = false;
293
+ private resizeStartX = 0;
294
+ private resizeStartWidth = 0;
295
+
296
+ ngOnChanges() {
297
+ // Tasks are already loaded
298
+ }
299
+
300
+ public onTaskClick(task: TaskEntity): void {
301
+ this.selectedTask = task;
302
+ this.taskClicked.emit(task);
303
+ }
304
+
305
+ public getAgentRunId(task: TaskEntity): string | null {
306
+ return this.agentRunMap?.get(task.ID) || null;
307
+ }
308
+
309
+ public closeDetailPanel(): void {
310
+ this.selectedTask = null;
311
+ }
312
+
313
+ public onOpenEntityRecord(event: { entityName: string; recordId: string }): void {
314
+ this.openEntityRecord.emit(event);
315
+ }
316
+
317
+ public startResize(event: MouseEvent): void {
318
+ this.isResizing = true;
319
+ this.resizeStartX = event.clientX;
320
+ this.resizeStartWidth = this.detailPanelWidth;
321
+ event.preventDefault();
322
+ }
323
+
324
+ @HostListener('document:mousemove', ['$event'])
325
+ handleResize(event: MouseEvent): void {
326
+ if (!this.isResizing) return;
327
+
328
+ const delta = this.resizeStartX - event.clientX;
329
+ const newWidth = this.resizeStartWidth + delta;
330
+
331
+ // Constrain width between min and max
332
+ this.detailPanelWidth = Math.max(300, Math.min(600, newWidth));
333
+ }
334
+
335
+ @HostListener('document:mouseup')
336
+ stopResize(): void {
337
+ this.isResizing = false;
338
+ }
339
+
340
+ public getStatusIcon(status: string): string {
341
+ switch (status) {
342
+ case 'Complete': return 'fa-check-circle';
343
+ case 'In Progress': return 'fa-spinner';
344
+ case 'Pending': return 'fa-clock';
345
+ case 'Blocked': return 'fa-ban';
346
+ case 'Failed': return 'fa-times-circle';
347
+ default: return 'fa-circle';
348
+ }
349
+ }
350
+
351
+ public formatDate(date: Date | null): string {
352
+ if (!date) return '';
353
+ const d = new Date(date);
354
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
355
+ }
356
+ }
@@ -0,0 +1,304 @@
1
+ import { Component, Input, Output, EventEmitter, OnInit, OnChanges } from '@angular/core';
2
+ import { CommonModule } from '@angular/common';
3
+ import { TaskEntity, AIAgentEntityExtended } from '@memberjunction/core-entities';
4
+ import { AIEngineBase } from '@memberjunction/ai-engine-base';
5
+
6
+ /**
7
+ * Task detail panel showing task information, agent details, and agent run
8
+ * Reusable across list and Gantt views
9
+ */
10
+ @Component({
11
+ selector: 'mj-task-detail-panel',
12
+ standalone: true,
13
+ imports: [CommonModule],
14
+ template: `
15
+ <div class="task-detail-panel">
16
+ <div class="detail-header">
17
+ <h3>{{ task.Name }}</h3>
18
+ <button class="close-detail-btn" (click)="closePanel.emit()">
19
+ <i class="fas fa-times"></i>
20
+ </button>
21
+ </div>
22
+
23
+ <div class="detail-content">
24
+ <div class="detail-field" *ngIf="task.Description">
25
+ <label>Description</label>
26
+ <p>{{ task.Description }}</p>
27
+ </div>
28
+
29
+ <div class="detail-field">
30
+ <label>Status</label>
31
+ <p>{{ task.Status }}</p>
32
+ </div>
33
+
34
+ <div class="detail-field" *ngIf="task.PercentComplete != null">
35
+ <label>Progress</label>
36
+ <div class="detail-progress">
37
+ <div class="progress-bar-detail">
38
+ <div class="progress-fill-detail" [style.width.%]="task.PercentComplete"></div>
39
+ </div>
40
+ <span>{{ task.PercentComplete }}%</span>
41
+ </div>
42
+ </div>
43
+
44
+ <div class="detail-field" *ngIf="task.StartedAt">
45
+ <label>Started</label>
46
+ <p>{{ formatDateTime(task.StartedAt) }}</p>
47
+ </div>
48
+
49
+ <div class="detail-field" *ngIf="task.DueAt">
50
+ <label>Due</label>
51
+ <p>{{ formatDateTime(task.DueAt) }}</p>
52
+ </div>
53
+
54
+ <div class="detail-field" *ngIf="task.CompletedAt">
55
+ <label>Completed</label>
56
+ <p>{{ formatDateTime(task.CompletedAt) }}</p>
57
+ </div>
58
+
59
+ <div class="detail-field" *ngIf="task.User">
60
+ <label>Assigned User</label>
61
+ <p>{{ task.User }}</p>
62
+ </div>
63
+
64
+ <!-- Agent Information -->
65
+ <div class="detail-field" *ngIf="agent">
66
+ <label>Agent</label>
67
+ <div class="agent-info" (click)="openAgent()">
68
+ <i [class]="'fas fa-' + ('robot')" class="agent-icon"></i>
69
+ <span class="agent-name">{{ agent.Name }}</span>
70
+ <i class="fas fa-external-link-alt link-icon"></i>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Agent Run Information -->
75
+ <div class="detail-field" *ngIf="agentRunId">
76
+ <label>Agent Run</label>
77
+ <div class="agent-run-link" (click)="openAgentRun()">
78
+ <span>View Run Details</span>
79
+ <i class="fas fa-external-link-alt link-icon"></i>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ `,
85
+ styles: [`
86
+ .task-detail-panel {
87
+ width: 100%;
88
+ height: calc(100% - 45px);
89
+ background: white;
90
+ display: flex;
91
+ flex-direction: column;
92
+ overflow: hidden;
93
+ }
94
+
95
+ .detail-header {
96
+ padding: 20px;
97
+ border-bottom: 1px solid #E5E7EB;
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: flex-start;
101
+ background: #F9FAFB;
102
+ flex-shrink: 0;
103
+ }
104
+
105
+ .detail-header h3 {
106
+ margin: 0;
107
+ font-size: 18px;
108
+ font-weight: 600;
109
+ color: #111827;
110
+ flex: 1;
111
+ padding-right: 12px;
112
+ }
113
+
114
+ .close-detail-btn {
115
+ background: none;
116
+ border: none;
117
+ color: #6B7280;
118
+ cursor: pointer;
119
+ padding: 4px;
120
+ width: 28px;
121
+ height: 28px;
122
+ border-radius: 4px;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ flex-shrink: 0;
127
+ }
128
+
129
+ .close-detail-btn:hover {
130
+ background: #E5E7EB;
131
+ color: #111827;
132
+ }
133
+
134
+ .detail-content {
135
+ padding: 20px;
136
+ flex: 1;
137
+ min-height: 0;
138
+ overflow-y: auto;
139
+ }
140
+
141
+ .detail-field {
142
+ margin-bottom: 20px;
143
+ }
144
+
145
+ .detail-field:last-child {
146
+ margin-bottom: 0;
147
+ }
148
+
149
+ .detail-field label {
150
+ display: block;
151
+ font-size: 12px;
152
+ font-weight: 600;
153
+ color: #6B7280;
154
+ text-transform: uppercase;
155
+ letter-spacing: 0.5px;
156
+ margin-bottom: 6px;
157
+ }
158
+
159
+ .detail-field p {
160
+ margin: 0;
161
+ font-size: 14px;
162
+ color: #111827;
163
+ line-height: 1.5;
164
+ }
165
+
166
+ .detail-progress {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 12px;
170
+ }
171
+
172
+ .progress-bar-detail {
173
+ flex: 1;
174
+ height: 8px;
175
+ background: #E5E7EB;
176
+ border-radius: 4px;
177
+ overflow: hidden;
178
+ }
179
+
180
+ .progress-fill-detail {
181
+ height: calc(100% - 69px);
182
+ background: #3B82F6;
183
+ transition: width 0.3s ease;
184
+ }
185
+
186
+ .detail-progress span {
187
+ font-size: 13px;
188
+ font-weight: 600;
189
+ color: #6B7280;
190
+ min-width: 40px;
191
+ }
192
+
193
+ .agent-info {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: 10px;
197
+ padding: 10px 12px;
198
+ background: #F3F4F6;
199
+ border-radius: 6px;
200
+ cursor: pointer;
201
+ transition: all 0.15s;
202
+ }
203
+
204
+ .agent-info:hover {
205
+ background: #E5E7EB;
206
+ }
207
+
208
+ .agent-icon {
209
+ font-size: 18px;
210
+ color: #3B82F6;
211
+ }
212
+
213
+ .agent-name {
214
+ flex: 1;
215
+ font-size: 14px;
216
+ font-weight: 500;
217
+ color: #111827;
218
+ }
219
+
220
+ .link-icon {
221
+ font-size: 12px;
222
+ color: #6B7280;
223
+ }
224
+
225
+ .agent-run-link {
226
+ display: flex;
227
+ align-items: center;
228
+ gap: 8px;
229
+ padding: 10px 12px;
230
+ background: #F3F4F6;
231
+ border-radius: 6px;
232
+ cursor: pointer;
233
+ transition: all 0.15s;
234
+ }
235
+
236
+ .agent-run-link:hover {
237
+ background: #E5E7EB;
238
+ }
239
+
240
+ .agent-run-link span {
241
+ flex: 1;
242
+ font-size: 14px;
243
+ font-weight: 500;
244
+ color: #111827;
245
+ }
246
+ `]
247
+ })
248
+ export class TaskDetailPanelComponent implements OnInit, OnChanges {
249
+ @Input() task!: TaskEntity;
250
+ @Input() agentRunId: string | null = null;
251
+ @Output() closePanel = new EventEmitter<void>();
252
+ @Output() openEntityRecord = new EventEmitter<{ entityName: string; recordId: string }>();
253
+
254
+ public agent: AIAgentEntityExtended | null = null;
255
+
256
+ ngOnInit(): void {
257
+ this.loadAgentInfo();
258
+ }
259
+
260
+ ngOnChanges(): void {
261
+ this.loadAgentInfo();
262
+ }
263
+
264
+ private loadAgentInfo(): void {
265
+ if (!this.task?.AgentID) {
266
+ this.agent = null;
267
+ return;
268
+ }
269
+
270
+ // Get agent from AIEngineBase
271
+ const agents = AIEngineBase.Instance.Agents;
272
+ this.agent = agents.find((a: AIAgentEntityExtended) => a.ID === this.task.AgentID) || null;
273
+ }
274
+
275
+ public formatDateTime(date: Date | null): string {
276
+ if (!date) return '';
277
+ const d = new Date(date);
278
+ return d.toLocaleDateString('en-US', {
279
+ month: 'short',
280
+ day: 'numeric',
281
+ year: 'numeric',
282
+ hour: '2-digit',
283
+ minute: '2-digit'
284
+ });
285
+ }
286
+
287
+ public openAgent(): void {
288
+ if (this.task.AgentID) {
289
+ this.openEntityRecord.emit({
290
+ entityName: 'AI Agents',
291
+ recordId: this.task.AgentID
292
+ });
293
+ }
294
+ }
295
+
296
+ public openAgentRun(): void {
297
+ if (this.agentRunId) {
298
+ this.openEntityRecord.emit({
299
+ entityName: 'MJ: AI Agent Runs',
300
+ recordId: this.agentRunId
301
+ });
302
+ }
303
+ }
304
+ }