diff --git a/src/app/admin/bookings/page.tsx b/src/app/admin/bookings/page.tsx index bf0d8f9..80ead69 100644 --- a/src/app/admin/bookings/page.tsx +++ b/src/app/admin/bookings/page.tsx @@ -15,6 +15,8 @@ interface GroupBooking { telegram?: string; notifiedConfirm: boolean; notifiedReminder: boolean; + status: BookingStatus; + confirmedDate?: string; createdAt: string; } @@ -48,12 +50,22 @@ interface OpenDayBooking { } type Tab = "reminders" | "classes" | "master-classes" | "open-day"; +type BookingStatus = "new" | "contacted" | "confirmed" | "declined"; +type BookingFilter = "all" | BookingStatus; + +const BOOKING_STATUSES: { key: BookingStatus; label: string; color: string; bg: string; border: string }[] = [ + { key: "new", label: "Новая", color: "text-gold", bg: "bg-gold/10", border: "border-gold/30" }, + { key: "contacted", label: "Связались", color: "text-blue-400", bg: "bg-blue-500/10", border: "border-blue-500/30" }, + { key: "confirmed", label: "Подтверждено", color: "text-emerald-400", bg: "bg-emerald-500/10", border: "border-emerald-500/30" }, + { key: "declined", label: "Отказ", color: "text-red-400", bg: "bg-red-500/10", border: "border-red-500/30" }, +]; // --- Group Bookings Tab --- function GroupBookingsTab() { const [bookings, setBookings] = useState([]); const [loading, setLoading] = useState(true); + const [filter, setFilter] = useState("all"); useEffect(() => { adminFetch("/api/admin/group-bookings") @@ -63,6 +75,27 @@ function GroupBookingsTab() { .finally(() => setLoading(false)); }, []); + const counts = useMemo(() => { + const c: Record = { new: 0, contacted: 0, confirmed: 0, declined: 0 }; + for (const b of bookings) c[b.status] = (c[b.status] || 0) + 1; + return c; + }, [bookings]); + + const filtered = useMemo(() => { + const list = filter === "all" ? bookings : bookings.filter((b) => b.status === filter); + const order: Record = { new: 0, contacted: 1, confirmed: 2, declined: 3 }; + return [...list].sort((a, b) => (order[a.status] ?? 0) - (order[b.status] ?? 0)); + }, [bookings, filter]); + + async function handleStatus(id: number, status: BookingStatus, confirmedDate?: string) { + setBookings((prev) => prev.map((b) => b.id === id ? { ...b, status, confirmedDate } : b)); + await adminFetch("/api/admin/group-bookings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "set-status", id, status, confirmedDate }), + }); + } + async function handleDelete(id: number) { await adminFetch(`/api/admin/group-bookings?id=${id}`, { method: "DELETE" }); setBookings((prev) => prev.filter((b) => b.id !== id)); @@ -71,40 +104,127 @@ function GroupBookingsTab() { if (loading) return ; return ( -
- {bookings.length === 0 && } - {bookings.map((b) => ( -
+ {/* Filter tabs */} +
+ + {BOOKING_STATUSES.map((s) => ( + + ))} +
+ + {/* Bookings list */} +
+ {filtered.length === 0 && } + {filtered.map((b) => { + const statusConf = BOOKING_STATUSES.find((s) => s.key === b.status) || BOOKING_STATUSES[0]; + return ( +
+
+
+ {b.name} + + {b.phone} - )} - {b.telegram && ( - - {b.telegram} - - )} - {b.groupInfo && ( - {b.groupInfo} - )} + {b.instagram && ( + + {b.instagram} + + )} + {b.telegram && ( + + {b.telegram} + + )} + {b.groupInfo && ( + {b.groupInfo} + )} +
+
+ {fmtDate(b.createdAt)} + handleDelete(b.id)} /> +
-
- {fmtDate(b.createdAt)} - handleDelete(b.id)} /> + {/* Linear status flow */} +
+ {/* Current status badge */} + + {statusConf.label} + + + {b.status === "confirmed" && b.confirmedDate && ( + + Дата: {new Date(b.confirmedDate + "T12:00").toLocaleDateString("ru-RU", { day: "numeric", month: "short" })} + + )} + + {/* Action buttons based on current state */} +
+ {b.status === "new" && ( + + )} + {b.status === "contacted" && ( + <> + { + if (e.target.value) handleStatus(b.id, "confirmed", e.target.value); + }} + className="h-6 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/30 px-2 text-[10px] cursor-pointer hover:bg-emerald-500/20 transition-all [color-scheme:dark]" + title="Подтвердить — выберите дату занятия" + /> + + + )} + {(b.status === "confirmed" || b.status === "declined") && ( + + )} +
-
- ))} + ); + })} +
); } diff --git a/src/app/api/admin/group-bookings/route.ts b/src/app/api/admin/group-bookings/route.ts index 1d134f5..254f7ad 100644 --- a/src/app/api/admin/group-bookings/route.ts +++ b/src/app/api/admin/group-bookings/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { getGroupBookings, toggleGroupBookingNotification, deleteGroupBooking } from "@/lib/db"; +import { getGroupBookings, toggleGroupBookingNotification, deleteGroupBooking, setGroupBookingStatus } from "@/lib/db"; +import type { BookingStatus } from "@/lib/db"; export async function GET() { const bookings = getGroupBookings(); @@ -22,6 +23,15 @@ export async function PUT(request: NextRequest) { toggleGroupBookingNotification(id, field, value); return NextResponse.json({ ok: true }); } + if (body.action === "set-status") { + const { id, status, confirmedDate } = body; + const valid: BookingStatus[] = ["new", "contacted", "confirmed", "declined"]; + if (!id || !valid.includes(status)) { + return NextResponse.json({ error: "id and valid status are required" }, { status: 400 }); + } + setGroupBookingStatus(id, status, confirmedDate); + return NextResponse.json({ ok: true }); + } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); } catch (err) { console.error("[admin/group-bookings] error:", err); diff --git a/src/lib/db.ts b/src/lib/db.ts index 2b26211..bb4ba5f 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -200,6 +200,19 @@ const migrations: Migration[] = [ `); }, }, + { + version: 10, + name: "add_group_booking_status", + up: (db) => { + const cols = db.prepare("PRAGMA table_info(group_bookings)").all() as { name: string }[]; + if (!cols.some((c) => c.name === "status")) { + db.exec("ALTER TABLE group_bookings ADD COLUMN status TEXT NOT NULL DEFAULT 'new'"); + } + if (!cols.some((c) => c.name === "confirmed_date")) { + db.exec("ALTER TABLE group_bookings ADD COLUMN confirmed_date TEXT"); + } + }, + }, ]; function runMigrations(db: Database.Database) { @@ -581,9 +594,13 @@ interface GroupBookingRow { notified_confirm: number; notified_reminder: number; reminder_status: string | null; + status: string; + confirmed_date: string | null; created_at: string; } +export type BookingStatus = "new" | "contacted" | "confirmed" | "declined"; + export interface GroupBooking { id: number; name: string; @@ -594,6 +611,8 @@ export interface GroupBooking { notifiedConfirm: boolean; notifiedReminder: boolean; reminderStatus?: string; + status: BookingStatus; + confirmedDate?: string; createdAt: string; } @@ -628,10 +647,21 @@ export function getGroupBookings(): GroupBooking[] { notifiedConfirm: !!r.notified_confirm, notifiedReminder: !!r.notified_reminder, reminderStatus: r.reminder_status ?? undefined, + status: (r.status || "new") as BookingStatus, + confirmedDate: r.confirmed_date ?? undefined, createdAt: r.created_at, })); } +export function setGroupBookingStatus(id: number, status: BookingStatus, confirmedDate?: string): void { + const db = getDb(); + if (status === "confirmed" && confirmedDate) { + db.prepare("UPDATE group_bookings SET status = ?, confirmed_date = ? WHERE id = ?").run(status, confirmedDate, id); + } else { + db.prepare("UPDATE group_bookings SET status = ?, confirmed_date = NULL WHERE id = ?").run(status, id); + } +} + export function updateGroupBooking( id: number, name: string,