feat: implement pool member management with listing and adding functionalities

This commit is contained in:
seaznCode 2026-01-23 21:54:23 +01:00
parent caa55f7827
commit fd37a522d0
8 changed files with 160 additions and 4 deletions

View File

@ -1,4 +1,5 @@
const { createPool, listPools, updatePoolState } = require('../../services/pool/PoolService'); const { createPool, listPools, updatePoolState } = require('../../services/pool/PoolService');
const PoolMemberService = require('../../services/pool/PoolMemberService');
module.exports = { module.exports = {
async create(req, res) { async create(req, res) {
@ -54,5 +55,34 @@ module.exports = {
console.error('[PoolController.updateActive]', e); console.error('[PoolController.updateActive]', e);
return res.status(500).json({ success: false, message: 'Internal server error' }); return res.status(500).json({ success: false, message: 'Internal server error' });
} }
},
async listMembers(req, res) {
try {
const { id } = req.params || {};
if (!id) return res.status(400).json({ success: false, message: 'id is required' });
const members = await PoolMemberService.listMembers(id);
return res.status(200).json({ success: true, members });
} catch (e) {
console.error('[PoolController.listMembers]', e);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
},
async addMembers(req, res) {
try {
const { id } = req.params || {};
const { userIds } = req.body || {};
const actorUserId = req.user && req.user.userId;
if (!id) return res.status(400).json({ success: false, message: 'id is required' });
if (!Array.isArray(userIds) || userIds.length === 0) {
return res.status(400).json({ success: false, message: 'userIds must be a non-empty array' });
}
await PoolMemberService.addMembers(id, userIds, actorUserId);
return res.status(200).json({ success: true });
} catch (e) {
console.error('[PoolController.addMembers]', e);
return res.status(500).json({ success: false, message: 'Internal server error' });
}
} }
}; };

View File

@ -1028,6 +1028,24 @@ const createDatabase = async () => {
`); `);
console.log('✅ Pools table created/verified'); console.log('✅ Pools table created/verified');
await connection.query(`
CREATE TABLE IF NOT EXISTS pool_members (
id INT AUTO_INCREMENT PRIMARY KEY,
pool_id INT NOT NULL,
user_id INT NOT NULL,
created_by INT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_pool_members_pool FOREIGN KEY (pool_id) REFERENCES pools(id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_pool_members_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_pool_members_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT uq_pool_members UNIQUE (pool_id, user_id),
INDEX idx_pool_members_pool (pool_id),
INDEX idx_pool_members_user (user_id)
);
`);
console.log('✅ pool_members table created/verified');
// --- user_matrix_metadata: add matrix_instance_id + alter PK --- // --- user_matrix_metadata: add matrix_instance_id + alter PK ---
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS user_matrix_metadata ( CREATE TABLE IF NOT EXISTS user_matrix_metadata (

View File

@ -1,5 +1,5 @@
class Pool { class Pool {
constructor({ id = null, pool_name, description = null, price = 0.00, pool_type = 'other', is_active = true, created_by = null, updated_by = null, created_at = null, updated_at = null }) { constructor({ id = null, pool_name, description = null, price = 0.00, pool_type = 'other', is_active = true, created_by = null, updated_by = null, created_at = null, updated_at = null, members_count = 0 }) {
this.id = id; this.id = id;
this.pool_name = pool_name; this.pool_name = pool_name;
this.description = description; this.description = description;
@ -10,6 +10,7 @@ class Pool {
this.updated_by = updated_by; this.updated_by = updated_by;
this.created_at = created_at; this.created_at = created_at;
this.updated_at = updated_at; this.updated_at = updated_at;
this.members_count = Number(members_count || 0);
} }
} }

View File

@ -0,0 +1,66 @@
const { logger } = require('../../middleware/logger');
class PoolMemberRepository {
constructor(uow) {
this.uow = uow;
}
async listMembers(poolId) {
const conn = this.uow.connection;
try {
logger.info('PoolMemberRepository.listMembers:start', { poolId });
const [rows] = await conn.execute(
`SELECT
u.id,
u.email,
u.user_type,
u.role,
pp.first_name,
pp.last_name,
cp.company_name,
pm.joined_at
FROM pool_members pm
JOIN users u ON u.id = pm.user_id
LEFT JOIN personal_profiles pp ON u.id = pp.user_id
LEFT JOIN company_profiles cp ON u.id = cp.user_id
WHERE pm.pool_id = ?
ORDER BY pm.joined_at DESC`,
[poolId]
);
logger.info('PoolMemberRepository.listMembers:success', { poolId, count: rows.length });
return rows;
} catch (error) {
logger.error('PoolMemberRepository.listMembers:error', { poolId, error: error.message });
throw error;
}
}
async addMembers(poolId, userIds, actorUserId = null) {
const conn = this.uow.connection;
if (!Array.isArray(userIds) || userIds.length === 0) return [];
try {
logger.info('PoolMemberRepository.addMembers:start', { poolId, count: userIds.length, actorUserId });
const placeholders = userIds.map(() => '(?, ?, ?)').join(', ');
const params = [];
for (const userId of userIds) {
params.push(poolId, userId, actorUserId);
}
await conn.execute(
`INSERT INTO pool_members (pool_id, user_id, created_by)
VALUES ${placeholders}
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP`,
params
);
logger.info('PoolMemberRepository.addMembers:success', { poolId, count: userIds.length });
return true;
} catch (error) {
logger.error('PoolMemberRepository.addMembers:error', { poolId, error: error.message });
throw error;
}
}
}
module.exports = PoolMemberRepository;

View File

@ -28,9 +28,14 @@ class PoolRepository {
const conn = this.uow.connection; // switched to connection const conn = this.uow.connection; // switched to connection
try { try {
console.info('PoolRepository.findAll:start'); console.info('PoolRepository.findAll:start');
const sql = `SELECT id, pool_name, description, price, pool_type, is_active, created_by, updated_by, created_at, updated_at const sql = `SELECT
FROM pools p.id, p.pool_name, p.description, p.price, p.pool_type, p.is_active,
ORDER BY created_at DESC`; p.created_by, p.updated_by, p.created_at, p.updated_at,
COUNT(pm.user_id) AS members_count
FROM pools p
LEFT JOIN pool_members pm ON pm.pool_id = p.id
GROUP BY p.id
ORDER BY p.created_at DESC`;
const [rows] = await conn.execute(sql); const [rows] = await conn.execute(sql);
console.info('PoolRepository.findAll:success', { count: rows.length }); console.info('PoolRepository.findAll:success', { count: rows.length });
return rows.map(r => new Pool(r)); return rows.map(r => new Pool(r));

View File

@ -128,6 +128,8 @@ router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixControlle
// NEW: Admin list pools // NEW: Admin list pools
router.get('/admin/pools', authMiddleware, adminOnly, PoolController.list); router.get('/admin/pools', authMiddleware, adminOnly, PoolController.list);
// NEW: Admin list pool members
router.get('/admin/pools/:id/members', authMiddleware, adminOnly, PoolController.listMembers);
// NEW: User matrices list and per-instance overview // NEW: User matrices list and per-instance overview
router.get('/matrix/me/list', authMiddleware, MatrixController.listMyMatrices); router.get('/matrix/me/list', authMiddleware, MatrixController.listMyMatrices);

View File

@ -153,6 +153,8 @@ router.post('/admin/matrix/remove-user', authMiddleware, adminOnly, MatrixContro
router.post('/admin/matrix/assign-vacancy', authMiddleware, adminOnly, MatrixController.assignVacancy); router.post('/admin/matrix/assign-vacancy', authMiddleware, adminOnly, MatrixController.assignVacancy);
// NEW: Admin create pool // NEW: Admin create pool
router.post('/admin/pools', authMiddleware, adminOnly, PoolController.create); router.post('/admin/pools', authMiddleware, adminOnly, PoolController.create);
// NEW: Admin add members to pool
router.post('/admin/pools/:id/members', authMiddleware, adminOnly, PoolController.addMembers);
// NEW: import VAT rates CSV // NEW: import VAT rates CSV
router.post('/tax/vat-rates/import', authMiddleware, adminOnly, upload.single('file'), TaxController.importVatRatesCsv); router.post('/tax/vat-rates/import', authMiddleware, adminOnly, upload.single('file'), TaxController.importVatRatesCsv);
// NEW: Admin create affiliate with logo upload // NEW: Admin create affiliate with logo upload

View File

@ -0,0 +1,32 @@
const UnitOfWork = require('../../database/UnitOfWork');
const PoolMemberRepository = require('../../repositories/pool/poolMemberRepository');
async function listMembers(poolId) {
const uow = new UnitOfWork();
try {
await uow.start();
const repo = new PoolMemberRepository(uow);
const members = await repo.listMembers(poolId);
await uow.commit();
return members;
} catch (err) {
try { await uow.rollback(err); } catch (_) {}
throw err;
}
}
async function addMembers(poolId, userIds, actorUserId) {
const uow = new UnitOfWork();
try {
await uow.start();
const repo = new PoolMemberRepository(uow);
await repo.addMembers(poolId, userIds, actorUserId);
await uow.commit();
return true;
} catch (err) {
try { await uow.rollback(err); } catch (_) {}
throw err;
}
}
module.exports = { listMembers, addMembers };