Implement application updates and refinements

This commit is contained in:
2026-07-20 09:44:06 +08:00 Unverified
parent 4fa001cece
commit d4e085b6c5
9 changed files with 1704 additions and 71 deletions
+53 -15
View File
@@ -219,6 +219,7 @@ const sqliteSchema = `
building TEXT NOT NULL,
floor TEXT,
capacity INTEGER NOT NULL CHECK (capacity > 0),
seat_plan TEXT,
seat_start INTEGER NOT NULL DEFAULT 1 CHECK (seat_start > 0),
seat_end INTEGER NOT NULL CHECK (seat_end >= seat_start),
room_type TEXT NOT NULL CHECK (room_type IN ('standard', 'computer', 'accessible', 'spare')),
@@ -259,6 +260,7 @@ const sqliteSchema = `
building TEXT NOT NULL,
floor TEXT,
capacity INTEGER NOT NULL CHECK (capacity > 0),
seat_plan TEXT,
seat_start INTEGER NOT NULL DEFAULT 1,
seat_end INTEGER NOT NULL,
room_type TEXT NOT NULL CHECK (room_type IN ('standard', 'computer', 'accessible', 'spare')),
@@ -597,6 +599,7 @@ const mysqlSchema = [
building VARCHAR(120) NOT NULL,
floor VARCHAR(40) NULL,
capacity INT UNSIGNED NOT NULL,
seat_plan VARCHAR(500) NULL,
seat_start INT UNSIGNED NOT NULL DEFAULT 1,
seat_end INT UNSIGNED NOT NULL,
room_type ENUM('standard', 'computer', 'accessible', 'spare') NOT NULL,
@@ -643,6 +646,7 @@ const mysqlSchema = [
building VARCHAR(120) NOT NULL,
floor VARCHAR(40) NULL,
capacity INT UNSIGNED NOT NULL,
seat_plan VARCHAR(500) NULL,
seat_start INT UNSIGNED NOT NULL DEFAULT 1,
seat_end INT UNSIGNED NOT NULL,
room_type ENUM('standard', 'computer', 'accessible', 'spare') NOT NULL,
@@ -788,7 +792,7 @@ function buildSeedOperations(state) {
const nullable = value => value == null || value === '' ? null : value;
add(
'UPDATE schema_metadata SET schema_version = 5, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
'UPDATE schema_metadata SET schema_version = 6, app_version = ?, self_registration_enabled = ?, created_at = ? WHERE id = 1',
Number(state.meta?.version || 1), state.settings?.selfRegistrationEnabled ? 1 : 0,
state.meta?.createdAt || new Date().toISOString()
);
@@ -918,10 +922,10 @@ function buildSeedOperations(state) {
for (const room of state.testRooms) {
add(
`INSERT INTO test_rooms (
id, center_id, code, name, building, floor, capacity, seat_start, seat_end, room_type, status, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, center_id, code, name, building, floor, capacity, seat_plan, seat_start, seat_end, room_type, status, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
room.id, room.centerId, room.code, room.name, room.building, nullable(room.floor), Number(room.capacity),
Number(room.seatStart || 1), Number(room.seatEnd), room.roomType, room.status || 'active', nullable(room.notes)
nullable(room.seatPlan), Number(room.seatStart || 1), Number(room.seatEnd || room.capacity), room.roomType, room.status || 'active', nullable(room.notes)
);
}
@@ -943,10 +947,10 @@ function buildSeedOperations(state) {
for (const room of state.centerChangeRooms) {
add(
`INSERT INTO center_change_rooms (
id, request_id, room_id, code, name, building, floor, capacity, seat_start, seat_end, room_type, status, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, request_id, room_id, code, name, building, floor, capacity, seat_plan, seat_start, seat_end, room_type, status, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
room.id, room.requestId, nullable(room.roomId), room.code, room.name, room.building, nullable(room.floor),
Number(room.capacity), Number(room.seatStart || 1), Number(room.seatEnd), room.roomType,
Number(room.capacity), nullable(room.seatPlan), Number(room.seatStart || 1), Number(room.seatEnd || room.capacity), room.roomType,
room.status || 'active', nullable(room.notes)
);
}
@@ -1225,6 +1229,7 @@ function stateFromRows(rows) {
building: row.building,
floor: row.floor || '',
capacity: Number(row.capacity),
seatPlan: row.seat_plan || '',
seatStart: Number(row.seat_start),
seatEnd: Number(row.seat_end),
roomType: row.room_type,
@@ -1262,6 +1267,7 @@ function stateFromRows(rows) {
building: row.building,
floor: row.floor || '',
capacity: Number(row.capacity),
seatPlan: row.seat_plan || '',
seatStart: Number(row.seat_start),
seatEnd: Number(row.seat_end),
roomType: row.room_type,
@@ -1717,6 +1723,27 @@ function createRepository({ client, location, read, transaction, close }) {
auditOperation(log)
]);
},
async saveSchoolClass(schoolClass, isNew, log) {
const change = isNew
? operation(
'INSERT INTO school_classes (id, school_id, name, grade, active) VALUES (?, ?, ?, ?, ?)',
schoolClass.id, schoolClass.schoolId, schoolClass.name, schoolClass.grade, schoolClass.active ? 1 : 0
)
: operation(
'UPDATE school_classes SET name = ?, grade = ?, active = ? WHERE id = ?',
schoolClass.name, schoolClass.grade, schoolClass.active ? 1 : 0, schoolClass.id
);
await transaction([change, auditOperation(log)]);
},
async updateAdmin(user, passwordChanged, log) {
const sql = passwordChanged
? 'UPDATE users SET display_name = ?, class_id = ?, active = ?, password_hash = ? WHERE id = ?'
: 'UPDATE users SET display_name = ?, class_id = ?, active = ? WHERE id = ?';
const params = passwordChanged
? [user.displayName, optional(user.classId), user.active ? 1 : 0, user.passwordHash, user.id]
: [user.displayName, optional(user.classId), user.active ? 1 : 0, user.id];
await transaction([operation(sql, ...params), auditOperation(log)]);
},
async saveTestCenter(center, isNew, log) {
const centerOperation = isNew
? operation(
@@ -1750,10 +1777,10 @@ function createRepository({ client, location, read, transaction, close }) {
];
for (const room of rooms) operations.push(operation(
`INSERT INTO center_change_rooms (
id, request_id, room_id, code, name, building, floor, capacity, seat_start, seat_end, room_type, status, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, request_id, room_id, code, name, building, floor, capacity, seat_plan, seat_start, seat_end, room_type, status, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
room.id, request.id, optional(room.roomId), room.code, room.name, room.building, optional(room.floor),
Number(room.capacity), Number(room.seatStart), Number(room.seatEnd), room.roomType, room.status,
Number(room.capacity), optional(room.seatPlan), 1, Number(room.capacity), room.roomType, room.status,
optional(room.notes)
));
operations.push(auditOperation(log));
@@ -1798,10 +1825,10 @@ function createRepository({ client, location, read, transaction, close }) {
operations.push(operation('DELETE FROM test_rooms WHERE center_id = ?', center.id));
for (const room of rooms) operations.push(operation(
`INSERT INTO test_rooms (
id, center_id, code, name, building, floor, capacity, seat_start, seat_end, room_type, status, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, center_id, code, name, building, floor, capacity, seat_plan, seat_start, seat_end, room_type, status, notes
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
room.id, center.id, room.code, room.name, room.building, optional(room.floor), Number(room.capacity),
Number(room.seatStart), Number(room.seatEnd), room.roomType, room.status, optional(room.notes)
optional(room.seatPlan), 1, Number(room.capacity), room.roomType, room.status, optional(room.notes)
));
}
operations.push(auditOperation(log));
@@ -1933,6 +1960,8 @@ async function createSqliteStore({ path, seed }) {
['code', 'TEXT'], ['manager_name', 'TEXT'], ['manager_phone', 'TEXT'], ['emergency_phone', 'TEXT'],
['gate_open_time', 'TEXT'], ['transport', 'TEXT'], ['status', "TEXT NOT NULL DEFAULT 'active'"], ['notes', 'TEXT']
]);
ensureColumns('test_rooms', [['seat_plan', 'TEXT']]);
ensureColumns('center_change_rooms', [['seat_plan', 'TEXT']]);
if (tableExists('workflow_definitions')) {
const definitionSql = connection.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'workflow_definitions'").get()?.sql || '';
if (!definitionSql.includes('candidate_account_batch')) {
@@ -2134,13 +2163,17 @@ async function createSqliteStore({ path, seed }) {
}
}
if (existingSystem && Number(existingSystem.app_version || 1) < 6) {
connection.prepare('UPDATE schema_metadata SET schema_version = 6, app_version = 6 WHERE id = 1').run();
}
if (!connection.prepare('SELECT id FROM schema_metadata WHERE id = 1').get()) {
const initialState = seed();
connection.exec('BEGIN IMMEDIATE');
try {
connection.prepare(`
INSERT INTO schema_metadata (id, schema_version, app_version, self_registration_enabled, created_at)
VALUES (1, 5, ?, ?, ?)
VALUES (1, 6, ?, ?, ?)
`).run(Number(initialState.meta?.version || 1), initialState.settings?.selfRegistrationEnabled ? 1 : 0, initialState.meta?.createdAt || new Date().toISOString());
for (const item of buildSeedOperations(initialState)) connection.prepare(item.sql).run(...item.params);
connection.exec('COMMIT');
@@ -2222,6 +2255,8 @@ async function createMysqlStore({ seed }) {
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS transport VARCHAR(500) NULL',
"ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS status ENUM('active', 'inactive') NOT NULL DEFAULT 'active'",
'ALTER TABLE test_centers ADD COLUMN IF NOT EXISTS notes VARCHAR(1000) NULL',
'ALTER TABLE test_rooms ADD COLUMN IF NOT EXISTS seat_plan VARCHAR(500) NULL',
'ALTER TABLE center_change_rooms ADD COLUMN IF NOT EXISTS seat_plan VARCHAR(500) NULL',
"ALTER TABLE workflow_definitions MODIFY COLUMN business_type ENUM('profile_change', 'registration_review', 'center_change', 'candidate_account_batch') NOT NULL"
];
for (const statement of mysqlColumnMigrations) await pool.execute(statement);
@@ -2395,6 +2430,9 @@ async function createMysqlStore({ seed }) {
connection.release();
}
}
if (Number(metadataRows[0]?.app_version || 1) < 6) {
await pool.execute('UPDATE schema_metadata SET schema_version = 6, app_version = 6 WHERE id = 1');
}
}
if (!existing.length) {
const initialState = seed();
@@ -2403,7 +2441,7 @@ async function createMysqlStore({ seed }) {
await connection.beginTransaction();
const [insert] = await connection.execute(`
INSERT IGNORE INTO schema_metadata (id, schema_version, app_version, self_registration_enabled, created_at)
VALUES (1, 5, ?, ?, ?)
VALUES (1, 6, ?, ?, ?)
`, [Number(initialState.meta?.version || 1), initialState.settings?.selfRegistrationEnabled ? 1 : 0, initialState.meta?.createdAt || new Date().toISOString()]);
if (insert.affectedRows === 1) {
for (const item of buildSeedOperations(initialState)) await connection.execute(item.sql, item.params);