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:
@@ -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);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user