feat: implement categorized global search with RBAC

- Added /api/search endpoint with strict role-based data isolation.

- Created searchGlobal function in dataService.

- Refined header UI with an interactive, categorized search results dropdown.
This commit is contained in:
Cauê Faleiros
2026-03-09 15:25:12 -03:00
parent 000bc38712
commit c07967188a
3 changed files with 222 additions and 9 deletions

View File

@@ -457,6 +457,78 @@ apiRouter.post('/users/:id/avatar', upload.single('avatar'), async (req, res) =>
});
// --- Global Search ---
apiRouter.get('/search', async (req, res) => {
const { q } = req.query;
if (!q || q.length < 2) return res.json({ members: [], teams: [], attendances: [] });
const queryStr = `%${q}%`;
const results = { members: [], teams: [], attendances: [] };
try {
// 1. Search Members
let membersQ = 'SELECT id, name, email, slug, role, team_id, avatar_url FROM users WHERE (name LIKE ? OR email LIKE ?)';
const membersParams = [queryStr, queryStr];
if (req.user.role === 'super_admin') {
// No extra filters
} else if (req.user.role === 'admin' || req.user.role === 'owner') {
membersQ += ' AND tenant_id = ?';
membersParams.push(req.user.tenant_id);
} else if (req.user.role === 'manager') {
membersQ += ' AND tenant_id = ? AND (team_id = ? OR id = ?)';
membersParams.push(req.user.tenant_id, req.user.team_id, req.user.id);
} else {
membersQ += ' AND id = ?';
membersParams.push(req.user.id);
}
const [members] = await pool.query(membersQ, membersParams);
results.members = members;
// 2. Search Teams (only for roles above agent)
if (req.user.role !== 'agent') {
let teamsQ = 'SELECT id, name, description FROM teams WHERE name LIKE ?';
const teamsParams = [queryStr];
if (req.user.role === 'super_admin') {
// No extra filters
} else if (req.user.role === 'admin' || req.user.role === 'owner') {
teamsQ += ' AND tenant_id = ?';
teamsParams.push(req.user.tenant_id);
} else if (req.user.role === 'manager') {
teamsQ += ' AND tenant_id = ? AND id = ?';
teamsParams.push(req.user.tenant_id, req.user.team_id);
}
const [teams] = await pool.query(teamsQ, teamsParams);
results.teams = teams;
}
// 3. Search Attendances
let attendancesQ = 'SELECT a.id, a.summary, a.created_at, u.name as user_name FROM attendances a JOIN users u ON a.user_id = u.id WHERE a.summary LIKE ?';
const attendancesParams = [queryStr];
if (req.user.role === 'super_admin') {
// No extra filters
} else if (req.user.role === 'admin' || req.user.role === 'owner') {
attendancesQ += ' AND a.tenant_id = ?';
attendancesParams.push(req.user.tenant_id);
} else if (req.user.role === 'manager') {
attendancesQ += ' AND a.tenant_id = ? AND u.team_id = ?';
attendancesParams.push(req.user.tenant_id, req.user.team_id);
} else {
attendancesQ += ' AND a.user_id = ?';
attendancesParams.push(req.user.id);
}
const [attendances] = await pool.query(attendancesQ, attendancesParams);
results.attendances = attendances;
res.json(results);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// --- Attendance Routes ---
apiRouter.get('/attendances', async (req, res) => {
try {