feat(rotas-entrada): dropdown de destino real (ramal/IVR/fila/grupo)
Pedido do usuário: "em vez de o campo de destino ser aberto tem que ter um dropdown listando todos os ramais do tennant e tb todas ivr e filas e grupos de ramais do tennant" — até aqui o destino era texto livre. Construir o dropdown revelou que fila e grupo NUNCA tinham sido implementados de verdade como destino possível — só ramal/IVR funcionavam. Oferecer as duas opções sem o mecanismo por trás seria mostrar um dropdown mentiroso, então: InboundRoute.destinationType (novo enum EXTENSION/IVR/QUEUE/CALL_GROUP) decide como buildInboundRouteXml interpreta o destino: - EXTENSION/IVR: inalterado, o mesmo transfer já testado (PHASE 56/58). - QUEUE: destinationNumber guarda o Queue.id; a resolução de entrada emite `answer` + `callcenter data="<queueId>@<domain>"` direto, sem tocar no dialplan "default". `callcenter` entrou no allowlist de applications com o mesmo risco zero de `pickup`. - CALL_GROUP: destinationNumber guarda o Extension.callGroup; a resolução consulta AGORA (nunca um snapshot salvo) todos os ramais com esse callGroup e emite um `bridge` multi-leg — toca todos ao mesmo tempo, quem atender primeiro cancela os outros. Trocar quem está no grupo depois de criar a rota já vale na próxima chamada. Testado ponta a ponta com chamadas reais nos 2 mecanismos novos: fila — softphone externo discou o DID, show channels confirmou a chamada dentro da application callcenter com o nome certo da fila; grupo — 2 ramais reais no mesmo callGroup, a chamada tocou nos DOIS ao mesmo tempo (mesmo call_uuid, ambas RINGING), atender em um cancelou o outro automaticamente — ring group de verdade. Tela: "Tipo de destino" + um segundo dropdown com as opções reais do tenant pra cada tipo (ramais, menus de IVR, filas, ou os valores distintos de callGroup já usados em algum ramal). Testado com Playwright: os 4 tipos aparecem, e trocar o tipo atualiza as opções do segundo dropdown corretamente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { IsBoolean, IsOptional, IsString, Matches, MaxLength } from "class-validator";
|
||||
import { IsBoolean, IsIn, IsOptional, IsString, Matches, MaxLength } from "class-validator";
|
||||
|
||||
const DESTINATION_TYPES = ["EXTENSION", "IVR", "QUEUE", "CALL_GROUP"] as const;
|
||||
|
||||
export class CreateInboundRouteDto {
|
||||
// Numero como o provedor de troncos manda no INVITE (destination_number) —
|
||||
@@ -14,16 +16,24 @@ export class CreateInboundRouteDto {
|
||||
@MaxLength(255)
|
||||
description?: string;
|
||||
|
||||
// PHASE 62 — decide como `destinationNumber`/`destinationContext` são
|
||||
// interpretados na resolução de entrada (ver dialplan-xml.ts):
|
||||
// EXTENSION/IVR continuam discando pelo dialplan do tenant; QUEUE/
|
||||
// CALL_GROUP são resolvidos direto, sem tocar no dialplan.
|
||||
@IsOptional()
|
||||
@IsIn(DESTINATION_TYPES)
|
||||
destinationType?: (typeof DESTINATION_TYPES)[number];
|
||||
|
||||
// Contexto de dialplan do PRÓPRIO tenant que recebe a chamada depois da
|
||||
// resolução — normalmente "default" (cai na discagem interna existente),
|
||||
// ou um contexto de IVR dedicado.
|
||||
// resolução — só usado quando destinationType é EXTENSION/IVR.
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(80)
|
||||
destinationContext?: string;
|
||||
|
||||
// destination_number sintético usado dentro desse contexto — numero de
|
||||
// ramal real, ou um destino reservado do menu de IVR.
|
||||
// Interpretação depende de destinationType: número de ramal (EXTENSION),
|
||||
// IVR_ENTRY_DESTINATION fixo (IVR), Queue.id (QUEUE), ou
|
||||
// Extension.callGroup (CALL_GROUP).
|
||||
@IsString()
|
||||
@Matches(/^[a-zA-Z0-9_-]{1,40}$/, { message: "destinationNumber deve ser alfanumérico (1 a 40 caracteres)" })
|
||||
destinationNumber!: string;
|
||||
@@ -39,6 +49,10 @@ export class UpdateInboundRouteDto {
|
||||
@MaxLength(255)
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(DESTINATION_TYPES)
|
||||
destinationType?: (typeof DESTINATION_TYPES)[number];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(80)
|
||||
|
||||
@@ -47,6 +47,7 @@ export class InboundRoutesController {
|
||||
tenantId,
|
||||
didNumber: dto.didNumber,
|
||||
description: dto.description,
|
||||
destinationType: dto.destinationType ?? "EXTENSION",
|
||||
destinationContext: dto.destinationContext ?? "default",
|
||||
destinationNumber: dto.destinationNumber,
|
||||
enabled: dto.enabled ?? true,
|
||||
@@ -105,6 +106,7 @@ export class InboundRoutesController {
|
||||
where: { id, tenantId, deletedAt: null },
|
||||
data: {
|
||||
...(dto.description !== undefined ? { description: dto.description } : {}),
|
||||
...(dto.destinationType !== undefined ? { destinationType: dto.destinationType } : {}),
|
||||
...(dto.destinationContext !== undefined ? { destinationContext: dto.destinationContext } : {}),
|
||||
...(dto.destinationNumber !== undefined ? { destinationNumber: dto.destinationNumber } : {}),
|
||||
...(dto.enabled !== undefined ? { enabled: dto.enabled } : {}),
|
||||
|
||||
Reference in New Issue
Block a user