feat: Call Center > Agentes + Telefonia > Dialplan (novo endpoint GET /users)

Fecha a lacuna de backend que travava Agentes: POST /agents exigia um
userId de um usuário já existente do tenant, mas não havia nenhum endpoint
pra listar usuários. Novo GET /users (gated por users.manage, não
agents.manage — listar identidade de login é administração de usuário)
via TenantMembership, mesmo padrão de RLS já usado em listUserTenants.

Duas telas novas: Call Center > Agentes (criar/listar/remover, seletor de
usuário só mostra quem ainda não é agente) e Telefonia > Dialplan (editor
estruturado do contexto default — regras com condição/ações dinâmicas,
painel de Versões com gerar/ativar, reativar versão antiga = rollback).

Testado ponta a ponta contra a API real do tenant Acme, incluindo o ciclo
completo de dialplan (criar regra -> gerar v1 -> ativar -> badge "Ativa").
Smoke test de regressão nas 13 telas anteriores do tenant + platform.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-29 16:10:45 -03:00
parent ca49504f0a
commit f1cadddc91
23 changed files with 1025 additions and 12 deletions

View File

@@ -72,3 +72,33 @@ export interface SuppressionEntry {
reason: string | null;
createdAt: string;
}
export interface Agent {
id: string;
userId: string;
extensionId: string | null;
name: string;
maxNoAnswer: number;
wrapUpTime: number;
state: string;
enabled: boolean;
createdAt: string;
}
export const AGENT_STATE_LABELS: Record<string, string> = {
OFFLINE: "Offline",
LOGGED_IN: "Logado",
AVAILABLE: "Disponível",
RESERVED: "Reservado",
RINGING: "Chamando",
IN_CALL: "Em chamada",
WRAP_UP: "Pós-atendimento",
PAUSED: "Em pausa",
};
export interface TenantUser {
id: string;
email: string;
name: string;
status: string;
}

View File

@@ -0,0 +1,67 @@
/** Duplicado do backend só pra UI (mesmo padrão de QUEUE_STRATEGIES em
* callcenter-types.ts) — o frontend não importa pacotes do backend, fala
* só HTTP. `apps/api` continua sendo a autoridade: a allowlist aqui é só
* pra montar o `<select>`, o servidor rejeita qualquer coisa fora dela
* (agente.md secao 180). */
export const ALLOWED_DIALPLAN_APPLICATIONS = [
"answer",
"pre_answer",
"bridge",
"hangup",
"park",
"playback",
"ring_ready",
"respond",
"set",
"export",
"transfer",
"sleep",
"record_session",
] as const;
export const ALLOWED_CONDITION_FIELDS = [
"destination_number",
"caller_id_number",
"caller_id_name",
"context",
"network_addr",
"source",
] as const;
export const CONDITION_FIELD_LABELS: Record<string, string> = {
destination_number: "Número discado",
caller_id_number: "Número do chamador",
caller_id_name: "Nome do chamador",
context: "Contexto",
network_addr: "Endereço de rede",
source: "Origem",
};
export interface DialplanAction {
application: string;
data?: string;
}
export interface DialplanExtension {
id: string;
context: string;
name: string;
conditionField: string;
conditionExpr: string;
actions: DialplanAction[];
antiActions: DialplanAction[] | null;
continueOnFalse: boolean;
order: number;
enabled: boolean;
createdAt: string;
}
export interface DialplanVersion {
id: string;
context: string;
version: number;
status: "DRAFT" | "ACTIVE" | "SUPERSEDED";
createdAt: string;
activatedAt: string | null;
createdByUserId: string;
}