235 lines
6.1 KiB
TypeScript
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'
|
|
);
|
|
},
|
|
});
|
|
}
|
|
}
|