This commit is contained in:
Bastian Wagner
2026-06-09 09:45:33 +02:00
commit 537c7cbbee
124 changed files with 27283 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
import { DatePipe } from '@angular/common';
import { Component, OnInit, computed, inject, signal } from '@angular/core';
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { finalize } from 'rxjs';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { getAuthErrorMessage } from '../../auth/error-message';
import { UserList, UserListItem } from '../lists.models';
import { ListsService } from '../lists.service';
@Component({
selector: 'app-list-detail',
imports: [
DatePipe,
ReactiveFormsModule,
RouterLink,
MatButtonModule,
MatCardModule,
MatCheckboxModule,
MatFormFieldModule,
MatIconModule,
MatInputModule,
MatProgressSpinnerModule,
MatSnackBarModule,
],
templateUrl: './list-detail.component.html',
styleUrl: './list-detail.component.scss',
})
export class ListDetailComponent implements OnInit {
private readonly formBuilder = inject(NonNullableFormBuilder);
private readonly listsService = inject(ListsService);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly snackBar = inject(MatSnackBar);
protected readonly list = signal<UserList | null>(null);
protected readonly isCreateMode = signal(false);
protected readonly loading = signal(true);
protected readonly saving = signal(false);
protected readonly addingItem = signal(false);
protected readonly errorMessage = signal<string | null>(null);
protected readonly updatingItemId = signal<string | null>(null);
protected readonly canEditItems = computed(() => Boolean(this.list()?.id));
protected readonly listForm = this.formBuilder.group({
name: ['', [Validators.required]],
description: [''],
});
protected readonly itemForm = this.formBuilder.group({
title: ['', [Validators.required]],
required: [true],
});
ngOnInit(): void {
this.isCreateMode.set(this.listId() === null);
if (this.isCreateMode()) {
this.loading.set(false);
this.listForm.reset({ name: '', description: '' });
return;
}
this.loadList();
}
protected loadList(): void {
const listId = this.listId();
if (!listId) {
this.errorMessage.set('Liste wurde nicht gefunden.');
this.loading.set(false);
return;
}
this.loading.set(true);
this.errorMessage.set(null);
this.listsService.getList(listId).subscribe({
next: (list) => {
this.setList(list);
this.loading.set(false);
},
error: (error: unknown) => {
this.errorMessage.set(getAuthErrorMessage(error));
this.loading.set(false);
},
});
}
protected saveList(): void {
const listId = this.listId();
if (this.listForm.invalid) {
this.listForm.markAllAsTouched();
return;
}
const formValue = this.listForm.getRawValue();
const payload = {
name: formValue.name.trim(),
description: formValue.description.trim() || undefined,
};
const saveRequest =
this.isCreateMode() || !listId
? this.listsService.createList(payload)
: this.listsService.updateList(listId, payload);
this.saving.set(true);
saveRequest.pipe(finalize(() => this.saving.set(false))).subscribe({
next: (list) => {
this.setList(list);
if (this.isCreateMode()) {
this.isCreateMode.set(false);
void this.router.navigate(['/lists', list.id], { replaceUrl: true });
}
this.snackBar.open('Liste gespeichert.', 'OK', { duration: 2500 });
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', { duration: 5000 });
},
});
}
protected addItem(): void {
const listId = this.listId();
if (!listId || !this.canEditItems() || this.itemForm.invalid) {
this.itemForm.markAllAsTouched();
return;
}
const formValue = this.itemForm.getRawValue();
this.addingItem.set(true);
this.listsService
.addItem(listId, {
title: formValue.title.trim(),
required: formValue.required,
})
.pipe(finalize(() => this.addingItem.set(false)))
.subscribe({
next: (list) => {
this.setList(list);
this.itemForm.reset({ title: '', required: true });
},
error: (error: unknown) => {
this.snackBar.open(getAuthErrorMessage(error), 'OK', { duration: 5000 });
},
});
}
protected toggleItem(item: UserListItem, checked: boolean): void {
const listId = this.listId();
const currentList = this.list();
if (!listId || !currentList || this.updatingItemId()) {
return;
}
this.updatingItemId.set(item.id);
this.list.set({
...currentList,
items: currentList.items.map((existingItem) =>
existingItem.id === item.id ? { ...existingItem, checked } : existingItem,
),
});
this.listsService
.updateItem(listId, item.id, { checked })
.pipe(finalize(() => this.updatingItemId.set(null)))
.subscribe({
next: (list) => {
this.setList(list);
},
error: (error: unknown) => {
this.list.set(currentList);
this.snackBar.open(getAuthErrorMessage(error), 'OK', { duration: 5000 });
},
});
}
protected checkedCount(list: UserList): number {
return list.items.filter((item) => item.checked).length;
}
protected async backToLists(): Promise<void> {
await this.router.navigateByUrl('/lists');
}
private setList(list: UserList): void {
this.list.set(list);
this.listForm.reset({
name: list.name,
description: list.description ?? '',
});
}
private listId(): string | null {
return this.route.snapshot.paramMap.get('listId');
}
}