diff --git a/.gitignore b/.gitignore index 5cfcb83..7ee9fa0 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ dist/ build/ .next/ .turbo/ +*.tsbuildinfo + +# Impeccable per-developer overrides (see .claude/skills/impeccable) +.impeccable/config.local.json # Logs *.log diff --git a/apps/frontend/.impeccable/review/dashboard-desktop.png b/apps/frontend/.impeccable/review/dashboard-desktop.png new file mode 100644 index 0000000..de11c47 Binary files /dev/null and b/apps/frontend/.impeccable/review/dashboard-desktop.png differ diff --git a/apps/frontend/.impeccable/review/dashboard-mobile.png b/apps/frontend/.impeccable/review/dashboard-mobile.png new file mode 100644 index 0000000..ccfbfc0 Binary files /dev/null and b/apps/frontend/.impeccable/review/dashboard-mobile.png differ diff --git a/apps/frontend/.impeccable/review/price-book-detail-desktop.png b/apps/frontend/.impeccable/review/price-book-detail-desktop.png new file mode 100644 index 0000000..7ea9a22 Binary files /dev/null and b/apps/frontend/.impeccable/review/price-book-detail-desktop.png differ diff --git a/apps/frontend/.impeccable/review/price-book-new-dark-desktop.png b/apps/frontend/.impeccable/review/price-book-new-dark-desktop.png new file mode 100644 index 0000000..d9f898d Binary files /dev/null and b/apps/frontend/.impeccable/review/price-book-new-dark-desktop.png differ diff --git a/apps/frontend/.impeccable/review/price-book-new-desktop.png b/apps/frontend/.impeccable/review/price-book-new-desktop.png new file mode 100644 index 0000000..4e8822f Binary files /dev/null and b/apps/frontend/.impeccable/review/price-book-new-desktop.png differ diff --git a/apps/frontend/.impeccable/review/price-book-new-mobile.png b/apps/frontend/.impeccable/review/price-book-new-mobile.png new file mode 100644 index 0000000..91e59a6 Binary files /dev/null and b/apps/frontend/.impeccable/review/price-book-new-mobile.png differ diff --git a/apps/frontend/.impeccable/review/rate-deck-new-desktop.png b/apps/frontend/.impeccable/review/rate-deck-new-desktop.png new file mode 100644 index 0000000..a13f746 Binary files /dev/null and b/apps/frontend/.impeccable/review/rate-deck-new-desktop.png differ diff --git a/apps/frontend/.impeccable/review/rate-deck-new-mobile.png b/apps/frontend/.impeccable/review/rate-deck-new-mobile.png new file mode 100644 index 0000000..405c74f Binary files /dev/null and b/apps/frontend/.impeccable/review/rate-deck-new-mobile.png differ diff --git a/apps/frontend/.impeccable/review/tarifas-dark-desktop.png b/apps/frontend/.impeccable/review/tarifas-dark-desktop.png new file mode 100644 index 0000000..cc3e92f Binary files /dev/null and b/apps/frontend/.impeccable/review/tarifas-dark-desktop.png differ diff --git a/apps/frontend/.impeccable/review/tarifas-empty-desktop.png b/apps/frontend/.impeccable/review/tarifas-empty-desktop.png new file mode 100644 index 0000000..ef63184 Binary files /dev/null and b/apps/frontend/.impeccable/review/tarifas-empty-desktop.png differ diff --git a/apps/frontend/.impeccable/review/tarifas-empty-mobile.png b/apps/frontend/.impeccable/review/tarifas-empty-mobile.png new file mode 100644 index 0000000..6930e06 Binary files /dev/null and b/apps/frontend/.impeccable/review/tarifas-empty-mobile.png differ diff --git a/apps/frontend/.impeccable/review/tarifas-with-data-desktop.png b/apps/frontend/.impeccable/review/tarifas-with-data-desktop.png new file mode 100644 index 0000000..c6a8d34 Binary files /dev/null and b/apps/frontend/.impeccable/review/tarifas-with-data-desktop.png differ diff --git a/apps/frontend/PRODUCT.md b/apps/frontend/PRODUCT.md new file mode 100644 index 0000000..a2e2404 --- /dev/null +++ b/apps/frontend/PRODUCT.md @@ -0,0 +1,66 @@ +# Product + + + +## Platform + +web + +## Stack + +Next.js, React, TypeScript, Tailwind CSS, shadcn/ui, TanStack Query, TanStack Table, Recharts, Lucide Icons (specified explicitly in the project's own build spec, `agente.md`, "Frontend" section — not delegated). + +## Users + +Two audiences, both authenticated staff (never a public visitor): + +- **Platform admins** (B2BCall's own operators): manage tenants, plans/subscriptions, quotas, global infrastructure (FreeSWITCH nodes, SIP profiles, health), AI provider catalog, billing rate decks/price books, system users/permissions/audit. +- **Tenant staff** (a client company's own team): tenant admins configure trunks/extensions/queues/campaigns/agents/AI settings/billing for their own account; agents work the live call queue; supervisors monitor campaigns/queues/agents in real time and review AI call analysis, quality scorecards, and reports. + +## Product Purpose + +B2BCall is a multi-tenant SaaS call-center platform: predictive dialer, inbound/outbound call center (FreeSWITCH-backed), AI-driven call transcription/analysis/QA scorecards, and usage-based billing, all under strict per-tenant data isolation. Success is a tenant running real campaigns and inbound queues, with agents connected and productive, in an interface that looks and feels like a modern SaaS product a client would trust with production traffic. + +## Positioning + +External, multi-tenant SaaS product sold to other companies running their own call centers — not an internal single-operator tool. The interface must carry plan/quota framing (a tenant sees its own consumption and limits) and customer-facing polish throughout, per the build spec's explicit instruction (section 164) that it must read as a modern professional SaaS product, never as an internal admin panel. + +## Operating Context + +Real-time operations are central: live call/agent/queue counters that move continuously, predictive-dialer campaigns paced against CPS/concurrency limits, agents moving through pause/wrap-up/available states, calls flowing through queues to agents. Supporting workflows: campaign creation (a defined 7-step wizard: General → Telephony → Dialing → Schedule → Recording & AI → Leads → Review), lead import/blocklist management, call recording playback with synced transcript, AI analysis review, quality scorecard evaluation, and monthly billing statement review (explicitly never called an invoice/nota fiscal — "Usage Statement" / "Billing Statement" / "Relatório de Consumo" only). + +## Capabilities and Constraints + +- Full navigation IA already specified for both audiences (`agente.md` sections 168-169): Platform menu = Visão Geral / Clientes (Tenants, Planos, Assinaturas, Quotas) / Billing (Consumo, Tarifas, Fechamentos, Relatórios) / Infraestrutura (FreeSWITCH, SIP Profiles, Nodes, Saúde) / IA (Providers, Modelos, Uso, Custos) / Sistema (Usuários, Permissões, Auditoria, Configurações). Tenant menu = Dashboard / Discador / Call Center / Telefonia / Monitoramento / Gravações / IA / Relatórios / Administração. Menu items the current user lacks permission for do not render. +- Layout is fixed by spec: collapsible sidebar + topbar + content (section 166). +- Server-side pagination, search, filters, sort, column visibility, row actions, bulk actions, and export are required on every data table (section 173). +- Status must never be color-only (accessibility, section 176); consistent badge colors are specified per campaign status (DRAFT gray, READY blue, RUNNING green, PAUSED yellow, DRAINING orange, STOPPED red, COMPLETED dark green, ERROR red). +- Light/Dark/System theming is required unless it compromises higher priorities (section 174). +- Desktop/notebook/tablet are the priority breakpoints; the agent's own screen must work well at smaller resolutions too (section 175). +- A real backend is live today (NestJS/Fastify REST API on `localhost:3000`, 20+ endpoints across auth, extensions, trunks, queues, agents, campaigns, leads, calls, recordings, AI providers/pipeline, quality scorecards) — this is not a mockup exercise; real data is available to wire against once auth is in place. No frontend code exists yet beyond this branding asset. +- Backend has no billing UI-facing endpoints yet for the newest schema (rate decks, price books, billing periods/statements) — those are mid-build; the Platform Dashboard itself (tenants, quotas, infra health, AI usage) is fully backed today. + +## Brand Commitments + +- Name: **B2BCall**. Wordmark and mark exist at `apps/frontend/public/branding/b2blogo.png` — a rounded-square chat/analytics icon (bar-chart bars + phone handset + chat-bubble tail) paired with a two-tone "B2B" (dark navy) + "Call" (bright blue) wordmark. Palette read directly off the mark: deep navy (~#0B1B3A), a saturated blue (~#2554FF), and a teal accent (~#2EC9C0) used sparingly for one bar/dot. This is binding — build from these colors and this mark, not a freely chosen new palette. +- Login screen must use this logo (section 167): "Brand B2BCall | Login Form" split layout on desktop, responsive down. +- Explicit anti-brief (section 164): must **not** read as FreePBX, FusionPBX, a generic Linux admin panel, dated Bootstrap, or any legacy on-prem PBX tool. + +## Evidence on Hand + +- Real logo/mark: `apps/frontend/public/branding/b2blogo.png`. +- Full build spec with exact page-level requirements for dashboards, menus, wizards, tables, and status semantics: `agente.md` sections 161-176 (and the whole document for domain vocabulary). +- Live backend and database schema this session already built and tested end-to-end (see `docs/*.md` at the repo root — `ARCHITECTURE.md`, `PREDICTIVE_DIALER.md`, `AI_PIPELINE.md`, `QUALITY_SCORECARDS.md`, etc.). +- No testimonials, customer logos, pricing page copy, or marketing claims exist — none should be fabricated; this build is internal admin/ops surfaces, not a marketing site. + +## Product Principles + +1. Real-time state is the product — counters, badges, and queues must read as genuinely live, not as a static report someone might refresh. +2. Never let this look like the telecom/PBX tooling it's replacing (section 164) — every screen should read as a modern, venture-funded SaaS dashboard first, telecom tool second. +3. Multi-tenant discipline shows up in the UI too — a tenant's plan/quota context (consumption vs. limit) is always nearby, never buried. +4. Status is never color-only; every state has a label, not just a hue (section 176). +5. Build the platform-admin surface first as the reference implementation of the design system (tokens, layout shell, table pattern, badge system) that every later tenant-facing screen extends, rather than a one-off. + +## Accessibility & Inclusion + +Explicit requirement (section 176): labels, full keyboard navigation, visible focus states, ARIA, sufficient contrast, and status conveyed by more than color alone. diff --git a/apps/frontend/next-env.d.ts b/apps/frontend/next-env.d.ts new file mode 100644 index 0000000..1b3be08 --- /dev/null +++ b/apps/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/frontend/next.config.ts b/apps/frontend/next.config.ts new file mode 100644 index 0000000..9952bf9 --- /dev/null +++ b/apps/frontend/next.config.ts @@ -0,0 +1,15 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + // sharp (compilacao nativa) fica desligado de proposito em + // pnpm-workspace.yaml (allowBuilds.sharp: false — nao roda script de + // instalacao nao revisado so' pra otimizacao de imagem). unoptimized=true + // evita o pipeline de otimizacao inteiro em vez de depender de um + // fallback silencioso; os unicos assets sao a logo estatica, sem ganho + // real em comprimir de novo em runtime. + images: { + unoptimized: true, + }, +}; + +export default nextConfig; diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000..bb5f0a2 --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "@b2bcall/frontend", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "next": "15.3.9", + "react": "19.1.0", + "react-dom": "19.1.0", + "@tanstack/react-query": "5.62.7", + "@tanstack/react-table": "8.20.5", + "recharts": "2.15.0", + "lucide-react": "0.469.0", + "clsx": "2.1.1", + "tailwind-merge": "2.5.5", + "class-variance-authority": "0.7.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-dropdown-menu": "2.1.4", + "@radix-ui/react-avatar": "1.1.2", + "@radix-ui/react-tooltip": "1.1.6", + "server-only": "0.0.1" + }, + "devDependencies": { + "@types/node": "^22.20.1", + "@types/react": "19.1.0", + "@types/react-dom": "19.1.0", + "typescript": "^5.7.0", + "tailwindcss": "3.4.17", + "postcss": "8.4.49", + "autoprefixer": "10.4.20" + } +} diff --git a/apps/frontend/postcss.config.mjs b/apps/frontend/postcss.config.mjs new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/apps/frontend/postcss.config.mjs @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/frontend/src/app/api/login/route.ts b/apps/frontend/src/app/api/login/route.ts new file mode 100644 index 0000000..c751fe8 --- /dev/null +++ b/apps/frontend/src/app/api/login/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from "next/server"; +import { API_BASE_URL } from "@/lib/api"; +import { sessionCookieName } from "@/lib/session"; + +export async function POST(request: Request) { + const { email, password } = await request.json(); + + const res = await fetch(`${API_BASE_URL}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + cache: "no-store", + }); + + if (!res.ok) { + const body = await res.json().catch(() => ({ message: "Falha ao entrar" })); + return NextResponse.json({ message: body.message ?? "Falha ao entrar" }, { status: res.status }); + } + + const data = await res.json(); + + if (data.mustChangePassword) { + return NextResponse.json( + { message: "Senha precisa ser trocada antes do primeiro acesso (use a API /auth/change-password por enquanto)." }, + { status: 403 }, + ); + } + + const response = NextResponse.json({ ok: true }); + response.cookies.set(sessionCookieName(), JSON.stringify({ accessToken: data.accessToken, refreshToken: data.refreshToken }), { + httpOnly: true, + sameSite: "lax", + secure: process.env.NODE_ENV === "production", + path: "/", + maxAge: 60 * 60 * 8, + }); + return response; +} diff --git a/apps/frontend/src/app/api/logout/route.ts b/apps/frontend/src/app/api/logout/route.ts new file mode 100644 index 0000000..a2ad0e6 --- /dev/null +++ b/apps/frontend/src/app/api/logout/route.ts @@ -0,0 +1,8 @@ +import { NextResponse } from "next/server"; +import { sessionCookieName } from "@/lib/session"; + +export async function POST() { + const response = NextResponse.json({ ok: true }); + response.cookies.delete(sessionCookieName()); + return response; +} diff --git a/apps/frontend/src/app/globals.css b/apps/frontend/src/app/globals.css new file mode 100644 index 0000000..444a7cf --- /dev/null +++ b/apps/frontend/src/app/globals.css @@ -0,0 +1,126 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* + * Tokens (agente.md secao 165: colors/typography/spacing/radius/shadows/ + * status colors). Paleta derivada do logo real (apps/frontend/public/ + * branding/b2blogo.png): navy profundo + azul saturado + teal de apoio — + * nunca uma paleta nova escolhida livremente (agente.md secao "Brand + * Commitments" em PRODUCT.md). + */ +:root { + --background: 210 40% 98%; + --foreground: 224 45% 11%; + --surface: 0 0% 100%; + --surface-raised: 210 35% 99%; + --border: 220 20% 88%; + --input: 220 20% 86%; + --muted: 220 22% 95%; + --muted-foreground: 220 12% 42%; + --primary: 226 100% 59%; + --primary-foreground: 0 0% 100%; + --accent: 175 63% 40%; + --accent-foreground: 0 0% 100%; + --destructive: 358 75% 48%; + --destructive-foreground: 0 0% 100%; + --ring: 226 100% 59%; + --shadow-color: 224 40% 20%; + --radius: 10px; + + --status-gray: 220 10% 52%; + --status-blue: 226 100% 59%; + --status-green: 152 58% 36%; + --status-yellow: 40 92% 46%; + --status-orange: 25 92% 50%; + --status-red: 358 75% 48%; + --status-green-dark: 152 62% 22%; +} + +@media (prefers-color-scheme: dark) { + html:not(.light) { + --background: 224 38% 7%; + --foreground: 210 30% 93%; + --surface: 224 32% 10%; + --surface-raised: 224 28% 13%; + --border: 224 22% 20%; + --input: 224 22% 22%; + --muted: 224 22% 15%; + --muted-foreground: 220 14% 64%; + --primary: 226 100% 68%; + --primary-foreground: 224 45% 9%; + --accent: 175 58% 54%; + --accent-foreground: 224 45% 9%; + --destructive: 358 82% 64%; + --destructive-foreground: 224 45% 9%; + --ring: 226 100% 68%; + --shadow-color: 0 0% 0%; + + --status-gray: 220 10% 66%; + --status-blue: 226 100% 68%; + --status-green: 152 55% 54%; + --status-yellow: 40 95% 62%; + --status-orange: 25 95% 62%; + --status-red: 358 85% 68%; + --status-green-dark: 152 45% 40%; + } +} + +html.dark { + --background: 224 38% 7%; + --foreground: 210 30% 93%; + --surface: 224 32% 10%; + --surface-raised: 224 28% 13%; + --border: 224 22% 20%; + --input: 224 22% 22%; + --muted: 224 22% 15%; + --muted-foreground: 220 14% 64%; + --primary: 226 100% 68%; + --primary-foreground: 224 45% 9%; + --accent: 175 58% 54%; + --accent-foreground: 224 45% 9%; + --destructive: 358 82% 64%; + --destructive-foreground: 224 45% 9%; + --ring: 226 100% 68%; + --shadow-color: 0 0% 0%; + + --status-gray: 220 10% 66%; + --status-blue: 226 100% 68%; + --status-green: 152 55% 54%; + --status-yellow: 40 95% 62%; + --status-orange: 25 95% 62%; + --status-red: 358 85% 68%; + --status-green-dark: 152 45% 40%; +} + +* { + border-color: hsl(var(--border)); +} + +html { + color-scheme: light dark; +} + +body { + background: hsl(var(--background)); + color: hsl(var(--foreground)); + font-feature-settings: + "cv02", + "cv03", + "cv04", + "cv11"; +} + +.tabular-nums { + font-variant-numeric: tabular-nums; +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx new file mode 100644 index 0000000..c94cedb --- /dev/null +++ b/apps/frontend/src/app/layout.tsx @@ -0,0 +1,57 @@ +import type { Metadata } from "next"; +import { IBM_Plex_Sans, IBM_Plex_Mono } from "next/font/google"; +import "./globals.css"; + +const plexSans = IBM_Plex_Sans({ + subsets: ["latin"], + weight: ["400", "500", "600", "700"], + variable: "--font-sans", + display: "swap", +}); + +const plexMono = IBM_Plex_Mono({ + subsets: ["latin"], + weight: ["400", "500", "600"], + variable: "--font-mono", + display: "swap", +}); + +export const metadata: Metadata = { + title: "B2BCall", + description: "Plataforma SaaS de call center, discador preditivo e IA — B2BCall.", +}; + +// Aplica o tema salvo ANTES do primeiro paint (sem isso, uma preferencia +// dark salva pisca a versao light por um frame — FOUC classico de theming +// client-side). Roda sincrono no , antes de qualquer CSS/JS de app. +const THEME_INIT_SCRIPT = ` +(function () { + try { + var stored = localStorage.getItem("b2bcall-theme"); + if (stored === "light" || stored === "dark") { + document.documentElement.classList.add(stored); + } + } catch (e) {} +})(); +`; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + +