Added Multi-Company Support
This commit is contained in:
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const { all, audit, json, now, one, parseJson, run } = require('../db');
|
||||
const { requireCapability } = require('../middleware/auth');
|
||||
const { formatAssetId } = require('../services/assets');
|
||||
const companies = require('../services/companies');
|
||||
const assetClasses = require('../services/assetClasses');
|
||||
const zones = require('../services/zones');
|
||||
const section179Limits = require('../services/section179Limits');
|
||||
@@ -95,30 +96,43 @@ router.delete('/asset-classes/:id', requireCapability('config.manage'), (req, re
|
||||
return res.status(204).end();
|
||||
});
|
||||
|
||||
// Companies (stored in `entities`). Returns the user's accessible active companies for the switcher;
|
||||
// company admins requesting ?includeDisposed=1 get the full management list (incl. disposed).
|
||||
router.get('/entities', (req, res) => {
|
||||
res.json({ entities: all('SELECT * FROM entities ORDER BY name') });
|
||||
if (req.query.includeDisposed === '1' && companies.isCompanyAdmin(req.user)) {
|
||||
return res.json({ entities: companies.listCompanies({ includeDisposed: true }) });
|
||||
}
|
||||
return res.json({ entities: companies.companiesForUser(req.user) });
|
||||
});
|
||||
|
||||
router.post('/entities', requireCapability('config.manage'), (req, res) => {
|
||||
const created = now();
|
||||
const result = run(
|
||||
'INSERT INTO entities (name, legal_name, tax_id, fiscal_year_end_month, base_currency, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[
|
||||
req.body.name,
|
||||
req.body.legal_name || null,
|
||||
req.body.tax_id || null,
|
||||
Number(req.body.fiscal_year_end_month || 12),
|
||||
req.body.base_currency || 'USD',
|
||||
created,
|
||||
created
|
||||
]
|
||||
);
|
||||
audit(req.user.id, 'create', 'entity', result.lastInsertRowid, null, req.body);
|
||||
res.status(201).json({ entity: one('SELECT * FROM entities WHERE id = ?', [result.lastInsertRowid]) });
|
||||
router.post('/entities', requireCapability('config.manage'), (req, res, next) => {
|
||||
try {
|
||||
res.status(201).json({ entity: companies.createCompany(req.body, req.user.id) });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/entities/:id', requireCapability('config.manage'), (req, res) => {
|
||||
const entity = companies.updateCompany(req.params.id, req.body, req.user.id);
|
||||
if (!entity) return res.status(404).json({ error: 'Company not found' });
|
||||
return res.json({ entity });
|
||||
});
|
||||
|
||||
router.post('/entities/:id/dispose', requireCapability('config.manage'), (req, res) => {
|
||||
const result = companies.disposeCompany(req.params.id, req.body || {}, req.user.id);
|
||||
if (!result.ok) return res.status(result.status).json({ error: result.error });
|
||||
return res.json({ entity: result.company });
|
||||
});
|
||||
|
||||
router.post('/entities/:id/restore', requireCapability('config.manage'), (req, res) => {
|
||||
const entity = companies.restoreCompany(req.params.id, req.user.id);
|
||||
if (!entity) return res.status(404).json({ error: 'Company not found' });
|
||||
return res.json({ entity });
|
||||
});
|
||||
|
||||
router.get('/references', (req, res) => {
|
||||
const rows = all('SELECT * FROM reference_values ORDER BY type, name');
|
||||
const rows = all('SELECT * FROM reference_values WHERE entity_id = ? ORDER BY type, name', [req.companyId]);
|
||||
res.json({ references: rows.map((row) => ({ ...row, metadata: parseJson(row.metadata, {}) })) });
|
||||
});
|
||||
|
||||
@@ -144,7 +158,7 @@ function categoryRow(row) {
|
||||
id_template_id: template ? templateId : null,
|
||||
id_template_name: template ? template.name : null,
|
||||
id_template_pattern: template ? template.pattern : null,
|
||||
asset_count: one('SELECT COUNT(*) AS c FROM assets WHERE category = ?', [row.name]).c
|
||||
asset_count: one('SELECT COUNT(*) AS c FROM assets WHERE category = ? AND entity_id = ?', [row.name, row.entity_id]).c
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,31 +179,31 @@ function resolveCategoryClass(body, prev = {}) {
|
||||
const categoryEditor = requireCapability('config.manage');
|
||||
|
||||
router.get('/asset-categories', (req, res) => {
|
||||
const rows = all("SELECT * FROM reference_values WHERE type = 'category' ORDER BY name");
|
||||
const rows = all("SELECT * FROM reference_values WHERE type = 'category' AND entity_id = ? ORDER BY name", [req.companyId]);
|
||||
res.json({ categories: rows.map(categoryRow) });
|
||||
});
|
||||
|
||||
router.post('/asset-categories', categoryEditor, (req, res) => {
|
||||
const name = String(req.body.name || '').trim();
|
||||
if (!name) return res.status(400).json({ error: 'Category name is required' });
|
||||
if (one("SELECT id FROM reference_values WHERE type = 'category' AND name = ?", [name])) {
|
||||
if (one("SELECT id FROM reference_values WHERE type = 'category' AND name = ? AND entity_id = ?", [name, req.companyId])) {
|
||||
return res.status(400).json({ error: 'That category already exists' });
|
||||
}
|
||||
const { classId, classNumber } = resolveCategoryClass(req.body);
|
||||
const result = run(
|
||||
"INSERT INTO reference_values (type, name, code, metadata) VALUES ('category', ?, ?, ?)",
|
||||
[name, req.body.code || null, json({ id_template_id: req.body.id_template_id || null, asset_class_id: classId, asset_class_number: classNumber })]
|
||||
"INSERT INTO reference_values (entity_id, type, name, code, metadata) VALUES (?, 'category', ?, ?, ?)",
|
||||
[req.companyId, name, req.body.code || null, json({ id_template_id: req.body.id_template_id || null, asset_class_id: classId, asset_class_number: classNumber })]
|
||||
);
|
||||
audit(req.user.id, 'create', 'asset_category', result.lastInsertRowid, null, { name });
|
||||
return res.status(201).json({ category: categoryRow(one('SELECT * FROM reference_values WHERE id = ?', [result.lastInsertRowid])) });
|
||||
});
|
||||
|
||||
router.put('/asset-categories/:id', categoryEditor, (req, res) => {
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'category'", [req.params.id]);
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'category' AND entity_id = ?", [req.params.id, req.companyId]);
|
||||
if (!before) return res.status(404).json({ error: 'Category not found' });
|
||||
const name = String(req.body.name ?? before.name).trim();
|
||||
if (!name) return res.status(400).json({ error: 'Category name is required' });
|
||||
if (name !== before.name && one("SELECT id FROM reference_values WHERE type = 'category' AND name = ?", [name])) {
|
||||
if (name !== before.name && one("SELECT id FROM reference_values WHERE type = 'category' AND name = ? AND entity_id = ?", [name, req.companyId])) {
|
||||
return res.status(400).json({ error: 'That category already exists' });
|
||||
}
|
||||
const meta = parseJson(before.metadata, {});
|
||||
@@ -203,20 +217,20 @@ router.put('/asset-categories/:id', categoryEditor, (req, res) => {
|
||||
run('UPDATE reference_values SET name = ?, code = ?, metadata = ? WHERE id = ?', [name, req.body.code ?? before.code, json(meta), req.params.id]);
|
||||
let renamed = 0;
|
||||
if (name !== before.name) {
|
||||
renamed = run('UPDATE assets SET category = ?, updated_at = ? WHERE category = ?', [name, now(), before.name]).changes || 0;
|
||||
renamed = run('UPDATE assets SET category = ?, updated_at = ? WHERE category = ? AND entity_id = ?', [name, now(), before.name, req.companyId]).changes || 0;
|
||||
}
|
||||
// The class number is shared by every asset in the category — cascade any change.
|
||||
// The class number is shared by every asset in the category — cascade any change (within this company).
|
||||
if (classChanged) {
|
||||
run('UPDATE assets SET asset_class_number = ?, updated_at = ? WHERE category = ?', [meta.asset_class_number, now(), name]);
|
||||
run('UPDATE assets SET asset_class_number = ?, updated_at = ? WHERE category = ? AND entity_id = ?', [meta.asset_class_number, now(), name, req.companyId]);
|
||||
}
|
||||
audit(req.user.id, 'update', 'asset_category', req.params.id, before, { name, renamed_assets: renamed });
|
||||
return res.json({ category: categoryRow(one('SELECT * FROM reference_values WHERE id = ?', [req.params.id])), renamed_assets: renamed });
|
||||
});
|
||||
|
||||
router.delete('/asset-categories/:id', categoryEditor, (req, res) => {
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'category'", [req.params.id]);
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'category' AND entity_id = ?", [req.params.id, req.companyId]);
|
||||
if (!before) return res.status(404).json({ error: 'Category not found' });
|
||||
const affected = one('SELECT COUNT(*) AS c FROM assets WHERE category = ?', [before.name]).c;
|
||||
const affected = one('SELECT COUNT(*) AS c FROM assets WHERE category = ? AND entity_id = ?', [before.name, req.companyId]).c;
|
||||
run('DELETE FROM reference_values WHERE id = ?', [req.params.id]);
|
||||
audit(req.user.id, 'delete', 'asset_category', req.params.id, before, { asset_count: affected });
|
||||
return res.json({ deleted: true, asset_count: affected });
|
||||
@@ -230,50 +244,50 @@ function departmentRow(row) {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
asset_count: one('SELECT COUNT(*) AS c FROM assets WHERE department = ?', [row.name]).c
|
||||
asset_count: one('SELECT COUNT(*) AS c FROM assets WHERE department = ? AND entity_id = ?', [row.name, row.entity_id]).c
|
||||
};
|
||||
}
|
||||
|
||||
router.get('/asset-departments', (req, res) => {
|
||||
const rows = all("SELECT * FROM reference_values WHERE type = 'department' ORDER BY name");
|
||||
const rows = all("SELECT * FROM reference_values WHERE type = 'department' AND entity_id = ? ORDER BY name", [req.companyId]);
|
||||
res.json({ departments: rows.map(departmentRow) });
|
||||
});
|
||||
|
||||
router.post('/asset-departments', categoryEditor, (req, res) => {
|
||||
const name = String(req.body.name || '').trim();
|
||||
if (!name) return res.status(400).json({ error: 'Department name is required' });
|
||||
if (one("SELECT id FROM reference_values WHERE type = 'department' AND name = ?", [name])) {
|
||||
if (one("SELECT id FROM reference_values WHERE type = 'department' AND name = ? AND entity_id = ?", [name, req.companyId])) {
|
||||
return res.status(400).json({ error: 'That department already exists' });
|
||||
}
|
||||
const result = run(
|
||||
"INSERT INTO reference_values (type, name, code, metadata) VALUES ('department', ?, ?, '{}')",
|
||||
[name, req.body.code || null]
|
||||
"INSERT INTO reference_values (entity_id, type, name, code, metadata) VALUES (?, 'department', ?, ?, '{}')",
|
||||
[req.companyId, name, req.body.code || null]
|
||||
);
|
||||
audit(req.user.id, 'create', 'asset_department', result.lastInsertRowid, null, { name });
|
||||
return res.status(201).json({ department: departmentRow(one('SELECT * FROM reference_values WHERE id = ?', [result.lastInsertRowid])) });
|
||||
});
|
||||
|
||||
router.put('/asset-departments/:id', categoryEditor, (req, res) => {
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'department'", [req.params.id]);
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'department' AND entity_id = ?", [req.params.id, req.companyId]);
|
||||
if (!before) return res.status(404).json({ error: 'Department not found' });
|
||||
const name = String(req.body.name ?? before.name).trim();
|
||||
if (!name) return res.status(400).json({ error: 'Department name is required' });
|
||||
if (name !== before.name && one("SELECT id FROM reference_values WHERE type = 'department' AND name = ?", [name])) {
|
||||
if (name !== before.name && one("SELECT id FROM reference_values WHERE type = 'department' AND name = ? AND entity_id = ?", [name, req.companyId])) {
|
||||
return res.status(400).json({ error: 'That department already exists' });
|
||||
}
|
||||
run('UPDATE reference_values SET name = ?, code = ? WHERE id = ?', [name, req.body.code ?? before.code, req.params.id]);
|
||||
let renamed = 0;
|
||||
if (name !== before.name) {
|
||||
renamed = run('UPDATE assets SET department = ?, updated_at = ? WHERE department = ?', [name, now(), before.name]).changes || 0;
|
||||
renamed = run('UPDATE assets SET department = ?, updated_at = ? WHERE department = ? AND entity_id = ?', [name, now(), before.name, req.companyId]).changes || 0;
|
||||
}
|
||||
audit(req.user.id, 'update', 'asset_department', req.params.id, before, { name, renamed_assets: renamed });
|
||||
return res.json({ department: departmentRow(one('SELECT * FROM reference_values WHERE id = ?', [req.params.id])), renamed_assets: renamed });
|
||||
});
|
||||
|
||||
router.delete('/asset-departments/:id', categoryEditor, (req, res) => {
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'department'", [req.params.id]);
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'department' AND entity_id = ?", [req.params.id, req.companyId]);
|
||||
if (!before) return res.status(404).json({ error: 'Department not found' });
|
||||
const affected = one('SELECT COUNT(*) AS c FROM assets WHERE department = ?', [before.name]).c;
|
||||
const affected = one('SELECT COUNT(*) AS c FROM assets WHERE department = ? AND entity_id = ?', [before.name, req.companyId]).c;
|
||||
run('DELETE FROM reference_values WHERE id = ?', [req.params.id]);
|
||||
audit(req.user.id, 'delete', 'asset_department', req.params.id, before, { asset_count: affected });
|
||||
return res.json({ deleted: true, asset_count: affected });
|
||||
@@ -287,50 +301,50 @@ function pmCategoryRow(row) {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
plan_count: one('SELECT COUNT(*) AS c FROM pm_plans WHERE category = ?', [row.name]).c
|
||||
plan_count: one('SELECT COUNT(*) AS c FROM pm_plans WHERE category = ? AND entity_id = ?', [row.name, row.entity_id]).c
|
||||
};
|
||||
}
|
||||
|
||||
router.get('/pm-categories', (req, res) => {
|
||||
const rows = all("SELECT * FROM reference_values WHERE type = 'pm_category' ORDER BY name");
|
||||
const rows = all("SELECT * FROM reference_values WHERE type = 'pm_category' AND entity_id = ? ORDER BY name", [req.companyId]);
|
||||
res.json({ categories: rows.map(pmCategoryRow) });
|
||||
});
|
||||
|
||||
router.post('/pm-categories', categoryEditor, (req, res) => {
|
||||
const name = String(req.body.name || '').trim();
|
||||
if (!name) return res.status(400).json({ error: 'Category name is required' });
|
||||
if (one("SELECT id FROM reference_values WHERE type = 'pm_category' AND name = ?", [name])) {
|
||||
if (one("SELECT id FROM reference_values WHERE type = 'pm_category' AND name = ? AND entity_id = ?", [name, req.companyId])) {
|
||||
return res.status(400).json({ error: 'That PM category already exists' });
|
||||
}
|
||||
const result = run(
|
||||
"INSERT INTO reference_values (type, name, code, metadata) VALUES ('pm_category', ?, ?, '{}')",
|
||||
[name, req.body.code || null]
|
||||
"INSERT INTO reference_values (entity_id, type, name, code, metadata) VALUES (?, 'pm_category', ?, ?, '{}')",
|
||||
[req.companyId, name, req.body.code || null]
|
||||
);
|
||||
audit(req.user.id, 'create', 'pm_category', result.lastInsertRowid, null, { name });
|
||||
return res.status(201).json({ category: pmCategoryRow(one('SELECT * FROM reference_values WHERE id = ?', [result.lastInsertRowid])) });
|
||||
});
|
||||
|
||||
router.put('/pm-categories/:id', categoryEditor, (req, res) => {
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'pm_category'", [req.params.id]);
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'pm_category' AND entity_id = ?", [req.params.id, req.companyId]);
|
||||
if (!before) return res.status(404).json({ error: 'PM category not found' });
|
||||
const name = String(req.body.name ?? before.name).trim();
|
||||
if (!name) return res.status(400).json({ error: 'Category name is required' });
|
||||
if (name !== before.name && one("SELECT id FROM reference_values WHERE type = 'pm_category' AND name = ?", [name])) {
|
||||
if (name !== before.name && one("SELECT id FROM reference_values WHERE type = 'pm_category' AND name = ? AND entity_id = ?", [name, req.companyId])) {
|
||||
return res.status(400).json({ error: 'That PM category already exists' });
|
||||
}
|
||||
run('UPDATE reference_values SET name = ?, code = ? WHERE id = ?', [name, req.body.code ?? before.code, req.params.id]);
|
||||
let renamed = 0;
|
||||
if (name !== before.name) {
|
||||
renamed = run('UPDATE pm_plans SET category = ?, updated_at = ? WHERE category = ?', [name, now(), before.name]).changes || 0;
|
||||
renamed = run('UPDATE pm_plans SET category = ?, updated_at = ? WHERE category = ? AND entity_id = ?', [name, now(), before.name, req.companyId]).changes || 0;
|
||||
}
|
||||
audit(req.user.id, 'update', 'pm_category', req.params.id, before, { name, renamed_plans: renamed });
|
||||
return res.json({ category: pmCategoryRow(one('SELECT * FROM reference_values WHERE id = ?', [req.params.id])), renamed_plans: renamed });
|
||||
});
|
||||
|
||||
router.delete('/pm-categories/:id', categoryEditor, (req, res) => {
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'pm_category'", [req.params.id]);
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'pm_category' AND entity_id = ?", [req.params.id, req.companyId]);
|
||||
if (!before) return res.status(404).json({ error: 'PM category not found' });
|
||||
const affected = one('SELECT COUNT(*) AS c FROM pm_plans WHERE category = ?', [before.name]).c;
|
||||
const affected = one('SELECT COUNT(*) AS c FROM pm_plans WHERE category = ? AND entity_id = ?', [before.name, req.companyId]).c;
|
||||
run('DELETE FROM reference_values WHERE id = ?', [req.params.id]);
|
||||
audit(req.user.id, 'delete', 'pm_category', req.params.id, before, { plan_count: affected });
|
||||
return res.json({ deleted: true, plan_count: affected });
|
||||
@@ -349,30 +363,30 @@ function partCategoryRow(row) {
|
||||
}
|
||||
|
||||
router.get('/part-categories', (req, res) => {
|
||||
const rows = all("SELECT * FROM reference_values WHERE type = 'part_category' ORDER BY name");
|
||||
const rows = all("SELECT * FROM reference_values WHERE type = 'part_category' AND entity_id = ? ORDER BY name", [req.companyId]);
|
||||
res.json({ categories: rows.map(partCategoryRow) });
|
||||
});
|
||||
|
||||
router.post('/part-categories', categoryEditor, (req, res) => {
|
||||
const name = String(req.body.name || '').trim();
|
||||
if (!name) return res.status(400).json({ error: 'Category name is required' });
|
||||
if (one("SELECT id FROM reference_values WHERE type = 'part_category' AND name = ?", [name])) {
|
||||
if (one("SELECT id FROM reference_values WHERE type = 'part_category' AND name = ? AND entity_id = ?", [name, req.companyId])) {
|
||||
return res.status(400).json({ error: 'That part category already exists' });
|
||||
}
|
||||
const result = run(
|
||||
"INSERT INTO reference_values (type, name, code, metadata) VALUES ('part_category', ?, ?, '{}')",
|
||||
[name, req.body.code || null]
|
||||
"INSERT INTO reference_values (entity_id, type, name, code, metadata) VALUES (?, 'part_category', ?, ?, '{}')",
|
||||
[req.companyId, name, req.body.code || null]
|
||||
);
|
||||
audit(req.user.id, 'create', 'part_category', result.lastInsertRowid, null, { name });
|
||||
return res.status(201).json({ category: partCategoryRow(one('SELECT * FROM reference_values WHERE id = ?', [result.lastInsertRowid])) });
|
||||
});
|
||||
|
||||
router.put('/part-categories/:id', categoryEditor, (req, res) => {
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'part_category'", [req.params.id]);
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'part_category' AND entity_id = ?", [req.params.id, req.companyId]);
|
||||
if (!before) return res.status(404).json({ error: 'Part category not found' });
|
||||
const name = String(req.body.name ?? before.name).trim();
|
||||
if (!name) return res.status(400).json({ error: 'Category name is required' });
|
||||
if (name !== before.name && one("SELECT id FROM reference_values WHERE type = 'part_category' AND name = ?", [name])) {
|
||||
if (name !== before.name && one("SELECT id FROM reference_values WHERE type = 'part_category' AND name = ? AND entity_id = ?", [name, req.companyId])) {
|
||||
return res.status(400).json({ error: 'That part category already exists' });
|
||||
}
|
||||
run('UPDATE reference_values SET name = ?, code = ? WHERE id = ?', [name, req.body.code ?? before.code, req.params.id]);
|
||||
@@ -385,7 +399,7 @@ router.put('/part-categories/:id', categoryEditor, (req, res) => {
|
||||
});
|
||||
|
||||
router.delete('/part-categories/:id', categoryEditor, (req, res) => {
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'part_category'", [req.params.id]);
|
||||
const before = one("SELECT * FROM reference_values WHERE id = ? AND type = 'part_category' AND entity_id = ?", [req.params.id, req.companyId]);
|
||||
if (!before) return res.status(404).json({ error: 'Part category not found' });
|
||||
const affected = one('SELECT COUNT(*) AS c FROM parts WHERE category = ?', [before.name]).c;
|
||||
run('DELETE FROM reference_values WHERE id = ?', [req.params.id]);
|
||||
|
||||
Reference in New Issue
Block a user