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:
@@ -35,6 +35,9 @@ export const ALLOWED_DIALPLAN_APPLICATIONS = [
|
||||
// timeout terminators file invalid_file var_name regexp
|
||||
// digit_timeout`), nunca executa nada; mesmo padrão de risco zero.
|
||||
"play_and_get_digits",
|
||||
// Rota de entrada pra fila (PHASE 62) — só recebe `<queueId>@<domain>`
|
||||
// como texto, nunca um comando; mesmo padrão de risco zero.
|
||||
"callcenter",
|
||||
] as const;
|
||||
|
||||
export type AllowedDialplanApplication = (typeof ALLOWED_DIALPLAN_APPLICATIONS)[number];
|
||||
@@ -161,22 +164,47 @@ function actionsXml(tag: "action" | "anti-action", actions: DialplanAction[] | u
|
||||
* default (`$${domain}` do vars.xml, ex.: "b2bcall.local"), nunca pro
|
||||
* domínio de verdade do tenant — a leg de entrada não é um ramal
|
||||
* registrado, então nada preenche essa variable sozinho.
|
||||
*
|
||||
* PHASE 62 (dropdown de destino real: ramal/IVR/fila/grupo) — EXTENSION
|
||||
* e IVR continuam usando exatamente o `transfer` acima (reaproveitam o
|
||||
* dialplan do tenant sem mudança nenhuma). QUEUE e CALL_GROUP nunca
|
||||
* passam pelo "default": a própria resolução de entrada já emite a
|
||||
* action final, porque nenhum dos dois é "discar um número" — fila é
|
||||
* `callcenter` de verdade, grupo é `bridge` simultâneo pra vários ramais.
|
||||
* `groupMembers` é resolvido pelo CHAMADOR (apps/freeswitch-config, com
|
||||
* acesso ao banco) toda vez que uma chamada de entrada chega — nunca um
|
||||
* snapshot salvo, então trocar quem está no grupo já vale na próxima
|
||||
* chamada, sem precisar re-salvar a rota.
|
||||
*/
|
||||
export function buildInboundRouteXml(params: {
|
||||
tenantId: string;
|
||||
domain: string;
|
||||
destinationNumber: string;
|
||||
destinationContext: string;
|
||||
}): string {
|
||||
export function buildInboundRouteXml(
|
||||
params:
|
||||
| { tenantId: string; domain: string; destinationType: "EXTENSION" | "IVR"; destinationNumber: string; destinationContext: string }
|
||||
| { tenantId: string; domain: string; destinationType: "QUEUE"; queueId: string }
|
||||
| { tenantId: string; domain: string; destinationType: "CALL_GROUP"; groupMembers: string[] },
|
||||
): string {
|
||||
const setup = ` <action application="set" data="b2bcall_tenant_id=${xmlEscape(params.tenantId)}"/>
|
||||
<action application="set" data="domain_name=${xmlEscape(params.domain)}"/>`;
|
||||
|
||||
let action: string;
|
||||
if (params.destinationType === "QUEUE") {
|
||||
action = ` <action application="answer"/>
|
||||
<action application="callcenter" data="${xmlEscape(params.queueId)}@${xmlEscape(params.domain)}"/>`;
|
||||
} else if (params.destinationType === "CALL_GROUP") {
|
||||
const legs = params.groupMembers.map((num) => `user/${xmlEscape(num)}@${xmlEscape(params.domain)}`).join(",");
|
||||
action = ` <action application="answer"/>
|
||||
<action application="bridge" data="${legs}"/>`;
|
||||
} else {
|
||||
action = ` <action application="transfer" data="${xmlEscape(params.destinationNumber)} XML ${xmlEscape(params.destinationContext)}"/>`;
|
||||
}
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="freeswitch/xml">
|
||||
<section name="dialplan">
|
||||
<context name="inbound">
|
||||
<extension name="inbound-route">
|
||||
<condition>
|
||||
<action application="set" data="b2bcall_tenant_id=${xmlEscape(params.tenantId)}"/>
|
||||
<action application="set" data="domain_name=${xmlEscape(params.domain)}"/>
|
||||
<action application="transfer" data="${xmlEscape(params.destinationNumber)} XML ${xmlEscape(params.destinationContext)}"/>
|
||||
${setup}
|
||||
${action}
|
||||
</condition>
|
||||
</extension>
|
||||
</context>
|
||||
|
||||
Reference in New Issue
Block a user