feat: add reminders tab with status tracking (coming/pending/cancelled)

Auto-surfaces bookings for today and tomorrow. Admin sets status per
person: coming, no answer, or cancelled. Summary stats per day.
DB migration 8 adds reminder_status column to all booking tables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 13:07:00 +03:00
parent b94ee69033
commit 4e766d6957
3 changed files with 354 additions and 3 deletions

View File

@@ -171,6 +171,19 @@ const migrations: Migration[] = [
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`);
}
}
},
},
];
function runMigrations(db: Database.Database) {
@@ -446,6 +459,7 @@ interface McRegistrationRow {
created_at: string;
notified_confirm: number;
notified_reminder: number;
reminder_status: string | null;
}
export interface McRegistration {
@@ -458,6 +472,7 @@ export interface McRegistration {
createdAt: string;
notifiedConfirm: boolean;
notifiedReminder: boolean;
reminderStatus?: string;
}
export function addMcRegistration(
@@ -506,6 +521,7 @@ function mapMcRow(r: McRegistrationRow): McRegistration {
createdAt: r.created_at,
notifiedConfirm: !!r.notified_confirm,
notifiedReminder: !!r.notified_reminder,
reminderStatus: r.reminder_status ?? undefined,
};
}
@@ -548,6 +564,7 @@ interface GroupBookingRow {
telegram: string | null;
notified_confirm: number;
notified_reminder: number;
reminder_status: string | null;
created_at: string;
}
@@ -560,6 +577,7 @@ export interface GroupBooking {
telegram?: string;
notifiedConfirm: boolean;
notifiedReminder: boolean;
reminderStatus?: string;
createdAt: string;
}
@@ -593,6 +611,7 @@ export function getGroupBookings(): GroupBooking[] {
telegram: r.telegram ?? undefined,
notifiedConfirm: !!r.notified_confirm,
notifiedReminder: !!r.notified_reminder,
reminderStatus: r.reminder_status ?? undefined,
createdAt: r.created_at,
}));
}
@@ -635,6 +654,109 @@ export interface UnreadCounts {
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 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 });
}
}
}
for (const { title, date, time } of upcomingTitles) {
const rows = db.prepare(
"SELECT * FROM mc_registrations WHERE master_class_title = ?"
).all(title) as McRegistrationRow[];
for (const r of rows) {
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: `${title}${time ? ` · ${time}` : ""}`,
eventDate: 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;
@@ -713,6 +835,7 @@ interface OpenDayBookingRow {
telegram: string | null;
notified_confirm: number;
notified_reminder: number;
reminder_status: string | null;
created_at: string;
class_style?: string;
class_trainer?: string;
@@ -730,6 +853,7 @@ export interface OpenDayBooking {
telegram?: string;
notifiedConfirm: boolean;
notifiedReminder: boolean;
reminderStatus?: string;
createdAt: string;
classStyle?: string;
classTrainer?: string;
@@ -777,6 +901,7 @@ function mapBookingRow(r: OpenDayBookingRow): OpenDayBooking {
telegram: r.telegram ?? undefined,
notifiedConfirm: !!r.notified_confirm,
notifiedReminder: !!r.notified_reminder,
reminderStatus: r.reminder_status ?? undefined,
createdAt: r.created_at,
classStyle: r.class_style ?? undefined,
classTrainer: r.class_trainer ?? undefined,