From a3b25d5e46bb661db930975c1cfeb3922f949037 Mon Sep 17 00:00:00 2001 From: Piotr Oleszczyk Date: Sat, 28 Feb 2026 23:46:33 +0100 Subject: [PATCH] feat(frontend): add grooming schedule CRUD page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New route /routines/grooming-schedule with load + create/update/delete actions - Entries grouped by day of week with inline editing - 4 API functions added to api.ts (get/create/update/delete) - Nav: add Grooming link, fix isActive to not highlight parent when child matches - Nav: replace ⌂ text char with 🏠 emoji for Dashboard Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/api.ts | 10 + frontend/src/routes/+layout.svelte | 11 +- .../grooming-schedule/+page.server.ts | 67 ++++++ .../routines/grooming-schedule/+page.svelte | 221 ++++++++++++++++++ 4 files changed, 307 insertions(+), 2 deletions(-) create mode 100644 frontend/src/routes/routines/grooming-schedule/+page.server.ts create mode 100644 frontend/src/routes/routines/grooming-schedule/+page.svelte diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 8414e81..69089b4 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,6 +1,7 @@ import { PUBLIC_API_BASE } from '$env/static/public'; import type { ActiveIngredient, + GroomingSchedule, LabResult, MedicationEntry, MedicationUsage, @@ -129,6 +130,15 @@ export const updateRoutineStep = (stepId: string, body: Record) export const deleteRoutineStep = (stepId: string): Promise => api.del(`/routines/steps/${stepId}`); +export const getGroomingSchedule = (): Promise => + api.get('/routines/grooming-schedule'); +export const createGroomingScheduleEntry = (body: Record): Promise => + api.post('/routines/grooming-schedule', body); +export const updateGroomingScheduleEntry = (id: string, body: Record): Promise => + api.patch(`/routines/grooming-schedule/${id}`, body); +export const deleteGroomingScheduleEntry = (id: string): Promise => + api.del(`/routines/grooming-schedule/${id}`); + // ─── Health – Medications ──────────────────────────────────────────────────── export interface MedicationListParams { diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index aaeb524..110b8ff 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -5,9 +5,10 @@ let { children } = $props(); const navItems = [ - { href: '/', label: 'Dashboard', icon: '⌂' }, + { href: '/', label: 'Dashboard', icon: '🏠' }, { href: '/products', label: 'Products', icon: '🧴' }, { href: '/routines', label: 'Routines', icon: '📋' }, + { href: '/routines/grooming-schedule', label: 'Grooming', icon: '🪒' }, { href: '/health/medications', label: 'Medications', icon: '💊' }, { href: '/health/lab-results', label: 'Lab Results', icon: '🔬' }, { href: '/skin', label: 'Skin', icon: '✨' } @@ -15,7 +16,13 @@ function isActive(href: string) { if (href === '/') return page.url.pathname === '/'; - return page.url.pathname.startsWith(href); + const pathname = page.url.pathname; + if (!pathname.startsWith(href)) return false; + // Don't mark parent as active if a more-specific nav item also matches + const moreSpecific = navItems.some( + (item) => item.href !== href && item.href.startsWith(href) && pathname.startsWith(item.href) + ); + return !moreSpecific; } diff --git a/frontend/src/routes/routines/grooming-schedule/+page.server.ts b/frontend/src/routes/routines/grooming-schedule/+page.server.ts new file mode 100644 index 0000000..baa4628 --- /dev/null +++ b/frontend/src/routes/routines/grooming-schedule/+page.server.ts @@ -0,0 +1,67 @@ +import { + createGroomingScheduleEntry, + deleteGroomingScheduleEntry, + getGroomingSchedule, + updateGroomingScheduleEntry +} from '$lib/api'; +import { fail } from '@sveltejs/kit'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + const schedule = await getGroomingSchedule(); + return { schedule }; +}; + +export const actions: Actions = { + create: async ({ request }) => { + const form = await request.formData(); + const day_of_week = form.get('day_of_week'); + const action = form.get('action') as string; + if (day_of_week === null || !action) { + return fail(400, { error: 'day_of_week and action are required' }); + } + const body: Record = { + day_of_week: Number(day_of_week), + action + }; + const notes = (form.get('notes') as string)?.trim(); + if (notes) body.notes = notes; + try { + await createGroomingScheduleEntry(body); + return { created: true }; + } catch (e) { + return fail(500, { error: (e as Error).message }); + } + }, + + update: async ({ request }) => { + const form = await request.formData(); + const id = form.get('id') as string; + if (!id) return fail(400, { error: 'Missing id' }); + const body: Record = {}; + const day_of_week = form.get('day_of_week'); + if (day_of_week !== null) body.day_of_week = Number(day_of_week); + const action = form.get('action') as string; + if (action) body.action = action; + const notes = (form.get('notes') as string)?.trim(); + body.notes = notes || null; + try { + await updateGroomingScheduleEntry(id, body); + return { updated: true }; + } catch (e) { + return fail(500, { error: (e as Error).message }); + } + }, + + delete: async ({ request }) => { + const form = await request.formData(); + const id = form.get('id') as string; + if (!id) return fail(400, { error: 'Missing id' }); + try { + await deleteGroomingScheduleEntry(id); + return { deleted: true }; + } catch (e) { + return fail(500, { error: (e as Error).message }); + } + } +}; diff --git a/frontend/src/routes/routines/grooming-schedule/+page.svelte b/frontend/src/routes/routines/grooming-schedule/+page.svelte new file mode 100644 index 0000000..3bcf684 --- /dev/null +++ b/frontend/src/routes/routines/grooming-schedule/+page.svelte @@ -0,0 +1,221 @@ + + +Grooming Schedule — innercontext + +
+
+
+ ← Routines +

Grooming Schedule

+
+ +
+ + {#if form?.error} +
{form.error}
+ {/if} + {#if form?.created} +
Wpis dodany.
+ {/if} + {#if form?.updated} +
Wpis zaktualizowany.
+ {/if} + {#if form?.deleted} +
Wpis usunięty.
+ {/if} + + + {#if showAddForm} + + +
{ + return async ({ update }) => { + await update(); + showAddForm = false; + }; + }} + class="grid grid-cols-2 gap-4" + > +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ {/if} + + + {#if schedule.length === 0} +

Brak wpisów. Kliknij "+ Dodaj wpis", aby zacząć.

+ {:else} +
+ {#each byDay as { name, entries, day } (day)} +
+

{name}

+ {#each entries as entry (entry.id)} +
+ +
+
+ {ACTION_LABELS[entry.action]} + {#if entry.notes} + {entry.notes} + {/if} +
+
+ +
{ + if (!confirm('Usunąć ten wpis?')) e.preventDefault(); + }} + > + + +
+
+
+ + + {#if editingId === entry.id} +
+
{ + return async ({ update }) => { + await update(); + editingId = null; + }; + }} + class="grid grid-cols-2 gap-3" + > + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ {/if} +
+ {/each} +
+ + {/each} +
+ {/if} +