FEAT: Múltiplos perfis para multiplas áreas/especilaliddes
All checks were successful
Deploy Dev / deploy (push) Successful in 3s

This commit is contained in:
Rafael Alves Lopes 2026-05-21 15:51:40 -03:00
parent babe525154
commit 3ae6b7e978
2 changed files with 59 additions and 10 deletions

View File

@ -41,7 +41,17 @@ export class AdminAccessController {
@Put('users/:id')
updateUserAccess(
@Param('id') id: string,
@Body() body: { perfilId?: number | null; areaId?: number | null },
@Body() body: {
perfilId?: number | null;
perfilIds?: number[];
areaId?: number | null;
especialidades?: Array<{
areaId: number;
funcao?: string | null;
principal?: boolean;
ativo?: boolean;
}>;
},
) {
return this.adminAccessService.updateUserAccess(Number(id), body);
}

View File

@ -3,7 +3,14 @@ import { DatabaseService } from '../../infra/database/database.service';
interface AccessUpdateInput {
perfilId?: number | null;
perfilIds?: number[];
areaId?: number | null;
especialidades?: Array<{
areaId: number;
funcao?: string | null;
principal?: boolean;
ativo?: boolean;
}>;
}
interface AreaInput {
@ -130,10 +137,17 @@ export class AdminAccessService {
a.ativo,
a.responsavel_usuario_id,
r.nome AS responsavel_nome,
COALESCE(
JSON_AGG(DISTINCT JSONB_BUILD_OBJECT('id', su.id, 'nome', su.nome))
FILTER (WHERE su.id IS NOT NULL),
'[]'
) AS supervisores,
COUNT(DISTINCT ua.usuario_id)::INTEGER AS members
FROM areas a
LEFT JOIN usuarios r ON r.id = a.responsavel_usuario_id
LEFT JOIN usuarios_areas ua ON ua.area_id = a.id AND ua.ativo = TRUE
LEFT JOIN usuarios_areas sua ON sua.area_id = a.id AND sua.ativo = TRUE AND sua.funcao = 'Supervisor'
LEFT JOIN usuarios su ON su.id = sua.usuario_id
GROUP BY a.id, r.nome
ORDER BY a.nome
`,
@ -156,7 +170,7 @@ export class AdminAccessService {
'[]'
) AS perfis,
COALESCE(
JSON_AGG(DISTINCT JSONB_BUILD_OBJECT('id', a.id, 'nome', a.nome, 'principal', ua.principal))
JSON_AGG(DISTINCT JSONB_BUILD_OBJECT('id', a.id, 'nome', a.nome, 'principal', ua.principal, 'funcao', ua.funcao))
FILTER (WHERE a.id IS NOT NULL AND ua.ativo = TRUE),
'[]'
) AS areas
@ -174,6 +188,7 @@ export class AdminAccessService {
const perfis = Array.isArray(user.perfis) ? user.perfis : [];
const areas = Array.isArray(user.areas) ? user.areas : [];
const primaryArea = areas.find((area) => area.principal) || areas[0] || null;
const isAdmin = perfis.some((perfil) => perfil.nome === 'Admin');
return {
id: user.id,
@ -184,7 +199,7 @@ export class AdminAccessService {
areas,
perfilPrincipal: perfis[0] || null,
areaPrincipal: primaryArea,
accessStatus: perfis.length && areas.length ? 'assigned' : 'unassigned',
accessStatus: perfis.length && (areas.length || isAdmin) ? 'assigned' : 'unassigned',
};
});
}
@ -194,22 +209,41 @@ export class AdminAccessService {
await client.query('DELETE FROM usuarios_perfis WHERE usuario_id = $1', [usuarioId]);
await client.query('DELETE FROM usuarios_areas WHERE usuario_id = $1', [usuarioId]);
if (input.perfilId) {
const perfilIds = Array.isArray(input.perfilIds)
? input.perfilIds
: input.perfilId
? [input.perfilId]
: [];
for (const perfilId of [...new Set(perfilIds.filter(Boolean))]) {
await client.query(
'INSERT INTO usuarios_perfis (usuario_id, perfil_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
[usuarioId, input.perfilId],
[usuarioId, perfilId],
);
}
if (input.areaId) {
const especialidades = Array.isArray(input.especialidades)
? input.especialidades
: input.areaId
? [{ areaId: input.areaId, funcao: 'Agente', principal: true, ativo: true }]
: [];
let hasPrincipal = false;
for (const item of especialidades) {
const areaId = Number(item.areaId);
if (!areaId) continue;
const isPrincipal = Boolean(item.principal) && !hasPrincipal;
hasPrincipal = hasPrincipal || isPrincipal;
await client.query(
`
INSERT INTO usuarios_areas (usuario_id, area_id, principal, ativo)
VALUES ($1, $2, TRUE, TRUE)
INSERT INTO usuarios_areas (usuario_id, area_id, funcao, principal, ativo)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (usuario_id, area_id)
DO UPDATE SET principal = TRUE, ativo = TRUE, updated_at = NOW()
DO UPDATE SET funcao = EXCLUDED.funcao, principal = EXCLUDED.principal, ativo = EXCLUDED.ativo, updated_at = NOW()
`,
[usuarioId, input.areaId],
[usuarioId, areaId, this.normalizeRole(item.funcao), isPrincipal, item.ativo !== false],
);
}
});
@ -298,4 +332,9 @@ export class AdminAccessService {
const text = String(value || '').trim();
return text || null;
}
private normalizeRole(value?: string | null) {
const role = String(value || '').trim();
return role === 'Supervisor' ? 'Supervisor' : 'Agente';
}
}