import { CommonModule } from '@angular/common';
import { Component, OnInit, inject, signal } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AddTaskDialogComponent } from './add-task-dialog.component';
import {
FormBuilder,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatDividerModule } from '@angular/material/divider';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
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';
@Component({
selector: 'lib-task-list',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatCardModule,
MatListModule,
MatProgressSpinnerModule,
MatToolbarModule,
MatIconModule,
MatButtonModule,
MatExpansionModule,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatDividerModule,
MatBadgeModule,
MatTooltipModule,
],
template: `
@if (loading()) {
} @else if (tasks().length === 0) {
assignment
No Tasks Found
There are currently no tasks in the system.
} @else {
@for (task of tasks(); track task.tasks.id) {
assignment
{{ task.tasks.title }}
@if (getTaskComments(task.tasks.id).length) {
}
By {{ getUserName(task.tasks.user_id) }}
{{
formatDate(task.tasks.timestamp)
}}
{{ task.tasks.description }}
}
}
`,
styles: [
`
.fab {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 1100;
}
.dialog-backdrop {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.2);
display: flex;
align-items: center;
justify-content: center;
z-index: 1200;
}
.dialog-card {
min-width: 320px;
max-width: 90vw;
}
.page-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.page-header {
margin-bottom: 1rem;
border-radius: 4px;
}
.spacer {
flex: 1 1 auto;
}
.loading-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 4rem 0;
gap: 1rem;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
border-radius: 8px;
padding: 3rem;
margin: 2rem 0;
text-align: center;
}
.empty-icon {
font-size: 4rem;
height: 4rem;
width: 4rem;
color: #bdbdbd;
margin-bottom: 1rem;
}
.task-container {
margin: 1rem 0;
}
.task-panel {
margin-bottom: 1rem;
border-radius: 8px;
}
.task-title-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
.comment-badge {
margin-left: 0.5rem;
}
.task-info {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.task-author {
font-weight: 500;
}
.task-timestamp {
font-size: 0.8rem;
color: #757575;
}
.task-details {
padding: 1rem 0;
}
.task-description {
font-size: 1rem;
line-height: 1.5;
margin-bottom: 1.5rem;
white-space: pre-line;
}
.comments-section {
margin-top: 1.5rem;
}
.comments-section h3 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.2rem;
margin-bottom: 1rem;
}
.comments-list {
margin-bottom: 1.5rem;
}
.comment-item {
padding: 1rem;
background-color: #f5f5f5;
border-radius: 8px;
margin-bottom: 1rem;
}
.comment-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.comment-author {
font-weight: 500;
}
.comment-time {
font-size: 0.8rem;
color: #757575;
}
.comment-content {
margin: 0;
white-space: pre-line;
}
.no-comments {
font-style: italic;
color: #757575;
text-align: center;
padding: 1rem;
background-color: #f5f5f5;
border-radius: 8px;
}
.comment-form {
background-color: #f9f9f9;
border-radius: 8px;
padding: 1rem;
margin-top: 1rem;
}
.comment-form h4 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1rem;
}
.form-row {
margin-bottom: 1rem;
}
.form-actions {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.full-width {
width: 100%;
}
button {
height: 56px;
display: flex;
align-items: center;
gap: 0.5rem;
}
.button-spinner {
margin-right: 0.5rem;
}
`,
],
})
export class TaskListComponent implements OnInit {
private tasksService = inject(TasksService);
private usersService = inject(UsersService);
private commentsService = inject(CommentsService);
private fb = inject(FormBuilder);
private toastService = inject(ToastService);
tasks = signal([]);
users = signal([]);
comments = signal([]);
loading = signal(true);
addingComment = signal(false);
private readonly dialog = inject(MatDialog);
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();
}
});
}
commentForm: FormGroup = this.fb.group({
content: ['', Validators.required],
userId: ['', Validators.required],
});
ngOnInit() {
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);
},
});
}
getUserName(userId: number): string {
const user = this.users().find((u) => u.id === userId);
return user ? user.name : 'Unknown User';
}
getTaskComments(taskId: number): CommentResponse[] {
return this.comments().filter(
(comment) => comment.comments.task_id === taskId
);
}
formatDate(timestamp?: string): string {
if (!timestamp) return 'Unknown date';
const date = new Date(timestamp);
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
return (
'Today at ' +
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
);
} else if (diffDays === 1) {
return (
'Yesterday at ' +
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
);
} else if (diffDays < 7) {
return `${diffDays} days ago`;
} else {
return date.toLocaleDateString([], {
day: 'numeric',
month: 'short',
year: 'numeric',
});
}
}
addComment(taskId: number) {
if (this.commentForm.invalid) return;
this.addingComment.set(true);
const comment = {
content: this.commentForm.value.content,
task_id: taskId,
user_id: parseInt(this.commentForm.value.userId),
};
this.commentsService
.createComment(comment)
.pipe(finalize(() => this.addingComment.set(false)))
.subscribe({
next: (newComment) => {
this.comments.update((comments) => [...comments, newComment]);
this.commentForm.reset({ content: '', userId: '' });
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'
);
},
});
}
}
forum
Comments
@if (getTaskComments(task.tasks.id).length) {{{ comment.comments.content }}
No comments yet.
}