Files
listify/listify-client/src/app/lists/lists.service.ts
Bastian Wagner 671fd62ad8 mcp
2026-06-26 10:13:31 +02:00

318 lines
9.1 KiB
TypeScript

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<UserList[]> {
if (!this.onlineStatus.online()) {
return of(this.readCachedLists());
}
return this.http
.get<UserList[]>(this.apiUrl)
.pipe(tap((lists) => this.writeCachedLists(lists)));
}
getList(listId: string): Observable<UserList> {
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<UserList>(`${this.apiUrl}/${listId}`)
.pipe(tap((list) => this.upsertCachedList(list)));
}
createList(data: CreateListRequest): Observable<UserList> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('Neue Listen koennen offline noch nicht erstellt werden.'),
);
}
return this.http.post<UserList>(this.apiUrl, data);
}
createListWithItemSuggestions(
data: CreateListRequest,
): Observable<CreateListWithItemSuggestionsResponse> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('Listen mit KI koennen nur online erstellt werden.'),
);
}
return this.http
.post<CreateListWithItemSuggestionsResponse>(
`${this.apiUrl}/with-item-suggestions`,
data,
)
.pipe(tap((response) => this.upsertCachedList(response.list)));
}
updateList(listId: string, data: UpdateListRequest): Observable<UserList> {
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<UserList>(`${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<UserList> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('Freigaben koennen offline nicht geaendert werden.'),
);
}
return this.http.post<UserList>(`${this.apiUrl}/${listId}/shares`, { userId });
}
removeShare(listId: string, userId: string): Observable<UserList> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('Freigaben koennen offline nicht geaendert werden.'),
);
}
return this.http.delete<UserList>(`${this.apiUrl}/${listId}/shares/${userId}`);
}
addItem(listId: string, data: AddListItemRequest): Observable<UserList> {
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<UserList>(`${this.apiUrl}/${listId}/items`, data)
.pipe(tap((list) => this.upsertCachedList(list)));
}
suggestItems(listId: string): Observable<ListItemSuggestionsResponse> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('Smart Suggestions sind nur online verfuegbar.'),
);
}
return this.http.post<ListItemSuggestionsResponse>(
`${this.apiUrl}/${listId}/item-suggestions`,
{},
);
}
suggestCleanup(listId: string): Observable<ListCleanupSuggestionsResponse> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('AI Cleanup ist nur online verfuegbar.'),
);
}
return this.http.post<ListCleanupSuggestionsResponse>(
`${this.apiUrl}/${listId}/cleanup-suggestions`,
{},
);
}
applyCleanup(listId: string): Observable<ApplyListCleanupResponse> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('AI Cleanup ist nur online verfuegbar.'),
);
}
return this.http
.post<ApplyListCleanupResponse>(
`${this.apiUrl}/${listId}/apply-cleanup`,
{},
)
.pipe(tap((response) => this.upsertCachedList(response.list)));
}
createTemplateFromList(listId: string): Observable<ListTemplate> {
if (!this.onlineStatus.online()) {
return throwError(
() => new Error('Templates koennen nur online aus Listen erstellt werden.'),
);
}
return this.http.post<ListTemplate>(`${this.apiUrl}/${listId}/template`, {});
}
updateItem(
listId: string,
itemId: string,
data: UpdateListItemRequest,
): Observable<UserList> {
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<UserList>(`${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),
);
}
}