feat: running timers, symptom log, search, PDF export
- dashboard: ActiveTimers widget — live h:mm:ss for any in-progress timed event (breastfeed, nap, sleep, walk…), Terminer button patches endedAt
- dashboard: reorganised quick-log groups; Santé group (Température, Traitement, Symptôme)
- events: SYMPTOM type — tag chips (Fièvre, Reflux, Colique, Éruption, Toux, Congestion, Diarrhée, Vomissement, Irritabilité, Pleurs excessifs), stored in metadata.tags
- prisma: migration adding SYMPTOM to EventType enum
- search: GET /api/search (events by notes ILIKE, journal by content ILIKE), /search page with debounced input + keyword highlight
- nav: Recherche link added to LINKS + More drawer
- growth: PDF export button — generates formatted table with all measurements, zebra rows, downloads croissance-{name}.pdf
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
||||
import { useBaby } from "@/contexts/baby-context";
|
||||
import { format, subDays } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
import { Plus, Pencil, Trash2 } from "lucide-react";
|
||||
import { Plus, Pencil, Trash2, FileDown } from "lucide-react";
|
||||
|
||||
interface GrowthLog {
|
||||
id: string;
|
||||
@@ -241,6 +241,53 @@ export default function GrowthPage() {
|
||||
qc.invalidateQueries({ queryKey: ["growth"] });
|
||||
}
|
||||
|
||||
function exportPDF() {
|
||||
import("jspdf").then(({ default: jsPDF }) => {
|
||||
const doc = new jsPDF();
|
||||
const pageW = doc.internal.pageSize.getWidth();
|
||||
|
||||
doc.setFontSize(18);
|
||||
doc.setTextColor(30, 41, 59);
|
||||
doc.text("Courbe de croissance", 14, 20);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(100, 116, 139);
|
||||
doc.text(`${baby!.name} · né${baby!.gender === "F" ? "e" : ""} le ${format(new Date(baby!.birthDate), "d MMMM yyyy", { locale: fr })}`, 14, 28);
|
||||
doc.text(`Généré le ${format(new Date(), "d MMMM yyyy", { locale: fr })}`, 14, 34);
|
||||
|
||||
// Table header
|
||||
const COL = [14, 55, 95, 130, 155];
|
||||
const headers = ["Date", "Poids (kg)", "Taille (cm)", "PC (cm)", "Notes"];
|
||||
let y = 46;
|
||||
|
||||
doc.setFillColor(238, 242, 255);
|
||||
doc.rect(14, y - 5, pageW - 28, 9, "F");
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(79, 70, 229);
|
||||
headers.forEach((h, i) => doc.text(h, COL[i], y));
|
||||
y += 8;
|
||||
|
||||
doc.setFontSize(9);
|
||||
const sorted = [...logs].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
|
||||
sorted.forEach((log, idx) => {
|
||||
if (y > 270) { doc.addPage(); y = 20; }
|
||||
if (idx % 2 === 0) {
|
||||
doc.setFillColor(248, 250, 252);
|
||||
doc.rect(14, y - 4, pageW - 28, 7, "F");
|
||||
}
|
||||
doc.setTextColor(30, 41, 59);
|
||||
doc.text(format(new Date(log.date), "d MMM yyyy", { locale: fr }), COL[0], y);
|
||||
doc.text(log.weight ? (log.weight / 1000).toFixed(3) : "—", COL[1], y);
|
||||
doc.text(log.height ? String(log.height) : "—", COL[2], y);
|
||||
doc.text(log.headCirc ? String(log.headCirc) : "—", COL[3], y);
|
||||
if (log.notes) doc.text(log.notes.slice(0, 30), COL[4], y);
|
||||
y += 7;
|
||||
});
|
||||
|
||||
doc.save(`croissance-${baby!.name.toLowerCase()}.pdf`);
|
||||
});
|
||||
}
|
||||
|
||||
const latest = logs.at(-1);
|
||||
|
||||
const rangeOptions: { label: string; value: TimeRange }[] = [
|
||||
@@ -257,13 +304,22 @@ export default function GrowthPage() {
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">Courbes de croissance</h1>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400 mt-0.5">{baby?.name}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => showForm ? setShowForm(false) : openAddForm()}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Mesure
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
{logs.length > 0 && (
|
||||
<button onClick={exportPDF}
|
||||
className="flex items-center gap-2 px-3 py-2 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-300 text-sm font-medium rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition">
|
||||
<FileDown className="w-4 h-4" />
|
||||
PDF
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => showForm ? setShowForm(false) : openAddForm()}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white text-sm font-medium rounded-lg hover:bg-indigo-700 transition"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Mesure
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add measurement form */}
|
||||
|
||||
Reference in New Issue
Block a user