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
185 lines
6.6 KiB
TypeScript
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();
|
|
}
|
|
}
|