aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-09-07 19:30:50 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-09-07 19:30:50 +0200
commit20a7a08e97e75c3a45b3430a74c2931554498f1c (patch)
treed74bdeef3d426e563b2127942ab71ae31aaf0707 /src/lib
parent37573109a05a07ac50827759ea63793209baa73a (diff)
Extract shared groceries helpers and BestRef component
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/components/BestRef.svelte16
-rw-r--r--src/lib/server/services/grocery-actions.ts14
-rw-r--r--src/lib/shared/group-by-category.test.ts50
-rw-r--r--src/lib/shared/group-by-category.ts23
4 files changed, 103 insertions, 0 deletions
diff --git a/src/lib/components/BestRef.svelte b/src/lib/components/BestRef.svelte
new file mode 100644
index 0000000..e194876
--- /dev/null
+++ b/src/lib/components/BestRef.svelte
@@ -0,0 +1,16 @@
+<script lang="ts">
+ import type { Reference } from '$lib/server/services/types';
+
+ let { ref }: { ref: Reference } = $props();
+</script>
+
+<span class="best-ref">
+ {ref.reference}
+ {#if ref.price && ref.unit}
+ &nbsp;{ref.price.toFixed(2)}
+ {ref.unit}
+ {/if}
+ {#if ref.qualityScore != null}
+ &nbsp;({ref.qualityScore}/100)
+ {/if}
+</span>
diff --git a/src/lib/server/services/grocery-actions.ts b/src/lib/server/services/grocery-actions.ts
new file mode 100644
index 0000000..0966c92
--- /dev/null
+++ b/src/lib/server/services/grocery-actions.ts
@@ -0,0 +1,14 @@
+import { fail } from '@sveltejs/kit';
+import { toggle } from './groceries';
+import { numericId } from './validation';
+
+export async function toggleGroceryAction(request: Request) {
+ const data = await request.formData();
+ try {
+ const id = numericId(data.get('id'));
+ await toggle(id);
+ } catch {
+ return fail(400, { error: 'ID invalide' });
+ }
+ return { success: true };
+}
diff --git a/src/lib/shared/group-by-category.test.ts b/src/lib/shared/group-by-category.test.ts
new file mode 100644
index 0000000..d9bb379
--- /dev/null
+++ b/src/lib/shared/group-by-category.test.ts
@@ -0,0 +1,50 @@
+import { describe, it, expect } from 'vitest';
+import { groupByCategory } from './group-by-category';
+
+type Item = { id: number; name: string; categoryName: string | null };
+type Cat = { id: number; name: string };
+
+describe('groupByCategory', () => {
+ const categories: Cat[] = [
+ { id: 1, name: 'Laitier' },
+ { id: 2, name: 'Fruits' },
+ { id: 3, name: 'Boulangerie' },
+ ];
+
+ it('groups items by category, ordered by category order', () => {
+ const items: Item[] = [
+ { id: 1, name: 'banane', categoryName: 'Fruits' },
+ { id: 2, name: 'lait', categoryName: 'Laitier' },
+ { id: 3, name: 'pomme', categoryName: 'Fruits' },
+ ];
+
+ const result = groupByCategory(items, categories);
+
+ expect(result.map(([cat]) => cat)).toEqual(['Laitier', 'Fruits']);
+ expect(result[0][1].map((i) => i.name)).toEqual(['lait']);
+ expect(result[1][1].map((i) => i.name)).toEqual(['banane', 'pomme']);
+ });
+
+ it('prepends a non-catégorisé group for items without a category', () => {
+ const items: Item[] = [
+ { id: 1, name: 'miel', categoryName: null },
+ { id: 2, name: 'lait', categoryName: 'Laitier' },
+ ];
+
+ const result = groupByCategory(items, categories);
+
+ expect(result.map(([cat]) => cat)).toEqual(['Non catégorisé', 'Laitier']);
+ });
+
+ it('omits categories without any item', () => {
+ const items: Item[] = [{ id: 1, name: 'lait', categoryName: 'Laitier' }];
+
+ const result = groupByCategory(items, categories);
+
+ expect(result.map(([cat]) => cat)).toEqual(['Laitier']);
+ });
+
+ it('returns no groups for empty items', () => {
+ expect(groupByCategory([], categories)).toEqual([]);
+ });
+});
diff --git a/src/lib/shared/group-by-category.ts b/src/lib/shared/group-by-category.ts
new file mode 100644
index 0000000..e9b0acd
--- /dev/null
+++ b/src/lib/shared/group-by-category.ts
@@ -0,0 +1,23 @@
+export function groupByCategory<
+ TItem extends { categoryName: string | null },
+ TCategory extends { name: string },
+>(items: TItem[], categories: TCategory[]): [string, TItem[]][] {
+ const groups = new Map<string, TItem[]>();
+ for (const item of items) {
+ const key = item.categoryName ?? 'Non catégorisé';
+ const list = groups.get(key);
+ if (list) list.push(item);
+ else groups.set(key, [item]);
+ }
+
+ const result: [string, TItem[]][] = [];
+ for (const cat of categories) {
+ const list = groups.get(cat.name);
+ if (list?.length) result.push([cat.name, list]);
+ }
+ const uncategorized = groups.get('Non catégorisé');
+ if (uncategorized?.length) {
+ result.unshift(['Non catégorisé', uncategorized]);
+ }
+ return result;
+}