- Reminders query now includes 'contacted' group bookings with confirmed_date, preventing people from being forgotten when admin hasn't clicked "Подтвердить" - Confirmation details (group, date) remain visible regardless of booking status, so "Вернуть" no longer hides previously entered info
1299 lines
41 KiB
TypeScript
1299 lines
41 KiB
TypeScript
import Database from "better-sqlite3";
|
|
import path from "path";
|
|
import type { SiteContent, TeamMember, RichListItem, VictoryItem } from "@/types/content";
|
|
|
|
const DB_PATH =
|
|
process.env.DATABASE_PATH ||
|
|
path.join(process.cwd(), "db", "blackheart.db");
|
|
|
|
let _db: Database.Database | null = null;
|
|
|
|
export function getDb(): Database.Database {
|
|
if (!_db) {
|
|
_db = new Database(DB_PATH);
|
|
_db.pragma("journal_mode = WAL");
|
|
_db.pragma("foreign_keys = ON");
|
|
runMigrations(_db);
|
|
}
|
|
return _db;
|
|
}
|
|
|
|
// --- Migrations ---
|
|
// Each migration has a unique version number and runs exactly once.
|
|
// Add new migrations to the end of the array. Never modify existing ones.
|
|
|
|
interface Migration {
|
|
version: number;
|
|
name: string;
|
|
up: (db: Database.Database) => void;
|
|
}
|
|
|
|
const migrations: Migration[] = [
|
|
{
|
|
version: 1,
|
|
name: "create_sections_and_team_members",
|
|
up: (db) => {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS sections (
|
|
key TEXT PRIMARY KEY,
|
|
data TEXT NOT NULL,
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
CREATE TABLE IF NOT EXISTS team_members (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
role TEXT NOT NULL,
|
|
image TEXT NOT NULL,
|
|
instagram TEXT,
|
|
description TEXT,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
},
|
|
},
|
|
{
|
|
version: 2,
|
|
name: "add_team_bio_columns",
|
|
up: (db) => {
|
|
const cols = db.prepare("PRAGMA table_info(team_members)").all() as { name: string }[];
|
|
const colNames = new Set(cols.map((c) => c.name));
|
|
if (!colNames.has("experience")) db.exec("ALTER TABLE team_members ADD COLUMN experience TEXT");
|
|
if (!colNames.has("victories")) db.exec("ALTER TABLE team_members ADD COLUMN victories TEXT");
|
|
if (!colNames.has("education")) db.exec("ALTER TABLE team_members ADD COLUMN education TEXT");
|
|
},
|
|
},
|
|
{
|
|
version: 3,
|
|
name: "create_mc_registrations",
|
|
up: (db) => {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS mc_registrations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
master_class_title TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
instagram TEXT NOT NULL,
|
|
telegram TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
},
|
|
},
|
|
{
|
|
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");
|
|
},
|
|
},
|
|
{
|
|
version: 8,
|
|
name: "add_reminder_status",
|
|
up: (db) => {
|
|
// reminder_status: null = not reminded, 'pending' = reminded but no answer, 'coming' = confirmed, 'cancelled' = not coming
|
|
for (const table of ["mc_registrations", "group_bookings", "open_day_bookings"]) {
|
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
|
|
if (!cols.some((c) => c.name === "reminder_status")) {
|
|
db.exec(`ALTER TABLE ${table} ADD COLUMN reminder_status TEXT`);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
version: 9,
|
|
name: "add_performance_indexes",
|
|
up: (db) => {
|
|
db.exec(`
|
|
CREATE INDEX IF NOT EXISTS idx_mc_registrations_title ON mc_registrations(master_class_title);
|
|
CREATE INDEX IF NOT EXISTS idx_mc_registrations_created ON mc_registrations(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_group_bookings_created ON group_bookings(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_open_day_bookings_event ON open_day_bookings(event_id);
|
|
CREATE INDEX IF NOT EXISTS idx_open_day_bookings_class ON open_day_bookings(class_id);
|
|
CREATE INDEX IF NOT EXISTS idx_open_day_bookings_phone ON open_day_bookings(event_id, phone);
|
|
CREATE INDEX IF NOT EXISTS idx_open_day_bookings_class_phone ON open_day_bookings(class_id, phone);
|
|
CREATE INDEX IF NOT EXISTS idx_open_day_classes_event ON open_day_classes(event_id);
|
|
`);
|
|
},
|
|
},
|
|
{
|
|
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");
|
|
}
|
|
if (!cols.some((c) => c.name === "confirmed_group")) {
|
|
db.exec("ALTER TABLE group_bookings ADD COLUMN confirmed_group TEXT");
|
|
}
|
|
if (!cols.some((c) => c.name === "confirmed_comment")) {
|
|
db.exec("ALTER TABLE group_bookings ADD COLUMN confirmed_comment TEXT");
|
|
}
|
|
},
|
|
},
|
|
{
|
|
version: 11,
|
|
name: "add_team_short_description",
|
|
up: (db) => {
|
|
const cols = db.prepare("PRAGMA table_info(team_members)").all() as { name: string }[];
|
|
if (!cols.some((c) => c.name === "short_description")) {
|
|
db.exec("ALTER TABLE team_members ADD COLUMN short_description TEXT");
|
|
}
|
|
},
|
|
},
|
|
{
|
|
version: 12,
|
|
name: "add_status_to_mc_and_openday",
|
|
up: (db) => {
|
|
for (const table of ["mc_registrations", "open_day_bookings"]) {
|
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
|
|
const colNames = new Set(cols.map((c) => c.name));
|
|
if (!colNames.has("status")) {
|
|
db.exec(`ALTER TABLE ${table} ADD COLUMN status TEXT NOT NULL DEFAULT 'new'`);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
{
|
|
version: 13,
|
|
name: "add_booking_notes",
|
|
up: (db) => {
|
|
for (const table of ["mc_registrations", "group_bookings", "open_day_bookings"]) {
|
|
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];
|
|
if (!cols.some((c) => c.name === "notes")) {
|
|
db.exec(`ALTER TABLE ${table} ADD COLUMN notes TEXT`);
|
|
}
|
|
}
|
|
},
|
|
},
|
|
];
|
|
|
|
function runMigrations(db: Database.Database) {
|
|
// Create migrations tracking table
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS _migrations (
|
|
version INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
applied_at TEXT DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
|
|
const applied = new Set(
|
|
(db.prepare("SELECT version FROM _migrations").all() as { version: number }[])
|
|
.map((r) => r.version)
|
|
);
|
|
|
|
const pending = migrations.filter((m) => !applied.has(m.version));
|
|
if (pending.length === 0) return;
|
|
|
|
const insertMigration = db.prepare(
|
|
"INSERT INTO _migrations (version, name) VALUES (?, ?)"
|
|
);
|
|
|
|
const tx = db.transaction(() => {
|
|
for (const m of pending) {
|
|
m.up(db);
|
|
insertMigration.run(m.version, m.name);
|
|
}
|
|
});
|
|
tx();
|
|
}
|
|
|
|
// --- Sections ---
|
|
|
|
export function getSection<T = unknown>(key: string): T | null {
|
|
const db = getDb();
|
|
const row = db.prepare("SELECT data FROM sections WHERE key = ?").get(key) as
|
|
| { data: string }
|
|
| undefined;
|
|
return row ? (JSON.parse(row.data) as T) : null;
|
|
}
|
|
|
|
export function setSection(key: string, data: unknown): void {
|
|
const db = getDb();
|
|
db.prepare(
|
|
`INSERT INTO sections (key, data, updated_at) VALUES (?, ?, datetime('now'))
|
|
ON CONFLICT(key) DO UPDATE SET data = excluded.data, updated_at = excluded.updated_at`
|
|
).run(key, JSON.stringify(data));
|
|
}
|
|
|
|
// --- Team Members ---
|
|
|
|
interface TeamMemberRow {
|
|
id: number;
|
|
name: string;
|
|
role: string;
|
|
image: string;
|
|
instagram: string | null;
|
|
short_description: string | null;
|
|
description: string | null;
|
|
experience: string | null;
|
|
victories: string | null;
|
|
education: string | null;
|
|
sort_order: number;
|
|
}
|
|
|
|
function parseJsonArray(val: string | null): string[] | undefined {
|
|
if (!val) return undefined;
|
|
try { const arr = JSON.parse(val); return Array.isArray(arr) && arr.length > 0 ? arr : undefined; } catch { return undefined; }
|
|
}
|
|
|
|
function parseRichList(val: string | null): RichListItem[] | undefined {
|
|
if (!val) return undefined;
|
|
try {
|
|
const arr = JSON.parse(val);
|
|
if (!Array.isArray(arr) || arr.length === 0) return undefined;
|
|
// Handle both old string[] and new RichListItem[] formats
|
|
return arr.map((item: string | RichListItem) =>
|
|
typeof item === "string" ? { text: item } : item
|
|
);
|
|
} catch { return undefined; }
|
|
}
|
|
|
|
function parseVictories(val: string | null): VictoryItem[] | undefined {
|
|
if (!val) return undefined;
|
|
try {
|
|
const arr = JSON.parse(val);
|
|
if (!Array.isArray(arr) || arr.length === 0) return undefined;
|
|
// Handle old string[], old RichListItem[], and new VictoryItem[] formats
|
|
return arr.map((item: string | Record<string, unknown>) => {
|
|
if (typeof item === "string") return { place: "", category: "", competition: item };
|
|
if ("text" in item && !("competition" in item)) return { place: "", category: "", competition: item.text as string, image: item.image as string | undefined, link: item.link as string | undefined };
|
|
return item as unknown as VictoryItem;
|
|
});
|
|
} catch { return undefined; }
|
|
}
|
|
|
|
export function getTeamMembers(): (TeamMember & { id: number })[] {
|
|
const db = getDb();
|
|
const rows = db
|
|
.prepare("SELECT * FROM team_members ORDER BY sort_order ASC, id ASC")
|
|
.all() as TeamMemberRow[];
|
|
return rows.map((r) => ({
|
|
id: r.id,
|
|
name: r.name,
|
|
role: r.role,
|
|
image: r.image,
|
|
instagram: r.instagram ?? undefined,
|
|
shortDescription: r.short_description ?? undefined,
|
|
description: r.description ?? undefined,
|
|
experience: parseJsonArray(r.experience),
|
|
victories: parseVictories(r.victories),
|
|
education: parseRichList(r.education),
|
|
}));
|
|
}
|
|
|
|
export function getTeamMember(
|
|
id: number
|
|
): (TeamMember & { id: number }) | null {
|
|
const db = getDb();
|
|
const r = db
|
|
.prepare("SELECT * FROM team_members WHERE id = ?")
|
|
.get(id) as TeamMemberRow | undefined;
|
|
if (!r) return null;
|
|
return {
|
|
id: r.id,
|
|
name: r.name,
|
|
role: r.role,
|
|
image: r.image,
|
|
instagram: r.instagram ?? undefined,
|
|
shortDescription: r.short_description ?? undefined,
|
|
description: r.description ?? undefined,
|
|
experience: parseJsonArray(r.experience),
|
|
victories: parseVictories(r.victories),
|
|
education: parseRichList(r.education),
|
|
};
|
|
}
|
|
|
|
export function createTeamMember(
|
|
data: Omit<TeamMember, "id">
|
|
): number {
|
|
const db = getDb();
|
|
const maxOrder = db
|
|
.prepare("SELECT COALESCE(MAX(sort_order), -1) as max FROM team_members")
|
|
.get() as { max: number };
|
|
const result = db
|
|
.prepare(
|
|
`INSERT INTO team_members (name, role, image, instagram, short_description, description, experience, victories, education, sort_order)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
)
|
|
.run(
|
|
data.name,
|
|
data.role,
|
|
data.image,
|
|
data.instagram ?? null,
|
|
data.shortDescription ?? null,
|
|
data.description ?? null,
|
|
data.experience?.length ? JSON.stringify(data.experience) : null,
|
|
data.victories?.length ? JSON.stringify(data.victories) : null,
|
|
data.education?.length ? JSON.stringify(data.education) : null,
|
|
maxOrder.max + 1
|
|
);
|
|
return result.lastInsertRowid as number;
|
|
}
|
|
|
|
export function updateTeamMember(
|
|
id: number,
|
|
data: Partial<Omit<TeamMember, "id">>
|
|
): void {
|
|
const db = getDb();
|
|
const fields: string[] = [];
|
|
const values: unknown[] = [];
|
|
|
|
if (data.name !== undefined) { fields.push("name = ?"); values.push(data.name); }
|
|
if (data.role !== undefined) { fields.push("role = ?"); values.push(data.role); }
|
|
if (data.image !== undefined) { fields.push("image = ?"); values.push(data.image); }
|
|
if (data.instagram !== undefined) { fields.push("instagram = ?"); values.push(data.instagram || null); }
|
|
if (data.shortDescription !== undefined) { fields.push("short_description = ?"); values.push(data.shortDescription || null); }
|
|
if (data.description !== undefined) { fields.push("description = ?"); values.push(data.description || null); }
|
|
if (data.experience !== undefined) { fields.push("experience = ?"); values.push(data.experience?.length ? JSON.stringify(data.experience) : null); }
|
|
if (data.victories !== undefined) { fields.push("victories = ?"); values.push(data.victories?.length ? JSON.stringify(data.victories) : null); }
|
|
if (data.education !== undefined) { fields.push("education = ?"); values.push(data.education?.length ? JSON.stringify(data.education) : null); }
|
|
|
|
if (fields.length === 0) return;
|
|
fields.push("updated_at = datetime('now')");
|
|
values.push(id);
|
|
|
|
db.prepare(`UPDATE team_members SET ${fields.join(", ")} WHERE id = ?`).run(
|
|
...values
|
|
);
|
|
}
|
|
|
|
export function deleteTeamMember(id: number): void {
|
|
const db = getDb();
|
|
db.prepare("DELETE FROM team_members WHERE id = ?").run(id);
|
|
}
|
|
|
|
export function reorderTeamMembers(ids: number[]): void {
|
|
const db = getDb();
|
|
const stmt = db.prepare(
|
|
"UPDATE team_members SET sort_order = ? WHERE id = ?"
|
|
);
|
|
const tx = db.transaction(() => {
|
|
ids.forEach((id, index) => stmt.run(index, id));
|
|
});
|
|
tx();
|
|
}
|
|
|
|
// --- Full site content ---
|
|
|
|
const SECTION_KEYS = [
|
|
"meta",
|
|
"hero",
|
|
"about",
|
|
"classes",
|
|
"masterClasses",
|
|
"faq",
|
|
"pricing",
|
|
"schedule",
|
|
"news",
|
|
"contact",
|
|
] as const;
|
|
|
|
export function getSiteContent(): SiteContent | null {
|
|
const db = getDb();
|
|
const rows = db.prepare("SELECT key, data FROM sections").all() as {
|
|
key: string;
|
|
data: string;
|
|
}[];
|
|
|
|
if (rows.length === 0) return null;
|
|
|
|
const sections: Record<string, unknown> = {};
|
|
for (const row of rows) {
|
|
sections[row.key] = JSON.parse(row.data);
|
|
}
|
|
|
|
// Merge team members from dedicated table
|
|
const members = getTeamMembers();
|
|
const teamSection = (sections.team as { title?: string }) || {};
|
|
|
|
return {
|
|
meta: sections.meta,
|
|
hero: sections.hero,
|
|
about: sections.about,
|
|
classes: sections.classes,
|
|
masterClasses: sections.masterClasses ?? { title: "Мастер-классы", items: [] },
|
|
faq: sections.faq,
|
|
pricing: sections.pricing,
|
|
schedule: sections.schedule,
|
|
news: sections.news ?? { title: "Новости", items: [] },
|
|
contact: sections.contact,
|
|
team: {
|
|
title: teamSection.title || "",
|
|
members: members.map(({ id, ...rest }) => rest),
|
|
},
|
|
} as SiteContent;
|
|
}
|
|
|
|
export function isDatabaseSeeded(): boolean {
|
|
const db = getDb();
|
|
const row = db
|
|
.prepare("SELECT COUNT(*) as count FROM sections")
|
|
.get() as { count: number };
|
|
return row.count > 0;
|
|
}
|
|
|
|
// --- MC Registrations ---
|
|
|
|
interface McRegistrationRow {
|
|
id: number;
|
|
master_class_title: string;
|
|
name: string;
|
|
instagram: string;
|
|
phone: string | null;
|
|
telegram: string | null;
|
|
created_at: string;
|
|
notified_confirm: number;
|
|
notified_reminder: number;
|
|
reminder_status: string | null;
|
|
status: string;
|
|
notes: string | null;
|
|
}
|
|
|
|
export interface McRegistration {
|
|
id: number;
|
|
masterClassTitle: string;
|
|
name: string;
|
|
instagram: string;
|
|
phone?: string;
|
|
telegram?: string;
|
|
createdAt: string;
|
|
notifiedConfirm: boolean;
|
|
notifiedReminder: boolean;
|
|
reminderStatus?: string;
|
|
status: string;
|
|
notes?: string;
|
|
}
|
|
|
|
export function addMcRegistration(
|
|
masterClassTitle: string,
|
|
name: string,
|
|
instagram: string,
|
|
telegram?: string,
|
|
phone?: string
|
|
): number {
|
|
const db = getDb();
|
|
const result = db
|
|
.prepare(
|
|
`INSERT INTO mc_registrations (master_class_title, name, instagram, telegram, phone)
|
|
VALUES (?, ?, ?, ?, ?)`
|
|
)
|
|
.run(masterClassTitle, name, instagram, telegram || null, phone || null);
|
|
return result.lastInsertRowid as number;
|
|
}
|
|
|
|
export function getMcRegistrations(masterClassTitle: string): McRegistration[] {
|
|
const db = getDb();
|
|
const rows = db
|
|
.prepare(
|
|
"SELECT * FROM mc_registrations WHERE master_class_title = ? ORDER BY created_at DESC"
|
|
)
|
|
.all(masterClassTitle) as McRegistrationRow[];
|
|
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,
|
|
reminderStatus: r.reminder_status ?? undefined,
|
|
status: r.status || "new",
|
|
notes: r.notes ?? undefined,
|
|
};
|
|
}
|
|
|
|
export function setMcRegistrationStatus(id: number, status: string): void {
|
|
const db = getDb();
|
|
db.prepare("UPDATE mc_registrations SET status = ?, notified_confirm = 1 WHERE id = ?").run(status, id);
|
|
}
|
|
|
|
export function updateMcRegistration(
|
|
id: number,
|
|
name: string,
|
|
instagram: string,
|
|
telegram?: string
|
|
): void {
|
|
const db = getDb();
|
|
db.prepare(
|
|
"UPDATE mc_registrations SET name = ?, instagram = ?, telegram = ? WHERE id = ?"
|
|
).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;
|
|
reminder_status: string | null;
|
|
status: string;
|
|
confirmed_date: string | null;
|
|
confirmed_group: string | null;
|
|
confirmed_comment: string | null;
|
|
notes: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export type BookingStatus = "new" | "contacted" | "confirmed" | "declined";
|
|
|
|
export interface GroupBooking {
|
|
id: number;
|
|
name: string;
|
|
phone: string;
|
|
groupInfo?: string;
|
|
instagram?: string;
|
|
telegram?: string;
|
|
notifiedConfirm: boolean;
|
|
notifiedReminder: boolean;
|
|
reminderStatus?: string;
|
|
status: BookingStatus;
|
|
confirmedDate?: string;
|
|
confirmedGroup?: string;
|
|
confirmedComment?: string;
|
|
notes?: string;
|
|
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,
|
|
reminderStatus: r.reminder_status ?? undefined,
|
|
status: (r.status || "new") as BookingStatus,
|
|
confirmedDate: r.confirmed_date ?? undefined,
|
|
confirmedGroup: r.confirmed_group ?? undefined,
|
|
confirmedComment: r.confirmed_comment ?? undefined,
|
|
notes: r.notes ?? undefined,
|
|
createdAt: r.created_at,
|
|
}));
|
|
}
|
|
|
|
export function setGroupBookingStatus(
|
|
id: number,
|
|
status: BookingStatus,
|
|
confirmation?: { date: string; group: string; comment?: string }
|
|
): void {
|
|
const db = getDb();
|
|
if (status === "confirmed" && confirmation) {
|
|
db.prepare(
|
|
"UPDATE group_bookings SET status = ?, confirmed_date = ?, confirmed_group = ?, confirmed_comment = ?, notified_confirm = 1 WHERE id = ?"
|
|
).run(status, confirmation.date, confirmation.group, confirmation.comment || null, id);
|
|
} else {
|
|
db.prepare(
|
|
"UPDATE group_bookings SET status = ?, confirmed_date = NULL, confirmed_group = NULL, confirmed_comment = NULL, notified_confirm = 1 WHERE id = ?"
|
|
).run(status, id);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// --- Reminder status ---
|
|
|
|
export type ReminderStatus = "pending" | "coming" | "cancelled";
|
|
|
|
export function setReminderStatus(
|
|
table: "mc_registrations" | "group_bookings" | "open_day_bookings",
|
|
id: number,
|
|
status: ReminderStatus | null
|
|
): void {
|
|
const db = getDb();
|
|
db.prepare(`UPDATE ${table} SET reminder_status = ? WHERE id = ?`).run(status, id);
|
|
}
|
|
|
|
export function updateBookingNotes(
|
|
table: "mc_registrations" | "group_bookings" | "open_day_bookings",
|
|
id: number,
|
|
notes: string
|
|
): void {
|
|
const db = getDb();
|
|
db.prepare(`UPDATE ${table} SET notes = ? WHERE id = ?`).run(notes || null, id);
|
|
}
|
|
|
|
export interface ReminderItem {
|
|
id: number;
|
|
type: "class" | "master-class" | "open-day";
|
|
table: "mc_registrations" | "group_bookings" | "open_day_bookings";
|
|
name: string;
|
|
phone?: string;
|
|
instagram?: string;
|
|
telegram?: string;
|
|
reminderStatus?: string;
|
|
eventLabel: string;
|
|
eventDate: string;
|
|
}
|
|
|
|
export function getUpcomingReminders(): ReminderItem[] {
|
|
const db = getDb();
|
|
const items: ReminderItem[] = [];
|
|
|
|
// Tomorrow and today dates
|
|
const now = new Date();
|
|
const today = now.toISOString().split("T")[0];
|
|
const tomorrow = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString().split("T")[0];
|
|
|
|
// MC registrations — match slots from sections JSON
|
|
try {
|
|
const mcSection = db.prepare("SELECT data FROM sections WHERE key = 'masterClasses'").get() as { data: string } | undefined;
|
|
if (mcSection) {
|
|
const mcData = JSON.parse(mcSection.data) as { items: { title: string; slots: { date: string; startTime?: string }[] }[] };
|
|
// Find MC titles with slots today or tomorrow
|
|
const upcomingTitles: { title: string; date: string; time?: string }[] = [];
|
|
for (const mc of mcData.items || []) {
|
|
for (const slot of mc.slots || []) {
|
|
if (slot.date === today || slot.date === tomorrow) {
|
|
upcomingTitles.push({ title: mc.title, date: slot.date, time: slot.startTime });
|
|
}
|
|
}
|
|
}
|
|
if (upcomingTitles.length > 0) {
|
|
const uniqueTitles = [...new Set(upcomingTitles.map((t) => t.title))];
|
|
const placeholders = uniqueTitles.map(() => "?").join(", ");
|
|
const rows = db.prepare(
|
|
`SELECT * FROM mc_registrations WHERE master_class_title IN (${placeholders})`
|
|
).all(...uniqueTitles) as McRegistrationRow[];
|
|
|
|
// Build a lookup: title → { date, time }
|
|
const titleInfo = new Map<string, { date: string; time?: string }>();
|
|
for (const t of upcomingTitles) {
|
|
titleInfo.set(t.title, { date: t.date, time: t.time });
|
|
}
|
|
|
|
for (const r of rows) {
|
|
const info = titleInfo.get(r.master_class_title);
|
|
if (!info) continue;
|
|
items.push({
|
|
id: r.id,
|
|
type: "master-class",
|
|
table: "mc_registrations",
|
|
name: r.name,
|
|
phone: r.phone ?? undefined,
|
|
instagram: r.instagram ?? undefined,
|
|
telegram: r.telegram ?? undefined,
|
|
reminderStatus: r.reminder_status ?? undefined,
|
|
eventLabel: `${r.master_class_title}${info.time ? ` · ${info.time}` : ""}`,
|
|
eventDate: info.date,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
// Group bookings — confirmed with date today/tomorrow
|
|
try {
|
|
const gbRows = db.prepare(
|
|
"SELECT * FROM group_bookings WHERE status IN ('confirmed', 'contacted') AND confirmed_date IN (?, ?)"
|
|
).all(today, tomorrow) as GroupBookingRow[];
|
|
for (const r of gbRows) {
|
|
items.push({
|
|
id: r.id,
|
|
type: "class",
|
|
table: "group_bookings",
|
|
name: r.name,
|
|
phone: r.phone ?? undefined,
|
|
instagram: r.instagram ?? undefined,
|
|
telegram: r.telegram ?? undefined,
|
|
reminderStatus: r.reminder_status ?? undefined,
|
|
eventLabel: r.confirmed_group || "Занятие",
|
|
eventDate: r.confirmed_date!,
|
|
});
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
// Open Day bookings — check event date
|
|
try {
|
|
const events = db.prepare(
|
|
"SELECT id, date, title FROM open_day_events WHERE date IN (?, ?) AND active = 1"
|
|
).all(today, tomorrow) as { id: number; date: string; title: string }[];
|
|
for (const ev of events) {
|
|
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 = ? AND c.cancelled = 0`
|
|
).all(ev.id) as OpenDayBookingRow[];
|
|
for (const r of rows) {
|
|
items.push({
|
|
id: r.id,
|
|
type: "open-day",
|
|
table: "open_day_bookings",
|
|
name: r.name,
|
|
phone: r.phone ?? undefined,
|
|
instagram: r.instagram ?? undefined,
|
|
telegram: r.telegram ?? undefined,
|
|
reminderStatus: r.reminder_status ?? undefined,
|
|
eventLabel: `${r.class_style} · ${r.class_trainer} · ${r.class_time} (${r.class_hall})`,
|
|
eventDate: ev.date,
|
|
});
|
|
}
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
return items;
|
|
}
|
|
|
|
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;
|
|
reminder_status: string | null;
|
|
status: string;
|
|
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;
|
|
reminderStatus?: string;
|
|
status: string;
|
|
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,
|
|
};
|
|
}
|
|
|
|
export function setOpenDayBookingStatus(id: number, status: string): void {
|
|
const db = getDb();
|
|
db.prepare("UPDATE open_day_bookings SET status = ?, notified_confirm = 1 WHERE id = ?").run(status, id);
|
|
}
|
|
|
|
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,
|
|
reminderStatus: r.reminder_status ?? undefined,
|
|
status: r.status || "new",
|
|
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 };
|