From a725d4c7d28fee701b6997cc46cffddefbcd26bf Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Wed, 30 Sep 2026 22:13:07 +0200 Subject: Allow granular routine frequencies like 5/2w --- src/lib/server/services/routine-actions.ts | 15 ++++++---- src/lib/server/services/routines.test.ts | 46 +++++++++++++++--------------- src/lib/server/services/routines.ts | 12 ++++---- src/lib/server/services/validation.ts | 10 ------- 4 files changed, 39 insertions(+), 44 deletions(-) (limited to 'src/lib/server/services') diff --git a/src/lib/server/services/routine-actions.ts b/src/lib/server/services/routine-actions.ts index 44ba2b8..1d7f8b5 100644 --- a/src/lib/server/services/routine-actions.ts +++ b/src/lib/server/services/routine-actions.ts @@ -1,7 +1,8 @@ import { fail, type ActionFailure } from '@sveltejs/kit'; import { archive, complete, edit, loadCurrent } from './routines'; -import { parseFormString, parseFrequencyDays } from './validation'; +import { parseFormString } from './validation'; import { instantFromDay } from '$lib/shared/temporal'; +import { Frequency } from '$lib/shared/frequency'; /** Shared routine edit/complete actions; rejects missing/archived routines. */ @@ -26,16 +27,20 @@ export async function editRoutineAction( return fail(400, { error: 'Le nom est requis' }); } - let frequencyDays: number; + let frequency: string; try { - frequencyDays = parseFrequencyDays(data.get('frequencyDays')); + frequency = Frequency.parse( + data.get('frequency')?.toString() ?? '', + ).toSpec(); } catch { return fail(400, { error: 'Fréquence invalide' }); } // Only touch description when the form sends one; list edits never wipe it. - const fields: { name: string; frequencyDays: number; description?: string } = - { name, frequencyDays }; + const fields: { name: string; frequency: string; description?: string } = { + name, + frequency, + }; const description = data.get('description'); if (description !== null) { fields.description = description.toString(); diff --git a/src/lib/server/services/routines.test.ts b/src/lib/server/services/routines.test.ts index eda8824..2ad409a 100644 --- a/src/lib/server/services/routines.test.ts +++ b/src/lib/server/services/routines.test.ts @@ -24,46 +24,46 @@ describe('routines service', () => { }); it('creates a routine with version 1', async () => { - const r = await add(projectId, 'clean kitchen', 7, db); + const r = await add(projectId, 'clean kitchen', '7d', db); expect(r.id).toBe(1); expect(r.version).toBe(1); - expect(r.frequencyDays).toBe(7); + expect(r.frequency).toBe('7d'); }); it('edit inserts a new snapshot version and keeps history', async () => { - const r = await add(projectId, 'clean kitchen', 7, db); + const r = await add(projectId, 'clean kitchen', '7d', db); const updated = await edit( r.id, - { name: 'clean kitchen deeply', frequencyDays: 3 }, + { name: 'clean kitchen deeply', frequency: '3d' }, db, ); expect(updated.version).toBe(2); - expect(updated.frequencyDays).toBe(3); + expect(updated.frequency).toBe('3d'); const versions = await loadAllVersions(r.id, db); expect(versions).toHaveLength(2); - expect(versions[0].frequencyDays).toBe(7); - expect(versions[1].frequencyDays).toBe(3); + expect(versions[0].frequency).toBe('7d'); + expect(versions[1].frequency).toBe('3d'); }); it('new routines start with an empty description', async () => { - const r = await add(projectId, 'clean kitchen', 7, db); + const r = await add(projectId, 'clean kitchen', '7d', db); expect(r.description).toBe(''); }); it('edit is partial: name/frequency edits keep the description and vice-versa', async () => { - const r = await add(projectId, 'clean kitchen', 7, db); + const r = await add(projectId, 'clean kitchen', '7d', db); const described = await edit(r.id, { description: 'wax on, wax off' }, db); expect(described.name).toBe('clean kitchen'); - expect(described.frequencyDays).toBe(7); + expect(described.frequency).toBe('7d'); expect(described.description).toBe('wax on, wax off'); const renamed = await edit(r.id, { name: 'clean the whole kitchen' }, db); expect(renamed.version).toBe(3); expect(renamed.description).toBe('wax on, wax off'); - expect(renamed.frequencyDays).toBe(7); + expect(renamed.frequency).toBe('7d'); - const refreq = await edit(r.id, { frequencyDays: 14 }, db); + const refreq = await edit(r.id, { frequency: '14d' }, db); expect(refreq.version).toBe(4); expect(refreq.name).toBe('clean the whole kitchen'); expect(refreq.description).toBe('wax on, wax off'); @@ -75,7 +75,7 @@ describe('routines service', () => { }); it('archive inserts an archived snapshot without removing history', async () => { - const r = await add(projectId, 'clean kitchen', 7, db); + const r = await add(projectId, 'clean kitchen', '7d', db); await archive(r.id, db); const versions = await loadAllVersions(r.id, db); expect(versions).toHaveLength(2); @@ -92,8 +92,8 @@ describe('routines service', () => { }); it('loadArchived lists archived routines; edits keep the archive date', async () => { - const a = await add(projectId, 'clean kitchen', 7, db); - await add(projectId, 'water plants', 3, db); + const a = await add(projectId, 'clean kitchen', '7d', db); + await add(projectId, 'water plants', '3d', db); await archive(a.id, db); const stamped = (await loadAllVersions(a.id, db))[1].archivedAt; expect(stamped).toBeTruthy(); @@ -105,13 +105,13 @@ describe('routines service', () => { const archived = await loadArchived(projectId, db); expect(archived.map((r) => r.id)).toEqual([a.id]); expect(archived[0].name).toBe('clean kitchen deeply'); - expect(archived[0].frequencyDays).toBe(7); + expect(archived[0].frequency).toBe('7d'); expect(archived[0].archivedAt).toBe(stamped); }); it('loadArchived sorts most recently archived first', async () => { - const a = await add(projectId, 'first', 1, db); - const b = await add(projectId, 'second', 1, db); + const a = await add(projectId, 'first', '1d', db); + const b = await add(projectId, 'second', '1d', db); await archive(a.id, db); await archive(b.id, db); @@ -126,8 +126,8 @@ describe('routines service', () => { }); it('complete records the snapshot version it was made against', async () => { - const r = await add(projectId, 'clean kitchen', 7, db); - await edit(r.id, { frequencyDays: 5 }, db); + const r = await add(projectId, 'clean kitchen', '7d', db); + await edit(r.id, { frequency: '5d' }, db); const doneAt = Temporal.Now.instant().toString({ smallestUnit: 'millisecond', }); @@ -137,7 +137,7 @@ describe('routines service', () => { }); it('dashboard computes urgency and health from completions', async () => { - const r = await add(projectId, 'clean kitchen', 7, db); + const r = await add(projectId, 'clean kitchen', '7d', db); const created = Temporal.Instant.from(r.createdAt); await complete( r.id, @@ -157,9 +157,9 @@ describe('routines service', () => { }); it('overview aggregates per-project health and a global score', async () => { - await add(projectId, 'clean kitchen', 7, db); + await add(projectId, 'clean kitchen', '7d', db); const growth = await addProject('Personal growth', db); - await add(growth.id, 'read 30 minutes', 1, db); + await add(growth.id, 'read 30 minutes', '1d', db); const now = Temporal.Now.instant(); const overview = await loadOverview(now, db); diff --git a/src/lib/server/services/routines.ts b/src/lib/server/services/routines.ts index 57611d6..b1e210b 100644 --- a/src/lib/server/services/routines.ts +++ b/src/lib/server/services/routines.ts @@ -23,7 +23,7 @@ export async function nextRoutineId(db: AppDatabase): Promise { export async function add( projectId: number, name: string, - frequencyDays: number, + frequency: string, db = defaultDb, ): Promise { const id = await nextRoutineId(db); @@ -34,7 +34,7 @@ export async function add( version: 1, projectId, name, - frequencyDays, + frequency, createdAt: Temporal.Now.instant().toString({ smallestUnit: 'millisecond', }), @@ -46,7 +46,7 @@ export async function add( /** Edit = insert a version+1 snapshot, copying unchanged fields forward. */ export async function edit( id: number, - fields: Partial>, + fields: Partial>, db = defaultDb, ): Promise { const [current] = await db @@ -64,7 +64,7 @@ export async function edit( version: current.version + 1, projectId: current.projectId, name: fields.name ?? current.name, - frequencyDays: fields.frequencyDays ?? current.frequencyDays, + frequency: fields.frequency ?? current.frequency, description: fields.description ?? current.description, archived: current.archived, archivedAt: current.archivedAt, @@ -96,7 +96,7 @@ export async function archive(id: number, db = defaultDb): Promise { export interface ArchivedRoutine { id: number; name: string; - frequencyDays: number; + frequency: string; createdAt: string; archivedAt: string; } @@ -125,7 +125,7 @@ export async function loadArchived( .map((r) => ({ id: r.id, name: r.name, - frequencyDays: r.frequencyDays, + frequency: r.frequency, createdAt: r.createdAt, archivedAt: r.archivedAt ?? r.createdAt, })) diff --git a/src/lib/server/services/validation.ts b/src/lib/server/services/validation.ts index b0a515c..a8cebd9 100644 --- a/src/lib/server/services/validation.ts +++ b/src/lib/server/services/validation.ts @@ -1,4 +1,3 @@ -import { parseIntervalToDays } from '$lib/shared/routine-metrics'; import * as v from 'valibot'; export const IdSchema = v.pipe(v.number(), v.integer(), v.minValue(1)); @@ -44,12 +43,3 @@ export function parseOptionalNumber( if (!Number.isFinite(n)) return null; return n; } - -/** Parses an interval like '2w' into days; throws on invalid format. */ -export function parseFrequencyDays(value: FormDataEntryValue | null): number { - const days = parseIntervalToDays(value?.toString() ?? ''); - if (days === null) { - throw new Error('Invalid interval'); - } - return days; -} -- cgit v1.3