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:
600
src/lib/db.ts
600
src/lib/db.ts
@@ -80,6 +80,97 @@ const migrations: Migration[] = [
|
||||
`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 4,
|
||||
name: "add_mc_notification_tracking",
|
||||
up: (db) => {
|
||||
const cols = db.prepare("PRAGMA table_info(mc_registrations)").all() as { name: string }[];
|
||||
const colNames = new Set(cols.map((c) => c.name));
|
||||
if (!colNames.has("notified_confirm"))
|
||||
db.exec("ALTER TABLE mc_registrations ADD COLUMN notified_confirm INTEGER NOT NULL DEFAULT 0");
|
||||
if (!colNames.has("notified_reminder"))
|
||||
db.exec("ALTER TABLE mc_registrations ADD COLUMN notified_reminder INTEGER NOT NULL DEFAULT 0");
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 5,
|
||||
name: "create_group_bookings",
|
||||
up: (db) => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS group_bookings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT NOT NULL,
|
||||
group_info TEXT,
|
||||
notified_confirm INTEGER NOT NULL DEFAULT 0,
|
||||
notified_reminder INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 6,
|
||||
name: "create_open_day_tables",
|
||||
up: (db) => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS open_day_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date TEXT NOT NULL,
|
||||
title TEXT NOT NULL DEFAULT 'День открытых дверей',
|
||||
description TEXT,
|
||||
price_per_class INTEGER NOT NULL DEFAULT 30,
|
||||
discount_price INTEGER NOT NULL DEFAULT 20,
|
||||
discount_threshold INTEGER NOT NULL DEFAULT 3,
|
||||
min_bookings INTEGER NOT NULL DEFAULT 4,
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
updated_at TEXT DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS open_day_classes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_id INTEGER NOT NULL REFERENCES open_day_events(id) ON DELETE CASCADE,
|
||||
hall TEXT NOT NULL,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
trainer TEXT NOT NULL,
|
||||
style TEXT NOT NULL,
|
||||
cancelled INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(event_id, hall, start_time)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS open_day_bookings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
class_id INTEGER NOT NULL REFERENCES open_day_classes(id) ON DELETE CASCADE,
|
||||
event_id INTEGER NOT NULL REFERENCES open_day_events(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
phone TEXT NOT NULL,
|
||||
instagram TEXT,
|
||||
telegram TEXT,
|
||||
notified_confirm INTEGER NOT NULL DEFAULT 0,
|
||||
notified_reminder INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT DEFAULT (datetime('now')),
|
||||
UNIQUE(class_id, phone)
|
||||
);
|
||||
`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 7,
|
||||
name: "unify_booking_fields",
|
||||
up: (db) => {
|
||||
// Add phone to mc_registrations
|
||||
const mcCols = db.prepare("PRAGMA table_info(mc_registrations)").all() as { name: string }[];
|
||||
const mcColNames = new Set(mcCols.map((c) => c.name));
|
||||
if (!mcColNames.has("phone")) db.exec("ALTER TABLE mc_registrations ADD COLUMN phone TEXT");
|
||||
|
||||
// Add instagram/telegram to group_bookings
|
||||
const gbCols = db.prepare("PRAGMA table_info(group_bookings)").all() as { name: string }[];
|
||||
const gbColNames = new Set(gbCols.map((c) => c.name));
|
||||
if (!gbColNames.has("instagram")) db.exec("ALTER TABLE group_bookings ADD COLUMN instagram TEXT");
|
||||
if (!gbColNames.has("telegram")) db.exec("ALTER TABLE group_bookings ADD COLUMN telegram TEXT");
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function runMigrations(db: Database.Database) {
|
||||
@@ -350,8 +441,11 @@ interface McRegistrationRow {
|
||||
master_class_title: string;
|
||||
name: string;
|
||||
instagram: string;
|
||||
phone: string | null;
|
||||
telegram: string | null;
|
||||
created_at: string;
|
||||
notified_confirm: number;
|
||||
notified_reminder: number;
|
||||
}
|
||||
|
||||
export interface McRegistration {
|
||||
@@ -359,23 +453,27 @@ export interface McRegistration {
|
||||
masterClassTitle: string;
|
||||
name: string;
|
||||
instagram: string;
|
||||
phone?: string;
|
||||
telegram?: string;
|
||||
createdAt: string;
|
||||
notifiedConfirm: boolean;
|
||||
notifiedReminder: boolean;
|
||||
}
|
||||
|
||||
export function addMcRegistration(
|
||||
masterClassTitle: string,
|
||||
name: string,
|
||||
instagram: string,
|
||||
telegram?: string
|
||||
telegram?: string,
|
||||
phone?: string
|
||||
): number {
|
||||
const db = getDb();
|
||||
const result = db
|
||||
.prepare(
|
||||
`INSERT INTO mc_registrations (master_class_title, name, instagram, telegram)
|
||||
VALUES (?, ?, ?, ?)`
|
||||
`INSERT INTO mc_registrations (master_class_title, name, instagram, telegram, phone)
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(masterClassTitle, name, instagram, telegram || null);
|
||||
.run(masterClassTitle, name, instagram, telegram || null, phone || null);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
@@ -386,14 +484,29 @@ export function getMcRegistrations(masterClassTitle: string): McRegistration[] {
|
||||
"SELECT * FROM mc_registrations WHERE master_class_title = ? ORDER BY created_at DESC"
|
||||
)
|
||||
.all(masterClassTitle) as McRegistrationRow[];
|
||||
return rows.map((r) => ({
|
||||
return rows.map(mapMcRow);
|
||||
}
|
||||
|
||||
export function getAllMcRegistrations(): McRegistration[] {
|
||||
const db = getDb();
|
||||
const rows = db
|
||||
.prepare("SELECT * FROM mc_registrations ORDER BY created_at DESC")
|
||||
.all() as McRegistrationRow[];
|
||||
return rows.map(mapMcRow);
|
||||
}
|
||||
|
||||
function mapMcRow(r: McRegistrationRow): McRegistration {
|
||||
return {
|
||||
id: r.id,
|
||||
masterClassTitle: r.master_class_title,
|
||||
name: r.name,
|
||||
instagram: r.instagram,
|
||||
phone: r.phone ?? undefined,
|
||||
telegram: r.telegram ?? undefined,
|
||||
createdAt: r.created_at,
|
||||
}));
|
||||
notifiedConfirm: !!r.notified_confirm,
|
||||
notifiedReminder: !!r.notified_reminder,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateMcRegistration(
|
||||
@@ -408,9 +521,484 @@ export function updateMcRegistration(
|
||||
).run(name, instagram, telegram || null, id);
|
||||
}
|
||||
|
||||
export function toggleMcNotification(
|
||||
id: number,
|
||||
field: "notified_confirm" | "notified_reminder",
|
||||
value: boolean
|
||||
): void {
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
`UPDATE mc_registrations SET ${field} = ? WHERE id = ?`
|
||||
).run(value ? 1 : 0, id);
|
||||
}
|
||||
|
||||
export function deleteMcRegistration(id: number): void {
|
||||
const db = getDb();
|
||||
db.prepare("DELETE FROM mc_registrations WHERE id = ?").run(id);
|
||||
}
|
||||
|
||||
// --- Group Bookings ---
|
||||
|
||||
interface GroupBookingRow {
|
||||
id: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
group_info: string | null;
|
||||
instagram: string | null;
|
||||
telegram: string | null;
|
||||
notified_confirm: number;
|
||||
notified_reminder: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface GroupBooking {
|
||||
id: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
groupInfo?: string;
|
||||
instagram?: string;
|
||||
telegram?: string;
|
||||
notifiedConfirm: boolean;
|
||||
notifiedReminder: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export function addGroupBooking(
|
||||
name: string,
|
||||
phone: string,
|
||||
groupInfo?: string,
|
||||
instagram?: string,
|
||||
telegram?: string
|
||||
): number {
|
||||
const db = getDb();
|
||||
const result = db
|
||||
.prepare(
|
||||
"INSERT INTO group_bookings (name, phone, group_info, instagram, telegram) VALUES (?, ?, ?, ?, ?)"
|
||||
)
|
||||
.run(name, phone, groupInfo || null, instagram || null, telegram || null);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
export function getGroupBookings(): GroupBooking[] {
|
||||
const db = getDb();
|
||||
const rows = db
|
||||
.prepare("SELECT * FROM group_bookings ORDER BY created_at DESC")
|
||||
.all() as GroupBookingRow[];
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
phone: r.phone,
|
||||
groupInfo: r.group_info ?? undefined,
|
||||
instagram: r.instagram ?? undefined,
|
||||
telegram: r.telegram ?? undefined,
|
||||
notifiedConfirm: !!r.notified_confirm,
|
||||
notifiedReminder: !!r.notified_reminder,
|
||||
createdAt: r.created_at,
|
||||
}));
|
||||
}
|
||||
|
||||
export function updateGroupBooking(
|
||||
id: number,
|
||||
name: string,
|
||||
phone: string,
|
||||
groupInfo?: string
|
||||
): void {
|
||||
const db = getDb();
|
||||
db.prepare(
|
||||
"UPDATE group_bookings SET name = ?, phone = ?, group_info = ? WHERE id = ?"
|
||||
).run(name, phone, groupInfo || null, id);
|
||||
}
|
||||
|
||||
export function toggleGroupBookingNotification(
|
||||
id: number,
|
||||
field: "notified_confirm" | "notified_reminder",
|
||||
value: boolean
|
||||
): void {
|
||||
const db = getDb();
|
||||
db.prepare(`UPDATE group_bookings SET ${field} = ? WHERE id = ?`).run(
|
||||
value ? 1 : 0,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteGroupBooking(id: number): void {
|
||||
const db = getDb();
|
||||
db.prepare("DELETE FROM group_bookings WHERE id = ?").run(id);
|
||||
}
|
||||
|
||||
// --- Unread booking counts (lightweight) ---
|
||||
|
||||
export interface UnreadCounts {
|
||||
groupBookings: number;
|
||||
mcRegistrations: number;
|
||||
openDayBookings: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export function getUnreadBookingCounts(): UnreadCounts {
|
||||
const db = getDb();
|
||||
const gb = (db.prepare("SELECT COUNT(*) as c FROM group_bookings WHERE notified_confirm = 0").get() as { c: number }).c;
|
||||
const mc = (db.prepare("SELECT COUNT(*) as c FROM mc_registrations WHERE notified_confirm = 0").get() as { c: number }).c;
|
||||
|
||||
let od = 0;
|
||||
// Check if open_day_bookings table exists (might not if no migration yet)
|
||||
try {
|
||||
od = (db.prepare("SELECT COUNT(*) as c FROM open_day_bookings WHERE notified_confirm = 0").get() as { c: number }).c;
|
||||
} catch { /* table doesn't exist yet */ }
|
||||
|
||||
return { groupBookings: gb, mcRegistrations: mc, openDayBookings: od, total: gb + mc + od };
|
||||
}
|
||||
|
||||
// --- Open Day Events ---
|
||||
|
||||
interface OpenDayEventRow {
|
||||
id: number;
|
||||
date: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
price_per_class: number;
|
||||
discount_price: number;
|
||||
discount_threshold: number;
|
||||
min_bookings: number;
|
||||
active: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface OpenDayEvent {
|
||||
id: number;
|
||||
date: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
pricePerClass: number;
|
||||
discountPrice: number;
|
||||
discountThreshold: number;
|
||||
minBookings: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface OpenDayClassRow {
|
||||
id: number;
|
||||
event_id: number;
|
||||
hall: string;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
trainer: string;
|
||||
style: string;
|
||||
cancelled: number;
|
||||
sort_order: number;
|
||||
booking_count?: number;
|
||||
}
|
||||
|
||||
export interface OpenDayClass {
|
||||
id: number;
|
||||
eventId: number;
|
||||
hall: string;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
trainer: string;
|
||||
style: string;
|
||||
cancelled: boolean;
|
||||
sortOrder: number;
|
||||
bookingCount: number;
|
||||
}
|
||||
|
||||
interface OpenDayBookingRow {
|
||||
id: number;
|
||||
class_id: number;
|
||||
event_id: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
instagram: string | null;
|
||||
telegram: string | null;
|
||||
notified_confirm: number;
|
||||
notified_reminder: number;
|
||||
created_at: string;
|
||||
class_style?: string;
|
||||
class_trainer?: string;
|
||||
class_time?: string;
|
||||
class_hall?: string;
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
function mapEventRow(r: OpenDayEventRow): OpenDayEvent {
|
||||
return {
|
||||
id: r.id,
|
||||
date: r.date,
|
||||
title: r.title,
|
||||
description: r.description ?? undefined,
|
||||
pricePerClass: r.price_per_class,
|
||||
discountPrice: r.discount_price,
|
||||
discountThreshold: r.discount_threshold,
|
||||
minBookings: r.min_bookings,
|
||||
active: !!r.active,
|
||||
};
|
||||
}
|
||||
|
||||
function mapClassRow(r: OpenDayClassRow): OpenDayClass {
|
||||
return {
|
||||
id: r.id,
|
||||
eventId: r.event_id,
|
||||
hall: r.hall,
|
||||
startTime: r.start_time,
|
||||
endTime: r.end_time,
|
||||
trainer: r.trainer,
|
||||
style: r.style,
|
||||
cancelled: !!r.cancelled,
|
||||
sortOrder: r.sort_order,
|
||||
bookingCount: r.booking_count ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
function mapBookingRow(r: OpenDayBookingRow): OpenDayBooking {
|
||||
return {
|
||||
id: r.id,
|
||||
classId: r.class_id,
|
||||
eventId: r.event_id,
|
||||
name: r.name,
|
||||
phone: r.phone,
|
||||
instagram: r.instagram ?? undefined,
|
||||
telegram: r.telegram ?? undefined,
|
||||
notifiedConfirm: !!r.notified_confirm,
|
||||
notifiedReminder: !!r.notified_reminder,
|
||||
createdAt: r.created_at,
|
||||
classStyle: r.class_style ?? undefined,
|
||||
classTrainer: r.class_trainer ?? undefined,
|
||||
classTime: r.class_time ?? undefined,
|
||||
classHall: r.class_hall ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function createOpenDayEvent(data: {
|
||||
date: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
pricePerClass?: number;
|
||||
discountPrice?: number;
|
||||
discountThreshold?: number;
|
||||
minBookings?: number;
|
||||
}): number {
|
||||
const db = getDb();
|
||||
const result = db
|
||||
.prepare(
|
||||
`INSERT INTO open_day_events (date, title, description, price_per_class, discount_price, discount_threshold, min_bookings)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(
|
||||
data.date,
|
||||
data.title || "День открытых дверей",
|
||||
data.description || null,
|
||||
data.pricePerClass ?? 30,
|
||||
data.discountPrice ?? 20,
|
||||
data.discountThreshold ?? 3,
|
||||
data.minBookings ?? 4
|
||||
);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
export function getOpenDayEvents(): OpenDayEvent[] {
|
||||
const db = getDb();
|
||||
const rows = db
|
||||
.prepare("SELECT * FROM open_day_events ORDER BY date DESC")
|
||||
.all() as OpenDayEventRow[];
|
||||
return rows.map(mapEventRow);
|
||||
}
|
||||
|
||||
export function getOpenDayEvent(id: number): OpenDayEvent | null {
|
||||
const db = getDb();
|
||||
const row = db
|
||||
.prepare("SELECT * FROM open_day_events WHERE id = ?")
|
||||
.get(id) as OpenDayEventRow | undefined;
|
||||
return row ? mapEventRow(row) : null;
|
||||
}
|
||||
|
||||
export function getActiveOpenDayEvent(): OpenDayEvent | null {
|
||||
const db = getDb();
|
||||
const row = db
|
||||
.prepare(
|
||||
"SELECT * FROM open_day_events WHERE active = 1 AND date >= date('now') ORDER BY date ASC LIMIT 1"
|
||||
)
|
||||
.get() as OpenDayEventRow | undefined;
|
||||
return row ? mapEventRow(row) : null;
|
||||
}
|
||||
|
||||
export function updateOpenDayEvent(
|
||||
id: number,
|
||||
data: Partial<{
|
||||
date: string;
|
||||
title: string;
|
||||
description: string;
|
||||
pricePerClass: number;
|
||||
discountPrice: number;
|
||||
discountThreshold: number;
|
||||
minBookings: number;
|
||||
active: boolean;
|
||||
}>
|
||||
): void {
|
||||
const db = getDb();
|
||||
const sets: string[] = [];
|
||||
const vals: unknown[] = [];
|
||||
if (data.date !== undefined) { sets.push("date = ?"); vals.push(data.date); }
|
||||
if (data.title !== undefined) { sets.push("title = ?"); vals.push(data.title); }
|
||||
if (data.description !== undefined) { sets.push("description = ?"); vals.push(data.description || null); }
|
||||
if (data.pricePerClass !== undefined) { sets.push("price_per_class = ?"); vals.push(data.pricePerClass); }
|
||||
if (data.discountPrice !== undefined) { sets.push("discount_price = ?"); vals.push(data.discountPrice); }
|
||||
if (data.discountThreshold !== undefined) { sets.push("discount_threshold = ?"); vals.push(data.discountThreshold); }
|
||||
if (data.minBookings !== undefined) { sets.push("min_bookings = ?"); vals.push(data.minBookings); }
|
||||
if (data.active !== undefined) { sets.push("active = ?"); vals.push(data.active ? 1 : 0); }
|
||||
if (sets.length === 0) return;
|
||||
sets.push("updated_at = datetime('now')");
|
||||
vals.push(id);
|
||||
db.prepare(`UPDATE open_day_events SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
|
||||
}
|
||||
|
||||
export function deleteOpenDayEvent(id: number): void {
|
||||
const db = getDb();
|
||||
db.prepare("DELETE FROM open_day_events WHERE id = ?").run(id);
|
||||
}
|
||||
|
||||
// --- Open Day Classes ---
|
||||
|
||||
export function addOpenDayClass(
|
||||
eventId: number,
|
||||
data: { hall: string; startTime: string; endTime: string; trainer: string; style: string }
|
||||
): number {
|
||||
const db = getDb();
|
||||
const maxOrder = (
|
||||
db.prepare("SELECT MAX(sort_order) as m FROM open_day_classes WHERE event_id = ?").get(eventId) as { m: number | null }
|
||||
).m ?? -1;
|
||||
const result = db
|
||||
.prepare(
|
||||
`INSERT INTO open_day_classes (event_id, hall, start_time, end_time, trainer, style, sort_order)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(eventId, data.hall, data.startTime, data.endTime, data.trainer, data.style, maxOrder + 1);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
export function getOpenDayClasses(eventId: number): OpenDayClass[] {
|
||||
const db = getDb();
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT c.*, COALESCE(b.cnt, 0) as booking_count
|
||||
FROM open_day_classes c
|
||||
LEFT JOIN (SELECT class_id, COUNT(*) as cnt FROM open_day_bookings GROUP BY class_id) b ON b.class_id = c.id
|
||||
WHERE c.event_id = ?
|
||||
ORDER BY c.hall, c.start_time`
|
||||
)
|
||||
.all(eventId) as OpenDayClassRow[];
|
||||
return rows.map(mapClassRow);
|
||||
}
|
||||
|
||||
export function updateOpenDayClass(
|
||||
id: number,
|
||||
data: Partial<{ hall: string; startTime: string; endTime: string; trainer: string; style: string; cancelled: boolean; sortOrder: number }>
|
||||
): void {
|
||||
const db = getDb();
|
||||
const sets: string[] = [];
|
||||
const vals: unknown[] = [];
|
||||
if (data.hall !== undefined) { sets.push("hall = ?"); vals.push(data.hall); }
|
||||
if (data.startTime !== undefined) { sets.push("start_time = ?"); vals.push(data.startTime); }
|
||||
if (data.endTime !== undefined) { sets.push("end_time = ?"); vals.push(data.endTime); }
|
||||
if (data.trainer !== undefined) { sets.push("trainer = ?"); vals.push(data.trainer); }
|
||||
if (data.style !== undefined) { sets.push("style = ?"); vals.push(data.style); }
|
||||
if (data.cancelled !== undefined) { sets.push("cancelled = ?"); vals.push(data.cancelled ? 1 : 0); }
|
||||
if (data.sortOrder !== undefined) { sets.push("sort_order = ?"); vals.push(data.sortOrder); }
|
||||
if (sets.length === 0) return;
|
||||
vals.push(id);
|
||||
db.prepare(`UPDATE open_day_classes SET ${sets.join(", ")} WHERE id = ?`).run(...vals);
|
||||
}
|
||||
|
||||
export function deleteOpenDayClass(id: number): void {
|
||||
const db = getDb();
|
||||
db.prepare("DELETE FROM open_day_classes WHERE id = ?").run(id);
|
||||
}
|
||||
|
||||
// --- Open Day Bookings ---
|
||||
|
||||
export function addOpenDayBooking(
|
||||
classId: number,
|
||||
eventId: number,
|
||||
data: { name: string; phone: string; instagram?: string; telegram?: string }
|
||||
): number {
|
||||
const db = getDb();
|
||||
const result = db
|
||||
.prepare(
|
||||
`INSERT INTO open_day_bookings (class_id, event_id, name, phone, instagram, telegram)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`
|
||||
)
|
||||
.run(classId, eventId, data.name, data.phone, data.instagram || null, data.telegram || null);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
export function getOpenDayBookings(eventId: number): OpenDayBooking[] {
|
||||
const db = getDb();
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT b.*, c.style as class_style, c.trainer as class_trainer, c.start_time as class_time, c.hall as class_hall
|
||||
FROM open_day_bookings b
|
||||
JOIN open_day_classes c ON c.id = b.class_id
|
||||
WHERE b.event_id = ?
|
||||
ORDER BY b.created_at DESC`
|
||||
)
|
||||
.all(eventId) as OpenDayBookingRow[];
|
||||
return rows.map(mapBookingRow);
|
||||
}
|
||||
|
||||
export function getOpenDayBookingCountsByClass(eventId: number): Record<number, number> {
|
||||
const db = getDb();
|
||||
const rows = db
|
||||
.prepare("SELECT class_id, COUNT(*) as cnt FROM open_day_bookings WHERE event_id = ? GROUP BY class_id")
|
||||
.all(eventId) as { class_id: number; cnt: number }[];
|
||||
const result: Record<number, number> = {};
|
||||
for (const r of rows) result[r.class_id] = r.cnt;
|
||||
return result;
|
||||
}
|
||||
|
||||
export function getPersonOpenDayBookings(eventId: number, phone: string): number {
|
||||
const db = getDb();
|
||||
const row = db
|
||||
.prepare("SELECT COUNT(*) as cnt FROM open_day_bookings WHERE event_id = ? AND phone = ?")
|
||||
.get(eventId, phone) as { cnt: number };
|
||||
return row.cnt;
|
||||
}
|
||||
|
||||
export function toggleOpenDayNotification(
|
||||
id: number,
|
||||
field: "notified_confirm" | "notified_reminder",
|
||||
value: boolean
|
||||
): void {
|
||||
const db = getDb();
|
||||
db.prepare(`UPDATE open_day_bookings SET ${field} = ? WHERE id = ?`).run(value ? 1 : 0, id);
|
||||
}
|
||||
|
||||
export function deleteOpenDayBooking(id: number): void {
|
||||
const db = getDb();
|
||||
db.prepare("DELETE FROM open_day_bookings WHERE id = ?").run(id);
|
||||
}
|
||||
|
||||
export function isOpenDayClassBookedByPhone(classId: number, phone: string): boolean {
|
||||
const db = getDb();
|
||||
const row = db
|
||||
.prepare("SELECT id FROM open_day_bookings WHERE class_id = ? AND phone = ? LIMIT 1")
|
||||
.get(classId, phone) as { id: number } | undefined;
|
||||
return !!row;
|
||||
}
|
||||
|
||||
export { SECTION_KEYS };
|
||||
|
||||
Reference in New Issue
Block a user