Files
listify/listify-client/src/app/lists/list-detail/list-detail.component.html
Bastian Wagner 3998923693 list suggestions
2026-06-15 14:42:58 +02:00

388 lines
15 KiB
HTML

<section class="list-detail-page">
<header class="detail-header">
<button mat-icon-button type="button" aria-label="Zurück" (click)="backToLists()">
<mat-icon aria-hidden="true">arrow_back</mat-icon>
</button>
<div>
<h1>{{ list()?.name || (isCreateMode() ? 'Neue Liste' : 'Liste') }}</h1>
@if (isCreateMode()) {
<p>Liste anlegen</p>
} @else if (list()) {
<p>
{{ checkedCount(list()!) }} / {{ list()!.items.length }} erledigt
@if (list()!.accessRole === 'collaborator') {
- geteilt von {{ list()!.ownerName || list()!.ownerEmail || 'Owner' }}
}
</p>
}
</div>
</header>
@if (loading()) {
<mat-card class="state-card" appearance="outlined">
<mat-card-content>
<mat-progress-spinner mode="indeterminate" diameter="40" />
<h2>Liste wird geladen</h2>
</mat-card-content>
</mat-card>
} @else if (errorMessage()) {
<mat-card class="state-card error-state" appearance="outlined">
<mat-card-content>
<mat-icon aria-hidden="true">error</mat-icon>
<h2>Liste konnte nicht geladen werden</h2>
<p>{{ errorMessage() }}</p>
<button mat-stroked-button type="button" (click)="loadList()">
<mat-icon aria-hidden="true">refresh</mat-icon>
Erneut laden
</button>
</mat-card-content>
</mat-card>
} @else {
<mat-card class="editor-card" appearance="outlined">
<mat-card-header>
<mat-card-title>Details</mat-card-title>
@if (!isCreateMode()) {
<div class="detail-actions">
<button
mat-stroked-button
type="button"
[disabled]="creatingTemplate()"
(click)="createTemplateFromList()"
>
@if (creatingTemplate()) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">content_copy</mat-icon>
}
Template
</button>
<button mat-stroked-button type="button" (click)="showEditor() ? cancelEditing() : startEditing()">
<mat-icon aria-hidden="true">{{ showEditor() ? 'close' : 'edit' }}</mat-icon>
{{ showEditor() ? 'Abbrechen' : 'Bearbeiten' }}
</button>
@if (canDeleteList()) {
<button
mat-stroked-button
class="delete-list-button"
type="button"
color="warn"
[disabled]="deletingList()"
(click)="deleteList()"
>
@if (deletingList()) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">delete</mat-icon>
}
Loeschen
</button>
}
</div>
}
</mat-card-header>
<mat-card-content>
@if (showEditor()) {
<form [formGroup]="listForm" class="list-form" (ngSubmit)="saveList()">
<mat-form-field appearance="outline">
<mat-label>Titel</mat-label>
<input matInput formControlName="name" autocomplete="off" />
@if (listForm.controls.name.hasError('required')) {
<mat-error>Titel ist erforderlich.</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Beschreibung</mat-label>
<textarea matInput formControlName="description" rows="4"></textarea>
</mat-form-field>
<div class="list-form-actions" [class.create-actions]="isCreateMode()">
<button mat-flat-button type="submit" [disabled]="saving() || creatingWithAi()">
@if (saving()) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">save</mat-icon>
}
{{ isCreateMode() ? 'Liste anlegen' : 'Speichern' }}
</button>
@if (isCreateMode()) {
<button
mat-stroked-button
type="button"
[disabled]="saving() || creatingWithAi() || !onlineStatus.online()"
(click)="createListWithAi()"
>
@if (creatingWithAi()) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">auto_awesome</mat-icon>
}
Liste mit KI erstellen
</button>
}
</div>
</form>
} @else {
<div class="list-summary">
<p>{{ list()?.description || 'Keine Beschreibung hinterlegt.' }}</p>
</div>
}
</mat-card-content>
</mat-card>
@if (list() && (showEditor() || list()!.collaborators.length > 0 || list()!.accessRole === 'collaborator')) {
<mat-card class="sharing-card" appearance="outlined">
<mat-card-header>
<mat-card-title>Freigaben</mat-card-title>
<mat-card-subtitle>
@if (canManageShares()) {
{{ list()!.collaborators.length }} Mitwirkende
} @else {
Geteilt von {{ list()!.ownerName || list()!.ownerEmail || 'Owner' }}
}
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
@if (showShareControls()) {
<mat-form-field appearance="outline" class="share-search-field">
<mat-label>User suchen</mat-label>
<mat-icon matPrefix aria-hidden="true">person_search</mat-icon>
<input
matInput
type="search"
[value]="shareSearchTerm()"
(input)="searchShareUsers($any($event.target).value)"
autocomplete="off"
/>
@if (searchingUsers()) {
<mat-progress-spinner matSuffix mode="indeterminate" diameter="18" />
}
</mat-form-field>
@if (availableShareSearchResults().length > 0) {
<ul class="share-results">
@for (user of availableShareSearchResults(); track user.id) {
<li>
<span>{{ displayUser(user) }}</span>
<button
mat-stroked-button
type="button"
[disabled]="sharingUserId() === user.id"
(click)="shareWithUser(user)"
>
@if (sharingUserId() === user.id) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">person_add</mat-icon>
}
Hinzufuegen
</button>
</li>
}
</ul>
} @else if (shareSearchTerm().trim().length >= 2 && !searchingUsers()) {
<div class="inline-empty">
<mat-icon aria-hidden="true">person_off</mat-icon>
<span>Keine passenden User gefunden.</span>
</div>
}
}
@if (list()!.collaborators.length > 0) {
<ul class="collaborator-list">
@for (collaborator of list()!.collaborators; track collaborator.id) {
<li>
<div>
<strong>{{ collaborator.name || collaborator.email }}</strong>
@if (collaborator.name) {
<span>{{ collaborator.email }}</span>
}
</div>
@if (showShareControls()) {
<button
mat-icon-button
type="button"
[attr.aria-label]="displayUser(collaborator) + ' entfernen'"
[disabled]="removingShareUserId() === collaborator.id"
(click)="removeCollaborator(collaborator.id)"
>
@if (removingShareUserId() === collaborator.id) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">person_remove</mat-icon>
}
</button>
}
</li>
}
</ul>
} @else {
<div class="inline-empty">
<mat-icon aria-hidden="true">group</mat-icon>
<span>Noch keine Mitwirkenden.</span>
</div>
}
</mat-card-content>
</mat-card>
}
<mat-card class="items-card" appearance="outlined">
<mat-card-header>
<mat-card-title>Items</mat-card-title>
<mat-card-subtitle>
@if (canEditItems()) {
{{ list()?.items?.length || 0 }} Einträge
} @else {
Nach dem Speichern verfügbar
}
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
@if (showEditor()) {
<form [formGroup]="itemForm" class="item-form" (ngSubmit)="addItem()">
<mat-form-field appearance="outline">
<mat-label>Neues Item</mat-label>
<input matInput formControlName="title" autocomplete="off" [disabled]="!canEditItems()" />
@if (itemForm.controls.title.hasError('required')) {
<mat-error>Item-Titel ist erforderlich.</mat-error>
}
</mat-form-field>
<mat-checkbox formControlName="required">Pflicht</mat-checkbox>
<button mat-flat-button type="submit" [disabled]="addingItem() || !canEditItems()">
@if (addingItem()) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">add</mat-icon>
}
Hinzufügen
</button>
</form>
@if (canEditItems()) {
<section class="suggestions-panel" aria-label="Smart Suggestions">
<div class="suggestions-header">
<div>
<h3>Smart Suggestions</h3>
<p>Passende Items basierend auf Name, Beschreibung und Inhalt.</p>
</div>
<button
mat-stroked-button
type="button"
[disabled]="loadingSuggestions() || !onlineStatus.online()"
(click)="loadSuggestions()"
>
@if (loadingSuggestions()) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">auto_awesome</mat-icon>
}
Vorschlaege
</button>
</div>
@if (itemSuggestions().length > 0) {
<ul class="suggestion-list">
@for (suggestion of itemSuggestions(); track suggestion.title) {
<li>
<div>
<strong>{{ suggestion.title }}</strong>
@if (suggestion.notes || suggestion.quantity || !suggestion.required) {
<span>
@if (suggestion.quantity) {
Menge: {{ suggestion.quantity }}
}
@if (suggestion.notes) {
{{ suggestion.quantity ? '- ' : '' }}{{ suggestion.notes }}
}
@if (!suggestion.required) {
{{ suggestion.quantity || suggestion.notes ? '- ' : '' }}Optional
}
</span>
}
</div>
<button
mat-icon-button
type="button"
[attr.aria-label]="suggestion.title + ' hinzufuegen'"
[disabled]="addingSuggestionTitle() === suggestion.title"
(click)="addSuggestion(suggestion)"
>
@if (addingSuggestionTitle() === suggestion.title) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
} @else {
<mat-icon aria-hidden="true">add</mat-icon>
}
</button>
</li>
}
</ul>
} @else if (suggestionsLoaded()) {
<div class="inline-empty">
<mat-icon aria-hidden="true">tips_and_updates</mat-icon>
<span>Keine neuen Vorschlaege gefunden.</span>
</div>
}
</section>
}
}
@if (!canEditItems()) {
<div class="inline-empty">
<mat-icon aria-hidden="true">save</mat-icon>
<span>Speichere die Liste, bevor du Items hinzufügst.</span>
</div>
} @else if (list()?.items?.length) {
<ul class="check-items">
@for (item of visibleItems(list()!); track item.id) {
<li [class.checked]="item.checked">
<mat-checkbox
[checked]="item.checked"
[disabled]="updatingItemId() === item.id"
(change)="toggleItem(item, $event.checked)"
>
<span class="item-title">{{ item.title }}</span>
</mat-checkbox>
@if (updatingItemId() === item.id) {
<mat-progress-spinner mode="indeterminate" diameter="18" />
}
@if (item.checked && item.checkedAt && item.checkedByName) {
<div class="check-meta">
<mat-icon aria-hidden="true">verified</mat-icon>
<span>
Abgehakt von {{ item.checkedByName }} am
{{ item.checkedAt | date: 'dd.MM.yyyy, HH:mm' }}
</span>
</div>
}
</li>
}
</ul>
} @else {
<div class="inline-empty">
<mat-icon aria-hidden="true">playlist_add</mat-icon>
<span>Noch keine Items.</span>
</div>
}
</mat-card-content>
</mat-card>
<a mat-button routerLink="/lists" class="secondary-back">
<mat-icon aria-hidden="true">arrow_back</mat-icon>
Zur Listenübersicht
</a>
}
</section>