const initSqlJs = require('sql.js'); const fs = require('fs'); const path = require('path'); const bcrypt = require('bcryptjs'); const DB_PATH = path.join(__dirname, 'data', 'worktime.db'); const dataDir = path.join(__dirname, 'data'); if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } let db = null; // Save database to disk function saveDB() { if (db) { const data = db.export(); const buffer = Buffer.from(data); fs.writeFileSync(DB_PATH, buffer); } } // Auto-save every 30 seconds setInterval(saveDB, 30000); // Wrapper to provide a simpler API class DBWrapper { constructor(sqlDb) { this.db = sqlDb; } run(sql, params = []) { this.db.run(sql, params); saveDB(); const result = this.db.exec("SELECT last_insert_rowid() as id"); return { lastInsertRowid: result.length > 0 ? result[0].values[0][0] : 0 }; } get(sql, params = []) { const stmt = this.db.prepare(sql); if (params.length > 0) stmt.bind(params); if (stmt.step()) { const cols = stmt.getColumnNames(); const vals = stmt.get(); stmt.free(); const row = {}; cols.forEach((c, i) => { row[c] = vals[i]; }); return row; } stmt.free(); return null; } all(sql, params = []) { const stmt = this.db.prepare(sql); if (params.length > 0) stmt.bind(params); const rows = []; while (stmt.step()) { const cols = stmt.getColumnNames(); const vals = stmt.get(); const row = {}; cols.forEach((c, i) => { row[c] = vals[i]; }); rows.push(row); } stmt.free(); return rows; } exec(sql) { this.db.run(sql); saveDB(); } } async function initDB() { const SQL = await initSqlJs(); if (fs.existsSync(DB_PATH)) { const fileBuffer = fs.readFileSync(DB_PATH); db = new SQL.Database(fileBuffer); } else { db = new SQL.Database(); } db.run("PRAGMA foreign_keys = ON"); // Create tables db.run(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, realname TEXT NOT NULL, role TEXT NOT NULL CHECK(role IN ('admin', 'sales', 'rd')), status INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) ) `); db.run(` CREATE TABLE IF NOT EXISTS clients ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT DEFAULT '', sales_id INTEGER NOT NULL, status INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (sales_id) REFERENCES users(id) ) `); db.run(` CREATE TABLE IF NOT EXISTS timesheets ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, client_id INTEGER NOT NULL, work_date TEXT NOT NULL, hours REAL NOT NULL CHECK(hours > 0 AND hours <= 8), remark TEXT DEFAULT '', service_type TEXT DEFAULT '售前', project_type TEXT DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (client_id) REFERENCES clients(id), UNIQUE(user_id, client_id, work_date, project_type) ) `); // Add columns to existing timesheets table if they don't exist try { db.run("ALTER TABLE timesheets ADD COLUMN service_type TEXT DEFAULT '售前'"); } catch (e) { } try { db.run("ALTER TABLE timesheets ADD COLUMN project_type TEXT DEFAULT ''"); } catch (e) { } // Add service_types to clients if not exist try { db.run("ALTER TABLE clients ADD COLUMN service_types TEXT DEFAULT '[]'"); } catch (e) { } // Migration: update UNIQUE constraint from (user_id, client_id, work_date) to include project_type try { const res = db.exec("SELECT sql FROM sqlite_master WHERE type='table' AND name='timesheets'"); if (res.length > 0) { const sqlString = res[0].values[0][0]; if (sqlString.includes('UNIQUE(user_id, client_id, work_date)') && !sqlString.includes('UNIQUE(user_id, client_id, work_date, project_type)')) { db.run("PRAGMA foreign_keys=off"); db.run(` CREATE TABLE IF NOT EXISTS timesheets_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, client_id INTEGER NOT NULL, work_date TEXT NOT NULL, hours REAL NOT NULL CHECK(hours > 0 AND hours <= 8), remark TEXT DEFAULT '', service_type TEXT DEFAULT '售前', project_type TEXT DEFAULT '', created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (client_id) REFERENCES clients(id), UNIQUE(user_id, client_id, work_date, project_type) ) `); db.run("INSERT INTO timesheets_new SELECT id, user_id, client_id, work_date, hours, remark, service_type, project_type, created_at FROM timesheets"); db.run("DROP TABLE timesheets"); db.run("ALTER TABLE timesheets_new RENAME TO timesheets"); db.run("PRAGMA foreign_keys=on"); console.log("✅ Migrated timesheets table to updated UNIQUE constraint"); } } } catch (e) { console.error("Migration error:", e.message); } db.run(` CREATE TABLE IF NOT EXISTS workdays ( id INTEGER PRIMARY KEY AUTOINCREMENT, year INTEGER NOT NULL, month INTEGER NOT NULL CHECK(month >= 1 AND month <= 12), days INTEGER NOT NULL CHECK(days >= 0 AND days <= 31), UNIQUE(year, month) ) `); db.run(` CREATE TABLE IF NOT EXISTS project_types ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) ) `); // Create indexes (ignore if exists) try { db.run("CREATE INDEX idx_timesheets_user ON timesheets(user_id)"); } catch (e) { } try { db.run("CREATE INDEX idx_timesheets_client ON timesheets(client_id)"); } catch (e) { } try { db.run("CREATE INDEX idx_timesheets_date ON timesheets(work_date)"); } catch (e) { } try { db.run("CREATE INDEX idx_clients_sales ON clients(sales_id)"); } catch (e) { } const wrapper = new DBWrapper(db); // Seed admin const adminExists = wrapper.get('SELECT id FROM users WHERE username = ?', ['admin']); if (!adminExists) { const hash = bcrypt.hashSync('admin123', 10); wrapper.run( 'INSERT INTO users (username, password, realname, role, status) VALUES (?, ?, ?, ?, ?)', ['admin', hash, '系统管理员', 'admin', 1] ); console.log('✅ Default admin account created (admin / admin123)'); } // Seed workdays for current year const currentYear = new Date().getFullYear(); const existingWorkdays = wrapper.get('SELECT id FROM workdays WHERE year = ?', [currentYear]); if (!existingWorkdays) { const defaultDays = [22, 20, 23, 22, 21, 21, 23, 22, 22, 22, 21, 23]; for (let i = 0; i < 12; i++) { try { wrapper.run('INSERT INTO workdays (year, month, days) VALUES (?, ?, ?)', [currentYear, i + 1, defaultDays[i]]); } catch (e) { } } console.log(`✅ Default workdays for ${currentYear} initialized`); } saveDB(); return wrapper; } module.exports = { initDB };