refactor: matrix stuff

This commit is contained in:
DeathKaioken 2025-12-06 12:34:17 +01:00
parent f862097417
commit e7de8ee3e0
5 changed files with 278 additions and 20 deletions

View File

@ -197,6 +197,39 @@ async function getMyMatrixSummary(req, res) {
}
}
async function removeUser(req, res) {
try {
const { matrixInstanceId, userId } = req.body;
const data = await MatrixService.removeUser({ matrixInstanceId, userId, actorUser: req.user });
return res.json({ success: true, data });
} catch (err) {
const status = err.status || 500;
return res.status(status).json({ success: false, message: err.message || 'Could not remove user' });
}
}
async function assignVacancy(req, res) {
try {
const { matrixInstanceId, parentUserId, position, userId } = req.body;
const data = await MatrixService.assignVacancy({ matrixInstanceId, parentUserId, position, userId, actorUser: req.user });
return res.json({ success: true, data });
} catch (err) {
const status = err.status || 500;
return res.status(status).json({ success: false, message: err.message || 'Could not assign vacancy' });
}
}
async function listVacancies(req, res) {
try {
const matrixInstanceId = req.query.matrixInstanceId || req.query.matrixId || req.query.id;
const data = await MatrixService.listVacancies({ matrixInstanceId, actorUser: req.user });
return res.json({ success: true, data });
} catch (err) {
const status = err.status || 500;
return res.status(status).json({ success: false, message: err.message || 'Could not list vacancies' });
}
}
module.exports = {
create,
stats,
@ -208,5 +241,8 @@ module.exports = {
activate, // NEW
listMyMatrices, // NEW
getMyOverviewByInstance, // NEW
getMyMatrixSummary // NEW
getMyMatrixSummary, // NEW
removeUser, // NEW
assignVacancy, // NEW
listVacancies // NEW
};

View File

@ -403,19 +403,13 @@ async function getUserSearchCandidates({ q, type = 'all', rootUserId, matrixInst
async function getMatrixUsers({ rootUserId, matrixInstanceId, maxDepth = 5, limit = 100, offset = 0, includeRoot = false, rogueOnly = false }) {
const conn = await pool.getConnection();
try {
let policyDepth = null;
try {
const [pRows] = await conn.query(
`SELECT max_depth FROM matrix_instances WHERE id = ? LIMIT 1`,
[matrixInstanceId]
);
if (pRows.length) policyDepth = pRows[0].max_depth == null ? null : Number(pRows[0].max_depth);
} catch (_) {}
// Determine policy: root unlimited, ego capped at 5
let startDepth = includeRoot ? 0 : 1;
const [instRows] = await conn.query(`SELECT root_user_id FROM matrix_instances WHERE id = ? LIMIT 1`, [matrixInstanceId]);
const isRootAnchor = Number(instRows[0]?.root_user_id) === Number(rootUserId);
let requestedDepth = Number(maxDepth);
if (!Number.isFinite(requestedDepth) || requestedDepth < 0) requestedDepth = 0;
const depthLimit = policyDepth == null ? requestedDepth : Math.min(requestedDepth, policyDepth);
const startDepth = includeRoot ? 0 : 1;
if (!Number.isFinite(requestedDepth) || requestedDepth < 0) requestedDepth = isRootAnchor ? 20 : 5;
const depthLimit = isRootAnchor ? requestedDepth : Math.min(requestedDepth, 5);
if (startDepth > depthLimit) return [];
const rogueClause = rogueOnly ? 'AND (e.rogue_user = TRUE)' : '';
@ -968,6 +962,187 @@ async function activateInstance(instanceId) {
}
}
// NEW: ensure vacancy table exists
async function ensureVacancyTable(conn) {
await conn.query(`
CREATE TABLE IF NOT EXISTS matrix_vacancies (
id INT AUTO_INCREMENT PRIMARY KEY,
matrix_instance_id INT NOT NULL,
parent_user_id INT NOT NULL,
position INT NOT NULL,
depth INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_vacancy (matrix_instance_id, parent_user_id, position)
)
`);
}
// NEW: remove user and create vacancy
async function removeUserAndCreateVacancy(matrixInstanceId, userId) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const mid = Number(matrixInstanceId);
const uid = Number(userId);
if (!Number.isFinite(mid) || mid <= 0 || !Number.isFinite(uid) || uid <= 0) {
const err = new Error('Invalid parameters');
err.status = 400;
throw err;
}
const [rootRow] = await conn.query(`SELECT root_user_id FROM matrix_instances WHERE id = ? LIMIT 1`, [mid]);
if (!rootRow.length) { const err = new Error('Matrix instance not found'); err.status = 404; throw err; }
if (Number(rootRow[0].root_user_id) === uid) {
const err = new Error('Cannot remove root user');
err.status = 400;
throw err;
}
const [edgeRows] = await conn.query(
`SELECT parent_user_id, position FROM user_tree_edges WHERE matrix_instance_id = ? AND child_user_id = ? LIMIT 1`,
[mid, uid]
);
if (!edgeRows.length) { const err = new Error('User not in matrix'); err.status = 404; throw err; }
const parentId = Number(edgeRows[0].parent_user_id);
const pos = Number(edgeRows[0].position);
await ensureVacancyTable(conn);
const [depthRows] = await conn.query(
`SELECT depth FROM user_tree_closure WHERE matrix_instance_id = ? AND ancestor_user_id = ? AND descendant_user_id = ? LIMIT 1`,
[mid, rootRow[0].root_user_id, parentId]
);
const depth = Number(depthRows[0]?.depth || 0) + 1;
await conn.query(
`INSERT INTO matrix_vacancies (matrix_instance_id, parent_user_id, position, depth)
VALUES (?,?,?,?)
ON DUPLICATE KEY UPDATE depth = VALUES(depth), created_at = CURRENT_TIMESTAMP`,
[mid, parentId, pos, depth]
);
// delete subtree closure and edges
await conn.query(
`DELETE c FROM user_tree_closure c
JOIN user_tree_closure d ON d.matrix_instance_id = c.matrix_instance_id AND d.descendant_user_id = c.descendant_user_id
WHERE d.matrix_instance_id = ? AND d.ancestor_user_id = ?`,
[mid, uid]
);
await conn.query(
`DELETE FROM user_tree_edges WHERE matrix_instance_id = ? AND child_user_id IN (
SELECT descendant_user_id FROM user_tree_closure WHERE matrix_instance_id = ? AND ancestor_user_id = ?
)`,
[mid, mid, uid]
);
await conn.commit();
return { matrixInstanceId: mid, vacancy: { parentUserId: parentId, position: pos, depth } };
} catch (e) {
try { await conn.rollback(); } catch (_) {}
throw e;
} finally {
conn.release();
}
}
// NEW: list vacancies
async function listVacancies(matrixInstanceId) {
const conn = await pool.getConnection();
try {
const mid = Number(matrixInstanceId);
if (!Number.isFinite(mid) || mid <= 0) return [];
await ensureVacancyTable(conn);
const [rootRow] = await conn.query(`SELECT root_user_id FROM matrix_instances WHERE id = ? LIMIT 1`, [mid]);
const rootId = Number(rootRow[0]?.root_user_id || 0);
const [rows] = await conn.query(
`SELECT v.matrix_instance_id AS matrixInstanceId, v.parent_user_id AS parentUserId, v.position, v.depth, v.created_at AS createdAt
FROM matrix_vacancies v
WHERE v.matrix_instance_id = ?
ORDER BY v.created_at DESC`,
[mid]
);
return rows.map(r => ({
matrixInstanceId: mid,
parentUserId: Number(r.parentUserId),
position: Number(r.position),
depth: Number(r.depth),
root: rootId === Number(r.parentUserId),
createdAt: r.createdAt
}));
} finally {
conn.release();
}
}
// NEW: assign user to vacancy
async function assignUserToVacancy({ matrixInstanceId, parentUserId, position, userId }) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const mid = Number(matrixInstanceId);
const pid = Number(parentUserId);
const pos = Number(position);
const uid = Number(userId);
if (![mid, pid, pos, uid].every(n => Number.isFinite(n) && n > 0)) {
const err = new Error('Invalid parameters');
err.status = 400;
throw err;
}
await ensureVacancyTable(conn);
const [vacRows] = await conn.query(
`SELECT depth FROM matrix_vacancies WHERE matrix_instance_id = ? AND parent_user_id = ? AND position = ? LIMIT 1`,
[mid, pid, pos]
);
if (!vacRows.length) { const err = new Error('Vacancy not found'); err.status = 404; throw err; }
const depthFromParent = Number(vacRows[0].depth);
const [rootRow] = await conn.query(`SELECT root_user_id FROM matrix_instances WHERE id = ? LIMIT 1`, [mid]);
const rootId = Number(rootRow[0]?.root_user_id || 0);
const [depthRows] = await conn.query(
`SELECT depth FROM user_tree_closure WHERE matrix_instance_id = ? AND ancestor_user_id = ? AND descendant_user_id = ? LIMIT 1`,
[mid, rootId, pid]
);
const parentDepth = Number(depthRows[0]?.depth || 0);
if (pid !== rootId && parentDepth + 1 > 5) {
const err = new Error('Depth exceeds ego policy');
err.status = 400;
throw err;
}
const [inMatrix] = await conn.query(
`SELECT 1 FROM user_tree_closure WHERE matrix_instance_id = ? AND descendant_user_id = ? LIMIT 1`,
[mid, uid]
);
if (inMatrix.length) {
const err = new Error('User already in matrix');
err.status = 409;
throw err;
}
await conn.query(
`INSERT INTO user_tree_edges (matrix_instance_id, parent_user_id, child_user_id, position, rogue_user)
VALUES (?,?,?,?,FALSE)`,
[mid, pid, uid, pos]
);
await conn.query(
`INSERT IGNORE INTO user_tree_closure (matrix_instance_id, ancestor_user_id, descendant_user_id, depth)
VALUES (?,?,?,0)`,
[mid, uid, uid]
);
const [ancRows] = await conn.query(
`SELECT ancestor_user_id, depth FROM user_tree_closure WHERE matrix_instance_id = ? AND descendant_user_id = ?`,
[mid, pid]
);
if (ancRows.length) {
const values = ancRows.map(r => `(${mid}, ${Number(r.ancestor_user_id)}, ${uid}, ${Number(r.depth) + 1})`).join(',');
await conn.query(
`INSERT IGNORE INTO user_tree_closure (matrix_instance_id, ancestor_user_id, descendant_user_id, depth) VALUES ${values}`
);
}
await conn.query(
`DELETE FROM matrix_vacancies WHERE matrix_instance_id = ? AND parent_user_id = ? AND position = ?`,
[mid, pid, pos]
);
await conn.commit();
return { matrixInstanceId: mid, parentUserId: pid, position: pos, userId: uid };
} catch (e) {
try { await conn.rollback(); } catch (_) {}
throw e;
} finally {
conn.release();
}
}
module.exports = {
createMatrix,
ensureUserExistsByEmail,
@ -982,5 +1157,8 @@ module.exports = {
deactivateInstance, // NEW
activateInstance, // NEW
listInstancesForUser, // NEW
userBelongsToInstance // NEW
userBelongsToInstance, // NEW
removeUserAndCreateVacancy, // NEW
listVacancies, // NEW
assignUserToVacancy // NEW
};

View File

@ -134,8 +134,11 @@ router.get('/matrix/:id/overview', authMiddleware, MatrixController.getMyOvervie
router.get('/matrix/:id/summary', authMiddleware, MatrixController.getMyMatrixSummary);
// Tax GETs
router.get('/tax/vat-rates', authMiddleware, adminOnly, TaxController.getAllVatRates);
router.get('/tax/vat-rates', authMiddleware, TaxController.getAllVatRates);
router.get('/tax/vat-history/:countryCode', authMiddleware, adminOnly, TaxController.getVatHistory);
// NEW: Admin list vacancies for a matrix
router.get('/admin/matrix/vacancies', authMiddleware, adminOnly, MatrixController.listVacancies);
// export
module.exports = router;

View File

@ -123,6 +123,10 @@ router.post('/company-stamps', authMiddleware, adminOnly, forceCompanyForAdmin,
router.post('/admin/coffee', authMiddleware, adminOnly, upload.single('picture'), CoffeeController.create);
// NEW: add user into matrix
router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixController.addUser); // already added
// NEW: remove matrix user and create vacancy
router.post('/admin/matrix/remove-user', authMiddleware, adminOnly, MatrixController.removeUser);
// NEW: assign user to vacancy
router.post('/admin/matrix/assign-vacancy', authMiddleware, adminOnly, MatrixController.assignVacancy);
// NEW: Admin create pool
router.post('/admin/pools', authMiddleware, adminOnly, PoolController.create);
// NEW: import VAT rates CSV

View File

@ -320,9 +320,11 @@ async function getMyOverview({ userId, actorUser }) {
// Load instance policy and root user
const instanceInfo = await MatrixRepository.getInstanceInfo(mid); // helper added below
const policyDepth = instanceInfo?.max_depth == null ? 5 : Number(instanceInfo.max_depth);
const rootUserId = Number(instanceInfo?.root_user_id || 0);
const isRoot = rootUserId === uid;
const policyDepth = instanceInfo?.max_depth == null
? (isRoot ? 20 : 5)
: (isRoot ? Math.max(20, Number(instanceInfo.max_depth)) : Math.min(5, Number(instanceInfo.max_depth)));
// Fetch descendants anchored at requester
const users = await MatrixRepository.getMatrixUsers({
@ -413,7 +415,9 @@ async function listMyMatrices({ userId, actorUser }) {
const mid = Number(m.id ?? m.matrixInstanceId);
// Load instance policy and root user
const instanceInfo = await MatrixRepository.getInstanceInfo(mid);
const policyDepth = instanceInfo?.max_depth == null ? 5 : Number(instanceInfo.max_depth);
const policyDepth = instanceInfo?.max_depth == null
? (instanceInfo?.root_user_id === uid ? 20 : 5)
: (instanceInfo?.root_user_id === uid ? Math.max(20, Number(instanceInfo.max_depth)) : Math.min(5, Number(instanceInfo.max_depth)));
const users = await MatrixRepository.getMatrixUsers({
rootUserId: uid,
@ -500,9 +504,11 @@ async function getMyOverviewByInstance({ userId, matrixInstanceId, actorUser })
err.status = 404;
throw err;
}
const policyDepth = instanceInfo?.max_depth == null ? 5 : Number(instanceInfo.max_depth);
const rootUserId = Number(instanceInfo?.root_user_id || 0);
const isRoot = rootUserId === uid;
const policyDepth = instanceInfo?.max_depth == null
? (isRoot ? 20 : 5)
: (isRoot ? Math.max(20, Number(instanceInfo.max_depth)) : Math.min(5, Number(instanceInfo.max_depth)));
// Fetch descendants anchored at requester within this instance
const users = await MatrixRepository.getMatrixUsers({
@ -602,7 +608,7 @@ async function getMyMatrixSummary({ userId, matrixInstanceId, actorUser }) {
err.status = 404;
throw err;
}
const policyDepth = instanceInfo?.max_depth == null ? 5 : Number(instanceInfo.max_depth);
const policyDepth = instanceInfo?.max_depth == null ? (instanceInfo?.root_user_id === uid ? 20 : 5) : (instanceInfo?.root_user_id === uid ? Math.max(20, Number(instanceInfo.max_depth)) : Math.min(5, Number(instanceInfo.max_depth)));
// Fetch all descendants anchored at requester within this instance
const users = await MatrixRepository.getMatrixUsers({
@ -719,6 +725,34 @@ async function activate({ matrixInstanceId, matrixId, rootUserId, topNodeEmail,
};
}
// NEW: remove user and create vacancy (admin)
async function removeUser({ matrixInstanceId, userId, actorUser }) {
if (!actorUser) { const err = new Error('Unauthorized'); err.status = 401; throw err; }
if (!isAdmin(actorUser)) { const err = new Error('Forbidden: Admins only'); err.status = 403; throw err; }
const mid = Number(matrixInstanceId);
const uid = Number(userId);
if (!Number.isFinite(mid) || mid <= 0 || !Number.isFinite(uid) || uid <= 0) {
const err = new Error('Invalid parameters');
err.status = 400;
throw err;
}
return await MatrixRepository.removeUserAndCreateVacancy(mid, uid);
}
// NEW: assign user to vacancy (admin)
async function assignVacancy({ matrixInstanceId, parentUserId, position, userId, actorUser }) {
if (!actorUser) { const err = new Error('Unauthorized'); err.status = 401; throw err; }
if (!isAdmin(actorUser)) { const err = new Error('Forbidden: Admins only'); err.status = 403; throw err; }
return await MatrixRepository.assignUserToVacancy({ matrixInstanceId, parentUserId, position, userId });
}
// NEW: list vacancies (admin)
async function listVacancies({ matrixInstanceId, actorUser }) {
if (!actorUser) { const err = new Error('Unauthorized'); err.status = 401; throw err; }
if (!isAdmin(actorUser)) { const err = new Error('Forbidden: Admins only'); err.status = 403; throw err; }
return await MatrixRepository.listVacancies(matrixInstanceId);
}
module.exports = {
create,
getStats,
@ -730,5 +764,8 @@ module.exports = {
getMyOverviewByInstance, // NEW
getMyMatrixSummary, // NEW
deactivate, // NEW
activate // NEW
activate, // NEW
removeUser, // NEW
assignVacancy, // NEW
listVacancies // NEW
};