generated from bastian/boilerplate
Initial commit
This commit is contained in:
169
apps/frontend/src/app/features/items/items.page.ts
Normal file
169
apps/frontend/src/app/features/items/items.page.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ApiClientService, type ApiErrorBody, type ItemDto } from '@boilerplate/api-client';
|
||||
import { UiPaginationComponent } from '../../shared/ui';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [ReactiveFormsModule, UiPaginationComponent],
|
||||
template: `
|
||||
<form class="ui-toolbar item-toolbar" (ngSubmit)="searchItems()">
|
||||
<label class="ui-form-field">
|
||||
<span class="ui-label">Suche</span>
|
||||
<input class="ui-control" [formControl]="search" placeholder="Item suchen" />
|
||||
</label>
|
||||
<button class="ui-button ui-button--primary" type="submit">Suchen</button>
|
||||
</form>
|
||||
|
||||
@if (error()) {
|
||||
<p class="ui-notice ui-notice--error">{{ error() }}</p>
|
||||
}
|
||||
|
||||
<section class="ui-grid item-grid">
|
||||
@for (item of items(); track item.id) {
|
||||
<button class="ui-card ui-card--interactive item-card" type="button" (click)="edit(item)">
|
||||
<strong>{{ item.name }}</strong>
|
||||
<span class="ui-help-text">{{ item.description || 'Keine Beschreibung' }}</span>
|
||||
<small class="ui-meta">{{ item.status }} - Version {{ item.version }}</small>
|
||||
</button>
|
||||
}
|
||||
</section>
|
||||
|
||||
@if (total() > 0) {
|
||||
<ui-pagination
|
||||
[page]="page()"
|
||||
[pageSize]="pageSize"
|
||||
[total]="total()"
|
||||
(pageChange)="goToPage($event)"
|
||||
/>
|
||||
}
|
||||
|
||||
<form class="ui-card ui-form" [formGroup]="form" (ngSubmit)="save()">
|
||||
<h2>{{ selected()?.id ? 'Item bearbeiten' : 'Item erstellen' }}</h2>
|
||||
<label class="ui-form-field">
|
||||
<span class="ui-label">Name</span>
|
||||
<input class="ui-control" formControlName="name" />
|
||||
</label>
|
||||
<label class="ui-form-field">
|
||||
<span class="ui-label">Beschreibung</span>
|
||||
<textarea class="ui-control" formControlName="description"></textarea>
|
||||
</label>
|
||||
<label class="ui-form-field">
|
||||
<span class="ui-label">Status</span>
|
||||
<select class="ui-control" formControlName="status">
|
||||
<option value="draft">Entwurf</option>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="archived">Archiviert</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="ui-actions">
|
||||
<button class="ui-button ui-button--primary" type="submit" [disabled]="form.invalid">
|
||||
Speichern
|
||||
</button>
|
||||
@if (selected(); as item) {
|
||||
<button class="ui-button ui-button--danger" type="button" (click)="delete(item)">
|
||||
Loeschen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.item-toolbar,
|
||||
.item-grid {
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
.item-card {
|
||||
text-align: left;
|
||||
}
|
||||
@media (min-width: 48rem) {
|
||||
.item-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class ItemsPageComponent {
|
||||
private readonly api = inject(ApiClientService);
|
||||
readonly items = signal<ItemDto[]>([]);
|
||||
readonly selected = signal<ItemDto | null>(null);
|
||||
readonly error = signal('');
|
||||
readonly page = signal(1);
|
||||
readonly total = signal(0);
|
||||
readonly pageSize = 20;
|
||||
readonly search = new FormControl('', { nonNullable: true });
|
||||
readonly form = new FormGroup({
|
||||
name: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [
|
||||
(control) => Validators.required(control),
|
||||
(control) => Validators.maxLength(160)(control),
|
||||
],
|
||||
}),
|
||||
description: new FormControl('', { nonNullable: true }),
|
||||
status: new FormControl<ItemDto['status']>('draft', { nonNullable: true }),
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.load();
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.api
|
||||
.items({
|
||||
search: this.search.value,
|
||||
page: this.page(),
|
||||
pageSize: this.pageSize,
|
||||
})
|
||||
.subscribe((page) => {
|
||||
this.items.set(page.items);
|
||||
this.page.set(page.page);
|
||||
this.total.set(page.total);
|
||||
});
|
||||
}
|
||||
|
||||
searchItems(): void {
|
||||
this.page.set(1);
|
||||
this.load();
|
||||
}
|
||||
|
||||
goToPage(page: number): void {
|
||||
this.page.set(page);
|
||||
this.load();
|
||||
}
|
||||
|
||||
edit(item: ItemDto): void {
|
||||
this.selected.set(item);
|
||||
this.form.setValue({
|
||||
name: item.name,
|
||||
description: item.description ?? '',
|
||||
status: item.status,
|
||||
});
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.error.set('');
|
||||
const value = this.form.getRawValue();
|
||||
const selected = this.selected();
|
||||
const request = selected
|
||||
? this.api.updateItem(selected.id, { ...value, version: selected.version })
|
||||
: this.api.createItem(value);
|
||||
request.subscribe({
|
||||
next: () => {
|
||||
this.selected.set(null);
|
||||
this.form.reset({ name: '', description: '', status: 'draft' });
|
||||
this.load();
|
||||
},
|
||||
error: (err: { error?: ApiErrorBody }) =>
|
||||
this.error.set(err.error?.message ?? 'Speichern fehlgeschlagen.'),
|
||||
});
|
||||
}
|
||||
|
||||
delete(item: ItemDto): void {
|
||||
if (confirm('Item wirklich loeschen?')) {
|
||||
this.api.deleteItem(item.id, item.version).subscribe(() => this.load());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user