diff --git a/src/routes/api/cospend/list/categorize/override/+server.ts b/src/routes/api/cospend/list/categorize/override/+server.ts new file mode 100644 index 0000000..2488552 --- /dev/null +++ b/src/routes/api/cospend/list/categorize/override/+server.ts @@ -0,0 +1,25 @@ +import { json, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { ShoppingItemCategory } from '$models/ShoppingItemCategory'; +import { dbConnect } from '$utils/db'; + +// POST /api/cospend/list/categorize/override — manually set category + icon for an item name +export const POST: RequestHandler = async ({ request, locals }) => { + const auth = await locals.auth(); + if (!auth?.user?.nickname) throw error(401, 'Not logged in'); + + const { name, category, icon } = await request.json(); + if (!name || typeof name !== 'string') throw error(400, 'name is required'); + if (!category || typeof category !== 'string') throw error(400, 'category is required'); + + const normalizedName = name.toLowerCase().trim(); + + await dbConnect(); + await ShoppingItemCategory.findOneAndUpdate( + { normalizedName }, + { normalizedName, originalName: name, category, icon: icon || null }, + { upsert: true } + ); + + return json({ ok: true }); +}; diff --git a/src/routes/cospend/list/+page.svelte b/src/routes/cospend/list/+page.svelte index db174a5..e6eafcb 100644 --- a/src/routes/cospend/list/+page.svelte +++ b/src/routes/cospend/list/+page.svelte @@ -2,10 +2,12 @@ import { onMount, onDestroy } from 'svelte'; import { getShoppingSync } from '$lib/js/shoppingSync.svelte'; import { SHOPPING_CATEGORIES } from '$lib/data/shoppingCategoryItems'; - import { Plus, ListX, Apple, Beef, Milk, Croissant, Wheat, FlameKindling, GlassWater, Candy, Snowflake, SprayCan, Sparkles, Package } from '@lucide/svelte'; + import { Plus, ListX, Apple, Beef, Milk, Croissant, Wheat, FlameKindling, GlassWater, Candy, Snowflake, SprayCan, Sparkles, Package, Search } from '@lucide/svelte'; + import SyncIndicator from '$lib/components/fitness/SyncIndicator.svelte'; import { flip } from 'svelte/animate'; import { slide } from 'svelte/transition'; import { SvelteSet } from 'svelte/reactivity'; + import catalogData from '$lib/data/shoppingCatalog.json'; let { data } = $props(); let user = $derived(data.session?.user?.nickname || ''); @@ -149,11 +151,70 @@ if (e.key === 'Enter') { e.preventDefault(); addItem(); } } + // --- Long press edit --- + + /** @type {number | null} */ + let longPressTimer = $state(null); + /** @type {import('$lib/js/shoppingSync.svelte').ShoppingItem | null} */ + let editingItem = $state(null); + let editCategory = $state(''); + let editIcon = $state(''); + let iconSearch = $state(''); + let editSaving = $state(false); + + const allIcons = Object.entries(/** @type {Record} */ (catalogData)); + + let filteredIcons = $derived( + iconSearch.trim() + ? allIcons.filter(([name]) => name.includes(iconSearch.toLowerCase())) + : allIcons + ); + + /** @param {import('$lib/js/shoppingSync.svelte').ShoppingItem} item */ + function startLongPress(item) { + longPressTimer = window.setTimeout(() => { + editingItem = item; + editCategory = item.category; + editIcon = item.icon || ''; + iconSearch = ''; + }, 500); + } + + function cancelLongPress() { + if (longPressTimer) { + clearTimeout(longPressTimer); + longPressTimer = null; + } + } + + function closeEdit() { + editingItem = null; + editSaving = false; + } + + async function saveEdit() { + if (!editingItem) return; + editSaving = true; + const cleanName = parseQuantity(editingItem.name).name; + try { + await fetch('/api/cospend/list/categorize/override', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: cleanName, category: editCategory, icon: editIcon || null }) + }); + sync.updateItemCategory(editingItem.id, editCategory, editIcon || null); + closeEdit(); + } catch (err) { + console.error('[shopping] Save override error:', err); + editSaving = false; + } + } +

{group.category}

+ {group.items.filter(i => !i.checked).length} - {group.items.filter(i => !i.checked).length}
@@ -201,6 +262,10 @@ class:checked={item.checked} animate:flip={{ duration: 200 }} onclick={() => sync.toggleItem(item.id, user)} + onpointerdown={() => startLongPress(item)} + onpointerup={cancelLongPress} + onpointerleave={cancelLongPress} + oncontextmenu={(e) => e.preventDefault()} > {#if parsed.qty} {parsed.qty} @@ -228,13 +293,62 @@ {/if} {/if} - {#if sync.status === 'offline'} -
Offline
- {:else if sync.status === 'syncing'} -
Synchronisiere...
- {/if}
+{#if editingItem} + + +
+ + +
e.stopPropagation()}> +

{parseQuantity(editingItem.name).name}

+ + +
+ {#each SHOPPING_CATEGORIES as cat} + {@const meta = categoryMeta[cat] || categoryMeta['Sonstiges']} + {@const CatIcon = meta.icon} + + {/each} +
+ + + +
+ {#each filteredIcons as [name, file]} + + {/each} +
+ +
+ + +
+
+
+{/if} +