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>
This commit is contained in:
54
src/app/api/open-day-register/route.ts
Normal file
54
src/app/api/open-day-register/route.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import {
|
||||
addOpenDayBooking,
|
||||
isOpenDayClassBookedByPhone,
|
||||
getPersonOpenDayBookings,
|
||||
getOpenDayEvent,
|
||||
} from "@/lib/db";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { classId, eventId, name, phone, instagram, telegram } = body;
|
||||
|
||||
if (!classId || !eventId || !name || !phone) {
|
||||
return NextResponse.json({ error: "classId, eventId, name, phone are required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const cleanName = (name as string).trim().slice(0, 100);
|
||||
const cleanPhone = (phone as string).replace(/\D/g, "").slice(0, 15);
|
||||
const cleanIg = instagram ? (instagram as string).trim().slice(0, 100) : undefined;
|
||||
const cleanTg = telegram ? (telegram as string).trim().slice(0, 100) : undefined;
|
||||
|
||||
if (!cleanPhone) {
|
||||
return NextResponse.json({ error: "Invalid phone" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Check not already booked
|
||||
if (isOpenDayClassBookedByPhone(classId, cleanPhone)) {
|
||||
return NextResponse.json({ error: "Вы уже записаны на это занятие" }, { status: 409 });
|
||||
}
|
||||
|
||||
const id = addOpenDayBooking(classId, eventId, {
|
||||
name: cleanName,
|
||||
phone: cleanPhone,
|
||||
instagram: cleanIg,
|
||||
telegram: cleanTg,
|
||||
});
|
||||
|
||||
// Return total bookings for this person (for discount calculation)
|
||||
const totalBookings = getPersonOpenDayBookings(eventId, cleanPhone);
|
||||
const event = getOpenDayEvent(eventId);
|
||||
const pricePerClass = event && totalBookings >= event.discountThreshold
|
||||
? event.discountPrice
|
||||
: event?.pricePerClass ?? 30;
|
||||
|
||||
return NextResponse.json({ ok: true, id, totalBookings, pricePerClass });
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : "Internal error";
|
||||
if (msg.includes("UNIQUE")) {
|
||||
return NextResponse.json({ error: "Вы уже записаны на это занятие" }, { status: 409 });
|
||||
}
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user