Replace notifications with activity feed
This commit is contained in:
@@ -15,7 +15,9 @@ const { createAuthRouter } = require('./routes/authRoutes');
|
||||
const { createUsersRouter } = require('./routes/usersRoutes');
|
||||
const { createTeamsRouter } = require('./routes/teamsRoutes');
|
||||
const { createTenantsRouter } = require('./routes/tenantsRoutes');
|
||||
const { createActivityRouter } = require('./routes/activityRoutes');
|
||||
const { canReadAttendance } = require('./policies/accessPolicy');
|
||||
const { recordActivity } = require('./services/activityService');
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -78,66 +80,7 @@ apiRouter.use(createTeamsRouter({ pool }));
|
||||
|
||||
apiRouter.use(createTenantsRouter({ pool, transporter, getBaseUrl }));
|
||||
|
||||
// --- Notifications Routes ---
|
||||
apiRouter.get('/notifications', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 50',
|
||||
[req.user.id]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.put('/notifications/read-all', async (req, res) => {
|
||||
try {
|
||||
await pool.query(
|
||||
'UPDATE notifications SET is_read = true WHERE user_id = ?',
|
||||
[req.user.id]
|
||||
);
|
||||
res.json({ message: 'All notifications marked as read' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.put('/notifications/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.query(
|
||||
'UPDATE notifications SET is_read = true WHERE id = ? AND user_id = ?',
|
||||
[req.params.id, req.user.id]
|
||||
);
|
||||
res.json({ message: 'Notification marked as read' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.delete('/notifications/clear-all', async (req, res) => {
|
||||
try {
|
||||
await pool.query(
|
||||
'DELETE FROM notifications WHERE user_id = ?',
|
||||
[req.user.id]
|
||||
);
|
||||
res.json({ message: 'All notifications deleted' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
apiRouter.delete('/notifications/:id', async (req, res) => {
|
||||
try {
|
||||
await pool.query(
|
||||
'DELETE FROM notifications WHERE id = ? AND user_id = ?',
|
||||
[req.params.id, req.user.id]
|
||||
);
|
||||
res.json({ message: 'Notification deleted' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
apiRouter.use(createActivityRouter({ pool }));
|
||||
|
||||
// --- Origin Routes (Groups & Items) ---
|
||||
apiRouter.get('/origins', async (req, res) => {
|
||||
@@ -719,10 +662,13 @@ apiRouter.post('/integration/attendances', requireRole(['admin']), async (req, r
|
||||
const agentName = agentInfo[0]?.name || 'Um agente';
|
||||
|
||||
for (const m of managers) {
|
||||
await pool.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), m.id, 'success', 'Venda Fechada!', `${agentName} converteu um lead em ${funnel_stage}.`, `/attendances/${attId}`]
|
||||
);
|
||||
await recordActivity(pool, {
|
||||
userId: m.id,
|
||||
type: 'success',
|
||||
title: 'Venda Fechada!',
|
||||
message: `${agentName} converteu um lead em ${funnel_stage}.`,
|
||||
link: `/attendances/${attId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,13 +757,6 @@ const provisionSuperAdmin = async (retries = 10, delay = 10000) => {
|
||||
console.log('Schema update note (populate slugs):', err.message);
|
||||
}
|
||||
|
||||
// Add sound_enabled column if it doesn't exist
|
||||
try {
|
||||
await connection.query('ALTER TABLE users ADD COLUMN sound_enabled BOOLEAN DEFAULT true');
|
||||
} catch (err) {
|
||||
if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (sound_enabled):', err.message);
|
||||
}
|
||||
|
||||
// Update origin to VARCHAR for custom origins
|
||||
try {
|
||||
await connection.query("ALTER TABLE attendances MODIFY COLUMN origin VARCHAR(255) NOT NULL");
|
||||
|
||||
30
backend/routes/activityRoutes.js
Normal file
30
backend/routes/activityRoutes.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const express = require('express');
|
||||
|
||||
const createActivityRouter = ({ pool }) => {
|
||||
const router = express.Router();
|
||||
|
||||
const listActivity = async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, type, title, message, link, created_at
|
||||
FROM notifications
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 50`,
|
||||
[req.user.id]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
router.get('/activity', listActivity);
|
||||
router.get('/notifications', listActivity);
|
||||
|
||||
return router;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createActivityRouter,
|
||||
};
|
||||
@@ -4,6 +4,7 @@ const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const { hashSecret, maskSecret } = require('../utils/security');
|
||||
const { requireRole } = require('../middleware/auth');
|
||||
const { recordActivity } = require('../services/activityService');
|
||||
|
||||
const createAuthRouter = ({ pool, transporter, getBaseUrl, jwtSecret }) => {
|
||||
const router = express.Router();
|
||||
@@ -229,19 +230,25 @@ const createAuthRouter = ({ pool, transporter, getBaseUrl, jwtSecret }) => {
|
||||
[user.tenant_id, user.id]
|
||||
);
|
||||
for (const n of notifiable) {
|
||||
await pool.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), n.id, 'info', 'Novo Membro Ativo', `${name} concluiu o cadastro e já pode acessar o sistema.`, `/users/${user.id}`]
|
||||
);
|
||||
await recordActivity(pool, {
|
||||
userId: n.id,
|
||||
type: 'info',
|
||||
title: 'Novo Membro Ativo',
|
||||
message: `${name} concluiu o cadastro e já pode acessar o sistema.`,
|
||||
link: `/users/${user.id}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (user.role === 'admin') {
|
||||
const [superAdmins] = await pool.query("SELECT id FROM users WHERE role = 'super_admin'");
|
||||
for (const sa of superAdmins) {
|
||||
await pool.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), sa.id, 'success', 'Admin Ativo', `O admin ${name} da organização configurou sua conta.`, `/super-admin`]
|
||||
);
|
||||
await recordActivity(pool, {
|
||||
userId: sa.id,
|
||||
type: 'success',
|
||||
title: 'Admin Ativo',
|
||||
message: `O admin ${name} da organização configurou sua conta.`,
|
||||
link: '/super-admin',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const crypto = require('crypto');
|
||||
const { requireRole } = require('../middleware/auth');
|
||||
const { recordActivity } = require('../services/activityService');
|
||||
|
||||
const createTenantsRouter = ({ pool, transporter, getBaseUrl }) => {
|
||||
const router = express.Router();
|
||||
@@ -37,10 +38,13 @@ const createTenantsRouter = ({ pool, transporter, getBaseUrl }) => {
|
||||
|
||||
const [superAdmins] = await connection.query("SELECT id FROM users WHERE role = 'super_admin'");
|
||||
for (const sa of superAdmins) {
|
||||
await connection.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), sa.id, 'success', 'Nova Organização', `A organização ${name} foi criada.`, '/super-admin']
|
||||
);
|
||||
await recordActivity(connection, {
|
||||
userId: sa.id,
|
||||
type: 'success',
|
||||
title: 'Nova Organização',
|
||||
message: `A organização ${name} foi criada.`,
|
||||
link: '/super-admin',
|
||||
});
|
||||
}
|
||||
|
||||
await transporter.sendMail({
|
||||
|
||||
@@ -8,8 +8,9 @@ const {
|
||||
canChangeUserEmail,
|
||||
canManageUserRoleOrTeam,
|
||||
} = require('../policies/accessPolicy');
|
||||
const { recordActivity } = require('../services/activityService');
|
||||
|
||||
const USER_PUBLIC_FIELDS = 'id, tenant_id, team_id, name, email, slug, role, status, bio, avatar_url, sound_enabled, created_at';
|
||||
const USER_PUBLIC_FIELDS = 'id, tenant_id, team_id, name, email, slug, role, status, bio, avatar_url, created_at';
|
||||
|
||||
const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
|
||||
const router = express.Router();
|
||||
@@ -118,7 +119,7 @@ const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
|
||||
});
|
||||
|
||||
router.put('/users/:id', async (req, res) => {
|
||||
const { name, bio, role, team_id, status, email, sound_enabled } = req.body;
|
||||
const { name, bio, role, team_id, status, email } = req.body;
|
||||
try {
|
||||
const [existing] = await pool.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
|
||||
if (existing.length === 0) return res.status(404).json({ error: 'Not found' });
|
||||
@@ -129,7 +130,6 @@ const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
|
||||
const finalTeamId = canManageUserRoleOrTeam(req.user) && team_id !== undefined ? team_id : existing[0].team_id;
|
||||
const finalStatus = canManageUserStatus(req.user) && status !== undefined ? status : existing[0].status;
|
||||
const finalEmail = canChangeUserEmail(req.user, existing[0]) && email !== undefined ? email : existing[0].email;
|
||||
const finalSoundEnabled = req.user.id === req.params.id && sound_enabled !== undefined ? sound_enabled : (existing[0].sound_enabled ?? true);
|
||||
|
||||
if (finalEmail !== existing[0].email) {
|
||||
const [emailCheck] = await pool.query('SELECT id FROM users WHERE email = ? AND id != ?', [finalEmail, req.params.id]);
|
||||
@@ -137,17 +137,20 @@ const createUsersRouter = ({ pool, upload, transporter, getBaseUrl }) => {
|
||||
}
|
||||
|
||||
await pool.query(
|
||||
'UPDATE users SET name = ?, bio = ?, email = ?, role = ?, team_id = ?, status = ?, sound_enabled = ? WHERE id = ?',
|
||||
[name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalEmail, finalRole, finalTeamId || null, finalStatus, finalSoundEnabled, req.params.id]
|
||||
'UPDATE users SET name = ?, bio = ?, email = ?, role = ?, team_id = ?, status = ? WHERE id = ?',
|
||||
[name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalEmail, finalRole, finalTeamId || null, finalStatus, req.params.id]
|
||||
);
|
||||
|
||||
if (finalTeamId && finalTeamId !== existing[0].team_id && existing[0].status === 'active') {
|
||||
const [team] = await pool.query('SELECT name FROM teams WHERE id = ?', [finalTeamId]);
|
||||
if (team.length > 0) {
|
||||
await pool.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), req.params.id, 'info', 'Novo Time', `Você foi adicionado ao time ${team[0].name}.`, '/']
|
||||
);
|
||||
await recordActivity(pool, {
|
||||
userId: req.params.id,
|
||||
type: 'info',
|
||||
title: 'Novo Time',
|
||||
message: `Você foi adicionado ao time ${team[0].name}.`,
|
||||
link: '/',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
backend/services/activityService.js
Normal file
14
backend/services/activityService.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
const recordActivity = async (db, { userId, type = 'info', title, message, link = null }) => {
|
||||
if (!userId || !title || !message) return;
|
||||
|
||||
await db.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[crypto.randomUUID(), userId, type, title, message, link]
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
recordActivity,
|
||||
};
|
||||
Reference in New Issue
Block a user