feat: add groupId support and redesign schedule GroupView hierarchy

- Add groupId field to ScheduleClass for admin-defined group identity
- Add versioned DB migration system (replaces initTables) to prevent data loss
- Redesign GroupView: Trainer → Class Type → Group → Datetimes hierarchy
- Group datetimes by day, merge days with identical time sets
- Auto-assign groupIds to legacy schedule entries in admin
- Add mc_registrations CRUD to db.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 19:55:34 +03:00
parent 84b0bc4d60
commit f5e80c792a
6 changed files with 358 additions and 139 deletions

View File

@@ -13,50 +13,104 @@ function getDb(): Database.Database {
_db = new Database(DB_PATH);
_db.pragma("journal_mode = WAL");
_db.pragma("foreign_keys = ON");
initTables(_db);
runMigrations(_db);
}
return _db;
}
function initTables(db: Database.Database) {
// --- 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'))
);
`);
},
},
];
function runMigrations(db: Database.Database) {
// Create migrations tracking table
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 mc_registrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
master_class_title TEXT NOT NULL,
CREATE TABLE IF NOT EXISTS _migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
instagram TEXT NOT NULL,
telegram TEXT,
created_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,
experience TEXT,
victories TEXT,
education TEXT,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
applied_at TEXT DEFAULT (datetime('now'))
);
`);
// Migrate: add bio columns if missing
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");
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 ---