Added Multi-Company Support

This commit is contained in:
2026-06-08 00:54:21 -05:00
parent c164395915
commit db614a6707
32 changed files with 1133 additions and 323 deletions

View File

@@ -44,9 +44,12 @@ const assetColumns = [
const defaultBooks = ['GAAP', 'FEDERAL', 'STATE', 'AMT', 'ACE', 'USER'];
// Depreciation books currently enabled in the registry (excludes the maintenance/PM book).
function enabledBookCodes() {
function enabledBookCodes(companyId) {
try {
const rows = all("SELECT code FROM books WHERE enabled = 1 AND book_type != 'maintenance' ORDER BY sort_order, id");
const rows = all(
"SELECT code FROM books WHERE enabled = 1 AND book_type != 'maintenance' AND (? IS NULL OR entity_id = ?) ORDER BY sort_order, id",
[companyId || null, companyId || null]
);
if (rows.length) return rows.map((row) => row.code);
} catch {
// books table may not exist on a very old database
@@ -121,17 +124,18 @@ function hydrateAsset(id) {
return asset;
}
function lookupAssetByCode(code) {
function lookupAssetByCode(code, companyId) {
const value = String(code || '').trim();
if (!value) return null;
const row = one(
`SELECT id FROM assets
WHERE barcode_value = ? COLLATE NOCASE
WHERE (? IS NULL OR entity_id = ?)
AND (barcode_value = ? COLLATE NOCASE
OR asset_id = ? COLLATE NOCASE
OR serial_number = ? COLLATE NOCASE
OR serial_number = ? COLLATE NOCASE)
ORDER BY (barcode_value = ? COLLATE NOCASE) DESC, id
LIMIT 1`,
[value, value, value, value]
[companyId || null, companyId || null, value, value, value, value]
);
return row ? hydrateAsset(row.id) : null;
}
@@ -212,8 +216,8 @@ function normalizeAssetInput(body) {
return input;
}
function createBooks(assetId, asset, books = []) {
const selectedBooks = books.length ? books : enabledBookCodes().map((book) => ({ book_type: book }));
function createBooks(assetId, asset, books = [], companyId) {
const selectedBooks = books.length ? books : enabledBookCodes(companyId).map((book) => ({ book_type: book }));
for (const book of selectedBooks) {
run(
`INSERT OR REPLACE INTO asset_books (
@@ -262,9 +266,10 @@ function listAssets(query = {}) {
]).map(assetFromRow);
}
function createAsset(body, userId) {
function createAsset(body, userId, companyId) {
const created = now();
const input = normalizeAssetInput(body);
if (companyId) input.entity_id = companyId; // new assets belong to the active company
const result = tx(() => {
input.asset_id = resolveNewAssetId(input); // category ID template, else default sequence
const insert = run(
@@ -272,7 +277,7 @@ function createAsset(body, userId) {
VALUES (${assetColumns.map(() => '?').join(', ')}, ?, ?, ?)`,
[...assetColumns.map((column) => input[column] ?? null), userId, created, created]
);
createBooks(insert.lastInsertRowid, input, body.books || []);
createBooks(insert.lastInsertRowid, input, body.books || [], input.entity_id);
run('UPDATE assets SET asset_class_number = ? WHERE id = ?', [assetClassNumberForCategory(input.category), insert.lastInsertRowid]);
return insert;
});
@@ -281,11 +286,13 @@ function createAsset(body, userId) {
return asset;
}
function updateAsset(id, body, userId) {
function updateAsset(id, body, userId, companyId) {
const before = hydrateAsset(id);
if (!before) return null;
if (companyId && before.entity_id !== companyId) return null; // belongs to another company
const input = normalizeAssetInput({ ...before, ...body });
if (companyId) input.entity_id = companyId; // assets stay within their company in phase 1
// When depreciation-critical fields change, the caller chooses when the change applies.
// 'placed_in_service' rebuilds the whole schedule by clearing prior depreciation;
// 'going_forward' keeps depreciation already recorded (the stored prior depreciation).
@@ -296,7 +303,7 @@ function updateAsset(id, body, userId) {
`UPDATE assets SET ${assetColumns.map((column) => `${column} = ?`).join(', ')}, updated_at = ? WHERE id = ?`,
[...assetColumns.map((column) => input[column] ?? null), updated, id]
);
if (Array.isArray(body.books)) createBooks(id, input, body.books);
if (Array.isArray(body.books)) createBooks(id, input, body.books, input.entity_id);
run('UPDATE assets SET asset_class_number = ? WHERE id = ?', [assetClassNumberForCategory(input.category), id]);
});
@@ -305,17 +312,23 @@ function updateAsset(id, body, userId) {
return asset;
}
function deleteAsset(id, userId) {
function deleteAsset(id, userId, companyId) {
const before = hydrateAsset(id);
if (!before) return false;
if (companyId && before.entity_id !== companyId) return false; // belongs to another company
run('DELETE FROM assets WHERE id = ?', [id]);
audit(userId, 'delete', 'asset', id, before, null);
return true;
}
function massUpdateAssets(ids, changes, userId) {
function massUpdateAssets(ids, changes, userId, companyId) {
const allowed = ['status', 'location', 'department', 'group_name', 'in_service_date', 'useful_life_months', 'condition', 'disposal_date'];
const columns = allowed.filter((column) => Object.prototype.hasOwnProperty.call(changes, column));
// Restrict to assets in the active company so a forged id list can't touch another company's data.
if (companyId && ids.length) {
const placeholders = ids.map(() => '?').join(',');
ids = all(`SELECT id FROM assets WHERE entity_id = ? AND id IN (${placeholders})`, [companyId, ...ids]).map((r) => r.id);
}
if (!ids.length || !columns.length) return 0;
tx(() => {
@@ -330,26 +343,29 @@ function massUpdateAssets(ids, changes, userId) {
return ids.length;
}
function assetExportRows() {
function assetExportRows(companyId) {
return all(`
SELECT a.*, e.name AS entity_name
FROM assets a
LEFT JOIN entities e ON e.id = a.entity_id
WHERE (? IS NULL OR a.entity_id = ?)
ORDER BY a.asset_id
`).map((asset) => ({
`, [companyId || null, companyId || null]).map((asset) => ({
...asset,
listed_property: Boolean(asset.listed_property),
custom_fields: parseJson(asset.custom_fields, {})
}));
}
function upsertImportedAssets(rows, userId) {
function upsertImportedAssets(rows, userId, companyId) {
let imported = 0;
tx(() => {
for (const row of rows) {
const input = normalizeAssetInput(row);
if (companyId) input.entity_id = companyId; // imports land in the active company
if (!input.asset_id) input.asset_id = resolveNewAssetId(input);
const existing = one('SELECT id FROM assets WHERE asset_id = ?', [input.asset_id]);
// Match within the active company so imports don't collide with another company's asset ids.
const existing = one('SELECT id FROM assets WHERE asset_id = ? AND (? IS NULL OR entity_id = ?)', [input.asset_id, companyId || null, companyId || null]);
const classNumber = assetClassNumberForCategory(input.category);
if (existing) {
run(
@@ -362,7 +378,7 @@ function upsertImportedAssets(rows, userId) {
VALUES (${assetColumns.map(() => '?').join(', ')}, ?, ?, ?)`,
[...assetColumns.map((column) => input[column] ?? null), userId, now(), now()]
);
createBooks(result.lastInsertRowid, input, []);
createBooks(result.lastInsertRowid, input, [], input.entity_id);
run('UPDATE assets SET asset_class_number = ? WHERE id = ?', [classNumber, result.lastInsertRowid]);
}
imported += 1;