feat: booking panel upgrade — refactor, notes, search, manual add, polling

Phase 1 — Refactor:
- Split monolith _shared.tsx into types.ts, BookingComponents, InlineNotes,
  GenericBookingsList, AddBookingModal, SearchBar (no more _ prefix)
- All 3 tabs use GenericBookingsList — shared status workflow, filters, archive

Phase 2 — Features:
- DB migration 13: add notes column to all booking tables
- Inline notes with amber highlight, auto-save 800ms debounce
- Confirm modal comment saves to notes field
- Manual add: 2 tabs (Занятие / Мероприятие), filters expired MCs, Open Day support
- Search bar: cross-table search by name/phone
- 10s polling for real-time updates (bookings page + sidebar badge)
- Status change marks booking as seen (fixes unread count on reset)
- Confirm modal stores human-readable group label instead of raw groupId
- Confirmed group bookings appear in Reminders tab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 13:34:16 +03:00
parent 87f488e2c1
commit c87c63bc4f
18 changed files with 1055 additions and 664 deletions

View File

@@ -8,7 +8,7 @@ const DB_PATH =
let _db: Database.Database | null = null;
function getDb(): Database.Database {
export function getDb(): Database.Database {
if (!_db) {
_db = new Database(DB_PATH);
_db.pragma("journal_mode = WAL");
@@ -242,6 +242,18 @@ const migrations: Migration[] = [
}
},
},
{
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) {
@@ -524,6 +536,7 @@ interface McRegistrationRow {
notified_reminder: number;
reminder_status: string | null;
status: string;
notes: string | null;
}
export interface McRegistration {
@@ -538,6 +551,7 @@ export interface McRegistration {
notifiedReminder: boolean;
reminderStatus?: string;
status: string;
notes?: string;
}
export function addMcRegistration(
@@ -588,12 +602,13 @@ function mapMcRow(r: McRegistrationRow): McRegistration {
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 = ? WHERE id = ?").run(status, id);
db.prepare("UPDATE mc_registrations SET status = ?, notified_confirm = 1 WHERE id = ?").run(status, id);
}
export function updateMcRegistration(
@@ -640,6 +655,7 @@ interface GroupBookingRow {
confirmed_date: string | null;
confirmed_group: string | null;
confirmed_comment: string | null;
notes: string | null;
created_at: string;
}
@@ -659,6 +675,7 @@ export interface GroupBooking {
confirmedDate?: string;
confirmedGroup?: string;
confirmedComment?: string;
notes?: string;
createdAt: string;
}
@@ -697,6 +714,7 @@ export function getGroupBookings(): GroupBooking[] {
confirmedDate: r.confirmed_date ?? undefined,
confirmedGroup: r.confirmed_group ?? undefined,
confirmedComment: r.confirmed_comment ?? undefined,
notes: r.notes ?? undefined,
createdAt: r.created_at,
}));
}
@@ -709,11 +727,11 @@ export function setGroupBookingStatus(
const db = getDb();
if (status === "confirmed" && confirmation) {
db.prepare(
"UPDATE group_bookings SET status = ?, confirmed_date = ?, confirmed_group = ?, confirmed_comment = ? WHERE id = ?"
"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 WHERE id = ?"
"UPDATE group_bookings SET status = ?, confirmed_date = NULL, confirmed_group = NULL, confirmed_comment = NULL, notified_confirm = 1 WHERE id = ?"
).run(status, id);
}
}
@@ -769,6 +787,15 @@ export function setReminderStatus(
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";
@@ -838,6 +865,27 @@ export function getUpcomingReminders(): ReminderItem[] {
}
} catch { /* ignore */ }
// Group bookings — confirmed with date today/tomorrow
try {
const gbRows = db.prepare(
"SELECT * FROM group_bookings WHERE status = 'confirmed' 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(
@@ -1007,7 +1055,7 @@ function mapClassRow(r: OpenDayClassRow): OpenDayClass {
export function setOpenDayBookingStatus(id: number, status: string): void {
const db = getDb();
db.prepare("UPDATE open_day_bookings SET status = ? WHERE id = ?").run(status, id);
db.prepare("UPDATE open_day_bookings SET status = ?, notified_confirm = 1 WHERE id = ?").run(status, id);
}
function mapBookingRow(r: OpenDayBookingRow): OpenDayBooking {