From 5bdacaa5ea468a5fc8ba2adf02aff06d990c864a Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 17 Apr 2026 14:02:37 +0200 Subject: [PATCH] =?UTF-8?q?feat(wishes):=20add=20W=C3=BCnsche=20module=20?= =?UTF-8?q?=E2=80=94=20wishlists=20with=20price=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New module for managing wishes/gift ideas with lists, price targets, product URLs, price history, and AI tools. Includes ListView with filter tabs, inline list management, search, and DetailView with notes and price history. Encrypted at rest (title, description, URLs, notes). Registered in database v24, module-registry, crypto registry, seed registry, tool init, and DnD type system. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/mana/apps/web/src/lib/data/database.ts | 11 + .../apps/web/src/lib/data/seed-registry.ts | 2 + .../src/lib/modules/wishes/ListView.svelte | 341 ++++++++++++++++ .../web/src/lib/modules/wishes/collections.ts | 67 ++++ .../apps/web/src/lib/modules/wishes/index.ts | 38 ++ .../src/lib/modules/wishes/module.config.ts | 10 + .../web/src/lib/modules/wishes/queries.ts | 129 ++++++ .../lib/modules/wishes/stores/lists.svelte.ts | 57 +++ .../wishes/stores/price-checks.svelte.ts | 29 ++ .../modules/wishes/stores/wishes.svelte.ts | 142 +++++++ .../apps/web/src/lib/modules/wishes/tools.ts | 109 +++++ .../apps/web/src/lib/modules/wishes/types.ts | 86 ++++ .../modules/wishes/views/DetailView.svelte | 373 ++++++++++++++++++ .../src/routes/(app)/wishes/+layout.svelte | 7 + .../web/src/routes/(app)/wishes/+page.svelte | 5 + .../src/routes/(app)/wishes/[id]/+page.svelte | 5 + packages/shared-ui/src/dnd/types.ts | 3 +- 17 files changed, 1413 insertions(+), 1 deletion(-) create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/collections.ts create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/index.ts create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/module.config.ts create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/queries.ts create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/stores/lists.svelte.ts create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/stores/price-checks.svelte.ts create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/stores/wishes.svelte.ts create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/tools.ts create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/types.ts create mode 100644 apps/mana/apps/web/src/lib/modules/wishes/views/DetailView.svelte create mode 100644 apps/mana/apps/web/src/routes/(app)/wishes/+layout.svelte create mode 100644 apps/mana/apps/web/src/routes/(app)/wishes/+page.svelte create mode 100644 apps/mana/apps/web/src/routes/(app)/wishes/[id]/+page.svelte diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index a731a31b0..a9a7720d2 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -563,6 +563,17 @@ db.version(23).stores({ userContext: 'id', }); +// v24 — Wishes module: wishlists with price tracking. +// wishesItems indexes [listId+order] for the per-list view, +// status for the active/fulfilled filter tabs. +// wishesPriceChecks indexes [wishId+checkedAt] for the per-wish +// price history timeline (reverse range scan). +db.version(24).stores({ + wishesItems: 'id, listId, status, priority, category, [listId+order], [status+order]', + wishesLists: 'id, order, isArchived', + wishesPriceChecks: 'id, wishId, checkedAt, [wishId+checkedAt]', +}); + // v25 — Wetter module: saved locations and user preferences. db.version(25).stores({ wetterLocations: 'id, isDefault, order', diff --git a/apps/mana/apps/web/src/lib/data/seed-registry.ts b/apps/mana/apps/web/src/lib/data/seed-registry.ts index 88101b892..56da92c86 100644 --- a/apps/mana/apps/web/src/lib/data/seed-registry.ts +++ b/apps/mana/apps/web/src/lib/data/seed-registry.ts @@ -35,6 +35,7 @@ import { MEDITATE_GUEST_SEED } from '$lib/modules/meditate/collections'; import { SLEEP_GUEST_SEED } from '$lib/modules/sleep/collections'; import { MOOD_GUEST_SEED } from '$lib/modules/mood/collections'; import { QUIZ_GUEST_SEED } from '$lib/modules/quiz/collections'; +import { WISHES_GUEST_SEED } from '$lib/modules/wishes/collections'; /** * Flat list of { tableName, rows } entries. Only modules with non-empty @@ -74,6 +75,7 @@ register(MEDITATE_GUEST_SEED); register(SLEEP_GUEST_SEED); register(MOOD_GUEST_SEED); register(QUIZ_GUEST_SEED); +register(WISHES_GUEST_SEED); /** * Seed all module guest data into empty tables. Idempotent: tables diff --git a/apps/mana/apps/web/src/lib/modules/wishes/ListView.svelte b/apps/mana/apps/web/src/lib/modules/wishes/ListView.svelte new file mode 100644 index 000000000..ab784cb22 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/ListView.svelte @@ -0,0 +1,341 @@ + + + + Wünsche - Mana + + +
+ +
+

+ {activeCount} offen · {fulfilledCount} erfüllt + {#if totalCost > 0} + · ~{totalCost.toLocaleString('de-DE')} € + {/if} +

+ +
+ + + {#if showAdd} +
{ + e.preventDefault(); + addWish(); + }} + class="rounded-lg border border-[hsl(var(--color-border))] bg-[hsl(var(--color-card))] p-3" + > +
+ +
+ + + {#if lists.length > 0} + + {/if} +
+
+ + +
+
+
+ {/if} + + +
+ {#each [{ key: 'active', label: 'Offen', icon: Star }, { key: 'fulfilled', label: 'Erfüllt', icon: Check }, { key: 'all', label: 'Alle', icon: Archive }] as tab (tab.key)} + + {/each} +
+ + +
+ {#if lists.length > 0} + + {/if} + {#each lists as list (list.id)} +
+ + +
+ {/each} + {#if showNewList} +
{ + e.preventDefault(); + addList(); + }} + class="flex items-center gap-1" + > + + + +
+ {:else} + + {/if} +
+ + +
+ + +
+ + + {#if filtered.length === 0} +
+ +

+ {filter === 'active' ? 'Noch keine Wünsche' : 'Keine Wünsche in dieser Ansicht'} +

+
+ {:else} +
+ {#each filtered as wish (wish.id)} + + {/each} +
+ {/if} +
diff --git a/apps/mana/apps/web/src/lib/modules/wishes/collections.ts b/apps/mana/apps/web/src/lib/modules/wishes/collections.ts new file mode 100644 index 000000000..3154e9c71 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/collections.ts @@ -0,0 +1,67 @@ +/** + * Wishes module — collection accessors and guest seed data. + * + * Uses prefixed table names in the unified DB: wishesItems, wishesLists, wishesPriceChecks. + */ + +import { db } from '$lib/data/database'; +import type { LocalWish, LocalWishList, LocalPriceCheck } from './types'; + +// ─── Collection Accessors ────────────────────────────────── + +export const wishTable = db.table('wishesItems'); +export const listTable = db.table('wishesLists'); +export const priceCheckTable = db.table('wishesPriceChecks'); + +// ─── Guest Seed ──────────────────────────────────────────── + +const DEMO_LIST_ID = 'demo-birthday-wishes'; + +export const WISHES_GUEST_SEED = { + wishesLists: [ + { + id: DEMO_LIST_ID, + name: 'Geburtstagsgeschenke', + description: 'Ideen für Geburtstag', + icon: '🎁', + color: '#ec4899', + isArchived: false, + order: 0, + }, + ], + wishesItems: [ + { + id: 'demo-wish-1', + title: 'Neue Kopfhörer', + description: 'Wireless, aktive Geräuschunterdrückung', + listId: DEMO_LIST_ID, + priority: 'medium' as const, + status: 'active' as const, + targetPrice: 150, + currency: 'EUR', + productUrls: [], + imageUrl: null, + category: 'Technik', + tags: ['audio', 'tech'], + notes: [], + order: 0, + }, + { + id: 'demo-wish-2', + title: 'Kochbuch — vegetarische Rezepte', + description: 'Am liebsten asiatisch-vegetarisch', + listId: DEMO_LIST_ID, + priority: 'low' as const, + status: 'active' as const, + targetPrice: 25, + currency: 'EUR', + productUrls: [], + imageUrl: null, + category: 'Bücher', + tags: ['books', 'cooking'], + notes: [], + order: 1, + }, + ], + wishesPriceChecks: [] as Record[], +}; diff --git a/apps/mana/apps/web/src/lib/modules/wishes/index.ts b/apps/mana/apps/web/src/lib/modules/wishes/index.ts new file mode 100644 index 000000000..f4d1a75e8 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/index.ts @@ -0,0 +1,38 @@ +/** + * Wishes module — barrel exports. + */ + +// Stores +export { wishesStore } from './stores/wishes.svelte'; +export { listsStore } from './stores/lists.svelte'; +export { priceChecksStore } from './stores/price-checks.svelte'; + +// Queries +export { + useAllWishes, + useAllLists, + usePriceChecks, + toWish, + toWishList, + toPriceCheck, + filterByStatus, + filterByPriority, + filterByList, + searchWishes, + getTotalEstimatedCost, +} from './queries'; + +// Collections +export { wishTable, listTable, priceCheckTable, WISHES_GUEST_SEED } from './collections'; + +// Types +export type { + LocalWish, + LocalWishList, + LocalPriceCheck, + Wish, + WishList, + PriceCheck, + WishStatus, + WishPriority, +} from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/wishes/module.config.ts b/apps/mana/apps/web/src/lib/modules/wishes/module.config.ts new file mode 100644 index 000000000..18ca22346 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/module.config.ts @@ -0,0 +1,10 @@ +import type { ModuleConfig } from '$lib/data/module-registry'; + +export const wishesModuleConfig: ModuleConfig = { + appId: 'wishes', + tables: [ + { name: 'wishesItems', syncName: 'items' }, + { name: 'wishesLists', syncName: 'lists' }, + { name: 'wishesPriceChecks', syncName: 'priceChecks' }, + ], +}; diff --git a/apps/mana/apps/web/src/lib/modules/wishes/queries.ts b/apps/mana/apps/web/src/lib/modules/wishes/queries.ts new file mode 100644 index 000000000..6bcd667f3 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/queries.ts @@ -0,0 +1,129 @@ +/** + * Reactive queries & pure helpers for Wishes — uses Dexie liveQuery on the unified DB. + */ + +import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; +import { db } from '$lib/data/database'; +import { decryptRecords } from '$lib/data/crypto'; +import type { + LocalWish, + LocalWishList, + LocalPriceCheck, + Wish, + WishList, + PriceCheck, +} from './types'; + +// ─── Type Converters ─────────────────────────────────────── + +export function toWish(local: LocalWish): Wish { + return { + id: local.id, + title: local.title, + description: local.description ?? null, + listId: local.listId ?? null, + priority: local.priority, + targetPrice: local.targetPrice ?? null, + currency: local.currency ?? null, + productUrls: local.productUrls ?? [], + imageUrl: local.imageUrl ?? null, + category: local.category ?? null, + status: local.status, + tags: local.tags ?? [], + notes: local.notes ?? [], + order: local.order, + createdAt: local.createdAt ?? new Date().toISOString(), + updatedAt: local.updatedAt ?? new Date().toISOString(), + }; +} + +export function toWishList(local: LocalWishList): WishList { + return { + id: local.id, + name: local.name, + description: local.description ?? null, + icon: local.icon ?? null, + color: local.color ?? null, + isArchived: local.isArchived, + order: local.order, + createdAt: local.createdAt ?? new Date().toISOString(), + updatedAt: local.updatedAt ?? new Date().toISOString(), + }; +} + +export function toPriceCheck(local: LocalPriceCheck): PriceCheck { + return { + id: local.id, + wishId: local.wishId, + url: local.url, + price: local.price, + currency: local.currency, + available: local.available, + checkedAt: local.checkedAt, + createdAt: local.createdAt ?? new Date().toISOString(), + }; +} + +// ─── Live Queries ────────────────────────────────────────── + +export function useAllWishes() { + return useLiveQueryWithDefault(async () => { + const locals = await db.table('wishesItems').orderBy('order').toArray(); + const visible = locals.filter((w) => !w.deletedAt); + const decrypted = await decryptRecords('wishesItems', visible); + return decrypted.map(toWish); + }, [] as Wish[]); +} + +export function useAllLists() { + return useLiveQueryWithDefault(async () => { + const locals = await db.table('wishesLists').orderBy('order').toArray(); + const visible = locals.filter((l) => !l.deletedAt && !l.isArchived); + return visible.map(toWishList); + }, [] as WishList[]); +} + +export function usePriceChecks(wishId: string) { + return useLiveQueryWithDefault(async () => { + const locals = await db + .table('wishesPriceChecks') + .where('wishId') + .equals(wishId) + .toArray(); + const visible = locals.filter((p) => !p.deletedAt); + return visible + .sort((a, b) => new Date(b.checkedAt).getTime() - new Date(a.checkedAt).getTime()) + .map(toPriceCheck); + }, [] as PriceCheck[]); +} + +// ─── Pure Filter Functions ──────────────────────────────── + +export function filterByStatus(wishes: Wish[], status: string): Wish[] { + return wishes.filter((w) => w.status === status); +} + +export function filterByPriority(wishes: Wish[], priority: string): Wish[] { + return wishes.filter((w) => w.priority === priority); +} + +export function filterByList(wishes: Wish[], listId: string | null): Wish[] { + if (listId === null) return wishes.filter((w) => !w.listId); + return wishes.filter((w) => w.listId === listId); +} + +export function searchWishes(wishes: Wish[], query: string): Wish[] { + if (!query.trim()) return wishes; + const q = query.toLowerCase().trim(); + return wishes.filter( + (w) => + w.title.toLowerCase().includes(q) || + w.description?.toLowerCase().includes(q) || + w.category?.toLowerCase().includes(q) || + w.tags.some((t) => t.toLowerCase().includes(q)) + ); +} + +export function getTotalEstimatedCost(wishes: Wish[]): number { + return wishes.reduce((sum, w) => sum + (w.targetPrice ?? 0), 0); +} diff --git a/apps/mana/apps/web/src/lib/modules/wishes/stores/lists.svelte.ts b/apps/mana/apps/web/src/lib/modules/wishes/stores/lists.svelte.ts new file mode 100644 index 000000000..e98fc1943 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/stores/lists.svelte.ts @@ -0,0 +1,57 @@ +/** + * Wish Lists Store — Mutations Only + */ + +import { listTable } from '../collections'; +import { toWishList } from '../queries'; +import type { LocalWishList } from '../types'; + +export const listsStore = { + async create(data: { name: string; description?: string; icon?: string; color?: string }) { + const all = await listTable.toArray(); + const active = all.filter((l) => !l.deletedAt); + + const newLocal: LocalWishList = { + id: crypto.randomUUID(), + name: data.name, + description: data.description ?? null, + icon: data.icon ?? null, + color: data.color ?? null, + isArchived: false, + order: active.length, + }; + await listTable.add(newLocal); + return toWishList(newLocal); + }, + + async update( + id: string, + data: Partial> + ) { + await listTable.update(id, { + ...data, + updatedAt: new Date().toISOString(), + }); + }, + + async archive(id: string) { + await listTable.update(id, { + isArchived: true, + updatedAt: new Date().toISOString(), + }); + }, + + async delete(id: string) { + await listTable.update(id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, + + async reorder(orderedIds: string[]) { + const now = new Date().toISOString(); + for (let i = 0; i < orderedIds.length; i++) { + await listTable.update(orderedIds[i], { order: i, updatedAt: now }); + } + }, +}; diff --git a/apps/mana/apps/web/src/lib/modules/wishes/stores/price-checks.svelte.ts b/apps/mana/apps/web/src/lib/modules/wishes/stores/price-checks.svelte.ts new file mode 100644 index 000000000..707e2a822 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/stores/price-checks.svelte.ts @@ -0,0 +1,29 @@ +/** + * Price Checks Store — Mutations Only + */ + +import { priceCheckTable } from '../collections'; +import type { LocalPriceCheck } from '../types'; + +export const priceChecksStore = { + async record(data: { + wishId: string; + url: string; + price: number; + currency: string; + available?: boolean; + }) { + const now = new Date().toISOString(); + const newLocal: LocalPriceCheck = { + id: crypto.randomUUID(), + wishId: data.wishId, + url: data.url, + price: data.price, + currency: data.currency, + available: data.available ?? true, + checkedAt: now, + }; + await priceCheckTable.add(newLocal); + return newLocal; + }, +}; diff --git a/apps/mana/apps/web/src/lib/modules/wishes/stores/wishes.svelte.ts b/apps/mana/apps/web/src/lib/modules/wishes/stores/wishes.svelte.ts new file mode 100644 index 000000000..02f8a9e5f --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/stores/wishes.svelte.ts @@ -0,0 +1,142 @@ +/** + * Wishes Store — Mutations Only + * + * All reads are handled by liveQuery hooks in queries.ts. + */ + +import { wishTable } from '../collections'; +import { toWish } from '../queries'; +import type { LocalWish, WishPriority } from '../types'; +import { encryptRecord } from '$lib/data/crypto'; +import { emitDomainEvent } from '$lib/data/events'; + +export const wishesStore = { + async create(data: { + title: string; + description?: string; + listId?: string | null; + priority?: WishPriority; + targetPrice?: number; + currency?: string; + productUrls?: string[]; + imageUrl?: string; + category?: string; + tags?: string[]; + }) { + const existing = await wishTable.toArray(); + const active = existing.filter((w) => !w.deletedAt); + + const newLocal: LocalWish = { + id: crypto.randomUUID(), + title: data.title, + description: data.description ?? null, + listId: data.listId ?? null, + priority: data.priority ?? 'medium', + targetPrice: data.targetPrice ?? null, + currency: data.currency ?? 'EUR', + productUrls: data.productUrls ?? [], + imageUrl: data.imageUrl ?? null, + category: data.category ?? null, + status: 'active', + tags: data.tags ?? [], + notes: [], + order: active.length, + }; + + const plaintextSnapshot = toWish(newLocal); + await encryptRecord('wishesItems', newLocal); + await wishTable.add(newLocal); + emitDomainEvent('WishCreated', 'wishes', 'wishesItems', newLocal.id, { + wishId: newLocal.id, + title: data.title, + listId: data.listId, + }); + return plaintextSnapshot; + }, + + async update( + id: string, + data: Partial< + Pick< + LocalWish, + | 'title' + | 'description' + | 'priority' + | 'status' + | 'targetPrice' + | 'currency' + | 'productUrls' + | 'imageUrl' + | 'category' + | 'tags' + | 'listId' + > + > + ) { + const diff: Partial = { + ...data, + updatedAt: new Date().toISOString(), + }; + await encryptRecord('wishesItems', diff); + await wishTable.update(id, diff); + }, + + async fulfill(id: string) { + await wishTable.update(id, { + status: 'fulfilled', + updatedAt: new Date().toISOString(), + }); + emitDomainEvent('WishFulfilled', 'wishes', 'wishesItems', id, { wishId: id }); + }, + + async archive(id: string) { + await wishTable.update(id, { + status: 'archived', + updatedAt: new Date().toISOString(), + }); + }, + + async delete(id: string) { + await wishTable.update(id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, + + async addNote(wishId: string, content: string) { + const wish = await wishTable.get(wishId); + if (!wish) return; + const now = new Date().toISOString(); + const note = { id: crypto.randomUUID(), content, createdAt: now }; + await wishTable.update(wishId, { + notes: [...wish.notes, note], + updatedAt: now, + }); + }, + + async addProductUrl(wishId: string, url: string) { + const wish = await wishTable.get(wishId); + if (!wish) return; + if (wish.productUrls.includes(url)) return; + await wishTable.update(wishId, { + productUrls: [...wish.productUrls, url], + updatedAt: new Date().toISOString(), + }); + }, + + async removeProductUrl(wishId: string, url: string) { + const wish = await wishTable.get(wishId); + if (!wish) return; + await wishTable.update(wishId, { + productUrls: wish.productUrls.filter((u) => u !== url), + updatedAt: new Date().toISOString(), + }); + }, + + async reorder(orderedIds: string[]) { + const now = new Date().toISOString(); + for (let i = 0; i < orderedIds.length; i++) { + await wishTable.update(orderedIds[i], { order: i, updatedAt: now }); + } + }, +}; diff --git a/apps/mana/apps/web/src/lib/modules/wishes/tools.ts b/apps/mana/apps/web/src/lib/modules/wishes/tools.ts new file mode 100644 index 000000000..7c5b24378 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/tools.ts @@ -0,0 +1,109 @@ +/** + * Wishes Tools — LLM-accessible operations for the wishes module. + */ + +import type { ModuleTool } from '$lib/data/tools/types'; + +export const wishesTools: ModuleTool[] = [ + { + name: 'create_wish', + module: 'wishes', + description: + 'Erstellt einen neuen Wunsch auf der Wunschliste. Nutze dies wenn der Nutzer sich etwas wünscht oder eine Geschenkidee hat.', + parameters: [ + { name: 'title', type: 'string', description: 'Wunsch-Titel', required: true }, + { name: 'description', type: 'string', description: 'Beschreibung', required: false }, + { + name: 'priority', + type: 'string', + description: 'Priorität', + required: false, + enum: ['low', 'medium', 'high'], + }, + { + name: 'targetPrice', + type: 'number', + description: 'Zielpreis / Budget in EUR', + required: false, + }, + { + name: 'category', + type: 'string', + description: 'Kategorie (z.B. Technik, Bücher)', + required: false, + }, + ], + async execute(params) { + const { wishesStore } = await import('./stores/wishes.svelte'); + const wish = await wishesStore.create({ + title: params.title as string, + description: params.description as string | undefined, + priority: (params.priority as 'low' | 'medium' | 'high') ?? undefined, + targetPrice: params.targetPrice as number | undefined, + category: params.category as string | undefined, + }); + return { success: true, data: wish, message: `Wunsch "${wish.title}" erstellt` }; + }, + }, + { + name: 'list_wishes', + module: 'wishes', + description: + 'Listet alle Wünsche auf der Wunschliste. Nutze dies wenn der Nutzer nach seinen Wünschen fragt.', + parameters: [ + { + name: 'filter', + type: 'string', + description: 'Nach Status filtern', + required: false, + enum: ['active', 'fulfilled', 'all'], + }, + ], + async execute(params) { + const { wishTable } = await import('./collections'); + const { toWish } = await import('./queries'); + const { decryptRecords } = await import('$lib/data/crypto'); + const all = await wishTable.toArray(); + const active = all.filter((w) => !w.deletedAt); + const decrypted = await decryptRecords('wishesItems', active); + const wishes = decrypted.map(toWish); + + const filter = (params.filter as string) ?? 'active'; + let filtered = wishes; + if (filter === 'active') filtered = wishes.filter((w) => w.status === 'active'); + else if (filter === 'fulfilled') filtered = wishes.filter((w) => w.status === 'fulfilled'); + + const list = filtered.map((w) => ({ + id: w.id, + title: w.title, + priority: w.priority, + targetPrice: w.targetPrice, + currency: w.currency, + category: w.category, + status: w.status, + })); + + return { + success: true, + data: list, + message: + list.length === 0 + ? `Keine ${filter === 'all' ? '' : filter + 'n'} Wünsche` + : `${list.length} Wünsche gefunden`, + }; + }, + }, + { + name: 'fulfill_wish', + module: 'wishes', + description: 'Markiert einen Wunsch als erfüllt.', + parameters: [ + { name: 'wishId', type: 'string', description: 'ID des Wunsches', required: true }, + ], + async execute(params) { + const { wishesStore } = await import('./stores/wishes.svelte'); + await wishesStore.fulfill(params.wishId as string); + return { success: true, message: 'Wunsch als erfüllt markiert' }; + }, + }, +]; diff --git a/apps/mana/apps/web/src/lib/modules/wishes/types.ts b/apps/mana/apps/web/src/lib/modules/wishes/types.ts new file mode 100644 index 000000000..f0230b15b --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/types.ts @@ -0,0 +1,86 @@ +/** + * Wishes module types for the unified app. + */ + +import type { BaseRecord } from '@mana/local-store'; + +export interface LocalWish extends BaseRecord { + title: string; + description?: string | null; + listId?: string | null; + priority: 'low' | 'medium' | 'high'; + targetPrice?: number | null; + currency?: string | null; + productUrls: string[]; + imageUrl?: string | null; + category?: string | null; + status: 'active' | 'fulfilled' | 'archived'; + tags: string[]; + notes: Array<{ id: string; content: string; createdAt: string }>; + order: number; +} + +export interface LocalWishList extends BaseRecord { + name: string; + description?: string | null; + icon?: string | null; + color?: string | null; + isArchived: boolean; + order: number; +} + +export interface LocalPriceCheck extends BaseRecord { + wishId: string; + url: string; + price: number; + currency: string; + available: boolean; + checkedAt: string; +} + +// ─── Public Types (post-decryption, used in UI) ─────────── + +export interface Wish { + id: string; + title: string; + description?: string | null; + listId?: string | null; + priority: 'low' | 'medium' | 'high'; + targetPrice?: number | null; + currency?: string | null; + productUrls: string[]; + imageUrl?: string | null; + category?: string | null; + status: 'active' | 'fulfilled' | 'archived'; + tags: string[]; + notes: Array<{ id: string; content: string; createdAt: string }>; + order: number; + createdAt: string; + updatedAt: string; +} + +export interface WishList { + id: string; + name: string; + description?: string | null; + icon?: string | null; + color?: string | null; + isArchived: boolean; + order: number; + createdAt: string; + updatedAt: string; +} + +export interface PriceCheck { + id: string; + wishId: string; + url: string; + price: number; + currency: string; + available: boolean; + checkedAt: string; + createdAt: string; +} + +export type WishStatus = Wish['status']; +export type WishPriority = Wish['priority']; diff --git a/apps/mana/apps/web/src/lib/modules/wishes/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/wishes/views/DetailView.svelte new file mode 100644 index 000000000..cb0ea646c --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/wishes/views/DetailView.svelte @@ -0,0 +1,373 @@ + + + + {wish?.title ?? 'Wunsch'} - Wünsche - Mana + + +{#if !wish} +
+

Wunsch nicht gefunden

+
+{:else} +
+ +
+ +
+ {#if wish.status === 'active'} + + {/if} + +
+
+ + + {#if editing} +
+ + +
+ + +
+
+ + +
+
+ {:else} + + {/if} + + +
+

+ + Produkt-Links ({wish.productUrls.length}) +

+ + {#if wish.productUrls.length > 0} +
    + {#each wish.productUrls as url} +
  • + + {url} + + +
  • + {/each} +
+ {/if} + +
{ + e.preventDefault(); + addUrl(); + }} + class="flex gap-2" + > + + +
+
+ + + {#if priceChecks.value.length > 0} +
+

Preisverlauf

+
+ {#each priceChecks.value.slice(0, 10) as check (check.id)} +
+ + {new Date(check.checkedAt).toLocaleDateString('de-DE')} + + + {check.price.toLocaleString('de-DE')} + {check.currency} + +
+ {/each} +
+
+ {/if} + + +
+

+ Notizen ({wish.notes.length}) +

+ + {#if wish.notes.length > 0} +
    + {#each wish.notes as note (note.id)} +
  • +

    {note.content}

    +

    + {new Date(note.createdAt).toLocaleString('de-DE')} +

    +
  • + {/each} +
+ {/if} + +
{ + e.preventDefault(); + addNote(); + }} + class="flex gap-2" + > + + +
+
+ + + {#if wish.tags.length > 0} +
+ {#each wish.tags as tag} + + {tag} + + {/each} +
+ {/if} +
+{/if} diff --git a/apps/mana/apps/web/src/routes/(app)/wishes/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/wishes/+layout.svelte new file mode 100644 index 000000000..ae9c9d035 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/wishes/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/apps/mana/apps/web/src/routes/(app)/wishes/+page.svelte b/apps/mana/apps/web/src/routes/(app)/wishes/+page.svelte new file mode 100644 index 000000000..7707c1aac --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/wishes/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/apps/mana/apps/web/src/routes/(app)/wishes/[id]/+page.svelte b/apps/mana/apps/web/src/routes/(app)/wishes/[id]/+page.svelte new file mode 100644 index 000000000..7f6ecdb83 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/wishes/[id]/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/shared-ui/src/dnd/types.ts b/packages/shared-ui/src/dnd/types.ts index d4be73af4..7a1a4b8e2 100644 --- a/packages/shared-ui/src/dnd/types.ts +++ b/packages/shared-ui/src/dnd/types.ts @@ -26,7 +26,8 @@ export type DragType = | 'place' | 'dream' | 'journal-entry' - | 'first'; + | 'first' + | 'wish'; export interface DragPayload> { type: DragType;