Updated A LOT

This commit is contained in:
2026-06-05 04:15:24 -05:00
parent 8335f1af1b
commit 4072695432
92 changed files with 16139 additions and 570 deletions

View File

@@ -84,12 +84,24 @@ function initialize() {
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('admin','finance','operations','viewer')),
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,
@@ -306,6 +318,8 @@ function initialize() {
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,
@@ -335,6 +349,309 @@ function initialize() {
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),
@@ -347,11 +664,100 @@ function initialize() {
);
`);
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',
@@ -391,7 +797,14 @@ function seed() {
['category', 'Computer Equipment', 'COMP'],
['category', 'Office Furniture', 'FURN'],
['category', 'Vehicles', 'AUTO'],
['category', 'Leasehold Improvements', 'LHI']
['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 (?, ?, ?, ?)', [
@@ -466,12 +879,68 @@ function seed() {
);
}
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'
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]);