- 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>
142 lines
5.9 KiB
TypeScript
142 lines
5.9 KiB
TypeScript
"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>
|
||
);
|
||
}
|