diff --git a/prisma/migrations/20260618133747_add_other_event_type/migration.sql b/prisma/migrations/20260618133747_add_other_event_type/migration.sql new file mode 100644 index 0000000..3c29103 --- /dev/null +++ b/prisma/migrations/20260618133747_add_other_event_type/migration.sql @@ -0,0 +1,37 @@ +-- AlterEnum +ALTER TYPE "EventType" ADD VALUE 'OTHER'; + +-- CreateTable +CREATE TABLE "PendingInvite" ( + "id" TEXT NOT NULL, + "familyId" TEXT NOT NULL, + "email" TEXT NOT NULL, + "role" "UserRole" NOT NULL DEFAULT 'PARENT', + "token" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PendingInvite_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Webhook" ( + "id" TEXT NOT NULL, + "familyId" TEXT NOT NULL, + "url" TEXT NOT NULL, + "secret" TEXT NOT NULL, + "events" TEXT[], + "active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Webhook_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "PendingInvite_token_key" ON "PendingInvite"("token"); + +-- AddForeignKey +ALTER TABLE "PendingInvite" ADD CONSTRAINT "PendingInvite_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "Family"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Webhook" ADD CONSTRAINT "Webhook_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "Family"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5783b99..e4f90a0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -193,6 +193,7 @@ enum EventType { WALK TUMMY_TIME SYMPTOM + OTHER } model GrowthLog { diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index 42718cf..6265d65 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -9,7 +9,7 @@ import { useBaby } from "@/contexts/baby-context"; import { checkFeedAlert, getThresholds } from "@/lib/notifications"; import { format, isToday } from "date-fns"; import { fr } from "date-fns/locale"; -import { AlertTriangle, Clock, CheckCircle2, Pin, PinOff, Pencil, Check, X, Plus, Trash2, FileText, ImageIcon } from "lucide-react"; +import { AlertTriangle, Clock, CheckCircle2, Pin, PinOff, Pencil, Check, X, Plus, FileText, ImageIcon } from "lucide-react"; import { useSession } from "next-auth/react"; const QUICK_LOG_GROUPS: { label: string; types: EventType[] }[] = [ @@ -25,6 +25,10 @@ const QUICK_LOG_GROUPS: { label: string; types: EventType[] }[] = [ label: "Santé", types: ["TEMPERATURE", "MEDICATION", "SYMPTOM"], }, + { + label: "Autre", + types: ["OTHER"], + }, ]; interface Event { @@ -37,12 +41,6 @@ interface Event { user: { id: string; name: string }; } -interface EventTemplate { - id: string; - label: string; - type: EventType; - metadata?: Record | null; -} function getBabyAge(birthDate: string): string { const birth = new Date(birthDate); @@ -384,115 +382,6 @@ function PinnedNoteWidget() { ); } -function TemplatesRow({ babyId, onSaved }: { babyId: string; onSaved: () => void }) { - const qc = useQueryClient(); - const [activeTemplate, setActiveTemplate] = useState(null); - const [addingType, setAddingType] = useState(null); - const [addLabel, setAddLabel] = useState(""); - const [deletingId, setDeletingId] = useState(null); - - const { data: templates = [] } = useQuery({ - queryKey: ["event-templates"], - queryFn: () => fetch("/api/event-templates").then((r) => r.json()), - }); - - async function deleteTemplate(id: string) { - setDeletingId(id); - await fetch(`/api/event-templates/${id}`, { method: "DELETE" }); - qc.invalidateQueries({ queryKey: ["event-templates"] }); - setDeletingId(null); - } - - async function createTemplate(e: React.FormEvent) { - e.preventDefault(); - if (!addingType || !addLabel.trim()) return; - await fetch("/api/event-templates", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ type: addingType, label: addLabel.trim() }), - }); - qc.invalidateQueries({ queryKey: ["event-templates"] }); - setAddingType(null); - setAddLabel(""); - } - - return ( -
-
-

Modèles rapides

-
- -
-
- - {addingType && ( -
-
- -
- setAddLabel(e.target.value)} - placeholder={`Nom du modèle (ex: Sieste midi)`} - className="flex-1 px-3 py-1.5 rounded-lg border border-slate-200 dark:border-slate-700 text-sm bg-white dark:bg-slate-800 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-400" - /> - - -
- )} - - {templates.length > 0 && ( -
- {templates.map((t) => { - const cfg = EVENT_CONFIG[t.type]; - return ( -
- - -
- ); - })} -
- )} - - {templates.length === 0 && !addingType && ( -

Aucun modèle. Créez-en un pour accélérer la saisie.

- )} - - {activeTemplate && ( - setActiveTemplate(null)} - onSaved={() => { onSaved(); setActiveTemplate(null); }} - /> - )} -
- ); -} export default function DashboardPage() { const qc = useQueryClient(); @@ -809,11 +698,6 @@ export default function DashboardPage() { ))} - {/* Event templates */} -
- qc.invalidateQueries({ queryKey: ["events"] })} /> -
- {/* Medication status */} {medProfiles.length > 0 && (() => { const activeMeds = medProfiles.filter((p) => lastIntakes[p.name]); diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index fcfd2cc..418793f 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -5,7 +5,7 @@ import { join } from "path"; import { randomUUID } from "crypto"; const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"]; -const MAX_SIZE = 5 * 1024 * 1024; // 5 MB +const MAX_SIZE = 30 * 1024 * 1024; // 30 MB // Configurable via env so Docker volume can be mounted at /data/uploads const UPLOAD_DIR = process.env.UPLOAD_DIR ?? join(process.cwd(), "public", "uploads"); @@ -19,7 +19,7 @@ export async function POST(req: Request) { if (!file) return NextResponse.json({ error: "Fichier manquant" }, { status: 400 }); if (!ALLOWED_TYPES.includes(file.type)) return NextResponse.json({ error: "Type de fichier non supporté" }, { status: 400 }); - if (file.size > MAX_SIZE) return NextResponse.json({ error: "Fichier trop volumineux (max 5 MB)" }, { status: 400 }); + if (file.size > MAX_SIZE) return NextResponse.json({ error: "Fichier trop volumineux (max 30 MB)" }, { status: 400 }); const ext = file.type.split("/")[1].replace("jpeg", "jpg"); const filename = `${randomUUID()}.${ext}`; diff --git a/src/components/event-modal.tsx b/src/components/event-modal.tsx index cb2db62..7e60476 100644 --- a/src/components/event-modal.tsx +++ b/src/components/event-modal.tsx @@ -140,6 +140,9 @@ export function EventModal({ type, babyId, onClose, onSaved, initialEvent }: Pro const [milkStocks, setMilkStocks] = useState([]); const [selectedStockId, setSelectedStockId] = useState(null); + // Combined diaper toggle + const [diaperBoth, setDiaperBoth] = useState(false); + // Pump → stock state const [addToStock, setAddToStock] = useState(false); const [pumpStockLocation, setPumpStockLocation] = useState<"room" | "fridge" | "freezer">("fridge"); @@ -358,6 +361,21 @@ export function EventModal({ type, babyId, onClose, onSaved, initialEvent }: Pro metadata: Object.keys(metadata).length > 0 ? metadata : null, }), }); + if (diaperBoth && (type === "DIAPER_WET" || type === "DIAPER_STOOL")) { + const secondType = type === "DIAPER_WET" ? "DIAPER_STOOL" : "DIAPER_WET"; + await fetch("/api/events", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + babyId, + type: secondType, + startedAt: toUTC(form.startedAt), + endedAt: null, + notes: null, + metadata: null, + }), + }); + } } if (type === "BOTTLE" && selectedStockId && form.bottleType === "maternal") { await fetch(`/api/milk/${selectedStockId}`, { @@ -637,6 +655,20 @@ export function EventModal({ type, babyId, onClose, onSaved, initialEvent }: Pro )} + {/* Combined diaper toggle */} + {(type === "DIAPER_WET" || type === "DIAPER_STOOL") && !isEditing && ( + + )} + {/* Medication — profile picker */} {type === "MEDICATION" && (
diff --git a/src/components/quick-add-fab.tsx b/src/components/quick-add-fab.tsx index 997d744..fbc88ce 100644 --- a/src/components/quick-add-fab.tsx +++ b/src/components/quick-add-fab.tsx @@ -21,6 +21,11 @@ const QUICK_TYPES: EventType[] = [ "NIGHT_SLEEP", "MEDICATION", "TEMPERATURE", + "BATH", + "WALK", + "TUMMY_TIME", + "SYMPTOM", + "OTHER", ]; export function QuickAddFab() { diff --git a/src/lib/event-config.ts b/src/lib/event-config.ts index 8a3d5b3..26e93e7 100644 --- a/src/lib/event-config.ts +++ b/src/lib/event-config.ts @@ -11,7 +11,8 @@ export type EventType = | "BATH" | "WALK" | "TUMMY_TIME" - | "SYMPTOM"; + | "SYMPTOM" + | "OTHER"; export const EVENT_CONFIG: Record< EventType, @@ -164,6 +165,16 @@ export const EVENT_CONFIG: Record< hasDuration: false, hasTimer: false, }, + OTHER: { + label: "Autre", + icon: "FileText", + color: "#64748b", + colorClass: "text-slate-500", + bgClass: "bg-slate-100 dark:bg-slate-800", + borderClass: "border-slate-200 dark:border-slate-700", + hasDuration: false, + hasTimer: false, + }, }; export const ALL_EVENT_TYPES = Object.keys(EVENT_CONFIG) as EventType[];