feat: pump tracker, photo log, milk stock, medication profiles, calendar, reminders, audit log, edit events

New pages:
- /calendar — monthly grid with event dots, day detail on tap
- /medications — CRUD for medication profiles (presets + custom, dose/unit/intervals/crisis mode)
- /milk — milk stock management with location-based expiry, alerts, mark-as-used
- /reminders — recurring medication reminder CRUD with Pushover notifications

New APIs:
- /api/medication-profiles — profiles CRUD + last-intakes endpoint (auto-seeds 10 presets per family)
- /api/milk — milk stock CRUD with auto-calculated expiry
- /api/reminders + /api/reminders/check — reminder CRUD + cron-callable check endpoint
- /api/upload — multipart photo upload to public/uploads/
- /api/invite/send-email — email invitation with family invite link
- /api/admin/audit — last 100 audit log entries (superadmin only)

Event modal improvements:
- PUMP event type (side selector + volume + timer)
- MEDICATION: profile chip picker, next-dose timing status, crisis mode toggle
- Photo attachment (upload + thumbnail preview)
- Datetime inputs split into date + time (iOS Safari datetime-local fix)
- Edit mode via initialEvent prop (pre-fills all fields, saves via PATCH)
- Timer now shows h:mm:ss when >= 1 hour

Timeline:
- Modify button opens pre-filled edit modal per event
- Photo thumbnail in expanded view

Dashboard:
- PUMP in quick-log
- Medication status card (too-soon / available with next-dose time)

Schema additions: MedicationProfile, MedcationReminder, MilkStock, AuditLog models

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 15:01:38 +02:00
parent 091af949ee
commit 020539e2e2
31 changed files with 2206 additions and 63 deletions
@@ -0,0 +1,40 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
export async function PATCH(
req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
const { id } = await params;
const body = await req.json();
const profile = await prisma.medicationProfile.update({
where: { id },
data: {
...(body.name !== undefined ? { name: body.name } : {}),
...(body.molecule !== undefined ? { molecule: body.molecule } : {}),
...(body.defaultDose !== undefined ? { defaultDose: body.defaultDose } : {}),
...(body.unit !== undefined ? { unit: body.unit } : {}),
...(body.intervalHours !== undefined ? { intervalHours: parseFloat(body.intervalHours) } : {}),
...(body.minIntervalHours !== undefined ? { minIntervalHours: body.minIntervalHours ? parseFloat(body.minIntervalHours) : null } : {}),
},
});
return NextResponse.json(profile);
}
export async function DELETE(
_req: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
const { id } = await params;
await prisma.medicationProfile.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
}
@@ -0,0 +1,39 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
// Returns the most recent MEDICATION event per medication name for a given baby.
// Used to compute next-allowed-dose times in the UI.
export async function GET(req: Request) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
const { searchParams } = new URL(req.url);
const babyId = searchParams.get("babyId");
if (!babyId) return NextResponse.json({ error: "babyId requis" }, { status: 400 });
// Fetch last 100 medication events — enough to find latest per med
const events = await prisma.event.findMany({
where: { babyId, type: "MEDICATION" },
orderBy: { startedAt: "desc" },
take: 100,
select: { id: true, startedAt: true, metadata: true },
});
// Deduplicate: keep only the latest per medication name
const latest: Record<string, { id: string; startedAt: Date; dose?: string; unit?: string }> = {};
for (const ev of events) {
const meta = ev.metadata as Record<string, unknown> | null;
const name = String(meta?.name ?? "");
if (name && !latest[name]) {
latest[name] = {
id: ev.id,
startedAt: ev.startedAt,
dose: meta?.dose ? String(meta.dose) : undefined,
unit: meta?.unit ? String(meta.unit) : undefined,
};
}
}
return NextResponse.json(latest);
}
+67
View File
@@ -0,0 +1,67 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { MEDICATION_PRESETS } from "@/lib/medication-presets";
export async function GET() {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { familyId: true } });
if (!user?.familyId) return NextResponse.json([]);
const profiles = await prisma.medicationProfile.findMany({
where: { familyId: user.familyId },
orderBy: [{ isPreset: "desc" }, { name: "asc" }],
});
// Auto-seed presets for this family if none exist yet
if (profiles.length === 0) {
await prisma.medicationProfile.createMany({
data: MEDICATION_PRESETS.map((p) => ({
familyId: user.familyId!,
name: p.name,
molecule: p.molecule,
defaultDose: p.defaultDose ?? null,
unit: p.unit,
intervalHours: p.intervalHours,
minIntervalHours: p.minIntervalHours,
isPreset: true,
})),
});
const seeded = await prisma.medicationProfile.findMany({
where: { familyId: user.familyId },
orderBy: [{ isPreset: "desc" }, { name: "asc" }],
});
return NextResponse.json(seeded);
}
return NextResponse.json(profiles);
}
export async function POST(req: Request) {
const session = await auth();
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { familyId: true } });
if (!user?.familyId) return NextResponse.json({ error: "Famille introuvable" }, { status: 400 });
const body = await req.json();
const { name, molecule, defaultDose, unit, intervalHours, minIntervalHours } = body;
if (!name || !intervalHours) return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
const profile = await prisma.medicationProfile.create({
data: {
familyId: user.familyId,
name,
molecule: molecule ?? null,
defaultDose: defaultDose ?? null,
unit: unit ?? "mL",
intervalHours: parseFloat(intervalHours),
minIntervalHours: minIntervalHours ? parseFloat(minIntervalHours) : null,
isPreset: false,
},
});
return NextResponse.json(profile, { status: 201 });
}