feat: feeding heatmap, diaper streak, quick note, handoff summary, weight percentile alert, growth curve labels

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 17:05:04 +02:00
parent b417eb648c
commit e27f42942c
5 changed files with 419 additions and 15 deletions
+24 -6
View File
@@ -101,6 +101,24 @@ function buildSimpleChartData(logs: GrowthLog[], baby: BabyShape, field: "height
const inputCls = "w-full px-3 py-2.5 rounded-lg border border-slate-200 dark:border-slate-700 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100";
function fmtAgeWeeks(weeks: number): string {
if (weeks === 0) return "Naiss.";
if (weeks < 4) return `${weeks}sem`;
const months = Math.round(weeks / 4.345);
if (months >= 12) return "1 an";
return `${months}m`;
}
function fmtAgeLabel(weeks: number): string {
if (weeks === 0) return "Naissance";
if (weeks < 4) return `${weeks} semaine${weeks > 1 ? "s" : ""}`;
const months = Math.floor(weeks / 4.345);
const remWeeks = Math.round(weeks - months * 4.345);
if (months >= 12) return "1 an";
if (remWeeks > 0) return `${months} mois ${remWeeks} sem.`;
return `${months} mois`;
}
function WeightTooltip(props: Record<string, unknown> & { style: React.CSSProperties }) {
const { active, payload, label, style } = props as {
active?: boolean;
@@ -113,7 +131,7 @@ function WeightTooltip(props: Record<string, unknown> & { style: React.CSSProper
if (!baby) return null;
return (
<div style={style} className="px-3 py-2 rounded-lg text-xs">
<p className="text-slate-400 mb-1">Semaine {label}</p>
<p className="text-slate-400 mb-1">{fmtAgeLabel(label ?? 0)}</p>
<p className="font-semibold text-indigo-500">Bébé : {baby.value.toFixed(3)} kg</p>
</div>
);
@@ -341,7 +359,7 @@ export default function GrowthPage() {
<ResponsiveContainer width="100%" height={200}>
<LineChart data={weightData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={(v) => `S${v}`} axisLine={false} tickLine={false} />
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} />
<Tooltip content={(props) => <WeightTooltip {...props} style={tooltipStyle} />} />
{WHO_CURVE_KEYS.map((key, i) => (
@@ -361,9 +379,9 @@ export default function GrowthPage() {
<ResponsiveContainer width="100%" height={160}>
<LineChart data={heightData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={(v) => `S${v}`} axisLine={false} tickLine={false} />
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} domain={["auto", "auto"]} />
<Tooltip formatter={(val) => [`${val} cm`, "Taille"]} contentStyle={tooltipStyle} />
<Tooltip formatter={(val) => [`${val} cm`, "Taille"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} />
<Line dataKey="value" stroke="#0ea5e9" strokeWidth={2.5} dot={{ r: 4, fill: "#0ea5e9", strokeWidth: 2, stroke: "#fff" }} />
</LineChart>
</ResponsiveContainer>
@@ -377,9 +395,9 @@ export default function GrowthPage() {
<ResponsiveContainer width="100%" height={160}>
<LineChart data={headData} margin={{ top: 0, right: 10, left: -20, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke={gridStroke} />
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={(v) => `S${v}`} axisLine={false} tickLine={false} />
<XAxis dataKey="ageWeeks" tick={{ fontSize: 10, fill: "#94a3b8" }} tickFormatter={fmtAgeWeeks} axisLine={false} tickLine={false} />
<YAxis tick={{ fontSize: 10, fill: "#94a3b8" }} axisLine={false} tickLine={false} domain={["auto", "auto"]} />
<Tooltip formatter={(val) => [`${val} cm`, "PC"]} contentStyle={tooltipStyle} />
<Tooltip formatter={(val) => [`${val} cm`, "PC"]} labelFormatter={(v) => fmtAgeLabel(Number(v))} contentStyle={tooltipStyle} />
<Line dataKey="value" stroke="#10b981" strokeWidth={2.5} dot={{ r: 4, fill: "#10b981", strokeWidth: 2, stroke: "#fff" }} />
</LineChart>
</ResponsiveContainer>