@flusys/ng-task-manager 5.3.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.
@@ -0,0 +1,3771 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, inject, signal, DestroyRef, computed, input, output, Component, model, effect, untracked } from '@angular/core';
3
+ import { ApiResourceService, TranslatePipe, AngularModule, PrimeModule, TranslateService, UserSelectComponent, resolveTranslationModule, SHARED_MESSAGES } from '@flusys/ng-shared';
4
+ import { firstValueFrom, Subject } from 'rxjs';
5
+ import { io } from 'socket.io-client';
6
+ import { APP_CONFIG } from '@flusys/ng-core';
7
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
+ import * as i9$1 from '@angular/common';
9
+ import { DatePipe } from '@angular/common';
10
+ import * as i1 from 'primeng/tag';
11
+ import { TagModule } from 'primeng/tag';
12
+ import * as i2 from 'primeng/tooltip';
13
+ import { TooltipModule } from 'primeng/tooltip';
14
+ import * as i3 from '@angular/forms';
15
+ import { FormsModule } from '@angular/forms';
16
+ import * as i9 from 'primeng/button';
17
+ import { ButtonModule } from 'primeng/button';
18
+ import * as i7 from 'primeng/datepicker';
19
+ import { DatePickerModule } from 'primeng/datepicker';
20
+ import * as i1$1 from 'primeng/dialog';
21
+ import { DialogModule } from 'primeng/dialog';
22
+ import * as i4 from 'primeng/inputtext';
23
+ import { InputTextModule } from 'primeng/inputtext';
24
+ import * as i8 from 'primeng/multiselect';
25
+ import { MultiSelectModule } from 'primeng/multiselect';
26
+ import * as i5 from 'primeng/textarea';
27
+ import { TextareaModule } from 'primeng/textarea';
28
+ import * as i6 from 'primeng/select';
29
+ import { SelectModule } from 'primeng/select';
30
+ import * as i2$1 from 'primeng/api';
31
+ import { ConfirmationService, MessageService } from 'primeng/api';
32
+ import * as i1$2 from 'primeng/drawer';
33
+ import { DrawerModule } from 'primeng/drawer';
34
+ import { Router } from '@angular/router';
35
+ import * as i6$1 from 'primeng/skeleton';
36
+ import * as i1$3 from '@angular/cdk/drag-drop';
37
+ import { moveItemInArray, transferArrayItem, DragDropModule } from '@angular/cdk/drag-drop';
38
+ import * as i9$2 from 'primeng/table';
39
+
40
+ class TaskBoardApiService extends ApiResourceService {
41
+ constructor() {
42
+ super('board', 'taskManager');
43
+ }
44
+ getMembers(boardId) {
45
+ return firstValueFrom(this.http.post(`${this.baseUrl}/get-members`, { boardId }));
46
+ }
47
+ addMember(boardId, userId, role = 'MEMBER') {
48
+ return firstValueFrom(this.http.post(`${this.baseUrl}/add-member`, { boardId, userId, role }));
49
+ }
50
+ removeMember(boardId, userId) {
51
+ return firstValueFrom(this.http.post(`${this.baseUrl}/remove-member`, {
52
+ boardId,
53
+ userId,
54
+ }));
55
+ }
56
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
57
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardApiService, providedIn: 'root' });
58
+ }
59
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardApiService, decorators: [{
60
+ type: Injectable,
61
+ args: [{ providedIn: 'root' }]
62
+ }], ctorParameters: () => [] });
63
+
64
+ class TaskApiService extends ApiResourceService {
65
+ constructor() {
66
+ super('task', 'taskManager');
67
+ }
68
+ moveTask(dto) {
69
+ return firstValueFrom(this.http.post(`${this.baseUrl}/move`, dto));
70
+ }
71
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
72
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskApiService, providedIn: 'root' });
73
+ }
74
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskApiService, decorators: [{
75
+ type: Injectable,
76
+ args: [{ providedIn: 'root' }]
77
+ }], ctorParameters: () => [] });
78
+ class TaskListApiService extends ApiResourceService {
79
+ constructor() {
80
+ super('list', 'taskManager');
81
+ }
82
+ updateList(dto) {
83
+ return firstValueFrom(this.http.post(`${this.baseUrl}/update`, dto));
84
+ }
85
+ reorderLists(dto) {
86
+ return firstValueFrom(this.http.post(`${this.baseUrl}/reorder`, dto));
87
+ }
88
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskListApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
89
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskListApiService, providedIn: 'root' });
90
+ }
91
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskListApiService, decorators: [{
92
+ type: Injectable,
93
+ args: [{ providedIn: 'root' }]
94
+ }], ctorParameters: () => [] });
95
+ class TaskLabelApiService extends ApiResourceService {
96
+ constructor() {
97
+ super('label', 'taskManager');
98
+ }
99
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskLabelApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
100
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskLabelApiService, providedIn: 'root' });
101
+ }
102
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskLabelApiService, decorators: [{
103
+ type: Injectable,
104
+ args: [{ providedIn: 'root' }]
105
+ }], ctorParameters: () => [] });
106
+ class TaskCommentApiService extends ApiResourceService {
107
+ constructor() {
108
+ super('comment', 'taskManager');
109
+ }
110
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCommentApiService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
111
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCommentApiService, providedIn: 'root' });
112
+ }
113
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCommentApiService, decorators: [{
114
+ type: Injectable,
115
+ args: [{ providedIn: 'root' }]
116
+ }], ctorParameters: () => [] });
117
+
118
+ class TaskBoardSocketService {
119
+ socket = null;
120
+ appConfig = inject(APP_CONFIG);
121
+ _isConnected = signal(false, ...(ngDevMode ? [{ debugName: "_isConnected" }] : []));
122
+ isConnected = this._isConnected.asReadonly();
123
+ boardEvent$ = new Subject();
124
+ onBoardEvent = this.boardEvent$.asObservable();
125
+ connect(accessToken) {
126
+ if (this.socket?.connected)
127
+ return;
128
+ const baseUrl = this.getBaseUrl();
129
+ this.socket = io(`${baseUrl}/task-boards`, {
130
+ auth: { token: accessToken },
131
+ transports: ['websocket', 'polling'],
132
+ reconnection: true,
133
+ reconnectionAttempts: 5,
134
+ reconnectionDelay: 1000,
135
+ });
136
+ this.socket.on('connect', () => this._isConnected.set(true));
137
+ this.socket.on('disconnect', (reason) => {
138
+ this._isConnected.set(false);
139
+ if (reason === 'io server disconnect')
140
+ this.socket?.connect();
141
+ });
142
+ this.socket.on('connect_error', () => this._isConnected.set(false));
143
+ this.socket.on('board:event', (event) => this.boardEvent$.next(event));
144
+ }
145
+ joinBoard(boardId) {
146
+ this.socket?.emit('board:join', { boardId });
147
+ }
148
+ leaveBoard(boardId) {
149
+ this.socket?.emit('board:leave', { boardId });
150
+ }
151
+ disconnect() {
152
+ this.socket?.disconnect();
153
+ this.socket = null;
154
+ this._isConnected.set(false);
155
+ }
156
+ getBaseUrl() {
157
+ const services = this.appConfig.services;
158
+ return services?.['taskManager']?.socketUrl ?? this.appConfig.apiBaseUrl;
159
+ }
160
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardSocketService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
161
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardSocketService, providedIn: 'root' });
162
+ }
163
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardSocketService, decorators: [{
164
+ type: Injectable,
165
+ args: [{ providedIn: 'root' }]
166
+ }] });
167
+
168
+ class TaskBoardStateService {
169
+ boardApi = inject(TaskBoardApiService);
170
+ taskApi = inject(TaskApiService);
171
+ listApi = inject(TaskListApiService);
172
+ labelApi = inject(TaskLabelApiService);
173
+ socketService = inject(TaskBoardSocketService);
174
+ destroyRef = inject(DestroyRef);
175
+ _boards = signal([], ...(ngDevMode ? [{ debugName: "_boards" }] : []));
176
+ _activeBoard = signal(null, ...(ngDevMode ? [{ debugName: "_activeBoard" }] : []));
177
+ _lists = signal([], ...(ngDevMode ? [{ debugName: "_lists" }] : []));
178
+ _tasks = signal([], ...(ngDevMode ? [{ debugName: "_tasks" }] : []));
179
+ _labels = signal([], ...(ngDevMode ? [{ debugName: "_labels" }] : []));
180
+ _members = signal([], ...(ngDevMode ? [{ debugName: "_members" }] : []));
181
+ _selectedTask = signal(null, ...(ngDevMode ? [{ debugName: "_selectedTask" }] : []));
182
+ _isLoading = signal(false, ...(ngDevMode ? [{ debugName: "_isLoading" }] : []));
183
+ _isBoardLoading = signal(false, ...(ngDevMode ? [{ debugName: "_isBoardLoading" }] : []));
184
+ boards = this._boards.asReadonly();
185
+ activeBoard = this._activeBoard.asReadonly();
186
+ lists = this._lists.asReadonly();
187
+ tasks = this._tasks.asReadonly();
188
+ labels = this._labels.asReadonly();
189
+ members = this._members.asReadonly();
190
+ selectedTask = this._selectedTask.asReadonly();
191
+ isLoading = this._isLoading.asReadonly();
192
+ isBoardLoading = this._isBoardLoading.asReadonly();
193
+ tasksByList = computed(() => {
194
+ const map = new Map();
195
+ for (const task of this._tasks()) {
196
+ const list = map.get(task.listId) ?? [];
197
+ list.push(task);
198
+ map.set(task.listId, list);
199
+ }
200
+ return map;
201
+ }, ...(ngDevMode ? [{ debugName: "tasksByList" }] : []));
202
+ listsWithTasks = computed(() => this._lists()
203
+ .slice()
204
+ .sort((a, b) => a.position - b.position)
205
+ .map((list) => ({
206
+ ...list,
207
+ tasks: (this.tasksByList().get(list.id) ?? []).slice().sort((a, b) => a.position - b.position),
208
+ })), ...(ngDevMode ? [{ debugName: "listsWithTasks" }] : []));
209
+ hasActiveBoard = computed(() => this._activeBoard() !== null, ...(ngDevMode ? [{ debugName: "hasActiveBoard" }] : []));
210
+ isSocketConnected = this.socketService.isConnected;
211
+ constructor() {
212
+ this.socketService.onBoardEvent
213
+ .pipe(takeUntilDestroyed(this.destroyRef))
214
+ .subscribe((event) => this.handleBoardEvent(event));
215
+ }
216
+ async loadBoards() {
217
+ this._isLoading.set(true);
218
+ try {
219
+ const res = await this.boardApi.getAll({ sort: { createdAt: 'DESC' } });
220
+ this._boards.set(res.data ?? []);
221
+ }
222
+ finally {
223
+ this._isLoading.set(false);
224
+ }
225
+ }
226
+ async openBoard(boardId) {
227
+ this._isBoardLoading.set(true);
228
+ try {
229
+ const prevBoard = this._activeBoard();
230
+ if (prevBoard)
231
+ this.socketService.leaveBoard(prevBoard.id);
232
+ const [boardRes, listsRes, tasksRes, labelsRes, membersRes] = await Promise.all([
233
+ this.boardApi.findById(boardId),
234
+ this.listApi.getAll({ filter: { boardId, isActive: true }, sort: { position: 'ASC' } }),
235
+ this.taskApi.getAll({ filter: { boardId }, sort: { position: 'ASC' } }),
236
+ this.labelApi.getAll({ filter: { boardId } }),
237
+ this.boardApi.getMembers(boardId),
238
+ ]);
239
+ this._activeBoard.set(boardRes.data ?? null);
240
+ this._lists.set(listsRes.data ?? []);
241
+ this._tasks.set(tasksRes.data ?? []);
242
+ this._labels.set(labelsRes.data ?? []);
243
+ this._members.set(membersRes.data ?? []);
244
+ this.socketService.joinBoard(boardId);
245
+ }
246
+ finally {
247
+ this._isBoardLoading.set(false);
248
+ }
249
+ }
250
+ closeBoard() {
251
+ const board = this._activeBoard();
252
+ if (board)
253
+ this.socketService.leaveBoard(board.id);
254
+ this._activeBoard.set(null);
255
+ this._lists.set([]);
256
+ this._tasks.set([]);
257
+ this._labels.set([]);
258
+ this._members.set([]);
259
+ this._selectedTask.set(null);
260
+ }
261
+ selectTask(task) {
262
+ this._selectedTask.set(task);
263
+ }
264
+ async addLabel(boardId, name, color) {
265
+ await this.labelApi.insert({ boardId, name, color });
266
+ const res = await this.labelApi.getAll({ filter: { boardId } });
267
+ this._labels.set(res.data ?? []);
268
+ }
269
+ async deleteLabel(labelId) {
270
+ await this.labelApi.delete({ id: labelId, type: 'delete' });
271
+ this._labels.update((list) => list.filter((l) => l.id !== labelId));
272
+ }
273
+ async addMember(userId, role = 'MEMBER') {
274
+ const board = this._activeBoard();
275
+ if (!board)
276
+ return;
277
+ const res = await this.boardApi.addMember(board.id, userId, role);
278
+ if (res.data)
279
+ this._members.update((list) => [...list, res.data]);
280
+ }
281
+ async removeMember(userId) {
282
+ const board = this._activeBoard();
283
+ if (!board)
284
+ return;
285
+ await this.boardApi.removeMember(board.id, userId);
286
+ this._members.update((list) => list.filter((m) => m.userId !== userId));
287
+ }
288
+ async reorderLists(boardId, orderedIds) {
289
+ const posMap = new Map(orderedIds.map((id, i) => [id, i * 100]));
290
+ this._lists.update((list) => list
291
+ .map((l) => (posMap.has(l.id) ? { ...l, position: posMap.get(l.id) } : l))
292
+ .sort((a, b) => a.position - b.position));
293
+ await this.listApi.reorderLists({ boardId, orderedIds });
294
+ }
295
+ async renameList(listId, name) {
296
+ await this.listApi.updateList({ id: listId, name });
297
+ this._lists.update((list) => list.map((l) => (l.id === listId ? { ...l, name } : l)));
298
+ }
299
+ async updateBoard(dto) {
300
+ await this.boardApi.update(dto);
301
+ const id = dto.id;
302
+ if (!id)
303
+ return;
304
+ this._activeBoard.update((b) => (b?.id === id ? { ...b, ...dto } : b));
305
+ this._boards.update((list) => list.map((b) => (b.id === id ? { ...b, ...dto } : b)));
306
+ }
307
+ addTaskOptimistic(task) {
308
+ this._tasks.update((list) => {
309
+ if (list.some((t) => t.id === task.id)) {
310
+ return list.map((t) => (t.id === task.id ? { ...t, ...task } : t));
311
+ }
312
+ return [...list, task];
313
+ });
314
+ }
315
+ async deleteTask(taskId) {
316
+ await this.taskApi.delete({ id: taskId, type: 'delete' });
317
+ this._tasks.update((list) => list.filter((t) => t.id !== taskId));
318
+ if (this._selectedTask()?.id === taskId)
319
+ this._selectedTask.set(null);
320
+ }
321
+ handleBoardEvent(event) {
322
+ switch (event.type) {
323
+ case 'task:created': {
324
+ const created = event.payload;
325
+ this._tasks.update((list) => {
326
+ if (list.some((t) => t.id === created.id))
327
+ return list;
328
+ return [...list, created];
329
+ });
330
+ break;
331
+ }
332
+ case 'task:updated':
333
+ case 'task:moved': {
334
+ const updated = event.payload;
335
+ this._tasks.update((list) => list.map((t) => (t.id === updated.id ? { ...t, ...updated } : t)));
336
+ if (this._selectedTask()?.id === updated.id) {
337
+ this._selectedTask.update((t) => (t ? { ...t, ...updated } : null));
338
+ }
339
+ break;
340
+ }
341
+ case 'task:deleted': {
342
+ const deleted = event.payload;
343
+ this._tasks.update((list) => list.filter((t) => t.id !== deleted.id));
344
+ if (this._selectedTask()?.id === deleted.id)
345
+ this._selectedTask.set(null);
346
+ break;
347
+ }
348
+ case 'list:created':
349
+ this._lists.update((list) => [...list, event.payload]);
350
+ break;
351
+ case 'list:updated': {
352
+ const updatedList = event.payload;
353
+ this._lists.update((list) => list.map((l) => (l.id === updatedList.id ? { ...l, ...updatedList } : l)));
354
+ break;
355
+ }
356
+ case 'list:deleted': {
357
+ const deletedList = event.payload;
358
+ this._lists.update((list) => list.filter((l) => l.id !== deletedList.id));
359
+ break;
360
+ }
361
+ case 'list:reordered': {
362
+ const { orderedIds } = event.payload;
363
+ const posMap = new Map(orderedIds.map((id, i) => [id, i * 100]));
364
+ this._lists.update((list) => list
365
+ .map((l) => (posMap.has(l.id) ? { ...l, position: posMap.get(l.id) } : l))
366
+ .sort((a, b) => a.position - b.position));
367
+ break;
368
+ }
369
+ }
370
+ }
371
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardStateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
372
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardStateService, providedIn: 'root' });
373
+ }
374
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskBoardStateService, decorators: [{
375
+ type: Injectable,
376
+ args: [{ providedIn: 'root' }]
377
+ }], ctorParameters: () => [] });
378
+
379
+ const MEMBER_COLOR_PALETTE = [
380
+ '#6366f1',
381
+ '#8b5cf6',
382
+ '#ec4899',
383
+ '#ef4444',
384
+ '#f97316',
385
+ '#eab308',
386
+ '#22c55e',
387
+ '#14b8a6',
388
+ '#3b82f6',
389
+ '#06b6d4',
390
+ ];
391
+ const BOARD_COLOR_PALETTE = [
392
+ ...MEMBER_COLOR_PALETTE,
393
+ '#64748b',
394
+ ];
395
+ function taskPrioritySeverity(priority) {
396
+ const map = {
397
+ LOW: 'secondary',
398
+ MEDIUM: 'info',
399
+ HIGH: 'warn',
400
+ URGENT: 'danger',
401
+ };
402
+ return map[priority] ?? 'info';
403
+ }
404
+ function memberInitials(name) {
405
+ if (!name)
406
+ return '?';
407
+ return name
408
+ .split(' ')
409
+ .slice(0, 2)
410
+ .map((n) => n[0]?.toUpperCase() ?? '')
411
+ .join('');
412
+ }
413
+ function isTaskOverdue(dueDate) {
414
+ return new Date(dueDate) < new Date();
415
+ }
416
+ function memberColorFromId(userId) {
417
+ let hash = 0;
418
+ for (let i = 0; i < userId.length; i++)
419
+ hash = userId.charCodeAt(i) + ((hash << 5) - hash);
420
+ return MEMBER_COLOR_PALETTE[Math.abs(hash) % MEMBER_COLOR_PALETTE.length];
421
+ }
422
+
423
+ class TaskCardComponent {
424
+ task = input.required(...(ngDevMode ? [{ debugName: "task" }] : []));
425
+ cardClick = output();
426
+ editClick = output();
427
+ deleteClick = output();
428
+ MAX_VISIBLE = 3;
429
+ visibleAssignees = computed(() => (this.task().assignees ?? []).slice(0, this.MAX_VISIBLE), ...(ngDevMode ? [{ debugName: "visibleAssignees" }] : []));
430
+ overflowCount = computed(() => Math.max(0, (this.task().assignees ?? []).length - this.MAX_VISIBLE), ...(ngDevMode ? [{ debugName: "overflowCount" }] : []));
431
+ prioritySeverity = taskPrioritySeverity;
432
+ initials = memberInitials;
433
+ isOverdue = isTaskOverdue;
434
+ memberColor = memberColorFromId;
435
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
436
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: TaskCardComponent, isStandalone: true, selector: "tm-task-card", inputs: { task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { cardClick: "cardClick", editClick: "editClick", deleteClick: "deleteClick" }, ngImport: i0, template: `
437
+ <div
438
+ class="task-card group relative rounded-lg border border-surface-200 dark:border-surface-700 p-3 cursor-pointer shadow-sm hover:shadow-md transition-shadow"
439
+ (click)="cardClick.emit(task())"
440
+ >
441
+ <div class="absolute top-2 right-2 hidden group-hover:flex gap-0.5">
442
+ <button
443
+ class="w-6 h-6 flex items-center justify-center rounded text-surface-400 hover:text-primary-500 hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors"
444
+ [pTooltip]="'shared.edit' | translate"
445
+ tooltipPosition="top"
446
+ (click)="$event.stopPropagation(); editClick.emit(task())"
447
+ >
448
+ <i class="pi pi-pencil text-xs"></i>
449
+ </button>
450
+ <button
451
+ class="w-6 h-6 flex items-center justify-center rounded text-surface-400 hover:text-red-500 hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors"
452
+ [pTooltip]="'shared.delete' | translate"
453
+ tooltipPosition="top"
454
+ (click)="$event.stopPropagation(); deleteClick.emit(task())"
455
+ >
456
+ <i class="pi pi-trash text-xs"></i>
457
+ </button>
458
+ </div>
459
+
460
+ @if (task().labels && task().labels!.length > 0) {
461
+ <div class="flex flex-wrap gap-1 mb-2">
462
+ @for (label of task().labels; track label.id) {
463
+ <span
464
+ class="inline-block h-1.5 rounded-full w-8"
465
+ [style.background-color]="label.color"
466
+ [pTooltip]="label.name"
467
+ ></span>
468
+ }
469
+ </div>
470
+ }
471
+
472
+ <p class="text-sm font-semibold line-clamp-2 leading-snug pr-14">
473
+ {{ task().title }}
474
+ </p>
475
+
476
+ <div class="flex items-center justify-between gap-2 mt-auto pt-2">
477
+ <p-tag
478
+ [value]="task().priority"
479
+ [severity]="prioritySeverity(task().priority)"
480
+ styleClass="text-xs"
481
+ />
482
+
483
+ @if (task().dueDate) {
484
+ <span
485
+ class="text-xs text-surface-500"
486
+ [class.text-red-500]="isOverdue(task().dueDate!)"
487
+ >
488
+ {{ task().dueDate | date: 'MMM d' }}
489
+ </span>
490
+ }
491
+
492
+ @if (visibleAssignees().length > 0) {
493
+ <div class="flex items-center -space-x-1.5">
494
+ @for (a of visibleAssignees(); track a.userId) {
495
+ <span
496
+ class="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
497
+ [style.background-color]="memberColor(a.userId)"
498
+ [pTooltip]="a.userName ?? undefined"
499
+ >
500
+ {{ initials(a.userName) }}
501
+ </span>
502
+ }
503
+ @if (overflowCount() > 0) {
504
+ <span
505
+ class="w-6 h-6 rounded-full bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300 flex items-center justify-center text-[10px] font-bold flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
506
+ >
507
+ +{{ overflowCount() }}
508
+ </span>
509
+ }
510
+ </div>
511
+ }
512
+ </div>
513
+ </div>
514
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: TagModule }, { kind: "component", type: i1.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i2.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
515
+ }
516
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskCardComponent, decorators: [{
517
+ type: Component,
518
+ args: [{
519
+ selector: 'tm-task-card',
520
+ imports: [TagModule, TooltipModule, DatePipe, TranslatePipe],
521
+ template: `
522
+ <div
523
+ class="task-card group relative rounded-lg border border-surface-200 dark:border-surface-700 p-3 cursor-pointer shadow-sm hover:shadow-md transition-shadow"
524
+ (click)="cardClick.emit(task())"
525
+ >
526
+ <div class="absolute top-2 right-2 hidden group-hover:flex gap-0.5">
527
+ <button
528
+ class="w-6 h-6 flex items-center justify-center rounded text-surface-400 hover:text-primary-500 hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors"
529
+ [pTooltip]="'shared.edit' | translate"
530
+ tooltipPosition="top"
531
+ (click)="$event.stopPropagation(); editClick.emit(task())"
532
+ >
533
+ <i class="pi pi-pencil text-xs"></i>
534
+ </button>
535
+ <button
536
+ class="w-6 h-6 flex items-center justify-center rounded text-surface-400 hover:text-red-500 hover:bg-surface-100 dark:hover:bg-surface-700 transition-colors"
537
+ [pTooltip]="'shared.delete' | translate"
538
+ tooltipPosition="top"
539
+ (click)="$event.stopPropagation(); deleteClick.emit(task())"
540
+ >
541
+ <i class="pi pi-trash text-xs"></i>
542
+ </button>
543
+ </div>
544
+
545
+ @if (task().labels && task().labels!.length > 0) {
546
+ <div class="flex flex-wrap gap-1 mb-2">
547
+ @for (label of task().labels; track label.id) {
548
+ <span
549
+ class="inline-block h-1.5 rounded-full w-8"
550
+ [style.background-color]="label.color"
551
+ [pTooltip]="label.name"
552
+ ></span>
553
+ }
554
+ </div>
555
+ }
556
+
557
+ <p class="text-sm font-semibold line-clamp-2 leading-snug pr-14">
558
+ {{ task().title }}
559
+ </p>
560
+
561
+ <div class="flex items-center justify-between gap-2 mt-auto pt-2">
562
+ <p-tag
563
+ [value]="task().priority"
564
+ [severity]="prioritySeverity(task().priority)"
565
+ styleClass="text-xs"
566
+ />
567
+
568
+ @if (task().dueDate) {
569
+ <span
570
+ class="text-xs text-surface-500"
571
+ [class.text-red-500]="isOverdue(task().dueDate!)"
572
+ >
573
+ {{ task().dueDate | date: 'MMM d' }}
574
+ </span>
575
+ }
576
+
577
+ @if (visibleAssignees().length > 0) {
578
+ <div class="flex items-center -space-x-1.5">
579
+ @for (a of visibleAssignees(); track a.userId) {
580
+ <span
581
+ class="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
582
+ [style.background-color]="memberColor(a.userId)"
583
+ [pTooltip]="a.userName ?? undefined"
584
+ >
585
+ {{ initials(a.userName) }}
586
+ </span>
587
+ }
588
+ @if (overflowCount() > 0) {
589
+ <span
590
+ class="w-6 h-6 rounded-full bg-surface-200 dark:bg-surface-700 text-surface-600 dark:text-surface-300 flex items-center justify-center text-[10px] font-bold flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
591
+ >
592
+ +{{ overflowCount() }}
593
+ </span>
594
+ }
595
+ </div>
596
+ }
597
+ </div>
598
+ </div>
599
+ `,
600
+ }]
601
+ }], propDecorators: { task: [{ type: i0.Input, args: [{ isSignal: true, alias: "task", required: true }] }], cardClick: [{ type: i0.Output, args: ["cardClick"] }], editClick: [{ type: i0.Output, args: ["editClick"] }], deleteClick: [{ type: i0.Output, args: ["deleteClick"] }] } });
602
+
603
+ const PRIORITY_OPTIONS = [
604
+ { label: 'task.priority.LOW', value: 'LOW' },
605
+ { label: 'task.priority.MEDIUM', value: 'MEDIUM' },
606
+ { label: 'task.priority.HIGH', value: 'HIGH' },
607
+ { label: 'task.priority.URGENT', value: 'URGENT' },
608
+ ];
609
+ class TaskFormDialogComponent {
610
+ taskApi = inject(TaskApiService);
611
+ visible = model(false, ...(ngDevMode ? [{ debugName: "visible" }] : []));
612
+ task = input(null, ...(ngDevMode ? [{ debugName: "task" }] : []));
613
+ boardId = input.required(...(ngDevMode ? [{ debugName: "boardId" }] : []));
614
+ listId = input.required(...(ngDevMode ? [{ debugName: "listId" }] : []));
615
+ members = input([], ...(ngDevMode ? [{ debugName: "members" }] : []));
616
+ labels = input([], ...(ngDevMode ? [{ debugName: "labels" }] : []));
617
+ saved = output();
618
+ isSaving = signal(false, ...(ngDevMode ? [{ debugName: "isSaving" }] : []));
619
+ priorityOptions = PRIORITY_OPTIONS;
620
+ title = '';
621
+ description = '';
622
+ priority = 'MEDIUM';
623
+ dueDate = null;
624
+ selectedAssigneeIds = [];
625
+ selectedLabelIds = [];
626
+ constructor() {
627
+ effect(() => {
628
+ const t = this.task();
629
+ if (t) {
630
+ this.title = t.title;
631
+ this.description = t.description ?? '';
632
+ this.priority = t.priority;
633
+ this.dueDate = t.dueDate ? new Date(t.dueDate) : null;
634
+ this.selectedAssigneeIds = t.assignees?.map((a) => a.userId) ?? [];
635
+ this.selectedLabelIds = t.labelIds ?? [];
636
+ }
637
+ else {
638
+ this.reset();
639
+ }
640
+ });
641
+ }
642
+ async onSave() {
643
+ if (!this.title.trim())
644
+ return;
645
+ this.isSaving.set(true);
646
+ try {
647
+ const dueDateStr = this.dueDate
648
+ ? this.dueDate.toISOString().slice(0, 10)
649
+ : undefined;
650
+ const existing = this.task();
651
+ const priority = this.priority;
652
+ let result;
653
+ if (existing) {
654
+ const dto = {
655
+ id: existing.id,
656
+ title: this.title,
657
+ description: this.description || undefined,
658
+ priority,
659
+ dueDate: dueDateStr,
660
+ assigneeIds: this.selectedAssigneeIds,
661
+ labelIds: this.selectedLabelIds,
662
+ };
663
+ result = await this.taskApi.update(dto);
664
+ }
665
+ else {
666
+ const dto = {
667
+ boardId: this.boardId(),
668
+ listId: this.listId(),
669
+ title: this.title,
670
+ description: this.description || undefined,
671
+ priority,
672
+ dueDate: dueDateStr,
673
+ assigneeIds: this.selectedAssigneeIds,
674
+ labelIds: this.selectedLabelIds,
675
+ };
676
+ result = await this.taskApi.insert(dto);
677
+ }
678
+ if (result.data)
679
+ this.saved.emit(result.data);
680
+ this.visible.set(false);
681
+ }
682
+ finally {
683
+ this.isSaving.set(false);
684
+ }
685
+ }
686
+ onClose() {
687
+ this.visible.set(false);
688
+ this.reset();
689
+ }
690
+ reset() {
691
+ this.title = '';
692
+ this.description = '';
693
+ this.priority = 'MEDIUM';
694
+ this.dueDate = null;
695
+ this.selectedAssigneeIds = [];
696
+ this.selectedLabelIds = [];
697
+ }
698
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskFormDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
699
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.5", type: TaskFormDialogComponent, isStandalone: true, selector: "tm-task-form-dialog", inputs: { visible: { classPropertyName: "visible", publicName: "visible", isSignal: true, isRequired: false, transformFunction: null }, task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: false, transformFunction: null }, boardId: { classPropertyName: "boardId", publicName: "boardId", isSignal: true, isRequired: true, transformFunction: null }, listId: { classPropertyName: "listId", publicName: "listId", isSignal: true, isRequired: true, transformFunction: null }, members: { classPropertyName: "members", publicName: "members", isSignal: true, isRequired: false, transformFunction: null }, labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { visible: "visibleChange", saved: "saved" }, ngImport: i0, template: `
700
+ <p-dialog
701
+ [header]="
702
+ (task() ? 'task.form.title.edit' : 'task.form.title.create') | translate
703
+ "
704
+ [(visible)]="visible"
705
+ [modal]="true"
706
+ [style]="{ width: '540px' }"
707
+ (onHide)="onClose()"
708
+ >
709
+ <div class="flex flex-col gap-4 py-3">
710
+ <div class="flex flex-col gap-1">
711
+ <label class="text-sm font-medium"
712
+ >{{ 'task.form.title.label' | translate }}
713
+ <span class="text-red-500">*</span></label
714
+ >
715
+ <input
716
+ pInputText
717
+ [(ngModel)]="title"
718
+ [placeholder]="'task.form.title.placeholder' | translate"
719
+ />
720
+ </div>
721
+
722
+ <div class="flex flex-col gap-1">
723
+ <label class="text-sm font-medium">{{
724
+ 'shared.description' | translate
725
+ }}</label>
726
+ <textarea
727
+ pTextarea
728
+ [(ngModel)]="description"
729
+ rows="3"
730
+ [placeholder]="'task.form.description.placeholder' | translate"
731
+ ></textarea>
732
+ </div>
733
+
734
+ <div class="grid grid-cols-2 gap-4">
735
+ <div class="flex flex-col gap-1">
736
+ <label class="text-sm font-medium">{{
737
+ 'task.form.priority.label' | translate
738
+ }}</label>
739
+ <p-select
740
+ [(ngModel)]="priority"
741
+ [options]="priorityOptions"
742
+ optionLabel="label"
743
+ optionValue="value"
744
+ [placeholder]="'task.form.priority.placeholder' | translate"
745
+ >
746
+ <ng-template #selectedItem let-opt>
747
+ {{ opt.label | translate }}
748
+ </ng-template>
749
+ <ng-template #item let-opt>
750
+ {{ opt.label | translate }}
751
+ </ng-template>
752
+ </p-select>
753
+ </div>
754
+
755
+ <div class="flex flex-col gap-1">
756
+ <label class="text-sm font-medium">{{
757
+ 'task.form.due.date.label' | translate
758
+ }}</label>
759
+ <p-datepicker
760
+ [(ngModel)]="dueDate"
761
+ [showClear]="true"
762
+ [placeholder]="'task.form.due.date.placeholder' | translate"
763
+ />
764
+ </div>
765
+ </div>
766
+
767
+ <div class="flex flex-col gap-1">
768
+ <label class="text-sm font-medium">{{
769
+ 'task.form.assignee.label' | translate
770
+ }}</label>
771
+ <p-multiselect
772
+ [(ngModel)]="selectedAssigneeIds"
773
+ [options]="members()"
774
+ optionLabel="userName"
775
+ optionValue="userId"
776
+ appendTo="body"
777
+ [placeholder]="'task.form.assignee.placeholder' | translate"
778
+ [showClear]="true"
779
+ />
780
+ </div>
781
+
782
+ <div class="flex flex-col gap-1">
783
+ <label class="text-sm font-medium">{{
784
+ 'task.form.labels.label' | translate
785
+ }}</label>
786
+ <p-multiselect
787
+ [(ngModel)]="selectedLabelIds"
788
+ [options]="labels()"
789
+ optionLabel="name"
790
+ optionValue="id"
791
+ appendTo="body"
792
+ [placeholder]="'task.form.labels.placeholder' | translate"
793
+ />
794
+ </div>
795
+ </div>
796
+
797
+ <ng-template pTemplate="footer">
798
+ <div class="flex justify-end gap-2 pt-1">
799
+ <p-button
800
+ [label]="'shared.cancel' | translate"
801
+ severity="secondary"
802
+ (onClick)="onClose()"
803
+ />
804
+ <p-button
805
+ [label]="'shared.save' | translate"
806
+ [loading]="isSaving()"
807
+ (onClick)="onSave()"
808
+ />
809
+ </div>
810
+ </ng-template>
811
+ </p-dialog>
812
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: DialogModule }, { kind: "component", type: i1$1.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i2$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i4.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "ngmodule", type: SelectModule }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: DatePickerModule }, { kind: "component", type: i7.DatePicker, selector: "p-datePicker, p-datepicker, p-date-picker", inputs: ["iconDisplay", "styleClass", "inputStyle", "inputId", "inputStyleClass", "placeholder", "ariaLabelledBy", "ariaLabel", "iconAriaLabel", "dateFormat", "multipleSeparator", "rangeSeparator", "inline", "showOtherMonths", "selectOtherMonths", "showIcon", "icon", "readonlyInput", "shortYearCutoff", "hourFormat", "timeOnly", "stepHour", "stepMinute", "stepSecond", "showSeconds", "showOnFocus", "showWeek", "startWeekFromFirstDayOfYear", "showClear", "dataType", "selectionMode", "maxDateCount", "showButtonBar", "todayButtonStyleClass", "clearButtonStyleClass", "autofocus", "autoZIndex", "baseZIndex", "panelStyleClass", "panelStyle", "keepInvalid", "hideOnDateTimeSelect", "touchUI", "timeSeparator", "focusTrap", "showTransitionOptions", "hideTransitionOptions", "tabindex", "minDate", "maxDate", "disabledDates", "disabledDays", "showTime", "responsiveOptions", "numberOfMonths", "firstDayOfWeek", "view", "defaultDate", "appendTo", "motionOptions"], outputs: ["onFocus", "onBlur", "onClose", "onSelect", "onClear", "onInput", "onTodayClick", "onClearClick", "onMonthChange", "onYearChange", "onClickOutside", "onShow"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i8.MultiSelect, selector: "p-multiSelect, p-multiselect, p-multi-select", inputs: ["id", "ariaLabel", "styleClass", "panelStyle", "panelStyleClass", "inputId", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "dataKey", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "chipIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "placeholder", "options", "filterValue", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus", "highlightOnSelect", "size", "variant", "fluid", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i9.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: "pipe", type: TranslatePipe, name: "translate" }] });
813
+ }
814
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskFormDialogComponent, decorators: [{
815
+ type: Component,
816
+ args: [{
817
+ selector: 'tm-task-form-dialog',
818
+ imports: [
819
+ DialogModule,
820
+ FormsModule,
821
+ InputTextModule,
822
+ TextareaModule,
823
+ SelectModule,
824
+ DatePickerModule,
825
+ MultiSelectModule,
826
+ ButtonModule,
827
+ TranslatePipe,
828
+ ],
829
+ template: `
830
+ <p-dialog
831
+ [header]="
832
+ (task() ? 'task.form.title.edit' : 'task.form.title.create') | translate
833
+ "
834
+ [(visible)]="visible"
835
+ [modal]="true"
836
+ [style]="{ width: '540px' }"
837
+ (onHide)="onClose()"
838
+ >
839
+ <div class="flex flex-col gap-4 py-3">
840
+ <div class="flex flex-col gap-1">
841
+ <label class="text-sm font-medium"
842
+ >{{ 'task.form.title.label' | translate }}
843
+ <span class="text-red-500">*</span></label
844
+ >
845
+ <input
846
+ pInputText
847
+ [(ngModel)]="title"
848
+ [placeholder]="'task.form.title.placeholder' | translate"
849
+ />
850
+ </div>
851
+
852
+ <div class="flex flex-col gap-1">
853
+ <label class="text-sm font-medium">{{
854
+ 'shared.description' | translate
855
+ }}</label>
856
+ <textarea
857
+ pTextarea
858
+ [(ngModel)]="description"
859
+ rows="3"
860
+ [placeholder]="'task.form.description.placeholder' | translate"
861
+ ></textarea>
862
+ </div>
863
+
864
+ <div class="grid grid-cols-2 gap-4">
865
+ <div class="flex flex-col gap-1">
866
+ <label class="text-sm font-medium">{{
867
+ 'task.form.priority.label' | translate
868
+ }}</label>
869
+ <p-select
870
+ [(ngModel)]="priority"
871
+ [options]="priorityOptions"
872
+ optionLabel="label"
873
+ optionValue="value"
874
+ [placeholder]="'task.form.priority.placeholder' | translate"
875
+ >
876
+ <ng-template #selectedItem let-opt>
877
+ {{ opt.label | translate }}
878
+ </ng-template>
879
+ <ng-template #item let-opt>
880
+ {{ opt.label | translate }}
881
+ </ng-template>
882
+ </p-select>
883
+ </div>
884
+
885
+ <div class="flex flex-col gap-1">
886
+ <label class="text-sm font-medium">{{
887
+ 'task.form.due.date.label' | translate
888
+ }}</label>
889
+ <p-datepicker
890
+ [(ngModel)]="dueDate"
891
+ [showClear]="true"
892
+ [placeholder]="'task.form.due.date.placeholder' | translate"
893
+ />
894
+ </div>
895
+ </div>
896
+
897
+ <div class="flex flex-col gap-1">
898
+ <label class="text-sm font-medium">{{
899
+ 'task.form.assignee.label' | translate
900
+ }}</label>
901
+ <p-multiselect
902
+ [(ngModel)]="selectedAssigneeIds"
903
+ [options]="members()"
904
+ optionLabel="userName"
905
+ optionValue="userId"
906
+ appendTo="body"
907
+ [placeholder]="'task.form.assignee.placeholder' | translate"
908
+ [showClear]="true"
909
+ />
910
+ </div>
911
+
912
+ <div class="flex flex-col gap-1">
913
+ <label class="text-sm font-medium">{{
914
+ 'task.form.labels.label' | translate
915
+ }}</label>
916
+ <p-multiselect
917
+ [(ngModel)]="selectedLabelIds"
918
+ [options]="labels()"
919
+ optionLabel="name"
920
+ optionValue="id"
921
+ appendTo="body"
922
+ [placeholder]="'task.form.labels.placeholder' | translate"
923
+ />
924
+ </div>
925
+ </div>
926
+
927
+ <ng-template pTemplate="footer">
928
+ <div class="flex justify-end gap-2 pt-1">
929
+ <p-button
930
+ [label]="'shared.cancel' | translate"
931
+ severity="secondary"
932
+ (onClick)="onClose()"
933
+ />
934
+ <p-button
935
+ [label]="'shared.save' | translate"
936
+ [loading]="isSaving()"
937
+ (onClick)="onSave()"
938
+ />
939
+ </div>
940
+ </ng-template>
941
+ </p-dialog>
942
+ `,
943
+ }]
944
+ }], ctorParameters: () => [], propDecorators: { visible: [{ type: i0.Input, args: [{ isSignal: true, alias: "visible", required: false }] }, { type: i0.Output, args: ["visibleChange"] }], task: [{ type: i0.Input, args: [{ isSignal: true, alias: "task", required: false }] }], boardId: [{ type: i0.Input, args: [{ isSignal: true, alias: "boardId", required: true }] }], listId: [{ type: i0.Input, args: [{ isSignal: true, alias: "listId", required: true }] }], members: [{ type: i0.Input, args: [{ isSignal: true, alias: "members", required: false }] }], labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: false }] }], saved: [{ type: i0.Output, args: ["saved"] }] } });
945
+
946
+ class TaskDetailPanelComponent {
947
+ commentApi = inject(TaskCommentApiService);
948
+ state = inject(TaskBoardStateService);
949
+ task = input(null, ...(ngDevMode ? [{ debugName: "task" }] : []));
950
+ closePanel = output();
951
+ editTask = output();
952
+ comments = signal([], ...(ngDevMode ? [{ debugName: "comments" }] : []));
953
+ isSendingComment = signal(false, ...(ngDevMode ? [{ debugName: "isSendingComment" }] : []));
954
+ isSendingReply = signal(false, ...(ngDevMode ? [{ debugName: "isSendingReply" }] : []));
955
+ replyingToId = signal(null, ...(ngDevMode ? [{ debugName: "replyingToId" }] : []));
956
+ isDeleting = signal(false, ...(ngDevMode ? [{ debugName: "isDeleting" }] : []));
957
+ newComment = '';
958
+ replyText = '';
959
+ assigneeMembers = computed(() => this.task()?.assignees ?? [], ...(ngDevMode ? [{ debugName: "assigneeMembers" }] : []));
960
+ rootComments = computed(() => this.comments().filter((c) => !c.parentCommentId), ...(ngDevMode ? [{ debugName: "rootComments" }] : []));
961
+ repliesFor(parentId) {
962
+ return this.comments().filter((c) => c.parentCommentId === parentId);
963
+ }
964
+ constructor() {
965
+ effect(() => {
966
+ const t = this.task();
967
+ if (t)
968
+ this.loadComments(t.id);
969
+ else {
970
+ this.comments.set([]);
971
+ this.replyingToId.set(null);
972
+ }
973
+ });
974
+ }
975
+ toggleReply(commentId) {
976
+ if (this.replyingToId() === commentId) {
977
+ this.cancelReply();
978
+ }
979
+ else {
980
+ this.replyingToId.set(commentId);
981
+ this.replyText = '';
982
+ }
983
+ }
984
+ cancelReply() {
985
+ this.replyingToId.set(null);
986
+ this.replyText = '';
987
+ }
988
+ async deleteTask(taskId) {
989
+ this.isDeleting.set(true);
990
+ try {
991
+ await this.state.deleteTask(taskId);
992
+ this.closePanel.emit();
993
+ }
994
+ finally {
995
+ this.isDeleting.set(false);
996
+ }
997
+ }
998
+ prioritySeverity = taskPrioritySeverity;
999
+ initials = memberInitials;
1000
+ async loadComments(taskId) {
1001
+ const res = await this.commentApi.getAll({ filter: { taskId } });
1002
+ this.comments.set(res.data ?? []);
1003
+ }
1004
+ async addComment(taskId) {
1005
+ if (!this.newComment.trim())
1006
+ return;
1007
+ this.isSendingComment.set(true);
1008
+ try {
1009
+ const res = await this.commentApi.insert({
1010
+ taskId,
1011
+ content: this.newComment,
1012
+ });
1013
+ if (res.data)
1014
+ this.comments.update((list) => [...list, res.data]);
1015
+ this.newComment = '';
1016
+ }
1017
+ finally {
1018
+ this.isSendingComment.set(false);
1019
+ }
1020
+ }
1021
+ async submitReply(taskId, parentCommentId) {
1022
+ if (!this.replyText.trim())
1023
+ return;
1024
+ this.isSendingReply.set(true);
1025
+ try {
1026
+ const res = await this.commentApi.insert({
1027
+ taskId,
1028
+ content: this.replyText,
1029
+ parentCommentId,
1030
+ });
1031
+ if (res.data)
1032
+ this.comments.update((list) => [...list, res.data]);
1033
+ this.cancelReply();
1034
+ }
1035
+ finally {
1036
+ this.isSendingReply.set(false);
1037
+ }
1038
+ }
1039
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskDetailPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1040
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: TaskDetailPanelComponent, isStandalone: true, selector: "tm-task-detail-panel", inputs: { task: { classPropertyName: "task", publicName: "task", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { closePanel: "closePanel", editTask: "editTask" }, ngImport: i0, template: `
1041
+ <p-drawer
1042
+ [visible]="!!task()"
1043
+ position="right"
1044
+ [style]="{ width: '500px' }"
1045
+ [header]="task()?.title ?? ''"
1046
+ (onHide)="closePanel.emit()"
1047
+ >
1048
+ @if (task(); as t) {
1049
+ <div class="flex flex-col gap-6 h-full overflow-y-auto pb-4">
1050
+ <div class="flex items-center justify-end gap-1">
1051
+ <p-button
1052
+ icon="pi pi-pencil"
1053
+ severity="secondary"
1054
+ size="small"
1055
+ [text]="true"
1056
+ [pTooltip]="'task.detail.edit' | translate"
1057
+ tooltipPosition="left"
1058
+ (onClick)="editTask.emit(t)"
1059
+ />
1060
+ <p-button
1061
+ icon="pi pi-trash"
1062
+ severity="danger"
1063
+ size="small"
1064
+ [text]="true"
1065
+ [loading]="isDeleting()"
1066
+ [pTooltip]="'task.detail.delete' | translate"
1067
+ tooltipPosition="left"
1068
+ (onClick)="deleteTask(t.id)"
1069
+ />
1070
+ </div>
1071
+
1072
+ <div class="flex items-center gap-3">
1073
+ <p-tag
1074
+ [value]="t.priority"
1075
+ [severity]="prioritySeverity(t.priority)"
1076
+ />
1077
+ @if (t.dueDate) {
1078
+ <span class="text-sm text-surface-500">
1079
+ {{ 'task.detail.due.label' | translate }}:
1080
+ {{ t.dueDate | date: 'mediumDate' }}
1081
+ </span>
1082
+ }
1083
+ </div>
1084
+
1085
+ @if (assigneeMembers().length > 0) {
1086
+ <div>
1087
+ <h4 class="text-sm font-semibold mb-2">
1088
+ {{ 'task.detail.assignees.label' | translate }}
1089
+ </h4>
1090
+ <div class="flex flex-wrap gap-2">
1091
+ @for (member of assigneeMembers(); track member.userId) {
1092
+ <div class="flex items-center gap-2 px-2 py-1 rounded-full ">
1093
+ <span
1094
+ class="w-6 h-6 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 flex items-center justify-center text-xs font-semibold"
1095
+ >
1096
+ {{ initials(member.userName) }}
1097
+ </span>
1098
+ <span class="text-sm">{{ member.userName }}</span>
1099
+ </div>
1100
+ }
1101
+ </div>
1102
+ </div>
1103
+ }
1104
+
1105
+ <div>
1106
+ <h4 class="text-sm font-semibold mb-2">
1107
+ {{ 'shared.description' | translate }}
1108
+ </h4>
1109
+ <p
1110
+ class="text-sm text-surface-600 dark:text-surface-400 whitespace-pre-wrap"
1111
+ >
1112
+ {{
1113
+ t.description || ('task.detail.description.empty' | translate)
1114
+ }}
1115
+ </p>
1116
+ </div>
1117
+
1118
+ <div class="flex flex-col gap-1">
1119
+ <h4 class="text-sm font-semibold mb-1">
1120
+ {{ 'task.detail.comments.label' | translate }} ({{
1121
+ rootComments().length
1122
+ }})
1123
+ </h4>
1124
+
1125
+ @for (comment of rootComments(); track comment.id) {
1126
+ <div
1127
+ class="group flex flex-col gap-1 py-2 border-b border-surface-100 dark:border-surface-800 last:border-0"
1128
+ >
1129
+ <div class="flex items-center justify-between">
1130
+ <span class="text-xs text-surface-400">{{
1131
+ comment.createdAt | date: 'MMM d, y, h:mm a'
1132
+ }}</span>
1133
+ <p-button
1134
+ [label]="'task.comment.reply' | translate"
1135
+ icon="pi pi-reply"
1136
+ size="small"
1137
+ [text]="true"
1138
+ severity="secondary"
1139
+ styleClass="opacity-0 group-hover:opacity-100 transition-opacity !py-0.5 !px-1.5 text-xs"
1140
+ (onClick)="toggleReply(comment.id)"
1141
+ />
1142
+ </div>
1143
+ <p class="text-sm">{{ comment.content }}</p>
1144
+
1145
+ @if (repliesFor(comment.id).length > 0) {
1146
+ <div
1147
+ class="mt-1 pl-3 border-l-2 border-surface-200 dark:border-surface-700 flex flex-col gap-2"
1148
+ >
1149
+ @for (reply of repliesFor(comment.id); track reply.id) {
1150
+ <div class="flex flex-col gap-0.5">
1151
+ <span class="text-xs text-surface-400">{{
1152
+ reply.createdAt | date: 'MMM d, y, h:mm a'
1153
+ }}</span>
1154
+ <p class="text-sm">{{ reply.content }}</p>
1155
+ </div>
1156
+ }
1157
+ </div>
1158
+ }
1159
+
1160
+ @if (replyingToId() === comment.id) {
1161
+ <div
1162
+ class="mt-2 pl-3 border-l-2 border-primary-300 dark:border-primary-700 flex gap-2"
1163
+ >
1164
+ <textarea
1165
+ pTextarea
1166
+ [(ngModel)]="replyText"
1167
+ rows="2"
1168
+ [placeholder]="
1169
+ 'task.comment.reply.placeholder' | translate
1170
+ "
1171
+ class="flex-1 resize-none text-sm"
1172
+ autoResize="true"
1173
+ ></textarea>
1174
+ <div class="flex flex-col gap-1">
1175
+ <p-button
1176
+ icon="pi pi-send"
1177
+ size="small"
1178
+ [loading]="isSendingReply()"
1179
+ [disabled]="!replyText.trim()"
1180
+ (onClick)="submitReply(t.id, comment.id)"
1181
+ />
1182
+ <p-button
1183
+ icon="pi pi-times"
1184
+ size="small"
1185
+ severity="secondary"
1186
+ [text]="true"
1187
+ (onClick)="cancelReply()"
1188
+ />
1189
+ </div>
1190
+ </div>
1191
+ }
1192
+ </div>
1193
+ }
1194
+
1195
+ @if (rootComments().length === 0) {
1196
+ <p class="text-sm text-surface-400 py-2">
1197
+ {{ 'task.detail.comments.empty' | translate }}
1198
+ </p>
1199
+ }
1200
+
1201
+ <div class="flex gap-2 mt-3">
1202
+ <textarea
1203
+ pTextarea
1204
+ [(ngModel)]="newComment"
1205
+ rows="2"
1206
+ [placeholder]="'task.detail.comment.placeholder' | translate"
1207
+ class="flex-1 resize-none"
1208
+ ></textarea>
1209
+ <p-button
1210
+ icon="pi pi-send"
1211
+ [loading]="isSendingComment()"
1212
+ [disabled]="!newComment.trim()"
1213
+ (onClick)="addComment(t.id)"
1214
+ />
1215
+ </div>
1216
+ </div>
1217
+ </div>
1218
+ }
1219
+ </p-drawer>
1220
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: DrawerModule }, { kind: "component", type: i1$2.Drawer, selector: "p-drawer", inputs: ["appendTo", "motionOptions", "blockScroll", "style", "styleClass", "ariaCloseLabel", "autoZIndex", "baseZIndex", "modal", "closeButtonProps", "dismissible", "showCloseIcon", "closeOnEscape", "transitionOptions", "visible", "position", "fullScreen", "header", "maskStyle", "closable"], outputs: ["onShow", "onHide", "visibleChange"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i9.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: TagModule }, { kind: "component", type: i1.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: TextareaModule }, { kind: "directive", type: i5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i2.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "pipe", type: DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1221
+ }
1222
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: TaskDetailPanelComponent, decorators: [{
1223
+ type: Component,
1224
+ args: [{
1225
+ selector: 'tm-task-detail-panel',
1226
+ imports: [
1227
+ DrawerModule,
1228
+ ButtonModule,
1229
+ TagModule,
1230
+ DatePipe,
1231
+ FormsModule,
1232
+ TextareaModule,
1233
+ TooltipModule,
1234
+ TranslatePipe,
1235
+ ],
1236
+ template: `
1237
+ <p-drawer
1238
+ [visible]="!!task()"
1239
+ position="right"
1240
+ [style]="{ width: '500px' }"
1241
+ [header]="task()?.title ?? ''"
1242
+ (onHide)="closePanel.emit()"
1243
+ >
1244
+ @if (task(); as t) {
1245
+ <div class="flex flex-col gap-6 h-full overflow-y-auto pb-4">
1246
+ <div class="flex items-center justify-end gap-1">
1247
+ <p-button
1248
+ icon="pi pi-pencil"
1249
+ severity="secondary"
1250
+ size="small"
1251
+ [text]="true"
1252
+ [pTooltip]="'task.detail.edit' | translate"
1253
+ tooltipPosition="left"
1254
+ (onClick)="editTask.emit(t)"
1255
+ />
1256
+ <p-button
1257
+ icon="pi pi-trash"
1258
+ severity="danger"
1259
+ size="small"
1260
+ [text]="true"
1261
+ [loading]="isDeleting()"
1262
+ [pTooltip]="'task.detail.delete' | translate"
1263
+ tooltipPosition="left"
1264
+ (onClick)="deleteTask(t.id)"
1265
+ />
1266
+ </div>
1267
+
1268
+ <div class="flex items-center gap-3">
1269
+ <p-tag
1270
+ [value]="t.priority"
1271
+ [severity]="prioritySeverity(t.priority)"
1272
+ />
1273
+ @if (t.dueDate) {
1274
+ <span class="text-sm text-surface-500">
1275
+ {{ 'task.detail.due.label' | translate }}:
1276
+ {{ t.dueDate | date: 'mediumDate' }}
1277
+ </span>
1278
+ }
1279
+ </div>
1280
+
1281
+ @if (assigneeMembers().length > 0) {
1282
+ <div>
1283
+ <h4 class="text-sm font-semibold mb-2">
1284
+ {{ 'task.detail.assignees.label' | translate }}
1285
+ </h4>
1286
+ <div class="flex flex-wrap gap-2">
1287
+ @for (member of assigneeMembers(); track member.userId) {
1288
+ <div class="flex items-center gap-2 px-2 py-1 rounded-full ">
1289
+ <span
1290
+ class="w-6 h-6 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 flex items-center justify-center text-xs font-semibold"
1291
+ >
1292
+ {{ initials(member.userName) }}
1293
+ </span>
1294
+ <span class="text-sm">{{ member.userName }}</span>
1295
+ </div>
1296
+ }
1297
+ </div>
1298
+ </div>
1299
+ }
1300
+
1301
+ <div>
1302
+ <h4 class="text-sm font-semibold mb-2">
1303
+ {{ 'shared.description' | translate }}
1304
+ </h4>
1305
+ <p
1306
+ class="text-sm text-surface-600 dark:text-surface-400 whitespace-pre-wrap"
1307
+ >
1308
+ {{
1309
+ t.description || ('task.detail.description.empty' | translate)
1310
+ }}
1311
+ </p>
1312
+ </div>
1313
+
1314
+ <div class="flex flex-col gap-1">
1315
+ <h4 class="text-sm font-semibold mb-1">
1316
+ {{ 'task.detail.comments.label' | translate }} ({{
1317
+ rootComments().length
1318
+ }})
1319
+ </h4>
1320
+
1321
+ @for (comment of rootComments(); track comment.id) {
1322
+ <div
1323
+ class="group flex flex-col gap-1 py-2 border-b border-surface-100 dark:border-surface-800 last:border-0"
1324
+ >
1325
+ <div class="flex items-center justify-between">
1326
+ <span class="text-xs text-surface-400">{{
1327
+ comment.createdAt | date: 'MMM d, y, h:mm a'
1328
+ }}</span>
1329
+ <p-button
1330
+ [label]="'task.comment.reply' | translate"
1331
+ icon="pi pi-reply"
1332
+ size="small"
1333
+ [text]="true"
1334
+ severity="secondary"
1335
+ styleClass="opacity-0 group-hover:opacity-100 transition-opacity !py-0.5 !px-1.5 text-xs"
1336
+ (onClick)="toggleReply(comment.id)"
1337
+ />
1338
+ </div>
1339
+ <p class="text-sm">{{ comment.content }}</p>
1340
+
1341
+ @if (repliesFor(comment.id).length > 0) {
1342
+ <div
1343
+ class="mt-1 pl-3 border-l-2 border-surface-200 dark:border-surface-700 flex flex-col gap-2"
1344
+ >
1345
+ @for (reply of repliesFor(comment.id); track reply.id) {
1346
+ <div class="flex flex-col gap-0.5">
1347
+ <span class="text-xs text-surface-400">{{
1348
+ reply.createdAt | date: 'MMM d, y, h:mm a'
1349
+ }}</span>
1350
+ <p class="text-sm">{{ reply.content }}</p>
1351
+ </div>
1352
+ }
1353
+ </div>
1354
+ }
1355
+
1356
+ @if (replyingToId() === comment.id) {
1357
+ <div
1358
+ class="mt-2 pl-3 border-l-2 border-primary-300 dark:border-primary-700 flex gap-2"
1359
+ >
1360
+ <textarea
1361
+ pTextarea
1362
+ [(ngModel)]="replyText"
1363
+ rows="2"
1364
+ [placeholder]="
1365
+ 'task.comment.reply.placeholder' | translate
1366
+ "
1367
+ class="flex-1 resize-none text-sm"
1368
+ autoResize="true"
1369
+ ></textarea>
1370
+ <div class="flex flex-col gap-1">
1371
+ <p-button
1372
+ icon="pi pi-send"
1373
+ size="small"
1374
+ [loading]="isSendingReply()"
1375
+ [disabled]="!replyText.trim()"
1376
+ (onClick)="submitReply(t.id, comment.id)"
1377
+ />
1378
+ <p-button
1379
+ icon="pi pi-times"
1380
+ size="small"
1381
+ severity="secondary"
1382
+ [text]="true"
1383
+ (onClick)="cancelReply()"
1384
+ />
1385
+ </div>
1386
+ </div>
1387
+ }
1388
+ </div>
1389
+ }
1390
+
1391
+ @if (rootComments().length === 0) {
1392
+ <p class="text-sm text-surface-400 py-2">
1393
+ {{ 'task.detail.comments.empty' | translate }}
1394
+ </p>
1395
+ }
1396
+
1397
+ <div class="flex gap-2 mt-3">
1398
+ <textarea
1399
+ pTextarea
1400
+ [(ngModel)]="newComment"
1401
+ rows="2"
1402
+ [placeholder]="'task.detail.comment.placeholder' | translate"
1403
+ class="flex-1 resize-none"
1404
+ ></textarea>
1405
+ <p-button
1406
+ icon="pi pi-send"
1407
+ [loading]="isSendingComment()"
1408
+ [disabled]="!newComment.trim()"
1409
+ (onClick)="addComment(t.id)"
1410
+ />
1411
+ </div>
1412
+ </div>
1413
+ </div>
1414
+ }
1415
+ </p-drawer>
1416
+ `,
1417
+ }]
1418
+ }], ctorParameters: () => [], propDecorators: { task: [{ type: i0.Input, args: [{ isSignal: true, alias: "task", required: false }] }], closePanel: [{ type: i0.Output, args: ["closePanel"] }], editTask: [{ type: i0.Output, args: ["editTask"] }] } });
1419
+
1420
+ class BoardListPageComponent {
1421
+ state = inject(TaskBoardStateService);
1422
+ boardApi = inject(TaskBoardApiService);
1423
+ router = inject(Router);
1424
+ showCreateDialog = signal(false, ...(ngDevMode ? [{ debugName: "showCreateDialog" }] : []));
1425
+ isCreating = signal(false, ...(ngDevMode ? [{ debugName: "isCreating" }] : []));
1426
+ newBoardName = '';
1427
+ newBoardDescription = '';
1428
+ newBoardColor = '#6366f1';
1429
+ boardColors = BOARD_COLOR_PALETTE;
1430
+ constructor() {
1431
+ this.state.loadBoards();
1432
+ }
1433
+ openBoard(boardId) {
1434
+ this.router.navigate(['/task-manager/board', boardId]);
1435
+ }
1436
+ closeCreateDialog() {
1437
+ this.showCreateDialog.set(false);
1438
+ this.newBoardName = '';
1439
+ this.newBoardDescription = '';
1440
+ this.newBoardColor = '#6366f1';
1441
+ }
1442
+ async createBoard() {
1443
+ if (!this.newBoardName.trim())
1444
+ return;
1445
+ this.isCreating.set(true);
1446
+ try {
1447
+ await this.boardApi.insert({
1448
+ name: this.newBoardName,
1449
+ description: this.newBoardDescription || undefined,
1450
+ color: this.newBoardColor,
1451
+ });
1452
+ await this.state.loadBoards();
1453
+ this.closeCreateDialog();
1454
+ }
1455
+ finally {
1456
+ this.isCreating.set(false);
1457
+ }
1458
+ }
1459
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BoardListPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1460
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: BoardListPageComponent, isStandalone: true, selector: "tm-board-list-page", ngImport: i0, template: `
1461
+ <div class="p-2 sm:p-4">
1462
+ <div
1463
+ class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-4"
1464
+ >
1465
+ <div>
1466
+ <h1 class="text-xl sm:text-2xl font-bold m-0">
1467
+ {{ 'task.manager.boards.title' | translate }}
1468
+ </h1>
1469
+ <p class="text-muted-color mt-1 text-sm sm:text-base">
1470
+ {{ 'task.manager.boards.subtitle' | translate }}
1471
+ </p>
1472
+ </div>
1473
+ <p-button
1474
+ [label]="'task.manager.board.new' | translate"
1475
+ icon="pi pi-plus"
1476
+ (onClick)="showCreateDialog.set(true)"
1477
+ />
1478
+ </div>
1479
+
1480
+ @if (state.isLoading()) {
1481
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
1482
+ @for (i of [1, 2, 3, 4, 5, 6]; track i) {
1483
+ <div class="card !p-0 overflow-hidden">
1484
+ <p-skeleton height="4px" borderRadius="0" />
1485
+ <div class="p-5">
1486
+ <p-skeleton height="18px" width="60%" class="mb-3" />
1487
+ <p-skeleton height="13px" class="mb-1" />
1488
+ <p-skeleton height="13px" width="70%" class="mb-5" />
1489
+ <div class="flex gap-5 pt-4 border-t border-surface-100">
1490
+ <p-skeleton height="14px" width="55px" />
1491
+ <p-skeleton height="14px" width="45px" />
1492
+ <p-skeleton height="14px" width="50px" />
1493
+ </div>
1494
+ <p-skeleton height="11px" width="100px" class="mt-2" />
1495
+ </div>
1496
+ </div>
1497
+ }
1498
+ </div>
1499
+ } @else if (state.boards().length === 0) {
1500
+ <div
1501
+ class="flex flex-col items-center justify-center py-20 text-muted-color"
1502
+ >
1503
+ <span
1504
+ class="w-16 h-16 rounded-2xl border-2 border-dashed border-surface-300 flex items-center justify-center mb-4"
1505
+ >
1506
+ <i class="pi pi-th-large text-2xl text-surface-400"></i>
1507
+ </span>
1508
+ <p class="text-base font-semibold mb-1">
1509
+ {{ 'task.manager.board.empty' | translate }}
1510
+ </p>
1511
+ <p class="text-sm text-muted-color mb-5">
1512
+ {{ 'task.manager.board.empty.hint' | translate }}
1513
+ </p>
1514
+ <p-button
1515
+ [label]="'task.manager.board.new' | translate"
1516
+ icon="pi pi-plus"
1517
+ size="small"
1518
+ (onClick)="showCreateDialog.set(true)"
1519
+ />
1520
+ </div>
1521
+ } @else {
1522
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
1523
+ @for (board of state.boards(); track board.id) {
1524
+ <div
1525
+ class="card !p-0 cursor-pointer overflow-hidden group hover:shadow-lg transition-all duration-200"
1526
+ (click)="openBoard(board.id)"
1527
+ >
1528
+ <div class="h-1" [style.background-color]="board.color"></div>
1529
+
1530
+ <div class="p-5">
1531
+ <div class="flex items-start justify-between gap-3 mb-2">
1532
+ <div class="flex items-center gap-2 min-w-0">
1533
+ <h3
1534
+ class="font-semibold text-base m-0 truncate group-hover:text-primary-500 transition-colors duration-150"
1535
+ >
1536
+ {{ board.name }}
1537
+ </h3>
1538
+ </div>
1539
+ <i
1540
+ class="pi pi-arrow-right text-xs text-surface-300 group-hover:text-primary-500 group-hover:translate-x-0.5 transition-all duration-150 flex-shrink-0 mt-1"
1541
+ ></i>
1542
+ </div>
1543
+
1544
+ <p
1545
+ class="text-sm text-muted-color line-clamp-2 pl-[18px] mb-0 min-h-[40px]"
1546
+ >
1547
+ {{ board.description || '' }}
1548
+ </p>
1549
+
1550
+ <div
1551
+ class="border-t border-surface-100 dark:border-surface-700 mt-4 pt-4 space-y-2.5"
1552
+ >
1553
+ <div class="flex items-center gap-4">
1554
+ <span class="flex items-center gap-1.5">
1555
+ <i class="pi pi-users text-[11px] text-muted-color"></i>
1556
+ <span class="text-sm font-semibold">{{
1557
+ board.totalMembers ?? 0
1558
+ }}</span>
1559
+ <span class="text-xs text-muted-color">{{
1560
+ 'task.manager.board.members' | translate
1561
+ }}</span>
1562
+ </span>
1563
+ <span
1564
+ class="w-px h-3 bg-surface-200 dark:bg-surface-600"
1565
+ ></span>
1566
+ <span class="flex items-center gap-1.5">
1567
+ <i class="pi pi-list text-[11px] text-muted-color"></i>
1568
+ <span class="text-sm font-semibold">{{
1569
+ board.totalLists ?? 0
1570
+ }}</span>
1571
+ <span class="text-xs text-muted-color">{{
1572
+ 'task.manager.board.lists' | translate
1573
+ }}</span>
1574
+ </span>
1575
+ <span
1576
+ class="w-px h-3 bg-surface-200 dark:bg-surface-600"
1577
+ ></span>
1578
+ <span class="flex items-center gap-1.5">
1579
+ <i
1580
+ class="pi pi-check-square text-[11px] text-muted-color"
1581
+ ></i>
1582
+ <span class="text-sm font-semibold">{{
1583
+ board.totalTasks ?? 0
1584
+ }}</span>
1585
+ <span class="text-xs text-muted-color">{{
1586
+ 'task.manager.board.tasks' | translate
1587
+ }}</span>
1588
+ </span>
1589
+ </div>
1590
+ <div
1591
+ class="flex items-center gap-1.5 text-xs text-muted-color"
1592
+ >
1593
+ <i class="pi pi-clock text-[10px]"></i>
1594
+ <span>{{ board.createdAt | date: 'MMM d, yyyy' }}</span>
1595
+ </div>
1596
+ </div>
1597
+ </div>
1598
+ </div>
1599
+ }
1600
+ </div>
1601
+ }
1602
+ </div>
1603
+
1604
+ <p-dialog
1605
+ [header]="'task.manager.board.new' | translate"
1606
+ [(visible)]="showCreateDialog"
1607
+ [modal]="true"
1608
+ [style]="{ width: '440px' }"
1609
+ >
1610
+ <div class="flex flex-col gap-4 py-2">
1611
+ <div class="flex flex-col gap-1">
1612
+ <label class="text-sm font-medium"
1613
+ >{{ 'shared.name' | translate }}
1614
+ <span class="text-red-500">*</span></label
1615
+ >
1616
+ <input
1617
+ pInputText
1618
+ [(ngModel)]="newBoardName"
1619
+ [placeholder]="'task.manager.board.name.placeholder' | translate"
1620
+ />
1621
+ </div>
1622
+ <div class="flex flex-col gap-1">
1623
+ <label class="text-sm font-medium">{{
1624
+ 'shared.description' | translate
1625
+ }}</label>
1626
+ <textarea
1627
+ pTextarea
1628
+ [(ngModel)]="newBoardDescription"
1629
+ rows="2"
1630
+ [placeholder]="
1631
+ 'task.manager.board.description.placeholder' | translate
1632
+ "
1633
+ ></textarea>
1634
+ </div>
1635
+ <div class="flex flex-col gap-2">
1636
+ <label class="text-sm font-medium">{{
1637
+ 'task.manager.board.color.label' | translate
1638
+ }}</label>
1639
+ <div class="flex flex-wrap gap-2">
1640
+ @for (c of boardColors; track c) {
1641
+ <button
1642
+ type="button"
1643
+ class="w-8 h-8 rounded-full border-2 transition-all hover:scale-110 focus:outline-none flex items-center justify-center"
1644
+ [style.background-color]="c"
1645
+ [class.ring-2]="newBoardColor === c"
1646
+ [class.ring-offset-2]="newBoardColor === c"
1647
+ [class.ring-surface-900]="newBoardColor === c"
1648
+ [pTooltip]="c"
1649
+ (click)="newBoardColor = c"
1650
+ >
1651
+ @if (newBoardColor === c) {
1652
+ <i class="pi pi-check text-white text-xs"></i>
1653
+ }
1654
+ </button>
1655
+ }
1656
+ </div>
1657
+ <div class="flex items-center gap-2 mt-1">
1658
+ <span
1659
+ class="w-8 h-8 rounded-full border border-surface-200 flex-shrink-0"
1660
+ [style.background-color]="newBoardColor"
1661
+ ></span>
1662
+ <span class="text-sm text-surface-500 font-mono">{{
1663
+ newBoardColor
1664
+ }}</span>
1665
+ </div>
1666
+ </div>
1667
+ </div>
1668
+
1669
+ <ng-template pTemplate="footer">
1670
+ <div class="flex justify-end gap-2">
1671
+ <p-button
1672
+ [label]="'shared.cancel' | translate"
1673
+ severity="secondary"
1674
+ (onClick)="closeCreateDialog()"
1675
+ />
1676
+ <p-button
1677
+ [label]="'shared.create' | translate"
1678
+ [loading]="isCreating()"
1679
+ (onClick)="createBoard()"
1680
+ />
1681
+ </div>
1682
+ </ng-template>
1683
+ </p-dialog>
1684
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i2$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i9.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: "component", type: i1$1.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i4.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i6$1.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "directive", type: i5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "directive", type: i2.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "pipe", type: i9$1.DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
1685
+ }
1686
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BoardListPageComponent, decorators: [{
1687
+ type: Component,
1688
+ args: [{
1689
+ selector: 'tm-board-list-page',
1690
+ imports: [AngularModule, PrimeModule, TranslatePipe],
1691
+ template: `
1692
+ <div class="p-2 sm:p-4">
1693
+ <div
1694
+ class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 mb-4"
1695
+ >
1696
+ <div>
1697
+ <h1 class="text-xl sm:text-2xl font-bold m-0">
1698
+ {{ 'task.manager.boards.title' | translate }}
1699
+ </h1>
1700
+ <p class="text-muted-color mt-1 text-sm sm:text-base">
1701
+ {{ 'task.manager.boards.subtitle' | translate }}
1702
+ </p>
1703
+ </div>
1704
+ <p-button
1705
+ [label]="'task.manager.board.new' | translate"
1706
+ icon="pi pi-plus"
1707
+ (onClick)="showCreateDialog.set(true)"
1708
+ />
1709
+ </div>
1710
+
1711
+ @if (state.isLoading()) {
1712
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
1713
+ @for (i of [1, 2, 3, 4, 5, 6]; track i) {
1714
+ <div class="card !p-0 overflow-hidden">
1715
+ <p-skeleton height="4px" borderRadius="0" />
1716
+ <div class="p-5">
1717
+ <p-skeleton height="18px" width="60%" class="mb-3" />
1718
+ <p-skeleton height="13px" class="mb-1" />
1719
+ <p-skeleton height="13px" width="70%" class="mb-5" />
1720
+ <div class="flex gap-5 pt-4 border-t border-surface-100">
1721
+ <p-skeleton height="14px" width="55px" />
1722
+ <p-skeleton height="14px" width="45px" />
1723
+ <p-skeleton height="14px" width="50px" />
1724
+ </div>
1725
+ <p-skeleton height="11px" width="100px" class="mt-2" />
1726
+ </div>
1727
+ </div>
1728
+ }
1729
+ </div>
1730
+ } @else if (state.boards().length === 0) {
1731
+ <div
1732
+ class="flex flex-col items-center justify-center py-20 text-muted-color"
1733
+ >
1734
+ <span
1735
+ class="w-16 h-16 rounded-2xl border-2 border-dashed border-surface-300 flex items-center justify-center mb-4"
1736
+ >
1737
+ <i class="pi pi-th-large text-2xl text-surface-400"></i>
1738
+ </span>
1739
+ <p class="text-base font-semibold mb-1">
1740
+ {{ 'task.manager.board.empty' | translate }}
1741
+ </p>
1742
+ <p class="text-sm text-muted-color mb-5">
1743
+ {{ 'task.manager.board.empty.hint' | translate }}
1744
+ </p>
1745
+ <p-button
1746
+ [label]="'task.manager.board.new' | translate"
1747
+ icon="pi pi-plus"
1748
+ size="small"
1749
+ (onClick)="showCreateDialog.set(true)"
1750
+ />
1751
+ </div>
1752
+ } @else {
1753
+ <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
1754
+ @for (board of state.boards(); track board.id) {
1755
+ <div
1756
+ class="card !p-0 cursor-pointer overflow-hidden group hover:shadow-lg transition-all duration-200"
1757
+ (click)="openBoard(board.id)"
1758
+ >
1759
+ <div class="h-1" [style.background-color]="board.color"></div>
1760
+
1761
+ <div class="p-5">
1762
+ <div class="flex items-start justify-between gap-3 mb-2">
1763
+ <div class="flex items-center gap-2 min-w-0">
1764
+ <h3
1765
+ class="font-semibold text-base m-0 truncate group-hover:text-primary-500 transition-colors duration-150"
1766
+ >
1767
+ {{ board.name }}
1768
+ </h3>
1769
+ </div>
1770
+ <i
1771
+ class="pi pi-arrow-right text-xs text-surface-300 group-hover:text-primary-500 group-hover:translate-x-0.5 transition-all duration-150 flex-shrink-0 mt-1"
1772
+ ></i>
1773
+ </div>
1774
+
1775
+ <p
1776
+ class="text-sm text-muted-color line-clamp-2 pl-[18px] mb-0 min-h-[40px]"
1777
+ >
1778
+ {{ board.description || '' }}
1779
+ </p>
1780
+
1781
+ <div
1782
+ class="border-t border-surface-100 dark:border-surface-700 mt-4 pt-4 space-y-2.5"
1783
+ >
1784
+ <div class="flex items-center gap-4">
1785
+ <span class="flex items-center gap-1.5">
1786
+ <i class="pi pi-users text-[11px] text-muted-color"></i>
1787
+ <span class="text-sm font-semibold">{{
1788
+ board.totalMembers ?? 0
1789
+ }}</span>
1790
+ <span class="text-xs text-muted-color">{{
1791
+ 'task.manager.board.members' | translate
1792
+ }}</span>
1793
+ </span>
1794
+ <span
1795
+ class="w-px h-3 bg-surface-200 dark:bg-surface-600"
1796
+ ></span>
1797
+ <span class="flex items-center gap-1.5">
1798
+ <i class="pi pi-list text-[11px] text-muted-color"></i>
1799
+ <span class="text-sm font-semibold">{{
1800
+ board.totalLists ?? 0
1801
+ }}</span>
1802
+ <span class="text-xs text-muted-color">{{
1803
+ 'task.manager.board.lists' | translate
1804
+ }}</span>
1805
+ </span>
1806
+ <span
1807
+ class="w-px h-3 bg-surface-200 dark:bg-surface-600"
1808
+ ></span>
1809
+ <span class="flex items-center gap-1.5">
1810
+ <i
1811
+ class="pi pi-check-square text-[11px] text-muted-color"
1812
+ ></i>
1813
+ <span class="text-sm font-semibold">{{
1814
+ board.totalTasks ?? 0
1815
+ }}</span>
1816
+ <span class="text-xs text-muted-color">{{
1817
+ 'task.manager.board.tasks' | translate
1818
+ }}</span>
1819
+ </span>
1820
+ </div>
1821
+ <div
1822
+ class="flex items-center gap-1.5 text-xs text-muted-color"
1823
+ >
1824
+ <i class="pi pi-clock text-[10px]"></i>
1825
+ <span>{{ board.createdAt | date: 'MMM d, yyyy' }}</span>
1826
+ </div>
1827
+ </div>
1828
+ </div>
1829
+ </div>
1830
+ }
1831
+ </div>
1832
+ }
1833
+ </div>
1834
+
1835
+ <p-dialog
1836
+ [header]="'task.manager.board.new' | translate"
1837
+ [(visible)]="showCreateDialog"
1838
+ [modal]="true"
1839
+ [style]="{ width: '440px' }"
1840
+ >
1841
+ <div class="flex flex-col gap-4 py-2">
1842
+ <div class="flex flex-col gap-1">
1843
+ <label class="text-sm font-medium"
1844
+ >{{ 'shared.name' | translate }}
1845
+ <span class="text-red-500">*</span></label
1846
+ >
1847
+ <input
1848
+ pInputText
1849
+ [(ngModel)]="newBoardName"
1850
+ [placeholder]="'task.manager.board.name.placeholder' | translate"
1851
+ />
1852
+ </div>
1853
+ <div class="flex flex-col gap-1">
1854
+ <label class="text-sm font-medium">{{
1855
+ 'shared.description' | translate
1856
+ }}</label>
1857
+ <textarea
1858
+ pTextarea
1859
+ [(ngModel)]="newBoardDescription"
1860
+ rows="2"
1861
+ [placeholder]="
1862
+ 'task.manager.board.description.placeholder' | translate
1863
+ "
1864
+ ></textarea>
1865
+ </div>
1866
+ <div class="flex flex-col gap-2">
1867
+ <label class="text-sm font-medium">{{
1868
+ 'task.manager.board.color.label' | translate
1869
+ }}</label>
1870
+ <div class="flex flex-wrap gap-2">
1871
+ @for (c of boardColors; track c) {
1872
+ <button
1873
+ type="button"
1874
+ class="w-8 h-8 rounded-full border-2 transition-all hover:scale-110 focus:outline-none flex items-center justify-center"
1875
+ [style.background-color]="c"
1876
+ [class.ring-2]="newBoardColor === c"
1877
+ [class.ring-offset-2]="newBoardColor === c"
1878
+ [class.ring-surface-900]="newBoardColor === c"
1879
+ [pTooltip]="c"
1880
+ (click)="newBoardColor = c"
1881
+ >
1882
+ @if (newBoardColor === c) {
1883
+ <i class="pi pi-check text-white text-xs"></i>
1884
+ }
1885
+ </button>
1886
+ }
1887
+ </div>
1888
+ <div class="flex items-center gap-2 mt-1">
1889
+ <span
1890
+ class="w-8 h-8 rounded-full border border-surface-200 flex-shrink-0"
1891
+ [style.background-color]="newBoardColor"
1892
+ ></span>
1893
+ <span class="text-sm text-surface-500 font-mono">{{
1894
+ newBoardColor
1895
+ }}</span>
1896
+ </div>
1897
+ </div>
1898
+ </div>
1899
+
1900
+ <ng-template pTemplate="footer">
1901
+ <div class="flex justify-end gap-2">
1902
+ <p-button
1903
+ [label]="'shared.cancel' | translate"
1904
+ severity="secondary"
1905
+ (onClick)="closeCreateDialog()"
1906
+ />
1907
+ <p-button
1908
+ [label]="'shared.create' | translate"
1909
+ [loading]="isCreating()"
1910
+ (onClick)="createBoard()"
1911
+ />
1912
+ </div>
1913
+ </ng-template>
1914
+ </p-dialog>
1915
+ `,
1916
+ }]
1917
+ }], ctorParameters: () => [] });
1918
+
1919
+ var boardListPage_component = /*#__PURE__*/Object.freeze({
1920
+ __proto__: null,
1921
+ BoardListPageComponent: BoardListPageComponent
1922
+ });
1923
+
1924
+ class BoardViewPageComponent {
1925
+ state = inject(TaskBoardStateService);
1926
+ taskApi = inject(TaskApiService);
1927
+ listApi = inject(TaskListApiService);
1928
+ confirmationService = inject(ConfirmationService);
1929
+ messageService = inject(MessageService);
1930
+ translateAdapter = inject(TranslateService, { optional: true });
1931
+ destroyRef = inject(DestroyRef);
1932
+ translate(key, vars) {
1933
+ return this.translateAdapter?.translate(key, vars) ?? key;
1934
+ }
1935
+ boardId = input.required(...(ngDevMode ? [{ debugName: "boardId" }] : []));
1936
+ viewMode = signal('board', ...(ngDevMode ? [{ debugName: "viewMode" }] : []));
1937
+ showTaskForm = signal(false, ...(ngDevMode ? [{ debugName: "showTaskForm" }] : []));
1938
+ editingTask = signal(null, ...(ngDevMode ? [{ debugName: "editingTask" }] : []));
1939
+ activeListId = signal('', ...(ngDevMode ? [{ debugName: "activeListId" }] : []));
1940
+ showAddListDialog = signal(false, ...(ngDevMode ? [{ debugName: "showAddListDialog" }] : []));
1941
+ isAddingList = signal(false, ...(ngDevMode ? [{ debugName: "isAddingList" }] : []));
1942
+ showLabelsDialog = signal(false, ...(ngDevMode ? [{ debugName: "showLabelsDialog" }] : []));
1943
+ isAddingLabel = signal(false, ...(ngDevMode ? [{ debugName: "isAddingLabel" }] : []));
1944
+ showMembersDialog = signal(false, ...(ngDevMode ? [{ debugName: "showMembersDialog" }] : []));
1945
+ isAddingMember = signal(false, ...(ngDevMode ? [{ debugName: "isAddingMember" }] : []));
1946
+ editingListId = signal(null, ...(ngDevMode ? [{ debugName: "editingListId" }] : []));
1947
+ editingListNameValue = signal('', ...(ngDevMode ? [{ debugName: "editingListNameValue" }] : []));
1948
+ showEditBoardDialog = signal(false, ...(ngDevMode ? [{ debugName: "showEditBoardDialog" }] : []));
1949
+ isSavingBoard = signal(false, ...(ngDevMode ? [{ debugName: "isSavingBoard" }] : []));
1950
+ newListName = '';
1951
+ newLabelName = '';
1952
+ newLabelColor = '#6366F1';
1953
+ selectedMemberUser = null;
1954
+ newMemberRole = 'MEMBER';
1955
+ editBoardName = '';
1956
+ editBoardDescription = '';
1957
+ editBoardColor = '#6366F1';
1958
+ boardColorPalette = BOARD_COLOR_PALETTE;
1959
+ memberRoleOptions = [
1960
+ { labelKey: 'task.manager.member.role.admin', value: 'ADMIN' },
1961
+ { labelKey: 'task.manager.member.role.member', value: 'MEMBER' },
1962
+ { labelKey: 'task.manager.member.role.viewer', value: 'VIEWER' },
1963
+ ];
1964
+ allTasksFlat = computed(() => this.state.listsWithTasks().flatMap((list) => list.tasks.map((task) => ({
1965
+ ...task,
1966
+ listName: list.name,
1967
+ listColor: list.color,
1968
+ }))), ...(ngDevMode ? [{ debugName: "allTasksFlat" }] : []));
1969
+ listDropIds = computed(() => this.state.listsWithTasks().map((l) => l.id), ...(ngDevMode ? [{ debugName: "listDropIds" }] : []));
1970
+ constructor() {
1971
+ effect(() => { const id = this.boardId(); untracked(() => this.state.openBoard(id)); });
1972
+ this.destroyRef.onDestroy(() => this.state.closeBoard());
1973
+ }
1974
+ openAddTask(list) {
1975
+ this.editingTask.set(null);
1976
+ this.activeListId.set(list.id);
1977
+ this.showTaskForm.set(true);
1978
+ }
1979
+ onTaskSaved(task) {
1980
+ this.state.addTaskOptimistic(task);
1981
+ }
1982
+ onDeleteTask(task) {
1983
+ this.confirmationService.confirm({
1984
+ message: this.translate('shared.confirm.delete.item', { name: task.title }),
1985
+ header: this.translate('task.detail.delete'),
1986
+ icon: 'pi pi-exclamation-triangle',
1987
+ acceptButtonStyleClass: 'p-button-danger',
1988
+ accept: async () => {
1989
+ await this.state.deleteTask(task.id);
1990
+ this.messageService.add({
1991
+ severity: 'success',
1992
+ summary: this.translate('shared.success'),
1993
+ detail: this.translate('task.delete.success'),
1994
+ });
1995
+ },
1996
+ });
1997
+ }
1998
+ onEditTask(task) {
1999
+ this.editingTask.set(task);
2000
+ this.activeListId.set(task.listId);
2001
+ this.showTaskForm.set(true);
2002
+ this.state.selectTask(null);
2003
+ }
2004
+ getMemberName(assigneeId) {
2005
+ return this.state.members().find((m) => m.userId === assigneeId)?.userName ?? null;
2006
+ }
2007
+ getMemberInitials(assigneeId) {
2008
+ return memberInitials(this.getMemberName(assigneeId));
2009
+ }
2010
+ memberInitials(member) {
2011
+ return memberInitials(member.userName);
2012
+ }
2013
+ memberColor(member) {
2014
+ return memberColorFromId(member.userId);
2015
+ }
2016
+ memberColorById(userId) {
2017
+ return memberColorFromId(userId);
2018
+ }
2019
+ roleBadgeClass(role) {
2020
+ const map = {
2021
+ OWNER: 'text-primary-600 dark:text-primary-400 font-semibold',
2022
+ ADMIN: 'text-orange-600 dark:text-orange-400 font-semibold',
2023
+ MEMBER: 'text-surface-500 dark:text-surface-400',
2024
+ VIEWER: 'text-surface-400 dark:text-surface-500',
2025
+ };
2026
+ return map[role] ?? map['MEMBER'];
2027
+ }
2028
+ prioritySeverity = taskPrioritySeverity;
2029
+ isOverdue = isTaskOverdue;
2030
+ async onTaskDrop(event) {
2031
+ if (event.previousContainer === event.container) {
2032
+ moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
2033
+ }
2034
+ else {
2035
+ transferArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex);
2036
+ const task = event.container.data[event.currentIndex];
2037
+ await this.taskApi.moveTask({
2038
+ taskId: task.id,
2039
+ targetListId: event.container.id,
2040
+ position: event.currentIndex * 100,
2041
+ });
2042
+ }
2043
+ }
2044
+ async onListDrop(event) {
2045
+ if (event.previousIndex === event.currentIndex)
2046
+ return;
2047
+ const lists = this.state.listsWithTasks().slice();
2048
+ moveItemInArray(lists, event.previousIndex, event.currentIndex);
2049
+ const orderedIds = lists.map((l) => l.id);
2050
+ await this.state.reorderLists(this.boardId(), orderedIds);
2051
+ }
2052
+ startRenameList(list) {
2053
+ this.editingListId.set(list.id);
2054
+ this.editingListNameValue.set(list.name);
2055
+ }
2056
+ cancelRenameList() {
2057
+ this.editingListId.set(null);
2058
+ this.editingListNameValue.set('');
2059
+ }
2060
+ async saveRenameList() {
2061
+ const id = this.editingListId();
2062
+ const name = this.editingListNameValue().trim();
2063
+ this.cancelRenameList();
2064
+ if (!id || !name)
2065
+ return;
2066
+ await this.state.renameList(id, name);
2067
+ this.messageService.add({
2068
+ severity: 'success',
2069
+ summary: this.translate('shared.success'),
2070
+ detail: this.translate('task.list.update.success'),
2071
+ });
2072
+ }
2073
+ openEditBoardDialog(board) {
2074
+ this.editBoardName = board.name;
2075
+ this.editBoardDescription = board.description ?? '';
2076
+ this.editBoardColor = board.color;
2077
+ this.showEditBoardDialog.set(true);
2078
+ }
2079
+ async saveBoard() {
2080
+ const board = this.state.activeBoard();
2081
+ if (!board || !this.editBoardName.trim())
2082
+ return;
2083
+ this.isSavingBoard.set(true);
2084
+ try {
2085
+ await this.state.updateBoard({
2086
+ id: board.id,
2087
+ name: this.editBoardName.trim(),
2088
+ description: this.editBoardDescription.trim() || undefined,
2089
+ color: this.editBoardColor,
2090
+ });
2091
+ this.showEditBoardDialog.set(false);
2092
+ this.messageService.add({
2093
+ severity: 'success',
2094
+ summary: this.translate('shared.success'),
2095
+ detail: this.translate('task.board.update.success'),
2096
+ });
2097
+ }
2098
+ finally {
2099
+ this.isSavingBoard.set(false);
2100
+ }
2101
+ }
2102
+ async addLabel() {
2103
+ if (!this.newLabelName.trim())
2104
+ return;
2105
+ this.isAddingLabel.set(true);
2106
+ try {
2107
+ await this.state.addLabel(this.boardId(), this.newLabelName.trim(), this.newLabelColor);
2108
+ this.newLabelName = '';
2109
+ this.newLabelColor = '#6366F1';
2110
+ }
2111
+ finally {
2112
+ this.isAddingLabel.set(false);
2113
+ }
2114
+ }
2115
+ async removeLabel(labelId) {
2116
+ await this.state.deleteLabel(labelId);
2117
+ }
2118
+ async addMember() {
2119
+ if (!this.selectedMemberUser)
2120
+ return;
2121
+ this.isAddingMember.set(true);
2122
+ try {
2123
+ await this.state.addMember(this.selectedMemberUser, this.newMemberRole);
2124
+ this.resetMemberForm();
2125
+ }
2126
+ finally {
2127
+ this.isAddingMember.set(false);
2128
+ }
2129
+ }
2130
+ resetMemberForm() {
2131
+ this.selectedMemberUser = null;
2132
+ this.newMemberRole = 'MEMBER';
2133
+ }
2134
+ async removeMember(userId) {
2135
+ await this.state.removeMember(userId);
2136
+ }
2137
+ async addList() {
2138
+ if (!this.newListName.trim())
2139
+ return;
2140
+ this.isAddingList.set(true);
2141
+ try {
2142
+ const position = this.state.lists().length * 100;
2143
+ await this.listApi.insert({
2144
+ boardId: this.boardId(),
2145
+ name: this.newListName,
2146
+ position,
2147
+ });
2148
+ await this.state.openBoard(this.boardId());
2149
+ this.showAddListDialog.set(false);
2150
+ this.newListName = '';
2151
+ }
2152
+ finally {
2153
+ this.isAddingList.set(false);
2154
+ }
2155
+ }
2156
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BoardViewPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
2157
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.5", type: BoardViewPageComponent, isStandalone: true, selector: "tm-board-view-page", inputs: { boardId: { classPropertyName: "boardId", publicName: "boardId", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
2158
+ <div class="flex flex-col h-full">
2159
+ <div class="flex flex-col">
2160
+ <div class="flex items-center justify-between px-5 py-2.5 gap-4">
2161
+ @if (state.activeBoard(); as board) {
2162
+ <div class="flex items-center gap-2.5 min-w-0 flex-1 group">
2163
+ <div class="flex flex-col min-w-0">
2164
+ <div class="flex items-center gap-1.5 min-w-0">
2165
+ <h1 class="text-base font-bold text-surface-900 dark:text-surface-0 truncate leading-snug">
2166
+ {{ board.name }}
2167
+ </h1>
2168
+ <p-button
2169
+ icon="pi pi-pencil"
2170
+ [text]="true"
2171
+ size="small"
2172
+ severity="secondary"
2173
+ styleClass="!w-6 !h-6 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
2174
+ [pTooltip]="'task.manager.board.edit' | translate"
2175
+ tooltipPosition="bottom"
2176
+ (onClick)="openEditBoardDialog(board)"
2177
+ />
2178
+ </div>
2179
+ @if (board.description) {
2180
+ <span class="text-xs text-surface-400 truncate max-w-sm hidden md:block leading-snug">
2181
+ {{ board.description }}
2182
+ </span>
2183
+ }
2184
+ </div>
2185
+ </div>
2186
+ }
2187
+
2188
+ <div class="flex items-center gap-1.5 flex-shrink-0">
2189
+ @if (state.members().length > 0) {
2190
+ <div
2191
+ class="flex items-center -space-x-2 cursor-pointer mr-1"
2192
+ (click)="showMembersDialog.set(true)"
2193
+ [pTooltip]="'task.manager.board.manage.members' | translate"
2194
+ tooltipPosition="bottom"
2195
+ >
2196
+ @for (member of state.members().slice(0, 4); track member.id) {
2197
+ <div
2198
+ class="w-6 h-6 rounded-full border-2 border-white dark:border-surface-900 flex items-center justify-center text-[9px] font-bold text-white flex-shrink-0 ring-1 ring-surface-100 dark:ring-surface-800"
2199
+ [style.background-color]="memberColor(member)"
2200
+ >
2201
+ {{ memberInitials(member) }}
2202
+ </div>
2203
+ }
2204
+ @if (state.members().length > 4) {
2205
+ <div
2206
+ class="w-6 h-6 rounded-full border-2 border-white dark:border-surface-900 bg-surface-200 dark:bg-surface-700 flex items-center justify-center text-[9px] font-bold text-surface-600 dark:text-surface-300 flex-shrink-0"
2207
+ >
2208
+ +{{ state.members().length - 4 }}
2209
+ </div>
2210
+ }
2211
+ </div>
2212
+ }
2213
+
2214
+ <div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
2215
+
2216
+ <div class="flex rounded-lg overflow-hidden border border-surface-200 dark:border-surface-700">
2217
+ <p-button
2218
+ icon="pi pi-th-large"
2219
+ [pTooltip]="'task.manager.view.board' | translate"
2220
+ tooltipPosition="bottom"
2221
+ size="small"
2222
+ [severity]="viewMode() === 'board' ? 'primary' : 'secondary'"
2223
+ [outlined]="viewMode() !== 'board'"
2224
+ styleClass="!rounded-none !border-0"
2225
+ (onClick)="viewMode.set('board')"
2226
+ />
2227
+ <p-button
2228
+ icon="pi pi-list"
2229
+ [pTooltip]="'task.manager.view.list' | translate"
2230
+ tooltipPosition="bottom"
2231
+ size="small"
2232
+ [severity]="viewMode() === 'list' ? 'primary' : 'secondary'"
2233
+ [outlined]="viewMode() !== 'list'"
2234
+ styleClass="!rounded-none !border-0"
2235
+ (onClick)="viewMode.set('list')"
2236
+ />
2237
+ </div>
2238
+
2239
+ <div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
2240
+
2241
+ <p-button
2242
+ icon="pi pi-users"
2243
+ [text]="true"
2244
+ severity="secondary"
2245
+ size="small"
2246
+ styleClass="!w-8 !h-8"
2247
+ [pTooltip]="'task.manager.board.manage.members' | translate"
2248
+ tooltipPosition="bottom"
2249
+ (onClick)="showMembersDialog.set(true)"
2250
+ />
2251
+ <p-button
2252
+ icon="pi pi-tag"
2253
+ [text]="true"
2254
+ severity="secondary"
2255
+ size="small"
2256
+ styleClass="!w-8 !h-8"
2257
+ [pTooltip]="'task.manager.board.manage.labels' | translate"
2258
+ tooltipPosition="bottom"
2259
+ (onClick)="showLabelsDialog.set(true)"
2260
+ />
2261
+
2262
+ @if (viewMode() === 'board') {
2263
+ <div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
2264
+ <p-button
2265
+ icon="pi pi-plus"
2266
+ [label]="'task.manager.board.add.list' | translate"
2267
+ size="small"
2268
+ (onClick)="showAddListDialog.set(true)"
2269
+ />
2270
+ }
2271
+ </div>
2272
+ </div>
2273
+
2274
+ @if (state.activeBoard(); as board) {
2275
+ <div class="h-0.5 w-full flex-shrink-0" [style.background-color]="board.color"></div>
2276
+ }
2277
+ </div>
2278
+
2279
+ @if (state.isBoardLoading()) {
2280
+ @if (viewMode() === 'board') {
2281
+ <div class="flex gap-4 p-6 overflow-x-auto">
2282
+ @for (i of [1, 2, 3]; track i) {
2283
+ <div class="flex-shrink-0 w-72">
2284
+ <p-skeleton height="36px" class="mb-3" />
2285
+ @for (j of [1, 2, 3]; track j) {
2286
+ <p-skeleton height="90px" class="mb-2" />
2287
+ }
2288
+ </div>
2289
+ }
2290
+ </div>
2291
+ } @else {
2292
+ <div class="p-6">
2293
+ @for (i of [1, 2, 3, 4, 5]; track i) {
2294
+ <p-skeleton height="48px" class="mb-2" />
2295
+ }
2296
+ </div>
2297
+ }
2298
+
2299
+ } @else if (viewMode() === 'board') {
2300
+ <div
2301
+ class="flex gap-4 p-5 overflow-x-auto flex-1"
2302
+ cdkDropList
2303
+ cdkDropListOrientation="horizontal"
2304
+ [cdkDropListData]="state.listsWithTasks()"
2305
+ (cdkDropListDropped)="onListDrop($event)"
2306
+ >
2307
+ @for (list of state.listsWithTasks(); track list.id) {
2308
+ <div
2309
+ cdkDrag
2310
+ class="flex-shrink-0 w-72 flex flex-col max-h-[calc(100vh-140px)] self-start"
2311
+ >
2312
+ <div
2313
+ class="flex items-center gap-1 px-2 py-2.5 rounded-t-xl border border-b-0 border-surface-200 dark:border-surface-700"
2314
+ [style.border-top]="'3px solid ' + list.color"
2315
+ >
2316
+ <i
2317
+ cdkDragHandle
2318
+ class="pi pi-bars text-surface-300 dark:text-surface-600 text-xs cursor-grab flex-shrink-0 px-1"
2319
+ ></i>
2320
+
2321
+ <div class="flex items-center gap-2 min-w-0 flex-1">
2322
+ @if (editingListId() === list.id) {
2323
+ <input
2324
+ pInputText
2325
+ class="text-sm font-semibold !h-6 !py-0 !px-1 w-full"
2326
+ [ngModel]="editingListNameValue()"
2327
+ (ngModelChange)="editingListNameValue.set($event)"
2328
+ (keydown.enter)="saveRenameList()"
2329
+ (keydown.escape)="cancelRenameList()"
2330
+ (blur)="cancelRenameList()"
2331
+ (click)="$event.stopPropagation()"
2332
+ autofocus
2333
+ />
2334
+ } @else {
2335
+ <span class="font-semibold text-sm truncate">{{ list.name }}</span>
2336
+ <span
2337
+ class="text-[11px] font-semibold px-1.5 py-0.5 rounded-full text-white flex-shrink-0"
2338
+ [style.background-color]="list.color"
2339
+ >
2340
+ {{ list.tasks.length }}
2341
+ </span>
2342
+ }
2343
+ </div>
2344
+
2345
+ @if (editingListId() === list.id) {
2346
+ <p-button
2347
+ icon="pi pi-check"
2348
+ [text]="true"
2349
+ size="small"
2350
+ severity="success"
2351
+ styleClass="!w-6 !h-6 flex-shrink-0"
2352
+ (mousedown)="$event.preventDefault()"
2353
+ (click)="saveRenameList(); $event.stopPropagation()"
2354
+ />
2355
+ <p-button
2356
+ icon="pi pi-times"
2357
+ [text]="true"
2358
+ size="small"
2359
+ severity="secondary"
2360
+ styleClass="!w-6 !h-6 flex-shrink-0"
2361
+ (mousedown)="$event.preventDefault()"
2362
+ (click)="cancelRenameList(); $event.stopPropagation()"
2363
+ />
2364
+ } @else {
2365
+ <p-button
2366
+ icon="pi pi-pencil"
2367
+ [text]="true"
2368
+ size="small"
2369
+ severity="secondary"
2370
+ styleClass="!w-6 !h-6 flex-shrink-0"
2371
+ [pTooltip]="'task.manager.list.rename' | translate"
2372
+ tooltipPosition="bottom"
2373
+ (click)="startRenameList(list); $event.stopPropagation()"
2374
+ />
2375
+ }
2376
+ </div>
2377
+
2378
+ <div
2379
+ cdkDropList
2380
+ [id]="list.id"
2381
+ [cdkDropListData]="list.tasks"
2382
+ [cdkDropListConnectedTo]="listDropIds()"
2383
+ (cdkDropListDropped)="onTaskDrop($event)"
2384
+ class="flex flex-col gap-2 p-2 overflow-y-auto flex-1 min-h-[60px] border border-t-0 border-b-0 border-surface-200 dark:border-surface-700"
2385
+ >
2386
+ @for (task of list.tasks; track task.id) {
2387
+ <div cdkDrag>
2388
+ <tm-task-card
2389
+ [task]="task"
2390
+ (cardClick)="state.selectTask($event)"
2391
+ (editClick)="onEditTask($event)"
2392
+ (deleteClick)="onDeleteTask($event)"
2393
+ />
2394
+ </div>
2395
+ }
2396
+ @if (list.tasks.length === 0) {
2397
+ <div
2398
+ class="flex items-center justify-center h-14 border-2 border-dashed border-surface-200 dark:border-surface-700 rounded-lg"
2399
+ >
2400
+ <span class="text-xs text-surface-400">{{
2401
+ 'task.manager.board.drop.tasks.here' | translate
2402
+ }}</span>
2403
+ </div>
2404
+ }
2405
+ </div>
2406
+
2407
+ <div
2408
+ class="rounded-b-xl border border-t-0 border-surface-200 dark:border-surface-700 p-1.5"
2409
+ >
2410
+ <p-button
2411
+ icon="pi pi-plus"
2412
+ [label]="'task.manager.board.add.task' | translate"
2413
+ severity="secondary"
2414
+ size="small"
2415
+ styleClass="w-full !justify-start"
2416
+ (onClick)="openAddTask(list)"
2417
+ />
2418
+ </div>
2419
+ </div>
2420
+ }
2421
+
2422
+ @if (state.listsWithTasks().length === 0) {
2423
+ <div class="flex-1 flex items-center justify-center">
2424
+ <div class="text-center text-surface-400">
2425
+ <div
2426
+ class="w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-4"
2427
+ >
2428
+ <i
2429
+ class="pi pi-list text-3xl text-surface-300 dark:text-surface-600"
2430
+ ></i>
2431
+ </div>
2432
+ <p class="font-medium text-surface-500">
2433
+ {{ 'task.manager.board.list.empty' | translate }}
2434
+ </p>
2435
+ <p-button
2436
+ icon="pi pi-plus"
2437
+ [label]="'task.manager.board.add.list' | translate"
2438
+ severity="secondary"
2439
+ size="small"
2440
+ class="mt-3"
2441
+ (onClick)="showAddListDialog.set(true)"
2442
+ />
2443
+ </div>
2444
+ </div>
2445
+ }
2446
+ </div>
2447
+
2448
+ } @else {
2449
+ <div class="p-4 sm:p-6 flex-1 overflow-auto">
2450
+ <div class="card">
2451
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
2452
+ <p-table
2453
+ [value]="allTasksFlat()"
2454
+ [paginator]="allTasksFlat().length > 20"
2455
+ [rows]="20"
2456
+ [rowsPerPageOptions]="[10, 20, 50]"
2457
+ [scrollable]="true"
2458
+ class="p-datatable-sm"
2459
+ [tableStyle]="{ 'min-width': '700px' }"
2460
+ >
2461
+ <ng-template #header>
2462
+ <tr>
2463
+ <th pSortableColumn="title" class="min-w-[240px]">
2464
+ {{ 'task.manager.table.title' | translate }}
2465
+ <p-sortIcon field="title" />
2466
+ </th>
2467
+ <th pSortableColumn="listName" class="w-40">
2468
+ {{ 'task.manager.table.list' | translate }}
2469
+ <p-sortIcon field="listName" />
2470
+ </th>
2471
+ <th pSortableColumn="priority" class="w-32">
2472
+ {{ 'task.manager.table.priority' | translate }}
2473
+ <p-sortIcon field="priority" />
2474
+ </th>
2475
+ <th class="w-36">
2476
+ {{ 'task.manager.table.assignee' | translate }}
2477
+ </th>
2478
+ <th pSortableColumn="dueDate" class="w-32">
2479
+ {{ 'task.manager.table.due.date' | translate }}
2480
+ <p-sortIcon field="dueDate" />
2481
+ </th>
2482
+ <th class="w-[100px]">{{ 'shared.actions' | translate }}</th>
2483
+ </tr>
2484
+ </ng-template>
2485
+ <ng-template #body let-task>
2486
+ <tr>
2487
+ <td>
2488
+ <span
2489
+ class="font-medium text-sm cursor-pointer hover:text-primary-500 transition-colors"
2490
+ (click)="state.selectTask(task)"
2491
+ >{{ task.title }}</span>
2492
+ </td>
2493
+ <td>
2494
+ <span class="inline-flex items-center gap-1.5 text-sm">
2495
+ <span
2496
+ class="w-2 h-2 rounded-full flex-shrink-0"
2497
+ [style.background-color]="task.listColor"
2498
+ ></span>
2499
+ {{ task.listName }}
2500
+ </span>
2501
+ </td>
2502
+ <td>
2503
+ <p-tag
2504
+ [value]="task.priority"
2505
+ [severity]="prioritySeverity(task.priority)"
2506
+ />
2507
+ </td>
2508
+ <td>
2509
+ @if (task.assignees?.length) {
2510
+ <div class="flex items-center -space-x-1.5">
2511
+ @for (a of task.assignees!.slice(0, 3); track a.userId) {
2512
+ <span
2513
+ class="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
2514
+ [style.background-color]="memberColorById(a.userId)"
2515
+ [pTooltip]="a.userName ?? ''"
2516
+ >
2517
+ {{ getMemberInitials(a.userId) }}
2518
+ </span>
2519
+ }
2520
+ @if (task.assignees!.length > 3) {
2521
+ <span
2522
+ class="w-6 h-6 rounded-full bg-surface-200 dark:bg-surface-700 flex items-center justify-center text-[10px] font-bold text-surface-600 ring-2 ring-white dark:ring-surface-800 flex-shrink-0"
2523
+ >
2524
+ +{{ task.assignees!.length - 3 }}
2525
+ </span>
2526
+ }
2527
+ </div>
2528
+ } @else {
2529
+ <span class="text-muted-color text-sm">—</span>
2530
+ }
2531
+ </td>
2532
+ <td>
2533
+ @if (task.dueDate) {
2534
+ <span
2535
+ class="text-sm"
2536
+ [class.text-red-500]="isOverdue(task.dueDate)"
2537
+ >
2538
+ {{ task.dueDate | date: 'MMM d, y' }}
2539
+ </span>
2540
+ } @else {
2541
+ <span class="text-muted-color text-sm">—</span>
2542
+ }
2543
+ </td>
2544
+ <td>
2545
+ <div class="flex gap-1">
2546
+ <p-button
2547
+ icon="pi pi-pencil"
2548
+ [text]="true"
2549
+ severity="secondary"
2550
+ size="small"
2551
+ [pTooltip]="'shared.edit' | translate"
2552
+ (onClick)="onEditTask(task)"
2553
+ />
2554
+ <p-button
2555
+ icon="pi pi-trash"
2556
+ [text]="true"
2557
+ severity="danger"
2558
+ size="small"
2559
+ [pTooltip]="'shared.delete' | translate"
2560
+ (onClick)="onDeleteTask(task)"
2561
+ />
2562
+ </div>
2563
+ </td>
2564
+ </tr>
2565
+ </ng-template>
2566
+ <ng-template #emptymessage>
2567
+ <tr>
2568
+ <td colspan="6" class="text-center text-muted-color py-8">
2569
+ <i class="pi pi-inbox text-4xl block mb-3"></i>
2570
+ {{ 'task.manager.table.empty' | translate }}
2571
+ </td>
2572
+ </tr>
2573
+ </ng-template>
2574
+ </p-table>
2575
+ </div>
2576
+ </div>
2577
+ </div>
2578
+ }
2579
+ </div>
2580
+
2581
+ <tm-task-form-dialog
2582
+ [(visible)]="showTaskForm"
2583
+ [task]="editingTask()"
2584
+ [boardId]="boardId()"
2585
+ [listId]="activeListId()"
2586
+ [members]="state.members()"
2587
+ [labels]="state.labels()"
2588
+ (saved)="onTaskSaved($event)"
2589
+ />
2590
+
2591
+ <tm-task-detail-panel
2592
+ [task]="state.selectedTask()"
2593
+ (closePanel)="state.selectTask(null)"
2594
+ (editTask)="onEditTask($event)"
2595
+ />
2596
+
2597
+ <p-dialog
2598
+ [header]="'task.manager.label.manage.title' | translate"
2599
+ [(visible)]="showLabelsDialog"
2600
+ [modal]="true"
2601
+ [style]="{ width: '420px' }"
2602
+ (onHide)="newLabelName = ''; newLabelColor = '#6366F1'"
2603
+ >
2604
+ <div class="flex flex-col gap-4 py-2">
2605
+ <div class="flex gap-2 items-end">
2606
+ <div class="flex flex-col gap-1 flex-1">
2607
+ <label class="text-sm font-medium">{{
2608
+ 'task.manager.label.name.label' | translate
2609
+ }}</label>
2610
+ <input
2611
+ pInputText
2612
+ [(ngModel)]="newLabelName"
2613
+ [placeholder]="'task.manager.label.name.placeholder' | translate"
2614
+ />
2615
+ </div>
2616
+ <div class="flex flex-col gap-1">
2617
+ <label class="text-sm font-medium">{{
2618
+ 'task.manager.label.color.label' | translate
2619
+ }}</label>
2620
+ <input
2621
+ type="color"
2622
+ [(ngModel)]="newLabelColor"
2623
+ class="w-10 h-10 rounded cursor-pointer border border-surface-300"
2624
+ />
2625
+ </div>
2626
+ <p-button
2627
+ icon="pi pi-plus"
2628
+ [label]="'task.manager.label.add' | translate"
2629
+ size="small"
2630
+ [loading]="isAddingLabel()"
2631
+ (onClick)="addLabel()"
2632
+ />
2633
+ </div>
2634
+
2635
+ <div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
2636
+ @if (state.labels().length === 0) {
2637
+ <p class="text-sm text-surface-400 text-center py-4">
2638
+ {{ 'task.manager.label.no.labels' | translate }}
2639
+ </p>
2640
+ }
2641
+ @for (label of state.labels(); track label.id) {
2642
+ <div
2643
+ class="flex items-center justify-between px-3 py-2 rounded-lg border border-surface-200 dark:border-surface-700"
2644
+ >
2645
+ <div class="flex items-center gap-2">
2646
+ <span
2647
+ class="w-4 h-4 rounded-full flex-shrink-0"
2648
+ [style.background-color]="label.color"
2649
+ ></span>
2650
+ <span class="text-sm font-medium">{{ label.name }}</span>
2651
+ </div>
2652
+ <p-button
2653
+ icon="pi pi-trash"
2654
+ severity="danger"
2655
+ size="small"
2656
+ [text]="true"
2657
+ (onClick)="removeLabel(label.id)"
2658
+ />
2659
+ </div>
2660
+ }
2661
+ </div>
2662
+ </div>
2663
+ </p-dialog>
2664
+
2665
+ <p-dialog
2666
+ [header]="'task.manager.member.manage.title' | translate"
2667
+ [(visible)]="showMembersDialog"
2668
+ [modal]="true"
2669
+ [style]="{ width: '480px' }"
2670
+ (onHide)="resetMemberForm()"
2671
+ >
2672
+ <div class="flex flex-col gap-4 py-2">
2673
+ <div class="flex flex-col gap-3">
2674
+ <div class="flex flex-col gap-1">
2675
+ <label class="text-sm font-medium">{{
2676
+ 'task.manager.member.user.label' | translate
2677
+ }}</label>
2678
+ <lib-user-select
2679
+ [(value)]="selectedMemberUser"
2680
+ [isEditMode]="true"
2681
+ [placeHolder]="'task.manager.member.user.placeholder' | translate"
2682
+ />
2683
+ </div>
2684
+
2685
+ <div class="flex gap-2 items-end">
2686
+ <div class="flex flex-col gap-1 flex-1">
2687
+ <label class="text-sm font-medium">{{
2688
+ 'task.manager.member.role.label' | translate
2689
+ }}</label>
2690
+ <p-select
2691
+ [(ngModel)]="newMemberRole"
2692
+ [options]="memberRoleOptions"
2693
+ optionValue="value"
2694
+ [placeholder]="
2695
+ 'task.manager.member.role.placeholder' | translate
2696
+ "
2697
+ class="w-full"
2698
+ appendTo="body"
2699
+ >
2700
+ <ng-template #selectedItem let-opt>
2701
+ {{ opt.labelKey | translate }}
2702
+ </ng-template>
2703
+ <ng-template #item let-opt>
2704
+ {{ opt.labelKey | translate }}
2705
+ </ng-template>
2706
+ </p-select>
2707
+ </div>
2708
+ <p-button
2709
+ icon="pi pi-plus"
2710
+ [label]="'task.manager.member.add' | translate"
2711
+ size="small"
2712
+ [loading]="isAddingMember()"
2713
+ [disabled]="!selectedMemberUser"
2714
+ (onClick)="addMember()"
2715
+ />
2716
+ </div>
2717
+ </div>
2718
+
2719
+ <div class="flex flex-col gap-2 max-h-72 overflow-y-auto">
2720
+ @if (state.members().length === 0) {
2721
+ <p class="text-sm text-surface-400 text-center py-4">
2722
+ {{ 'task.manager.member.no.members' | translate }}
2723
+ </p>
2724
+ }
2725
+ @for (member of state.members(); track member.id) {
2726
+ <div
2727
+ class="flex items-center justify-between px-3 py-2.5 rounded-lg border border-surface-200 dark:border-surface-700"
2728
+ >
2729
+ <div class="flex items-center gap-3">
2730
+ <div
2731
+ class="w-8 h-8 rounded-full flex items-center justify-center text-[11px] font-bold text-white flex-shrink-0"
2732
+ [style.background-color]="memberColor(member)"
2733
+ >
2734
+ {{ memberInitials(member) }}
2735
+ </div>
2736
+ <div>
2737
+ <div class="text-sm font-medium ">
2738
+ {{ member.userName ?? member.userId }}
2739
+ </div>
2740
+ @if (member.userEmail) {
2741
+ <div class="text-xs text-surface-400">
2742
+ {{ member.userEmail }}
2743
+ </div>
2744
+ }
2745
+ </div>
2746
+ </div>
2747
+ <div class="flex items-center gap-2">
2748
+ <span
2749
+ class="text-[10px] font-semibold px-2 py-0.5 rounded-full"
2750
+ [class]="roleBadgeClass(member.role)"
2751
+ >
2752
+ {{ ('task.manager.member.role.' + member.role.toLowerCase()) | translate }}
2753
+ </span>
2754
+ @if (member.role !== 'OWNER') {
2755
+ <p-button
2756
+ icon="pi pi-trash"
2757
+ severity="danger"
2758
+ size="small"
2759
+ [text]="true"
2760
+ (onClick)="removeMember(member.userId)"
2761
+ />
2762
+ }
2763
+ </div>
2764
+ </div>
2765
+ }
2766
+ </div>
2767
+ </div>
2768
+ </p-dialog>
2769
+
2770
+ <p-dialog
2771
+ [header]="'task.manager.list.new' | translate"
2772
+ [(visible)]="showAddListDialog"
2773
+ [modal]="true"
2774
+ [style]="{ width: '360px' }"
2775
+ (onHide)="newListName = ''"
2776
+ >
2777
+ <div class="flex flex-col gap-2 py-2">
2778
+ <label class="text-sm font-medium">
2779
+ {{ 'task.manager.list.name.label' | translate }}
2780
+ <span class="text-red-500">*</span>
2781
+ </label>
2782
+ <input
2783
+ pInputText
2784
+ [(ngModel)]="newListName"
2785
+ [placeholder]="'task.manager.list.name.placeholder' | translate"
2786
+ />
2787
+ </div>
2788
+ <ng-template pTemplate="footer">
2789
+ <div class="flex justify-end gap-2">
2790
+ <p-button
2791
+ [label]="'shared.cancel' | translate"
2792
+ severity="secondary"
2793
+ (onClick)="showAddListDialog.set(false)"
2794
+ />
2795
+ <p-button
2796
+ [label]="'shared.create' | translate"
2797
+ [loading]="isAddingList()"
2798
+ (onClick)="addList()"
2799
+ />
2800
+ </div>
2801
+ </ng-template>
2802
+ </p-dialog>
2803
+
2804
+ <p-dialog
2805
+ [header]="'task.manager.board.edit' | translate"
2806
+ [(visible)]="showEditBoardDialog"
2807
+ [modal]="true"
2808
+ [style]="{ width: '420px' }"
2809
+ >
2810
+ <div class="flex flex-col gap-4 py-2">
2811
+ <div class="flex flex-col gap-1">
2812
+ <label class="text-sm font-medium">
2813
+ {{ 'task.manager.board.name.label' | translate }}
2814
+ <span class="text-red-500">*</span>
2815
+ </label>
2816
+ <input pInputText [(ngModel)]="editBoardName" />
2817
+ </div>
2818
+ <div class="flex flex-col gap-1">
2819
+ <label class="text-sm font-medium">{{
2820
+ 'task.manager.board.description.label' | translate
2821
+ }}</label>
2822
+ <textarea
2823
+ pTextarea
2824
+ [(ngModel)]="editBoardDescription"
2825
+ [rows]="3"
2826
+ [placeholder]="'task.manager.board.description.placeholder' | translate"
2827
+ ></textarea>
2828
+ </div>
2829
+ <div class="flex flex-col gap-1">
2830
+ <label class="text-sm font-medium">{{
2831
+ 'task.manager.board.color.label' | translate
2832
+ }}</label>
2833
+ <div class="flex items-center gap-3">
2834
+ <input
2835
+ type="color"
2836
+ [(ngModel)]="editBoardColor"
2837
+ class="w-10 h-10 rounded cursor-pointer border border-surface-300"
2838
+ />
2839
+ <div class="flex gap-2 flex-wrap">
2840
+ @for (color of boardColorPalette; track color) {
2841
+ <button
2842
+ type="button"
2843
+ class="w-6 h-6 rounded-full border-2 transition-transform hover:scale-110"
2844
+ [style.background-color]="color"
2845
+ [class.border-surface-900]="editBoardColor === color"
2846
+ [class.border-transparent]="editBoardColor !== color"
2847
+ (click)="editBoardColor = color"
2848
+ ></button>
2849
+ }
2850
+ </div>
2851
+ </div>
2852
+ </div>
2853
+ </div>
2854
+ <ng-template pTemplate="footer">
2855
+ <div class="flex justify-end gap-2">
2856
+ <p-button
2857
+ [label]="'shared.cancel' | translate"
2858
+ severity="secondary"
2859
+ (onClick)="showEditBoardDialog.set(false)"
2860
+ />
2861
+ <p-button
2862
+ [label]="'shared.save' | translate"
2863
+ [loading]="isSavingBoard()"
2864
+ [disabled]="!editBoardName.trim()"
2865
+ (onClick)="saveBoard()"
2866
+ />
2867
+ </div>
2868
+ </ng-template>
2869
+ </p-dialog>
2870
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i1$3.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i1$3.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i1$3.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "ngmodule", type: AngularModule }, { kind: "directive", type: i3.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i3.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: PrimeModule }, { kind: "directive", type: i2$1.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "component", type: i9.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: "component", type: i1$1.Dialog, selector: "p-dialog", inputs: ["hostName", "header", "draggable", "resizable", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "maskMotionOptions", "motionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "closeButtonProps", "maximizeButtonProps", "visible", "style", "position", "role", "appendTo", "content", "contentTemplate", "footerTemplate", "closeIconTemplate", "maximizeIconTemplate", "minimizeIconTemplate", "headlessTemplate"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "directive", type: i4.InputText, selector: "[pInputText]", inputs: ["hostName", "ptInputText", "pInputTextPT", "pInputTextUnstyled", "pSize", "variant", "fluid", "invalid"] }, { kind: "component", type: i6.Select, selector: "p-select", inputs: ["id", "scrollHeight", "filter", "panelStyle", "styleClass", "panelStyleClass", "readonly", "editable", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "filterValue", "options", "appendTo", "motionOptions"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "component", type: i6$1.Skeleton, selector: "p-skeleton", inputs: ["styleClass", "shape", "animation", "borderRadius", "size", "width", "height"] }, { kind: "component", type: i9$2.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "size", "showGridlines", "stripedRows", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i9$2.SortableColumn, selector: "[pSortableColumn]", inputs: ["pSortableColumn", "pSortableColumnDisabled"] }, { kind: "component", type: i9$2.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { kind: "component", type: i1.Tag, selector: "p-tag", inputs: ["styleClass", "severity", "value", "icon", "rounded"] }, { kind: "directive", type: i5.Textarea, selector: "[pTextarea], [pInputTextarea]", inputs: ["pTextareaPT", "pTextareaUnstyled", "autoResize", "pSize", "variant", "fluid", "invalid"], outputs: ["onResize"] }, { kind: "directive", type: i2.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "showOnEllipsis", "pTooltip", "tooltipDisabled", "tooltipOptions", "appendTo", "ptTooltip", "pTooltipPT", "pTooltipUnstyled"] }, { kind: "component", type: TaskCardComponent, selector: "tm-task-card", inputs: ["task"], outputs: ["cardClick", "editClick", "deleteClick"] }, { kind: "component", type: TaskFormDialogComponent, selector: "tm-task-form-dialog", inputs: ["visible", "task", "boardId", "listId", "members", "labels"], outputs: ["visibleChange", "saved"] }, { kind: "component", type: TaskDetailPanelComponent, selector: "tm-task-detail-panel", inputs: ["task"], outputs: ["closePanel", "editTask"] }, { kind: "component", type: UserSelectComponent, selector: "lib-user-select", inputs: ["showClear", "value"], outputs: ["valueChange", "userSelected"] }, { kind: "pipe", type: i9$1.DatePipe, name: "date" }, { kind: "pipe", type: TranslatePipe, name: "translate" }] });
2871
+ }
2872
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.5", ngImport: i0, type: BoardViewPageComponent, decorators: [{
2873
+ type: Component,
2874
+ args: [{
2875
+ selector: 'tm-board-view-page',
2876
+ imports: [
2877
+ DragDropModule,
2878
+ AngularModule,
2879
+ PrimeModule,
2880
+ TaskCardComponent,
2881
+ TaskFormDialogComponent,
2882
+ TaskDetailPanelComponent,
2883
+ TranslatePipe,
2884
+ UserSelectComponent,
2885
+ ],
2886
+ template: `
2887
+ <div class="flex flex-col h-full">
2888
+ <div class="flex flex-col">
2889
+ <div class="flex items-center justify-between px-5 py-2.5 gap-4">
2890
+ @if (state.activeBoard(); as board) {
2891
+ <div class="flex items-center gap-2.5 min-w-0 flex-1 group">
2892
+ <div class="flex flex-col min-w-0">
2893
+ <div class="flex items-center gap-1.5 min-w-0">
2894
+ <h1 class="text-base font-bold text-surface-900 dark:text-surface-0 truncate leading-snug">
2895
+ {{ board.name }}
2896
+ </h1>
2897
+ <p-button
2898
+ icon="pi pi-pencil"
2899
+ [text]="true"
2900
+ size="small"
2901
+ severity="secondary"
2902
+ styleClass="!w-6 !h-6 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
2903
+ [pTooltip]="'task.manager.board.edit' | translate"
2904
+ tooltipPosition="bottom"
2905
+ (onClick)="openEditBoardDialog(board)"
2906
+ />
2907
+ </div>
2908
+ @if (board.description) {
2909
+ <span class="text-xs text-surface-400 truncate max-w-sm hidden md:block leading-snug">
2910
+ {{ board.description }}
2911
+ </span>
2912
+ }
2913
+ </div>
2914
+ </div>
2915
+ }
2916
+
2917
+ <div class="flex items-center gap-1.5 flex-shrink-0">
2918
+ @if (state.members().length > 0) {
2919
+ <div
2920
+ class="flex items-center -space-x-2 cursor-pointer mr-1"
2921
+ (click)="showMembersDialog.set(true)"
2922
+ [pTooltip]="'task.manager.board.manage.members' | translate"
2923
+ tooltipPosition="bottom"
2924
+ >
2925
+ @for (member of state.members().slice(0, 4); track member.id) {
2926
+ <div
2927
+ class="w-6 h-6 rounded-full border-2 border-white dark:border-surface-900 flex items-center justify-center text-[9px] font-bold text-white flex-shrink-0 ring-1 ring-surface-100 dark:ring-surface-800"
2928
+ [style.background-color]="memberColor(member)"
2929
+ >
2930
+ {{ memberInitials(member) }}
2931
+ </div>
2932
+ }
2933
+ @if (state.members().length > 4) {
2934
+ <div
2935
+ class="w-6 h-6 rounded-full border-2 border-white dark:border-surface-900 bg-surface-200 dark:bg-surface-700 flex items-center justify-center text-[9px] font-bold text-surface-600 dark:text-surface-300 flex-shrink-0"
2936
+ >
2937
+ +{{ state.members().length - 4 }}
2938
+ </div>
2939
+ }
2940
+ </div>
2941
+ }
2942
+
2943
+ <div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
2944
+
2945
+ <div class="flex rounded-lg overflow-hidden border border-surface-200 dark:border-surface-700">
2946
+ <p-button
2947
+ icon="pi pi-th-large"
2948
+ [pTooltip]="'task.manager.view.board' | translate"
2949
+ tooltipPosition="bottom"
2950
+ size="small"
2951
+ [severity]="viewMode() === 'board' ? 'primary' : 'secondary'"
2952
+ [outlined]="viewMode() !== 'board'"
2953
+ styleClass="!rounded-none !border-0"
2954
+ (onClick)="viewMode.set('board')"
2955
+ />
2956
+ <p-button
2957
+ icon="pi pi-list"
2958
+ [pTooltip]="'task.manager.view.list' | translate"
2959
+ tooltipPosition="bottom"
2960
+ size="small"
2961
+ [severity]="viewMode() === 'list' ? 'primary' : 'secondary'"
2962
+ [outlined]="viewMode() !== 'list'"
2963
+ styleClass="!rounded-none !border-0"
2964
+ (onClick)="viewMode.set('list')"
2965
+ />
2966
+ </div>
2967
+
2968
+ <div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
2969
+
2970
+ <p-button
2971
+ icon="pi pi-users"
2972
+ [text]="true"
2973
+ severity="secondary"
2974
+ size="small"
2975
+ styleClass="!w-8 !h-8"
2976
+ [pTooltip]="'task.manager.board.manage.members' | translate"
2977
+ tooltipPosition="bottom"
2978
+ (onClick)="showMembersDialog.set(true)"
2979
+ />
2980
+ <p-button
2981
+ icon="pi pi-tag"
2982
+ [text]="true"
2983
+ severity="secondary"
2984
+ size="small"
2985
+ styleClass="!w-8 !h-8"
2986
+ [pTooltip]="'task.manager.board.manage.labels' | translate"
2987
+ tooltipPosition="bottom"
2988
+ (onClick)="showLabelsDialog.set(true)"
2989
+ />
2990
+
2991
+ @if (viewMode() === 'board') {
2992
+ <div class="h-5 w-px bg-surface-200 dark:bg-surface-700 mx-0.5"></div>
2993
+ <p-button
2994
+ icon="pi pi-plus"
2995
+ [label]="'task.manager.board.add.list' | translate"
2996
+ size="small"
2997
+ (onClick)="showAddListDialog.set(true)"
2998
+ />
2999
+ }
3000
+ </div>
3001
+ </div>
3002
+
3003
+ @if (state.activeBoard(); as board) {
3004
+ <div class="h-0.5 w-full flex-shrink-0" [style.background-color]="board.color"></div>
3005
+ }
3006
+ </div>
3007
+
3008
+ @if (state.isBoardLoading()) {
3009
+ @if (viewMode() === 'board') {
3010
+ <div class="flex gap-4 p-6 overflow-x-auto">
3011
+ @for (i of [1, 2, 3]; track i) {
3012
+ <div class="flex-shrink-0 w-72">
3013
+ <p-skeleton height="36px" class="mb-3" />
3014
+ @for (j of [1, 2, 3]; track j) {
3015
+ <p-skeleton height="90px" class="mb-2" />
3016
+ }
3017
+ </div>
3018
+ }
3019
+ </div>
3020
+ } @else {
3021
+ <div class="p-6">
3022
+ @for (i of [1, 2, 3, 4, 5]; track i) {
3023
+ <p-skeleton height="48px" class="mb-2" />
3024
+ }
3025
+ </div>
3026
+ }
3027
+
3028
+ } @else if (viewMode() === 'board') {
3029
+ <div
3030
+ class="flex gap-4 p-5 overflow-x-auto flex-1"
3031
+ cdkDropList
3032
+ cdkDropListOrientation="horizontal"
3033
+ [cdkDropListData]="state.listsWithTasks()"
3034
+ (cdkDropListDropped)="onListDrop($event)"
3035
+ >
3036
+ @for (list of state.listsWithTasks(); track list.id) {
3037
+ <div
3038
+ cdkDrag
3039
+ class="flex-shrink-0 w-72 flex flex-col max-h-[calc(100vh-140px)] self-start"
3040
+ >
3041
+ <div
3042
+ class="flex items-center gap-1 px-2 py-2.5 rounded-t-xl border border-b-0 border-surface-200 dark:border-surface-700"
3043
+ [style.border-top]="'3px solid ' + list.color"
3044
+ >
3045
+ <i
3046
+ cdkDragHandle
3047
+ class="pi pi-bars text-surface-300 dark:text-surface-600 text-xs cursor-grab flex-shrink-0 px-1"
3048
+ ></i>
3049
+
3050
+ <div class="flex items-center gap-2 min-w-0 flex-1">
3051
+ @if (editingListId() === list.id) {
3052
+ <input
3053
+ pInputText
3054
+ class="text-sm font-semibold !h-6 !py-0 !px-1 w-full"
3055
+ [ngModel]="editingListNameValue()"
3056
+ (ngModelChange)="editingListNameValue.set($event)"
3057
+ (keydown.enter)="saveRenameList()"
3058
+ (keydown.escape)="cancelRenameList()"
3059
+ (blur)="cancelRenameList()"
3060
+ (click)="$event.stopPropagation()"
3061
+ autofocus
3062
+ />
3063
+ } @else {
3064
+ <span class="font-semibold text-sm truncate">{{ list.name }}</span>
3065
+ <span
3066
+ class="text-[11px] font-semibold px-1.5 py-0.5 rounded-full text-white flex-shrink-0"
3067
+ [style.background-color]="list.color"
3068
+ >
3069
+ {{ list.tasks.length }}
3070
+ </span>
3071
+ }
3072
+ </div>
3073
+
3074
+ @if (editingListId() === list.id) {
3075
+ <p-button
3076
+ icon="pi pi-check"
3077
+ [text]="true"
3078
+ size="small"
3079
+ severity="success"
3080
+ styleClass="!w-6 !h-6 flex-shrink-0"
3081
+ (mousedown)="$event.preventDefault()"
3082
+ (click)="saveRenameList(); $event.stopPropagation()"
3083
+ />
3084
+ <p-button
3085
+ icon="pi pi-times"
3086
+ [text]="true"
3087
+ size="small"
3088
+ severity="secondary"
3089
+ styleClass="!w-6 !h-6 flex-shrink-0"
3090
+ (mousedown)="$event.preventDefault()"
3091
+ (click)="cancelRenameList(); $event.stopPropagation()"
3092
+ />
3093
+ } @else {
3094
+ <p-button
3095
+ icon="pi pi-pencil"
3096
+ [text]="true"
3097
+ size="small"
3098
+ severity="secondary"
3099
+ styleClass="!w-6 !h-6 flex-shrink-0"
3100
+ [pTooltip]="'task.manager.list.rename' | translate"
3101
+ tooltipPosition="bottom"
3102
+ (click)="startRenameList(list); $event.stopPropagation()"
3103
+ />
3104
+ }
3105
+ </div>
3106
+
3107
+ <div
3108
+ cdkDropList
3109
+ [id]="list.id"
3110
+ [cdkDropListData]="list.tasks"
3111
+ [cdkDropListConnectedTo]="listDropIds()"
3112
+ (cdkDropListDropped)="onTaskDrop($event)"
3113
+ class="flex flex-col gap-2 p-2 overflow-y-auto flex-1 min-h-[60px] border border-t-0 border-b-0 border-surface-200 dark:border-surface-700"
3114
+ >
3115
+ @for (task of list.tasks; track task.id) {
3116
+ <div cdkDrag>
3117
+ <tm-task-card
3118
+ [task]="task"
3119
+ (cardClick)="state.selectTask($event)"
3120
+ (editClick)="onEditTask($event)"
3121
+ (deleteClick)="onDeleteTask($event)"
3122
+ />
3123
+ </div>
3124
+ }
3125
+ @if (list.tasks.length === 0) {
3126
+ <div
3127
+ class="flex items-center justify-center h-14 border-2 border-dashed border-surface-200 dark:border-surface-700 rounded-lg"
3128
+ >
3129
+ <span class="text-xs text-surface-400">{{
3130
+ 'task.manager.board.drop.tasks.here' | translate
3131
+ }}</span>
3132
+ </div>
3133
+ }
3134
+ </div>
3135
+
3136
+ <div
3137
+ class="rounded-b-xl border border-t-0 border-surface-200 dark:border-surface-700 p-1.5"
3138
+ >
3139
+ <p-button
3140
+ icon="pi pi-plus"
3141
+ [label]="'task.manager.board.add.task' | translate"
3142
+ severity="secondary"
3143
+ size="small"
3144
+ styleClass="w-full !justify-start"
3145
+ (onClick)="openAddTask(list)"
3146
+ />
3147
+ </div>
3148
+ </div>
3149
+ }
3150
+
3151
+ @if (state.listsWithTasks().length === 0) {
3152
+ <div class="flex-1 flex items-center justify-center">
3153
+ <div class="text-center text-surface-400">
3154
+ <div
3155
+ class="w-16 h-16 rounded-2xl flex items-center justify-center mx-auto mb-4"
3156
+ >
3157
+ <i
3158
+ class="pi pi-list text-3xl text-surface-300 dark:text-surface-600"
3159
+ ></i>
3160
+ </div>
3161
+ <p class="font-medium text-surface-500">
3162
+ {{ 'task.manager.board.list.empty' | translate }}
3163
+ </p>
3164
+ <p-button
3165
+ icon="pi pi-plus"
3166
+ [label]="'task.manager.board.add.list' | translate"
3167
+ severity="secondary"
3168
+ size="small"
3169
+ class="mt-3"
3170
+ (onClick)="showAddListDialog.set(true)"
3171
+ />
3172
+ </div>
3173
+ </div>
3174
+ }
3175
+ </div>
3176
+
3177
+ } @else {
3178
+ <div class="p-4 sm:p-6 flex-1 overflow-auto">
3179
+ <div class="card">
3180
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
3181
+ <p-table
3182
+ [value]="allTasksFlat()"
3183
+ [paginator]="allTasksFlat().length > 20"
3184
+ [rows]="20"
3185
+ [rowsPerPageOptions]="[10, 20, 50]"
3186
+ [scrollable]="true"
3187
+ class="p-datatable-sm"
3188
+ [tableStyle]="{ 'min-width': '700px' }"
3189
+ >
3190
+ <ng-template #header>
3191
+ <tr>
3192
+ <th pSortableColumn="title" class="min-w-[240px]">
3193
+ {{ 'task.manager.table.title' | translate }}
3194
+ <p-sortIcon field="title" />
3195
+ </th>
3196
+ <th pSortableColumn="listName" class="w-40">
3197
+ {{ 'task.manager.table.list' | translate }}
3198
+ <p-sortIcon field="listName" />
3199
+ </th>
3200
+ <th pSortableColumn="priority" class="w-32">
3201
+ {{ 'task.manager.table.priority' | translate }}
3202
+ <p-sortIcon field="priority" />
3203
+ </th>
3204
+ <th class="w-36">
3205
+ {{ 'task.manager.table.assignee' | translate }}
3206
+ </th>
3207
+ <th pSortableColumn="dueDate" class="w-32">
3208
+ {{ 'task.manager.table.due.date' | translate }}
3209
+ <p-sortIcon field="dueDate" />
3210
+ </th>
3211
+ <th class="w-[100px]">{{ 'shared.actions' | translate }}</th>
3212
+ </tr>
3213
+ </ng-template>
3214
+ <ng-template #body let-task>
3215
+ <tr>
3216
+ <td>
3217
+ <span
3218
+ class="font-medium text-sm cursor-pointer hover:text-primary-500 transition-colors"
3219
+ (click)="state.selectTask(task)"
3220
+ >{{ task.title }}</span>
3221
+ </td>
3222
+ <td>
3223
+ <span class="inline-flex items-center gap-1.5 text-sm">
3224
+ <span
3225
+ class="w-2 h-2 rounded-full flex-shrink-0"
3226
+ [style.background-color]="task.listColor"
3227
+ ></span>
3228
+ {{ task.listName }}
3229
+ </span>
3230
+ </td>
3231
+ <td>
3232
+ <p-tag
3233
+ [value]="task.priority"
3234
+ [severity]="prioritySeverity(task.priority)"
3235
+ />
3236
+ </td>
3237
+ <td>
3238
+ @if (task.assignees?.length) {
3239
+ <div class="flex items-center -space-x-1.5">
3240
+ @for (a of task.assignees!.slice(0, 3); track a.userId) {
3241
+ <span
3242
+ class="w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white flex-shrink-0 ring-2 ring-white dark:ring-surface-800"
3243
+ [style.background-color]="memberColorById(a.userId)"
3244
+ [pTooltip]="a.userName ?? ''"
3245
+ >
3246
+ {{ getMemberInitials(a.userId) }}
3247
+ </span>
3248
+ }
3249
+ @if (task.assignees!.length > 3) {
3250
+ <span
3251
+ class="w-6 h-6 rounded-full bg-surface-200 dark:bg-surface-700 flex items-center justify-center text-[10px] font-bold text-surface-600 ring-2 ring-white dark:ring-surface-800 flex-shrink-0"
3252
+ >
3253
+ +{{ task.assignees!.length - 3 }}
3254
+ </span>
3255
+ }
3256
+ </div>
3257
+ } @else {
3258
+ <span class="text-muted-color text-sm">—</span>
3259
+ }
3260
+ </td>
3261
+ <td>
3262
+ @if (task.dueDate) {
3263
+ <span
3264
+ class="text-sm"
3265
+ [class.text-red-500]="isOverdue(task.dueDate)"
3266
+ >
3267
+ {{ task.dueDate | date: 'MMM d, y' }}
3268
+ </span>
3269
+ } @else {
3270
+ <span class="text-muted-color text-sm">—</span>
3271
+ }
3272
+ </td>
3273
+ <td>
3274
+ <div class="flex gap-1">
3275
+ <p-button
3276
+ icon="pi pi-pencil"
3277
+ [text]="true"
3278
+ severity="secondary"
3279
+ size="small"
3280
+ [pTooltip]="'shared.edit' | translate"
3281
+ (onClick)="onEditTask(task)"
3282
+ />
3283
+ <p-button
3284
+ icon="pi pi-trash"
3285
+ [text]="true"
3286
+ severity="danger"
3287
+ size="small"
3288
+ [pTooltip]="'shared.delete' | translate"
3289
+ (onClick)="onDeleteTask(task)"
3290
+ />
3291
+ </div>
3292
+ </td>
3293
+ </tr>
3294
+ </ng-template>
3295
+ <ng-template #emptymessage>
3296
+ <tr>
3297
+ <td colspan="6" class="text-center text-muted-color py-8">
3298
+ <i class="pi pi-inbox text-4xl block mb-3"></i>
3299
+ {{ 'task.manager.table.empty' | translate }}
3300
+ </td>
3301
+ </tr>
3302
+ </ng-template>
3303
+ </p-table>
3304
+ </div>
3305
+ </div>
3306
+ </div>
3307
+ }
3308
+ </div>
3309
+
3310
+ <tm-task-form-dialog
3311
+ [(visible)]="showTaskForm"
3312
+ [task]="editingTask()"
3313
+ [boardId]="boardId()"
3314
+ [listId]="activeListId()"
3315
+ [members]="state.members()"
3316
+ [labels]="state.labels()"
3317
+ (saved)="onTaskSaved($event)"
3318
+ />
3319
+
3320
+ <tm-task-detail-panel
3321
+ [task]="state.selectedTask()"
3322
+ (closePanel)="state.selectTask(null)"
3323
+ (editTask)="onEditTask($event)"
3324
+ />
3325
+
3326
+ <p-dialog
3327
+ [header]="'task.manager.label.manage.title' | translate"
3328
+ [(visible)]="showLabelsDialog"
3329
+ [modal]="true"
3330
+ [style]="{ width: '420px' }"
3331
+ (onHide)="newLabelName = ''; newLabelColor = '#6366F1'"
3332
+ >
3333
+ <div class="flex flex-col gap-4 py-2">
3334
+ <div class="flex gap-2 items-end">
3335
+ <div class="flex flex-col gap-1 flex-1">
3336
+ <label class="text-sm font-medium">{{
3337
+ 'task.manager.label.name.label' | translate
3338
+ }}</label>
3339
+ <input
3340
+ pInputText
3341
+ [(ngModel)]="newLabelName"
3342
+ [placeholder]="'task.manager.label.name.placeholder' | translate"
3343
+ />
3344
+ </div>
3345
+ <div class="flex flex-col gap-1">
3346
+ <label class="text-sm font-medium">{{
3347
+ 'task.manager.label.color.label' | translate
3348
+ }}</label>
3349
+ <input
3350
+ type="color"
3351
+ [(ngModel)]="newLabelColor"
3352
+ class="w-10 h-10 rounded cursor-pointer border border-surface-300"
3353
+ />
3354
+ </div>
3355
+ <p-button
3356
+ icon="pi pi-plus"
3357
+ [label]="'task.manager.label.add' | translate"
3358
+ size="small"
3359
+ [loading]="isAddingLabel()"
3360
+ (onClick)="addLabel()"
3361
+ />
3362
+ </div>
3363
+
3364
+ <div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
3365
+ @if (state.labels().length === 0) {
3366
+ <p class="text-sm text-surface-400 text-center py-4">
3367
+ {{ 'task.manager.label.no.labels' | translate }}
3368
+ </p>
3369
+ }
3370
+ @for (label of state.labels(); track label.id) {
3371
+ <div
3372
+ class="flex items-center justify-between px-3 py-2 rounded-lg border border-surface-200 dark:border-surface-700"
3373
+ >
3374
+ <div class="flex items-center gap-2">
3375
+ <span
3376
+ class="w-4 h-4 rounded-full flex-shrink-0"
3377
+ [style.background-color]="label.color"
3378
+ ></span>
3379
+ <span class="text-sm font-medium">{{ label.name }}</span>
3380
+ </div>
3381
+ <p-button
3382
+ icon="pi pi-trash"
3383
+ severity="danger"
3384
+ size="small"
3385
+ [text]="true"
3386
+ (onClick)="removeLabel(label.id)"
3387
+ />
3388
+ </div>
3389
+ }
3390
+ </div>
3391
+ </div>
3392
+ </p-dialog>
3393
+
3394
+ <p-dialog
3395
+ [header]="'task.manager.member.manage.title' | translate"
3396
+ [(visible)]="showMembersDialog"
3397
+ [modal]="true"
3398
+ [style]="{ width: '480px' }"
3399
+ (onHide)="resetMemberForm()"
3400
+ >
3401
+ <div class="flex flex-col gap-4 py-2">
3402
+ <div class="flex flex-col gap-3">
3403
+ <div class="flex flex-col gap-1">
3404
+ <label class="text-sm font-medium">{{
3405
+ 'task.manager.member.user.label' | translate
3406
+ }}</label>
3407
+ <lib-user-select
3408
+ [(value)]="selectedMemberUser"
3409
+ [isEditMode]="true"
3410
+ [placeHolder]="'task.manager.member.user.placeholder' | translate"
3411
+ />
3412
+ </div>
3413
+
3414
+ <div class="flex gap-2 items-end">
3415
+ <div class="flex flex-col gap-1 flex-1">
3416
+ <label class="text-sm font-medium">{{
3417
+ 'task.manager.member.role.label' | translate
3418
+ }}</label>
3419
+ <p-select
3420
+ [(ngModel)]="newMemberRole"
3421
+ [options]="memberRoleOptions"
3422
+ optionValue="value"
3423
+ [placeholder]="
3424
+ 'task.manager.member.role.placeholder' | translate
3425
+ "
3426
+ class="w-full"
3427
+ appendTo="body"
3428
+ >
3429
+ <ng-template #selectedItem let-opt>
3430
+ {{ opt.labelKey | translate }}
3431
+ </ng-template>
3432
+ <ng-template #item let-opt>
3433
+ {{ opt.labelKey | translate }}
3434
+ </ng-template>
3435
+ </p-select>
3436
+ </div>
3437
+ <p-button
3438
+ icon="pi pi-plus"
3439
+ [label]="'task.manager.member.add' | translate"
3440
+ size="small"
3441
+ [loading]="isAddingMember()"
3442
+ [disabled]="!selectedMemberUser"
3443
+ (onClick)="addMember()"
3444
+ />
3445
+ </div>
3446
+ </div>
3447
+
3448
+ <div class="flex flex-col gap-2 max-h-72 overflow-y-auto">
3449
+ @if (state.members().length === 0) {
3450
+ <p class="text-sm text-surface-400 text-center py-4">
3451
+ {{ 'task.manager.member.no.members' | translate }}
3452
+ </p>
3453
+ }
3454
+ @for (member of state.members(); track member.id) {
3455
+ <div
3456
+ class="flex items-center justify-between px-3 py-2.5 rounded-lg border border-surface-200 dark:border-surface-700"
3457
+ >
3458
+ <div class="flex items-center gap-3">
3459
+ <div
3460
+ class="w-8 h-8 rounded-full flex items-center justify-center text-[11px] font-bold text-white flex-shrink-0"
3461
+ [style.background-color]="memberColor(member)"
3462
+ >
3463
+ {{ memberInitials(member) }}
3464
+ </div>
3465
+ <div>
3466
+ <div class="text-sm font-medium ">
3467
+ {{ member.userName ?? member.userId }}
3468
+ </div>
3469
+ @if (member.userEmail) {
3470
+ <div class="text-xs text-surface-400">
3471
+ {{ member.userEmail }}
3472
+ </div>
3473
+ }
3474
+ </div>
3475
+ </div>
3476
+ <div class="flex items-center gap-2">
3477
+ <span
3478
+ class="text-[10px] font-semibold px-2 py-0.5 rounded-full"
3479
+ [class]="roleBadgeClass(member.role)"
3480
+ >
3481
+ {{ ('task.manager.member.role.' + member.role.toLowerCase()) | translate }}
3482
+ </span>
3483
+ @if (member.role !== 'OWNER') {
3484
+ <p-button
3485
+ icon="pi pi-trash"
3486
+ severity="danger"
3487
+ size="small"
3488
+ [text]="true"
3489
+ (onClick)="removeMember(member.userId)"
3490
+ />
3491
+ }
3492
+ </div>
3493
+ </div>
3494
+ }
3495
+ </div>
3496
+ </div>
3497
+ </p-dialog>
3498
+
3499
+ <p-dialog
3500
+ [header]="'task.manager.list.new' | translate"
3501
+ [(visible)]="showAddListDialog"
3502
+ [modal]="true"
3503
+ [style]="{ width: '360px' }"
3504
+ (onHide)="newListName = ''"
3505
+ >
3506
+ <div class="flex flex-col gap-2 py-2">
3507
+ <label class="text-sm font-medium">
3508
+ {{ 'task.manager.list.name.label' | translate }}
3509
+ <span class="text-red-500">*</span>
3510
+ </label>
3511
+ <input
3512
+ pInputText
3513
+ [(ngModel)]="newListName"
3514
+ [placeholder]="'task.manager.list.name.placeholder' | translate"
3515
+ />
3516
+ </div>
3517
+ <ng-template pTemplate="footer">
3518
+ <div class="flex justify-end gap-2">
3519
+ <p-button
3520
+ [label]="'shared.cancel' | translate"
3521
+ severity="secondary"
3522
+ (onClick)="showAddListDialog.set(false)"
3523
+ />
3524
+ <p-button
3525
+ [label]="'shared.create' | translate"
3526
+ [loading]="isAddingList()"
3527
+ (onClick)="addList()"
3528
+ />
3529
+ </div>
3530
+ </ng-template>
3531
+ </p-dialog>
3532
+
3533
+ <p-dialog
3534
+ [header]="'task.manager.board.edit' | translate"
3535
+ [(visible)]="showEditBoardDialog"
3536
+ [modal]="true"
3537
+ [style]="{ width: '420px' }"
3538
+ >
3539
+ <div class="flex flex-col gap-4 py-2">
3540
+ <div class="flex flex-col gap-1">
3541
+ <label class="text-sm font-medium">
3542
+ {{ 'task.manager.board.name.label' | translate }}
3543
+ <span class="text-red-500">*</span>
3544
+ </label>
3545
+ <input pInputText [(ngModel)]="editBoardName" />
3546
+ </div>
3547
+ <div class="flex flex-col gap-1">
3548
+ <label class="text-sm font-medium">{{
3549
+ 'task.manager.board.description.label' | translate
3550
+ }}</label>
3551
+ <textarea
3552
+ pTextarea
3553
+ [(ngModel)]="editBoardDescription"
3554
+ [rows]="3"
3555
+ [placeholder]="'task.manager.board.description.placeholder' | translate"
3556
+ ></textarea>
3557
+ </div>
3558
+ <div class="flex flex-col gap-1">
3559
+ <label class="text-sm font-medium">{{
3560
+ 'task.manager.board.color.label' | translate
3561
+ }}</label>
3562
+ <div class="flex items-center gap-3">
3563
+ <input
3564
+ type="color"
3565
+ [(ngModel)]="editBoardColor"
3566
+ class="w-10 h-10 rounded cursor-pointer border border-surface-300"
3567
+ />
3568
+ <div class="flex gap-2 flex-wrap">
3569
+ @for (color of boardColorPalette; track color) {
3570
+ <button
3571
+ type="button"
3572
+ class="w-6 h-6 rounded-full border-2 transition-transform hover:scale-110"
3573
+ [style.background-color]="color"
3574
+ [class.border-surface-900]="editBoardColor === color"
3575
+ [class.border-transparent]="editBoardColor !== color"
3576
+ (click)="editBoardColor = color"
3577
+ ></button>
3578
+ }
3579
+ </div>
3580
+ </div>
3581
+ </div>
3582
+ </div>
3583
+ <ng-template pTemplate="footer">
3584
+ <div class="flex justify-end gap-2">
3585
+ <p-button
3586
+ [label]="'shared.cancel' | translate"
3587
+ severity="secondary"
3588
+ (onClick)="showEditBoardDialog.set(false)"
3589
+ />
3590
+ <p-button
3591
+ [label]="'shared.save' | translate"
3592
+ [loading]="isSavingBoard()"
3593
+ [disabled]="!editBoardName.trim()"
3594
+ (onClick)="saveBoard()"
3595
+ />
3596
+ </div>
3597
+ </ng-template>
3598
+ </p-dialog>
3599
+ `,
3600
+ }]
3601
+ }], ctorParameters: () => [], propDecorators: { boardId: [{ type: i0.Input, args: [{ isSignal: true, alias: "boardId", required: true }] }] } });
3602
+
3603
+ var boardViewPage_component = /*#__PURE__*/Object.freeze({
3604
+ __proto__: null,
3605
+ BoardViewPageComponent: BoardViewPageComponent
3606
+ });
3607
+
3608
+ const TASK_MANAGER_MESSAGES = {
3609
+ // Board API messages (keys must match NestJS message-keys.ts)
3610
+ 'task.board.not.found': 'Board not found',
3611
+ 'task.board.create.success': 'Board created successfully',
3612
+ 'task.board.create.many.success': 'Boards created successfully',
3613
+ 'task.board.get.success': 'Board retrieved successfully',
3614
+ 'task.board.get.all.success': 'Boards retrieved successfully',
3615
+ 'task.board.update.success': 'Board updated successfully',
3616
+ 'task.board.update.many.success': 'Boards updated successfully',
3617
+ 'task.board.delete.success': 'Board deleted successfully',
3618
+ 'task.board.restore.success': 'Board restored successfully',
3619
+ 'task.board.member.added': 'Member added to board',
3620
+ 'task.board.member.removed': 'Member removed from board',
3621
+ 'task.board.member.not.found': 'Board member not found',
3622
+ 'task.board.member.already.exists': 'User is already a board member',
3623
+ 'task.board.member.get.all.success': 'Board members retrieved successfully',
3624
+ 'task.list.not.found': 'List not found',
3625
+ 'task.list.create.success': 'List created successfully',
3626
+ 'task.list.create.many.success': 'Lists created successfully',
3627
+ 'task.list.get.success': 'List retrieved successfully',
3628
+ 'task.list.get.all.success': 'Lists retrieved successfully',
3629
+ 'task.list.update.success': 'List updated successfully',
3630
+ 'task.list.update.many.success': 'Lists updated successfully',
3631
+ 'task.list.delete.success': 'List deleted successfully',
3632
+ 'task.list.restore.success': 'List restored successfully',
3633
+ 'task.list.reorder.success': 'Lists reordered successfully',
3634
+ 'task.not.found': 'Task not found',
3635
+ 'task.create.success': 'Task created successfully',
3636
+ 'task.create.many.success': 'Tasks created successfully',
3637
+ 'task.get.success': 'Task retrieved successfully',
3638
+ 'task.get.all.success': 'Tasks retrieved successfully',
3639
+ 'task.update.success': 'Task updated successfully',
3640
+ 'task.update.many.success': 'Tasks updated successfully',
3641
+ 'task.delete.success': 'Task deleted successfully',
3642
+ 'task.restore.success': 'Task restored successfully',
3643
+ 'task.move.success': 'Task moved successfully',
3644
+ 'task.assign.success': 'Task assigned successfully',
3645
+ 'task.assignee.not.board.member': 'Assignee is not a board member',
3646
+ 'task.label.not.found': 'Label not found',
3647
+ 'task.label.create.success': 'Label created successfully',
3648
+ 'task.label.create.many.success': 'Labels created successfully',
3649
+ 'task.label.get.success': 'Label retrieved successfully',
3650
+ 'task.label.get.all.success': 'Labels retrieved successfully',
3651
+ 'task.label.update.success': 'Label updated successfully',
3652
+ 'task.label.update.many.success': 'Labels updated successfully',
3653
+ 'task.label.delete.success': 'Label deleted successfully',
3654
+ 'task.label.restore.success': 'Label restored successfully',
3655
+ 'task.comment.not.found': 'Comment not found',
3656
+ 'task.comment.create.success': 'Comment added successfully',
3657
+ 'task.comment.create.many.success': 'Comments added successfully',
3658
+ 'task.comment.get.success': 'Comment retrieved successfully',
3659
+ 'task.comment.get.all.success': 'Comments retrieved successfully',
3660
+ 'task.comment.update.success': 'Comment updated successfully',
3661
+ 'task.comment.update.many.success': 'Comments updated successfully',
3662
+ 'task.comment.delete.success': 'Comment deleted successfully',
3663
+ 'task.comment.restore.success': 'Comment restored successfully',
3664
+ 'task.comment.forbidden': "Cannot edit another user's comment",
3665
+ 'task.manager.boards.title': 'Task Boards',
3666
+ 'task.manager.boards.subtitle': 'Manage and organize your project boards',
3667
+ 'task.manager.board.new': 'New Board',
3668
+ 'task.manager.board.name.placeholder': 'e.g. Product Backlog',
3669
+ 'task.manager.board.description.placeholder': 'Optional description',
3670
+ 'task.manager.board.color.label': 'Board Color',
3671
+ 'task.manager.board.empty': 'No boards yet',
3672
+ 'task.manager.board.empty.hint': 'Create your first board to get started',
3673
+ 'task.manager.board.members': 'Members',
3674
+ 'task.manager.board.lists': 'Lists',
3675
+ 'task.manager.board.tasks': 'Tasks',
3676
+ 'task.manager.board.add.list': 'Add List',
3677
+ 'task.manager.board.add.task': 'Add task',
3678
+ 'task.manager.board.drop.tasks.here': 'Drop tasks here',
3679
+ 'task.manager.board.list.empty': 'No lists yet. Click "Add List" to create one.',
3680
+ 'task.manager.list.new': 'New List',
3681
+ 'task.manager.list.name.label': 'List Name',
3682
+ 'task.manager.list.name.placeholder': 'e.g. In Progress',
3683
+ 'task.manager.list.rename': 'Rename List',
3684
+ 'task.manager.board.manage.members': 'Members',
3685
+ 'task.manager.member.manage.title': 'Manage Members',
3686
+ 'task.manager.member.add': 'Add Member',
3687
+ 'task.manager.member.no.members': 'No members yet.',
3688
+ 'task.manager.member.user.label': 'User',
3689
+ 'task.manager.member.user.placeholder': 'Search by name or email',
3690
+ 'task.manager.member.role.label': 'Role',
3691
+ 'task.manager.member.role.placeholder': 'Select role',
3692
+ 'task.manager.member.role.owner': 'Owner',
3693
+ 'task.manager.member.role.admin': 'Admin',
3694
+ 'task.manager.member.role.member': 'Member',
3695
+ 'task.manager.member.role.viewer': 'Viewer',
3696
+ 'task.manager.board.manage.labels': 'Labels',
3697
+ 'task.manager.label.manage.title': 'Manage Labels',
3698
+ 'task.manager.label.name.label': 'Label Name',
3699
+ 'task.manager.label.name.placeholder': 'e.g. Bug, Feature',
3700
+ 'task.manager.label.color.label': 'Color',
3701
+ 'task.manager.label.add': 'Add Label',
3702
+ 'task.manager.label.no.labels': 'No labels yet.',
3703
+ 'task.manager.board.edit': 'Edit Board',
3704
+ 'task.manager.board.name.label': 'Board Name',
3705
+ 'task.manager.board.description.label': 'Description',
3706
+ 'task.manager.view.board': 'Board',
3707
+ 'task.manager.view.list': 'List',
3708
+ 'task.manager.table.title': 'Title',
3709
+ 'task.manager.table.list': 'List',
3710
+ 'task.manager.table.priority': 'Priority',
3711
+ 'task.manager.table.assignee': 'Assignee',
3712
+ 'task.manager.table.due.date': 'Due Date',
3713
+ 'task.manager.table.empty': 'No tasks yet.',
3714
+ 'task.form.title.create': 'New Task',
3715
+ 'task.form.title.edit': 'Edit Task',
3716
+ 'task.form.title.label': 'Title',
3717
+ 'task.form.title.placeholder': 'Task title',
3718
+ 'task.form.description.placeholder': 'Add a description...',
3719
+ 'task.form.priority.label': 'Priority',
3720
+ 'task.form.priority.placeholder': 'Select priority',
3721
+ 'task.form.due.date.label': 'Due Date',
3722
+ 'task.form.due.date.placeholder': 'Pick a date',
3723
+ 'task.form.assignee.label': 'Assignee',
3724
+ 'task.form.assignee.placeholder': 'Assign to board member',
3725
+ 'task.form.labels.label': 'Labels',
3726
+ 'task.form.labels.placeholder': 'Select labels',
3727
+ 'task.priority.LOW': 'Low',
3728
+ 'task.priority.MEDIUM': 'Medium',
3729
+ 'task.priority.HIGH': 'High',
3730
+ 'task.priority.URGENT': 'Urgent',
3731
+ 'task.detail.description.empty': 'No description',
3732
+ 'task.detail.comments.label': 'Comments',
3733
+ 'task.detail.comments.empty': 'No comments yet',
3734
+ 'task.detail.comment.placeholder': 'Add a comment...',
3735
+ 'task.detail.due.label': 'Due',
3736
+ 'task.detail.assignees.label': 'Assignees',
3737
+ 'task.detail.edit': 'Edit task',
3738
+ 'task.detail.delete': 'Delete task',
3739
+ 'task.comment.reply': 'Reply',
3740
+ 'task.comment.reply.placeholder': 'Write a reply...',
3741
+ };
3742
+
3743
+ const TASK_MANAGER_ROUTES = [
3744
+ {
3745
+ path: '',
3746
+ resolve: {
3747
+ translations: resolveTranslationModule({
3748
+ modules: ['task-manager'],
3749
+ fallbackMessages: { ...TASK_MANAGER_MESSAGES, ...SHARED_MESSAGES },
3750
+ }),
3751
+ },
3752
+ loadComponent: () => Promise.resolve().then(function () { return boardListPage_component; }).then((m) => m.BoardListPageComponent),
3753
+ },
3754
+ {
3755
+ path: 'board/:boardId',
3756
+ resolve: {
3757
+ translations: resolveTranslationModule({
3758
+ modules: ['task-manager'],
3759
+ fallbackMessages: { ...TASK_MANAGER_MESSAGES, ...SHARED_MESSAGES },
3760
+ }),
3761
+ },
3762
+ loadComponent: () => Promise.resolve().then(function () { return boardViewPage_component; }).then((m) => m.BoardViewPageComponent),
3763
+ },
3764
+ ];
3765
+
3766
+ /**
3767
+ * Generated bundle index. Do not edit.
3768
+ */
3769
+
3770
+ export { BOARD_COLOR_PALETTE, BoardListPageComponent, BoardViewPageComponent, MEMBER_COLOR_PALETTE, TASK_MANAGER_MESSAGES, TASK_MANAGER_ROUTES, TaskApiService, TaskBoardApiService, TaskBoardSocketService, TaskBoardStateService, TaskCardComponent, TaskCommentApiService, TaskDetailPanelComponent, TaskFormDialogComponent, TaskLabelApiService, TaskListApiService, isTaskOverdue, memberColorFromId, memberInitials, taskPrioritySeverity };
3771
+ //# sourceMappingURL=flusys-ng-task-manager.mjs.map