FEAT: adiciona endpoints administrativos de overview e áreas
All checks were successful
Deploy Dev / deploy (push) Successful in 3s
All checks were successful
Deploy Dev / deploy (push) Successful in 3s
- cria endpoint de overview com métricas reais do mês - adiciona listagem de áreas com responsável e total de usuários - permite criar novas áreas - permite alterar responsável de uma área - promove responsável a supervisor da área automaticamente
This commit is contained in:
parent
da17cbda2d
commit
babe525154
@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common';
|
||||
import { AdminAccessService } from './admin-access.service';
|
||||
|
||||
@Controller('admin/access')
|
||||
@ -10,6 +10,29 @@ export class AdminAccessController {
|
||||
return this.adminAccessService.getOptions();
|
||||
}
|
||||
|
||||
@Get('overview')
|
||||
getOverview() {
|
||||
return this.adminAccessService.getOverview();
|
||||
}
|
||||
|
||||
@Get('areas')
|
||||
listAreas() {
|
||||
return this.adminAccessService.listAreas();
|
||||
}
|
||||
|
||||
@Post('areas')
|
||||
createArea(@Body() body: { nome: string; descricao?: string | null; responsavelUsuarioId?: number | null }) {
|
||||
return this.adminAccessService.createArea(body);
|
||||
}
|
||||
|
||||
@Put('areas/:id')
|
||||
updateArea(
|
||||
@Param('id') id: string,
|
||||
@Body() body: { nome?: string; descricao?: string | null; responsavelUsuarioId?: number | null; ativo?: boolean },
|
||||
) {
|
||||
return this.adminAccessService.updateArea(Number(id), body);
|
||||
}
|
||||
|
||||
@Get('users')
|
||||
listUsers() {
|
||||
return this.adminAccessService.listUsers();
|
||||
|
||||
@ -6,6 +6,13 @@ interface AccessUpdateInput {
|
||||
areaId?: number | null;
|
||||
}
|
||||
|
||||
interface AreaInput {
|
||||
nome?: string;
|
||||
descricao?: string | null;
|
||||
responsavelUsuarioId?: number | null;
|
||||
ativo?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AdminAccessService {
|
||||
constructor(private readonly database: DatabaseService) {}
|
||||
@ -26,6 +33,115 @@ export class AdminAccessService {
|
||||
};
|
||||
}
|
||||
|
||||
async getOverview() {
|
||||
const [
|
||||
currentMonth,
|
||||
previousMonth,
|
||||
activeUsers,
|
||||
totalUsers,
|
||||
channels,
|
||||
avgHandling,
|
||||
avgFirstResponse,
|
||||
] = await Promise.all([
|
||||
this.database.query<{ total: string }>(
|
||||
`
|
||||
SELECT COUNT(*)::TEXT AS total
|
||||
FROM whatsapp_chat_atribuicoes
|
||||
WHERE conversation_started_at >= date_trunc('month', CURRENT_DATE)
|
||||
`,
|
||||
),
|
||||
this.database.query<{ total: string }>(
|
||||
`
|
||||
SELECT COUNT(*)::TEXT AS total
|
||||
FROM whatsapp_chat_atribuicoes
|
||||
WHERE conversation_started_at >= date_trunc('month', CURRENT_DATE) - INTERVAL '1 month'
|
||||
AND conversation_started_at < date_trunc('month', CURRENT_DATE)
|
||||
`,
|
||||
),
|
||||
this.database.query<{ total: string }>(
|
||||
`
|
||||
SELECT COUNT(DISTINCT user_id)::TEXT AS total
|
||||
FROM whatsapp_chat_atribuicoes
|
||||
WHERE user_id IS NOT NULL
|
||||
AND conversation_started_at >= date_trunc('month', CURRENT_DATE)
|
||||
`,
|
||||
),
|
||||
this.database.query<{ total: string }>('SELECT COUNT(*)::TEXT AS total FROM usuarios WHERE ativo = TRUE'),
|
||||
this.database.query<{ whatsapp: string; email: string; sms: string }>(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*)::TEXT AS whatsapp,
|
||||
'0'::TEXT AS email,
|
||||
'0'::TEXT AS sms
|
||||
FROM whatsapp_chat_atribuicoes
|
||||
WHERE conversation_started_at >= date_trunc('month', CURRENT_DATE)
|
||||
`,
|
||||
),
|
||||
this.database.query<{ minutes: string | null }>(
|
||||
`
|
||||
SELECT ROUND(AVG(EXTRACT(EPOCH FROM (updated_at - assigned_at))) / 60)::TEXT AS minutes
|
||||
FROM whatsapp_chat_atribuicoes
|
||||
WHERE assigned_at IS NOT NULL
|
||||
AND updated_at IS NOT NULL
|
||||
AND status IN ('assigned', 'queued', 'expired')
|
||||
AND conversation_started_at >= date_trunc('month', CURRENT_DATE)
|
||||
`,
|
||||
),
|
||||
this.database.query<{ minutes: string | null }>(
|
||||
`
|
||||
SELECT ROUND(AVG(EXTRACT(EPOCH FROM (assigned_at - conversation_started_at))) / 60)::TEXT AS minutes
|
||||
FROM whatsapp_chat_atribuicoes
|
||||
WHERE assigned_at IS NOT NULL
|
||||
AND conversation_started_at IS NOT NULL
|
||||
AND conversation_started_at >= date_trunc('month', CURRENT_DATE)
|
||||
`,
|
||||
),
|
||||
]);
|
||||
|
||||
const currentTotal = Number(currentMonth.rows[0]?.total || 0);
|
||||
const previousTotal = Number(previousMonth.rows[0]?.total || 0);
|
||||
const variation = previousTotal
|
||||
? Math.round(((currentTotal - previousTotal) / previousTotal) * 100)
|
||||
: null;
|
||||
|
||||
return {
|
||||
totalAttendances: currentTotal,
|
||||
previousMonthVariation: variation,
|
||||
activeAttendants: Number(activeUsers.rows[0]?.total || 0),
|
||||
totalActiveUsers: Number(totalUsers.rows[0]?.total || 0),
|
||||
channels: {
|
||||
whatsapp: Number(channels.rows[0]?.whatsapp || 0),
|
||||
email: Number(channels.rows[0]?.email || 0),
|
||||
sms: Number(channels.rows[0]?.sms || 0),
|
||||
},
|
||||
avgHandlingMinutes: avgHandling.rows[0]?.minutes ? Number(avgHandling.rows[0].minutes) : null,
|
||||
avgFirstResponseMinutes: avgFirstResponse.rows[0]?.minutes ? Number(avgFirstResponse.rows[0].minutes) : null,
|
||||
satisfactionRate: null,
|
||||
};
|
||||
}
|
||||
|
||||
async listAreas() {
|
||||
const result = await this.database.query(
|
||||
`
|
||||
SELECT
|
||||
a.id,
|
||||
a.nome,
|
||||
a.descricao,
|
||||
a.ativo,
|
||||
a.responsavel_usuario_id,
|
||||
r.nome AS responsavel_nome,
|
||||
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
|
||||
GROUP BY a.id, r.nome
|
||||
ORDER BY a.nome
|
||||
`,
|
||||
);
|
||||
|
||||
return result.rows;
|
||||
}
|
||||
|
||||
async listUsers() {
|
||||
const result = await this.database.query(
|
||||
`
|
||||
@ -100,4 +216,86 @@ export class AdminAccessService {
|
||||
|
||||
return this.listUsers().then((users) => users.find((user) => user.id === usuarioId));
|
||||
}
|
||||
|
||||
async createArea(input: AreaInput) {
|
||||
const nome = String(input.nome || '').trim();
|
||||
if (!nome) {
|
||||
throw new Error('Nome da area e obrigatorio');
|
||||
}
|
||||
|
||||
const result = await this.database.query(
|
||||
`
|
||||
INSERT INTO areas (nome, descricao, responsavel_usuario_id, ativo, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, TRUE, NOW(), NOW())
|
||||
RETURNING *
|
||||
`,
|
||||
[nome, this.normalizeText(input.descricao), input.responsavelUsuarioId || null],
|
||||
);
|
||||
|
||||
if (input.responsavelUsuarioId) {
|
||||
await this.ensureAreaSupervisor(input.responsavelUsuarioId, result.rows[0].id);
|
||||
}
|
||||
|
||||
return this.listAreas();
|
||||
}
|
||||
|
||||
async updateArea(areaId: number, input: AreaInput) {
|
||||
const result = await this.database.query(
|
||||
`
|
||||
UPDATE areas
|
||||
SET
|
||||
nome = COALESCE($2, nome),
|
||||
descricao = $3,
|
||||
responsavel_usuario_id = $4,
|
||||
ativo = COALESCE($5, ativo),
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
`,
|
||||
[
|
||||
areaId,
|
||||
input.nome ? String(input.nome).trim() : null,
|
||||
this.normalizeText(input.descricao),
|
||||
input.responsavelUsuarioId || null,
|
||||
typeof input.ativo === 'boolean' ? input.ativo : null,
|
||||
],
|
||||
);
|
||||
|
||||
if (input.responsavelUsuarioId && result.rows[0]) {
|
||||
await this.ensureAreaSupervisor(input.responsavelUsuarioId, areaId);
|
||||
}
|
||||
|
||||
return this.listAreas();
|
||||
}
|
||||
|
||||
private async ensureAreaSupervisor(usuarioId: number, areaId: number) {
|
||||
const supervisorProfile = await this.database.query<{ id: number }>(
|
||||
`SELECT id FROM perfis_acesso WHERE nome = 'Supervisor' LIMIT 1`,
|
||||
);
|
||||
const perfilId = supervisorProfile.rows[0]?.id;
|
||||
|
||||
await this.database.transaction(async (client) => {
|
||||
if (perfilId) {
|
||||
await client.query(
|
||||
`INSERT INTO usuarios_perfis (usuario_id, perfil_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`,
|
||||
[usuarioId, perfilId],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO usuarios_areas (usuario_id, area_id, funcao, principal, ativo)
|
||||
VALUES ($1, $2, 'Supervisor', TRUE, TRUE)
|
||||
ON CONFLICT (usuario_id, area_id)
|
||||
DO UPDATE SET funcao = 'Supervisor', principal = TRUE, ativo = TRUE, updated_at = NOW()
|
||||
`,
|
||||
[usuarioId, areaId],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeText(value?: string | null) {
|
||||
const text = String(value || '').trim();
|
||||
return text || null;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user