970 lines
32 KiB
JavaScript
970 lines
32 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const { DatabaseSync } = require('node:sqlite');
|
|
const bcrypt = require('bcryptjs');
|
|
const { expandRuleSet } = require('./rule-catalog');
|
|
|
|
const DATA_DIR = process.env.MIXEDASSETS_DATA_DIR || path.join(process.cwd(), 'data');
|
|
const DB_PATH = process.env.MIXEDASSETS_DB_PATH || path.join(DATA_DIR, 'mixedassets.sqlite');
|
|
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
|
|
const db = new DatabaseSync(DB_PATH);
|
|
db.exec('PRAGMA foreign_keys = ON;');
|
|
db.exec('PRAGMA journal_mode = WAL;');
|
|
|
|
function now() {
|
|
return new Date().toISOString();
|
|
}
|
|
|
|
function json(value, fallback = null) {
|
|
if (value === undefined) return fallback === null ? null : JSON.stringify(fallback);
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function parseJson(value, fallback = null) {
|
|
if (value === null || value === undefined || value === '') return fallback;
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function bind(statement, params) {
|
|
if (Array.isArray(params)) return statement;
|
|
return statement;
|
|
}
|
|
|
|
function args(params) {
|
|
if (Array.isArray(params)) return params;
|
|
if (params && Object.keys(params).length > 0) return [params];
|
|
return [];
|
|
}
|
|
|
|
function one(sql, params = {}) {
|
|
const statement = bind(db.prepare(sql), params);
|
|
return statement.get(...args(params));
|
|
}
|
|
|
|
function all(sql, params = {}) {
|
|
const statement = bind(db.prepare(sql), params);
|
|
return statement.all(...args(params));
|
|
}
|
|
|
|
function run(sql, params = {}) {
|
|
const statement = bind(db.prepare(sql), params);
|
|
return statement.run(...args(params));
|
|
}
|
|
|
|
function tx(fn) {
|
|
db.exec('BEGIN IMMEDIATE');
|
|
try {
|
|
const result = fn();
|
|
db.exec('COMMIT');
|
|
return result;
|
|
} catch (error) {
|
|
db.exec('ROLLBACK');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function initialize() {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS teams (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
team_id INTEGER REFERENCES teams(id),
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'viewer',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS roles (
|
|
key TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
capabilities_json TEXT NOT NULL DEFAULT '[]',
|
|
is_system INTEGER NOT NULL DEFAULT 0,
|
|
locked INTEGER NOT NULL DEFAULT 0,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_preferences (
|
|
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
preferences_json TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS entities (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
legal_name TEXT,
|
|
tax_id TEXT,
|
|
fiscal_year_end_month INTEGER NOT NULL DEFAULT 12,
|
|
base_currency TEXT NOT NULL DEFAULT 'USD',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS reference_values (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
type TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
code TEXT,
|
|
metadata TEXT,
|
|
UNIQUE(type, name)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS employees (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
workday_worker_id TEXT UNIQUE,
|
|
employee_number TEXT,
|
|
name TEXT NOT NULL,
|
|
email TEXT,
|
|
department TEXT,
|
|
location TEXT,
|
|
manager_name TEXT,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
source TEXT NOT NULL DEFAULT 'manual',
|
|
source_payload TEXT,
|
|
last_synced_at TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS assets (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id TEXT NOT NULL UNIQUE,
|
|
entity_id INTEGER REFERENCES entities(id),
|
|
template_id INTEGER,
|
|
description TEXT NOT NULL,
|
|
category TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'in_service',
|
|
acquisition_cost REAL NOT NULL DEFAULT 0,
|
|
acquired_date TEXT,
|
|
in_service_date TEXT,
|
|
useful_life_months INTEGER NOT NULL DEFAULT 60,
|
|
salvage_value REAL NOT NULL DEFAULT 0,
|
|
land_value REAL NOT NULL DEFAULT 0,
|
|
prior_depreciation REAL NOT NULL DEFAULT 0,
|
|
location TEXT,
|
|
department TEXT,
|
|
group_name TEXT,
|
|
custodian TEXT,
|
|
vendor TEXT,
|
|
serial_number TEXT,
|
|
invoice_number TEXT,
|
|
gl_asset_account TEXT,
|
|
gl_expense_account TEXT,
|
|
gl_accumulated_account TEXT,
|
|
barcode_value TEXT,
|
|
condition TEXT,
|
|
new_or_used TEXT DEFAULT 'new',
|
|
listed_property INTEGER NOT NULL DEFAULT 0,
|
|
business_use_percent REAL NOT NULL DEFAULT 100,
|
|
investment_use_percent REAL NOT NULL DEFAULT 0,
|
|
disposal_date TEXT,
|
|
disposal_price REAL,
|
|
disposal_expense REAL,
|
|
disposal_type TEXT,
|
|
property_type TEXT,
|
|
notes TEXT,
|
|
custom_fields TEXT,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_books (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
book_type TEXT NOT NULL,
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
cost REAL,
|
|
depreciation_method TEXT NOT NULL DEFAULT 'straight_line',
|
|
convention TEXT NOT NULL DEFAULT 'half_year',
|
|
useful_life_months INTEGER,
|
|
section_179_amount REAL NOT NULL DEFAULT 0,
|
|
bonus_percent REAL NOT NULL DEFAULT 0,
|
|
business_use_percent REAL,
|
|
investment_use_percent REAL,
|
|
manual_depreciation TEXT,
|
|
UNIQUE(asset_id, book_type)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_templates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
description TEXT,
|
|
defaults TEXT NOT NULL,
|
|
custom_fields TEXT NOT NULL,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tax_rule_sets (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
jurisdiction TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
effective_date TEXT NOT NULL,
|
|
version TEXT NOT NULL,
|
|
rules_json TEXT NOT NULL,
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
source_note TEXT,
|
|
created_at TEXT NOT NULL,
|
|
UNIQUE(jurisdiction, version)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS leases (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER REFERENCES assets(id) ON DELETE CASCADE,
|
|
lessor TEXT,
|
|
contract_value REAL NOT NULL DEFAULT 0,
|
|
start_date TEXT,
|
|
end_date TEXT,
|
|
discount_rate REAL NOT NULL DEFAULT 0,
|
|
payment_frequency TEXT NOT NULL DEFAULT 'monthly',
|
|
notes TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS warranties (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
provider TEXT,
|
|
phone TEXT,
|
|
start_date TEXT,
|
|
expiration_date TEXT,
|
|
warranty_limit TEXT,
|
|
actual_usage TEXT,
|
|
coverage_details TEXT,
|
|
document_name TEXT,
|
|
document_base64 TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_files (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
file_name TEXT NOT NULL,
|
|
mime_type TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
content_base64 TEXT NOT NULL,
|
|
uploaded_by INTEGER REFERENCES users(id),
|
|
uploaded_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER REFERENCES assets(id) ON DELETE CASCADE,
|
|
title TEXT NOT NULL,
|
|
type TEXT NOT NULL DEFAULT 'maintenance',
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
due_date TEXT,
|
|
assigned_to INTEGER REFERENCES users(id),
|
|
notes TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS checkouts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
checked_out_to TEXT NOT NULL,
|
|
destination TEXT,
|
|
due_date TEXT,
|
|
checked_out_at TEXT NOT NULL,
|
|
checked_in_at TEXT,
|
|
notes TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_assignments (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
employee_id INTEGER REFERENCES employees(id),
|
|
department TEXT,
|
|
location TEXT,
|
|
status TEXT NOT NULL DEFAULT 'assigned',
|
|
assigned_at TEXT NOT NULL,
|
|
released_at TEXT,
|
|
assigned_by INTEGER REFERENCES users(id),
|
|
release_reason TEXT,
|
|
notes TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS workday_connections (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
tenant TEXT,
|
|
base_url TEXT,
|
|
token_url TEXT,
|
|
client_id TEXT,
|
|
client_secret TEXT,
|
|
workers_path TEXT NOT NULL DEFAULT '/workers',
|
|
enabled INTEGER NOT NULL DEFAULT 0,
|
|
sync_enabled INTEGER NOT NULL DEFAULT 0,
|
|
sync_interval_hours INTEGER NOT NULL DEFAULT 24,
|
|
last_sync_at TEXT,
|
|
last_sync_status TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS saved_reports (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
report_type TEXT NOT NULL,
|
|
options_json TEXT NOT NULL,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS app_settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_notes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
body TEXT NOT NULL,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS disposals (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
disposal_date TEXT NOT NULL,
|
|
method TEXT NOT NULL DEFAULT 'sale',
|
|
property_type TEXT,
|
|
partial INTEGER NOT NULL DEFAULT 0,
|
|
disposed_cost REAL NOT NULL DEFAULT 0,
|
|
disposal_price REAL NOT NULL DEFAULT 0,
|
|
disposal_expense REAL NOT NULL DEFAULT 0,
|
|
accumulated_depreciation REAL NOT NULL DEFAULT 0,
|
|
net_book_value REAL NOT NULL DEFAULT 0,
|
|
gain_loss REAL NOT NULL DEFAULT 0,
|
|
book_type TEXT NOT NULL DEFAULT 'GAAP',
|
|
notes TEXT,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_adjustments (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
adjustment_date TEXT NOT NULL,
|
|
type TEXT NOT NULL DEFAULT 'impairment',
|
|
amount REAL NOT NULL DEFAULT 0,
|
|
book_type TEXT NOT NULL DEFAULT 'GAAP',
|
|
reason TEXT,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS books (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
code TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
book_type TEXT NOT NULL DEFAULT 'tax',
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
tax_rule_set_id INTEGER REFERENCES tax_rule_sets(id),
|
|
default_method TEXT,
|
|
default_convention TEXT,
|
|
fiscal_year_end_month INTEGER NOT NULL DEFAULT 12,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS alerts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
type TEXT NOT NULL,
|
|
reference_type TEXT NOT NULL,
|
|
reference_id INTEGER,
|
|
asset_id INTEGER REFERENCES assets(id) ON DELETE CASCADE,
|
|
severity TEXT NOT NULL DEFAULT 'warning',
|
|
title TEXT NOT NULL,
|
|
message TEXT,
|
|
due_date TEXT,
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
notified_at TEXT,
|
|
acknowledged_by INTEGER REFERENCES users(id),
|
|
acknowledged_at TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
UNIQUE(reference_type, reference_id, type)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pm_plans (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
category TEXT,
|
|
frequency_value INTEGER NOT NULL DEFAULT 1,
|
|
frequency_unit TEXT NOT NULL DEFAULT 'months',
|
|
estimated_minutes INTEGER,
|
|
instructions TEXT,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pm_plan_steps (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
plan_id INTEGER NOT NULL REFERENCES pm_plans(id) ON DELETE CASCADE,
|
|
step_order INTEGER NOT NULL DEFAULT 0,
|
|
title TEXT NOT NULL,
|
|
details TEXT,
|
|
est_minutes INTEGER
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pm_plan_components (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
plan_id INTEGER NOT NULL REFERENCES pm_plans(id) ON DELETE CASCADE,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
part_number TEXT,
|
|
description TEXT,
|
|
supplier TEXT,
|
|
cost REAL NOT NULL DEFAULT 0
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_pm_plans (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
plan_id INTEGER REFERENCES pm_plans(id),
|
|
frequency_value INTEGER NOT NULL DEFAULT 1,
|
|
frequency_unit TEXT NOT NULL DEFAULT 'months',
|
|
start_date TEXT,
|
|
end_date TEXT,
|
|
next_due_date TEXT,
|
|
last_completed_date TEXT,
|
|
assigned_to INTEGER REFERENCES users(id),
|
|
active INTEGER NOT NULL DEFAULT 1,
|
|
notes TEXT,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pm_completions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_pm_id INTEGER NOT NULL REFERENCES asset_pm_plans(id) ON DELETE CASCADE,
|
|
completed_at TEXT NOT NULL,
|
|
completed_by INTEGER REFERENCES users(id),
|
|
notes TEXT,
|
|
notes_json TEXT,
|
|
steps_json TEXT,
|
|
cost REAL NOT NULL DEFAULT 0,
|
|
components_json TEXT,
|
|
signature TEXT,
|
|
rating_safety INTEGER,
|
|
rating_physical INTEGER,
|
|
rating_operating INTEGER,
|
|
next_due_date TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pm_completion_photos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
completion_id INTEGER NOT NULL REFERENCES pm_completions(id) ON DELETE CASCADE,
|
|
name TEXT,
|
|
mime_type TEXT,
|
|
data_base64 TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS contacts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
type TEXT NOT NULL DEFAULT 'other',
|
|
first_name TEXT,
|
|
last_name TEXT,
|
|
organization TEXT,
|
|
email TEXT,
|
|
phone TEXT,
|
|
address_line1 TEXT,
|
|
address_line2 TEXT,
|
|
city TEXT,
|
|
state_region TEXT,
|
|
postal_code TEXT,
|
|
country TEXT,
|
|
notes TEXT,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
employee_id TEXT,
|
|
tax_id TEXT,
|
|
department TEXT,
|
|
work_location TEXT,
|
|
manager_first_name TEXT,
|
|
manager_last_name TEXT,
|
|
manager_email TEXT,
|
|
start_date TEXT,
|
|
rating INTEGER,
|
|
credit_term TEXT,
|
|
source TEXT NOT NULL DEFAULT 'manual',
|
|
workday_worker_id TEXT UNIQUE,
|
|
source_payload TEXT,
|
|
last_synced_at TEXT,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS contact_notes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
contact_id INTEGER NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
|
|
body TEXT NOT NULL,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS parts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
part_number TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
description TEXT,
|
|
category TEXT,
|
|
manufacturer TEXT,
|
|
manufacturer_part_number TEXT,
|
|
barcode TEXT,
|
|
unit_of_measure TEXT NOT NULL DEFAULT 'each',
|
|
unit_cost REAL NOT NULL DEFAULT 0,
|
|
reorder_point REAL NOT NULL DEFAULT 0,
|
|
reorder_quantity REAL NOT NULL DEFAULT 0,
|
|
measurements TEXT,
|
|
supplier_contact_id INTEGER REFERENCES contacts(id),
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
notes TEXT,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS part_stock (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
part_id INTEGER NOT NULL REFERENCES parts(id) ON DELETE CASCADE,
|
|
location_contact_id INTEGER REFERENCES contacts(id),
|
|
aisle TEXT,
|
|
bin TEXT,
|
|
quantity REAL NOT NULL DEFAULT 0,
|
|
reserved REAL NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS part_photos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
part_id INTEGER NOT NULL REFERENCES parts(id) ON DELETE CASCADE,
|
|
name TEXT,
|
|
mime_type TEXT,
|
|
data_base64 TEXT NOT NULL,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS part_transactions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
part_id INTEGER NOT NULL REFERENCES parts(id) ON DELETE CASCADE,
|
|
stock_id INTEGER REFERENCES part_stock(id) ON DELETE SET NULL,
|
|
type TEXT NOT NULL DEFAULT 'receive',
|
|
quantity REAL NOT NULL DEFAULT 0,
|
|
unit_cost REAL,
|
|
asset_id INTEGER REFERENCES assets(id) ON DELETE SET NULL,
|
|
reference TEXT,
|
|
notes TEXT,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_photos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
asset_id INTEGER NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
|
name TEXT,
|
|
mime_type TEXT,
|
|
caption TEXT,
|
|
data_base64 TEXT NOT NULL,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS pm_plan_photos (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
plan_id INTEGER NOT NULL REFERENCES pm_plans(id) ON DELETE CASCADE,
|
|
name TEXT,
|
|
mime_type TEXT,
|
|
caption TEXT,
|
|
data_base64 TEXT NOT NULL,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_id_templates (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
pattern TEXT NOT NULL,
|
|
next_number INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS asset_classes (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
class_number TEXT,
|
|
description TEXT NOT NULL,
|
|
gds REAL,
|
|
ads REAL,
|
|
source TEXT NOT NULL DEFAULT 'custom',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS depreciation_zones (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
code TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
allowance_percent REAL NOT NULL DEFAULT 0,
|
|
pis_start TEXT,
|
|
pis_end TEXT,
|
|
real_property_end TEXT,
|
|
max_recovery_years INTEGER,
|
|
leasehold_life_years INTEGER,
|
|
notes TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
actor_id INTEGER REFERENCES users(id),
|
|
action TEXT NOT NULL,
|
|
entity_type TEXT NOT NULL,
|
|
entity_id TEXT,
|
|
before_json TEXT,
|
|
after_json TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
`);
|
|
|
|
migrate();
|
|
seed();
|
|
}
|
|
|
|
// Additive column migrations for databases created before these columns existed.
|
|
function ensureColumn(table, column, definition) {
|
|
const columns = all(`PRAGMA table_info(${table})`);
|
|
if (!columns.some((col) => col.name === column)) {
|
|
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
}
|
|
}
|
|
|
|
function migrate() {
|
|
ensureColumn('pm_plan_steps', 'est_minutes', 'INTEGER');
|
|
ensureColumn('pm_completions', 'cost', 'REAL NOT NULL DEFAULT 0');
|
|
ensureColumn('pm_completions', 'components_json', 'TEXT');
|
|
ensureColumn('pm_completions', 'notes_json', 'TEXT');
|
|
ensureColumn('pm_completions', 'signature', 'TEXT');
|
|
ensureColumn('pm_completions', 'rating_safety', 'INTEGER');
|
|
ensureColumn('pm_completions', 'rating_physical', 'INTEGER');
|
|
ensureColumn('pm_completions', 'rating_operating', 'INTEGER');
|
|
ensureColumn('workday_connections', 'sync_enabled', 'INTEGER NOT NULL DEFAULT 0');
|
|
ensureColumn('workday_connections', 'sync_interval_hours', 'INTEGER NOT NULL DEFAULT 24');
|
|
ensureColumn('alerts', 'external_ticket_id', 'TEXT');
|
|
ensureColumn('alerts', 'external_ticket_number', 'TEXT');
|
|
ensureColumn('alerts', 'external_ticket_url', 'TEXT');
|
|
ensureColumn('alerts', 'external_ticket_system', 'TEXT');
|
|
ensureColumn('assets', 'servicenow_sys_id', 'TEXT');
|
|
ensureColumn('assets', 'asset_class_number', 'TEXT');
|
|
ensureColumn('assets', 'special_zone', 'TEXT');
|
|
dropUsersRoleCheck();
|
|
}
|
|
|
|
// Custom roles require the legacy CHECK(role IN (...)) constraint on `users` to be removed.
|
|
// SQLite can't drop a column constraint in place, so rebuild the table when the old DDL is present.
|
|
function dropUsersRoleCheck() {
|
|
const ddl = one("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'users'");
|
|
if (!ddl || !/CHECK\s*\(\s*role/i.test(ddl.sql)) return;
|
|
db.exec('PRAGMA foreign_keys=OFF');
|
|
db.exec(`
|
|
CREATE TABLE users_rebuild (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
team_id INTEGER REFERENCES teams(id),
|
|
name TEXT NOT NULL,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'viewer',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
created_at TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
INSERT INTO users_rebuild (id, team_id, name, email, password_hash, role, status, created_at, updated_at)
|
|
SELECT id, team_id, name, email, password_hash, role, status, created_at, updated_at FROM users;
|
|
DROP TABLE users;
|
|
ALTER TABLE users_rebuild RENAME TO users;
|
|
`);
|
|
db.exec('PRAGMA foreign_keys=ON');
|
|
}
|
|
|
|
function seed() {
|
|
const createdAt = now();
|
|
|
|
// Built-in roles (capability sets). Insert missing ones; keep admin's wildcard in sync.
|
|
const { DEFAULT_ROLES } = require('./services/accessControl');
|
|
for (const role of DEFAULT_ROLES) {
|
|
run(
|
|
`INSERT INTO roles (key, name, description, capabilities_json, is_system, locked, sort_order, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET is_system = excluded.is_system, locked = excluded.locked`,
|
|
[role.key, role.name, role.description, json(role.capabilities), role.is_system, role.locked, role.sort_order, createdAt, createdAt]
|
|
);
|
|
}
|
|
run("UPDATE roles SET capabilities_json = ? WHERE key = 'admin'", [json(['*'])]);
|
|
|
|
// Asset class catalog: seed from the IRS reference tables once; afterwards it is
|
|
// fully user-managed (edits/deletes persist and are not re-seeded).
|
|
if (!one('SELECT id FROM asset_classes LIMIT 1')) {
|
|
const { IRS_ASSET_CLASSES } = require('./data/irsAssetClasses');
|
|
for (const item of IRS_ASSET_CLASSES) {
|
|
run(
|
|
'INSERT INTO asset_classes (class_number, description, gds, ads, source, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
[item.class_number || null, item.description, item.gds ?? null, item.ads ?? null, item.table || 'common', createdAt, createdAt]
|
|
);
|
|
}
|
|
}
|
|
|
|
// Special depreciation zones (e.g. New York Liberty Zone). Seeded once, then user-managed.
|
|
run(
|
|
`INSERT OR IGNORE INTO depreciation_zones
|
|
(code, name, allowance_percent, pis_start, pis_end, real_property_end, max_recovery_years, leasehold_life_years, notes, enabled, created_at, updated_at)
|
|
VALUES ('ny_liberty', 'New York Liberty Zone', 30, '2001-09-11', '2006-12-31', '2009-12-31', 20, 5,
|
|
'30% special depreciation allowance for qualified NY Liberty Zone property; qualified leasehold improvements use a 5-year life; increased Section 179. Verify current tax law before filing.', 1, ?, ?)`,
|
|
[createdAt, createdAt]
|
|
);
|
|
if (!one('SELECT id FROM teams LIMIT 1')) {
|
|
run('INSERT INTO teams (name, description, created_at) VALUES (?, ?, ?)', [
|
|
'Finance Operations',
|
|
'Default MixedAssets team',
|
|
createdAt
|
|
]);
|
|
}
|
|
|
|
const team = one('SELECT id FROM teams WHERE name = ?', ['Finance Operations']);
|
|
if (!one('SELECT id FROM users LIMIT 1')) {
|
|
run(
|
|
'INSERT INTO users (team_id, name, email, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
[
|
|
team.id,
|
|
'MixedAssets Admin',
|
|
'admin@mixedassets.local',
|
|
bcrypt.hashSync(process.env.MIXEDASSETS_ADMIN_PASSWORD || 'ChangeMe123!', 12),
|
|
'admin',
|
|
createdAt,
|
|
createdAt
|
|
]
|
|
);
|
|
}
|
|
|
|
if (!one('SELECT id FROM entities LIMIT 1')) {
|
|
run(
|
|
'INSERT INTO entities (name, legal_name, fiscal_year_end_month, base_currency, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
|
|
['Demo Company', 'Demo Company LLC', 12, 'USD', createdAt, createdAt]
|
|
);
|
|
}
|
|
|
|
const refs = [
|
|
['location', 'Headquarters', 'HQ'],
|
|
['location', 'Warehouse', 'WH'],
|
|
['department', 'Accounting', 'ACCT'],
|
|
['department', 'Operations', 'OPS'],
|
|
['category', 'Computer Equipment', 'COMP'],
|
|
['category', 'Office Furniture', 'FURN'],
|
|
['category', 'Vehicles', 'AUTO'],
|
|
['category', 'Leasehold Improvements', 'LHI'],
|
|
['part_category', 'Electrical', null],
|
|
['part_category', 'Plumbing', null],
|
|
['part_category', 'Structural Component', null],
|
|
['part_category', 'Electronic Component', null],
|
|
['part_category', 'Fire Safety', null],
|
|
['part_category', 'Mechanical Safety', null],
|
|
['part_category', 'Other', null]
|
|
];
|
|
for (const item of refs) {
|
|
run('INSERT OR IGNORE INTO reference_values (type, name, code, metadata) VALUES (?, ?, ?, ?)', [
|
|
item[0],
|
|
item[1],
|
|
item[2],
|
|
'{}'
|
|
]);
|
|
}
|
|
|
|
run(
|
|
`INSERT OR IGNORE INTO employees (
|
|
employee_number, name, email, department, location, manager_name, status, source, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
['E-1001', 'Jordan Avery', 'jordan.avery@example.com', 'Operations', 'Headquarters', 'MixedAssets Admin', 'active', 'seed', createdAt, createdAt]
|
|
);
|
|
|
|
run(
|
|
`INSERT OR IGNORE INTO workday_connections (
|
|
name, tenant, base_url, token_url, workers_path, enabled, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
['Primary Workday', '', '', '', '/workers', 0, createdAt, createdAt]
|
|
);
|
|
|
|
const starterTemplate = {
|
|
category: 'Computer Equipment',
|
|
useful_life_months: 60,
|
|
depreciation_method: 'macrs_gds_200db_5',
|
|
convention: 'half_year',
|
|
gl_asset_account: '1500',
|
|
gl_expense_account: '6400',
|
|
gl_accumulated_account: '1590',
|
|
business_use_percent: 100
|
|
};
|
|
run(
|
|
'INSERT OR IGNORE INTO asset_templates (name, description, defaults, custom_fields, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
|
|
[
|
|
'Laptop or workstation',
|
|
'Standard computer equipment template with 5-year tax life.',
|
|
json(starterTemplate),
|
|
json([
|
|
{ key: 'assigned_employee_id', label: 'Employee ID', type: 'text', required: false },
|
|
{ key: 'software_profile', label: 'Software profile', type: 'text', required: false }
|
|
]),
|
|
createdAt,
|
|
createdAt
|
|
]
|
|
);
|
|
|
|
const federalPath = path.join(process.cwd(), 'data', 'tax-rules', 'federal-2026.json');
|
|
if (fs.existsSync(federalPath)) {
|
|
const rules = expandRuleSet(JSON.parse(fs.readFileSync(federalPath, 'utf8')));
|
|
run(
|
|
`INSERT INTO tax_rule_sets (jurisdiction, name, effective_date, version, rules_json, active, source_note, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(jurisdiction, version) DO UPDATE SET
|
|
name = excluded.name,
|
|
effective_date = excluded.effective_date,
|
|
rules_json = excluded.rules_json,
|
|
active = excluded.active,
|
|
source_note = excluded.source_note`,
|
|
[
|
|
rules.jurisdiction || 'US-FED',
|
|
rules.name || 'Federal starter depreciation rules',
|
|
rules.effectiveDate || rules.effective_date || '2026-01-01',
|
|
rules.version || '2026.2',
|
|
json(rules),
|
|
1,
|
|
rules.sourceNote || 'Starter rule template. Confirm current tax law before production filing.',
|
|
createdAt
|
|
]
|
|
);
|
|
}
|
|
|
|
if (!one('SELECT id FROM books LIMIT 1')) {
|
|
const federalRule = one("SELECT id FROM tax_rule_sets WHERE active = 1 ORDER BY effective_date DESC, id DESC LIMIT 1");
|
|
const ruleId = federalRule ? federalRule.id : null;
|
|
const bookDefs = [
|
|
['GAAP', 'GAAP / Financial', 'Primary book for financial statements', 'financial', 1, 1, 'straight_line', 'half_year'],
|
|
['FEDERAL', 'Federal Tax', 'US federal income tax depreciation', 'tax', 1, 0, 'macrs_gds_200db_5', 'half_year'],
|
|
['STATE', 'State Tax', 'State income tax depreciation', 'tax', 1, 0, 'macrs_gds_200db_5', 'half_year'],
|
|
['AMT', 'Alternative Minimum Tax', 'AMT depreciation adjustments', 'tax', 1, 0, 'macrs_gds_150db_5', 'half_year'],
|
|
['ACE', 'Adjusted Current Earnings', 'ACE depreciation', 'tax', 1, 0, 'macrs_ads_sl_5', 'half_year'],
|
|
['USER', 'User-defined', 'Custom internal reporting book', 'internal', 0, 0, 'straight_line', 'half_year']
|
|
];
|
|
bookDefs.forEach((def, index) => {
|
|
run(
|
|
`INSERT INTO books (code, name, description, book_type, enabled, is_primary, tax_rule_set_id, default_method, default_convention, sort_order, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[def[0], def[1], def[2], def[3], def[4], def[5], ruleId, def[6], def[7], index, createdAt, createdAt]
|
|
);
|
|
});
|
|
}
|
|
|
|
// The PM book tracks actual preventative-maintenance spend per asset (not depreciation).
|
|
run(
|
|
`INSERT OR IGNORE INTO books (code, name, description, book_type, enabled, is_primary, sort_order, created_at, updated_at)
|
|
VALUES ('PM', 'PM / Maintenance', 'Actual preventative-maintenance spend per asset', 'maintenance', 1, 0, 6, ?, ?)`,
|
|
[createdAt, createdAt]
|
|
);
|
|
|
|
const settings = {
|
|
server_fqdn: process.env.MIXEDASSETS_SERVER_FQDN || 'localhost',
|
|
cors_allowed_domains: process.env.MIXEDASSETS_CORS_ORIGINS || 'http://localhost:5173',
|
|
database_driver: 'node:sqlite',
|
|
database_path: DB_PATH,
|
|
default_books: 'GAAP,FEDERAL,STATE,AMT,ACE,USER',
|
|
alerts_enabled: 'false',
|
|
alerts_lead_days: '30',
|
|
alerts_recipients: '',
|
|
smtp_host: '',
|
|
smtp_port: '587',
|
|
smtp_secure: 'false',
|
|
smtp_user: '',
|
|
smtp_password: '',
|
|
smtp_from: 'MixedAssets <no-reply@mixedassets.local>',
|
|
webhook_enabled: 'false',
|
|
webhook_url: '',
|
|
webhook_secret: '',
|
|
servicenow_instance_url: '',
|
|
servicenow_username: '',
|
|
servicenow_password: '',
|
|
servicenow_incident_table: 'incident',
|
|
servicenow_assignment_group: '',
|
|
servicenow_caller_id: '',
|
|
servicenow_ticket_enabled: 'false',
|
|
servicenow_auto_ticket: 'false',
|
|
servicenow_cmdb_table: 'cmdb_ci_hardware',
|
|
servicenow_cmdb_query: '',
|
|
servicenow_cmdb_limit: '200',
|
|
servicenow_cmdb_map: '',
|
|
servicenow_last_sync_at: '',
|
|
servicenow_last_sync_status: '',
|
|
pm_default_frequency_value: '3',
|
|
pm_default_frequency_unit: 'months',
|
|
pm_lead_days: '7'
|
|
};
|
|
for (const [key, value] of Object.entries(settings)) {
|
|
run('INSERT OR IGNORE INTO app_settings (key, value, updated_at) VALUES (?, ?, ?)', [key, String(value), createdAt]);
|
|
}
|
|
}
|
|
|
|
function audit(actorId, action, entityType, entityId, beforeValue, afterValue) {
|
|
run(
|
|
'INSERT INTO audit_logs (actor_id, action, entity_type, entity_id, before_json, after_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
[actorId || null, action, entityType, entityId === undefined ? null : String(entityId), json(beforeValue), json(afterValue), now()]
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
DB_PATH,
|
|
all,
|
|
audit,
|
|
db,
|
|
initialize,
|
|
json,
|
|
now,
|
|
one,
|
|
parseJson,
|
|
run,
|
|
tx
|
|
};
|