aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/components/ScoreBadge.svelte13
-rw-r--r--src/lib/components/StatusBadge.svelte26
-rw-r--r--src/lib/server/services/routine-actions.ts86
-rw-r--r--src/lib/server/services/validation.ts10
4 files changed, 135 insertions, 0 deletions
diff --git a/src/lib/components/ScoreBadge.svelte b/src/lib/components/ScoreBadge.svelte
new file mode 100644
index 0000000..6384667
--- /dev/null
+++ b/src/lib/components/ScoreBadge.svelte
@@ -0,0 +1,13 @@
+<script lang="ts">
+ let { score }: { score: number } = $props();
+
+ const className = $derived(
+ score >= 80
+ ? 'score bg-success'
+ : score >= 50
+ ? 'score bg-accent'
+ : 'score bg-danger',
+ );
+</script>
+
+<span class={className}>{score}</span>
diff --git a/src/lib/components/StatusBadge.svelte b/src/lib/components/StatusBadge.svelte
new file mode 100644
index 0000000..babcbfa
--- /dev/null
+++ b/src/lib/components/StatusBadge.svelte
@@ -0,0 +1,26 @@
+<script lang="ts">
+ import {
+ STATUS_LABELS,
+ type RoutineStatus,
+ } from '$lib/shared/routine-metrics';
+
+ let {
+ status,
+ overdueDays = 0,
+ }: { status: RoutineStatus; overdueDays?: number } = $props();
+
+ const className = $derived(
+ status === 'ok'
+ ? 'status bg-success text-white'
+ : status === 'due'
+ ? 'status bg-accent-soft text-accent'
+ : status === 'late'
+ ? 'status bg-accent text-white'
+ : 'status bg-danger text-white',
+ );
+</script>
+
+<span class={className}>
+ {STATUS_LABELS[status]}{#if overdueDays > 0}
+ ({overdueDays} j){/if}
+</span>
diff --git a/src/lib/server/services/routine-actions.ts b/src/lib/server/services/routine-actions.ts
new file mode 100644
index 0000000..b546ec7
--- /dev/null
+++ b/src/lib/server/services/routine-actions.ts
@@ -0,0 +1,86 @@
+import { fail, type ActionFailure } from '@sveltejs/kit';
+import { archive, complete, edit, loadCurrent } from './routines';
+import { parseFormString, parseFrequencyDays } from './validation';
+
+/**
+ * Shared implementations of the routine edit/complete form actions, used by
+ * both the project dashboard (/projects/[id]) and the routine page
+ * (/projects/[projectId]/routine/[routineId]). Guarding and failure messages
+ * are the strictest common denominator: a missing or archived routine is
+ * rejected before any write.
+ */
+
+export type EditRoutineResult =
+ ActionFailure<{ error: string }> | { success: boolean; type?: 'edit' };
+
+export async function editRoutineAction(
+ data: FormData,
+ routineId: number,
+ extra?: { type: 'edit' },
+): Promise<EditRoutineResult> {
+ const current = await loadCurrent(routineId);
+ if (!current) {
+ return fail(400, { error: 'Routine introuvable' });
+ }
+ if (current.archived) {
+ return fail(400, { error: 'Routine archivée' });
+ }
+
+ const name = parseFormString(data.get('name'));
+ if (!name) {
+ return fail(400, { error: 'Le nom est requis' });
+ }
+
+ let frequencyDays: number;
+ try {
+ frequencyDays = parseFrequencyDays(data.get('frequencyDays'));
+ } catch {
+ return fail(400, { error: 'Fréquence invalide' });
+ }
+
+ // The dashboard row form has no description field; only touch it when the
+ // form actually sends one, so list edits never wipe a description.
+ const fields: { name: string; frequencyDays: number; description?: string } =
+ { name, frequencyDays };
+ const description = data.get('description');
+ if (description !== null) {
+ fields.description = description.toString();
+ }
+
+ try {
+ await edit(routineId, fields);
+ } catch {
+ return fail(400, { error: 'Routine introuvable' });
+ }
+ return { success: true, type: extra?.type };
+}
+
+export async function completeRoutineAction(data: FormData, routineId: number) {
+ const current = await loadCurrent(routineId);
+ if (!current) {
+ return fail(400, { error: 'Routine introuvable' });
+ }
+ if (current.archived) {
+ return fail(400, { error: 'Routine archivée' });
+ }
+
+ try {
+ await complete(
+ routineId,
+ parseFormString(data.get('note')),
+ new Date().toISOString(),
+ );
+ } catch {
+ return fail(400, { error: 'Routine introuvable' });
+ }
+ return { success: true };
+}
+
+export async function archiveRoutineAction(routineId: number) {
+ try {
+ await archive(routineId);
+ } catch {
+ return fail(400, { error: 'Routine introuvable' });
+ }
+ return { success: true };
+}
diff --git a/src/lib/server/services/validation.ts b/src/lib/server/services/validation.ts
index a8cebd9..69820ea 100644
--- a/src/lib/server/services/validation.ts
+++ b/src/lib/server/services/validation.ts
@@ -1,3 +1,4 @@
+import { parseIntervalToDays } from '$lib/shared/routine-metrics';
import * as v from 'valibot';
export const IdSchema = v.pipe(v.number(), v.integer(), v.minValue(1));
@@ -43,3 +44,12 @@ export function parseOptionalNumber(
if (!Number.isFinite(n)) return null;
return n;
}
+
+/** Parses an interval like '2w' into days; throws when the format is invalid. */
+export function parseFrequencyDays(value: FormDataEntryValue | null): number {
+ const days = parseIntervalToDays(value?.toString() ?? '');
+ if (days === null) {
+ throw new Error('Invalid interval');
+ }
+ return days;
+}