"use client"; import { useState, useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import { X, ChevronDown } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; type Tab = "classes" | "events"; type EventType = "master-class" | "open-day"; interface McOption { title: string; date: string } interface OdClass { id: number; style: string; start_time: string; hall: string; trainer: string } interface OdEvent { id: number; date: string; title?: string } interface ScheduleClass { type: string; trainer: string; time: string; day: string; hall: string } function shortName(fullName: string) { const parts = fullName.trim().split(/\s+/); // Names stored as "Имя Фамилия" → show "Фамилия И." return parts.length > 1 ? `${parts[1]} ${parts[0][0]}.` : parts[0]; } const SHORT_DAYS: Record = { "Понедельник": "Пн", "Вторник": "Вт", "Среда": "Ср", "Четверг": "Чт", "Пятница": "Пт", "Суббота": "Сб", "Воскресенье": "Вс", }; // --- Searchable dropdown --- interface SearchSelectOption { value: string; label: string } function SearchSelect({ options, value, onChange, placeholder }: { options: SearchSelectOption[]; value: string; onChange: (v: string) => void; placeholder: string; }) { const [open, setOpen] = useState(false); const [search, setSearch] = useState(""); const containerRef = useRef(null); const inputRef = useRef(null); const selected = options.find((o) => o.value === value); const filtered = search ? options.filter((o) => o.label.toLowerCase().includes(search.toLowerCase())) : options; useEffect(() => { if (!open) return; function handle(e: MouseEvent) { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false); setSearch(""); } } document.addEventListener("mousedown", handle); return () => document.removeEventListener("mousedown", handle); }, [open]); return (
{ setOpen(true); setTimeout(() => inputRef.current?.focus(), 0); }} className={`flex items-center gap-2 w-full rounded-lg border px-3 py-2 text-sm cursor-text transition-colors ${ open ? "border-gold/40 bg-white/[0.06]" : "border-white/[0.08] bg-white/[0.04]" }`} > {open ? ( setSearch(e.target.value)} placeholder={selected ? selected.label : placeholder} className="flex-1 bg-transparent text-white placeholder-neutral-500 outline-none text-sm" onKeyDown={(e) => { if (e.key === "Escape") { setOpen(false); setSearch(""); } if (e.key === "Backspace" && !search && value) { onChange(""); } }} /> ) : ( {selected ? selected.label : placeholder} )} {value && !open ? ( ) : ( )}
{open && (
{filtered.length === 0 && (

Ничего не найдено

)} {filtered.map((o) => ( ))}
)}
); } // --- Modal --- export function AddBookingModal({ open, onClose, onAdded, }: { open: boolean; onClose: () => void; onAdded: () => void; }) { const [tab, setTab] = useState("classes"); const [eventType, setEventType] = useState("master-class"); const [name, setName] = useState(""); const [phone, setPhone] = useState("+375 "); const [instagram, setInstagram] = useState(""); const [telegram, setTelegram] = useState(""); const [mcTitle, setMcTitle] = useState(""); const [mcOptions, setMcOptions] = useState([]); const [odClasses, setOdClasses] = useState([]); const [odEventId, setOdEventId] = useState(null); const [odClassId, setOdClassId] = useState(""); const [scheduleClasses, setScheduleClasses] = useState([]); const [classInfo, setClassInfo] = useState(""); const [saving, setSaving] = useState(false); useEffect(() => { if (!open) return; setName(""); setPhone("+375 "); setInstagram(""); setTelegram(""); setMcTitle(""); setOdClassId(""); setClassInfo(""); // Fetch schedule classes adminFetch("/api/admin/sections/schedule").then((r) => r.json()).then((data: { locations?: { name: string; days: { day: string; classes: { type: string; trainer: string; time: string }[] }[] }[] }) => { const classes: ScheduleClass[] = []; for (const loc of data.locations || []) { for (const day of loc.days) { for (const cls of day.classes) { classes.push({ type: cls.type, trainer: cls.trainer, time: cls.time, day: day.day, hall: loc.name }); } } } // Deduplicate by type+trainer+time+day+hall const seen = new Set(); const unique = classes.filter((c) => { const key = `${c.type}|${c.trainer}|${c.time}|${c.day}|${c.hall}`; if (seen.has(key)) return false; seen.add(key); return true; }); setScheduleClasses(unique); }).catch(() => {}); // Fetch upcoming MCs adminFetch("/api/admin/sections/masterClasses").then((r) => r.json()).then((data: { items?: { title: string; slots: { date: string }[] }[] }) => { const today = new Date().toISOString().split("T")[0]; const upcoming = (data.items || []) .filter((mc) => { const earliest = mc.slots?.reduce((min, s) => s.date < min ? s.date : min, mc.slots[0]?.date ?? ""); return earliest && earliest >= today; }) .map((mc) => ({ title: mc.title, date: mc.slots.reduce((min, s) => s.date < min ? s.date : min, mc.slots[0]?.date ?? ""), })); setMcOptions(upcoming); if (upcoming.length === 0 && tab === "events") setEventType("open-day"); }).catch(() => {}); // Fetch active Open Day event + classes adminFetch("/api/admin/open-day").then((r) => r.json()).then(async (events: OdEvent[]) => { const today = new Date().toISOString().split("T")[0]; const active = events.find((e) => e.date >= today); if (!active) { setOdEventId(null); setOdClasses([]); return; } setOdEventId(active.id); const classes = await adminFetch(`/api/admin/open-day/classes?eventId=${active.id}`).then((r) => r.json()); setOdClasses(classes); }).catch(() => {}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); useEffect(() => { if (!open) return; function onKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, onClose]); function handlePhoneChange(raw: string) { let digits = raw.replace(/\D/g, ""); if (!digits.startsWith("375")) digits = "375" + digits.replace(/^375?/, ""); digits = digits.slice(0, 12); let formatted = "+375"; const rest = digits.slice(3); if (rest.length > 0) formatted += " (" + rest.slice(0, 2); if (rest.length >= 2) formatted += ") "; if (rest.length > 2) formatted += rest.slice(2, 5); if (rest.length > 5) formatted += "-" + rest.slice(5, 7); if (rest.length > 7) formatted += "-" + rest.slice(7, 9); setPhone(formatted); } const hasUpcomingMc = mcOptions.length > 0; const hasOpenDay = odEventId !== null && odClasses.length > 0; // Build options for each dropdown const classOptions: SearchSelectOption[] = scheduleClasses.map((c, i) => ({ value: String(i), label: `${shortName(c.trainer)} — ${c.type} · ${SHORT_DAYS[c.day] || c.day} ${c.time} · ${c.hall}`, })); const mcSelectOptions: SearchSelectOption[] = mcOptions.map((mc) => ({ value: mc.title, label: mc.title, })); const odSelectOptions: SearchSelectOption[] = odClasses.map((c) => ({ value: String(c.id), label: `${shortName(c.trainer)} — ${c.start_time} · ${c.hall}`, })); async function handleSubmit() { if (!name.trim() || !phone.trim()) return; setSaving(true); try { if (tab === "classes") { const selectedClass = classInfo ? scheduleClasses[Number(classInfo)] : null; const groupInfo = selectedClass ? `${selectedClass.type}, ${shortName(selectedClass.trainer)}, ${SHORT_DAYS[selectedClass.day] || selectedClass.day} ${selectedClass.time}, ${selectedClass.hall}` : undefined; await adminFetch("/api/admin/group-bookings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: name.trim(), phone: phone.trim(), ...(groupInfo && { groupInfo }), ...(instagram.trim() && { instagram: instagram.trim() }), ...(telegram.trim() && { telegram: telegram.trim() }), }), }); } else if (eventType === "master-class") { const title = mcTitle || mcOptions[0]?.title || "Мастер-класс"; await adminFetch("/api/admin/mc-registrations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ masterClassTitle: title, name: name.trim(), instagram: "-", phone: phone.trim() }), }); } else if (eventType === "open-day" && odClassId && odEventId) { await adminFetch("/api/open-day-register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ classId: Number(odClassId), eventId: odEventId, name: name.trim(), phone: phone.trim() }), }); } onAdded(); onClose(); } finally { setSaving(false); } } if (!open) return null; const inputClass = "w-full rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-sm text-white outline-none focus:border-gold/40 placeholder-neutral-500"; const canSubmit = name.trim() && phone.trim() && !saving && (tab === "classes" || (tab === "events" && eventType === "master-class" && hasUpcomingMc) || (tab === "events" && eventType === "open-day" && odClassId)); return createPortal(
e.stopPropagation()}>

Добавить запись

Ручная запись (Instagram, звонок, лично)

{/* Type selector — single row */}
{hasUpcomingMc && ( )} {hasOpenDay && ( )}
{/* Class selector (optional for Занятие) */} {tab === "classes" && classOptions.length > 0 && ( )} {/* MC selector */} {tab === "events" && eventType === "master-class" && mcSelectOptions.length > 0 && ( )} {/* Open Day class selector */} {tab === "events" && eventType === "open-day" && odSelectOptions.length > 0 && ( )} setName(e.target.value)} placeholder="Имя" className={inputClass} /> handlePhoneChange(e.target.value)} placeholder="+375 (__) ___-__-__" className={inputClass} />
setInstagram(e.target.value)} placeholder="Instagram" className={inputClass} /> setTelegram(e.target.value)} placeholder="Telegram" className={inputClass} />
, document.body ); }