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

@@ -100,7 +100,7 @@ function decorate(row, totals) {
};
}
function listParts(query = {}) {
function listParts(query = {}, companyId) {
const rows = all(
`SELECT p.*,
sup.id AS supplier_id, sup.organization AS supplier_org, sup.first_name AS supplier_first, sup.last_name AS supplier_last,
@@ -111,7 +111,8 @@ function listParts(query = {}) {
SELECT part_id, SUM(quantity) AS on_hand, SUM(reserved) AS reserved, COUNT(*) AS locations
FROM part_stock GROUP BY part_id
) st ON st.part_id = p.id
WHERE (? IS NULL OR p.status = ?)
WHERE (? IS NULL OR p.entity_id = ?)
AND (? IS NULL OR p.status = ?)
AND (? IS NULL OR p.category = ?)
AND (? IS NULL OR (
p.part_number LIKE '%' || ? || '%' OR p.name LIKE '%' || ? || '%' OR
@@ -120,6 +121,7 @@ function listParts(query = {}) {
))
ORDER BY p.name`,
[
companyId || null, companyId || null,
query.status || null, query.status || null,
query.category || null, query.category || null,
query.search || null, query.search || null, query.search || null,
@@ -134,11 +136,11 @@ function listParts(query = {}) {
return rows;
}
function getPart(id) {
function getPart(id, companyId) {
const row = one(
`SELECT p.*, sup.id AS supplier_id, sup.organization AS supplier_org, sup.first_name AS supplier_first, sup.last_name AS supplier_last
FROM parts p LEFT JOIN contacts sup ON sup.id = p.supplier_contact_id WHERE p.id = ?`,
[id]
FROM parts p LEFT JOIN contacts sup ON sup.id = p.supplier_contact_id WHERE p.id = ? AND (? IS NULL OR p.entity_id = ?)`,
[id, companyId || null, companyId || null]
);
if (!row) return null;
const stock = stockRows(id);
@@ -163,28 +165,28 @@ function normalize(body) {
return input;
}
function createPart(body, userId) {
function createPart(body, userId, companyId) {
const input = normalize(body);
if (!input.part_number) throw Object.assign(new Error('A part number is required'), { status: 400 });
if (one('SELECT id FROM parts WHERE part_number = ?', [input.part_number])) {
if (one('SELECT id FROM parts WHERE part_number = ? AND entity_id = ?', [input.part_number, companyId])) {
throw Object.assign(new Error('That part number already exists'), { status: 400 });
}
const ts = now();
const cols = COLUMNS.filter((c) => input[c] !== undefined);
const result = run(
`INSERT INTO parts (${cols.join(', ')}, created_by, created_at, updated_at)
VALUES (${cols.map(() => '?').join(', ')}, ?, ?, ?)`,
[...cols.map((c) => input[c]), userId, ts, ts]
`INSERT INTO parts (entity_id, ${cols.join(', ')}, created_by, created_at, updated_at)
VALUES (?, ${cols.map(() => '?').join(', ')}, ?, ?, ?)`,
[companyId || null, ...cols.map((c) => input[c]), userId, ts, ts]
);
audit(userId, 'create', 'part', result.lastInsertRowid, null, { part_number: input.part_number });
return getPart(result.lastInsertRowid);
return getPart(result.lastInsertRowid, companyId);
}
function updatePart(id, body, userId) {
function updatePart(id, body, userId, companyId) {
const before = one('SELECT * FROM parts WHERE id = ?', [id]);
if (!before) return null;
if (!before || (companyId && before.entity_id !== companyId)) return null;
const input = normalize({ ...before, ...body });
if (input.part_number !== before.part_number && one('SELECT id FROM parts WHERE part_number = ? AND id <> ?', [input.part_number, id])) {
if (input.part_number !== before.part_number && one('SELECT id FROM parts WHERE part_number = ? AND entity_id = ? AND id <> ?', [input.part_number, before.entity_id, id])) {
throw Object.assign(new Error('That part number already exists'), { status: 400 });
}
run(
@@ -192,12 +194,12 @@ function updatePart(id, body, userId) {
[...COLUMNS.map((c) => input[c] ?? null), now(), id]
);
audit(userId, 'update', 'part', id, before, body);
return getPart(id);
return getPart(id, companyId);
}
function deletePart(id, userId) {
function deletePart(id, userId, companyId) {
const before = one('SELECT * FROM parts WHERE id = ?', [id]);
if (!before) return false;
if (!before || (companyId && before.entity_id !== companyId)) return false;
run('DELETE FROM parts WHERE id = ?', [id]);
audit(userId, 'delete', 'part', id, before, null);
return true;
@@ -205,8 +207,12 @@ function deletePart(id, userId) {
// ---- Stock locations -------------------------------------------------------
function saveStock(partId, body, userId) {
if (!one('SELECT id FROM parts WHERE id = ?', [partId])) return null;
function partInCompany(partId, companyId) {
return Boolean(one('SELECT id FROM parts WHERE id = ? AND (? IS NULL OR entity_id = ?)', [partId, companyId || null, companyId || null]));
}
function saveStock(partId, body, userId, companyId) {
if (!partInCompany(partId, companyId)) return null;
const ts = now();
const location = body.location_contact_id || null;
const aisle = body.aisle || null;
@@ -228,20 +234,21 @@ function saveStock(partId, body, userId) {
);
audit(userId, 'create', 'part_stock', result.lastInsertRowid, null, { part_id: partId });
}
return getPart(partId);
return getPart(partId, companyId);
}
function deleteStock(partId, stockId, userId) {
function deleteStock(partId, stockId, userId, companyId) {
if (!partInCompany(partId, companyId)) return null;
const result = run('DELETE FROM part_stock WHERE id = ? AND part_id = ?', [stockId, partId]);
if (!result.changes) return null;
audit(userId, 'delete', 'part_stock', stockId, { part_id: partId }, null);
return getPart(partId);
return getPart(partId, companyId);
}
// ---- Stock movements -------------------------------------------------------
function recordTransaction(partId, body, userId) {
if (!one('SELECT id FROM parts WHERE id = ?', [partId])) return null;
function recordTransaction(partId, body, userId, companyId) {
if (!partInCompany(partId, companyId)) return null;
const type = TRANSACTION_TYPES.includes(body.type) ? body.type : 'receive';
const stock = one('SELECT * FROM part_stock WHERE id = ? AND part_id = ?', [body.stock_id, partId]);
if (!stock) throw Object.assign(new Error('Select a stock location for this movement'), { status: 400 });
@@ -272,28 +279,29 @@ function recordTransaction(partId, body, userId) {
]
);
audit(userId, 'movement', 'part', partId, null, { type, quantity: qty, stock_id: stock.id });
return getPart(partId);
return getPart(partId, companyId);
});
}
// ---- Photos ----------------------------------------------------------------
function addPhoto(partId, body, userId) {
if (!one('SELECT id FROM parts WHERE id = ?', [partId])) return null;
function addPhoto(partId, body, userId, companyId) {
if (!partInCompany(partId, companyId)) return null;
if (!body.data) throw Object.assign(new Error('Photo data is required'), { status: 400 });
run(
'INSERT INTO part_photos (part_id, name, mime_type, data_base64, created_at) VALUES (?, ?, ?, ?, ?)',
[partId, body.name || null, body.mime || body.mime_type || 'image/jpeg', body.data, now()]
);
audit(userId, 'create', 'part_photo', partId, null, { name: body.name });
return getPart(partId);
return getPart(partId, companyId);
}
function deletePhoto(partId, photoId, userId) {
function deletePhoto(partId, photoId, userId, companyId) {
if (!partInCompany(partId, companyId)) return null;
const result = run('DELETE FROM part_photos WHERE id = ? AND part_id = ?', [photoId, partId]);
if (!result.changes) return null;
audit(userId, 'delete', 'part_photo', photoId, { part_id: partId }, null);
return getPart(partId);
return getPart(partId, companyId);
}
module.exports = {