refactor: remove booking section from Open Day admin (managed in bookings page)
This commit is contained in:
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useMemo, useCallback } from "react";
|
import { useState, useEffect, useMemo, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
Plus, X, Loader2, Calendar, Trash2, Ban, CheckCircle2, ChevronDown, ChevronUp,
|
Plus, X, Loader2, Calendar, Trash2, Ban, CheckCircle2,
|
||||||
Phone, Instagram, Send,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { adminFetch } from "@/lib/csrf";
|
import { adminFetch } from "@/lib/csrf";
|
||||||
import { NotifyToggle } from "../_components/NotifyToggle";
|
|
||||||
|
|
||||||
// --- Types ---
|
// --- Types ---
|
||||||
|
|
||||||
@@ -35,22 +33,6 @@ interface OpenDayClass {
|
|||||||
bookingCount: number;
|
bookingCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
||||||
@@ -411,155 +393,6 @@ function ScheduleGrid({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Bookings Table ---
|
|
||||||
|
|
||||||
function BookingsSection({
|
|
||||||
eventId,
|
|
||||||
eventDate,
|
|
||||||
}: {
|
|
||||||
eventId: number;
|
|
||||||
eventDate: string;
|
|
||||||
}) {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [bookings, setBookings] = useState<OpenDayBooking[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const reminderUrgent = useMemo(() => {
|
|
||||||
if (!eventDate) return false;
|
|
||||||
const now = Date.now();
|
|
||||||
const twoDays = 2 * 24 * 60 * 60 * 1000;
|
|
||||||
const eventTime = new Date(eventDate + "T10:00").getTime();
|
|
||||||
const diff = eventTime - now;
|
|
||||||
return diff >= 0 && diff <= twoDays;
|
|
||||||
}, [eventDate]);
|
|
||||||
|
|
||||||
function load() {
|
|
||||||
setLoading(true);
|
|
||||||
adminFetch(`/api/admin/open-day/bookings?eventId=${eventId}`)
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((data: OpenDayBooking[]) => setBookings(data))
|
|
||||||
.catch(() => {})
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (!open) load();
|
|
||||||
setOpen(!open);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleToggle(id: number, field: "notified_confirm" | "notified_reminder") {
|
|
||||||
const b = bookings.find((x) => x.id === id);
|
|
||||||
if (!b) return;
|
|
||||||
const key = field === "notified_confirm" ? "notifiedConfirm" : "notifiedReminder";
|
|
||||||
const newValue = !b[key];
|
|
||||||
setBookings((prev) => prev.map((x) => x.id === id ? { ...x, [key]: newValue } : x));
|
|
||||||
await adminFetch("/api/admin/open-day/bookings", {
|
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ action: "toggle-notify", id, field, value: newValue }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDelete(id: number) {
|
|
||||||
await adminFetch(`/api/admin/open-day/bookings?id=${id}`, { method: "DELETE" });
|
|
||||||
setBookings((prev) => prev.filter((x) => x.id !== id));
|
|
||||||
}
|
|
||||||
|
|
||||||
const newCount = bookings.filter((b) => !b.notifiedConfirm).length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-white/10 bg-neutral-900 p-5">
|
|
||||||
<button
|
|
||||||
onClick={toggle}
|
|
||||||
className="flex items-center gap-2 text-lg font-bold hover:text-gold transition-colors"
|
|
||||||
>
|
|
||||||
{open ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
|
|
||||||
Записи
|
|
||||||
{bookings.length > 0 && (
|
|
||||||
<span className="text-sm font-normal text-neutral-400">({bookings.length})</span>
|
|
||||||
)}
|
|
||||||
{newCount > 0 && (
|
|
||||||
<span className="rounded-full bg-red-500/20 text-red-400 px-2 py-0.5 text-[10px] font-medium">
|
|
||||||
{newCount} новых
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{open && (
|
|
||||||
<div className="mt-3 space-y-2">
|
|
||||||
{loading && (
|
|
||||||
<div className="flex items-center gap-2 text-neutral-500 text-sm py-4 justify-center">
|
|
||||||
<Loader2 size={14} className="animate-spin" />
|
|
||||||
Загрузка...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!loading && bookings.length === 0 && (
|
|
||||||
<p className="text-sm text-neutral-500 text-center py-4">Пока нет записей</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{bookings.map((b) => (
|
|
||||||
<div
|
|
||||||
key={b.id}
|
|
||||||
className={`rounded-lg p-3 space-y-1.5 ${
|
|
||||||
!b.notifiedConfirm ? "bg-gold/[0.03] border border-gold/20" : "bg-neutral-800/50 border border-white/5"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2 flex-wrap text-sm">
|
|
||||||
<span className="font-medium text-white">{b.name}</span>
|
|
||||||
<a
|
|
||||||
href={`tel:${b.phone}`}
|
|
||||||
className="inline-flex items-center gap-1 text-emerald-400 hover:text-emerald-300 text-xs"
|
|
||||||
>
|
|
||||||
<Phone size={10} />
|
|
||||||
{b.phone}
|
|
||||||
</a>
|
|
||||||
{b.instagram && (
|
|
||||||
<a
|
|
||||||
href={`https://ig.me/m/${b.instagram.replace(/^@/, "")}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-1 text-pink-400 hover:text-pink-300 text-xs"
|
|
||||||
>
|
|
||||||
<Instagram size={10} />
|
|
||||||
{b.instagram}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{b.telegram && (
|
|
||||||
<a
|
|
||||||
href={`https://t.me/${b.telegram.replace(/^@/, "")}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="inline-flex items-center gap-1 text-blue-400 hover:text-blue-300 text-xs"
|
|
||||||
>
|
|
||||||
<Send size={10} />
|
|
||||||
{b.telegram}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
<span className="text-[10px] text-neutral-500 ml-auto">
|
|
||||||
{b.classHall} {b.classTime} · {b.classStyle}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDelete(b.id)}
|
|
||||||
className="rounded p-1 text-neutral-500 hover:text-red-400"
|
|
||||||
>
|
|
||||||
<Trash2 size={12} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<NotifyToggle
|
|
||||||
confirmed={b.notifiedConfirm}
|
|
||||||
reminded={b.notifiedReminder}
|
|
||||||
reminderUrgent={reminderUrgent && !b.notifiedReminder}
|
|
||||||
onToggleConfirm={() => handleToggle(b.id, "notified_confirm")}
|
|
||||||
onToggleReminder={() => handleToggle(b.id, "notified_reminder")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Main Page ---
|
// --- Main Page ---
|
||||||
|
|
||||||
@@ -705,7 +538,6 @@ export default function OpenDayAdminPage() {
|
|||||||
onClassesChange={() => loadClasses(event.id)}
|
onClassesChange={() => loadClasses(event.id)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<BookingsSection eventId={event.id} eventDate={event.date} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user