From 18a0f9c874aaa09c0f71252dad853c23f9511ed6 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Mon, 7 Sep 2026 11:32:41 +0200 Subject: Add projects with routines, urgency tracking and health scores Projects group recurring routines (chores included) with an append-only snapshot table (id, version) so edits never rewrite history; completions reference the exact snapshot. Pure metrics compute due dates, urgency buckets (ok/due/late/veryLate) and 0-100 health scores per routine, project, and globally. Home page lists projects; project dashboard sorts routines most-urgent first with one-tap completion and interval parser inputs (1d/2w/3m/1y). --- src/routes/+page.server.ts | 20 ++ src/routes/+page.svelte | 144 ++++++++++- src/routes/projects/[id]/+page.server.ts | 102 ++++++++ src/routes/projects/[id]/+page.svelte | 395 +++++++++++++++++++++++++++++++ 4 files changed, 657 insertions(+), 4 deletions(-) create mode 100644 src/routes/+page.server.ts create mode 100644 src/routes/projects/[id]/+page.server.ts create mode 100644 src/routes/projects/[id]/+page.svelte (limited to 'src/routes') diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..fc160df --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,20 @@ +import { fail } from '@sveltejs/kit'; +import { loadOverview } from '$lib/server/services/routines'; +import { add as addProject } from '$lib/server/services/projects'; +import { parseFormString } from '$lib/server/services/validation'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + return loadOverview(Date.now()); +}; + +export const actions: Actions = { + addProject: async ({ request }) => { + const name = parseFormString((await request.formData()).get('name')); + if (!name) { + return fail(400, { error: 'Le nom est requis' }); + } + await addProject(name); + return { success: true }; + }, +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 5bdee46..4d4777a 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,4 +1,140 @@ -

Accueil — à venir : projets et routines.

- + + +
+ + +
+ + +
+ + {#if form?.error} +

{form.error}

+ {/if} + + + + {#if data.projects.length === 0} +

Aucun projet

+ {/if} +
+ + diff --git a/src/routes/projects/[id]/+page.server.ts b/src/routes/projects/[id]/+page.server.ts new file mode 100644 index 0000000..b20af6c --- /dev/null +++ b/src/routes/projects/[id]/+page.server.ts @@ -0,0 +1,102 @@ +import { error, fail, redirect } from '@sveltejs/kit'; +import { getById } from '$lib/server/services/projects'; +import { + add, + edit, + archive, + complete, + loadProjectDashboard, +} from '$lib/server/services/routines'; +import { numericId, parseFormString } from '$lib/server/services/validation'; +import { parseIntervalToDays } from '$lib/shared/routine-metrics'; +import type { Actions, PageServerLoad } from './$types'; + +function parseFrequencyDays(value: FormDataEntryValue | null): number { + const raw = value?.toString() ?? ''; + const days = parseIntervalToDays(raw); + if (days === null) { + throw new Error('Invalid interval'); + } + return days; +} + +export const load: PageServerLoad = async ({ params }) => { + const id = numericId(params.id); + const project = await getById(id); + if (!project || project.archived) { + throw error(404, 'Projet introuvable'); + } + const dashboard = await loadProjectDashboard(id, Date.now()); + return { + project: { id: project.id, name: project.name }, + routines: dashboard.routines, + healthScore: dashboard.healthScore, + }; +}; + +export const actions: Actions = { + addRoutine: async ({ request, params }) => { + const data = await request.formData(); + const name = parseFormString(data.get('name')); + if (!name) { + return fail(400, { error: 'Le nom est requis' }); + } + try { + await add( + numericId(params.id), + name, + parseFrequencyDays(data.get('frequencyDays')), + ); + } catch { + return fail(400, { error: 'Fréquence invalide' }); + } + return { success: true }; + }, + + editRoutine: async ({ request }) => { + const data = await request.formData(); + const name = parseFormString(data.get('name')); + if (!name) { + return fail(400, { error: 'Le nom est requis' }); + } + try { + await edit( + numericId(data.get('id')), + name, + parseFrequencyDays(data.get('frequencyDays')), + ); + } catch { + return fail(400, { error: 'Modification invalide' }); + } + return { success: true }; + }, + + complete: async ({ request }) => { + const data = await request.formData(); + try { + await complete( + numericId(data.get('id')), + parseFormString(data.get('note')), + new Date().toISOString(), + ); + } catch { + return fail(400, { error: 'Routine introuvable' }); + } + return { success: true }; + }, + + archiveRoutine: async ({ request }) => { + const data = await request.formData(); + try { + await archive(numericId(data.get('id'))); + } catch { + return fail(400, { error: 'Routine introuvable' }); + } + return { success: true }; + }, + + archiveProject: async ({ params }) => { + await archive(numericId(params.id)); + throw redirect(303, '/'); + }, +}; diff --git a/src/routes/projects/[id]/+page.svelte b/src/routes/projects/[id]/+page.svelte new file mode 100644 index 0000000..d9747a9 --- /dev/null +++ b/src/routes/projects/[id]/+page.svelte @@ -0,0 +1,395 @@ + + +
+ + +
{ + return async ({ update }) => { + name = ''; + frequency = ''; + await update({ reset: false }); + }; + }} + > + + + +
+ + {#if frequency !== '' && frequencyDays === null} +

Intervalle invalide — ex : 1d, 2w, 3m, 1y

+ {/if} + + {#if form?.error} +

{form.error}

+ {/if} + + + + {#if data.routines.length === 0} +

Aucune routine

+ {/if} + +
+
+ +
+
+
+ + -- cgit v1.3