diff --git a/listify-client/src/app/lists/list-cleanup-result-dialog/list-cleanup-result-dialog.component.html b/listify-client/src/app/lists/list-cleanup-result-dialog/list-cleanup-result-dialog.component.html new file mode 100644 index 0000000..d9a6b7b --- /dev/null +++ b/listify-client/src/app/lists/list-cleanup-result-dialog/list-cleanup-result-dialog.component.html @@ -0,0 +1,146 @@ +
+ +
+

AI Cleanup angewendet

+

{{ data.response.list.name }}

+
+
+ + +
+ @for (item of summaryItems; track item.label) { +
+ + {{ item.value }} + {{ item.label }} +
+ } +
+ + @if (!hasChanges()) { +
+ +

Die KI hat keine sinnvollen Aenderungen gefunden.

+
+ } + + @if (data.response.cleanup.listUpdate) { +
+

Listendetails

+
+ @if (data.response.cleanup.listUpdate.name) { +
+ Titel +

{{ data.before?.name || '-' }}

+ +

{{ data.response.list.name }}

+
+ } + + @if (data.response.cleanup.listUpdate.description) { +
+ Beschreibung +

{{ data.before?.description || '-' }}

+ +

{{ data.response.list.description || '-' }}

+
+ } + + @if (data.response.cleanup.listUpdate.reason) { +

{{ data.response.cleanup.listUpdate.reason }}

+ } +
+
+ } + + @if (data.response.cleanup.itemUpdates.length > 0) { +
+

Verbesserte Items

+
+ @for (update of data.response.cleanup.itemUpdates; track update.itemId) { +
+
+ + {{ itemTitle(update.itemId) }} +
+ + @for (change of itemChanges(update); track change.label) { +
+ {{ change.label }} +

{{ change.before }}

+ +

{{ change.after }}

+
+ } + + @if (update.reason) { +

{{ update.reason }}

+ } +
+ } +
+
+ } + + @if (data.response.cleanup.duplicateDeletions.length > 0) { +
+

Entfernte Duplikate

+
+ @for (deletion of data.response.cleanup.duplicateDeletions; track deletion.itemId) { +
+
+ + {{ itemTitle(deletion.itemId) }} +
+

+ Entfernt, weil {{ itemTitle(deletion.keptItemId) }} + erhalten bleibt. +

+ @if (deletion.reason) { +

{{ deletion.reason }}

+ } +
+ } +
+
+ } + + @if (data.response.cleanup.itemAdditions.length > 0) { +
+

Neue Items

+
+ @for (addition of data.response.cleanup.itemAdditions; track addition.title) { +
+
+ + {{ addition.title }} +
+ @if (addition.notes || addition.quantity || !addition.required) { +

+ @if (addition.quantity) { + Menge: {{ addition.quantity }} + } + @if (addition.notes) { + {{ addition.quantity ? '- ' : '' }}{{ addition.notes }} + } + @if (!addition.required) { + {{ addition.quantity || addition.notes ? '- ' : '' }}Optional + } +

+ } + @if (addition.reason) { +

{{ addition.reason }}

+ } +
+ } +
+
+ } +
+ + + + diff --git a/listify-client/src/app/lists/list-cleanup-result-dialog/list-cleanup-result-dialog.component.scss b/listify-client/src/app/lists/list-cleanup-result-dialog/list-cleanup-result-dialog.component.scss new file mode 100644 index 0000000..081c55e --- /dev/null +++ b/listify-client/src/app/lists/list-cleanup-result-dialog/list-cleanup-result-dialog.component.scss @@ -0,0 +1,192 @@ +.cleanup-dialog-heading { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 0.75rem; + padding: 1rem 1.5rem 0; +} + +.cleanup-dialog-heading > mat-icon { + width: 42px; + height: 42px; + color: var(--mat-sys-primary); + font-size: 42px; +} + +.cleanup-dialog-heading h2, +.cleanup-dialog-heading p { + margin: 0; +} + +.cleanup-dialog-heading p { + overflow: hidden; + color: var(--mat-sys-on-surface-variant); + text-overflow: ellipsis; + white-space: nowrap; +} + +mat-dialog-content { + display: grid; + gap: 1rem; + min-width: min(760px, calc(100vw - 64px)); + max-height: min(72vh, 760px); + padding-top: 1rem; +} + +.summary-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.75rem; +} + +.summary-tile { + display: grid; + justify-items: center; + gap: 0.25rem; + min-width: 0; + padding: 0.75rem 0.5rem; + border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 72%, transparent); + border-radius: 8px; + background: color-mix(in srgb, var(--mat-sys-primary) 7%, var(--mat-sys-surface)); + text-align: center; +} + +.summary-tile mat-icon { + color: var(--mat-sys-primary); +} + +.summary-tile strong { + font-size: 1.35rem; +} + +.summary-tile span { + color: var(--mat-sys-on-surface-variant); + font-size: 0.82rem; +} + +.change-section { + display: grid; + gap: 0.6rem; +} + +.change-section h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; +} + +.change-list { + display: grid; + gap: 0.65rem; +} + +.change-card { + display: grid; + gap: 0.55rem; + min-width: 0; + padding: 0.8rem; + border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 74%, transparent); + border-radius: 8px; + background: var(--mat-sys-surface); +} + +.change-card header { + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 0; +} + +.change-card header strong { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.change-card header mat-icon { + color: var(--mat-sys-primary); +} + +.change-card p { + margin: 0; + overflow-wrap: anywhere; +} + +.field-change { + display: grid; + grid-template-columns: 7rem minmax(0, 1fr) auto minmax(0, 1fr); + align-items: start; + gap: 0.5rem; +} + +.field-change span { + color: var(--mat-sys-on-surface-variant); + font-size: 0.83rem; +} + +.field-change mat-icon { + width: 18px; + height: 18px; + color: var(--mat-sys-outline); + font-size: 18px; +} + +.before, +.after { + min-width: 0; + padding: 0.45rem 0.55rem; + border-radius: 6px; + overflow-wrap: anywhere; +} + +.before { + background: color-mix(in srgb, var(--mat-sys-error) 7%, var(--mat-sys-surface)); +} + +.after { + background: color-mix(in srgb, var(--mat-sys-primary) 8%, var(--mat-sys-surface)); +} + +.reason { + color: var(--mat-sys-on-surface-variant); + font-size: 0.88rem; +} + +.duplicate-card { + border-color: color-mix(in srgb, var(--mat-sys-error) 32%, var(--mat-sys-outline-variant)); +} + +.addition-card { + border-color: color-mix(in srgb, var(--mat-sys-primary) 32%, var(--mat-sys-outline-variant)); +} + +.empty-state { + display: grid; + justify-items: center; + gap: 0.5rem; + padding: 1.5rem 1rem; + color: var(--mat-sys-on-surface-variant); + text-align: center; +} + +.empty-state mat-icon { + color: var(--mat-sys-primary); +} + +@media (max-width: 700px) { + mat-dialog-content { + min-width: 0; + } + + .summary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .field-change { + grid-template-columns: 1fr; + } + + .field-change mat-icon { + transform: rotate(90deg); + } +} diff --git a/listify-client/src/app/lists/list-cleanup-result-dialog/list-cleanup-result-dialog.component.ts b/listify-client/src/app/lists/list-cleanup-result-dialog/list-cleanup-result-dialog.component.ts new file mode 100644 index 0000000..1a733ba --- /dev/null +++ b/listify-client/src/app/lists/list-cleanup-result-dialog/list-cleanup-result-dialog.component.ts @@ -0,0 +1,135 @@ +import { Component, inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { + ApplyListCleanupResponse, + ListCleanupItemUpdate, + UserList, + UserListItem, +} from '../lists.models'; + +export interface ListCleanupResultDialogData { + before: UserList | null; + response: ApplyListCleanupResponse; +} + +@Component({ + selector: 'app-list-cleanup-result-dialog', + imports: [MatButtonModule, MatDialogModule, MatIconModule], + templateUrl: './list-cleanup-result-dialog.component.html', + styleUrl: './list-cleanup-result-dialog.component.scss', +}) +export class ListCleanupResultDialogComponent { + protected readonly data = inject(MAT_DIALOG_DATA); + + protected readonly summaryItems = [ + { + icon: 'edit_note', + label: 'Details', + value: this.data.response.summary.listUpdated ? 1 : 0, + }, + { + icon: 'rule', + label: 'Verbessert', + value: this.data.response.summary.itemsUpdated, + }, + { + icon: 'content_copy_off', + label: 'Duplikate', + value: this.data.response.summary.duplicatesDeleted, + }, + { + icon: 'playlist_add', + label: 'Neu', + value: this.data.response.summary.itemsAdded, + }, + ]; + + protected hasChanges(): boolean { + const summary = this.data.response.summary; + + return ( + summary.listUpdated || + summary.itemsUpdated > 0 || + summary.duplicatesDeleted > 0 || + summary.itemsAdded > 0 + ); + } + + protected beforeItem(itemId: string): UserListItem | undefined { + return this.data.before?.items.find((item) => item.id === itemId); + } + + protected afterItem(itemId: string): UserListItem | undefined { + return this.data.response.list.items.find((item) => item.id === itemId); + } + + protected itemTitle(itemId: string): string { + return ( + this.beforeItem(itemId)?.title ?? + this.afterItem(itemId)?.title ?? + 'Unbekanntes Item' + ); + } + + protected itemChanges(update: ListCleanupItemUpdate): Array<{ + label: string; + before: string; + after: string; + }> { + const before = this.beforeItem(update.itemId); + const after = this.afterItem(update.itemId); + + return [ + update.title !== undefined + ? { + label: 'Titel', + before: before?.title ?? '-', + after: after?.title ?? update.title, + } + : null, + update.notes !== undefined + ? { + label: 'Notizen', + before: before?.notes ?? '-', + after: after?.notes ?? update.notes, + } + : null, + update.quantity !== undefined + ? { + label: 'Menge', + before: this.displayQuantity(before?.quantity), + after: this.displayQuantity(after?.quantity ?? update.quantity), + } + : null, + update.required !== undefined + ? { + label: 'Pflicht', + before: this.displayBoolean(before?.required), + after: this.displayBoolean(after?.required ?? update.required), + } + : null, + ].filter( + ( + change, + ): change is { + label: string; + before: string; + after: string; + } => change !== null, + ); + } + + protected displayQuantity(value: number | undefined): string { + return typeof value === 'number' ? String(value) : '-'; + } + + protected displayBoolean(value: boolean | undefined): string { + if (value === undefined) { + return '-'; + } + + return value ? 'Ja' : 'Nein'; + } +} diff --git a/listify-client/src/app/lists/list-detail/list-detail.component.ts b/listify-client/src/app/lists/list-detail/list-detail.component.ts index 2f35ec8..077df21 100644 --- a/listify-client/src/app/lists/list-detail/list-detail.component.ts +++ b/listify-client/src/app/lists/list-detail/list-detail.component.ts @@ -346,6 +346,7 @@ export class ListDetailComponent implements OnInit { protected applyCleanup(): void { const listId = this.currentListId(); + const listBeforeCleanup = this.list(); if (!listId || this.cleaningList()) { return; @@ -360,9 +361,7 @@ export class ListDetailComponent implements OnInit { this.setList(response.list, !this.showEditor()); this.itemSuggestions.set([]); this.suggestionsLoaded.set(false); - this.snackBar.open(this.cleanupSummary(response), 'OK', { - duration: 4000, - }); + void this.openCleanupResultDialog(listBeforeCleanup, response); }, error: (error: unknown) => { this.snackBar.open(getAuthErrorMessage(error), 'OK', { duration: 5000 }); @@ -644,22 +643,21 @@ export class ListDetailComponent implements OnInit { return value.trim().replace(/\s+/g, ' ').toLowerCase(); } - private cleanupSummary(response: ApplyListCleanupResponse): string { - const parts = [ - response.summary.listUpdated ? 'Details aktualisiert' : null, - response.summary.itemsUpdated > 0 - ? `${response.summary.itemsUpdated} Items verbessert` - : null, - response.summary.duplicatesDeleted > 0 - ? `${response.summary.duplicatesDeleted} Duplikate entfernt` - : null, - response.summary.itemsAdded > 0 - ? `${response.summary.itemsAdded} Items hinzugefuegt` - : null, - ].filter((part): part is string => part !== null); + private async openCleanupResultDialog( + before: UserList | null, + response: ApplyListCleanupResponse, + ): Promise { + const { ListCleanupResultDialogComponent } = await import( + '../list-cleanup-result-dialog/list-cleanup-result-dialog.component' + ); - return parts.length > 0 - ? `Cleanup angewendet: ${parts.join(', ')}.` - : 'Cleanup geprueft: keine sinnvollen Aenderungen gefunden.'; + this.dialog.open(ListCleanupResultDialogComponent, { + data: { + before, + response, + }, + maxWidth: '900px', + width: 'calc(100vw - 32px)', + }); } }