"use client"; import { useState, useEffect, useMemo } from "react"; import { Loader2, Trash2, Phone, Instagram, Send, ChevronDown, ChevronRight, Bell, CheckCircle2, XCircle, Clock, Star, Calendar, DoorOpen } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; // --- Types --- interface GroupBooking { id: number; name: string; phone: string; groupInfo?: string; instagram?: string; telegram?: string; notifiedConfirm: boolean; notifiedReminder: boolean; createdAt: string; } interface McRegistration { id: number; masterClassTitle: string; name: string; phone?: string; instagram: string; telegram?: string; notifiedConfirm: boolean; notifiedReminder: boolean; createdAt: string; } interface OpenDayBooking { id: number; classId: number; eventId: number; name: string; phone: string; instagram?: string; telegram?: string; notifiedConfirm: boolean; notifiedReminder: boolean; createdAt: string; classStyle?: string; classTrainer?: string; classTime?: string; classHall?: string; } type Tab = "reminders" | "classes" | "master-classes" | "open-day"; // --- Group Bookings Tab --- function GroupBookingsTab() { const [bookings, setBookings] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { adminFetch("/api/admin/group-bookings") .then((r) => r.json()) .then((data: GroupBooking[]) => setBookings(data)) .catch(() => {}) .finally(() => setLoading(false)); }, []); async function handleDelete(id: number) { await adminFetch(`/api/admin/group-bookings?id=${id}`, { method: "DELETE" }); setBookings((prev) => prev.filter((b) => b.id !== id)); } if (loading) return ; return (
{bookings.length === 0 && } {bookings.map((b) => (
{b.name} {b.phone} {b.instagram && ( {b.instagram} )} {b.telegram && ( {b.telegram} )} {b.groupInfo && ( {b.groupInfo} )}
{fmtDate(b.createdAt)} handleDelete(b.id)} />
))}
); } // --- MC Registrations Tab --- function McRegistrationsTab() { const [regs, setRegs] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { adminFetch("/api/admin/mc-registrations") .then((r) => r.json()) .then((data: McRegistration[]) => setRegs(data)) .catch(() => {}) .finally(() => setLoading(false)); }, []); // Group by MC title const grouped = useMemo(() => { const map: Record = {}; for (const r of regs) { if (!map[r.masterClassTitle]) map[r.masterClassTitle] = []; map[r.masterClassTitle].push(r); } return map; }, [regs]); const [expanded, setExpanded] = useState>({}); function toggleExpand(key: string) { setExpanded((prev) => ({ ...prev, [key]: !prev[key] })); } async function handleDelete(id: number) { await adminFetch(`/api/admin/mc-registrations?id=${id}`, { method: "DELETE" }); setRegs((prev) => prev.filter((r) => r.id !== id)); } if (loading) return ; return (
{Object.keys(grouped).length === 0 && } {Object.entries(grouped).map(([title, items]) => { const isOpen = expanded[title] ?? false; return (
{isOpen && (
{items.map((r) => (
{r.name} {r.phone && ( {r.phone} )} {r.instagram && ( {r.instagram} )} {r.telegram && ( {r.telegram} )} {fmtDate(r.createdAt)} handleDelete(r.id)} />
))}
)}
); })}
); } // --- Open Day Bookings Tab --- function OpenDayBookingsTab() { const [bookings, setBookings] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { adminFetch("/api/admin/open-day") .then((r) => r.json()) .then((events: { id: number; date: string }[]) => { if (events.length === 0) { setLoading(false); return; } const ev = events[0]; return adminFetch(`/api/admin/open-day/bookings?eventId=${ev.id}`) .then((r) => r.json()) .then((data: OpenDayBooking[]) => setBookings(data)); }) .catch(() => {}) .finally(() => setLoading(false)); }, []); // Group by class — sorted by hall then time const grouped = useMemo(() => { const map: Record = {}; for (const b of bookings) { const key = `${b.classHall}|${b.classTime}|${b.classStyle}`; if (!map[key]) map[key] = { hall: b.classHall || "—", time: b.classTime || "—", style: b.classStyle || "—", trainer: b.classTrainer || "—", items: [] }; map[key].items.push(b); } // Sort by hall, then time return Object.entries(map).sort(([, a], [, b]) => { const hallCmp = a.hall.localeCompare(b.hall); return hallCmp !== 0 ? hallCmp : a.time.localeCompare(b.time); }); }, [bookings]); const [expanded, setExpanded] = useState>({}); function toggleExpand(key: string) { setExpanded((prev) => ({ ...prev, [key]: !prev[key] })); } async function handleDelete(id: number) { await adminFetch(`/api/admin/open-day/bookings?id=${id}`, { method: "DELETE" }); setBookings((prev) => prev.filter((b) => b.id !== id)); } if (loading) return ; return (
{grouped.length === 0 && } {grouped.map(([key, group]) => { const isOpen = expanded[key] ?? false; return (
{isOpen && (
{group.items.map((b) => (
{b.name} {b.phone} {b.instagram && ( {b.instagram} )} {b.telegram && ( {b.telegram} )} {fmtDate(b.createdAt)} handleDelete(b.id)} />
))}
)}
); })}
); } // --- Reminders Tab --- interface ReminderItem { id: number; type: "class" | "master-class" | "open-day"; table: "mc_registrations" | "group_bookings" | "open_day_bookings"; name: string; phone?: string; instagram?: string; telegram?: string; reminderStatus?: string; eventLabel: string; eventDate: string; } type ReminderStatus = "pending" | "coming" | "cancelled"; const STATUS_CONFIG: Record = { pending: { label: "Нет ответа", icon: Clock, color: "text-amber-400", bg: "bg-amber-500/10", border: "border-amber-500/20" }, coming: { label: "Придёт", icon: CheckCircle2, color: "text-emerald-400", bg: "bg-emerald-500/10", border: "border-emerald-500/20" }, cancelled: { label: "Не придёт", icon: XCircle, color: "text-red-400", bg: "bg-red-500/10", border: "border-red-500/20" }, }; const TYPE_CONFIG = { "master-class": { label: "МК", icon: Star, color: "text-purple-400" }, "open-day": { label: "Open Day", icon: DoorOpen, color: "text-gold" }, "class": { label: "Занятие", icon: Calendar, color: "text-blue-400" }, }; function RemindersTab() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { adminFetch("/api/admin/reminders") .then((r) => r.json()) .then((data: ReminderItem[]) => setItems(data)) .catch(() => {}) .finally(() => setLoading(false)); }, []); async function setStatus(item: ReminderItem, status: ReminderStatus | null) { setItems((prev) => prev.map((i) => i.id === item.id && i.table === item.table ? { ...i, reminderStatus: status ?? undefined } : i)); await adminFetch("/api/admin/reminders", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ table: item.table, id: item.id, status }), }); } if (loading) return ; const today = new Date().toISOString().split("T")[0]; const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().split("T")[0]; const todayItems = items.filter((i) => i.eventDate === today); const tomorrowItems = items.filter((i) => i.eventDate === tomorrow); // Stats function countByStatus(list: ReminderItem[]) { const coming = list.filter((i) => i.reminderStatus === "coming").length; const cancelled = list.filter((i) => i.reminderStatus === "cancelled").length; const pending = list.filter((i) => i.reminderStatus === "pending").length; const notAsked = list.filter((i) => !i.reminderStatus).length; return { coming, cancelled, pending, notAsked, total: list.length }; } if (items.length === 0) { return (

Нет напоминаний — все на контроле

Здесь появятся записи на сегодня и завтра

); } // Group items by event within each day function groupByEvent(dayItems: ReminderItem[]) { const map: Record = {}; for (const item of dayItems) { const key = `${item.type}|${item.eventLabel}`; if (!map[key]) map[key] = { type: item.type, label: item.eventLabel, items: [] }; map[key].items.push(item); } return Object.values(map); } const STATUS_SECTIONS = [ { key: "not-asked", label: "Не спрошены", color: "text-gold", bg: "bg-gold/10", match: (i: ReminderItem) => !i.reminderStatus }, { key: "pending", label: "Нет ответа", color: "text-amber-400", bg: "bg-amber-500/10", match: (i: ReminderItem) => i.reminderStatus === "pending" }, { key: "coming", label: "Придёт", color: "text-emerald-400", bg: "bg-emerald-500/10", match: (i: ReminderItem) => i.reminderStatus === "coming" }, { key: "cancelled", label: "Не придёт", color: "text-red-400", bg: "bg-red-500/10", match: (i: ReminderItem) => i.reminderStatus === "cancelled" }, ]; function renderPerson(item: ReminderItem) { const currentStatus = item.reminderStatus as ReminderStatus | undefined; return (
{item.name} {item.phone && ( {item.phone} )} {item.instagram && ( {item.instagram} )} {item.telegram && ( {item.telegram} )}
{(["coming", "pending", "cancelled"] as ReminderStatus[]).map((st) => { const conf = STATUS_CONFIG[st]; const Icon = conf.icon; const active = currentStatus === st; return ( ); })}
); } return (
{[ { label: "Сегодня", date: today, items: todayItems }, { label: "Завтра", date: tomorrow, items: tomorrowItems }, ] .filter((g) => g.items.length > 0) .map((group) => { const eventGroups = groupByEvent(group.items); return (

{group.label}

{new Date(group.date + "T12:00").toLocaleDateString("ru-RU", { weekday: "long", day: "numeric", month: "long" })}
{eventGroups.map((eg) => { const typeConf = TYPE_CONFIG[eg.type]; const TypeIcon = typeConf.icon; const egStats = countByStatus(eg.items); return (
{eg.label} {eg.items.length} чел.
{egStats.coming > 0 && {egStats.coming} придёт} {egStats.cancelled > 0 && {egStats.cancelled} не придёт} {egStats.pending > 0 && {egStats.pending} нет ответа} {egStats.notAsked > 0 && {egStats.notAsked} не спрошены}
{STATUS_SECTIONS .map((sec) => ({ ...sec, items: eg.items.filter(sec.match) })) .filter((sec) => sec.items.length > 0) .map((sec) => (
{sec.label} · {sec.items.length}
{sec.items.map(renderPerson)}
))}
); })}
); })}
); } // --- Shared helpers --- function LoadingSpinner() { return (
Загрузка...
); } function EmptyState({ total }: { total: number }) { return (

{total === 0 ? "Пока нет записей" : "Нет записей по фильтру"}

); } function DeleteBtn({ onClick }: { onClick: () => void }) { return ( ); } function fmtDate(iso: string): string { return new Date(iso).toLocaleDateString("ru-RU"); } // --- Main Page --- const TABS: { key: Tab; label: string }[] = [ { key: "reminders", label: "Напоминания" }, { key: "classes", label: "Занятия" }, { key: "master-classes", label: "Мастер-классы" }, { key: "open-day", label: "День открытых дверей" }, ]; export default function BookingsPage() { const [tab, setTab] = useState("reminders"); return (

Записи

Все заявки и записи в одном месте

{/* Tabs */}
{TABS.map((t) => ( ))}
{/* Tab content */}
{tab === "reminders" && } {tab === "classes" && } {tab === "master-classes" && } {tab === "open-day" && }
); }