refactor: matrix stuff
This commit is contained in:
parent
f862097417
commit
e7de8ee3e0
@ -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
|
||||
};
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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;
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user