Initial commit — Grow baby tracker
Next.js 16 App Router, Prisma + PostgreSQL, NextAuth v5 JWT. Features: dashboard quick-log, timeline, growth charts (WHO percentiles), stats, journal/notes, doctor notes, milestones, vaccinations, settings, superadmin panel. Mobile-first with sidebar nav + bottom nav + quick-add FAB. Dark mode, PWA push notifications, multi-family invite system. Docker: multi-stage Dockerfile + docker-compose with postgres service.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const [families, babies, events, growthLogs, journalNotes, milestones] = await Promise.all([
|
||||
prisma.family.findMany({
|
||||
include: { users: { select: { id: true, name: true, email: true, role: true, isSuperAdmin: true, createdAt: true } } },
|
||||
}),
|
||||
prisma.baby.findMany(),
|
||||
prisma.event.findMany({ orderBy: { createdAt: "desc" }, take: 10000 }),
|
||||
prisma.growthLog.findMany(),
|
||||
prisma.journalNote.findMany(),
|
||||
prisma.milestone.findMany(),
|
||||
]);
|
||||
|
||||
const payload = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
families,
|
||||
babies,
|
||||
events,
|
||||
growthLogs,
|
||||
journalNotes,
|
||||
milestones,
|
||||
};
|
||||
|
||||
const filename = `grow-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
|
||||
return new NextResponse(JSON.stringify(payload, null, 2), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Disposition": `attachment; filename="${filename}"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
const ALLOWED_KEYS = [
|
||||
"email_provider",
|
||||
"mailgun_api_key",
|
||||
"mailgun_domain",
|
||||
"mailgun_from",
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
"smtp_user",
|
||||
"smtp_pass",
|
||||
"smtp_from",
|
||||
"pushover_app_token",
|
||||
"allow_registration",
|
||||
"maintenance_mode",
|
||||
];
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const configs = await prisma.systemConfig.findMany();
|
||||
const result: Record<string, string> = {};
|
||||
for (const c of configs) result[c.key] = c.value;
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const body: Record<string, string> = await req.json();
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(body)
|
||||
.filter(([key]) => ALLOWED_KEYS.includes(key))
|
||||
.map(([key, value]) =>
|
||||
prisma.systemConfig.upsert({
|
||||
where: { key },
|
||||
update: { value },
|
||||
create: { key, value },
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const families = await prisma.family.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
inviteCode: true,
|
||||
createdAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
users: true,
|
||||
babies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
|
||||
return NextResponse.json(families);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
function generateInviteCode(): string {
|
||||
return crypto.randomUUID().replace(/-/g, "").slice(0, 25);
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const inviteCode = generateInviteCode();
|
||||
|
||||
await prisma.family.update({
|
||||
where: { id },
|
||||
data: { inviteCode },
|
||||
});
|
||||
|
||||
return NextResponse.json({ inviteCode });
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const [userCount, familyCount, babyCount, eventCount, growthLogCount] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.family.count(),
|
||||
prisma.baby.count(),
|
||||
prisma.event.count(),
|
||||
prisma.growthLog.count(),
|
||||
]);
|
||||
|
||||
return NextResponse.json({ userCount, familyCount, babyCount, eventCount, growthLogCount });
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { sendEmail } from "@/lib/email";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
if (!session.user?.email) {
|
||||
return NextResponse.json({ error: "Email de l'administrateur introuvable" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
await sendEmail({
|
||||
to: session.user!.email as string,
|
||||
subject: "Test email — Grow",
|
||||
html: "<p>La configuration email fonctionne correctement.</p>",
|
||||
text: "La configuration email fonctionne correctement.",
|
||||
});
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Erreur inconnue";
|
||||
return NextResponse.json({ error: message }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user!.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function POST() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const [appTokenRow, user] = await Promise.all([
|
||||
prisma.systemConfig.findUnique({ where: { key: "pushover_app_token" } }),
|
||||
prisma.user.findUnique({ where: { id: session.user!.id }, select: { pushoverUserKey: true } }),
|
||||
]);
|
||||
|
||||
const appToken = appTokenRow?.value ?? process.env.PUSHOVER_APP_TOKEN ?? "";
|
||||
const userKey = user?.pushoverUserKey ?? "";
|
||||
|
||||
if (!appToken) {
|
||||
return NextResponse.json({ error: "Token application Pushover non configuré" }, { status: 400 });
|
||||
}
|
||||
if (!userKey) {
|
||||
return NextResponse.json({ error: "Clé utilisateur Pushover non configurée" }, { status: 400 });
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("token", appToken);
|
||||
formData.append("user", userKey);
|
||||
formData.append("title", "Grow — Test de notification");
|
||||
formData.append("message", "La configuration Pushover fonctionne correctement.");
|
||||
|
||||
const res = await fetch("https://api.pushover.net/1/messages.json", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
return NextResponse.json({ error: `Erreur Pushover : ${text}` }, { status: 502 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
if (id === session.user?.id) {
|
||||
return NextResponse.json({ error: "Impossible de vous déconnecter vous-même" }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id },
|
||||
data: { forcedLogoutAt: new Date() },
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
const VALID_ROLES = ["PARENT", "READONLY", "DOCTOR"] as const;
|
||||
type ValidRole = typeof VALID_ROLES[number];
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
const body: { role?: string } = await req.json();
|
||||
|
||||
if (!body.role || !(VALID_ROLES as readonly string[]).includes(body.role)) {
|
||||
return NextResponse.json({ error: "Rôle invalide" }, { status: 400 });
|
||||
}
|
||||
|
||||
const updated = await prisma.user.update({
|
||||
where: { id },
|
||||
data: { role: body.role as ValidRole },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
if (id === session.user?.id) {
|
||||
return NextResponse.json({ error: "Impossible de supprimer votre propre compte" }, { status: 400 });
|
||||
}
|
||||
|
||||
await prisma.user.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const SUPERADMIN_EMAIL = process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return null;
|
||||
if (SUPERADMIN_EMAIL && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.isSuperAdmin && session.user.email !== SUPERADMIN_EMAIL) return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const session = await requireSuperAdmin();
|
||||
if (!session) return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
isSuperAdmin: true,
|
||||
familyId: true,
|
||||
family: { select: { name: true } },
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
});
|
||||
|
||||
return NextResponse.json(users);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/lib/auth";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { sendEmail, resetPasswordEmail } from "@/lib/email";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { email } = await req.json();
|
||||
if (!email) return NextResponse.json({ error: "Email requis" }, { status: 400 });
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
// Always return success to prevent user enumeration
|
||||
if (!user) return NextResponse.json({ ok: true });
|
||||
|
||||
const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
|
||||
const reset = await prisma.passwordResetToken.create({
|
||||
data: { userId: user.id, expiresAt },
|
||||
});
|
||||
|
||||
const origin = process.env.NEXTAUTH_URL ?? process.env.AUTH_URL ?? "http://localhost:3000";
|
||||
const resetUrl = `${origin}/auth/reset-password?token=${reset.token}`;
|
||||
|
||||
try {
|
||||
const { html, text } = resetPasswordEmail(user.name, resetUrl);
|
||||
await sendEmail({ to: user.email, subject: "Réinitialisation de votre mot de passe — Grow", html, text });
|
||||
} catch (err) {
|
||||
console.error("Email send failed:", err);
|
||||
// Still succeed silently to avoid leaking info
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { token, password } = await req.json();
|
||||
if (!token || !password || password.length < 8) {
|
||||
return NextResponse.json({ error: "Données invalides" }, { status: 400 });
|
||||
}
|
||||
|
||||
const reset = await prisma.passwordResetToken.findUnique({ where: { token } });
|
||||
if (!reset || reset.used || reset.expiresAt < new Date()) {
|
||||
return NextResponse.json({ error: "Lien expiré ou invalide" }, { status: 400 });
|
||||
}
|
||||
|
||||
const hashed = await bcrypt.hash(password, 12);
|
||||
await Promise.all([
|
||||
prisma.user.update({ where: { id: reset.userId }, data: { password: hashed } }),
|
||||
prisma.passwordResetToken.update({ where: { id: reset.id }, data: { used: true } }),
|
||||
]);
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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 { name, birthDate, gender } = await req.json();
|
||||
|
||||
const baby = await prisma.baby.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(name ? { name } : {}),
|
||||
...(birthDate ? { birthDate: new Date(birthDate) } : {}),
|
||||
...(gender !== undefined ? { gender } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(baby);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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 },
|
||||
include: {
|
||||
family: {
|
||||
include: {
|
||||
babies: true,
|
||||
users: { select: { id: true, name: true, email: true, role: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(user?.family ?? null);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { name, birthDate, gender } = await req.json();
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
if (!user?.familyId) return NextResponse.json({ error: "Aucune famille" }, { status: 400 });
|
||||
|
||||
const baby = await prisma.baby.create({
|
||||
data: { name, birthDate: new Date(birthDate), gender, familyId: user.familyId },
|
||||
});
|
||||
|
||||
return NextResponse.json(baby);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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 userRole = (session.user as { role?: string }).role;
|
||||
if (userRole !== "DOCTOR") {
|
||||
return NextResponse.json({ error: "Réservé aux médecins" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const note = await prisma.doctorNote.findUnique({ where: { id } });
|
||||
|
||||
if (!note) {
|
||||
return NextResponse.json({ error: "Observation introuvable" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (note.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: "Vous ne pouvez supprimer que vos propres observations" }, { status: 403 });
|
||||
}
|
||||
|
||||
await prisma.doctorNote.delete({ where: { id } });
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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 });
|
||||
|
||||
const notes = await prisma.doctorNote.findMany({
|
||||
where: { babyId },
|
||||
orderBy: { date: "desc" },
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
return NextResponse.json(notes);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const userRole = (session.user as { role?: string }).role;
|
||||
if (userRole !== "DOCTOR") {
|
||||
return NextResponse.json({ error: "Réservé aux médecins" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { babyId, content, date } = body;
|
||||
|
||||
if (!babyId || !content || !date) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const noteDate = new Date(date);
|
||||
noteDate.setHours(12, 0, 0, 0);
|
||||
|
||||
const note = await prisma.doctorNote.create({
|
||||
data: {
|
||||
babyId,
|
||||
userId: session.user.id,
|
||||
content,
|
||||
date: noteDate,
|
||||
},
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
return NextResponse.json(note, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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 event = await prisma.event.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.notes !== undefined ? { notes: body.notes } : {}),
|
||||
...(body.startedAt ? { startedAt: new Date(body.startedAt) } : {}),
|
||||
...(body.endedAt !== undefined
|
||||
? { endedAt: body.endedAt ? new Date(body.endedAt) : null }
|
||||
: {}),
|
||||
...(body.metadata !== undefined ? { metadata: body.metadata } : {}),
|
||||
},
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
return NextResponse.json(event);
|
||||
}
|
||||
|
||||
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.event.delete({ where: { id } });
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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");
|
||||
const limit = parseInt(searchParams.get("limit") ?? "50");
|
||||
const offset = parseInt(searchParams.get("offset") ?? "0");
|
||||
const type = searchParams.get("type");
|
||||
const dateFrom = searchParams.get("dateFrom");
|
||||
const dateTo = searchParams.get("dateTo");
|
||||
|
||||
if (!babyId) return NextResponse.json({ error: "babyId requis" }, { status: 400 });
|
||||
|
||||
const where: Record<string, unknown> = { babyId };
|
||||
if (type) where.type = type;
|
||||
if (dateFrom || dateTo) {
|
||||
where.startedAt = {
|
||||
...(dateFrom ? { gte: new Date(dateFrom) } : {}),
|
||||
...(dateTo ? { lte: new Date(dateTo) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const [events, total] = await Promise.all([
|
||||
prisma.event.findMany({
|
||||
where,
|
||||
orderBy: { startedAt: "desc" },
|
||||
take: limit,
|
||||
skip: offset,
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
}),
|
||||
prisma.event.count({ where }),
|
||||
]);
|
||||
|
||||
return NextResponse.json({ events, total });
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const { babyId, type, startedAt, endedAt, notes, metadata } = body;
|
||||
|
||||
if (!babyId || !type || !startedAt) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const event = await prisma.event.create({
|
||||
data: {
|
||||
babyId,
|
||||
userId: session.user.id,
|
||||
type,
|
||||
startedAt: new Date(startedAt),
|
||||
endedAt: endedAt ? new Date(endedAt) : null,
|
||||
notes: notes ?? null,
|
||||
metadata: metadata ?? null,
|
||||
},
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
return NextResponse.json(event, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { EVENT_CONFIG, EventType } from "@/lib/event-config";
|
||||
import { format } from "date-fns";
|
||||
import { fr } from "date-fns/locale";
|
||||
|
||||
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");
|
||||
const dateFrom = searchParams.get("dateFrom");
|
||||
const dateTo = searchParams.get("dateTo");
|
||||
const outputFormat = searchParams.get("format");
|
||||
|
||||
if (!babyId) return NextResponse.json({ error: "babyId requis" }, { status: 400 });
|
||||
|
||||
const where: Record<string, unknown> = { babyId };
|
||||
if (dateFrom || dateTo) {
|
||||
where.startedAt = {
|
||||
...(dateFrom ? { gte: new Date(dateFrom) } : {}),
|
||||
...(dateTo ? { lte: new Date(dateTo) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const events = await prisma.event.findMany({
|
||||
where,
|
||||
orderBy: { startedAt: "asc" },
|
||||
include: { user: { select: { name: true } } },
|
||||
});
|
||||
|
||||
if (outputFormat === "json") {
|
||||
return NextResponse.json(events.map((e) => ({
|
||||
type: e.type,
|
||||
startedAt: e.startedAt,
|
||||
endedAt: e.endedAt,
|
||||
notes: e.notes,
|
||||
metadata: e.metadata,
|
||||
user: e.user.name,
|
||||
})));
|
||||
}
|
||||
|
||||
const rows = [
|
||||
["Date", "Heure", "Type", "Durée (min)", "Noté par", "Notes", "Détails"].join(","),
|
||||
...events.map((e) => {
|
||||
const duration =
|
||||
e.endedAt
|
||||
? Math.round((e.endedAt.getTime() - e.startedAt.getTime()) / 60000)
|
||||
: "";
|
||||
const details = e.metadata ? JSON.stringify(e.metadata).replace(/"/g, "'") : "";
|
||||
return [
|
||||
format(e.startedAt, "dd/MM/yyyy", { locale: fr }),
|
||||
format(e.startedAt, "HH:mm"),
|
||||
EVENT_CONFIG[e.type as EventType]?.label ?? e.type,
|
||||
duration,
|
||||
e.user.name,
|
||||
`"${(e.notes ?? "").replace(/"/g, "'")}"`,
|
||||
`"${details}"`,
|
||||
].join(",");
|
||||
}),
|
||||
].join("\n");
|
||||
|
||||
return new NextResponse(rows, {
|
||||
headers: {
|
||||
"Content-Type": "text/csv; charset=utf-8",
|
||||
"Content-Disposition": `attachment; filename="grow-export-${format(new Date(), "yyyy-MM-dd")}.csv"`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 role = (session.user as { role?: string }).role;
|
||||
if (role === "READONLY") return NextResponse.json({ error: "Accès en lecture seule" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
const { date, weight, height, headCirc, notes } = await req.json();
|
||||
|
||||
const log = await prisma.growthLog.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(date ? { date: new Date(date) } : {}),
|
||||
...(weight !== undefined ? { weight: weight ? weight * 1000 : null } : {}),
|
||||
...(height !== undefined ? { height: height ?? null } : {}),
|
||||
...(headCirc !== undefined ? { headCirc: headCirc ?? null } : {}),
|
||||
...(notes !== undefined ? { notes: notes || null } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(log);
|
||||
}
|
||||
|
||||
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 role = (session.user as { role?: string }).role;
|
||||
if (role === "READONLY") return NextResponse.json({ error: "Accès en lecture seule" }, { status: 403 });
|
||||
|
||||
const { id } = await params;
|
||||
await prisma.growthLog.delete({ where: { id } });
|
||||
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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 });
|
||||
|
||||
const logs = await prisma.growthLog.findMany({
|
||||
where: { babyId },
|
||||
orderBy: { date: "asc" },
|
||||
});
|
||||
|
||||
return NextResponse.json(logs);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { babyId, date, weight, height, headCirc, notes } = await req.json();
|
||||
|
||||
if (!babyId || !date) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const log = await prisma.growthLog.create({
|
||||
data: {
|
||||
babyId,
|
||||
date: new Date(date),
|
||||
weight: weight ?? null,
|
||||
height: height ?? null,
|
||||
headCirc: headCirc ?? null,
|
||||
notes: notes ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(log, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const VALID_ROLES = ["PARENT", "READONLY", "DOCTOR"] as const;
|
||||
type Role = (typeof VALID_ROLES)[number];
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { name, email, password, inviteCode, role } = await req.json();
|
||||
|
||||
if (!name || !email || !password || !inviteCode) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const assignedRole: Role = VALID_ROLES.includes(role) ? role : "PARENT";
|
||||
|
||||
const family = await prisma.family.findUnique({ where: { inviteCode } });
|
||||
if (!family) {
|
||||
return NextResponse.json({ error: "Code d'invitation invalide" }, { status: 404 });
|
||||
}
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { email } });
|
||||
if (existing) {
|
||||
return NextResponse.json({ error: "Email déjà utilisé" }, { status: 409 });
|
||||
}
|
||||
|
||||
const hashed = await bcrypt.hash(password, 12);
|
||||
const user = await prisma.user.create({
|
||||
data: { name, email, password: hashed, familyId: family.id, role: assignedRole },
|
||||
});
|
||||
|
||||
return NextResponse.json({ id: user.id, email: user.email, name: user.name, role: user.role });
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
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();
|
||||
|
||||
if (body.content === undefined) {
|
||||
return NextResponse.json({ error: "content requis" }, { status: 400 });
|
||||
}
|
||||
|
||||
const existing = await prisma.journalNote.findUnique({ where: { id } });
|
||||
if (!existing) return NextResponse.json({ error: "Note introuvable" }, { status: 404 });
|
||||
|
||||
const userRole = (session.user as { role?: string }).role;
|
||||
const isOwner = existing.userId === session.user.id;
|
||||
const isParent = userRole === "PARENT";
|
||||
|
||||
if (!isOwner && !isParent) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const note = await prisma.journalNote.update({
|
||||
where: { id },
|
||||
data: { content: body.content },
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
return NextResponse.json(note);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
const existing = await prisma.journalNote.findUnique({ where: { id } });
|
||||
if (!existing) return NextResponse.json({ error: "Note introuvable" }, { status: 404 });
|
||||
|
||||
const userRole = (session.user as { role?: string }).role;
|
||||
const isOwner = existing.userId === session.user.id;
|
||||
const isParent = userRole === "PARENT";
|
||||
|
||||
if (!isOwner && !isParent) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
await prisma.journalNote.delete({ where: { id } });
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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");
|
||||
const date = searchParams.get("date");
|
||||
|
||||
if (!babyId) return NextResponse.json({ error: "babyId requis" }, { status: 400 });
|
||||
|
||||
const where: Record<string, unknown> = { babyId };
|
||||
|
||||
if (date) {
|
||||
const start = new Date(date);
|
||||
start.setHours(0, 0, 0, 0);
|
||||
const end = new Date(date);
|
||||
end.setHours(23, 59, 59, 999);
|
||||
where.date = { gte: start, lte: end };
|
||||
}
|
||||
|
||||
const notes = await prisma.journalNote.findMany({
|
||||
where,
|
||||
orderBy: { date: "desc" },
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
return NextResponse.json(notes);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const userRole = (session.user as { role?: string }).role;
|
||||
if (userRole === "READONLY") {
|
||||
return NextResponse.json({ error: "Accès en lecture seule" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { babyId, date, content } = body;
|
||||
|
||||
if (!babyId || !date || !content) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const noteDate = new Date(date);
|
||||
noteDate.setHours(12, 0, 0, 0);
|
||||
|
||||
const note = await prisma.journalNote.create({
|
||||
data: {
|
||||
babyId,
|
||||
userId: session.user.id,
|
||||
date: noteDate,
|
||||
content,
|
||||
},
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
return NextResponse.json(note, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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 role = (session.user as { role?: string }).role;
|
||||
if (role === "READONLY") {
|
||||
return NextResponse.json({ error: "Accès en lecture seule" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
const milestone = await prisma.milestone.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.title !== undefined ? { title: body.title } : {}),
|
||||
...(body.date !== undefined ? { date: new Date(body.date) } : {}),
|
||||
...(body.notes !== undefined ? { notes: body.notes } : {}),
|
||||
...(body.icon !== undefined ? { icon: body.icon } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(milestone);
|
||||
}
|
||||
|
||||
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 role = (session.user as { role?: string }).role;
|
||||
if (role === "READONLY") {
|
||||
return NextResponse.json({ error: "Accès en lecture seule" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
await prisma.milestone.delete({ where: { id } });
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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 });
|
||||
|
||||
const milestones = await prisma.milestone.findMany({
|
||||
where: { babyId },
|
||||
orderBy: { date: "desc" },
|
||||
});
|
||||
|
||||
return NextResponse.json(milestones);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const role = (session.user as { role?: string }).role;
|
||||
if (role === "READONLY") {
|
||||
return NextResponse.json({ error: "Accès en lecture seule" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { babyId, title, date, notes, icon } = body;
|
||||
|
||||
if (!babyId || !title || !date) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const milestone = await prisma.milestone.create({
|
||||
data: {
|
||||
babyId,
|
||||
title,
|
||||
date: new Date(date),
|
||||
notes: notes ?? null,
|
||||
icon: icon ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(milestone, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { notifyFamilyFeedAlert } from "@/lib/push-notify";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { babyId, thresholdHours } = await req.json();
|
||||
if (!babyId) return NextResponse.json({ error: "babyId requis" }, { status: 400 });
|
||||
|
||||
const baby = await prisma.baby.findUnique({
|
||||
where: { id: babyId },
|
||||
select: { name: true, familyId: true },
|
||||
});
|
||||
if (!baby) return NextResponse.json({ error: "Bébé introuvable" }, { status: 404 });
|
||||
|
||||
const lastFeed = await prisma.event.findFirst({
|
||||
where: { babyId, type: { in: ["BREASTFEED", "BOTTLE"] } },
|
||||
orderBy: { startedAt: "desc" },
|
||||
});
|
||||
|
||||
if (!lastFeed) {
|
||||
return NextResponse.json({ sent: false, reason: "no_feed_data" });
|
||||
}
|
||||
|
||||
const hoursAgo = (Date.now() - lastFeed.startedAt.getTime()) / 3600000;
|
||||
const threshold = thresholdHours ?? 3;
|
||||
|
||||
if (hoursAgo < threshold) {
|
||||
return NextResponse.json({ sent: false, reason: "threshold_not_reached", hoursAgo });
|
||||
}
|
||||
|
||||
const hStr = Math.floor(hoursAgo);
|
||||
const mStr = Math.round((hoursAgo % 1) * 60);
|
||||
const message = `${baby.name} n'a pas mangé depuis ${hStr}h${mStr.toString().padStart(2, "0")} — pensez à la prochaine tétée.`;
|
||||
|
||||
try {
|
||||
await notifyFamilyFeedAlert(baby.familyId, message);
|
||||
return NextResponse.json({ sent: true, hoursAgo });
|
||||
} catch (err) {
|
||||
return NextResponse.json({ sent: false, error: String(err) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { name, email, password } = await req.json();
|
||||
|
||||
if (!name || !email || !password) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check if registration is disabled (superadmin can always register)
|
||||
const isSuperAdminEmail = !!process.env.SUPERADMIN_EMAIL && email === process.env.SUPERADMIN_EMAIL;
|
||||
if (!isSuperAdminEmail) {
|
||||
const regConfig = await prisma.systemConfig.findUnique({ where: { key: "allow_registration" } });
|
||||
if (regConfig && regConfig.value === "false") {
|
||||
return NextResponse.json({ error: "Les inscriptions sont désactivées." }, { status: 403 });
|
||||
}
|
||||
}
|
||||
|
||||
const existing = await prisma.user.findUnique({ where: { email } });
|
||||
if (existing) {
|
||||
return NextResponse.json({ error: "Email déjà utilisé" }, { status: 409 });
|
||||
}
|
||||
|
||||
const hashed = await bcrypt.hash(password, 12);
|
||||
|
||||
const family = await prisma.family.create({
|
||||
data: { name: `Famille de ${name}` },
|
||||
});
|
||||
|
||||
const isSuperAdmin = !!process.env.SUPERADMIN_EMAIL && email === process.env.SUPERADMIN_EMAIL;
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: { name, email, password: hashed, familyId: family.id, isSuperAdmin },
|
||||
});
|
||||
|
||||
return NextResponse.json({ id: user.id, email: user.email, name: user.name });
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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: { pushoverUserKey: true } });
|
||||
return NextResponse.json({ pushoverUserKey: user?.pushoverUserKey ?? "" });
|
||||
}
|
||||
|
||||
export async function PATCH(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
const { pushoverUserKey } = await req.json();
|
||||
await prisma.user.update({ where: { id: session.user.id }, data: { pushoverUserKey: pushoverUserKey || null } });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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 role = (session.user as { role?: string }).role;
|
||||
if (role === "READONLY") {
|
||||
return NextResponse.json({ error: "Accès en lecture seule" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
const vaccination = await prisma.vaccination.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.name !== undefined ? { name: body.name } : {}),
|
||||
...(body.date !== undefined
|
||||
? { date: body.date ? new Date(body.date) : null }
|
||||
: {}),
|
||||
...(body.dueDate !== undefined
|
||||
? { dueDate: body.dueDate ? new Date(body.dueDate) : null }
|
||||
: {}),
|
||||
...(body.notes !== undefined ? { notes: body.notes } : {}),
|
||||
...(body.done !== undefined ? { done: body.done } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(vaccination);
|
||||
}
|
||||
|
||||
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 role = (session.user as { role?: string }).role;
|
||||
if (role === "READONLY") {
|
||||
return NextResponse.json({ error: "Accès en lecture seule" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
await prisma.vaccination.delete({ where: { id } });
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
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 });
|
||||
|
||||
const vaccinations = await prisma.vaccination.findMany({
|
||||
where: { babyId },
|
||||
orderBy: [{ dueDate: "asc" }, { date: "asc" }],
|
||||
});
|
||||
|
||||
// Move nulls last for dueDate
|
||||
const withDue = vaccinations.filter((v) => v.dueDate !== null);
|
||||
const withoutDue = vaccinations.filter((v) => v.dueDate === null);
|
||||
|
||||
return NextResponse.json([...withDue, ...withoutDue]);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id)
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const role = (session.user as { role?: string }).role;
|
||||
if (role === "READONLY") {
|
||||
return NextResponse.json({ error: "Accès en lecture seule" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const { babyId, name, date, dueDate, notes } = body;
|
||||
|
||||
if (!babyId || !name) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const vaccination = await prisma.vaccination.create({
|
||||
data: {
|
||||
babyId,
|
||||
name,
|
||||
date: date ? new Date(date) : null,
|
||||
dueDate: dueDate ? new Date(dueDate) : null,
|
||||
notes: notes ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(vaccination, { status: 201 });
|
||||
}
|
||||
Reference in New Issue
Block a user