224 lines
7.4 KiB
JavaScript
224 lines
7.4 KiB
JavaScript
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 };
|