From e27f42942c1452efbc183b1fa0e6be2844bc54e5 Mon Sep 17 00:00:00 2001 From: Arnaud Nelissen Date: Mon, 15 Jun 2026 17:05:04 +0200 Subject: [PATCH] feat: feeding heatmap, diaper streak, quick note, handoff summary, weight percentile alert, growth curve labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stats: 7×24 feed density heatmap (Mon–Sun × 24h, indigo intensity, dark mode) - stats: diaper streak card — current consecutive days ≥4 couches + best streak for period - dashboard: QuickNoteWidget — inline journal note (sky card) without leaving dashboard - dashboard: HandoffWidget — localStorage "since last visit" digest (feeds/diapers/sleep) shown after 2h gap - growth: replace S0/S1/S2 x-axis with human labels (Naiss., 1sem, 2sem, 1m, 2m … 1 an) - growth: tooltip shows age in full words (e.g. "3 mois 1 sem.") - cron: POST /api/cron/weight-percentile-alert — alerts when baby weight exits configurable WHO band (weight_percentile_min / weight_percentile_max SystemConfig keys, deduped per measurement) Co-Authored-By: Claude Sonnet 4.6 --- FEATURES.md | 12 +- src/app/(app)/dashboard/page.tsx | 166 +++++++++++++++++- src/app/(app)/growth/page.tsx | 30 +++- src/app/(app)/stats/page.tsx | 109 ++++++++++++ .../api/cron/weight-percentile-alert/route.ts | 117 ++++++++++++ 5 files changed, 419 insertions(+), 15 deletions(-) create mode 100644 src/app/api/cron/weight-percentile-alert/route.ts diff --git a/FEATURES.md b/FEATURES.md index ead2f06..3ff8028 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -65,15 +65,15 @@ 🔲 Feeding pattern AI insight (detect sleep regression, feeding cluster, etc.) 🔲 Contacts / care team directory (pediatrician, midwife, family contact numbers) 🔲 Data retention policy — auto-archive events older than N months -🔲 Offline support / PWA install prompt +✅ Offline support / PWA install prompt — service worker via next-pwa, PNG icons, apple-touch-icon, offline fallback page 🔲 Multi-language UI (FR/EN toggle) ✅ Daily digest push notification — /api/cron/daily-digest: last feed, last diaper, sleep total, next medication due ✅ Baby photo timeline — /photos, grid by day, full-screen lightbox with prev/next nav ✅ Milk stock expiry alert push — /api/cron/milk-expiry: lots expiring within 24h, grouped by family ✅ Batch delete events — "Sélectionner" mode in timeline, checkboxes per row, confirm + bulk delete ✅ Event duration history chart — avg NAP/NIGHT_SLEEP duration per day, dual-line chart in stats -🔲 Feeding session heatmap — 7×24 grid showing feed density by hour/day of week -🔲 Diaper tracker streak — consecutive days with at least N diapers (regression alert) -🔲 Quick note from dashboard — one-tap free text note without navigating to /notes -🔲 Caregiver handoff summary — auto-generated "since last login" digest shown on dashboard login -🔲 Weight percentile push alert — notify when weight drops below/above a configurable WHO percentile band +✅ Feeding session heatmap — 7×24 grid showing feed density by hour/day of week, in stats page +✅ Diaper tracker streak — consecutive days with ≥4 couches; current + best streak in stats +✅ Quick note from dashboard — inline journal note widget (sky card) without leaving dashboard +✅ Caregiver handoff summary — localStorage-based "since last visit" digest on dashboard (feeds/diapers/sleep, shown after 2h gap) +✅ Weight percentile push alert — /api/cron/weight-percentile-alert: alerts when weight outside configurable WHO band (weight_percentile_min / weight_percentile_max SystemConfig keys) diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 370ca2d..e5a785f 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -9,7 +9,8 @@ import { useBaby } from "@/contexts/baby-context"; import { checkFeedAlert, getThresholds } from "@/lib/notifications"; import { format, isToday } from "date-fns"; import { fr } from "date-fns/locale"; -import { AlertTriangle, Clock, CheckCircle2, Pin, PinOff, Pencil, Check, X, Plus, Trash2 } from "lucide-react"; +import { AlertTriangle, Clock, CheckCircle2, Pin, PinOff, Pencil, Check, X, Plus, Trash2, FileText } from "lucide-react"; +import { useSession } from "next-auth/react"; const QUICK_LOG_GROUPS: { label: string; types: EventType[] }[] = [ { @@ -69,6 +70,160 @@ function getEventSummary(event: Event): string { return ""; } +function QuickNoteWidget({ babyId }: { babyId: string }) { + const qc = useQueryClient(); + const todayStr = format(new Date(), "yyyy-MM-dd"); + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(""); + const [saving, setSaving] = useState(false); + const textareaRef = useRef(null); + + const { data: notes = [] } = useQuery>({ + queryKey: ["journal-today", babyId, todayStr], + queryFn: () => fetch(`/api/journal?babyId=${babyId}&date=${todayStr}`).then((r) => r.json()), + }); + const todayNote = notes[0] ?? null; + + function startEdit() { + setDraft(todayNote?.content ?? ""); + setEditing(true); + setTimeout(() => textareaRef.current?.focus(), 50); + } + + async function save() { + if (!draft.trim()) return; + setSaving(true); + if (todayNote) { + await fetch(`/api/journal/${todayNote.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: draft }), + }); + } else { + await fetch("/api/journal", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ babyId, date: todayStr, content: draft }), + }); + } + qc.invalidateQueries({ queryKey: ["journal-today", babyId] }); + setEditing(false); + setSaving(false); + } + + return ( +
+
+
+ + Note du jour +
+ {!editing && ( + + )} +
+ {editing ? ( +
+