aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/server/services/groceries.ts21
-rw-r--r--src/routes/catalog/+page.server.ts25
-rw-r--r--src/routes/catalog/+page.svelte97
3 files changed, 140 insertions, 3 deletions
diff --git a/src/lib/server/services/groceries.ts b/src/lib/server/services/groceries.ts
index db9e736..3c6de70 100644
--- a/src/lib/server/services/groceries.ts
+++ b/src/lib/server/services/groceries.ts
@@ -1,6 +1,6 @@
import { db as defaultDb } from '$lib/server/db';
import { groceries, categories, references } from '$lib/server/db/schema';
-import { eq, desc } from 'drizzle-orm';
+import { eq, desc, inArray } from 'drizzle-orm';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import type * as schema from '$lib/server/db/schema';
import type { Category, Grocery, Reference } from './types';
@@ -109,6 +109,25 @@ export async function updateGrocery(
.where(eq(groceries.id, id));
}
+export async function updateGroceryCategory(
+ id: number,
+ categoryId: number | null,
+ db = defaultDb,
+): Promise<void> {
+ await db.update(groceries).set({ categoryId }).where(eq(groceries.id, id));
+}
+
+export async function bulkUpdateGroceryCategory(
+ ids: number[],
+ categoryId: number | null,
+ db = defaultDb,
+): Promise<void> {
+ await db
+ .update(groceries)
+ .set({ categoryId })
+ .where(inArray(groceries.id, ids));
+}
+
export async function deleteGrocery(id: number, db = defaultDb): Promise<void> {
await db.delete(groceries).where(eq(groceries.id, id));
}
diff --git a/src/routes/catalog/+page.server.ts b/src/routes/catalog/+page.server.ts
index 69a775a..22464dc 100644
--- a/src/routes/catalog/+page.server.ts
+++ b/src/routes/catalog/+page.server.ts
@@ -1,6 +1,13 @@
import { fail } from '@sveltejs/kit';
-import { loadAll, toggle } from '$lib/server/services/groceries';
-import { numericId } from '$lib/server/services/validation';
+import {
+ loadAll,
+ toggle,
+ bulkUpdateGroceryCategory,
+} from '$lib/server/services/groceries';
+import {
+ numericId,
+ parseOptionalNumber,
+} from '$lib/server/services/validation';
import type { EnrichedGrocery } from '$lib/server/services/groceries';
import type { Category } from '$lib/server/services/types';
import type { Actions, PageServerLoad } from './$types';
@@ -25,4 +32,18 @@ export const actions: Actions = {
}
return { success: true };
},
+
+ bulkUpdateCategory: async ({ request }) => {
+ const data = await request.formData();
+ const ids = data.getAll('ids').map(numericId);
+ const rawCategoryId = parseOptionalNumber(data.get('category_id'));
+ const categoryId = rawCategoryId === -1 ? null : rawCategoryId;
+
+ if (ids.length === 0) {
+ return fail(400, { error: 'Aucun article sélectionné' });
+ }
+
+ await bulkUpdateGroceryCategory(ids, categoryId);
+ return { success: true, type: 'bulkUpdate' as const };
+ },
};
diff --git a/src/routes/catalog/+page.svelte b/src/routes/catalog/+page.svelte
index 7d3ebbe..2079d12 100644
--- a/src/routes/catalog/+page.svelte
+++ b/src/routes/catalog/+page.svelte
@@ -1,9 +1,16 @@
<script lang="ts">
import { enhance } from '$app/forms';
+ import { SvelteSet } from 'svelte/reactivity';
let { data, form } = $props();
let catalogItems = $derived(data?.items ?? []);
+ let selectedIds = new SvelteSet<number>();
+
+ function toggleId(id: number) {
+ if (selectedIds.has(id)) selectedIds.delete(id);
+ else selectedIds.add(id);
+ }
function groupByCategory(items: typeof catalogItems) {
const groups: Record<string, typeof catalogItems> = {};
@@ -15,7 +22,43 @@
}
</script>
+{#if catalogItems.length > 0}
+ <div class="bulk-bar">
+ <form
+ method="POST"
+ action="?/bulkUpdateCategory"
+ use:enhance={() => {
+ return async ({ update }) => {
+ await update({ reset: false });
+ selectedIds.clear();
+ };
+ }}
+ >
+ {#each Array.from(selectedIds) as id (id)}
+ <input type="hidden" name="ids" value={id} />
+ {/each}
+ <select name="category_id" class="form-select">
+ <option value={-1}>Non catégorisé</option>
+ {#each data.categories as cat (cat.id)}
+ <option value={cat.id}>{cat.name}</option>
+ {/each}
+ </select>
+ <button
+ class="btn-primary"
+ type="submit"
+ disabled={selectedIds.size === 0}
+ >
+ Mettre à jour ({selectedIds.size})
+ </button>
+ </form>
+ </div>
+{/if}
+
<main>
+ {#if form?.error}
+ <p class="error">{form.error}</p>
+ {/if}
+
{#if catalogItems.length === 0}
<p class="empty">Aucun article</p>
{:else}
@@ -25,6 +68,12 @@
<ul class="item-list">
{#each items as item (item.id)}
<li class="item-row item-row--fixed">
+ <input
+ type="checkbox"
+ class="item-check"
+ checked={selectedIds.has(item.id)}
+ onchange={() => toggleId(item.id)}
+ />
<button
class="item-btn item-btn--grow"
name="id"
@@ -60,12 +109,60 @@
</main>
<style>
+ .bulk-bar {
+ display: flex;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ position: sticky;
+ top: var(--nav-height);
+ background: var(--bg);
+ border-bottom: 1px solid var(--border);
+ z-index: 10;
+ }
+
+ .bulk-bar form {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ width: 100%;
+ }
+
+ .bulk-bar .form-select {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .bulk-bar .btn-primary {
+ padding: 0.75rem 1rem;
+ align-self: center;
+ white-space: nowrap;
+ }
+
+ .bulk-bar .btn-primary:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+ }
+
+ .item-check {
+ width: 1.25rem;
+ height: 1.25rem;
+ accent-color: var(--accent);
+ cursor: pointer;
+ flex-shrink: 0;
+ }
+
.checked {
text-decoration: line-through;
color: var(--text-secondary);
font-weight: 400;
}
+ .error {
+ margin-bottom: 1rem;
+ }
+
.empty {
margin-top: 1rem;
padding: 2rem;