Skip to main content
Documento interno de uma sessão extensa de bugfixes + features. Para guia do usuário, veja as páginas de cada feature em Transações.

Resumo executivo

5 entregas, 4 commits no main:
  1. fix — Cliente não conseguia ser removido (RPC soft_delete com whitelist sem clientes)
  2. fixButton asChild não implementava Radix Slot (warning + HTML inválido)
  3. featPageActionsMenu unifica ações de import/export em todas as páginas
  4. feat — Popover Filtros em /gastos (status pills + cartão/forma agrupados + chips ativos)
  5. featSincronizar do histórico em assinaturas: detecta ~80 marcas + recorrência custom
  6. featMover entre workspaces em gastos, parcelas, assinaturas, receitas

1. Fix: delete cliente

Sintoma

Toast de erro “Tabela não permitida: clientes” ao clicar Remover em qualquer cliente da página /receitas/clientes.

Causa

A RPC Postgres soft_delete(p_tabela, p_id) tem whitelist hardcoded de tabelas. clientes e cliente_cobrancas não estavam na lista (foram adicionadas como tabelas novas mas a RPC não foi atualizada).

Fix

Migration soft_delete_add_clientes_workspace_check:
  • Adiciona clientes e cliente_cobrancas à whitelist
  • Para essas tabelas, troca o ownership check de usuario_id = auth.uid() (que quebra em workspace compartilhado) para join com workspace_members — qualquer membro do workspace pode remover
  • Demais tabelas mantêm o check antigo por usuario_id

Arquivos

Migration aplicada direto no Postgres (projeto não tem supabase/migrations/). Para versionar essa mudança em código, criar essa estrutura.

2. Fix: Button asChild

Sintoma

Warning recorrente no console:
Warning: React does not recognize the `asChild` prop on a DOM element.
  at Button (components/ui/button.tsx:43)
  at EmpresaGuard (components/empresa-guard.tsx:64)

Causa

O componente Button declarava asChild?: boolean em ButtonProps mas a implementação só fazia <button {...props}>, jogando asChild direto no DOM. Onde era usado (<Button asChild><Link>...</Link></Button> em EmpresaGuard, sidebar, /convite), gerava <button><a>...</a></button> — HTML inválido.

Fix

Implementa Radix Slot:
import { Slot } from "@radix-ui/react-slot"

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
  }
)
@radix-ui/react-slot já estava instalado como dependência transitiva, não precisou adicionar nada.

3. PageActionsMenu

Problema

Header das páginas tinha botões soltos: Exportar, Importar OFX, Importar CSV, Sincronizar. No mobile virava 4-5 botões empilhados ocupando espaço crítico.

Solução

Componente components/page-actions-menu.tsx com Radix DropdownMenu:
  • Mobile (< sm): só ícone MoreVertical (3 pontos)
  • Desktop (≥ sm): ⋮ Mais ações
  • Itens agrupados com DropdownMenuLabel (Importar / Exportar / Ações)
  • ExportCsvMenuItem reutilizável que renderiza como DropdownMenuItem em vez de <Button>

Aplicado em

  • /gastos — Importar OFX, Importar CSV, Exportar CSV
  • /cartoes — Importar extrato OFX, Exportar CSV
  • /receitas — Exportar CSV
  • /parcelas — Exportar CSV
  • /assinaturas — Sincronizar do histórico, Exportar CSV
Botão primário (Novo Gasto, Novo Cartão, etc) continua sempre visível fora do menu, com flex-1 sm:flex-initial pra ocupar a linha no mobile.

Novo arquivo

components/ui/dropdown-menu.tsx — primitivo shadcn/Radix DropdownMenu com todas as variantes (Item, CheckboxItem, RadioItem, Sub, etc).

4. Popover Filtros em /gastos

Problema

Filtros em pill-row horizontal: 3 linhas (Cartão, Status, Forma) que no mobile quebravam em 5+ linhas.

Solução

  • Status continua como pills (3 opções, troca rápida)
  • Cartão + Forma vão pro popover do botão ⚙ Filtros com badge contador (Filtros · 2)
  • Chips ativos ao lado mostram filtros aplicados (Cartão: Nubank ✕, Forma: PIX, Crédito ✕) com × pra remover individual sem reabrir popover

Novo arquivo

components/ui/popover.tsx — primitivo shadcn/Radix Popover. Instalado @radix-ui/react-popover.

5. Sincronizar assinaturas do histórico

Problema

Botão Sincronizar do histórico em /assinaturas retornava sempre “0 gastos analisados”.

Causas

  1. Filtro por categoria “Streaming” — você tem 2 categorias com nome “Streaming” (sistema + sua workspace). O .maybeSingle() pega só uma; se não bate com a categoria dos seus gastos, retorna 0.
  2. Dicionário pequeno — só ~30 marcas de streaming/cloud/edu. Sem Hostinger, Nuvemshop, Gemini, AWS, Adobe, Hotmart, etc.

Fixes

Em app/cartoes/_actions/ofx-actions.ts → syncStreamingToAssinaturas:
  • Removeu filtro por categoria — escaneia todos os gastos do workspace
  • Mantém detecção por marca via regex em descricao
Em lib/ofx/subscription-detect.ts:
  • Expandido de 30 → ~80 marcas em 8 grupos (streaming, cloud, hospedagem, e-commerce, IA, design, marketing, educação, VPN/senhas)
Adicionou fallback por recorrência:
  • Gastos com mesmo nome+valor em 2+ meses distintos viram candidatos
  • Pega marcas custom não-listadas (SaaS BR pequenos, contador, plano regional, etc)
  • Safeguards contra falso positivo:
    • Valor mínimo R$10 (descarta estacionamento, pequenas)
    • Blocklist de keywords não-assinatura (posto, combustível, ifood, uber, mercado, padaria, restaurante, pedágio, farmácia, hospital)

6. Mover entre workspaces

Problema

Após importar OFX, todos os lançamentos ficam no workspace ativo. Se importou no errado, era apagar tudo e re-criar.

Solução

Cada card ganha menu com Mover pra workspace listando os outros workspaces (com bolinha colorida + tipo). Ao mover, zera referências por-workspace e atualiza workspace_id.

Aplicado em

EntidadeO que zera ao mover
Gastocartao_id + categoria_id (se customizada)
Parcelacartao_id + categoria_id (se customizada)
Assinaturanada (categoria é string livre, sem FKs por-workspace)
Receitacliente_id (clientes são por workspace)

Não aplicado em

Cartão — propositalmente. Mover cartão deixaria gastos órfãos (apontando pra cartão de outro workspace) e quebra fatura. Caso de uso raro.

Componente compartilhado

components/move-to-workspace-menu.tsx — esconde se só tem 1 workspace, usa useConfirm antes de chamar callback. Aceita itemLabel e warningMessage customizáveis. AssinaturaCard inline o dropdown (em vez de usar o componente compartilhado) porque o card tem styling próprio (gradient, texto colorido) e o trigger precisa adaptar.

Dependências adicionadas

"@radix-ui/react-dropdown-menu": "^x.x.x",
"@radix-ui/react-popover": "^x.x.x"

Commits

06d30ba feat: PageActionsMenu unificado + popover Filtros + fix delete cliente
eabee71 fix(assinaturas): sync agora detecta TODAS marcas + recorrência custom
be1675f feat(gastos): mover gasto pra outro workspace via menu ⋮
50c8140 feat: mover entre workspaces em parcelas, assinaturas e receitas

Próximos passos identificados (não implementados)

  • Mover bulk com checkboxes — útil pra migrar 100 gastos importados no workspace errado
  • Sync similar pra /contas-fixas — detectar Vivo, Sabesp, Enel, etc
  • Sync similar pra /gasolina — detectar postos por marca (Shell, Ipiranga, BR)
  • Cofrinho oculto (workspace pessoal com PIN) — discutido mas não implementado
  • “Mover pra → Assinatura/Conta Fixa/Parcela” em vez de só workspace — converte tipo do lançamento