388 lines
15 KiB
HTML
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>
|