tehnicni-app/client/libs/web/tasks/src/lib/task-list.component.ts
Gal Podlipnik fd276b2c07 theme
2025-07-14 20:40:45 +02:00

235 lines
6.1 KiB
TypeScript

import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
inject,
signal,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import {
CommentResponse,
CommentsService,
TaskResponse,
TasksService,
User,
UsersService,
} from '@org/shared/api';
import { ToastService } from '@org/shared/toast';
import { catchError, finalize, forkJoin, of } from 'rxjs';
import { AddTaskDialogComponent } from './add-task-dialog.component';
import { TaskCardComponent } from './task-card.component';
@Component({
selector: 'lib-task-list',
imports: [
CommonModule,
RouterModule,
MatProgressSpinnerModule,
MatToolbarModule,
MatIconModule,
MatButtonModule,
MatTooltipModule,
TaskCardComponent,
],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="page-container">
<mat-toolbar color="primary" class="page-header">
<span>Tasks</span>
<span class="spacer"></span>
<button mat-icon-button matTooltip="Refresh data" (click)="loadData()">
<mat-icon>refresh</mat-icon>
</button>
</mat-toolbar>
<button class="fab" mat-fab color="accent" (click)="openAddTaskDialog()">
<mat-icon>add</mat-icon>
</button>
@if (loading()) {
<div class="loading-container">
<mat-progress-spinner mode="indeterminate"></mat-progress-spinner>
<p>Loading tasks...</p>
</div>
} @else if (tasks().length === 0) {
<div class="empty-state">
<mat-icon class="empty-icon">assignment</mat-icon>
<h2>No Tasks Found</h2>
<p>There are currently no tasks in the system.</p>
</div>
} @else {
<div class="task-container">
@for (task of tasks(); track task.tasks.id) {
<lib-task-card
[task]="task"
[users]="users()"
[comments]="comments()"
[addingComment]="addingComment()"
(commentAdded)="addComment($event)"
></lib-task-card>
}
</div>
}
</div>
`,
styles: [
`
.fab {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 100;
}
.page-container {
max-width: 1200px;
margin: 0 auto;
}
.page-header {
margin-bottom: 1.5rem;
border-radius: var(--mat-sys-corner-small);
box-shadow: var(--mat-sys-level1);
}
.spacer {
flex: 1 1 auto;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
margin: 3rem 0;
gap: 1rem;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--mat-sys-surface-container-low);
border-radius: var(--mat-sys-corner-small);
padding: 3rem 1.5rem;
margin: 1.5rem 0;
text-align: center;
box-shadow: var(--mat-sys-level1);
}
.empty-icon {
font-size: 3rem;
height: 3rem;
width: 3rem;
color: var(--mat-sys-outline);
margin-bottom: 1rem;
}
.task-container {
margin: 1rem 0;
}
@media (max-width: 768px) {
.page-container {
padding: 0 0.5rem 5rem 0.5rem;
}
}
`,
],
})
export class TaskListComponent {
private readonly tasksService = inject(TasksService);
private readonly usersService = inject(UsersService);
private readonly commentsService = inject(CommentsService);
private readonly toastService = inject(ToastService);
private readonly dialog = inject(MatDialog);
readonly tasks = signal<TaskResponse[]>([]);
readonly users = signal<User[]>([]);
readonly comments = signal<CommentResponse[]>([]);
readonly loading = signal(true);
readonly addingComment = signal(false);
constructor() {
this.loadData();
}
loadData() {
this.loading.set(true);
forkJoin({
users: this.usersService.getUsers(),
tasks: this.tasksService.getTasks(),
comments: this.commentsService.getComments(),
})
.pipe(
catchError((error) => {
console.error('Error loading data:', error);
this.toastService.show(
'Failed to load data. Please try again.',
'error'
);
return of({ users: [], tasks: [], comments: [] });
}),
finalize(() => this.loading.set(false))
)
.subscribe({
next: (data) => {
this.users.set(data.users);
this.tasks.set(data.tasks);
this.comments.set(data.comments);
},
});
}
openAddTaskDialog() {
const dialogRef = this.dialog.open(AddTaskDialogComponent, {
width: '400px',
enterAnimationDuration: '220ms',
exitAnimationDuration: '180ms',
autoFocus: true,
restoreFocus: true,
disableClose: false,
});
dialogRef.afterClosed().subscribe((result) => {
if (result) {
this.loadData();
}
});
}
// Methods moved to child components
addComment(data: { taskId: number; content: string; userId: number }) {
this.addingComment.set(true);
const comment = {
content: data.content,
taskId: data.taskId,
userId: data.userId,
};
this.commentsService
.createComment(comment)
.pipe(finalize(() => this.addingComment.set(false)))
.subscribe({
next: (newComment) => {
this.comments.update((comments) => [...comments, newComment]);
this.toastService.show('Comment added successfully!');
},
error: (error) => {
console.error('Error adding comment:', error);
this.toastService.show(
'Failed to add comment! Please try again.',
'error'
);
},
});
}
}