Files
blackheart-website/src/app/admin/page.tsx
diana.dolgolyova b94ee69033 feat: add booking management, Open Day, unified signup modal
- MC registrations: notification toggles (confirm/remind) with urgency
- Group bookings: save to DB from BookingModal, admin CRUD at /admin/bookings
- Open Day: full event system with schedule grid (halls × time), per-class
  booking, discount pricing (30 BYN / 20 BYN from 3+), auto-cancel threshold
- Unified SignupModal replaces 3 separate forms — consistent fields
  (name, phone, instagram, telegram), Instagram DM fallback on network error
- Centralized /admin/bookings page with 3 tabs (classes, MC, Open Day),
  collapsible sections, notification toggles, filter chips
- Unread booking badge on sidebar + dashboard widget with per-type breakdown
- Pricing: contact hint (Instagram/Telegram/phone) on price & rental tabs,
  admin toggle to show/hide
- DB migrations 5-7: group_bookings table, open_day tables, unified fields

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:58:04 +03:00

142 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import {
Globe,
Sparkles,
FileText,
Users,
BookOpen,
Star,
Calendar,
DollarSign,
HelpCircle,
Newspaper,
Phone,
ClipboardList,
DoorOpen,
UserPlus,
} from "lucide-react";
import { adminFetch } from "@/lib/csrf";
interface UnreadCounts {
groupBookings: number;
mcRegistrations: number;
openDayBookings: number;
total: number;
}
const CARDS = [
{ href: "/admin/meta", label: "SEO / Мета", icon: Globe, desc: "Заголовок и описание сайта" },
{ href: "/admin/hero", label: "Главный экран", icon: Sparkles, desc: "Заголовок, подзаголовок, кнопка" },
{ href: "/admin/about", label: "О студии", icon: FileText, desc: "Текст о студии" },
{ href: "/admin/team", label: "Команда", icon: Users, desc: "Тренеры и инструкторы" },
{ href: "/admin/classes", label: "Направления", icon: BookOpen, desc: "Типы занятий" },
{ href: "/admin/master-classes", label: "Мастер-классы", icon: Star, desc: "Мастер-классы и записи" },
{ href: "/admin/open-day", label: "День открытых дверей", icon: DoorOpen, desc: "Открытые занятия, расписание, записи" },
{ href: "/admin/schedule", label: "Расписание", icon: Calendar, desc: "Расписание занятий" },
{ href: "/admin/bookings", label: "Записи", icon: ClipboardList, desc: "Все записи и заявки" },
{ href: "/admin/pricing", label: "Цены", icon: DollarSign, desc: "Абонементы и аренда" },
{ href: "/admin/faq", label: "FAQ", icon: HelpCircle, desc: "Часто задаваемые вопросы" },
{ href: "/admin/news", label: "Новости", icon: Newspaper, desc: "Новости и анонсы" },
{ href: "/admin/contact", label: "Контакты", icon: Phone, desc: "Адреса, телефон, карта" },
];
function UnreadWidget({ counts }: { counts: UnreadCounts }) {
if (counts.total === 0) return null;
const items: { label: string; count: number; tab: string }[] = [];
if (counts.groupBookings > 0) items.push({ label: "Занятия", count: counts.groupBookings, tab: "classes" });
if (counts.mcRegistrations > 0) items.push({ label: "Мастер-классы", count: counts.mcRegistrations, tab: "master-classes" });
if (counts.openDayBookings > 0) items.push({ label: "День открытых дверей", count: counts.openDayBookings, tab: "open-day" });
return (
<Link
href="/admin/bookings"
className="block rounded-xl border border-gold/20 bg-gold/[0.03] p-5 transition-all hover:border-gold/40 hover:bg-gold/[0.06]"
>
<div className="flex items-center gap-3 mb-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-red-500/10 text-red-400">
<UserPlus size={20} />
</div>
<div>
<h2 className="font-medium text-white">
Новые записи
<span className="ml-2 inline-flex items-center justify-center rounded-full bg-red-500 text-white text-[11px] font-bold min-w-[20px] h-[20px] px-1.5">
{counts.total}
</span>
</h2>
<p className="text-xs text-neutral-400">Не подтверждённые заявки</p>
</div>
</div>
<div className="flex gap-3">
{items.map((item) => (
<div key={item.tab} className="flex items-center gap-1.5 text-xs">
<span className="rounded-full bg-gold/15 text-gold font-medium px-2 py-0.5">
{item.count}
</span>
<span className="text-neutral-400">{item.label}</span>
</div>
))}
</div>
</Link>
);
}
export default function AdminDashboard() {
const [counts, setCounts] = useState<UnreadCounts | null>(null);
useEffect(() => {
adminFetch("/api/admin/unread-counts")
.then((r) => r.json())
.then((data: UnreadCounts) => setCounts(data))
.catch(() => {});
}, []);
return (
<div>
<h1 className="text-2xl font-bold">Панель управления</h1>
<p className="mt-1 text-neutral-400">Выберите раздел для редактирования</p>
{/* Unread bookings widget */}
{counts && counts.total > 0 && (
<div className="mt-6">
<UnreadWidget counts={counts} />
</div>
)}
<div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{CARDS.map((card) => {
const Icon = card.icon;
const isBookings = card.href === "/admin/bookings";
return (
<Link
key={card.href}
href={card.href}
className="group rounded-xl border border-white/10 bg-neutral-900 p-5 transition-all hover:border-gold/30 hover:bg-neutral-900/80"
>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gold/10 text-gold">
<Icon size={20} />
</div>
<div className="flex-1 min-w-0">
<h2 className="font-medium text-white group-hover:text-gold transition-colors flex items-center gap-2">
{card.label}
{isBookings && counts && counts.total > 0 && (
<span className="rounded-full bg-red-500 text-white text-[10px] font-bold min-w-[18px] h-[18px] flex items-center justify-center px-1">
{counts.total}
</span>
)}
</h2>
<p className="text-xs text-neutral-500">{card.desc}</p>
</div>
</div>
</Link>
);
})}
</div>
</div>
);
}