diff --git a/src/app/(app)/timeline/page.tsx b/src/app/(app)/timeline/page.tsx index 47dd48f..fe068fe 100644 --- a/src/app/(app)/timeline/page.tsx +++ b/src/app/(app)/timeline/page.tsx @@ -8,7 +8,7 @@ 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 } from "lucide-react"; +import { Trash2, StickyNote, Pencil, CheckSquare, Square, XCircle, ImageIcon, Copy } from "lucide-react"; import Image from "next/image"; import { EventModal } from "@/components/event-modal"; @@ -83,6 +83,10 @@ export default function TimelinePage() { const [page, setPage] = useState(0); const [selectMode, setSelectMode] = useState(false); const [selected, setSelected] = useState>(new Set()); + const [duplicating, setDuplicating] = useState(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], @@ -106,6 +110,27 @@ export default function TimelinePage() { 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" }))); @@ -241,8 +266,48 @@ export default function TimelinePage() { {dayLabel(day)}
{grouped[day].length} + {!selectMode && ( + + )}
+ {duplicating === day && ( +
+

+ Copier les {grouped[day].length} événements vers… +

+
+ { 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" + /> + + +
+ {dupError &&

{dupError}

} +
+ )} +
{grouped[day].map((ev) => { const cfg = EVENT_CONFIG[ev.type]; diff --git a/src/app/api/events/duplicate-day/route.ts b/src/app/api/events/duplicate-day/route.ts new file mode 100644 index 0000000..b7c6e91 --- /dev/null +++ b/src/app/api/events/duplicate-day/route.ts @@ -0,0 +1,64 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +export async function POST(req: Request) { + const session = await auth(); + if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + + const { babyId, sourceDate, targetDate } = await req.json(); + + if (!babyId || !sourceDate || !targetDate) { + return NextResponse.json({ error: "babyId, sourceDate et targetDate requis" }, { status: 400 }); + } + if (isNaN(new Date(sourceDate).getTime()) || isNaN(new Date(targetDate).getTime())) { + return NextResponse.json({ error: "Date invalide" }, { status: 400 }); + } + if (sourceDate === targetDate) { + return NextResponse.json({ error: "Les dates source et cible doivent être différentes" }, { status: 400 }); + } + + const familyId = (session.user as { familyId?: string }).familyId; + if (!familyId) return NextResponse.json({ error: "Non autorisé" }, { status: 401 }); + const ownedBaby = await prisma.baby.findFirst({ where: { id: babyId, familyId } }); + if (!ownedBaby) return NextResponse.json({ error: "Bébé introuvable" }, { status: 404 }); + + const dayStart = new Date(sourceDate); + dayStart.setHours(0, 0, 0, 0); + const dayEnd = new Date(sourceDate); + dayEnd.setHours(23, 59, 59, 999); + + const source = await prisma.event.findMany({ + where: { babyId, startedAt: { gte: dayStart, lte: dayEnd } }, + orderBy: { startedAt: "asc" }, + }); + + if (source.length === 0) { + return NextResponse.json({ error: "Aucun événement ce jour-là" }, { status: 404 }); + } + + // Compute day offset in ms between sourceDate and targetDate + const srcMidnight = new Date(sourceDate); + srcMidnight.setHours(0, 0, 0, 0); + const tgtMidnight = new Date(targetDate); + tgtMidnight.setHours(0, 0, 0, 0); + const offsetMs = tgtMidnight.getTime() - srcMidnight.getTime(); + + const created = await prisma.$transaction( + source.map((ev) => + prisma.event.create({ + data: { + babyId, + userId: session.user.id, + type: ev.type, + startedAt: new Date(ev.startedAt.getTime() + offsetMs), + endedAt: ev.endedAt ? new Date(ev.endedAt.getTime() + offsetMs) : null, + notes: ev.notes, + metadata: ev.metadata ?? undefined, + }, + }) + ) + ); + + return NextResponse.json({ count: created.length, events: created }, { status: 201 }); +}