REFACTOR: Modularizado agente e presença operacional
Some checks failed
Deploy Dev / deploy (push) Has been cancelled
Some checks failed
Deploy Dev / deploy (push) Has been cancelled
- Criado módulo agent para presença e notas do atendente - Removidas responsabilidades de agente do módulo admin - Movido SQL de presença e notas para repositories - Adicionados DTOs para rotas de agente - Presença e notas passam a usar usuário autenticado via JWT - Mantida compatibilidade com userId legado do frontend - Atualizada wiki backend de Admin, Agent e WhatsApp
This commit is contained in:
parent
9298f1a162
commit
41dc1d78b5
@ -4,6 +4,7 @@ import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
|
|||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { DatabaseModule } from './infra/database/database.module';
|
import { DatabaseModule } from './infra/database/database.module';
|
||||||
import { AdminModule } from './modules/admin/admin.module';
|
import { AdminModule } from './modules/admin/admin.module';
|
||||||
|
import { AgentModule } from './modules/agent/agent.module';
|
||||||
import { AttendanceModule } from './modules/attendance/attendance.module';
|
import { AttendanceModule } from './modules/attendance/attendance.module';
|
||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
import { ContactsModule } from './modules/contacts/contacts.module';
|
import { ContactsModule } from './modules/contacts/contacts.module';
|
||||||
@ -21,6 +22,7 @@ import { WhatsappModule } from './modules/whatsapp/whatsapp.module';
|
|||||||
DatabaseModule,
|
DatabaseModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
|
AgentModule,
|
||||||
AttendanceModule,
|
AttendanceModule,
|
||||||
ContactsModule,
|
ContactsModule,
|
||||||
KnowledgeBaseModule,
|
KnowledgeBaseModule,
|
||||||
|
|||||||
@ -1,24 +1,15 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AdminAccessController } from './admin-access.controller';
|
import { AdminAccessController } from './admin-access.controller';
|
||||||
import { AdminAccessService } from './admin-access.service';
|
import { AdminAccessService } from './admin-access.service';
|
||||||
import { AgentNotesController } from './agent-notes.controller';
|
|
||||||
import { AgentNotesService } from './agent-notes.service';
|
|
||||||
import { AgentPresenceController } from './agent-presence.controller';
|
|
||||||
import { AgentPresenceService } from './agent-presence.service';
|
|
||||||
import { AdminAccessRepository } from './repositories/admin-access.repository';
|
import { AdminAccessRepository } from './repositories/admin-access.repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [
|
controllers: [
|
||||||
AdminAccessController,
|
AdminAccessController,
|
||||||
AgentNotesController,
|
|
||||||
AgentPresenceController,
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
AdminAccessService,
|
AdminAccessService,
|
||||||
AdminAccessRepository,
|
AdminAccessRepository,
|
||||||
AgentNotesService,
|
|
||||||
AgentPresenceService,
|
|
||||||
],
|
],
|
||||||
exports: [AgentPresenceService],
|
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, Post, Query } from '@nestjs/common';
|
|
||||||
import { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger';
|
|
||||||
import { AgentNotesService } from './agent-notes.service';
|
|
||||||
|
|
||||||
@ApiTags('Notas do agente')
|
|
||||||
@Controller('agent/notes')
|
|
||||||
export class AgentNotesController {
|
|
||||||
constructor(private readonly agentNotesService: AgentNotesService) {}
|
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Lista notas do agente',
|
|
||||||
description: 'Retorna lembretes e anotacoes pessoais cadastradas por um agente no painel.',
|
|
||||||
})
|
|
||||||
@ApiQuery({ name: 'userId', required: true, description: 'ID do usuario dono das notas.' })
|
|
||||||
@Get()
|
|
||||||
listNotes(@Query('userId') userId: string) {
|
|
||||||
return this.agentNotesService.listNotes(Number(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Cria nota do agente',
|
|
||||||
description: 'Adiciona uma anotacao pessoal para organizacao do atendente.',
|
|
||||||
})
|
|
||||||
@ApiBody({
|
|
||||||
schema: {
|
|
||||||
type: 'object',
|
|
||||||
required: ['userId', 'text'],
|
|
||||||
properties: {
|
|
||||||
userId: { type: 'number', example: 10 },
|
|
||||||
text: { type: 'string', example: 'Retornar contato no fim do dia.' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@Post()
|
|
||||||
createNote(@Body() body: { userId: number; text: string }) {
|
|
||||||
return this.agentNotesService.createNote(Number(body.userId), body.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Remove nota do agente',
|
|
||||||
description: 'Exclui uma nota pessoal do agente validando o usuario dono da anotacao.',
|
|
||||||
})
|
|
||||||
@ApiParam({ name: 'id', description: 'ID da nota.' })
|
|
||||||
@ApiQuery({ name: 'userId', required: true, description: 'ID do usuario dono da nota.' })
|
|
||||||
@Delete(':id')
|
|
||||||
deleteNote(@Param('id') id: string, @Query('userId') userId: string) {
|
|
||||||
return this.agentNotesService.deleteNote(Number(userId), Number(id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { DatabaseService } from '../../infra/database/database.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AgentNotesService implements OnModuleInit {
|
|
||||||
constructor(private readonly database: DatabaseService) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
await this.database.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS agent_notes (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
user_id INTEGER NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE,
|
|
||||||
text TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listNotes(userId: number) {
|
|
||||||
const result = await this.database.query(
|
|
||||||
`
|
|
||||||
SELECT id, user_id, text, created_at
|
|
||||||
FROM agent_notes
|
|
||||||
WHERE user_id = $1
|
|
||||||
ORDER BY created_at DESC
|
|
||||||
`,
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createNote(userId: number, text: string) {
|
|
||||||
const result = await this.database.query(
|
|
||||||
`
|
|
||||||
INSERT INTO agent_notes (user_id, text)
|
|
||||||
VALUES ($1, $2)
|
|
||||||
RETURNING id, user_id, text, created_at
|
|
||||||
`,
|
|
||||||
[userId, text],
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNote(userId: number, noteId: number) {
|
|
||||||
await this.database.query(
|
|
||||||
`
|
|
||||||
DELETE FROM agent_notes
|
|
||||||
WHERE id = $1 AND user_id = $2
|
|
||||||
`,
|
|
||||||
[noteId, userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { ok: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
|
||||||
import { ApiBody, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
|
||||||
import { AgentPresenceService } from './agent-presence.service';
|
|
||||||
|
|
||||||
@ApiTags('Presenca do agente')
|
|
||||||
@Controller('agent/presence')
|
|
||||||
export class AgentPresenceController {
|
|
||||||
constructor(private readonly agentPresenceService: AgentPresenceService) {}
|
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Lista presenca dos agentes',
|
|
||||||
description: 'Retorna o estado atual dos agentes para acompanhamento operacional, incluindo disponibilidade e pausas.',
|
|
||||||
})
|
|
||||||
@Get()
|
|
||||||
listPresence() {
|
|
||||||
return this.agentPresenceService.listPresence();
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Consulta presenca do agente logado',
|
|
||||||
description: 'Retorna o status de presenca de um usuario especifico.',
|
|
||||||
})
|
|
||||||
@ApiQuery({ name: 'userId', required: true, description: 'ID do usuario.' })
|
|
||||||
@Get('me')
|
|
||||||
getPresence(@Query('userId') userId: string) {
|
|
||||||
return this.agentPresenceService.getPresence(Number(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Coloca agente em pausa',
|
|
||||||
description: 'Marca o agente como pausado para que ele nao receba novos atendimentos enquanto estiver indisponivel.',
|
|
||||||
})
|
|
||||||
@ApiBody({ schema: { type: 'object', required: ['userId'], properties: { userId: { type: 'number', example: 10 } } } })
|
|
||||||
@Post('pause')
|
|
||||||
pause(@Body() body: { userId: number }) {
|
|
||||||
return this.agentPresenceService.pause(Number(body.userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Retoma disponibilidade do agente',
|
|
||||||
description: 'Remove a pausa do agente e marca o usuario como disponivel novamente.',
|
|
||||||
})
|
|
||||||
@ApiBody({ schema: { type: 'object', required: ['userId'], properties: { userId: { type: 'number', example: 10 } } } })
|
|
||||||
@Post('resume')
|
|
||||||
resume(@Body() body: { userId: number }) {
|
|
||||||
return this.agentPresenceService.resume(Number(body.userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiOperation({
|
|
||||||
summary: 'Marca agente como offline',
|
|
||||||
description: 'Atualiza a presenca do agente para offline, usado em saida do painel ou encerramento de expediente.',
|
|
||||||
})
|
|
||||||
@ApiBody({ schema: { type: 'object', required: ['userId'], properties: { userId: { type: 'number', example: 10 } } } })
|
|
||||||
@Post('offline')
|
|
||||||
offline(@Body() body: { userId: number }) {
|
|
||||||
return this.agentPresenceService.offline(Number(body.userId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,260 +0,0 @@
|
|||||||
import { BadRequestException, Injectable, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { DatabaseService } from '../../infra/database/database.service';
|
|
||||||
|
|
||||||
type AgentPresenceStatus = 'available' | 'paused' | 'offline';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AgentPresenceService implements OnModuleInit {
|
|
||||||
constructor(private readonly database: DatabaseService) {}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
await this.ensureSchema();
|
|
||||||
}
|
|
||||||
|
|
||||||
async ensureSchema() {
|
|
||||||
await this.database.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS agent_presence (
|
|
||||||
user_id INTEGER PRIMARY KEY REFERENCES usuarios(id) ON DELETE CASCADE,
|
|
||||||
status VARCHAR(40) NOT NULL DEFAULT 'offline',
|
|
||||||
paused_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
await this.database.query(`
|
|
||||||
ALTER TABLE whatsapp_chat_atribuicoes
|
|
||||||
ADD COLUMN IF NOT EXISTS reserved_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL,
|
|
||||||
ADD COLUMN IF NOT EXISTS reserved_at TIMESTAMP WITH TIME ZONE,
|
|
||||||
ADD COLUMN IF NOT EXISTS pause_released_at TIMESTAMP WITH TIME ZONE;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listPresence() {
|
|
||||||
const result = await this.database.query(`
|
|
||||||
SELECT
|
|
||||||
u.id AS user_id,
|
|
||||||
u.nome,
|
|
||||||
u.email,
|
|
||||||
COALESCE(ap.status, 'offline') AS status,
|
|
||||||
ap.paused_at,
|
|
||||||
ap.last_seen_at,
|
|
||||||
ap.updated_at,
|
|
||||||
CASE
|
|
||||||
WHEN ap.status = 'paused' AND ap.paused_at IS NOT NULL
|
|
||||||
THEN FLOOR(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - ap.paused_at)))::INTEGER
|
|
||||||
ELSE 0
|
|
||||||
END AS paused_seconds,
|
|
||||||
COUNT(DISTINCT assigned.chat_id)::INTEGER AS assigned_count,
|
|
||||||
COUNT(DISTINCT reserved.chat_id)::INTEGER AS reserved_count
|
|
||||||
FROM usuarios u
|
|
||||||
LEFT JOIN agent_presence ap ON ap.user_id = u.id
|
|
||||||
LEFT JOIN whatsapp_chat_atribuicoes assigned
|
|
||||||
ON assigned.user_id = u.id
|
|
||||||
AND assigned.status = 'assigned'
|
|
||||||
LEFT JOIN whatsapp_chat_atribuicoes reserved
|
|
||||||
ON reserved.reserved_user_id = u.id
|
|
||||||
AND reserved.status = 'queued'
|
|
||||||
AND reserved.user_id IS NULL
|
|
||||||
WHERE u.ativo = TRUE
|
|
||||||
GROUP BY u.id, u.nome, u.email, ap.status, ap.paused_at, ap.last_seen_at, ap.updated_at
|
|
||||||
ORDER BY u.nome ASC
|
|
||||||
`);
|
|
||||||
|
|
||||||
return result.rows;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPresence(userId: number) {
|
|
||||||
this.assertUserId(userId);
|
|
||||||
await this.touchPresence(userId, 'available', false);
|
|
||||||
|
|
||||||
const result = await this.database.query(
|
|
||||||
`
|
|
||||||
SELECT
|
|
||||||
user_id,
|
|
||||||
status,
|
|
||||||
paused_at,
|
|
||||||
last_seen_at,
|
|
||||||
updated_at,
|
|
||||||
CASE
|
|
||||||
WHEN status = 'paused' AND paused_at IS NOT NULL
|
|
||||||
THEN FLOOR(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - paused_at)))::INTEGER
|
|
||||||
ELSE 0
|
|
||||||
END AS paused_seconds
|
|
||||||
FROM agent_presence
|
|
||||||
WHERE user_id = $1
|
|
||||||
`,
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.rows[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async pause(userId: number) {
|
|
||||||
this.assertUserId(userId);
|
|
||||||
|
|
||||||
return this.database.transaction(async (client) => {
|
|
||||||
const presence = await client.query(
|
|
||||||
`
|
|
||||||
INSERT INTO agent_presence (user_id, status, paused_at, last_seen_at, updated_at)
|
|
||||||
VALUES ($1, 'paused', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
status = 'paused',
|
|
||||||
paused_at = COALESCE(agent_presence.paused_at, CURRENT_TIMESTAMP),
|
|
||||||
last_seen_at = CURRENT_TIMESTAMP,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const released = await client.query(
|
|
||||||
`
|
|
||||||
UPDATE whatsapp_chat_atribuicoes
|
|
||||||
SET
|
|
||||||
reserved_user_id = $1,
|
|
||||||
reserved_at = CURRENT_TIMESTAMP,
|
|
||||||
pause_released_at = CURRENT_TIMESTAMP,
|
|
||||||
user_id = NULL,
|
|
||||||
status = 'queued',
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE user_id = $1
|
|
||||||
AND status = 'assigned'
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
presence: presence.rows[0],
|
|
||||||
releasedCount: released.rowCount,
|
|
||||||
releasedChats: released.rows,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async resume(userId: number) {
|
|
||||||
this.assertUserId(userId);
|
|
||||||
|
|
||||||
return this.database.transaction(async (client) => {
|
|
||||||
const presence = await client.query(
|
|
||||||
`
|
|
||||||
INSERT INTO agent_presence (user_id, status, paused_at, last_seen_at, updated_at)
|
|
||||||
VALUES ($1, 'available', NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
status = 'available',
|
|
||||||
paused_at = NULL,
|
|
||||||
last_seen_at = CURRENT_TIMESTAMP,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const restored = await client.query(
|
|
||||||
`
|
|
||||||
UPDATE whatsapp_chat_atribuicoes
|
|
||||||
SET
|
|
||||||
user_id = $1,
|
|
||||||
status = 'assigned',
|
|
||||||
assigned_at = CURRENT_TIMESTAMP,
|
|
||||||
reserved_user_id = NULL,
|
|
||||||
reserved_at = NULL,
|
|
||||||
pause_released_at = NULL,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE reserved_user_id = $1
|
|
||||||
AND status = 'queued'
|
|
||||||
AND user_id IS NULL
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
presence: presence.rows[0],
|
|
||||||
restoredCount: restored.rowCount,
|
|
||||||
restoredChats: restored.rows,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async offline(userId: number) {
|
|
||||||
this.assertUserId(userId);
|
|
||||||
|
|
||||||
return this.database.transaction(async (client) => {
|
|
||||||
const presence = await client.query(
|
|
||||||
`
|
|
||||||
INSERT INTO agent_presence (user_id, status, paused_at, last_seen_at, updated_at)
|
|
||||||
VALUES ($1, 'offline', NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
status = 'offline',
|
|
||||||
paused_at = NULL,
|
|
||||||
last_seen_at = CURRENT_TIMESTAMP,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const released = await client.query(
|
|
||||||
`
|
|
||||||
UPDATE whatsapp_chat_atribuicoes
|
|
||||||
SET
|
|
||||||
user_id = NULL,
|
|
||||||
status = 'queued',
|
|
||||||
reserved_user_id = NULL,
|
|
||||||
reserved_at = NULL,
|
|
||||||
pause_released_at = NULL,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE (user_id = $1 OR reserved_user_id = $1)
|
|
||||||
AND status IN ('assigned', 'queued')
|
|
||||||
RETURNING *
|
|
||||||
`,
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
presence: presence.rows[0],
|
|
||||||
releasedCount: released.rowCount,
|
|
||||||
releasedChats: released.rows,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async isAvailable(userId: number) {
|
|
||||||
this.assertUserId(userId);
|
|
||||||
const result = await this.database.query<{ status: AgentPresenceStatus }>(
|
|
||||||
`
|
|
||||||
SELECT COALESCE(ap.status, 'offline') AS status
|
|
||||||
FROM usuarios u
|
|
||||||
LEFT JOIN agent_presence ap ON ap.user_id = u.id
|
|
||||||
WHERE u.id = $1
|
|
||||||
AND u.ativo = TRUE
|
|
||||||
LIMIT 1
|
|
||||||
`,
|
|
||||||
[userId],
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.rows[0]?.status === 'available';
|
|
||||||
}
|
|
||||||
|
|
||||||
private async touchPresence(userId: number, fallbackStatus: AgentPresenceStatus, updateStatus = true) {
|
|
||||||
await this.database.query(
|
|
||||||
`
|
|
||||||
INSERT INTO agent_presence (user_id, status, last_seen_at, updated_at)
|
|
||||||
VALUES ($1, $2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
|
||||||
status = CASE WHEN $3 THEN EXCLUDED.status ELSE agent_presence.status END,
|
|
||||||
last_seen_at = CURRENT_TIMESTAMP,
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
|
||||||
`,
|
|
||||||
[userId, fallbackStatus, updateStatus],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private assertUserId(userId: number) {
|
|
||||||
if (!Number.isFinite(userId) || userId <= 0) {
|
|
||||||
throw new BadRequestException('Usuario invalido para controle de presenca.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
64
src/modules/agent/agent-notes.controller.ts
Normal file
64
src/modules/agent/agent-notes.controller.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common';
|
||||||
|
import { ApiBody, ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AgentNotesService } from './agent-notes.service';
|
||||||
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
|
import { AuthTokenPayload } from '../auth/auth-token.service';
|
||||||
|
import { CreateAgentNoteDto } from './dto/agent.dto';
|
||||||
|
|
||||||
|
@ApiTags('Notas do agente')
|
||||||
|
@Controller('agent/notes')
|
||||||
|
export class AgentNotesController {
|
||||||
|
constructor(private readonly agentNotesService: AgentNotesService) {}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Lista notas do agente',
|
||||||
|
description: 'Retorna lembretes e anotacoes pessoais cadastradas por um agente no painel.',
|
||||||
|
})
|
||||||
|
@Roles('Admin', 'Supervisor', 'Agente')
|
||||||
|
@ApiQuery({ name: 'userId', required: false, description: 'Mantido por compatibilidade; o backend usa o usuario autenticado no JWT.' })
|
||||||
|
@Get()
|
||||||
|
listNotes(@CurrentUser() user: AuthTokenPayload, @Query('userId') _userId?: string) {
|
||||||
|
return this.agentNotesService.listNotes(this.currentUserId(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Cria nota do agente',
|
||||||
|
description: 'Adiciona uma anotacao pessoal para organizacao do atendente.',
|
||||||
|
})
|
||||||
|
@Roles('Admin', 'Supervisor', 'Agente')
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['text'],
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'number', example: 10, description: 'Mantido por compatibilidade; ignorado pelo backend.' },
|
||||||
|
text: { type: 'string', example: 'Retornar contato no fim do dia.' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@Post()
|
||||||
|
createNote(@CurrentUser() user: AuthTokenPayload, @Body() body: CreateAgentNoteDto) {
|
||||||
|
return this.agentNotesService.createNote(this.currentUserId(user), body.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Remove nota do agente',
|
||||||
|
description: 'Exclui uma nota pessoal do agente validando o usuario dono da anotacao.',
|
||||||
|
})
|
||||||
|
@Roles('Admin', 'Supervisor', 'Agente')
|
||||||
|
@ApiParam({ name: 'id', description: 'ID da nota.' })
|
||||||
|
@ApiQuery({ name: 'userId', required: false, description: 'Mantido por compatibilidade; o backend usa o usuario autenticado no JWT.' })
|
||||||
|
@Delete(':id')
|
||||||
|
deleteNote(
|
||||||
|
@CurrentUser() user: AuthTokenPayload,
|
||||||
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
@Query('userId') _userId?: string,
|
||||||
|
) {
|
||||||
|
return this.agentNotesService.deleteNote(this.currentUserId(user), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private currentUserId(user: AuthTokenPayload) {
|
||||||
|
return Number(user.sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/modules/agent/agent-notes.service.ts
Normal file
43
src/modules/agent/agent-notes.service.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { AgentNotesRepository } from './repositories/agent-notes.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentNotesService {
|
||||||
|
constructor(private readonly agentNotesRepository: AgentNotesRepository) {}
|
||||||
|
|
||||||
|
async listNotes(userId: number) {
|
||||||
|
this.assertUserId(userId);
|
||||||
|
const result = await this.agentNotesRepository.listNotes(userId);
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNote(userId: number, text: string) {
|
||||||
|
this.assertUserId(userId);
|
||||||
|
const normalizedText = String(text || '').trim();
|
||||||
|
if (!normalizedText) {
|
||||||
|
throw new BadRequestException('Texto da nota e obrigatorio.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.agentNotesRepository.createNote(userId, normalizedText);
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteNote(userId: number, noteId: number) {
|
||||||
|
this.assertUserId(userId);
|
||||||
|
if (!Number.isFinite(noteId) || noteId <= 0) {
|
||||||
|
throw new BadRequestException('Nota invalida para exclusao.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.agentNotesRepository.deleteNote(userId, noteId);
|
||||||
|
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertUserId(userId: number) {
|
||||||
|
if (!Number.isFinite(userId) || userId <= 0) {
|
||||||
|
throw new BadRequestException('Usuario invalido para notas do agente.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/modules/agent/agent-presence.controller.ts
Normal file
71
src/modules/agent/agent-presence.controller.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
||||||
|
import { ApiBody, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AgentPresenceService } from './agent-presence.service';
|
||||||
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
|
import { Roles } from '../auth/decorators/roles.decorator';
|
||||||
|
import { AuthTokenPayload } from '../auth/auth-token.service';
|
||||||
|
import { AgentPresenceUserDto } from './dto/agent.dto';
|
||||||
|
|
||||||
|
@ApiTags('Presenca do agente')
|
||||||
|
@Controller('agent/presence')
|
||||||
|
export class AgentPresenceController {
|
||||||
|
constructor(private readonly agentPresenceService: AgentPresenceService) {}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Lista presenca dos agentes',
|
||||||
|
description: 'Retorna o estado atual dos agentes para acompanhamento operacional, incluindo disponibilidade e pausas.',
|
||||||
|
})
|
||||||
|
@Roles('Admin', 'Supervisor')
|
||||||
|
@Get()
|
||||||
|
listPresence() {
|
||||||
|
return this.agentPresenceService.listPresence();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Consulta presenca do agente logado',
|
||||||
|
description: 'Retorna o status de presenca de um usuario especifico.',
|
||||||
|
})
|
||||||
|
@Roles('Admin', 'Supervisor', 'Agente')
|
||||||
|
@ApiQuery({ name: 'userId', required: false, description: 'Mantido por compatibilidade; o backend usa o usuario autenticado no JWT.' })
|
||||||
|
@Get('me')
|
||||||
|
getPresence(@CurrentUser() user: AuthTokenPayload, @Query('userId') _userId?: string) {
|
||||||
|
return this.agentPresenceService.getPresence(this.currentUserId(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Coloca agente em pausa',
|
||||||
|
description: 'Marca o agente como pausado para que ele nao receba novos atendimentos enquanto estiver indisponivel.',
|
||||||
|
})
|
||||||
|
@Roles('Admin', 'Supervisor', 'Agente')
|
||||||
|
@ApiBody({ schema: { type: 'object', properties: { userId: { type: 'number', example: 10, description: 'Mantido por compatibilidade; ignorado pelo backend.' } } } })
|
||||||
|
@Post('pause')
|
||||||
|
pause(@CurrentUser() user: AuthTokenPayload, @Body() _body: AgentPresenceUserDto) {
|
||||||
|
return this.agentPresenceService.pause(this.currentUserId(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Retoma disponibilidade do agente',
|
||||||
|
description: 'Remove a pausa do agente e marca o usuario como disponivel novamente.',
|
||||||
|
})
|
||||||
|
@Roles('Admin', 'Supervisor', 'Agente')
|
||||||
|
@ApiBody({ schema: { type: 'object', properties: { userId: { type: 'number', example: 10, description: 'Mantido por compatibilidade; ignorado pelo backend.' } } } })
|
||||||
|
@Post('resume')
|
||||||
|
resume(@CurrentUser() user: AuthTokenPayload, @Body() _body: AgentPresenceUserDto) {
|
||||||
|
return this.agentPresenceService.resume(this.currentUserId(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Marca agente como offline',
|
||||||
|
description: 'Atualiza a presenca do agente para offline, usado em saida do painel ou encerramento de expediente.',
|
||||||
|
})
|
||||||
|
@Roles('Admin', 'Supervisor', 'Agente')
|
||||||
|
@ApiBody({ schema: { type: 'object', properties: { userId: { type: 'number', example: 10, description: 'Mantido por compatibilidade; ignorado pelo backend.' } } } })
|
||||||
|
@Post('offline')
|
||||||
|
offline(@CurrentUser() user: AuthTokenPayload, @Body() _body: AgentPresenceUserDto) {
|
||||||
|
return this.agentPresenceService.offline(this.currentUserId(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
private currentUserId(user: AuthTokenPayload) {
|
||||||
|
return Number(user.sub);
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/modules/agent/agent-presence.service.ts
Normal file
86
src/modules/agent/agent-presence.service.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { AgentPresenceRepository } from './repositories/agent-presence.repository';
|
||||||
|
|
||||||
|
type AgentPresenceStatus = 'available' | 'paused' | 'offline';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentPresenceService {
|
||||||
|
constructor(private readonly agentPresenceRepository: AgentPresenceRepository) {}
|
||||||
|
|
||||||
|
async listPresence() {
|
||||||
|
const result = await this.agentPresenceRepository.listPresence();
|
||||||
|
|
||||||
|
return result.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPresence(userId: number) {
|
||||||
|
this.assertUserId(userId);
|
||||||
|
await this.touchPresence(userId, 'available', false);
|
||||||
|
|
||||||
|
const result = await this.agentPresenceRepository.getPresence(userId);
|
||||||
|
|
||||||
|
return result.rows[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async pause(userId: number) {
|
||||||
|
this.assertUserId(userId);
|
||||||
|
|
||||||
|
return this.agentPresenceRepository.transaction(async (client) => {
|
||||||
|
const presence = await this.agentPresenceRepository.pausePresence(client, userId);
|
||||||
|
const released = await this.agentPresenceRepository.releaseAssignedChatsForPause(client, userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
presence: presence.rows[0],
|
||||||
|
releasedCount: released.rowCount,
|
||||||
|
releasedChats: released.rows,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async resume(userId: number) {
|
||||||
|
this.assertUserId(userId);
|
||||||
|
|
||||||
|
return this.agentPresenceRepository.transaction(async (client) => {
|
||||||
|
const presence = await this.agentPresenceRepository.resumePresence(client, userId);
|
||||||
|
const restored = await this.agentPresenceRepository.restoreReservedChats(client, userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
presence: presence.rows[0],
|
||||||
|
restoredCount: restored.rowCount,
|
||||||
|
restoredChats: restored.rows,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async offline(userId: number) {
|
||||||
|
this.assertUserId(userId);
|
||||||
|
|
||||||
|
return this.agentPresenceRepository.transaction(async (client) => {
|
||||||
|
const presence = await this.agentPresenceRepository.offlinePresence(client, userId);
|
||||||
|
const released = await this.agentPresenceRepository.releaseAssignedOrReservedChats(client, userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
presence: presence.rows[0],
|
||||||
|
releasedCount: released.rowCount,
|
||||||
|
releasedChats: released.rows,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAvailable(userId: number) {
|
||||||
|
this.assertUserId(userId);
|
||||||
|
const result = await this.agentPresenceRepository.getAvailability(userId);
|
||||||
|
|
||||||
|
return result.rows[0]?.status === 'available';
|
||||||
|
}
|
||||||
|
|
||||||
|
private async touchPresence(userId: number, fallbackStatus: AgentPresenceStatus, updateStatus = true) {
|
||||||
|
await this.agentPresenceRepository.touchPresence(userId, fallbackStatus, updateStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertUserId(userId: number) {
|
||||||
|
if (!Number.isFinite(userId) || userId <= 0) {
|
||||||
|
throw new BadRequestException('Usuario invalido para controle de presenca.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/modules/agent/agent.module.ts
Normal file
19
src/modules/agent/agent.module.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AgentNotesController } from './agent-notes.controller';
|
||||||
|
import { AgentNotesService } from './agent-notes.service';
|
||||||
|
import { AgentPresenceController } from './agent-presence.controller';
|
||||||
|
import { AgentPresenceService } from './agent-presence.service';
|
||||||
|
import { AgentNotesRepository } from './repositories/agent-notes.repository';
|
||||||
|
import { AgentPresenceRepository } from './repositories/agent-presence.repository';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AgentNotesController, AgentPresenceController],
|
||||||
|
providers: [
|
||||||
|
AgentNotesService,
|
||||||
|
AgentNotesRepository,
|
||||||
|
AgentPresenceService,
|
||||||
|
AgentPresenceRepository,
|
||||||
|
],
|
||||||
|
exports: [AgentPresenceService],
|
||||||
|
})
|
||||||
|
export class AgentModule {}
|
||||||
20
src/modules/agent/dto/agent.dto.ts
Normal file
20
src/modules/agent/dto/agent.dto.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsInt, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class AgentPresenceUserDto {
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
userId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateAgentNoteDto {
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
userId?: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
40
src/modules/agent/repositories/agent-notes.repository.ts
Normal file
40
src/modules/agent/repositories/agent-notes.repository.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DatabaseService } from '../../../infra/database/database.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentNotesRepository {
|
||||||
|
constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
listNotes(userId: number) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
SELECT id, user_id, text, created_at
|
||||||
|
FROM agent_notes
|
||||||
|
WHERE user_id = $1
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNote(userId: number, text: string) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
INSERT INTO agent_notes (user_id, text)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
RETURNING id, user_id, text, created_at
|
||||||
|
`,
|
||||||
|
[userId, text],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteNote(userId: number, noteId: number) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
DELETE FROM agent_notes
|
||||||
|
WHERE id = $1 AND user_id = $2
|
||||||
|
`,
|
||||||
|
[noteId, userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
202
src/modules/agent/repositories/agent-presence.repository.ts
Normal file
202
src/modules/agent/repositories/agent-presence.repository.ts
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PoolClient } from 'pg';
|
||||||
|
import { DatabaseService } from '../../../infra/database/database.service';
|
||||||
|
|
||||||
|
type AgentPresenceStatus = 'available' | 'paused' | 'offline';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AgentPresenceRepository {
|
||||||
|
constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
listPresence() {
|
||||||
|
return this.database.query(`
|
||||||
|
SELECT
|
||||||
|
u.id AS user_id,
|
||||||
|
u.nome,
|
||||||
|
u.email,
|
||||||
|
COALESCE(ap.status, 'offline') AS status,
|
||||||
|
ap.paused_at,
|
||||||
|
ap.last_seen_at,
|
||||||
|
ap.updated_at,
|
||||||
|
CASE
|
||||||
|
WHEN ap.status = 'paused' AND ap.paused_at IS NOT NULL
|
||||||
|
THEN FLOOR(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - ap.paused_at)))::INTEGER
|
||||||
|
ELSE 0
|
||||||
|
END AS paused_seconds,
|
||||||
|
COUNT(DISTINCT assigned.chat_id)::INTEGER AS assigned_count,
|
||||||
|
COUNT(DISTINCT reserved.chat_id)::INTEGER AS reserved_count
|
||||||
|
FROM usuarios u
|
||||||
|
LEFT JOIN agent_presence ap ON ap.user_id = u.id
|
||||||
|
LEFT JOIN whatsapp_chat_atribuicoes assigned
|
||||||
|
ON assigned.user_id = u.id
|
||||||
|
AND assigned.status = 'assigned'
|
||||||
|
LEFT JOIN whatsapp_chat_atribuicoes reserved
|
||||||
|
ON reserved.reserved_user_id = u.id
|
||||||
|
AND reserved.status = 'queued'
|
||||||
|
AND reserved.user_id IS NULL
|
||||||
|
WHERE u.ativo = TRUE
|
||||||
|
GROUP BY u.id, u.nome, u.email, ap.status, ap.paused_at, ap.last_seen_at, ap.updated_at
|
||||||
|
ORDER BY u.nome ASC
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPresence(userId: number) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
user_id,
|
||||||
|
status,
|
||||||
|
paused_at,
|
||||||
|
last_seen_at,
|
||||||
|
updated_at,
|
||||||
|
CASE
|
||||||
|
WHEN status = 'paused' AND paused_at IS NOT NULL
|
||||||
|
THEN FLOOR(EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - paused_at)))::INTEGER
|
||||||
|
ELSE 0
|
||||||
|
END AS paused_seconds
|
||||||
|
FROM agent_presence
|
||||||
|
WHERE user_id = $1
|
||||||
|
`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvailability(userId: number) {
|
||||||
|
return this.database.query<{ status: AgentPresenceStatus }>(
|
||||||
|
`
|
||||||
|
SELECT COALESCE(ap.status, 'offline') AS status
|
||||||
|
FROM usuarios u
|
||||||
|
LEFT JOIN agent_presence ap ON ap.user_id = u.id
|
||||||
|
WHERE u.id = $1
|
||||||
|
AND u.ativo = TRUE
|
||||||
|
LIMIT 1
|
||||||
|
`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
touchPresence(userId: number, fallbackStatus: AgentPresenceStatus, updateStatus: boolean) {
|
||||||
|
return this.database.query(
|
||||||
|
`
|
||||||
|
INSERT INTO agent_presence (user_id, status, last_seen_at, updated_at)
|
||||||
|
VALUES ($1, $2, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (user_id) DO UPDATE SET
|
||||||
|
status = CASE WHEN $3 THEN EXCLUDED.status ELSE agent_presence.status END,
|
||||||
|
last_seen_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
`,
|
||||||
|
[userId, fallbackStatus, updateStatus],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction<T>(handler: (client: PoolClient) => Promise<T>) {
|
||||||
|
return this.database.transaction(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
pausePresence(client: PoolClient, userId: number) {
|
||||||
|
return client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO agent_presence (user_id, status, paused_at, last_seen_at, updated_at)
|
||||||
|
VALUES ($1, 'paused', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (user_id) DO UPDATE SET
|
||||||
|
status = 'paused',
|
||||||
|
paused_at = COALESCE(agent_presence.paused_at, CURRENT_TIMESTAMP),
|
||||||
|
last_seen_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseAssignedChatsForPause(client: PoolClient, userId: number) {
|
||||||
|
return client.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_chat_atribuicoes
|
||||||
|
SET
|
||||||
|
reserved_user_id = $1,
|
||||||
|
reserved_at = CURRENT_TIMESTAMP,
|
||||||
|
pause_released_at = CURRENT_TIMESTAMP,
|
||||||
|
user_id = NULL,
|
||||||
|
status = 'queued',
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND status = 'assigned'
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resumePresence(client: PoolClient, userId: number) {
|
||||||
|
return client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO agent_presence (user_id, status, paused_at, last_seen_at, updated_at)
|
||||||
|
VALUES ($1, 'available', NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (user_id) DO UPDATE SET
|
||||||
|
status = 'available',
|
||||||
|
paused_at = NULL,
|
||||||
|
last_seen_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreReservedChats(client: PoolClient, userId: number) {
|
||||||
|
return client.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_chat_atribuicoes
|
||||||
|
SET
|
||||||
|
user_id = $1,
|
||||||
|
status = 'assigned',
|
||||||
|
assigned_at = CURRENT_TIMESTAMP,
|
||||||
|
reserved_user_id = NULL,
|
||||||
|
reserved_at = NULL,
|
||||||
|
pause_released_at = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE reserved_user_id = $1
|
||||||
|
AND status = 'queued'
|
||||||
|
AND user_id IS NULL
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
offlinePresence(client: PoolClient, userId: number) {
|
||||||
|
return client.query(
|
||||||
|
`
|
||||||
|
INSERT INTO agent_presence (user_id, status, paused_at, last_seen_at, updated_at)
|
||||||
|
VALUES ($1, 'offline', NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (user_id) DO UPDATE SET
|
||||||
|
status = 'offline',
|
||||||
|
paused_at = NULL,
|
||||||
|
last_seen_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseAssignedOrReservedChats(client: PoolClient, userId: number) {
|
||||||
|
return client.query(
|
||||||
|
`
|
||||||
|
UPDATE whatsapp_chat_atribuicoes
|
||||||
|
SET
|
||||||
|
user_id = NULL,
|
||||||
|
status = 'queued',
|
||||||
|
reserved_user_id = NULL,
|
||||||
|
reserved_at = NULL,
|
||||||
|
pause_released_at = NULL,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE (user_id = $1 OR reserved_user_id = $1)
|
||||||
|
AND status IN ('assigned', 'queued')
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
[userId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||||
import { AgentPresenceService } from '../admin/agent-presence.service';
|
import { AgentPresenceService } from '../agent/agent-presence.service';
|
||||||
import { AttendanceAssignmentRepository } from './repositories/attendance-assignment.repository';
|
import { AttendanceAssignmentRepository } from './repositories/attendance-assignment.repository';
|
||||||
|
|
||||||
interface TransferInput {
|
interface TransferInput {
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AdminModule } from '../admin/admin.module';
|
import { AgentModule } from '../agent/agent.module';
|
||||||
import { AttendanceAssignmentService } from './attendance-assignment.service';
|
import { AttendanceAssignmentService } from './attendance-assignment.service';
|
||||||
import { AttendanceAssignmentRepository } from './repositories/attendance-assignment.repository';
|
import { AttendanceAssignmentRepository } from './repositories/attendance-assignment.repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AdminModule],
|
imports: [AgentModule],
|
||||||
providers: [AttendanceAssignmentService, AttendanceAssignmentRepository],
|
providers: [AttendanceAssignmentService, AttendanceAssignmentRepository],
|
||||||
exports: [AttendanceAssignmentService],
|
exports: [AttendanceAssignmentService],
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user