🪜 Pipeline: Estados do Funil
Pipeline genérico não serve ninguém. Adaptar os estados ao nicho do cliente é o que faz o CRM virar ferramenta usada todo dia, não tela parada.
📋 Pipeline padrão B2B (default do InboxAI)
| Nicho | Pipeline adaptado |
|---|---|
| Clínica médica | Lead → Pré-agendado → Atendeu → Em tratamento → Alta |
| Imobiliária | Lead → Visitou → Negociando → Contrato → Fechado/Perdido |
| SaaS B2B | Lead → Demo → POC → Proposta → Closed Won/Lost |
| E-commerce | Carrinho abandonado → Recuperado → Pago → Entregue → Fidelizado |
💡 Estados terminais sempre dois
Todo pipeline acaba em Won ou Lost (ou equivalente). Sem o "Lost" claro, deals ficam acumulando em "Negociando" há 6 meses e o vendedor não sabe que perdeu. Forçar fechamento é parte da disciplina que o produto tem que impor.
🔄 Drag-and-Drop entre Colunas
Implementar DnD próprio dá mais trabalho que vale; biblioteca testada economiza dias e tem acessibilidade já resolvida. @hello-pangea/dnd é o fork mantido do react-beautiful-dnd e é a escolha óbvia em 2026.
📦 Instalação
npm install @hello-pangea/dnd
⚙️ Snippet — Kanban com DnD
'use client';
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
import type { DropResult } from '@hello-pangea/dnd';
export function PipelineBoard({ stages, deals, onMove }: Props) {
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const { draggableId, destination } = result;
onMove(draggableId, destination.droppableId, destination.index);
};
return (
<DragDropContext onDragEnd={handleDragEnd}>
<div className="flex gap-4 overflow-x-auto">
{stages.map((stage) => (
<Droppable key={stage.id} droppableId={stage.id}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className="flex-shrink-0 w-72 bg-neutral-800 rounded-lg p-3"
>
<h3 className="text-sm font-bold text-purple-400 mb-3">
{stage.name} ({stage.dealCount})
</h3>
{deals.filter((d) => d.stageId === stage.id).map((deal, i) => (
<Draggable key={deal.id} draggableId={deal.id} index={i}>
{(p) => (
<div ref={p.innerRef} {...p.draggableProps} {...p.dragHandleProps}>
<DealCard deal={deal} />
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
))}
</div>
</DragDropContext>
);
}
⚡ Optimistic UI: o detalhe que separa amador de pro
Em onMove, atualize o estado local antes de chamar a API. Se a API falhar, reverta. Sem isso, o card "pula" pra origem 200ms depois — o usuário sente o lag e desconfia. Com isso, parece instantâneo.
💳 Card do Deal
Cards bem desenhados são lidos em 2 segundos. Mal desenhados, o vendedor abre cada um pra entender — e aí o kanban deixa de ter graça.
📐 Anatomia do card
📋 Hierarquia de info
- 1. Empresa + contato — quem é, em 1 segundo.
- 2. Valor — número grande, cor de receita (verde).
- 3. Probabilidade % — pill no canto.
- 4. Tempo parado — alerta visual após 7 dias.
- 5. Tags — só se houver, máximo 3.
⚙️ Snippet — DealCard
type Deal = {
id: string;
company: string;
contactName: string;
amount: number;
probability: number; // 0-100
stageEnteredAt: number; // epoch
tags: string[];
};
export function DealCard({ deal }: { deal: Deal }) {
const daysIdle = Math.floor((Date.now() - deal.stageEnteredAt) / 86400000);
const isStale = daysIdle >= 7;
return (
<div className="bg-neutral-700 rounded-lg p-3 mb-2 hover:ring-1 hover:ring-purple-500">
<div className="flex justify-between items-start mb-1">
<div className="font-semibold text-sm">{deal.company}</div>
<span className="text-xs bg-purple-500/20 text-purple-400 px-2 rounded-full">
{deal.probability}%
</span>
</div>
<div className="text-xs text-neutral-400 mb-2">{deal.contactName}</div>
<div className="text-lg font-bold text-emerald-400 mb-2">
{deal.amount.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
</div>
{isStale && (
<span className="text-xs bg-amber-500/20 text-amber-400 px-2 rounded-full">
⚠️ {daysIdle}d parado
</span>
)}
</div>
);
}
⏰ O alerta de "dias parado" é o segredo
Vendedor procrastina deal complicado. Sem sinal visual, ele some no meio dos 50 cards. Com o badge âmbar após 7 dias, vira chamada à ação no formato exato em que o cérebro humano responde — vermelho/âmbar = "olha aqui agora".
📈 Dashboard: Métricas-Chave
Dashboard com 30 métricas é dashboard que ninguém olha. Foco em 4 que conectam com receita é o que faz dono de negócio abrir todo dia.
🎯 Vanity vs Actionable
Métrica vanity: "1.247 conversas neste mês". Bonita, inacionável.
Métrica actionable: "tempo mediano de resposta subiu de 2m pra 4m". Mostra o quê fazer: contratar mais um operador, ativar IA de primeira resposta, ou redistribuir turno.
📊 Charts com Recharts
Recharts é a biblioteca de fato pra Next.js — declarativa, baseada em SVG, fácil de customizar. Saber configurar bem cada chart (cores, tooltip, eixos) diferencia dashboard amador de profissional.
📦 Instalação
npm install recharts
LineChart
Tendência ao longo do tempo. Conversões/dia, tempo de resposta semanal.
BarChart
Comparação entre categorias. Volume por canal, ranking de vendedores.
DonutChart
Proporção de um todo. % por canal, distribuição de status.
⚙️ Snippet — LineChart configurado
import {
LineChart, Line, XAxis, YAxis,
Tooltip, ResponsiveContainer, CartesianGrid,
} from 'recharts';
export function ResponseTimeChart({ data }: { data: Point[] }) {
return (
<ResponsiveContainer width="100%" height={240}>
<LineChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
<XAxis dataKey="day" stroke="#9ca3af" fontSize={12} />
<YAxis stroke="#9ca3af" fontSize={12} unit="m" />
<Tooltip
contentStyle={{ background: '#1f2937', border: '1px solid #7c3aed' }}
labelStyle={{ color: '#a78bfa' }}
/>
<Line
type="monotone"
dataKey="responseMin"
stroke="#7c3aed"
strokeWidth={2}
dot={{ fill: '#7c3aed', r: 3 }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
);
}
🎨 Paleta consistente em todos os charts
Defina uma paleta global (purple-400 primário, emerald-400 sucesso, amber-400 alerta, red-400 crítico) e use em todo dashboard. Charts coloridos arbitrariamente parecem amadores. 3-4 cores semânticas é o teto.
🔍 Filtros e Segmentações
Filtro mal feito ocupa metade da tela e ninguém usa. Filtro bem feito (tipo Linear) destrava produtividade sem custo visual — aparece quando precisa, some quando não.
❌ Filtros mal feitos
- • 8 dropdowns sempre visíveis
- • Sem indicador de quantos filtros ativos
- • Reset sumido no canto
- • Não persiste ao recarregar
- • Não dá pra salvar combinação
✅ Filtros bem feitos
- ✓ Botão "+ Filter" abre popover
- ✓ Pills horizontais mostram ativos
- ✓ X em cada pill remove individual
- ✓ "Clear all" só aparece com 2+
- ✓ Salvar como "Saved view"
🏷️ Filtros essenciais para CRM
| Filtro | UI sugerida | Caso de uso |
|---|---|---|
| Canal | Multi-select com ícones | Ver só leads do WhatsApp |
| Atendente | Avatar picker | Meus deals |
| Faixa de valor | Range slider | Deals enterprise (R$ 10k+) |
| Tag | Combobox com autocomplete | Vertical específica |
| Status temporal | Pills exclusivos | "Parados > 7 dias" |
💾 Saved Views: o cliente vai pedir
A partir do segundo mês de uso, todo cliente pede "salvar essa combinação de filtros". Antecipe — implemente Saved Views já. Use query params na URL pra cada view ser compartilhável (link copiado abre o filtro). Ganho de UX gigante por trabalho mínimo.
✅ O que Aprendemos
Próximo Módulo:
3.4 — Browser Embutido + Iteração Visual: o recurso exclusivo do Codex App que corta ciclos de feedback de minutos para segundos.