From fd37a522d0a496c9f28736e06b9285b0783d2431 Mon Sep 17 00:00:00 2001 From: seaznCode Date: Fri, 23 Jan 2026 21:54:23 +0100 Subject: [PATCH] feat: implement pool member management with listing and adding functionalities --- controller/pool/PoolController.js | 30 +++++++++++ database/createDb.js | 18 +++++++ models/Pool.js | 3 +- repositories/pool/poolMemberRepository.js | 66 +++++++++++++++++++++++ repositories/pool/poolRepository.js | 11 ++-- routes/getRoutes.js | 2 + routes/postRoutes.js | 2 + services/pool/PoolMemberService.js | 32 +++++++++++ 8 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 repositories/pool/poolMemberRepository.js create mode 100644 services/pool/PoolMemberService.js diff --git a/controller/pool/PoolController.js b/controller/pool/PoolController.js index 0621239..bf01839 100644 --- a/controller/pool/PoolController.js +++ b/controller/pool/PoolController.js @@ -1,4 +1,5 @@ const { createPool, listPools, updatePoolState } = require('../../services/pool/PoolService'); +const PoolMemberService = require('../../services/pool/PoolMemberService'); module.exports = { async create(req, res) { @@ -54,5 +55,34 @@ module.exports = { console.error('[PoolController.updateActive]', e); 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' }); + } } }; \ No newline at end of file diff --git a/database/createDb.js b/database/createDb.js index c5c78fa..2968827 100644 --- a/database/createDb.js +++ b/database/createDb.js @@ -1028,6 +1028,24 @@ const createDatabase = async () => { `); 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 --- await connection.query(` CREATE TABLE IF NOT EXISTS user_matrix_metadata ( diff --git a/models/Pool.js b/models/Pool.js index dc58594..eb3eb89 100644 --- a/models/Pool.js +++ b/models/Pool.js @@ -1,5 +1,5 @@ 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.pool_name = pool_name; this.description = description; @@ -10,6 +10,7 @@ class Pool { this.updated_by = updated_by; this.created_at = created_at; this.updated_at = updated_at; + this.members_count = Number(members_count || 0); } } diff --git a/repositories/pool/poolMemberRepository.js b/repositories/pool/poolMemberRepository.js new file mode 100644 index 0000000..daa8339 --- /dev/null +++ b/repositories/pool/poolMemberRepository.js @@ -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; diff --git a/repositories/pool/poolRepository.js b/repositories/pool/poolRepository.js index 947d459..d96b7b5 100644 --- a/repositories/pool/poolRepository.js +++ b/repositories/pool/poolRepository.js @@ -28,9 +28,14 @@ class PoolRepository { const conn = this.uow.connection; // switched to connection try { 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 - FROM pools - ORDER BY created_at DESC`; + const sql = `SELECT + p.id, p.pool_name, p.description, p.price, p.pool_type, p.is_active, + 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); console.info('PoolRepository.findAll:success', { count: rows.length }); return rows.map(r => new Pool(r)); diff --git a/routes/getRoutes.js b/routes/getRoutes.js index ef21c5b..8c5777d 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -128,6 +128,8 @@ router.post('/admin/matrix/add-user', authMiddleware, adminOnly, MatrixControlle // NEW: Admin list pools 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 router.get('/matrix/me/list', authMiddleware, MatrixController.listMyMatrices); diff --git a/routes/postRoutes.js b/routes/postRoutes.js index 2a493f3..313db69 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -153,6 +153,8 @@ router.post('/admin/matrix/remove-user', authMiddleware, adminOnly, MatrixContro router.post('/admin/matrix/assign-vacancy', authMiddleware, adminOnly, MatrixController.assignVacancy); // NEW: Admin create pool 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 router.post('/tax/vat-rates/import', authMiddleware, adminOnly, upload.single('file'), TaxController.importVatRatesCsv); // NEW: Admin create affiliate with logo upload diff --git a/services/pool/PoolMemberService.js b/services/pool/PoolMemberService.js new file mode 100644 index 0000000..e7908e9 --- /dev/null +++ b/services/pool/PoolMemberService.js @@ -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 };