feat: implement pool member management with listing and adding functionalities
This commit is contained in:
parent
caa55f7827
commit
fd37a522d0
@ -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' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -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 (
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
66
repositories/pool/poolMemberRepository.js
Normal file
66
repositories/pool/poolMemberRepository.js
Normal 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;
|
||||||
@ -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));
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
32
services/pool/PoolMemberService.js
Normal file
32
services/pool/PoolMemberService.js
Normal 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 };
|
||||||
Loading…
Reference in New Issue
Block a user