feat(frontend): Telefonia > Ramais — CRUD, reset de senha, isolamento por tenant
Tela /app/telefonia/ramais completa: listagem com busca/ordenacao, criacao com reveal de senha SIP (gerada pela API, mostrada uma unica vez), detalhe com redefinir senha (novo POST /extensions/:id/reset-password — unica forma de "editar" a senha, nunca reexpoe a existente) e remover (soft delete), confirmacao inline de 2 cliques em vez de modal. Cada tela deixa explicito de qual tenant os ramais sao, alem do isolamento por RLS que ja existia. Corrige de quebra um bug real que afeta qualquer acao futura sem payload: apiFetch sempre mandava Content-Type: application/json mesmo em requests sem body, e o parser do Fastify rejeita body vazio com esse header. Testado ponta a ponta com um tenant real (Acme Call Center): criar ramal, revelar senha, redefinir (senha nova confirmada diferente da original), remover, lista voltando vazia. Dark mode e mobile conferidos. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EWHKmcVJtstQFErbZ1AanY
This commit is contained in:
@@ -11,13 +11,14 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { NavSection } from "../shell/nav-types";
|
||||
|
||||
/** IA fixa do menu Tenant (agente.md secao 169) — "Dashboard" tem página
|
||||
* construída até agora (PHASE 23), o resto existe pra provar a
|
||||
* arquitetura completa (Product Principle #5 em PRODUCT.md), renderizado
|
||||
* como indisponível em vez de virar link morto. "Itens sem permissão não
|
||||
* aparecem" (secao 169) ainda não é aplicado aqui — este primeiro corte
|
||||
* mostra a IA inteira pra qualquer usuário tenant autenticado; filtrar por
|
||||
* permission real fica pra quando mais telas existirem pra testar contra. */
|
||||
/** IA fixa do menu Tenant (agente.md secao 169) — "Dashboard" (PHASE 23) e
|
||||
* "Telefonia > Ramais" (PHASE 25) têm página construída até agora, o
|
||||
* resto existe pra provar a arquitetura completa (Product Principle #5
|
||||
* em PRODUCT.md), renderizado como indisponível em vez de virar link
|
||||
* morto. "Itens sem permissão não aparecem" (secao 169) ainda não é
|
||||
* aplicado aqui — este primeiro corte mostra a IA inteira pra qualquer
|
||||
* usuário tenant autenticado; filtrar por permission real fica pra
|
||||
* quando mais telas existirem pra testar contra. */
|
||||
export const TENANT_NAV: NavSection[] = [
|
||||
{
|
||||
label: "Dashboard",
|
||||
@@ -44,7 +45,15 @@ export const TENANT_NAV: NavSection[] = [
|
||||
{
|
||||
label: "Telefonia",
|
||||
icon: Router,
|
||||
children: [{ label: "Ramais" }, { label: "Troncos" }, { label: "Dialplan" }],
|
||||
children: [
|
||||
{
|
||||
label: "Ramais",
|
||||
href: "/app/telefonia/ramais",
|
||||
description: "Ramais SIP deste tenant — números, senha, caller ID",
|
||||
},
|
||||
{ label: "Troncos" },
|
||||
{ label: "Dialplan" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Monitoramento",
|
||||
|
||||
43
apps/frontend/src/components/ui/secret-reveal.tsx
Normal file
43
apps/frontend/src/components/ui/secret-reveal.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Copy, Check, TriangleAlert } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Mostra um segredo em texto puro UMA vez (senha SIP recém-gerada,
|
||||
* agente.md secao 39: "nunca mostrar novamente a senha inteira" depois
|
||||
* desta tela). Sempre acompanhado do aviso — nunca só o valor sozinho,
|
||||
* pra ninguém copiar sem entender que é a única chance.
|
||||
*/
|
||||
export function SecretReveal({ label, value }: { label: string; value: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function onCopy() {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-status-yellow/40 bg-status-yellow/10 p-4">
|
||||
<div className="flex items-start gap-2 text-sm font-medium text-foreground">
|
||||
<TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-status-yellow" aria-hidden />
|
||||
<span>{label} — anote agora, ela não será mostrada de novo.</span>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<code className={cn("flex-1 select-all rounded-md border border-border bg-surface px-3 py-2 font-mono text-sm text-foreground")}>
|
||||
{value}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCopy}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
aria-label="Copiar senha"
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-status-green" aria-hidden /> : <Copy className="h-4 w-4" aria-hidden />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user