Files
B2BCall-dialer/apps/api/src/queues/queues.controller.ts
Matheus 26cc79cec2 fix(filas): adiciona edição de fila após criação
Achado real reportado pelo usuário: "na fila nao tem opcao de editar a
fila apos a criacao". queues.controller.ts só tinha create/list/delete
— nunca existiu PATCH.

UpdateQueueDto (mesmos campos do create, todos opcionais) + PATCH
/queues/:id. Tela ganhou um botão "Editar" por linha, abrindo um
formulário inline pré-preenchido (reaproveita o mesmo componente do
"Nova fila", só muda entre createQueue/updateQueue).

Testado ponta a ponta com Playwright de verdade (não só a API): abrir o
form de edição confirmando que veio pré-preenchido com os valores reais
da fila, editar nome e espera máxima, salvar, e confirmar que a tabela
atualizou com os novos valores.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
2026-08-30 20:13:03 -03:00

185 lines
6.6 KiB
TypeScript

import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
NotFoundException,
Param,
Patch,
Post,
UseGuards,
} from "@nestjs/common";
import { getPrismaClient, withTenantContext } from "@b2bcall/database";
import { recordAuditEvent, type AccessTokenClaims } from "@b2bcall/auth";
import { assertQuota } from "@b2bcall/entitlements";
import { JwtAuthGuard } from "../common/guards/jwt-auth.guard";
import { PermissionGuard } from "../common/guards/permission.guard";
import { RequirePermission } from "../common/decorators/require-permission.decorator";
import { CurrentUser } from "../common/decorators/current-user.decorator";
import { getRedisClient } from "../common/redis";
import { CreateQueueDto, UpdateQueueDto } from "./dto/create-queue.dto";
const QUEUES_SYNC_CHANNEL = "b2bcall:queues:sync";
async function notifyQueuesChanged(): Promise<void> {
await getRedisClient().publish(QUEUES_SYNC_CHANNEL, "sync");
}
@UseGuards(JwtAuthGuard, PermissionGuard)
@Controller("queues")
export class QueuesController {
@RequirePermission("queues.manage")
@Post()
async create(@CurrentUser() user: AccessTokenClaims, @Body() dto: CreateQueueDto) {
const prisma = getPrismaClient();
const tenantId = user.tenantId!;
const activeCount = await withTenantContext(prisma, tenantId, (tx) =>
tx.queue.count({ where: { tenantId, deletedAt: null } }),
);
await assertQuota(tenantId, "maxQueues", activeCount);
const queue = await withTenantContext(prisma, tenantId, (tx) =>
tx.queue.create({
data: {
tenantId,
name: dto.name,
description: dto.description,
strategy: dto.strategy ?? "LONGEST_IDLE_AGENT",
mohSound: dto.mohSound,
announceSound: dto.announceSound,
announceFrequency: dto.announceFrequency ?? 0,
maxWaitTime: dto.maxWaitTime ?? 0,
maxWaitTimeWithNoAgent: dto.maxWaitTimeWithNoAgent ?? 0,
agentNoAnswerStatus: dto.agentNoAnswerStatus,
tierRulesApply: dto.tierRulesApply ?? false,
tierRuleWaitSecond: dto.tierRuleWaitSecond ?? 300,
discardAbandonedAfter: dto.discardAbandonedAfter ?? 60,
abandonedResumeAllowed: dto.abandonedResumeAllowed ?? false,
skipAgentsWithExternalCalls: dto.skipAgentsWithExternalCalls ?? true,
recordingEnabled: dto.recordingEnabled ?? false,
},
}),
);
await recordAuditEvent(prisma, {
action: "QUEUE_CREATE",
tenantId,
userId: user.sub,
entityType: "queue",
entityId: queue.id,
after: { name: queue.name, strategy: queue.strategy },
});
await notifyQueuesChanged();
return queue;
}
@RequirePermission("queues.view")
@Get()
async list(@CurrentUser() user: AccessTokenClaims) {
const prisma = getPrismaClient();
const tenantId = user.tenantId!;
return withTenantContext(prisma, tenantId, (tx) =>
tx.queue.findMany({ where: { deletedAt: null }, orderBy: { name: "asc" } }),
);
}
@RequirePermission("queues.view")
@Get(":id")
async get(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string) {
const prisma = getPrismaClient();
const tenantId = user.tenantId!;
const queue = await withTenantContext(prisma, tenantId, (tx) =>
tx.queue.findFirst({ where: { id, deletedAt: null } }),
);
if (!queue) {
throw new NotFoundException();
}
return queue;
}
/**
* Achado real reportado pelo usuário: "na fila nao tem opcao de editar
* a fila apos a criacao" — só existia create/delete até aqui.
*/
@RequirePermission("queues.manage")
@Patch(":id")
async update(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string, @Body() dto: UpdateQueueDto) {
const prisma = getPrismaClient();
const tenantId = user.tenantId!;
const result = await withTenantContext(prisma, tenantId, (tx) =>
tx.queue.updateMany({
where: { id, tenantId, deletedAt: null },
data: {
...(dto.name !== undefined ? { name: dto.name } : {}),
...(dto.description !== undefined ? { description: dto.description } : {}),
...(dto.strategy !== undefined ? { strategy: dto.strategy } : {}),
...(dto.mohSound !== undefined ? { mohSound: dto.mohSound } : {}),
...(dto.announceSound !== undefined ? { announceSound: dto.announceSound } : {}),
...(dto.announceFrequency !== undefined ? { announceFrequency: dto.announceFrequency } : {}),
...(dto.maxWaitTime !== undefined ? { maxWaitTime: dto.maxWaitTime } : {}),
...(dto.maxWaitTimeWithNoAgent !== undefined ? { maxWaitTimeWithNoAgent: dto.maxWaitTimeWithNoAgent } : {}),
...(dto.agentNoAnswerStatus !== undefined ? { agentNoAnswerStatus: dto.agentNoAnswerStatus } : {}),
...(dto.tierRulesApply !== undefined ? { tierRulesApply: dto.tierRulesApply } : {}),
...(dto.tierRuleWaitSecond !== undefined ? { tierRuleWaitSecond: dto.tierRuleWaitSecond } : {}),
...(dto.discardAbandonedAfter !== undefined ? { discardAbandonedAfter: dto.discardAbandonedAfter } : {}),
...(dto.abandonedResumeAllowed !== undefined ? { abandonedResumeAllowed: dto.abandonedResumeAllowed } : {}),
...(dto.skipAgentsWithExternalCalls !== undefined ? { skipAgentsWithExternalCalls: dto.skipAgentsWithExternalCalls } : {}),
...(dto.recordingEnabled !== undefined ? { recordingEnabled: dto.recordingEnabled } : {}),
},
}),
);
if (result.count === 0) {
throw new NotFoundException();
}
const updated = await withTenantContext(prisma, tenantId, (tx) => tx.queue.findFirstOrThrow({ where: { id } }));
await recordAuditEvent(prisma, {
action: "QUEUE_UPDATE",
tenantId,
userId: user.sub,
entityType: "queue",
entityId: id,
after: { ...dto },
});
await notifyQueuesChanged();
return updated;
}
@RequirePermission("queues.manage")
@Delete(":id")
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@CurrentUser() user: AccessTokenClaims, @Param("id") id: string) {
const prisma = getPrismaClient();
const tenantId = user.tenantId!;
const result = await withTenantContext(prisma, tenantId, (tx) =>
tx.queue.updateMany({
where: { id, deletedAt: null },
data: { deletedAt: new Date(), enabled: false },
}),
);
if (result.count === 0) {
throw new NotFoundException();
}
await recordAuditEvent(prisma, {
action: "QUEUE_DELETE",
tenantId,
userId: user.sub,
entityType: "queue",
entityId: id,
});
await notifyQueuesChanged();
}
}