@flusys/ng-task-manager 9.1.0 → 9.1.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,2345 @@
1
+ import { BOARD_COLOR_PALETTE, TaskApiService, TaskBoardStateService, TaskCardComponent, TaskDetailPanelComponent, TaskFormDialogComponent, TaskListApiService, isTaskOverdue, memberColorFromId, memberInitials, taskPrioritySeverity } from "./flusys-ng-task-manager.mjs";
2
+ import * as i0 from "@angular/core";
3
+ import { Component, DestroyRef, computed, effect, inject, input, signal, untracked } from "@angular/core";
4
+ import { TranslatePipe, TranslateService, UserSelectComponent } from "@flusys/ng-shared";
5
+ import { DatePipe } from "@angular/common";
6
+ import { AvatarComponent, ConfirmationService, DialogComponent, FButtonDirective, InputTextDirective, MessageService, SelectComponent, SkeletonComponent, SortIconComponent, SortableColumnDirective, TableComponent, TagComponent, TextareaDirective, TooltipDirective, UiIconComponent } from "@flusys/ng-ui";
7
+ import * as i2 from "@angular/forms";
8
+ import { FormsModule } from "@angular/forms";
9
+ import * as i1 from "@angular/cdk/drag-drop";
10
+ import { DragDropModule, moveItemInArray, transferArrayItem } from "@angular/cdk/drag-drop";
11
+ var BoardViewPageComponent = class BoardViewPageComponent {
12
+ state = inject(TaskBoardStateService);
13
+ taskApi = inject(TaskApiService);
14
+ listApi = inject(TaskListApiService);
15
+ confirmationService = inject(ConfirmationService);
16
+ messageService = inject(MessageService);
17
+ translateAdapter = inject(TranslateService, { optional: true });
18
+ destroyRef = inject(DestroyRef);
19
+ translate(key, vars) {
20
+ return this.translateAdapter?.translate(key, vars) ?? key;
21
+ }
22
+ boardId = input.required(...ngDevMode ? [{ debugName: "boardId" }] : /* istanbul ignore next */ []);
23
+ viewMode = signal("board", ...ngDevMode ? [{ debugName: "viewMode" }] : /* istanbul ignore next */ []);
24
+ showTaskForm = signal(false, ...ngDevMode ? [{ debugName: "showTaskForm" }] : /* istanbul ignore next */ []);
25
+ editingTask = signal(null, ...ngDevMode ? [{ debugName: "editingTask" }] : /* istanbul ignore next */ []);
26
+ activeListId = signal("", ...ngDevMode ? [{ debugName: "activeListId" }] : /* istanbul ignore next */ []);
27
+ showAddListDialog = signal(false, ...ngDevMode ? [{ debugName: "showAddListDialog" }] : /* istanbul ignore next */ []);
28
+ isAddingList = signal(false, ...ngDevMode ? [{ debugName: "isAddingList" }] : /* istanbul ignore next */ []);
29
+ showLabelsDialog = signal(false, ...ngDevMode ? [{ debugName: "showLabelsDialog" }] : /* istanbul ignore next */ []);
30
+ isAddingLabel = signal(false, ...ngDevMode ? [{ debugName: "isAddingLabel" }] : /* istanbul ignore next */ []);
31
+ showMembersDialog = signal(false, ...ngDevMode ? [{ debugName: "showMembersDialog" }] : /* istanbul ignore next */ []);
32
+ isAddingMember = signal(false, ...ngDevMode ? [{ debugName: "isAddingMember" }] : /* istanbul ignore next */ []);
33
+ editingListId = signal(null, ...ngDevMode ? [{ debugName: "editingListId" }] : /* istanbul ignore next */ []);
34
+ editingListNameValue = signal("", ...ngDevMode ? [{ debugName: "editingListNameValue" }] : /* istanbul ignore next */ []);
35
+ showEditBoardDialog = signal(false, ...ngDevMode ? [{ debugName: "showEditBoardDialog" }] : /* istanbul ignore next */ []);
36
+ isSavingBoard = signal(false, ...ngDevMode ? [{ debugName: "isSavingBoard" }] : /* istanbul ignore next */ []);
37
+ newListName = "";
38
+ newLabelName = "";
39
+ newLabelColor = BOARD_COLOR_PALETTE[0];
40
+ selectedMemberUser = null;
41
+ newMemberRole = "MEMBER";
42
+ editBoardName = "";
43
+ editBoardDescription = "";
44
+ editBoardColor = BOARD_COLOR_PALETTE[0];
45
+ boardColorPalette = BOARD_COLOR_PALETTE;
46
+ memberRoleOptions = [
47
+ {
48
+ labelKey: "task.manager.member.role.admin",
49
+ value: "ADMIN"
50
+ },
51
+ {
52
+ labelKey: "task.manager.member.role.member",
53
+ value: "MEMBER"
54
+ },
55
+ {
56
+ labelKey: "task.manager.member.role.viewer",
57
+ value: "VIEWER"
58
+ }
59
+ ];
60
+ allTasksFlat = computed(() => this.state.listsWithTasks().flatMap((list) => list.tasks.map((task) => ({
61
+ ...task,
62
+ listName: list.name,
63
+ listColor: list.color
64
+ }))), ...ngDevMode ? [{ debugName: "allTasksFlat" }] : /* istanbul ignore next */ []);
65
+ listDropIds = computed(() => this.state.listsWithTasks().map((l) => l.id), ...ngDevMode ? [{ debugName: "listDropIds" }] : /* istanbul ignore next */ []);
66
+ constructor() {
67
+ effect(() => {
68
+ const id = this.boardId();
69
+ untracked(() => this.state.openBoard(id));
70
+ });
71
+ this.destroyRef.onDestroy(() => this.state.closeBoard());
72
+ }
73
+ openAddTask(list) {
74
+ this.editingTask.set(null);
75
+ this.activeListId.set(list.id);
76
+ this.showTaskForm.set(true);
77
+ }
78
+ onTaskSaved(task) {
79
+ this.state.addTaskOptimistic(task);
80
+ }
81
+ onDeleteTask(task) {
82
+ this.confirmationService.confirm({
83
+ message: this.translate("shared.confirm.delete.item", { name: task.title }),
84
+ header: this.translate("task.detail.delete"),
85
+ icon: "exclamation-triangle",
86
+ acceptButtonStyleClass: "p-button-danger",
87
+ accept: async () => {
88
+ try {
89
+ await this.state.deleteTask(task.id);
90
+ this.messageService.add({
91
+ severity: "success",
92
+ summary: this.translate("shared.success"),
93
+ detail: this.translate("task.delete.success")
94
+ });
95
+ } catch {}
96
+ }
97
+ });
98
+ }
99
+ onEditTask(task) {
100
+ this.editingTask.set(task);
101
+ this.activeListId.set(task.listId);
102
+ this.showTaskForm.set(true);
103
+ this.state.selectTask(null);
104
+ }
105
+ getMemberName(assigneeId) {
106
+ return this.state.members().find((m) => m.userId === assigneeId)?.userName ?? null;
107
+ }
108
+ getMemberInitials(assigneeId) {
109
+ return memberInitials(this.getMemberName(assigneeId));
110
+ }
111
+ memberInitials(member) {
112
+ return memberInitials(member.userName);
113
+ }
114
+ memberColor(member) {
115
+ return memberColorFromId(member.userId);
116
+ }
117
+ memberColorById(userId) {
118
+ return memberColorFromId(userId);
119
+ }
120
+ roleBadgeClass(role) {
121
+ const map = {
122
+ OWNER: "text-primary-600 dark:text-primary-400 font-semibold",
123
+ ADMIN: "text-orange-600 dark:text-orange-400 font-semibold",
124
+ MEMBER: "text-surface-500 dark:text-surface-400",
125
+ VIEWER: "text-surface-400 dark:text-surface-500"
126
+ };
127
+ return map[role] ?? map["MEMBER"];
128
+ }
129
+ prioritySeverity = taskPrioritySeverity;
130
+ isOverdue = isTaskOverdue;
131
+ async onTaskDrop(event) {
132
+ if (event.previousContainer === event.container) moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
133
+ else {
134
+ transferArrayItem(event.previousContainer.data, event.container.data, event.previousIndex, event.currentIndex);
135
+ const task = event.container.data[event.currentIndex];
136
+ await this.taskApi.moveTask({
137
+ taskId: task.id,
138
+ targetListId: event.container.id,
139
+ position: event.currentIndex * 100
140
+ });
141
+ }
142
+ }
143
+ async onListDrop(event) {
144
+ if (event.previousIndex === event.currentIndex) return;
145
+ const lists = this.state.listsWithTasks().slice();
146
+ moveItemInArray(lists, event.previousIndex, event.currentIndex);
147
+ const orderedIds = lists.map((l) => l.id);
148
+ await this.state.reorderLists(this.boardId(), orderedIds);
149
+ }
150
+ startRenameList(list) {
151
+ this.editingListId.set(list.id);
152
+ this.editingListNameValue.set(list.name);
153
+ }
154
+ cancelRenameList() {
155
+ this.editingListId.set(null);
156
+ this.editingListNameValue.set("");
157
+ }
158
+ async saveRenameList() {
159
+ const id = this.editingListId();
160
+ const name = this.editingListNameValue().trim();
161
+ this.cancelRenameList();
162
+ if (!id || !name) return;
163
+ await this.state.renameList(id, name);
164
+ this.messageService.add({
165
+ severity: "success",
166
+ summary: this.translate("shared.success"),
167
+ detail: this.translate("task.list.update.success")
168
+ });
169
+ }
170
+ openEditBoardDialog(board) {
171
+ this.editBoardName = board.name;
172
+ this.editBoardDescription = board.description ?? "";
173
+ this.editBoardColor = board.color;
174
+ this.showEditBoardDialog.set(true);
175
+ }
176
+ async saveBoard() {
177
+ const board = this.state.activeBoard();
178
+ if (!board || !this.editBoardName.trim()) return;
179
+ this.isSavingBoard.set(true);
180
+ try {
181
+ await this.state.updateBoard({
182
+ id: board.id,
183
+ name: this.editBoardName.trim(),
184
+ description: this.editBoardDescription.trim() || void 0,
185
+ color: this.editBoardColor
186
+ });
187
+ this.showEditBoardDialog.set(false);
188
+ this.messageService.add({
189
+ severity: "success",
190
+ summary: this.translate("shared.success"),
191
+ detail: this.translate("task.board.update.success")
192
+ });
193
+ } finally {
194
+ this.isSavingBoard.set(false);
195
+ }
196
+ }
197
+ async addLabel() {
198
+ if (!this.newLabelName.trim()) return;
199
+ this.isAddingLabel.set(true);
200
+ try {
201
+ await this.state.addLabel(this.boardId(), this.newLabelName.trim(), this.newLabelColor);
202
+ this.newLabelName = "";
203
+ this.newLabelColor = BOARD_COLOR_PALETTE[0];
204
+ } finally {
205
+ this.isAddingLabel.set(false);
206
+ }
207
+ }
208
+ async removeLabel(labelId) {
209
+ await this.state.deleteLabel(labelId);
210
+ }
211
+ async addMember() {
212
+ if (!this.selectedMemberUser) return;
213
+ this.isAddingMember.set(true);
214
+ try {
215
+ await this.state.addMember(this.selectedMemberUser, this.newMemberRole);
216
+ this.resetMemberForm();
217
+ } finally {
218
+ this.isAddingMember.set(false);
219
+ }
220
+ }
221
+ resetMemberForm() {
222
+ this.selectedMemberUser = null;
223
+ this.newMemberRole = "MEMBER";
224
+ }
225
+ async removeMember(userId) {
226
+ await this.state.removeMember(userId);
227
+ }
228
+ async addList() {
229
+ if (!this.newListName.trim()) return;
230
+ this.isAddingList.set(true);
231
+ try {
232
+ const position = this.state.lists().length * 100;
233
+ await this.listApi.insert({
234
+ boardId: this.boardId(),
235
+ name: this.newListName,
236
+ position
237
+ });
238
+ await this.state.openBoard(this.boardId());
239
+ this.showAddListDialog.set(false);
240
+ this.newListName = "";
241
+ } finally {
242
+ this.isAddingList.set(false);
243
+ }
244
+ }
245
+ static ɵfac = i0.ɵɵngDeclareFactory({
246
+ minVersion: "12.0.0",
247
+ version: "22.2.1",
248
+ ngImport: i0,
249
+ type: BoardViewPageComponent,
250
+ deps: [],
251
+ target: i0.ɵɵFactoryTarget.Component
252
+ });
253
+ static ɵcmp = i0.ɵɵngDeclareComponent({
254
+ minVersion: "17.0.0",
255
+ version: "22.2.1",
256
+ type: BoardViewPageComponent,
257
+ isStandalone: true,
258
+ selector: "tm-board-view-page",
259
+ inputs: { boardId: {
260
+ classPropertyName: "boardId",
261
+ publicName: "boardId",
262
+ isSignal: true,
263
+ isRequired: true,
264
+ transformFunction: null
265
+ } },
266
+ ngImport: i0,
267
+ template: `
268
+ <div class="flex flex-col h-full">
269
+ <div class="flex flex-col">
270
+ <div class="flex items-center justify-between px-5 py-2.5 gap-4">
271
+ @if (state.activeBoard(); as board) {
272
+ <div class="flex items-center gap-2.5 min-w-0 flex-1 group">
273
+ <div class="flex flex-col min-w-0">
274
+ <div class="flex items-center gap-1.5 min-w-0">
275
+ <h1
276
+ class="text-base font-bold text-surface-900 dark:text-surface-0 truncate leading-snug"
277
+ >
278
+ {{ board.name }}
279
+ </h1>
280
+ <button
281
+ fButton
282
+ [text]="true"
283
+ size="small"
284
+ severity="secondary"
285
+ class="!w-6 !h-6 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
286
+ [fTooltip]="'task.manager.board.edit' | translate"
287
+ tooltipPosition="bottom"
288
+ (click)="openEditBoardDialog(board)"
289
+ >
290
+ <ui-icon pButtonIcon name="pencil"></ui-icon>
291
+ </button>
292
+ </div>
293
+ @if (board.description) {
294
+ <span
295
+ class="text-xs text-muted-color truncate max-w-sm hidden md:block leading-snug"
296
+ >
297
+ {{ board.description }}
298
+ </span>
299
+ }
300
+ </div>
301
+ </div>
302
+ }
303
+
304
+ <div class="flex items-center gap-1.5 flex-shrink-0">
305
+ @if (state.members().length > 0) {
306
+ <div
307
+ class="flex items-center -space-x-2 cursor-pointer mr-1"
308
+ (click)="showMembersDialog.set(true)"
309
+ [fTooltip]="'task.manager.board.manage.members' | translate"
310
+ tooltipPosition="bottom"
311
+ >
312
+ @for (member of state.members().slice(0, 4); track member.id) {
313
+ <f-avatar
314
+ shape="circle"
315
+ size="small"
316
+ [label]="memberInitials(member)"
317
+ [color]="memberColor(member)"
318
+ styleClass="!border-2 !border-surface-card ring-1 ring-surface flex-shrink-0"
319
+ />
320
+ }
321
+ @if (state.members().length > 4) {
322
+ <f-avatar
323
+ shape="circle"
324
+ size="small"
325
+ [label]="'+' + (state.members().length - 4)"
326
+ styleClass="!border-2 !border-surface-card !bg-surface-200 dark:!bg-surface-700 !text-surface-600 dark:!text-surface-300 flex-shrink-0"
327
+ />
328
+ }
329
+ </div>
330
+ }
331
+
332
+ <div
333
+ class="h-5 w-px bg-surface-300 dark:bg-surface-600 mx-0.5"
334
+ ></div>
335
+
336
+ <div
337
+ class="flex rounded-border overflow-hidden border border-surface"
338
+ >
339
+ <button
340
+ fButton
341
+ [fTooltip]="'task.manager.view.board' | translate"
342
+ tooltipPosition="bottom"
343
+ size="small"
344
+ [severity]="viewMode() === 'board' ? 'primary' : 'secondary'"
345
+ [outlined]="viewMode() !== 'board'"
346
+ class="!rounded-none !border-0"
347
+ (click)="viewMode.set('board')"
348
+ >
349
+ <ui-icon pButtonIcon name="th-large"></ui-icon>
350
+ </button>
351
+ <button
352
+ fButton
353
+ [fTooltip]="'task.manager.view.list' | translate"
354
+ tooltipPosition="bottom"
355
+ size="small"
356
+ [severity]="viewMode() === 'list' ? 'primary' : 'secondary'"
357
+ [outlined]="viewMode() !== 'list'"
358
+ class="!rounded-none !border-0"
359
+ (click)="viewMode.set('list')"
360
+ >
361
+ <ui-icon pButtonIcon name="list"></ui-icon>
362
+ </button>
363
+ </div>
364
+
365
+ <div
366
+ class="h-5 w-px bg-surface-300 dark:bg-surface-600 mx-0.5"
367
+ ></div>
368
+
369
+ <button
370
+ fButton
371
+ [text]="true"
372
+ severity="secondary"
373
+ size="small"
374
+ class="!w-8 !h-8"
375
+ [fTooltip]="'task.manager.board.manage.members' | translate"
376
+ tooltipPosition="bottom"
377
+ (click)="showMembersDialog.set(true)"
378
+ >
379
+ <ui-icon pButtonIcon name="users"></ui-icon>
380
+ </button>
381
+ <button
382
+ fButton
383
+ [text]="true"
384
+ severity="secondary"
385
+ size="small"
386
+ class="!w-8 !h-8"
387
+ [fTooltip]="'task.manager.board.manage.labels' | translate"
388
+ tooltipPosition="bottom"
389
+ (click)="showLabelsDialog.set(true)"
390
+ >
391
+ <ui-icon pButtonIcon name="tag"></ui-icon>
392
+ </button>
393
+
394
+ @if (viewMode() === 'board') {
395
+ <div
396
+ class="h-5 w-px bg-surface-300 dark:bg-surface-600 mx-0.5"
397
+ ></div>
398
+ <button
399
+ fButton
400
+ size="small"
401
+ (click)="showAddListDialog.set(true)"
402
+ >
403
+ <ui-icon pButtonIcon name="plus"></ui-icon>
404
+ <span pButtonLabel>{{
405
+ 'task.manager.board.add.list' | translate
406
+ }}</span>
407
+ </button>
408
+ }
409
+ </div>
410
+ </div>
411
+
412
+ @if (state.activeBoard(); as board) {
413
+ <div
414
+ class="h-0.5 w-full flex-shrink-0"
415
+ [style.background-color]="board.color"
416
+ ></div>
417
+ }
418
+ </div>
419
+
420
+ @if (state.isBoardLoading()) {
421
+ @if (viewMode() === 'board') {
422
+ <div class="flex gap-4 p-6 overflow-x-auto">
423
+ @for (i of [1, 2, 3]; track i) {
424
+ <div class="flex-shrink-0 w-72">
425
+ <f-skeleton height="36px" class="mb-3" />
426
+ @for (j of [1, 2, 3]; track j) {
427
+ <f-skeleton height="90px" class="mb-2" />
428
+ }
429
+ </div>
430
+ }
431
+ </div>
432
+ } @else {
433
+ <div class="p-6">
434
+ @for (i of [1, 2, 3, 4, 5]; track i) {
435
+ <f-skeleton height="48px" class="mb-2" />
436
+ }
437
+ </div>
438
+ }
439
+ } @else if (viewMode() === 'board') {
440
+ <div
441
+ class="flex gap-4 p-5 overflow-x-auto flex-1"
442
+ cdkDropList
443
+ cdkDropListOrientation="horizontal"
444
+ [cdkDropListData]="state.listsWithTasks()"
445
+ (cdkDropListDropped)="onListDrop($event)"
446
+ >
447
+ @for (list of state.listsWithTasks(); track list.id) {
448
+ <div
449
+ cdkDrag
450
+ 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"
451
+ >
452
+ <div
453
+ class="flex items-center gap-1 px-2.5 py-2.5 border-b border-surface"
454
+ >
455
+ <ui-icon
456
+ cdkDragHandle
457
+ name="bars"
458
+ class="text-muted-color text-xs cursor-grab flex-shrink-0 px-1"
459
+ ></ui-icon>
460
+
461
+ <div class="flex items-center gap-2 min-w-0 flex-1">
462
+ @if (editingListId() === list.id) {
463
+ <input
464
+ fInputText
465
+ class="text-sm font-semibold !h-6 !py-0 !px-1 w-full"
466
+ [ngModel]="editingListNameValue()"
467
+ (ngModelChange)="editingListNameValue.set($event)"
468
+ (keydown.enter)="saveRenameList()"
469
+ (keydown.escape)="cancelRenameList()"
470
+ (blur)="cancelRenameList()"
471
+ (click)="$event.stopPropagation()"
472
+ autofocus
473
+ />
474
+ } @else {
475
+ <span class="font-semibold text-sm truncate">{{
476
+ list.name
477
+ }}</span>
478
+ <span
479
+ class="text-[11px] font-semibold px-1.5 py-0.5 rounded-full text-white flex-shrink-0"
480
+ [style.background-color]="list.color"
481
+ >
482
+ {{ list.tasks.length }}
483
+ </span>
484
+ }
485
+ </div>
486
+
487
+ @if (editingListId() === list.id) {
488
+ <button
489
+ fButton
490
+ [text]="true"
491
+ size="small"
492
+ severity="success"
493
+ class="!w-6 !h-6 flex-shrink-0"
494
+ (mousedown)="$event.preventDefault()"
495
+ (click)="saveRenameList(); $event.stopPropagation()"
496
+ >
497
+ <ui-icon pButtonIcon name="check"></ui-icon>
498
+ </button>
499
+ <button
500
+ fButton
501
+ [text]="true"
502
+ size="small"
503
+ severity="secondary"
504
+ class="!w-6 !h-6 flex-shrink-0"
505
+ (mousedown)="$event.preventDefault()"
506
+ (click)="cancelRenameList(); $event.stopPropagation()"
507
+ >
508
+ <ui-icon pButtonIcon name="times"></ui-icon>
509
+ </button>
510
+ } @else {
511
+ <button
512
+ fButton
513
+ [text]="true"
514
+ size="small"
515
+ severity="secondary"
516
+ class="!w-6 !h-6 flex-shrink-0"
517
+ [fTooltip]="'task.manager.list.rename' | translate"
518
+ tooltipPosition="bottom"
519
+ (click)="startRenameList(list); $event.stopPropagation()"
520
+ >
521
+ <ui-icon pButtonIcon name="pencil"></ui-icon>
522
+ </button>
523
+ }
524
+ </div>
525
+
526
+ <div
527
+ cdkDropList
528
+ [id]="list.id"
529
+ [cdkDropListData]="list.tasks"
530
+ [cdkDropListConnectedTo]="listDropIds()"
531
+ (cdkDropListDropped)="onTaskDrop($event)"
532
+ class="flex flex-col gap-2 p-2 overflow-y-auto flex-1 min-h-[60px]"
533
+ >
534
+ @for (task of list.tasks; track task.id) {
535
+ <div cdkDrag>
536
+ <tm-task-card
537
+ [task]="task"
538
+ (cardClick)="state.selectTask($event)"
539
+ (editClick)="onEditTask($event)"
540
+ (deleteClick)="onDeleteTask($event)"
541
+ />
542
+ </div>
543
+ }
544
+ @if (list.tasks.length === 0) {
545
+ <div
546
+ class="flex items-center justify-center h-14 border-2 border-dashed border-surface rounded-border"
547
+ >
548
+ <span class="text-xs text-muted-color">{{
549
+ 'task.manager.board.drop.tasks.here' | translate
550
+ }}</span>
551
+ </div>
552
+ }
553
+ </div>
554
+
555
+ <div class="border-t border-surface p-1.5">
556
+ <button
557
+ fButton
558
+ severity="secondary"
559
+ [text]="true"
560
+ size="small"
561
+ class="w-full !justify-start"
562
+ (click)="openAddTask(list)"
563
+ >
564
+ <ui-icon pButtonIcon name="plus"></ui-icon>
565
+ <span pButtonLabel>{{
566
+ 'task.manager.board.add.task' | translate
567
+ }}</span>
568
+ </button>
569
+ </div>
570
+ </div>
571
+ }
572
+
573
+ @if (state.listsWithTasks().length === 0) {
574
+ <div class="flex-1 flex items-center justify-center">
575
+ <div class="text-center text-muted-color">
576
+ <div
577
+ class="w-16 h-16 rounded-2xl border-2 border-dashed border-surface flex items-center justify-center mx-auto mb-4"
578
+ >
579
+ <ui-icon
580
+ name="list"
581
+ class="text-3xl text-muted-color"
582
+ ></ui-icon>
583
+ </div>
584
+ <p class="font-medium text-muted-color">
585
+ {{ 'task.manager.board.list.empty' | translate }}
586
+ </p>
587
+ <button
588
+ fButton
589
+ severity="secondary"
590
+ size="small"
591
+ class="mt-3"
592
+ (click)="showAddListDialog.set(true)"
593
+ >
594
+ <ui-icon pButtonIcon name="plus"></ui-icon>
595
+ <span pButtonLabel>{{
596
+ 'task.manager.board.add.list' | translate
597
+ }}</span>
598
+ </button>
599
+ </div>
600
+ </div>
601
+ }
602
+ </div>
603
+ } @else {
604
+ <div class="p-4 sm:p-6 flex-1 overflow-auto">
605
+ <div class="card">
606
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
607
+ <f-table
608
+ [value]="allTasksFlat()"
609
+ [paginator]="allTasksFlat().length > 20"
610
+ [rows]="20"
611
+ [rowsPerPageOptions]="[10, 20, 50]"
612
+ [scrollable]="true"
613
+ [tableStyle]="{ 'min-width': '700px' }"
614
+ >
615
+ <ng-template #header>
616
+ <tr>
617
+ <th fSortableColumn="title" class="min-w-[240px]">
618
+ {{ 'task.manager.table.title' | translate }}
619
+ <f-sorticon field="title" />
620
+ </th>
621
+ <th fSortableColumn="listName" class="w-40">
622
+ {{ 'task.manager.table.list' | translate }}
623
+ <f-sorticon field="listName" />
624
+ </th>
625
+ <th fSortableColumn="priority" class="w-32">
626
+ {{ 'task.manager.table.priority' | translate }}
627
+ <f-sorticon field="priority" />
628
+ </th>
629
+ <th class="w-36">
630
+ {{ 'task.manager.table.assignee' | translate }}
631
+ </th>
632
+ <th fSortableColumn="dueDate" class="w-32">
633
+ {{ 'task.manager.table.due.date' | translate }}
634
+ <f-sorticon field="dueDate" />
635
+ </th>
636
+ <th class="w-[100px]">
637
+ {{ 'shared.actions' | translate }}
638
+ </th>
639
+ </tr>
640
+ </ng-template>
641
+ <ng-template #body let-task>
642
+ <tr>
643
+ <td>
644
+ <span
645
+ class="font-medium text-sm cursor-pointer hover:text-primary-500 transition-colors"
646
+ (click)="state.selectTask(task)"
647
+ >{{ task.title }}</span
648
+ >
649
+ </td>
650
+ <td>
651
+ <span class="inline-flex items-center gap-1.5 text-sm">
652
+ <span
653
+ class="w-2 h-2 rounded-full flex-shrink-0"
654
+ [style.background-color]="task.listColor"
655
+ ></span>
656
+ {{ task.listName }}
657
+ </span>
658
+ </td>
659
+ <td>
660
+ <f-tag
661
+ [value]="task.priority"
662
+ [severity]="prioritySeverity(task.priority)"
663
+ />
664
+ </td>
665
+ <td>
666
+ @if (task.assignees?.length) {
667
+ <div class="flex items-center -space-x-1.5">
668
+ @for (
669
+ a of task.assignees!.slice(0, 3);
670
+ track a.userId
671
+ ) {
672
+ <f-avatar
673
+ shape="circle"
674
+ size="small"
675
+ [label]="getMemberInitials(a.userId)"
676
+ [color]="memberColorById(a.userId)"
677
+ styleClass="!ring-surface-card flex-shrink-0"
678
+ [fTooltip]="a.userName ?? ''"
679
+ />
680
+ }
681
+ @if (task.assignees!.length > 3) {
682
+ <f-avatar
683
+ shape="circle"
684
+ size="small"
685
+ [label]="'+' + (task.assignees!.length - 3)"
686
+ styleClass="!bg-surface-200 dark:!bg-surface-700 !text-surface-600 !ring-surface-card flex-shrink-0"
687
+ />
688
+ }
689
+ </div>
690
+ } @else {
691
+ <span class="text-muted-color text-sm">—</span>
692
+ }
693
+ </td>
694
+ <td>
695
+ @if (task.dueDate) {
696
+ <span
697
+ class="text-sm"
698
+ [class.text-red-500]="isOverdue(task.dueDate)"
699
+ >
700
+ {{ task.dueDate | date: 'MMM d, y' }}
701
+ </span>
702
+ } @else {
703
+ <span class="text-muted-color text-sm">—</span>
704
+ }
705
+ </td>
706
+ <td>
707
+ <div class="flex gap-1">
708
+ <button
709
+ fButton
710
+ [text]="true"
711
+ severity="secondary"
712
+ size="small"
713
+ [fTooltip]="'shared.edit' | translate"
714
+ (click)="onEditTask(task)"
715
+ >
716
+ <ui-icon pButtonIcon name="pencil"></ui-icon>
717
+ </button>
718
+ <button
719
+ fButton
720
+ [text]="true"
721
+ severity="danger"
722
+ size="small"
723
+ [fTooltip]="'shared.delete' | translate"
724
+ (click)="onDeleteTask(task)"
725
+ >
726
+ <ui-icon pButtonIcon name="trash"></ui-icon>
727
+ </button>
728
+ </div>
729
+ </td>
730
+ </tr>
731
+ </ng-template>
732
+ <ng-template #emptymessage>
733
+ <tr>
734
+ <td colspan="6" class="text-center text-muted-color py-8">
735
+ <ui-icon
736
+ name="inbox"
737
+ class="text-4xl block mb-3"
738
+ ></ui-icon>
739
+ {{ 'task.manager.table.empty' | translate }}
740
+ </td>
741
+ </tr>
742
+ </ng-template>
743
+ </f-table>
744
+ </div>
745
+ </div>
746
+ </div>
747
+ }
748
+ </div>
749
+
750
+ <tm-task-form-dialog
751
+ [(visible)]="showTaskForm"
752
+ [task]="editingTask()"
753
+ [boardId]="boardId()"
754
+ [listId]="activeListId()"
755
+ [members]="state.members()"
756
+ [labels]="state.labels()"
757
+ (saved)="onTaskSaved($event)"
758
+ />
759
+
760
+ <tm-task-detail-panel
761
+ [task]="state.selectedTask()"
762
+ (closePanel)="state.selectTask(null)"
763
+ (editTask)="onEditTask($event)"
764
+ />
765
+
766
+ <f-dialog
767
+ [header]="'task.manager.label.manage.title' | translate"
768
+ [(visible)]="showLabelsDialog"
769
+ [modal]="true"
770
+ [style]="{ width: '420px' }"
771
+ (onHide)="newLabelName = ''; newLabelColor = boardColorPalette[0]"
772
+ >
773
+ <div class="flex flex-col gap-4 py-2">
774
+ <div class="flex gap-2 items-end">
775
+ <div class="flex flex-col gap-1 flex-1">
776
+ <label class="text-sm font-medium">{{
777
+ 'task.manager.label.name.label' | translate
778
+ }}</label>
779
+ <input
780
+ fInputText
781
+ [(ngModel)]="newLabelName"
782
+ [placeholder]="'task.manager.label.name.placeholder' | translate"
783
+ />
784
+ </div>
785
+ <div class="flex flex-col gap-1">
786
+ <label class="text-sm font-medium">{{
787
+ 'task.manager.label.color.label' | translate
788
+ }}</label>
789
+ <input
790
+ type="color"
791
+ [(ngModel)]="newLabelColor"
792
+ class="w-10 h-10 rounded cursor-pointer border border-surface"
793
+ />
794
+ </div>
795
+ <button
796
+ fButton
797
+ size="small"
798
+ (click)="addLabel()"
799
+ [disabled]="isAddingLabel()"
800
+ >
801
+ @if (isAddingLabel()) {
802
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
803
+ } @else {
804
+ <ui-icon pButtonIcon name="plus"></ui-icon>
805
+ }
806
+ <span pButtonLabel>{{ 'task.manager.label.add' | translate }}</span>
807
+ </button>
808
+ </div>
809
+
810
+ <div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
811
+ @if (state.labels().length === 0) {
812
+ <p class="text-sm text-muted-color text-center py-4">
813
+ {{ 'task.manager.label.no.labels' | translate }}
814
+ </p>
815
+ }
816
+ @for (label of state.labels(); track label.id) {
817
+ <div
818
+ class="flex items-center justify-between px-3 py-2 rounded-border border border-surface"
819
+ >
820
+ <div class="flex items-center gap-2">
821
+ <span
822
+ class="w-4 h-4 rounded-full flex-shrink-0"
823
+ [style.background-color]="label.color"
824
+ ></span>
825
+ <span class="text-sm font-medium">{{ label.name }}</span>
826
+ </div>
827
+ <button
828
+ fButton
829
+ severity="danger"
830
+ size="small"
831
+ [text]="true"
832
+ (click)="removeLabel(label.id)"
833
+ >
834
+ <ui-icon pButtonIcon name="trash"></ui-icon>
835
+ </button>
836
+ </div>
837
+ }
838
+ </div>
839
+ </div>
840
+ </f-dialog>
841
+
842
+ <f-dialog
843
+ [header]="'task.manager.member.manage.title' | translate"
844
+ [(visible)]="showMembersDialog"
845
+ [modal]="true"
846
+ [style]="{ width: '480px' }"
847
+ (onHide)="resetMemberForm()"
848
+ >
849
+ <div class="flex flex-col gap-4 py-2">
850
+ <div class="flex flex-col gap-3">
851
+ <div class="flex flex-col gap-1">
852
+ <label class="text-sm font-medium">{{
853
+ 'task.manager.member.user.label' | translate
854
+ }}</label>
855
+ <lib-user-select
856
+ [(value)]="selectedMemberUser"
857
+ [isEditMode]="true"
858
+ [placeHolder]="'task.manager.member.user.placeholder' | translate"
859
+ />
860
+ </div>
861
+
862
+ <div class="flex gap-2 items-end">
863
+ <div class="flex flex-col gap-1 flex-1">
864
+ <label class="text-sm font-medium">{{
865
+ 'task.manager.member.role.label' | translate
866
+ }}</label>
867
+ <f-select
868
+ [(ngModel)]="newMemberRole"
869
+ [options]="memberRoleOptions"
870
+ optionValue="value"
871
+ [placeholder]="
872
+ 'task.manager.member.role.placeholder' | translate
873
+ "
874
+ class="w-full"
875
+ >
876
+ <ng-template #selectedItem let-opt>
877
+ {{ opt.labelKey | translate }}
878
+ </ng-template>
879
+ <ng-template #item let-opt>
880
+ {{ opt.labelKey | translate }}
881
+ </ng-template>
882
+ </f-select>
883
+ </div>
884
+ <button
885
+ fButton
886
+ size="small"
887
+ [disabled]="!selectedMemberUser || isAddingMember()"
888
+ (click)="addMember()"
889
+ >
890
+ @if (isAddingMember()) {
891
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
892
+ } @else {
893
+ <ui-icon pButtonIcon name="plus"></ui-icon>
894
+ }
895
+ <span pButtonLabel>{{
896
+ 'task.manager.member.add' | translate
897
+ }}</span>
898
+ </button>
899
+ </div>
900
+ </div>
901
+
902
+ <div class="flex flex-col gap-2 max-h-72 overflow-y-auto">
903
+ @if (state.members().length === 0) {
904
+ <p class="text-sm text-muted-color text-center py-4">
905
+ {{ 'task.manager.member.no.members' | translate }}
906
+ </p>
907
+ }
908
+ @for (member of state.members(); track member.id) {
909
+ <div
910
+ class="flex items-center justify-between px-3 py-2.5 rounded-border border border-surface"
911
+ >
912
+ <div class="flex items-center gap-3">
913
+ <f-avatar
914
+ shape="circle"
915
+ [label]="memberInitials(member)"
916
+ [color]="memberColor(member)"
917
+ styleClass="!text-[11px] flex-shrink-0"
918
+ />
919
+ <div>
920
+ <div class="text-sm font-medium ">
921
+ {{ member.userName ?? member.userId }}
922
+ </div>
923
+ @if (member.userEmail) {
924
+ <div class="text-xs text-muted-color">
925
+ {{ member.userEmail }}
926
+ </div>
927
+ }
928
+ </div>
929
+ </div>
930
+ <div class="flex items-center gap-2">
931
+ <span
932
+ class="text-[10px] font-semibold px-2 py-0.5 rounded-full"
933
+ [class]="roleBadgeClass(member.role)"
934
+ >
935
+ {{
936
+ 'task.manager.member.role.' + member.role.toLowerCase()
937
+ | translate
938
+ }}
939
+ </span>
940
+ @if (member.role !== 'OWNER') {
941
+ <button
942
+ fButton
943
+ severity="danger"
944
+ size="small"
945
+ [text]="true"
946
+ (click)="removeMember(member.userId)"
947
+ >
948
+ <ui-icon pButtonIcon name="trash"></ui-icon>
949
+ </button>
950
+ }
951
+ </div>
952
+ </div>
953
+ }
954
+ </div>
955
+ </div>
956
+ </f-dialog>
957
+
958
+ <f-dialog
959
+ [header]="'task.manager.list.new' | translate"
960
+ [(visible)]="showAddListDialog"
961
+ [modal]="true"
962
+ [style]="{ width: '360px' }"
963
+ (onHide)="newListName = ''"
964
+ >
965
+ <div class="flex flex-col gap-2 py-2">
966
+ <label class="text-sm font-medium">
967
+ {{ 'task.manager.list.name.label' | translate }}
968
+ <span class="text-red-500">*</span>
969
+ </label>
970
+ <input
971
+ fInputText
972
+ [(ngModel)]="newListName"
973
+ [placeholder]="'task.manager.list.name.placeholder' | translate"
974
+ />
975
+ </div>
976
+ <ng-template #footer>
977
+ <div class="flex justify-end gap-2">
978
+ <button
979
+ fButton
980
+ severity="secondary"
981
+ (click)="showAddListDialog.set(false)"
982
+ >
983
+ <span pButtonLabel>{{ 'shared.cancel' | translate }}</span>
984
+ </button>
985
+ <button fButton (click)="addList()" [disabled]="isAddingList()">
986
+ @if (isAddingList()) {
987
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
988
+ }
989
+ <span pButtonLabel>{{ 'shared.create' | translate }}</span>
990
+ </button>
991
+ </div>
992
+ </ng-template>
993
+ </f-dialog>
994
+
995
+ <f-dialog
996
+ [header]="'task.manager.board.edit' | translate"
997
+ [(visible)]="showEditBoardDialog"
998
+ [modal]="true"
999
+ [style]="{ width: '420px' }"
1000
+ >
1001
+ <div class="flex flex-col gap-4 py-2">
1002
+ <div class="flex flex-col gap-1">
1003
+ <label class="text-sm font-medium">
1004
+ {{ 'task.manager.board.name.label' | translate }}
1005
+ <span class="text-red-500">*</span>
1006
+ </label>
1007
+ <input fInputText [(ngModel)]="editBoardName" />
1008
+ </div>
1009
+ <div class="flex flex-col gap-1">
1010
+ <label class="text-sm font-medium">{{
1011
+ 'task.manager.board.description.label' | translate
1012
+ }}</label>
1013
+ <textarea
1014
+ fTextarea
1015
+ [(ngModel)]="editBoardDescription"
1016
+ [rows]="3"
1017
+ [placeholder]="
1018
+ 'task.manager.board.description.placeholder' | translate
1019
+ "
1020
+ ></textarea>
1021
+ </div>
1022
+ <div class="flex flex-col gap-1">
1023
+ <label class="text-sm font-medium">{{
1024
+ 'task.manager.board.color.label' | translate
1025
+ }}</label>
1026
+ <div class="flex items-center gap-3">
1027
+ <input
1028
+ type="color"
1029
+ [(ngModel)]="editBoardColor"
1030
+ class="w-10 h-10 rounded cursor-pointer border border-surface"
1031
+ />
1032
+ <div class="flex gap-2 flex-wrap">
1033
+ @for (color of boardColorPalette; track color) {
1034
+ <button
1035
+ type="button"
1036
+ class="w-6 h-6 rounded-full border-2 transition-transform hover:scale-110"
1037
+ [style.background-color]="color"
1038
+ [class.border-surface-900]="editBoardColor === color"
1039
+ [class.dark:border-surface-0]="editBoardColor === color"
1040
+ [class.border-transparent]="editBoardColor !== color"
1041
+ (click)="editBoardColor = color"
1042
+ ></button>
1043
+ }
1044
+ </div>
1045
+ </div>
1046
+ </div>
1047
+ </div>
1048
+ <ng-template #footer>
1049
+ <div class="flex justify-end gap-2">
1050
+ <button
1051
+ fButton
1052
+ severity="secondary"
1053
+ (click)="showEditBoardDialog.set(false)"
1054
+ >
1055
+ <span pButtonLabel>{{ 'shared.cancel' | translate }}</span>
1056
+ </button>
1057
+ <button
1058
+ fButton
1059
+ [disabled]="!editBoardName.trim() || isSavingBoard()"
1060
+ (click)="saveBoard()"
1061
+ >
1062
+ @if (isSavingBoard()) {
1063
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
1064
+ }
1065
+ <span pButtonLabel>{{ 'shared.save' | translate }}</span>
1066
+ </button>
1067
+ </div>
1068
+ </ng-template>
1069
+ </f-dialog>
1070
+ `,
1071
+ isInline: true,
1072
+ dependencies: [
1073
+ {
1074
+ kind: "component",
1075
+ type: AvatarComponent,
1076
+ selector: "f-avatar",
1077
+ inputs: [
1078
+ "label",
1079
+ "icon",
1080
+ "image",
1081
+ "shape",
1082
+ "size",
1083
+ "color",
1084
+ "styleClass",
1085
+ "ariaLabel",
1086
+ "ariaLabelledBy"
1087
+ ],
1088
+ outputs: ["onImageError"]
1089
+ },
1090
+ {
1091
+ kind: "component",
1092
+ type: DialogComponent,
1093
+ selector: "f-dialog",
1094
+ inputs: [
1095
+ "header",
1096
+ "visible",
1097
+ "modal",
1098
+ "closable",
1099
+ "closeOnEscape",
1100
+ "dismissableMask",
1101
+ "resizable",
1102
+ "draggable",
1103
+ "maximizable",
1104
+ "blockScroll",
1105
+ "returnFocusOnClose",
1106
+ "position",
1107
+ "style",
1108
+ "styleClass",
1109
+ "breakpoints"
1110
+ ],
1111
+ outputs: [
1112
+ "visibleChange",
1113
+ "onShow",
1114
+ "onHide",
1115
+ "onMaximize"
1116
+ ]
1117
+ },
1118
+ {
1119
+ kind: "directive",
1120
+ type: FButtonDirective,
1121
+ selector: "[fButton]",
1122
+ inputs: [
1123
+ "severity",
1124
+ "text",
1125
+ "outlined",
1126
+ "link",
1127
+ "variant",
1128
+ "rounded",
1129
+ "raised",
1130
+ "fluid",
1131
+ "disabled",
1132
+ "loading",
1133
+ "loadingIcon",
1134
+ "size",
1135
+ "label",
1136
+ "icon",
1137
+ "iconPos",
1138
+ "ariaLabel",
1139
+ "plain"
1140
+ ]
1141
+ },
1142
+ {
1143
+ kind: "directive",
1144
+ type: InputTextDirective,
1145
+ selector: "[fInputText]",
1146
+ inputs: [
1147
+ "invalid",
1148
+ "touched",
1149
+ "variant",
1150
+ "size",
1151
+ "fluid"
1152
+ ]
1153
+ },
1154
+ {
1155
+ kind: "component",
1156
+ type: SelectComponent,
1157
+ selector: "f-select",
1158
+ inputs: [
1159
+ "options",
1160
+ "optionLabel",
1161
+ "optionValue",
1162
+ "placeholder",
1163
+ "disabled",
1164
+ "filter",
1165
+ "filterBy",
1166
+ "filterMatchMode",
1167
+ "filterLocale",
1168
+ "filterPlaceholder",
1169
+ "resetFilterOnHide",
1170
+ "autofocusFilter",
1171
+ "emptyMessage",
1172
+ "emptyFilterMessage",
1173
+ "showClear",
1174
+ "loading",
1175
+ "loadingIcon",
1176
+ "dropdownIcon",
1177
+ "clearIcon",
1178
+ "inputId",
1179
+ "invalid",
1180
+ "touched",
1181
+ "styleClass",
1182
+ "style",
1183
+ "appendTo",
1184
+ "editable",
1185
+ "readonly",
1186
+ "autofocus",
1187
+ "checkmark",
1188
+ "group",
1189
+ "optionGroupLabel",
1190
+ "optionGroupChildren",
1191
+ "virtualScroll",
1192
+ "virtualScrollItemSize",
1193
+ "lazy",
1194
+ "selectedObject"
1195
+ ],
1196
+ outputs: [
1197
+ "onFilter",
1198
+ "onChange",
1199
+ "onShow",
1200
+ "onHide",
1201
+ "onBlur",
1202
+ "onFocus",
1203
+ "onClear",
1204
+ "onLazyLoad"
1205
+ ]
1206
+ },
1207
+ {
1208
+ kind: "component",
1209
+ type: SkeletonComponent,
1210
+ selector: "f-skeleton",
1211
+ inputs: [
1212
+ "width",
1213
+ "height",
1214
+ "size",
1215
+ "borderRadius",
1216
+ "shape",
1217
+ "animation",
1218
+ "styleClass"
1219
+ ]
1220
+ },
1221
+ {
1222
+ kind: "directive",
1223
+ type: SortableColumnDirective,
1224
+ selector: "[fSortableColumn]",
1225
+ inputs: ["fSortableColumn"]
1226
+ },
1227
+ {
1228
+ kind: "component",
1229
+ type: SortIconComponent,
1230
+ selector: "f-sorticon",
1231
+ inputs: ["field"]
1232
+ },
1233
+ {
1234
+ kind: "component",
1235
+ type: TableComponent,
1236
+ selector: "f-table",
1237
+ inputs: [
1238
+ "value",
1239
+ "lazy",
1240
+ "loading",
1241
+ "rows",
1242
+ "totalRecords",
1243
+ "rowHover",
1244
+ "dataKey",
1245
+ "scrollable",
1246
+ "scrollHeight",
1247
+ "paginator",
1248
+ "first",
1249
+ "rowsPerPageOptions",
1250
+ "showCurrentPageReport",
1251
+ "currentPageReportTemplate",
1252
+ "tableStyle",
1253
+ "styleClass",
1254
+ "globalFilterFields",
1255
+ "filterDelay",
1256
+ "responsiveLayout",
1257
+ "selectionMode",
1258
+ "sortMode",
1259
+ "rowExpandMode",
1260
+ "resizableColumns",
1261
+ "rowStyleClass",
1262
+ "virtualScroll",
1263
+ "virtualScrollItemSize",
1264
+ "sortField",
1265
+ "sortOrder",
1266
+ "multiSortMeta",
1267
+ "filters",
1268
+ "selection",
1269
+ "expandedRowKeys",
1270
+ "contextMenuSelection"
1271
+ ],
1272
+ outputs: [
1273
+ "sortFieldChange",
1274
+ "sortOrderChange",
1275
+ "multiSortMetaChange",
1276
+ "filtersChange",
1277
+ "selectionChange",
1278
+ "expandedRowKeysChange",
1279
+ "contextMenuSelectionChange",
1280
+ "onLazyLoad",
1281
+ "onSort",
1282
+ "onFilter",
1283
+ "onRowSelect",
1284
+ "onRowUnselect",
1285
+ "onSelectAllChange",
1286
+ "onRowExpand",
1287
+ "onRowCollapse",
1288
+ "onColResize",
1289
+ "onRowContextMenu"
1290
+ ]
1291
+ },
1292
+ {
1293
+ kind: "component",
1294
+ type: TagComponent,
1295
+ selector: "f-tag",
1296
+ inputs: [
1297
+ "value",
1298
+ "severity",
1299
+ "icon",
1300
+ "rounded",
1301
+ "color",
1302
+ "styleClass",
1303
+ "style"
1304
+ ]
1305
+ },
1306
+ {
1307
+ kind: "directive",
1308
+ type: TextareaDirective,
1309
+ selector: "[fTextarea]",
1310
+ inputs: [
1311
+ "invalid",
1312
+ "touched",
1313
+ "variant",
1314
+ "fluid",
1315
+ "autoResize"
1316
+ ],
1317
+ outputs: ["onResize"]
1318
+ },
1319
+ {
1320
+ kind: "directive",
1321
+ type: TooltipDirective,
1322
+ selector: "[fTooltip]",
1323
+ inputs: [
1324
+ "fTooltip",
1325
+ "tooltipPosition",
1326
+ "positionStyle",
1327
+ "tooltipStyleClass",
1328
+ "disabled",
1329
+ "tooltipEvent",
1330
+ "showDelay",
1331
+ "hideDelay",
1332
+ "life",
1333
+ "autoHide",
1334
+ "positionTop",
1335
+ "positionLeft",
1336
+ "escape"
1337
+ ]
1338
+ },
1339
+ {
1340
+ kind: "component",
1341
+ type: UiIconComponent,
1342
+ selector: "ui-icon",
1343
+ inputs: [
1344
+ "name",
1345
+ "size",
1346
+ "spin",
1347
+ "fill",
1348
+ "severity"
1349
+ ]
1350
+ },
1351
+ {
1352
+ kind: "ngmodule",
1353
+ type: DragDropModule
1354
+ },
1355
+ {
1356
+ kind: "directive",
1357
+ type: i1.CdkDropList,
1358
+ selector: "[cdkDropList], cdk-drop-list",
1359
+ inputs: [
1360
+ "cdkDropListConnectedTo",
1361
+ "cdkDropListData",
1362
+ "cdkDropListOrientation",
1363
+ "id",
1364
+ "cdkDropListLockAxis",
1365
+ "cdkDropListDisabled",
1366
+ "cdkDropListSortingDisabled",
1367
+ "cdkDropListEnterPredicate",
1368
+ "cdkDropListSortPredicate",
1369
+ "cdkDropListAutoScrollDisabled",
1370
+ "cdkDropListAutoScrollStep",
1371
+ "cdkDropListElementContainer",
1372
+ "cdkDropListHasAnchor"
1373
+ ],
1374
+ outputs: [
1375
+ "cdkDropListDropped",
1376
+ "cdkDropListEntered",
1377
+ "cdkDropListExited",
1378
+ "cdkDropListSorted"
1379
+ ],
1380
+ exportAs: ["cdkDropList"]
1381
+ },
1382
+ {
1383
+ kind: "directive",
1384
+ type: i1.CdkDrag,
1385
+ selector: "[cdkDrag]",
1386
+ inputs: [
1387
+ "cdkDragData",
1388
+ "cdkDragLockAxis",
1389
+ "cdkDragRootElement",
1390
+ "cdkDragBoundary",
1391
+ "cdkDragStartDelay",
1392
+ "cdkDragFreeDragPosition",
1393
+ "cdkDragDisabled",
1394
+ "cdkDragConstrainPosition",
1395
+ "cdkDragPreviewClass",
1396
+ "cdkDragPreviewContainer",
1397
+ "cdkDragScale"
1398
+ ],
1399
+ outputs: [
1400
+ "cdkDragStarted",
1401
+ "cdkDragReleased",
1402
+ "cdkDragEnded",
1403
+ "cdkDragEntered",
1404
+ "cdkDragExited",
1405
+ "cdkDragDropped",
1406
+ "cdkDragMoved"
1407
+ ],
1408
+ exportAs: ["cdkDrag"]
1409
+ },
1410
+ {
1411
+ kind: "directive",
1412
+ type: i1.CdkDragHandle,
1413
+ selector: "[cdkDragHandle]",
1414
+ inputs: ["cdkDragHandleDisabled"]
1415
+ },
1416
+ {
1417
+ kind: "ngmodule",
1418
+ type: FormsModule
1419
+ },
1420
+ {
1421
+ kind: "directive",
1422
+ type: i2.DefaultValueAccessor,
1423
+ 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]"
1424
+ },
1425
+ {
1426
+ kind: "directive",
1427
+ type: i2.NgControlStatus,
1428
+ selector: "[formControlName],[ngModel],[formControl]"
1429
+ },
1430
+ {
1431
+ kind: "directive",
1432
+ type: i2.NgModel,
1433
+ selector: "[ngModel]:not([formControlName]):not([formControl])",
1434
+ inputs: [
1435
+ "name",
1436
+ "disabled",
1437
+ "ngModel",
1438
+ "ngModelOptions"
1439
+ ],
1440
+ outputs: ["ngModelChange"],
1441
+ exportAs: ["ngModel"]
1442
+ },
1443
+ {
1444
+ kind: "component",
1445
+ type: TaskCardComponent,
1446
+ selector: "tm-task-card",
1447
+ inputs: ["task"],
1448
+ outputs: [
1449
+ "cardClick",
1450
+ "editClick",
1451
+ "deleteClick"
1452
+ ]
1453
+ },
1454
+ {
1455
+ kind: "component",
1456
+ type: TaskFormDialogComponent,
1457
+ selector: "tm-task-form-dialog",
1458
+ inputs: [
1459
+ "visible",
1460
+ "task",
1461
+ "boardId",
1462
+ "listId",
1463
+ "members",
1464
+ "labels"
1465
+ ],
1466
+ outputs: ["visibleChange", "saved"]
1467
+ },
1468
+ {
1469
+ kind: "component",
1470
+ type: TaskDetailPanelComponent,
1471
+ selector: "tm-task-detail-panel",
1472
+ inputs: ["task"],
1473
+ outputs: ["closePanel", "editTask"]
1474
+ },
1475
+ {
1476
+ kind: "component",
1477
+ type: UserSelectComponent,
1478
+ selector: "lib-user-select",
1479
+ inputs: ["showClear", "value"],
1480
+ outputs: ["valueChange", "userSelected"]
1481
+ },
1482
+ {
1483
+ kind: "pipe",
1484
+ type: DatePipe,
1485
+ name: "date"
1486
+ },
1487
+ {
1488
+ kind: "pipe",
1489
+ type: TranslatePipe,
1490
+ name: "translate"
1491
+ }
1492
+ ]
1493
+ });
1494
+ };
1495
+ i0.ɵɵngDeclareClassMetadata({
1496
+ minVersion: "12.0.0",
1497
+ version: "22.2.1",
1498
+ ngImport: i0,
1499
+ type: BoardViewPageComponent,
1500
+ decorators: [{
1501
+ type: Component,
1502
+ args: [{
1503
+ selector: "tm-board-view-page",
1504
+ imports: [
1505
+ AvatarComponent,
1506
+ DialogComponent,
1507
+ FButtonDirective,
1508
+ InputTextDirective,
1509
+ SelectComponent,
1510
+ SkeletonComponent,
1511
+ SortableColumnDirective,
1512
+ SortIconComponent,
1513
+ TableComponent,
1514
+ TagComponent,
1515
+ TextareaDirective,
1516
+ TooltipDirective,
1517
+ UiIconComponent,
1518
+ DragDropModule,
1519
+ DatePipe,
1520
+ FormsModule,
1521
+ TaskCardComponent,
1522
+ TaskFormDialogComponent,
1523
+ TaskDetailPanelComponent,
1524
+ TranslatePipe,
1525
+ UserSelectComponent
1526
+ ],
1527
+ template: `
1528
+ <div class="flex flex-col h-full">
1529
+ <div class="flex flex-col">
1530
+ <div class="flex items-center justify-between px-5 py-2.5 gap-4">
1531
+ @if (state.activeBoard(); as board) {
1532
+ <div class="flex items-center gap-2.5 min-w-0 flex-1 group">
1533
+ <div class="flex flex-col min-w-0">
1534
+ <div class="flex items-center gap-1.5 min-w-0">
1535
+ <h1
1536
+ class="text-base font-bold text-surface-900 dark:text-surface-0 truncate leading-snug"
1537
+ >
1538
+ {{ board.name }}
1539
+ </h1>
1540
+ <button
1541
+ fButton
1542
+ [text]="true"
1543
+ size="small"
1544
+ severity="secondary"
1545
+ class="!w-6 !h-6 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
1546
+ [fTooltip]="'task.manager.board.edit' | translate"
1547
+ tooltipPosition="bottom"
1548
+ (click)="openEditBoardDialog(board)"
1549
+ >
1550
+ <ui-icon pButtonIcon name="pencil"></ui-icon>
1551
+ </button>
1552
+ </div>
1553
+ @if (board.description) {
1554
+ <span
1555
+ class="text-xs text-muted-color truncate max-w-sm hidden md:block leading-snug"
1556
+ >
1557
+ {{ board.description }}
1558
+ </span>
1559
+ }
1560
+ </div>
1561
+ </div>
1562
+ }
1563
+
1564
+ <div class="flex items-center gap-1.5 flex-shrink-0">
1565
+ @if (state.members().length > 0) {
1566
+ <div
1567
+ class="flex items-center -space-x-2 cursor-pointer mr-1"
1568
+ (click)="showMembersDialog.set(true)"
1569
+ [fTooltip]="'task.manager.board.manage.members' | translate"
1570
+ tooltipPosition="bottom"
1571
+ >
1572
+ @for (member of state.members().slice(0, 4); track member.id) {
1573
+ <f-avatar
1574
+ shape="circle"
1575
+ size="small"
1576
+ [label]="memberInitials(member)"
1577
+ [color]="memberColor(member)"
1578
+ styleClass="!border-2 !border-surface-card ring-1 ring-surface flex-shrink-0"
1579
+ />
1580
+ }
1581
+ @if (state.members().length > 4) {
1582
+ <f-avatar
1583
+ shape="circle"
1584
+ size="small"
1585
+ [label]="'+' + (state.members().length - 4)"
1586
+ styleClass="!border-2 !border-surface-card !bg-surface-200 dark:!bg-surface-700 !text-surface-600 dark:!text-surface-300 flex-shrink-0"
1587
+ />
1588
+ }
1589
+ </div>
1590
+ }
1591
+
1592
+ <div
1593
+ class="h-5 w-px bg-surface-300 dark:bg-surface-600 mx-0.5"
1594
+ ></div>
1595
+
1596
+ <div
1597
+ class="flex rounded-border overflow-hidden border border-surface"
1598
+ >
1599
+ <button
1600
+ fButton
1601
+ [fTooltip]="'task.manager.view.board' | translate"
1602
+ tooltipPosition="bottom"
1603
+ size="small"
1604
+ [severity]="viewMode() === 'board' ? 'primary' : 'secondary'"
1605
+ [outlined]="viewMode() !== 'board'"
1606
+ class="!rounded-none !border-0"
1607
+ (click)="viewMode.set('board')"
1608
+ >
1609
+ <ui-icon pButtonIcon name="th-large"></ui-icon>
1610
+ </button>
1611
+ <button
1612
+ fButton
1613
+ [fTooltip]="'task.manager.view.list' | translate"
1614
+ tooltipPosition="bottom"
1615
+ size="small"
1616
+ [severity]="viewMode() === 'list' ? 'primary' : 'secondary'"
1617
+ [outlined]="viewMode() !== 'list'"
1618
+ class="!rounded-none !border-0"
1619
+ (click)="viewMode.set('list')"
1620
+ >
1621
+ <ui-icon pButtonIcon name="list"></ui-icon>
1622
+ </button>
1623
+ </div>
1624
+
1625
+ <div
1626
+ class="h-5 w-px bg-surface-300 dark:bg-surface-600 mx-0.5"
1627
+ ></div>
1628
+
1629
+ <button
1630
+ fButton
1631
+ [text]="true"
1632
+ severity="secondary"
1633
+ size="small"
1634
+ class="!w-8 !h-8"
1635
+ [fTooltip]="'task.manager.board.manage.members' | translate"
1636
+ tooltipPosition="bottom"
1637
+ (click)="showMembersDialog.set(true)"
1638
+ >
1639
+ <ui-icon pButtonIcon name="users"></ui-icon>
1640
+ </button>
1641
+ <button
1642
+ fButton
1643
+ [text]="true"
1644
+ severity="secondary"
1645
+ size="small"
1646
+ class="!w-8 !h-8"
1647
+ [fTooltip]="'task.manager.board.manage.labels' | translate"
1648
+ tooltipPosition="bottom"
1649
+ (click)="showLabelsDialog.set(true)"
1650
+ >
1651
+ <ui-icon pButtonIcon name="tag"></ui-icon>
1652
+ </button>
1653
+
1654
+ @if (viewMode() === 'board') {
1655
+ <div
1656
+ class="h-5 w-px bg-surface-300 dark:bg-surface-600 mx-0.5"
1657
+ ></div>
1658
+ <button
1659
+ fButton
1660
+ size="small"
1661
+ (click)="showAddListDialog.set(true)"
1662
+ >
1663
+ <ui-icon pButtonIcon name="plus"></ui-icon>
1664
+ <span pButtonLabel>{{
1665
+ 'task.manager.board.add.list' | translate
1666
+ }}</span>
1667
+ </button>
1668
+ }
1669
+ </div>
1670
+ </div>
1671
+
1672
+ @if (state.activeBoard(); as board) {
1673
+ <div
1674
+ class="h-0.5 w-full flex-shrink-0"
1675
+ [style.background-color]="board.color"
1676
+ ></div>
1677
+ }
1678
+ </div>
1679
+
1680
+ @if (state.isBoardLoading()) {
1681
+ @if (viewMode() === 'board') {
1682
+ <div class="flex gap-4 p-6 overflow-x-auto">
1683
+ @for (i of [1, 2, 3]; track i) {
1684
+ <div class="flex-shrink-0 w-72">
1685
+ <f-skeleton height="36px" class="mb-3" />
1686
+ @for (j of [1, 2, 3]; track j) {
1687
+ <f-skeleton height="90px" class="mb-2" />
1688
+ }
1689
+ </div>
1690
+ }
1691
+ </div>
1692
+ } @else {
1693
+ <div class="p-6">
1694
+ @for (i of [1, 2, 3, 4, 5]; track i) {
1695
+ <f-skeleton height="48px" class="mb-2" />
1696
+ }
1697
+ </div>
1698
+ }
1699
+ } @else if (viewMode() === 'board') {
1700
+ <div
1701
+ class="flex gap-4 p-5 overflow-x-auto flex-1"
1702
+ cdkDropList
1703
+ cdkDropListOrientation="horizontal"
1704
+ [cdkDropListData]="state.listsWithTasks()"
1705
+ (cdkDropListDropped)="onListDrop($event)"
1706
+ >
1707
+ @for (list of state.listsWithTasks(); track list.id) {
1708
+ <div
1709
+ cdkDrag
1710
+ 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"
1711
+ >
1712
+ <div
1713
+ class="flex items-center gap-1 px-2.5 py-2.5 border-b border-surface"
1714
+ >
1715
+ <ui-icon
1716
+ cdkDragHandle
1717
+ name="bars"
1718
+ class="text-muted-color text-xs cursor-grab flex-shrink-0 px-1"
1719
+ ></ui-icon>
1720
+
1721
+ <div class="flex items-center gap-2 min-w-0 flex-1">
1722
+ @if (editingListId() === list.id) {
1723
+ <input
1724
+ fInputText
1725
+ class="text-sm font-semibold !h-6 !py-0 !px-1 w-full"
1726
+ [ngModel]="editingListNameValue()"
1727
+ (ngModelChange)="editingListNameValue.set($event)"
1728
+ (keydown.enter)="saveRenameList()"
1729
+ (keydown.escape)="cancelRenameList()"
1730
+ (blur)="cancelRenameList()"
1731
+ (click)="$event.stopPropagation()"
1732
+ autofocus
1733
+ />
1734
+ } @else {
1735
+ <span class="font-semibold text-sm truncate">{{
1736
+ list.name
1737
+ }}</span>
1738
+ <span
1739
+ class="text-[11px] font-semibold px-1.5 py-0.5 rounded-full text-white flex-shrink-0"
1740
+ [style.background-color]="list.color"
1741
+ >
1742
+ {{ list.tasks.length }}
1743
+ </span>
1744
+ }
1745
+ </div>
1746
+
1747
+ @if (editingListId() === list.id) {
1748
+ <button
1749
+ fButton
1750
+ [text]="true"
1751
+ size="small"
1752
+ severity="success"
1753
+ class="!w-6 !h-6 flex-shrink-0"
1754
+ (mousedown)="$event.preventDefault()"
1755
+ (click)="saveRenameList(); $event.stopPropagation()"
1756
+ >
1757
+ <ui-icon pButtonIcon name="check"></ui-icon>
1758
+ </button>
1759
+ <button
1760
+ fButton
1761
+ [text]="true"
1762
+ size="small"
1763
+ severity="secondary"
1764
+ class="!w-6 !h-6 flex-shrink-0"
1765
+ (mousedown)="$event.preventDefault()"
1766
+ (click)="cancelRenameList(); $event.stopPropagation()"
1767
+ >
1768
+ <ui-icon pButtonIcon name="times"></ui-icon>
1769
+ </button>
1770
+ } @else {
1771
+ <button
1772
+ fButton
1773
+ [text]="true"
1774
+ size="small"
1775
+ severity="secondary"
1776
+ class="!w-6 !h-6 flex-shrink-0"
1777
+ [fTooltip]="'task.manager.list.rename' | translate"
1778
+ tooltipPosition="bottom"
1779
+ (click)="startRenameList(list); $event.stopPropagation()"
1780
+ >
1781
+ <ui-icon pButtonIcon name="pencil"></ui-icon>
1782
+ </button>
1783
+ }
1784
+ </div>
1785
+
1786
+ <div
1787
+ cdkDropList
1788
+ [id]="list.id"
1789
+ [cdkDropListData]="list.tasks"
1790
+ [cdkDropListConnectedTo]="listDropIds()"
1791
+ (cdkDropListDropped)="onTaskDrop($event)"
1792
+ class="flex flex-col gap-2 p-2 overflow-y-auto flex-1 min-h-[60px]"
1793
+ >
1794
+ @for (task of list.tasks; track task.id) {
1795
+ <div cdkDrag>
1796
+ <tm-task-card
1797
+ [task]="task"
1798
+ (cardClick)="state.selectTask($event)"
1799
+ (editClick)="onEditTask($event)"
1800
+ (deleteClick)="onDeleteTask($event)"
1801
+ />
1802
+ </div>
1803
+ }
1804
+ @if (list.tasks.length === 0) {
1805
+ <div
1806
+ class="flex items-center justify-center h-14 border-2 border-dashed border-surface rounded-border"
1807
+ >
1808
+ <span class="text-xs text-muted-color">{{
1809
+ 'task.manager.board.drop.tasks.here' | translate
1810
+ }}</span>
1811
+ </div>
1812
+ }
1813
+ </div>
1814
+
1815
+ <div class="border-t border-surface p-1.5">
1816
+ <button
1817
+ fButton
1818
+ severity="secondary"
1819
+ [text]="true"
1820
+ size="small"
1821
+ class="w-full !justify-start"
1822
+ (click)="openAddTask(list)"
1823
+ >
1824
+ <ui-icon pButtonIcon name="plus"></ui-icon>
1825
+ <span pButtonLabel>{{
1826
+ 'task.manager.board.add.task' | translate
1827
+ }}</span>
1828
+ </button>
1829
+ </div>
1830
+ </div>
1831
+ }
1832
+
1833
+ @if (state.listsWithTasks().length === 0) {
1834
+ <div class="flex-1 flex items-center justify-center">
1835
+ <div class="text-center text-muted-color">
1836
+ <div
1837
+ class="w-16 h-16 rounded-2xl border-2 border-dashed border-surface flex items-center justify-center mx-auto mb-4"
1838
+ >
1839
+ <ui-icon
1840
+ name="list"
1841
+ class="text-3xl text-muted-color"
1842
+ ></ui-icon>
1843
+ </div>
1844
+ <p class="font-medium text-muted-color">
1845
+ {{ 'task.manager.board.list.empty' | translate }}
1846
+ </p>
1847
+ <button
1848
+ fButton
1849
+ severity="secondary"
1850
+ size="small"
1851
+ class="mt-3"
1852
+ (click)="showAddListDialog.set(true)"
1853
+ >
1854
+ <ui-icon pButtonIcon name="plus"></ui-icon>
1855
+ <span pButtonLabel>{{
1856
+ 'task.manager.board.add.list' | translate
1857
+ }}</span>
1858
+ </button>
1859
+ </div>
1860
+ </div>
1861
+ }
1862
+ </div>
1863
+ } @else {
1864
+ <div class="p-4 sm:p-6 flex-1 overflow-auto">
1865
+ <div class="card">
1866
+ <div class="overflow-x-auto -mx-4 sm:mx-0">
1867
+ <f-table
1868
+ [value]="allTasksFlat()"
1869
+ [paginator]="allTasksFlat().length > 20"
1870
+ [rows]="20"
1871
+ [rowsPerPageOptions]="[10, 20, 50]"
1872
+ [scrollable]="true"
1873
+ [tableStyle]="{ 'min-width': '700px' }"
1874
+ >
1875
+ <ng-template #header>
1876
+ <tr>
1877
+ <th fSortableColumn="title" class="min-w-[240px]">
1878
+ {{ 'task.manager.table.title' | translate }}
1879
+ <f-sorticon field="title" />
1880
+ </th>
1881
+ <th fSortableColumn="listName" class="w-40">
1882
+ {{ 'task.manager.table.list' | translate }}
1883
+ <f-sorticon field="listName" />
1884
+ </th>
1885
+ <th fSortableColumn="priority" class="w-32">
1886
+ {{ 'task.manager.table.priority' | translate }}
1887
+ <f-sorticon field="priority" />
1888
+ </th>
1889
+ <th class="w-36">
1890
+ {{ 'task.manager.table.assignee' | translate }}
1891
+ </th>
1892
+ <th fSortableColumn="dueDate" class="w-32">
1893
+ {{ 'task.manager.table.due.date' | translate }}
1894
+ <f-sorticon field="dueDate" />
1895
+ </th>
1896
+ <th class="w-[100px]">
1897
+ {{ 'shared.actions' | translate }}
1898
+ </th>
1899
+ </tr>
1900
+ </ng-template>
1901
+ <ng-template #body let-task>
1902
+ <tr>
1903
+ <td>
1904
+ <span
1905
+ class="font-medium text-sm cursor-pointer hover:text-primary-500 transition-colors"
1906
+ (click)="state.selectTask(task)"
1907
+ >{{ task.title }}</span
1908
+ >
1909
+ </td>
1910
+ <td>
1911
+ <span class="inline-flex items-center gap-1.5 text-sm">
1912
+ <span
1913
+ class="w-2 h-2 rounded-full flex-shrink-0"
1914
+ [style.background-color]="task.listColor"
1915
+ ></span>
1916
+ {{ task.listName }}
1917
+ </span>
1918
+ </td>
1919
+ <td>
1920
+ <f-tag
1921
+ [value]="task.priority"
1922
+ [severity]="prioritySeverity(task.priority)"
1923
+ />
1924
+ </td>
1925
+ <td>
1926
+ @if (task.assignees?.length) {
1927
+ <div class="flex items-center -space-x-1.5">
1928
+ @for (
1929
+ a of task.assignees!.slice(0, 3);
1930
+ track a.userId
1931
+ ) {
1932
+ <f-avatar
1933
+ shape="circle"
1934
+ size="small"
1935
+ [label]="getMemberInitials(a.userId)"
1936
+ [color]="memberColorById(a.userId)"
1937
+ styleClass="!ring-surface-card flex-shrink-0"
1938
+ [fTooltip]="a.userName ?? ''"
1939
+ />
1940
+ }
1941
+ @if (task.assignees!.length > 3) {
1942
+ <f-avatar
1943
+ shape="circle"
1944
+ size="small"
1945
+ [label]="'+' + (task.assignees!.length - 3)"
1946
+ styleClass="!bg-surface-200 dark:!bg-surface-700 !text-surface-600 !ring-surface-card flex-shrink-0"
1947
+ />
1948
+ }
1949
+ </div>
1950
+ } @else {
1951
+ <span class="text-muted-color text-sm">—</span>
1952
+ }
1953
+ </td>
1954
+ <td>
1955
+ @if (task.dueDate) {
1956
+ <span
1957
+ class="text-sm"
1958
+ [class.text-red-500]="isOverdue(task.dueDate)"
1959
+ >
1960
+ {{ task.dueDate | date: 'MMM d, y' }}
1961
+ </span>
1962
+ } @else {
1963
+ <span class="text-muted-color text-sm">—</span>
1964
+ }
1965
+ </td>
1966
+ <td>
1967
+ <div class="flex gap-1">
1968
+ <button
1969
+ fButton
1970
+ [text]="true"
1971
+ severity="secondary"
1972
+ size="small"
1973
+ [fTooltip]="'shared.edit' | translate"
1974
+ (click)="onEditTask(task)"
1975
+ >
1976
+ <ui-icon pButtonIcon name="pencil"></ui-icon>
1977
+ </button>
1978
+ <button
1979
+ fButton
1980
+ [text]="true"
1981
+ severity="danger"
1982
+ size="small"
1983
+ [fTooltip]="'shared.delete' | translate"
1984
+ (click)="onDeleteTask(task)"
1985
+ >
1986
+ <ui-icon pButtonIcon name="trash"></ui-icon>
1987
+ </button>
1988
+ </div>
1989
+ </td>
1990
+ </tr>
1991
+ </ng-template>
1992
+ <ng-template #emptymessage>
1993
+ <tr>
1994
+ <td colspan="6" class="text-center text-muted-color py-8">
1995
+ <ui-icon
1996
+ name="inbox"
1997
+ class="text-4xl block mb-3"
1998
+ ></ui-icon>
1999
+ {{ 'task.manager.table.empty' | translate }}
2000
+ </td>
2001
+ </tr>
2002
+ </ng-template>
2003
+ </f-table>
2004
+ </div>
2005
+ </div>
2006
+ </div>
2007
+ }
2008
+ </div>
2009
+
2010
+ <tm-task-form-dialog
2011
+ [(visible)]="showTaskForm"
2012
+ [task]="editingTask()"
2013
+ [boardId]="boardId()"
2014
+ [listId]="activeListId()"
2015
+ [members]="state.members()"
2016
+ [labels]="state.labels()"
2017
+ (saved)="onTaskSaved($event)"
2018
+ />
2019
+
2020
+ <tm-task-detail-panel
2021
+ [task]="state.selectedTask()"
2022
+ (closePanel)="state.selectTask(null)"
2023
+ (editTask)="onEditTask($event)"
2024
+ />
2025
+
2026
+ <f-dialog
2027
+ [header]="'task.manager.label.manage.title' | translate"
2028
+ [(visible)]="showLabelsDialog"
2029
+ [modal]="true"
2030
+ [style]="{ width: '420px' }"
2031
+ (onHide)="newLabelName = ''; newLabelColor = boardColorPalette[0]"
2032
+ >
2033
+ <div class="flex flex-col gap-4 py-2">
2034
+ <div class="flex gap-2 items-end">
2035
+ <div class="flex flex-col gap-1 flex-1">
2036
+ <label class="text-sm font-medium">{{
2037
+ 'task.manager.label.name.label' | translate
2038
+ }}</label>
2039
+ <input
2040
+ fInputText
2041
+ [(ngModel)]="newLabelName"
2042
+ [placeholder]="'task.manager.label.name.placeholder' | translate"
2043
+ />
2044
+ </div>
2045
+ <div class="flex flex-col gap-1">
2046
+ <label class="text-sm font-medium">{{
2047
+ 'task.manager.label.color.label' | translate
2048
+ }}</label>
2049
+ <input
2050
+ type="color"
2051
+ [(ngModel)]="newLabelColor"
2052
+ class="w-10 h-10 rounded cursor-pointer border border-surface"
2053
+ />
2054
+ </div>
2055
+ <button
2056
+ fButton
2057
+ size="small"
2058
+ (click)="addLabel()"
2059
+ [disabled]="isAddingLabel()"
2060
+ >
2061
+ @if (isAddingLabel()) {
2062
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
2063
+ } @else {
2064
+ <ui-icon pButtonIcon name="plus"></ui-icon>
2065
+ }
2066
+ <span pButtonLabel>{{ 'task.manager.label.add' | translate }}</span>
2067
+ </button>
2068
+ </div>
2069
+
2070
+ <div class="flex flex-col gap-2 max-h-64 overflow-y-auto">
2071
+ @if (state.labels().length === 0) {
2072
+ <p class="text-sm text-muted-color text-center py-4">
2073
+ {{ 'task.manager.label.no.labels' | translate }}
2074
+ </p>
2075
+ }
2076
+ @for (label of state.labels(); track label.id) {
2077
+ <div
2078
+ class="flex items-center justify-between px-3 py-2 rounded-border border border-surface"
2079
+ >
2080
+ <div class="flex items-center gap-2">
2081
+ <span
2082
+ class="w-4 h-4 rounded-full flex-shrink-0"
2083
+ [style.background-color]="label.color"
2084
+ ></span>
2085
+ <span class="text-sm font-medium">{{ label.name }}</span>
2086
+ </div>
2087
+ <button
2088
+ fButton
2089
+ severity="danger"
2090
+ size="small"
2091
+ [text]="true"
2092
+ (click)="removeLabel(label.id)"
2093
+ >
2094
+ <ui-icon pButtonIcon name="trash"></ui-icon>
2095
+ </button>
2096
+ </div>
2097
+ }
2098
+ </div>
2099
+ </div>
2100
+ </f-dialog>
2101
+
2102
+ <f-dialog
2103
+ [header]="'task.manager.member.manage.title' | translate"
2104
+ [(visible)]="showMembersDialog"
2105
+ [modal]="true"
2106
+ [style]="{ width: '480px' }"
2107
+ (onHide)="resetMemberForm()"
2108
+ >
2109
+ <div class="flex flex-col gap-4 py-2">
2110
+ <div class="flex flex-col gap-3">
2111
+ <div class="flex flex-col gap-1">
2112
+ <label class="text-sm font-medium">{{
2113
+ 'task.manager.member.user.label' | translate
2114
+ }}</label>
2115
+ <lib-user-select
2116
+ [(value)]="selectedMemberUser"
2117
+ [isEditMode]="true"
2118
+ [placeHolder]="'task.manager.member.user.placeholder' | translate"
2119
+ />
2120
+ </div>
2121
+
2122
+ <div class="flex gap-2 items-end">
2123
+ <div class="flex flex-col gap-1 flex-1">
2124
+ <label class="text-sm font-medium">{{
2125
+ 'task.manager.member.role.label' | translate
2126
+ }}</label>
2127
+ <f-select
2128
+ [(ngModel)]="newMemberRole"
2129
+ [options]="memberRoleOptions"
2130
+ optionValue="value"
2131
+ [placeholder]="
2132
+ 'task.manager.member.role.placeholder' | translate
2133
+ "
2134
+ class="w-full"
2135
+ >
2136
+ <ng-template #selectedItem let-opt>
2137
+ {{ opt.labelKey | translate }}
2138
+ </ng-template>
2139
+ <ng-template #item let-opt>
2140
+ {{ opt.labelKey | translate }}
2141
+ </ng-template>
2142
+ </f-select>
2143
+ </div>
2144
+ <button
2145
+ fButton
2146
+ size="small"
2147
+ [disabled]="!selectedMemberUser || isAddingMember()"
2148
+ (click)="addMember()"
2149
+ >
2150
+ @if (isAddingMember()) {
2151
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
2152
+ } @else {
2153
+ <ui-icon pButtonIcon name="plus"></ui-icon>
2154
+ }
2155
+ <span pButtonLabel>{{
2156
+ 'task.manager.member.add' | translate
2157
+ }}</span>
2158
+ </button>
2159
+ </div>
2160
+ </div>
2161
+
2162
+ <div class="flex flex-col gap-2 max-h-72 overflow-y-auto">
2163
+ @if (state.members().length === 0) {
2164
+ <p class="text-sm text-muted-color text-center py-4">
2165
+ {{ 'task.manager.member.no.members' | translate }}
2166
+ </p>
2167
+ }
2168
+ @for (member of state.members(); track member.id) {
2169
+ <div
2170
+ class="flex items-center justify-between px-3 py-2.5 rounded-border border border-surface"
2171
+ >
2172
+ <div class="flex items-center gap-3">
2173
+ <f-avatar
2174
+ shape="circle"
2175
+ [label]="memberInitials(member)"
2176
+ [color]="memberColor(member)"
2177
+ styleClass="!text-[11px] flex-shrink-0"
2178
+ />
2179
+ <div>
2180
+ <div class="text-sm font-medium ">
2181
+ {{ member.userName ?? member.userId }}
2182
+ </div>
2183
+ @if (member.userEmail) {
2184
+ <div class="text-xs text-muted-color">
2185
+ {{ member.userEmail }}
2186
+ </div>
2187
+ }
2188
+ </div>
2189
+ </div>
2190
+ <div class="flex items-center gap-2">
2191
+ <span
2192
+ class="text-[10px] font-semibold px-2 py-0.5 rounded-full"
2193
+ [class]="roleBadgeClass(member.role)"
2194
+ >
2195
+ {{
2196
+ 'task.manager.member.role.' + member.role.toLowerCase()
2197
+ | translate
2198
+ }}
2199
+ </span>
2200
+ @if (member.role !== 'OWNER') {
2201
+ <button
2202
+ fButton
2203
+ severity="danger"
2204
+ size="small"
2205
+ [text]="true"
2206
+ (click)="removeMember(member.userId)"
2207
+ >
2208
+ <ui-icon pButtonIcon name="trash"></ui-icon>
2209
+ </button>
2210
+ }
2211
+ </div>
2212
+ </div>
2213
+ }
2214
+ </div>
2215
+ </div>
2216
+ </f-dialog>
2217
+
2218
+ <f-dialog
2219
+ [header]="'task.manager.list.new' | translate"
2220
+ [(visible)]="showAddListDialog"
2221
+ [modal]="true"
2222
+ [style]="{ width: '360px' }"
2223
+ (onHide)="newListName = ''"
2224
+ >
2225
+ <div class="flex flex-col gap-2 py-2">
2226
+ <label class="text-sm font-medium">
2227
+ {{ 'task.manager.list.name.label' | translate }}
2228
+ <span class="text-red-500">*</span>
2229
+ </label>
2230
+ <input
2231
+ fInputText
2232
+ [(ngModel)]="newListName"
2233
+ [placeholder]="'task.manager.list.name.placeholder' | translate"
2234
+ />
2235
+ </div>
2236
+ <ng-template #footer>
2237
+ <div class="flex justify-end gap-2">
2238
+ <button
2239
+ fButton
2240
+ severity="secondary"
2241
+ (click)="showAddListDialog.set(false)"
2242
+ >
2243
+ <span pButtonLabel>{{ 'shared.cancel' | translate }}</span>
2244
+ </button>
2245
+ <button fButton (click)="addList()" [disabled]="isAddingList()">
2246
+ @if (isAddingList()) {
2247
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
2248
+ }
2249
+ <span pButtonLabel>{{ 'shared.create' | translate }}</span>
2250
+ </button>
2251
+ </div>
2252
+ </ng-template>
2253
+ </f-dialog>
2254
+
2255
+ <f-dialog
2256
+ [header]="'task.manager.board.edit' | translate"
2257
+ [(visible)]="showEditBoardDialog"
2258
+ [modal]="true"
2259
+ [style]="{ width: '420px' }"
2260
+ >
2261
+ <div class="flex flex-col gap-4 py-2">
2262
+ <div class="flex flex-col gap-1">
2263
+ <label class="text-sm font-medium">
2264
+ {{ 'task.manager.board.name.label' | translate }}
2265
+ <span class="text-red-500">*</span>
2266
+ </label>
2267
+ <input fInputText [(ngModel)]="editBoardName" />
2268
+ </div>
2269
+ <div class="flex flex-col gap-1">
2270
+ <label class="text-sm font-medium">{{
2271
+ 'task.manager.board.description.label' | translate
2272
+ }}</label>
2273
+ <textarea
2274
+ fTextarea
2275
+ [(ngModel)]="editBoardDescription"
2276
+ [rows]="3"
2277
+ [placeholder]="
2278
+ 'task.manager.board.description.placeholder' | translate
2279
+ "
2280
+ ></textarea>
2281
+ </div>
2282
+ <div class="flex flex-col gap-1">
2283
+ <label class="text-sm font-medium">{{
2284
+ 'task.manager.board.color.label' | translate
2285
+ }}</label>
2286
+ <div class="flex items-center gap-3">
2287
+ <input
2288
+ type="color"
2289
+ [(ngModel)]="editBoardColor"
2290
+ class="w-10 h-10 rounded cursor-pointer border border-surface"
2291
+ />
2292
+ <div class="flex gap-2 flex-wrap">
2293
+ @for (color of boardColorPalette; track color) {
2294
+ <button
2295
+ type="button"
2296
+ class="w-6 h-6 rounded-full border-2 transition-transform hover:scale-110"
2297
+ [style.background-color]="color"
2298
+ [class.border-surface-900]="editBoardColor === color"
2299
+ [class.dark:border-surface-0]="editBoardColor === color"
2300
+ [class.border-transparent]="editBoardColor !== color"
2301
+ (click)="editBoardColor = color"
2302
+ ></button>
2303
+ }
2304
+ </div>
2305
+ </div>
2306
+ </div>
2307
+ </div>
2308
+ <ng-template #footer>
2309
+ <div class="flex justify-end gap-2">
2310
+ <button
2311
+ fButton
2312
+ severity="secondary"
2313
+ (click)="showEditBoardDialog.set(false)"
2314
+ >
2315
+ <span pButtonLabel>{{ 'shared.cancel' | translate }}</span>
2316
+ </button>
2317
+ <button
2318
+ fButton
2319
+ [disabled]="!editBoardName.trim() || isSavingBoard()"
2320
+ (click)="saveBoard()"
2321
+ >
2322
+ @if (isSavingBoard()) {
2323
+ <ui-icon pButtonIcon name="spinner" [spin]="true"></ui-icon>
2324
+ }
2325
+ <span pButtonLabel>{{ 'shared.save' | translate }}</span>
2326
+ </button>
2327
+ </div>
2328
+ </ng-template>
2329
+ </f-dialog>
2330
+ `
2331
+ }]
2332
+ }],
2333
+ ctorParameters: () => [],
2334
+ propDecorators: { boardId: [{
2335
+ type: i0.Input,
2336
+ args: [{
2337
+ isSignal: true,
2338
+ alias: "boardId",
2339
+ required: true
2340
+ }]
2341
+ }] }
2342
+ });
2343
+ export { BoardViewPageComponent };
2344
+
2345
+ //# sourceMappingURL=flusys-ng-task-manager-board-view-page.component-CkC8-UKF.mjs.map