@flusys/ng-task-manager 1.2.0-rc.1

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