split meds_taken into morning/evening doses
All checks were successful
Build & Deploy / deploy (push) Successful in 1m30s
All checks were successful
Build & Deploy / deploy (push) Successful in 1m30s
Arthur has to take his meds once in the morning and once in the evening, so the daily check-in now tracks both doses independently. The dashboard shows two sliders (Matin / Soir), the API toggle accepts a slot, and the AI toggle_daily_checkin function takes an optional slot argument so the LLM can target the right dose when the user specifies a moment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
810575221f
commit
a1eeca1236
@ -314,7 +314,8 @@ create table google_oauth_tokens ( -- single-row pour Arthur
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
create table daily_checkins (
|
create table daily_checkins (
|
||||||
day date primary key,
|
day date primary key,
|
||||||
meds_taken boolean default false,
|
meds_morning boolean default false, -- prise du matin
|
||||||
|
meds_evening boolean default false, -- prise du soir
|
||||||
note text,
|
note text,
|
||||||
updated_at timestamptz default now()
|
updated_at timestamptz default now()
|
||||||
);
|
);
|
||||||
@ -395,8 +396,8 @@ POST /agenda/google/sync
|
|||||||
GET /agenda/ical/:secret.ics → text/calendar
|
GET /agenda/ical/:secret.ics → text/calendar
|
||||||
|
|
||||||
# HEALTH
|
# HEALTH
|
||||||
GET /health-tab/today { day, meds_taken, note }
|
GET /health-tab/today { day, meds_morning, meds_evening, note }
|
||||||
POST /health-tab/today/toggle → flips meds_taken, returns row
|
POST /health-tab/today/toggle { slot?: 'morning'|'evening', value?: bool, note? } → row mise à jour
|
||||||
GET /health-tab/history?days=30
|
GET /health-tab/history?days=30
|
||||||
|
|
||||||
# AI
|
# AI
|
||||||
@ -420,7 +421,7 @@ type ProposedAction =
|
|||||||
| { fn: 'add_project_idea', args: { project_id, content } }
|
| { fn: 'add_project_idea', args: { project_id, content } }
|
||||||
| { fn: 'add_project_step', args: { project_id, title, status? } }
|
| { fn: 'add_project_step', args: { project_id, title, status? } }
|
||||||
| { fn: 'create_calendar_event', args: { title, starts_at, ends_at, location?, description? } }
|
| { fn: 'create_calendar_event', args: { title, starts_at, ends_at, location?, description? } }
|
||||||
| { fn: 'toggle_daily_checkin', args: { note? } };
|
| { fn: 'toggle_daily_checkin', args: { slot?: 'morning'|'evening'|'both', note? } };
|
||||||
```
|
```
|
||||||
|
|
||||||
Flow garanti : l'API **ne jamais** exécute une action directement. Elle renvoie un `ProposedAction[]` à la PWA, qui affiche une modal de confirmation. Seul `/ai/command/confirm` écrit en DB.
|
Flow garanti : l'API **ne jamais** exécute une action directement. Elle renvoie un `ProposedAction[]` à la PWA, qui affiche une modal de confirmation. Seul `/ai/command/confirm` écrit en DB.
|
||||||
|
|||||||
@ -231,20 +231,50 @@ export class AiService {
|
|||||||
|
|
||||||
case "toggle_daily_checkin": {
|
case "toggle_daily_checkin": {
|
||||||
const today = new Date().toLocaleDateString("fr-CA", { timeZone: "Europe/Paris" });
|
const today = new Date().toLocaleDateString("fr-CA", { timeZone: "Europe/Paris" });
|
||||||
|
const slot = action.args.slot ?? "both";
|
||||||
|
|
||||||
|
// INSERT : on part de false/false puis on coche le(s) slot(s) concerné(s).
|
||||||
|
const insertMorning = slot === "morning" || slot === "both";
|
||||||
|
const insertEvening = slot === "evening" || slot === "both";
|
||||||
|
|
||||||
|
// UPDATE on conflict :
|
||||||
|
// - slot "both" → force les 2 à true (sémantique "j'ai tout pris")
|
||||||
|
// - slot spécifique → flip uniquement ce slot
|
||||||
|
const updateSet =
|
||||||
|
slot === "both"
|
||||||
|
? {
|
||||||
|
medsMorning: sql`true`,
|
||||||
|
medsEvening: sql`true`,
|
||||||
|
note: action.args.note ?? null,
|
||||||
|
updatedAt: sql`now()`,
|
||||||
|
}
|
||||||
|
: slot === "morning"
|
||||||
|
? {
|
||||||
|
medsMorning: sql`NOT ${dailyCheckins.medsMorning}`,
|
||||||
|
note: action.args.note ?? null,
|
||||||
|
updatedAt: sql`now()`,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
medsEvening: sql`NOT ${dailyCheckins.medsEvening}`,
|
||||||
|
note: action.args.note ?? null,
|
||||||
|
updatedAt: sql`now()`,
|
||||||
|
};
|
||||||
|
|
||||||
const [row] = await this.db
|
const [row] = await this.db
|
||||||
.insert(dailyCheckins)
|
.insert(dailyCheckins)
|
||||||
.values({ day: today, medsTaken: true, note: action.args.note ?? null })
|
.values({
|
||||||
|
day: today,
|
||||||
|
medsMorning: insertMorning,
|
||||||
|
medsEvening: insertEvening,
|
||||||
|
note: action.args.note ?? null,
|
||||||
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: dailyCheckins.day,
|
target: dailyCheckins.day,
|
||||||
set: {
|
set: updateSet,
|
||||||
medsTaken: sql`NOT ${dailyCheckins.medsTaken}`,
|
|
||||||
note: action.args.note ?? null,
|
|
||||||
updatedAt: sql`now()`,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
if (!row) throw new Error("Upsert daily_checkin: aucune ligne renvoyée");
|
if (!row) throw new Error("Upsert daily_checkin: aucune ligne renvoyée");
|
||||||
return { day: row.day, meds_taken: row.medsTaken };
|
return { day: row.day, meds_morning: row.medsMorning, meds_evening: row.medsEvening };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -95,10 +95,16 @@ export const MISTRAL_TOOLS = [
|
|||||||
function: {
|
function: {
|
||||||
name: "toggle_daily_checkin",
|
name: "toggle_daily_checkin",
|
||||||
description:
|
description:
|
||||||
"Marque le check-in quotidien d'Arthur (médocs pris, état du jour). À utiliser sur 'j'ai pris mes médocs', 'check in'.",
|
"Marque le check-in quotidien d'Arthur (médocs pris matin/soir, état du jour). Arthur doit prendre ses médicaments une fois le matin et une fois le soir — précise `slot` si c'est spécifiquement la prise du matin ou celle du soir.",
|
||||||
parameters: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
properties: {
|
properties: {
|
||||||
|
slot: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["morning", "evening", "both"],
|
||||||
|
description:
|
||||||
|
"Quelle prise valider. 'morning' = médocs du matin, 'evening' = médocs du soir, 'both' = les deux (si ambigu / formulation globale). Défaut : 'both'.",
|
||||||
|
},
|
||||||
note: { type: "string" },
|
note: { type: "string" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -174,7 +180,7 @@ const SYSTEM_PROMPT = `Tu es l'assistant personnel d'Arthur dans ordinarthur-os.
|
|||||||
## Autres règles
|
## Autres règles
|
||||||
|
|
||||||
- Formulations déclaratives d'agenda : "j'ai rendez-vous", "je dois aller", "j'ai un rdv", "dans mon agenda" → create_calendar_event.
|
- Formulations déclaratives d'agenda : "j'ai rendez-vous", "je dois aller", "j'ai un rdv", "dans mon agenda" → create_calendar_event.
|
||||||
- "j'ai pris mes médocs", "médicaments pris" → toggle_daily_checkin.
|
- "j'ai pris mes médocs", "médicaments pris" → toggle_daily_checkin. Arthur prend ses médocs matin + soir : si la phrase précise le moment ("du matin", "ce matin", "du soir", "ce soir"), pose slot en conséquence ('morning' ou 'evening') ; sinon laisse 'both'.
|
||||||
- Pour create_calendar_event : si la durée n'est pas précisée, suppose 1h.
|
- Pour create_calendar_event : si la durée n'est pas précisée, suppose 1h.
|
||||||
- Dates : convertis les expressions relatives ('demain', 'vendredi soir', 'dans 2h') en ISO 8601 avec l'offset Europe/Paris (+02:00 en été, +01:00 en hiver). Ex : "2024-04-16T17:40:00+02:00". Ne jamais utiliser UTC si l'utilisateur parle d'une heure locale.`;
|
- Dates : convertis les expressions relatives ('demain', 'vendredi soir', 'dans 2h') en ISO 8601 avec l'offset Europe/Paris (+02:00 en été, +01:00 en hiver). Ex : "2024-04-16T17:40:00+02:00". Ne jamais utiliser UTC si l'utilisateur parle d'une heure locale.`;
|
||||||
|
|
||||||
|
|||||||
@ -33,14 +33,17 @@ export class HealthTabService {
|
|||||||
// tant qu'Arthur ne coche rien.
|
// tant qu'Arthur ne coche rien.
|
||||||
return {
|
return {
|
||||||
day,
|
day,
|
||||||
meds_taken: false,
|
meds_morning: false,
|
||||||
|
meds_evening: false,
|
||||||
note: null,
|
note: null,
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upsert sur la ligne du jour. Sans `meds_taken` explicite, flip la valeur.
|
* Upsert sur la ligne du jour.
|
||||||
|
* - Avec `slot` : flip (ou set à `value`) uniquement cette prise.
|
||||||
|
* - Sans `slot` : met à jour uniquement la note (rétro-compat / future extension).
|
||||||
*/
|
*/
|
||||||
async toggle(patch: DailyCheckinToggleRequest): Promise<DailyCheckin> {
|
async toggle(patch: DailyCheckinToggleRequest): Promise<DailyCheckin> {
|
||||||
const day = todayInParis();
|
const day = todayInParis();
|
||||||
@ -49,19 +52,37 @@ export class HealthTabService {
|
|||||||
.from(dailyCheckins)
|
.from(dailyCheckins)
|
||||||
.where(eq(dailyCheckins.day, day));
|
.where(eq(dailyCheckins.day, day));
|
||||||
|
|
||||||
const currentMeds = existing[0]?.medsTaken ?? false;
|
const currentMorning = existing[0]?.medsMorning ?? false;
|
||||||
|
const currentEvening = existing[0]?.medsEvening ?? false;
|
||||||
const currentNote = existing[0]?.note ?? null;
|
const currentNote = existing[0]?.note ?? null;
|
||||||
|
|
||||||
const nextMeds = patch.meds_taken ?? !currentMeds;
|
let nextMorning = currentMorning;
|
||||||
|
let nextEvening = currentEvening;
|
||||||
|
if (patch.slot === "morning") {
|
||||||
|
nextMorning = patch.value ?? !currentMorning;
|
||||||
|
} else if (patch.slot === "evening") {
|
||||||
|
nextEvening = patch.value ?? !currentEvening;
|
||||||
|
}
|
||||||
const nextNote = patch.note !== undefined ? patch.note : currentNote;
|
const nextNote = patch.note !== undefined ? patch.note : currentNote;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
const [row] = await this.db
|
const [row] = await this.db
|
||||||
.insert(dailyCheckins)
|
.insert(dailyCheckins)
|
||||||
.values({ day, medsTaken: nextMeds, note: nextNote, updatedAt: now })
|
.values({
|
||||||
|
day,
|
||||||
|
medsMorning: nextMorning,
|
||||||
|
medsEvening: nextEvening,
|
||||||
|
note: nextNote,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
target: dailyCheckins.day,
|
target: dailyCheckins.day,
|
||||||
set: { medsTaken: nextMeds, note: nextNote, updatedAt: now },
|
set: {
|
||||||
|
medsMorning: nextMorning,
|
||||||
|
medsEvening: nextEvening,
|
||||||
|
note: nextNote,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@ -73,7 +94,8 @@ export class HealthTabService {
|
|||||||
function rowToCheckin(row: typeof dailyCheckins.$inferSelect): DailyCheckin {
|
function rowToCheckin(row: typeof dailyCheckins.$inferSelect): DailyCheckin {
|
||||||
return {
|
return {
|
||||||
day: row.day,
|
day: row.day,
|
||||||
meds_taken: row.medsTaken,
|
meds_morning: row.medsMorning,
|
||||||
|
meds_evening: row.medsEvening,
|
||||||
note: row.note ?? null,
|
note: row.note ?? null,
|
||||||
updated_at: row.updatedAt.toISOString(),
|
updated_at: row.updatedAt.toISOString(),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -280,8 +280,14 @@ function summarize(action: ProposedAction): string {
|
|||||||
return `${action.args.title}${action.args.status ? ` · ${action.args.status}` : ""}`;
|
return `${action.args.title}${action.args.status ? ` · ${action.args.status}` : ""}`;
|
||||||
case "create_calendar_event":
|
case "create_calendar_event":
|
||||||
return `${action.args.title} · ${formatDatetime(action.args.starts_at)} → ${formatTime(action.args.ends_at)}`;
|
return `${action.args.title} · ${formatDatetime(action.args.starts_at)} → ${formatTime(action.args.ends_at)}`;
|
||||||
case "toggle_daily_checkin":
|
case "toggle_daily_checkin": {
|
||||||
return action.args.note ?? "Check-in du jour";
|
const slot = action.args.slot ?? "both";
|
||||||
|
const slotLabel =
|
||||||
|
slot === "morning" ? "médocs matin"
|
||||||
|
: slot === "evening" ? "médocs soir"
|
||||||
|
: "médocs (matin + soir)";
|
||||||
|
return action.args.note ? `${slotLabel} · ${action.args.note}` : slotLabel;
|
||||||
|
}
|
||||||
case "capture_idea":
|
case "capture_idea":
|
||||||
return action.args.content;
|
return action.args.content;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { DailyCheckin } from "@ordinarthur-os/shared";
|
import type { DailyCheckin, MedsSlot } from "@ordinarthur-os/shared";
|
||||||
import { api } from "@/api/client";
|
import { api } from "@/api/client";
|
||||||
import { Label } from "@/design";
|
import { Label } from "@/design";
|
||||||
import { MedsSlider } from "./MedsSlider";
|
import { MedsSlider } from "./MedsSlider";
|
||||||
@ -13,27 +13,60 @@ export function MedsSection() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const toggle = useMutation({
|
const toggle = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: (slot: MedsSlot) =>
|
||||||
api<DailyCheckin>("/health-tab/today/toggle", { method: "POST", body: "{}" }),
|
api<DailyCheckin>("/health-tab/today/toggle", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ slot }),
|
||||||
|
}),
|
||||||
onSuccess: (updated) => qc.setQueryData(["health-tab", "today"], updated),
|
onSuccess: (updated) => qc.setQueryData(["health-tab", "today"], updated),
|
||||||
});
|
});
|
||||||
|
|
||||||
const medsTaken = data?.meds_taken ?? false;
|
const morningTaken = data?.meds_morning ?? false;
|
||||||
|
const eveningTaken = data?.meds_evening ?? false;
|
||||||
|
const bothTaken = morningTaken && eveningTaken;
|
||||||
|
const pendingSlot = toggle.isPending ? toggle.variables : null;
|
||||||
|
|
||||||
|
const statusText = isLoading
|
||||||
|
? "…"
|
||||||
|
: bothTaken
|
||||||
|
? "Matin + soir pris"
|
||||||
|
: morningTaken
|
||||||
|
? "Matin pris · soir en attente"
|
||||||
|
: eveningTaken
|
||||||
|
? "Soir pris · matin en attente"
|
||||||
|
: "Non pris";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="border border-ink">
|
<section className="border border-ink">
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-ink">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-ink">
|
||||||
<Label prefix="[ 02 ]">MÉDOCS DU JOUR</Label>
|
<Label prefix="[ 02 ]">MÉDOCS DU JOUR</Label>
|
||||||
<span className={`font-mono text-[11px] uppercase tracking-label ${medsTaken ? "text-accent" : "text-muted"}`}>
|
<span
|
||||||
{isLoading ? "…" : medsTaken ? "Pris" : "Non pris"}
|
className={`font-mono text-[11px] uppercase tracking-label ${bothTaken ? "text-accent" : "text-muted"}`}
|
||||||
|
>
|
||||||
|
{statusText}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MedsSlider
|
<div className="divide-y divide-ink">
|
||||||
medsTaken={medsTaken}
|
<div className="px-4 py-3 space-y-2">
|
||||||
onToggle={() => toggle.mutate()}
|
<Label className="block">MATIN</Label>
|
||||||
disabled={toggle.isPending || isLoading}
|
<MedsSlider
|
||||||
/>
|
medsTaken={morningTaken}
|
||||||
|
onToggle={() => toggle.mutate("morning")}
|
||||||
|
disabled={(toggle.isPending && pendingSlot === "morning") || isLoading}
|
||||||
|
slotLabel="matin"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-3 space-y-2">
|
||||||
|
<Label className="block">SOIR</Label>
|
||||||
|
<MedsSlider
|
||||||
|
medsTaken={eveningTaken}
|
||||||
|
onToggle={() => toggle.mutate("evening")}
|
||||||
|
disabled={(toggle.isPending && pendingSlot === "evening") || isLoading}
|
||||||
|
slotLabel="soir"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,9 +6,10 @@ interface Props {
|
|||||||
medsTaken: boolean;
|
medsTaken: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
slotLabel?: string; // ex: "matin" / "soir" — utilisé dans les textes du slider
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MedsSlider({ medsTaken, onToggle, disabled = false }: Props) {
|
export function MedsSlider({ medsTaken, onToggle, disabled = false, slotLabel }: Props) {
|
||||||
const trackRef = useRef<HTMLDivElement>(null);
|
const trackRef = useRef<HTMLDivElement>(null);
|
||||||
const [dragX, setDragX] = useState(0);
|
const [dragX, setDragX] = useState(0);
|
||||||
const dragging = useRef(false);
|
const dragging = useRef(false);
|
||||||
@ -68,7 +69,7 @@ export function MedsSlider({ medsTaken, onToggle, disabled = false }: Props) {
|
|||||||
className="font-mono text-[11px] uppercase tracking-label text-ink"
|
className="font-mono text-[11px] uppercase tracking-label text-ink"
|
||||||
style={{ opacity: Math.max(0, 1 - progress * 2.5) }}
|
style={{ opacity: Math.max(0, 1 - progress * 2.5) }}
|
||||||
>
|
>
|
||||||
Glisse → médicaments pris
|
Glisse → médocs {slotLabel ?? "pris"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!medsTaken && progress > 0.3 && (
|
{!medsTaken && progress > 0.3 && (
|
||||||
@ -81,7 +82,7 @@ export function MedsSlider({ medsTaken, onToggle, disabled = false }: Props) {
|
|||||||
)}
|
)}
|
||||||
{medsTaken && (
|
{medsTaken && (
|
||||||
<span className="font-mono text-[11px] uppercase tracking-label text-bg">
|
<span className="font-mono text-[11px] uppercase tracking-label text-bg">
|
||||||
✓ Médicaments pris — tap pour annuler
|
✓ Médocs {slotLabel ?? ""} pris — tap pour annuler
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
15
packages/db/migrations/0008_meds_morning_evening.sql
Normal file
15
packages/db/migrations/0008_meds_morning_evening.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
-- 0008_meds_morning_evening.sql — split meds_taken en matin / soir
|
||||||
|
set search_path to ordinarthur_os, public;
|
||||||
|
|
||||||
|
alter table ordinarthur_os.daily_checkins
|
||||||
|
add column if not exists meds_morning boolean not null default false,
|
||||||
|
add column if not exists meds_evening boolean not null default false;
|
||||||
|
|
||||||
|
-- Migrer l'ancien meds_taken : s'il était true on coche les 2 prises,
|
||||||
|
-- sinon on reste par défaut à false/false.
|
||||||
|
update ordinarthur_os.daily_checkins
|
||||||
|
set meds_morning = true, meds_evening = true
|
||||||
|
where meds_taken = true;
|
||||||
|
|
||||||
|
alter table ordinarthur_os.daily_checkins
|
||||||
|
drop column if exists meds_taken;
|
||||||
@ -3,7 +3,8 @@ import { appSchema } from "./_schema";
|
|||||||
|
|
||||||
export const dailyCheckins = appSchema.table("daily_checkins", {
|
export const dailyCheckins = appSchema.table("daily_checkins", {
|
||||||
day: date("day").primaryKey(),
|
day: date("day").primaryKey(),
|
||||||
medsTaken: boolean("meds_taken").notNull().default(false),
|
medsMorning: boolean("meds_morning").notNull().default(false),
|
||||||
|
medsEvening: boolean("meds_evening").notNull().default(false),
|
||||||
note: text("note"),
|
note: text("note"),
|
||||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -328,6 +328,7 @@ export const ProposedAction = z.discriminatedUnion("fn", [
|
|||||||
z.object({
|
z.object({
|
||||||
fn: z.literal("toggle_daily_checkin"),
|
fn: z.literal("toggle_daily_checkin"),
|
||||||
args: z.object({
|
args: z.object({
|
||||||
|
slot: z.enum(["morning", "evening", "both"]).optional(),
|
||||||
note: z.string().optional(),
|
note: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
@ -437,16 +438,21 @@ export type GoogleSyncResponse = z.infer<typeof GoogleSyncResponse>;
|
|||||||
// Phase 7 — Daily check-in (médocs + note)
|
// Phase 7 — Daily check-in (médocs + note)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const MedsSlot = z.enum(["morning", "evening"]);
|
||||||
|
export type MedsSlot = z.infer<typeof MedsSlot>;
|
||||||
|
|
||||||
export const DailyCheckin = z.object({
|
export const DailyCheckin = z.object({
|
||||||
day: z.string(), // YYYY-MM-DD
|
day: z.string(), // YYYY-MM-DD
|
||||||
meds_taken: z.boolean(),
|
meds_morning: z.boolean(),
|
||||||
|
meds_evening: z.boolean(),
|
||||||
note: z.string().nullable(),
|
note: z.string().nullable(),
|
||||||
updated_at: z.string(),
|
updated_at: z.string(),
|
||||||
});
|
});
|
||||||
export type DailyCheckin = z.infer<typeof DailyCheckin>;
|
export type DailyCheckin = z.infer<typeof DailyCheckin>;
|
||||||
|
|
||||||
export const DailyCheckinToggleRequest = z.object({
|
export const DailyCheckinToggleRequest = z.object({
|
||||||
meds_taken: z.boolean().optional(),
|
slot: MedsSlot.optional(), // si absent : bascule les deux slots (note-only)
|
||||||
|
value: z.boolean().optional(), // si absent : flip la valeur actuelle du slot
|
||||||
note: z.string().nullable().optional(),
|
note: z.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
export type DailyCheckinToggleRequest = z.infer<typeof DailyCheckinToggleRequest>;
|
export type DailyCheckinToggleRequest = z.infer<typeof DailyCheckinToggleRequest>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user