feat: Administração > Configurações, remover usuário do tenant, Discador > Callbacks

Fecha os últimos gaps do módulo Administração (agente.md secao 169): tela de
Configurações self-service do próprio tenant (GET/PATCH /tenant-settings, nunca
aceita tenantId arbitrário — só user.tenantId das claims), e DELETE /users/:id pra
remover alguém do tenant, com duas proteções que não existiam antes (não deixa
remover a si mesmo, não deixa remover/rebaixar o último Tenant Admin).

Corrige um bug real achado testando a remoção: o delete de TenantMembership (FORCE
RLS) rodava dentro de um prisma.$transaction([...]) em forma de array, que nunca
seta app.current_tenant_id — Prisma devolvia P2025 "not found" com a linha
existindo (500 pro cliente). Mesma classe de bug já corrigida antes em
TenantsController.create; corrigido com $transaction(async (tx) => ...) + set_config
explícito.

Adiciona Discador > Callbacks (GET/PATCH /leads/callbacks, tenant-wide): reagendar,
tentar de novo sem esperar, ou desistir de um lead que pediu retorno em outro
horário. Remove "Importações" do menu — decisão já registrada na PHASE 39 de não
duplicar uma tela pro que já existe (CSV em lote no wizard/detalhe da campanha).

Testado ponta a ponta via curl e Puppeteer contra o tenant Acme real: tenant-settings
GET/PATCH, proteção de último-admin nos dois endpoints que a usam, convite+remoção
de um admin temporário, e o fluxo completo de callback (lead forçado pra CALLBACK
via SQL, reagendar rejeitado pro passado/aceito pro futuro, requeue confirmado).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
2026-08-30 08:16:38 -03:00
parent 52e1b4f3b7
commit 2fb5010283
24 changed files with 798 additions and 11 deletions

View File

@@ -16,3 +16,25 @@ export const TENANT_ROLE_OPTIONS: { key: string; name: string }[] = [
{ key: "supervisor", name: "Supervisor" },
{ key: "agent", name: "Agente" },
];
export interface TenantSettings {
id: string;
code: string;
legalName: string;
tradeName: string | null;
taxId: string | null;
status: "TRIAL" | "ACTIVE" | "SUSPENDED" | "PAST_DUE" | "CANCELLED";
timezone: string;
locale: string;
billingCurrency: string;
telephonyDomain: string | null;
aiPrivacyLevel: "AI_OFF" | "TRANSCRIPTION_ONLY" | "TRANSCRIPTION_AND_ANALYSIS";
plan: { key: string; name: string };
createdAt: string;
}
export const AI_PRIVACY_LEVEL_LABELS: Record<string, string> = {
AI_OFF: "Desligada — nenhum áudio/transcrição sai deste tenant",
TRANSCRIPTION_ONLY: "Só transcrição — sem análise por IA",
TRANSCRIPTION_AND_ANALYSIS: "Transcrição e análise por IA",
};

View File

@@ -88,6 +88,10 @@ export interface Lead {
createdAt: string;
}
export interface CallbackLead extends Lead {
campaign: { id: string; name: string };
}
export const LEAD_STATUS_LABELS: Record<string, string> = {
NEW: "Novo",
READY: "Pronto",