febf9ed107
- Add 'Débordement' toggle to diaper events (wet + stool), stored as metadata.leaked; shown in timeline and dashboard summaries - Add milk stock stats section to stats page (gated on hasPump): summary cards (stock mL, lots, pumped in period), 24h expiry warning, location breakdown pills, and daily pump volume bar chart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
462 lines
22 KiB
TypeScript
462 lines
22 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useSearchParams, useRouter, usePathname } from "next/navigation";
|
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { EVENT_CONFIG, EventType, formatDuration } from "@/lib/event-config";
|
|
import { EventIcon } from "@/components/event-icon";
|
|
import { useBaby } from "@/contexts/baby-context";
|
|
import { format, isToday, isYesterday } from "date-fns";
|
|
import { fr } from "date-fns/locale";
|
|
import { Trash2, StickyNote, Pencil, CheckSquare, Square, XCircle, ImageIcon, Copy } from "lucide-react";
|
|
import Image from "next/image";
|
|
import { EventModal } from "@/components/event-modal";
|
|
|
|
interface Event {
|
|
id: string;
|
|
type: EventType;
|
|
startedAt: string;
|
|
endedAt?: string;
|
|
notes?: string;
|
|
metadata?: Record<string, unknown>;
|
|
user: { id: string; name: string };
|
|
}
|
|
|
|
const PAGE_SIZE = 50;
|
|
|
|
function groupByDay(events: Event[]): Record<string, Event[]> {
|
|
const groups: Record<string, Event[]> = {};
|
|
for (const ev of events) {
|
|
const key = format(new Date(ev.startedAt), "yyyy-MM-dd");
|
|
if (!groups[key]) groups[key] = [];
|
|
groups[key].push(ev);
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
function dayLabel(key: string): string {
|
|
const d = new Date(key);
|
|
if (isToday(d)) return "Aujourd'hui";
|
|
if (isYesterday(d)) return "Hier";
|
|
return format(d, "EEEE d MMMM", { locale: fr });
|
|
}
|
|
|
|
function getEventSummary(event: Event): string {
|
|
const meta = event.metadata ?? {};
|
|
if (event.type === "BREASTFEED") {
|
|
const side = meta.breastSide === "left" ? "gauche" : meta.breastSide === "right" ? "droite" : "les deux";
|
|
if (event.endedAt) return `${formatDuration(new Date(event.startedAt), new Date(event.endedAt))} · ${side}`;
|
|
return side;
|
|
}
|
|
if (event.type === "BOTTLE") return [meta.volume ? `${meta.volume} mL` : "", meta.bottleType === "maternal" ? "maternel" : "artificiel"].filter(Boolean).join(" · ");
|
|
if (event.type === "PUMP") {
|
|
const side = meta.side === "left" ? "gauche" : meta.side === "right" ? "droite" : "les deux";
|
|
const parts = [meta.volume ? `${meta.volume} mL` : "", side];
|
|
if (event.endedAt) parts.unshift(formatDuration(new Date(event.startedAt), new Date(event.endedAt)));
|
|
return parts.filter(Boolean).join(" · ");
|
|
}
|
|
if (event.type === "DIAPER_STOOL") return [String(meta.color ?? ""), meta.leaked ? "débordement" : ""].filter(Boolean).join(" · ");
|
|
if (event.type === "DIAPER_WET") return meta.leaked ? "débordement" : "";
|
|
if (event.type === "TEMPERATURE") return meta.value ? `${meta.value}°${meta.unit ?? "C"}` : "";
|
|
if (event.type === "MEDICATION") return String(meta.name ?? "");
|
|
if (event.type === "SYMPTOM") {
|
|
const tags = (meta.tags as string[] | undefined) ?? [];
|
|
return tags.length > 0 ? tags.join(", ") : "";
|
|
}
|
|
if ((event.type === "NAP" || event.type === "NIGHT_SLEEP") && event.endedAt) {
|
|
return formatDuration(new Date(event.startedAt), new Date(event.endedAt));
|
|
}
|
|
if (event.type === "OTHER") return event.notes ? event.notes.slice(0, 50) : "";
|
|
return "";
|
|
}
|
|
|
|
export default function TimelinePage() {
|
|
const qc = useQueryClient();
|
|
const { selectedBaby } = useBaby();
|
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
|
const [editingNotes, setEditingNotes] = useState<{ id: string; notes: string } | null>(null);
|
|
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
|
const [savedNoteId, setSavedNoteId] = useState<string | null>(null);
|
|
const searchParams = useSearchParams();
|
|
const router = useRouter();
|
|
const pathname = usePathname();
|
|
const filterType = (searchParams.get("type") as EventType | null) ?? "";
|
|
const [page, setPage] = useState(0);
|
|
const [selectMode, setSelectMode] = useState(false);
|
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
|
const [duplicating, setDuplicating] = useState<string | null>(null); // sourceDate being duplicated
|
|
const [dupTarget, setDupTarget] = useState(""); // target date string
|
|
const [dupLoading, setDupLoading] = useState(false);
|
|
const [dupError, setDupError] = useState("");
|
|
|
|
const { data: eventsData, isLoading } = useQuery({
|
|
queryKey: ["events", selectedBaby?.id, filterType, page],
|
|
queryFn: () =>
|
|
fetch(`/api/events?babyId=${selectedBaby!.id}&limit=${PAGE_SIZE}&offset=${page * PAGE_SIZE}${filterType ? `&type=${filterType}` : ""}`).then(
|
|
(r) => r.json()
|
|
),
|
|
enabled: !!selectedBaby?.id,
|
|
refetchInterval: 10_000,
|
|
});
|
|
|
|
const events: Event[] = eventsData?.events ?? [];
|
|
const total: number = eventsData?.total ?? 0;
|
|
const hasMore = (page + 1) * PAGE_SIZE < total;
|
|
|
|
const grouped = groupByDay(events);
|
|
const days = Object.keys(grouped).sort((a, b) => b.localeCompare(a));
|
|
|
|
async function deleteEvent(id: string) {
|
|
await fetch(`/api/events/${id}`, { method: "DELETE" });
|
|
qc.invalidateQueries({ queryKey: ["events"] });
|
|
}
|
|
|
|
async function duplicateDay(sourceDate: string, targetDate: string) {
|
|
if (!selectedBaby?.id) return;
|
|
setDupLoading(true);
|
|
setDupError("");
|
|
const res = await fetch("/api/events/duplicate-day", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ babyId: selectedBaby.id, sourceDate, targetDate }),
|
|
});
|
|
setDupLoading(false);
|
|
if (!res.ok) {
|
|
const data = await res.json();
|
|
setDupError(data.error ?? "Erreur");
|
|
return;
|
|
}
|
|
setDuplicating(null);
|
|
setDupTarget("");
|
|
setDupError("");
|
|
qc.invalidateQueries({ queryKey: ["events"] });
|
|
}
|
|
|
|
async function deleteSelected() {
|
|
if (selected.size === 0) return;
|
|
await Promise.all([...selected].map((id) => fetch(`/api/events/${id}`, { method: "DELETE" })));
|
|
setSelected(new Set());
|
|
setSelectMode(false);
|
|
qc.invalidateQueries({ queryKey: ["events"] });
|
|
}
|
|
|
|
function toggleSelect(id: string) {
|
|
setSelected((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id); else next.add(id);
|
|
return next;
|
|
});
|
|
}
|
|
|
|
async function saveNotes(id: string, notes: string) {
|
|
await fetch(`/api/events/${id}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ notes }),
|
|
});
|
|
qc.invalidateQueries({ queryKey: ["events"] });
|
|
setEditingNotes(null);
|
|
setSavedNoteId(id);
|
|
setTimeout(() => setSavedNoteId(null), 2000);
|
|
}
|
|
|
|
function handleFilterChange(t: EventType | "") {
|
|
const params = new URLSearchParams(searchParams.toString());
|
|
if (t === "" || t === filterType) { params.delete("type"); } else { params.set("type", t); }
|
|
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
|
setPage(0);
|
|
}
|
|
|
|
return (
|
|
<div className="p-4 md:p-8 pb-24 md:pb-8">
|
|
<div className="mb-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h1 className="text-2xl font-bold text-slate-900 dark:text-slate-100">
|
|
Journal{selectedBaby ? ` — ${selectedBaby.name}` : ""}
|
|
</h1>
|
|
<button
|
|
onClick={() => { setSelectMode((v) => !v); setSelected(new Set()); }}
|
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium border transition ${
|
|
selectMode
|
|
? "bg-indigo-600 text-white border-indigo-600"
|
|
: "bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-600"
|
|
}`}
|
|
>
|
|
<CheckSquare className="w-3.5 h-3.5" />
|
|
{selectMode ? "Annuler" : "Sélectionner"}
|
|
</button>
|
|
</div>
|
|
{/* Filter chips */}
|
|
<div className="sticky top-0 z-10 bg-white dark:bg-slate-900 -mx-4 px-4 py-2 border-b border-slate-100 dark:border-slate-800">
|
|
<div className="flex gap-2 overflow-x-auto no-scrollbar">
|
|
<button
|
|
onClick={() => handleFilterChange("")}
|
|
className={`flex-shrink-0 px-3 py-1.5 rounded-full text-xs font-medium transition border ${
|
|
filterType === "" ? "bg-indigo-600 text-white border-indigo-600" : "bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-slate-300 dark:hover:border-slate-500"
|
|
}`}
|
|
>
|
|
Tout {total > 0 && <span className="opacity-70 ml-0.5">({total})</span>}
|
|
</button>
|
|
{(Object.keys(EVENT_CONFIG) as EventType[]).map((t) => (
|
|
<button
|
|
key={t}
|
|
onClick={() => handleFilterChange(filterType === t ? "" : t)}
|
|
className={`flex-shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium transition border ${
|
|
filterType === t ? "bg-indigo-600 text-white border-indigo-600" : "bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 border-slate-200 dark:border-slate-600 hover:border-slate-300 dark:hover:border-slate-500"
|
|
}`}
|
|
>
|
|
<EventIcon name={EVENT_CONFIG[t].icon} className={`w-3 h-3 ${filterType === t ? "text-white" : EVENT_CONFIG[t].colorClass}`} />
|
|
{EVENT_CONFIG[t].label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{selectMode && (
|
|
<div className="flex items-center justify-between mb-4 px-4 py-2.5 bg-indigo-50 dark:bg-indigo-950/50 border border-indigo-200 dark:border-indigo-800 rounded-xl">
|
|
<span className="text-sm text-indigo-700 dark:text-indigo-300 font-medium">
|
|
{selected.size} sélectionné{selected.size > 1 ? "s" : ""}
|
|
</span>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => { setSelectMode(false); setSelected(new Set()); }}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-slate-200 dark:border-slate-600 text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-800"
|
|
>
|
|
<XCircle className="w-3.5 h-3.5" />
|
|
Annuler
|
|
</button>
|
|
<button
|
|
onClick={() => { if (confirm(`Supprimer ${selected.size} événement${selected.size > 1 ? "s" : ""} ?`)) deleteSelected(); }}
|
|
disabled={selected.size === 0}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg bg-red-600 text-white font-medium disabled:opacity-40"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
Supprimer ({selected.size})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{isLoading && (
|
|
<div className="space-y-6">
|
|
{[1, 2, 3].map((g) => (
|
|
<div key={g}>
|
|
<div className="h-3 w-24 bg-slate-200 dark:bg-slate-700 rounded animate-pulse mb-3" />
|
|
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden divide-y divide-slate-100 dark:divide-slate-700">
|
|
{[1, 2, 3, 4].map((r) => (
|
|
<div key={r} className="flex items-center gap-3 px-4 py-3">
|
|
<div className="w-8 h-8 rounded-lg bg-slate-200 dark:bg-slate-700 animate-pulse flex-shrink-0" />
|
|
<div className="flex-1 space-y-1.5">
|
|
<div className="h-3 w-28 bg-slate-200 dark:bg-slate-700 rounded animate-pulse" />
|
|
<div className="h-2.5 w-16 bg-slate-100 dark:bg-slate-800 rounded animate-pulse" />
|
|
</div>
|
|
<div className="h-3 w-10 bg-slate-200 dark:bg-slate-700 rounded animate-pulse" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-6">
|
|
{days.map((day) => (
|
|
<div key={day}>
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<span className="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wide capitalize">{dayLabel(day)}</span>
|
|
<div className="flex-1 h-px bg-slate-200 dark:bg-slate-700" />
|
|
<span className="text-xs text-slate-400 dark:text-slate-500">{grouped[day].length}</span>
|
|
{!selectMode && (
|
|
<button
|
|
onClick={() => { setDuplicating(day); setDupTarget(""); setDupError(""); }}
|
|
title="Dupliquer ce jour"
|
|
className="p-1 rounded-lg text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition"
|
|
>
|
|
<Copy className="w-3.5 h-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{duplicating === day && (
|
|
<div className="mb-3 px-4 py-3 bg-indigo-50 dark:bg-indigo-950/40 border border-indigo-200 dark:border-indigo-800 rounded-xl">
|
|
<p className="text-xs font-medium text-indigo-700 dark:text-indigo-300 mb-2">
|
|
Copier les {grouped[day].length} événements vers…
|
|
</p>
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="date"
|
|
value={dupTarget}
|
|
onChange={(e) => { setDupTarget(e.target.value); setDupError(""); }}
|
|
min={format(new Date(), "yyyy-MM-dd")}
|
|
className="flex-1 px-3 py-1.5 text-sm rounded-lg border border-indigo-200 dark:border-indigo-700 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 focus:outline-none focus:ring-2 focus:ring-indigo-400"
|
|
/>
|
|
<button
|
|
disabled={!dupTarget || dupLoading}
|
|
onClick={() => duplicateDay(day, dupTarget)}
|
|
className="px-3 py-1.5 text-xs font-medium bg-indigo-600 hover:bg-indigo-700 disabled:bg-indigo-300 dark:disabled:bg-indigo-900 text-white rounded-lg transition"
|
|
>
|
|
{dupLoading ? "…" : "Copier"}
|
|
</button>
|
|
<button
|
|
onClick={() => { setDuplicating(null); setDupError(""); }}
|
|
className="px-3 py-1.5 text-xs text-slate-500 dark:text-slate-400 border border-slate-200 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 hover:text-slate-700 transition"
|
|
>
|
|
Annuler
|
|
</button>
|
|
</div>
|
|
{dupError && <p className="mt-1.5 text-xs text-red-500">{dupError}</p>}
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl overflow-hidden divide-y divide-slate-100 dark:divide-slate-700">
|
|
{grouped[day].map((ev) => {
|
|
const cfg = EVENT_CONFIG[ev.type];
|
|
const isExpanded = expandedId === ev.id;
|
|
const summary = getEventSummary(ev);
|
|
|
|
const isSelected = selected.has(ev.id);
|
|
|
|
return (
|
|
<div key={ev.id} className={isSelected ? "bg-indigo-50 dark:bg-indigo-950/30" : ""}>
|
|
<button
|
|
className="w-full text-left flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition"
|
|
onClick={() => selectMode ? toggleSelect(ev.id) : setExpandedId(isExpanded ? null : ev.id)}
|
|
>
|
|
{selectMode && (
|
|
<div className="flex-shrink-0">
|
|
{isSelected
|
|
? <CheckSquare className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />
|
|
: <Square className="w-4 h-4 text-slate-300 dark:text-slate-600" />}
|
|
</div>
|
|
)}
|
|
<div className={`w-8 h-8 rounded-lg ${cfg.bgClass} flex items-center justify-center flex-shrink-0`}>
|
|
<EventIcon name={cfg.icon} className={`w-4 h-4 ${cfg.colorClass}`} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<span className="text-sm font-medium text-slate-700 dark:text-slate-200">{cfg.label}</span>
|
|
{summary && <span className="text-xs text-slate-400 dark:text-slate-500 ml-2">{summary}</span>}
|
|
{ev.notes && <p className="text-xs text-slate-400 dark:text-slate-500 truncate mt-0.5 italic">{ev.notes}</p>}
|
|
</div>
|
|
{typeof ev.metadata?.photo === "string" && ev.metadata.photo && (
|
|
<ImageIcon className="w-3.5 h-3.5 text-slate-400 dark:text-slate-500 flex-shrink-0" />
|
|
)}
|
|
<span className="text-xs text-slate-400 dark:text-slate-500 flex-shrink-0 font-mono">
|
|
{format(new Date(ev.startedAt), "HH:mm")}
|
|
</span>
|
|
</button>
|
|
|
|
{isExpanded && (
|
|
<div className="px-4 pb-4 bg-slate-50 dark:bg-slate-800/60 border-t border-slate-100 dark:border-slate-700 space-y-3 pt-3">
|
|
<div className="grid grid-cols-3 gap-4 text-xs">
|
|
<div>
|
|
<p className="text-slate-400 dark:text-slate-500 mb-0.5">Début</p>
|
|
<p className="font-mono font-medium text-slate-700 dark:text-slate-200">{format(new Date(ev.startedAt), "HH:mm")}</p>
|
|
</div>
|
|
{ev.endedAt && (
|
|
<div>
|
|
<p className="text-slate-400 dark:text-slate-500 mb-0.5">Fin</p>
|
|
<p className="font-mono font-medium text-slate-700 dark:text-slate-200">{format(new Date(ev.endedAt), "HH:mm")}</p>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<p className="text-slate-400 dark:text-slate-500 mb-0.5">Noté par</p>
|
|
<p className="font-medium text-slate-700 dark:text-slate-200">{ev.user.name}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{editingNotes?.id === ev.id ? (
|
|
<div className="space-y-2">
|
|
<textarea
|
|
value={editingNotes.notes}
|
|
onChange={(e) => setEditingNotes({ id: ev.id, notes: e.target.value })}
|
|
rows={2}
|
|
className="w-full px-3 py-2 rounded-lg border border-slate-200 dark:border-slate-600 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400 resize-none bg-white dark:bg-slate-700 dark:text-slate-100"
|
|
placeholder="Observations..."
|
|
autoFocus
|
|
/>
|
|
<div className="flex gap-2">
|
|
<button onClick={() => saveNotes(ev.id, editingNotes.notes)}
|
|
className="flex-1 py-1.5 bg-indigo-600 text-white text-xs rounded-lg font-medium">
|
|
Sauvegarder
|
|
</button>
|
|
<button onClick={() => setEditingNotes(null)}
|
|
className="py-1.5 px-3 border border-slate-200 dark:border-slate-600 text-xs rounded-lg text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-700">
|
|
Annuler
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<button
|
|
onClick={() => setEditingNotes({ id: ev.id, notes: ev.notes ?? "" })}
|
|
className="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition"
|
|
>
|
|
<StickyNote className="w-3.5 h-3.5" />
|
|
<span>{ev.notes ? ev.notes : "Ajouter une note..."}</span>
|
|
{savedNoteId === ev.id && <span className="text-xs text-emerald-500 ml-2">✓</span>}
|
|
</button>
|
|
)}
|
|
|
|
{typeof ev.metadata?.photo === "string" && (
|
|
<a href={ev.metadata.photo} target="_blank" rel="noopener noreferrer" className="block relative w-full h-48 rounded-xl overflow-hidden border border-slate-200 dark:border-slate-600">
|
|
<Image src={ev.metadata.photo} alt="Photo" fill className="object-cover" unoptimized />
|
|
</a>
|
|
)}
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={() => { setEditingEvent(ev); setExpandedId(null); }}
|
|
className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition"
|
|
>
|
|
<Pencil className="w-3.5 h-3.5" />
|
|
Modifier
|
|
</button>
|
|
<button
|
|
onClick={() => { if (confirm("Supprimer cet événement ?")) deleteEvent(ev.id); }}
|
|
className="flex items-center gap-1.5 text-xs text-red-400 hover:text-red-600 transition"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{hasMore && (
|
|
<button
|
|
onClick={() => setPage((p) => p + 1)}
|
|
className="w-full py-2.5 border border-slate-200 dark:border-slate-700 rounded-xl text-sm text-slate-600 dark:text-slate-300 hover:border-indigo-300 dark:hover:border-indigo-500 hover:text-indigo-600 dark:hover:text-indigo-400 bg-white dark:bg-slate-800 transition"
|
|
>
|
|
Charger plus · {total - (page + 1) * PAGE_SIZE} restants
|
|
</button>
|
|
)}
|
|
|
|
{!isLoading && events.length === 0 && (
|
|
<div className="text-center py-16 text-slate-400 dark:text-slate-500">
|
|
<p className="text-sm">Aucun événement enregistré</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{editingEvent && (
|
|
<EventModal
|
|
type={editingEvent.type}
|
|
babyId={selectedBaby!.id}
|
|
initialEvent={editingEvent}
|
|
onClose={() => setEditingEvent(null)}
|
|
onSaved={() => {
|
|
qc.invalidateQueries({ queryKey: ["events"] });
|
|
setEditingEvent(null);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|