Resumo executivo
5 entregas, 4 commits nomain:
fix— Cliente não conseguia ser removido (RPCsoft_deletecom whitelist semclientes)fix—Button asChildnão implementava Radix Slot (warning + HTML inválido)feat—PageActionsMenuunifica ações de import/export em todas as páginasfeat— Popover Filtros em/gastos(status pills + cartão/forma agrupados + chips ativos)feat— Sincronizar do histórico em assinaturas: detecta ~80 marcas + recorrência customfeat— Mover 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 Postgressoft_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
Migrationsoft_delete_add_clientes_workspace_check:
- Adiciona
clientesecliente_cobrancasà whitelist - Para essas tabelas, troca o ownership check de
usuario_id = auth.uid()(que quebra em workspace compartilhado) para join comworkspace_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 temsupabase/migrations/). Para versionar essa mudança em código, criar essa estrutura.
2. Fix: Button asChild
Sintoma
Warning recorrente no console:Causa
O componenteButton 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:@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
Componentecomponents/page-actions-menu.tsx com Radix DropdownMenu:
- Mobile (
< sm): só íconeMoreVertical(3 pontos) - Desktop (
≥ sm):⋮ Mais ações - Itens agrupados com
DropdownMenuLabel(Importar / Exportar / Ações) ExportCsvMenuItemreutilizável que renderiza comoDropdownMenuItemem 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
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
⚙ Filtroscom 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
- 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. - Dicionário pequeno — só ~30 marcas de streaming/cloud/edu. Sem Hostinger, Nuvemshop, Gemini, AWS, Adobe, Hotmart, etc.
Fixes
Emapp/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
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)
- 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
| Entidade | O que zera ao mover |
|---|---|
| Gasto | cartao_id + categoria_id (se customizada) |
| Parcela | cartao_id + categoria_id (se customizada) |
| Assinatura | nada (categoria é string livre, sem FKs por-workspace) |
| Receita | cliente_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
Commits
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