import { HttpClient } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; import { Observable } from 'rxjs'; import { of, tap, throwError } from 'rxjs'; import { AddListItemRequest, ApplyListCleanupResponse, CreateListRequest, CreateListWithItemSuggestionsResponse, ListCleanupSuggestionsResponse, ListItemSuggestionsResponse, UpdateListItemRequest, UpdateListRequest, UserList, } from './lists.models'; import { ListTemplate } from '../templates/templates.models'; import { OfflineSyncService } from '../offline/offline-sync.service'; import { OnlineStatusService } from '../offline/online-status.service'; const LIST_CACHE_KEY = 'listify.lists.cache.v1'; @Injectable({ providedIn: 'root' }) export class ListsService { private readonly http = inject(HttpClient); private readonly offlineSync = inject(OfflineSyncService); private readonly onlineStatus = inject(OnlineStatusService); private readonly apiUrl = '/api/lists'; listLists(): Observable { if (!this.onlineStatus.online()) { return of(this.readCachedLists()); } return this.http .get(this.apiUrl) .pipe(tap((lists) => this.writeCachedLists(lists))); } getList(listId: string): Observable { if (!this.onlineStatus.online()) { const cachedList = this.readCachedLists().find((list) => list.id === listId); if (cachedList) { return of(cachedList); } return throwError(() => new Error('Liste ist offline nicht im Cache.')); } return this.http .get(`${this.apiUrl}/${listId}`) .pipe(tap((list) => this.upsertCachedList(list))); } createList(data: CreateListRequest): Observable { if (!this.onlineStatus.online()) { return throwError( () => new Error('Neue Listen koennen offline noch nicht erstellt werden.'), ); } return this.http.post(this.apiUrl, data); } createListWithItemSuggestions( data: CreateListRequest, ): Observable { if (!this.onlineStatus.online()) { return throwError( () => new Error('Listen mit KI koennen nur online erstellt werden.'), ); } return this.http .post( `${this.apiUrl}/with-item-suggestions`, data, ) .pipe(tap((response) => this.upsertCachedList(response.list))); } updateList(listId: string, data: UpdateListRequest): Observable { if (!this.onlineStatus.online()) { const updatedList = this.updateCachedList(listId, (list) => ({ ...list, ...data, updatedAt: new Date().toISOString(), })); this.offlineSync.enqueueUpdateList(listId, data); return updatedList ? of(updatedList) : throwError(() => new Error('Liste ist offline nicht im Cache.')); } return this.http .patch(`${this.apiUrl}/${listId}`, data) .pipe(tap((list) => this.upsertCachedList(list))); } deleteList(listId: string): Observable<{ message: string }> { if (!this.onlineStatus.online()) { this.removeCachedList(listId); this.offlineSync.enqueueDeleteList(listId); return of({ message: 'List queued for deletion.' }); } return this.http .delete<{ message: string }>(`${this.apiUrl}/${listId}`) .pipe(tap(() => this.removeCachedList(listId))); } shareList(listId: string, userId: string): Observable { if (!this.onlineStatus.online()) { return throwError( () => new Error('Freigaben koennen offline nicht geaendert werden.'), ); } return this.http.post(`${this.apiUrl}/${listId}/shares`, { userId }); } removeShare(listId: string, userId: string): Observable { if (!this.onlineStatus.online()) { return throwError( () => new Error('Freigaben koennen offline nicht geaendert werden.'), ); } return this.http.delete(`${this.apiUrl}/${listId}/shares/${userId}`); } addItem(listId: string, data: AddListItemRequest): Observable { if (!this.onlineStatus.online()) { const updatedList = this.updateCachedList(listId, (list) => ({ ...list, items: [ ...list.items, { id: `offline-${Date.now()}-${Math.random().toString(16).slice(2)}`, title: data.title, notes: data.notes, quantity: data.quantity, required: data.required ?? true, checked: false, position: list.items.length, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, ], updatedAt: new Date().toISOString(), })); this.offlineSync.enqueueAddItem(listId, data); return updatedList ? of(updatedList) : throwError(() => new Error('Liste ist offline nicht im Cache.')); } return this.http .post(`${this.apiUrl}/${listId}/items`, data) .pipe(tap((list) => this.upsertCachedList(list))); } suggestItems(listId: string): Observable { if (!this.onlineStatus.online()) { return throwError( () => new Error('Smart Suggestions sind nur online verfuegbar.'), ); } return this.http.post( `${this.apiUrl}/${listId}/item-suggestions`, {}, ); } suggestCleanup(listId: string): Observable { if (!this.onlineStatus.online()) { return throwError( () => new Error('AI Cleanup ist nur online verfuegbar.'), ); } return this.http.post( `${this.apiUrl}/${listId}/cleanup-suggestions`, {}, ); } applyCleanup(listId: string): Observable { if (!this.onlineStatus.online()) { return throwError( () => new Error('AI Cleanup ist nur online verfuegbar.'), ); } return this.http .post( `${this.apiUrl}/${listId}/apply-cleanup`, {}, ) .pipe(tap((response) => this.upsertCachedList(response.list))); } createTemplateFromList(listId: string): Observable { if (!this.onlineStatus.online()) { return throwError( () => new Error('Templates koennen nur online aus Listen erstellt werden.'), ); } return this.http.post(`${this.apiUrl}/${listId}/template`, {}); } updateItem( listId: string, itemId: string, data: UpdateListItemRequest, ): Observable { if (!this.onlineStatus.online()) { const updatedList = this.updateCachedList(listId, (list) => ({ ...list, items: list.items.map((item) => item.id === itemId ? { ...item, ...data, checkedAt: data.checked === true ? new Date().toISOString() : data.checked === false ? undefined : item.checkedAt, updatedAt: new Date().toISOString(), } : item, ), updatedAt: new Date().toISOString(), })); if (!itemId.startsWith('offline-')) { this.offlineSync.enqueueUpdateItem(listId, itemId, data); } return updatedList ? of(updatedList) : throwError(() => new Error('Liste ist offline nicht im Cache.')); } return this.http .patch(`${this.apiUrl}/${listId}/items/${itemId}`, data) .pipe(tap((list) => this.upsertCachedList(list))); } private readCachedLists(): UserList[] { if (typeof localStorage === 'undefined') { return []; } try { const value = localStorage.getItem(LIST_CACHE_KEY); return value ? (JSON.parse(value) as UserList[]) : []; } catch { return []; } } private writeCachedLists(lists: UserList[]): void { if (typeof localStorage === 'undefined') { return; } localStorage.setItem(LIST_CACHE_KEY, JSON.stringify(lists)); } private upsertCachedList(list: UserList): void { const lists = this.readCachedLists(); const existingIndex = lists.findIndex((existingList) => existingList.id === list.id); if (existingIndex === -1) { this.writeCachedLists([...lists, list]); return; } this.writeCachedLists( lists.map((existingList, index) => (index === existingIndex ? list : existingList)), ); } private updateCachedList( listId: string, updater: (list: UserList) => UserList, ): UserList | null { const lists = this.readCachedLists(); let updatedList: UserList | null = null; this.writeCachedLists( lists.map((list) => { if (list.id !== listId) { return list; } updatedList = updater(list); return updatedList; }), ); return updatedList; } private removeCachedList(listId: string): void { this.writeCachedLists( this.readCachedLists().filter((list) => list.id !== listId), ); } }