bad828cab3f176c9d89900a645588035bd1e54aa
223 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bad828cab3 |
Melissa pages: esconde page-title-icon em mobile
No mobile o botao "Menu Lancamentos/Notificacoes/etc" ja indica a sessao,
entao o pi-list/pi-bell ao lado do contador era redundante. Adiciona
.<prefix>-page__title-icon { display: none; } no @media max-width: 1023px.
Em MelissaConversas usa > i:first-child (icone nao tem classe dedicada).
Pages: FinanceiroLancamentos, Compromissos, Documentos, CadastrosRecebidos,
Conversas, AgendamentosRecebidos, Financeiro, Grupos, Notificacoes, Tags,
Medicos, Relatorios, Recorrencias.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
02af119dc6 |
Melissa drawers: footer colado no bottom (pattern AppMenu)
Refator do mobile drawer em todas as Melissa Pages com sidebar: scroll move pra dentro de .xx-side__scroll (flex: 1 + min-height: 0) e o __footer vira flex-shrink: 0 last child de flex column. Espelha o pattern do AppMenu/layout-sidebar Rail. Substitui o sticky/margin:auto que falhava quando o conteudo era pequeno (deixava espaco vazio sob o "Limpar filtros"). Pages: Compromissos, Conversas, Documentos, FinanceiroLancamentos, Grupos, Medicos, Notificacoes, Pacientes, Recorrencias, Relatorios, Tags. Pacientes (caso especial): mp-quick fixo no topo (max-height: 50%) + mp-side flex: 1 com scroll/footer interno. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
48bf2726a5 |
Drawer mobile + footer colado + Menu nomeado + tenant ensureLoaded
Tres ajustes globais nas Melissa Pages com sidebar:
1) FOOTER "Limpar filtros" colado no bottom do drawer mobile
Problema: o sticky bottom precisa que algum container parent
tenha altura definida e overflow. No drawer, o `.xx-side` tinha
`height: auto` — entao o footer ficava no fluxo natural (logo
apos os cards) mesmo com pouco conteudo, em vez de empurrado pro
bottom do drawer.
Fix: `.xx-mobile-drawer__scroll .xx-side` ganha
`flex: 1; min-height: 0; display: flex; flex-direction: column`
pra ocupar altura disponivel; o `.xx-side__footer` ganha
`margin: auto -12px -24px` (margin-top: auto empurra pro fim).
Sticky bottom continua pro caso de scroll com muito conteudo.
Aplicado em: Compromissos, Grupos, Tags, Medicos, Conversas,
Recorrencias, Pacientes (caso especial — separa .mp-side de
.mp-quick), Cadastros Recebidos, FinanceiroLancamentos.
2) DRAWER MOBILE adicionado em Notificacoes, Documentos e
Relatorios (estavam com sidebar virando topo via max-height
50vh — faltava o pattern oficial das demais Melissa Pages).
Pattern aplicado:
- Aside host com id="<prefix>-mobile-drawer-target" + Transition
backdrop com fade
- Botao "Menu <Secao>" no header (esquerda do titulo)
- <Teleport :disabled="!isMobile"> envolvendo a sidebar
- Script: drawerOpen + isMobile + matchMedia listener registrado
no onMounted, removido no onBeforeUnmount
- CSS completo: .xx-mobile-drawer (fixed, transform translateX),
__scroll (overflow + padding), __backdrop (rgba 0.45 + blur),
overrides quando teleportada (sidebar perde bg/border-right,
footer vira sticky bottom com margin-top auto)
3) Botao "Menu" passa a ter sufixo da pagina:
- "Menu Lancamentos" (FinanceiroLancamentos)
- "Menu Notificacoes" (Notificacoes)
- "Menu Documentos" (Documentos)
- "Menu Relatorios" (Relatorios)
- "Menu Agendamentos" (AgendamentosRecebidos — corrigido tambem)
4) Bug de "lista vazia ao carregar via URL direto":
FinanceiroLancamentos e Relatorios usam composables que dependem
de tenantStore.activeTenantId. Quando aberta direto via URL
(sem navegar pelo menu), o tenantStore pode nao estar inicializado
ainda — entao fetchRecords() / loadSessions() retornam vazio.
Fix: adicionar `await tenantStore.ensureLoaded()` no onMounted
antes do fetch. Ja era pattern usado em outras Melissa Pages
(Compromissos, etc).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
532204708e |
Documentos + Templates + Relatorios nativas (so resta online-scheduling)
Promove '/melissa/documentos', '/melissa/documentos-templates' e '/melissa/relatorios' do embed pra paginas nativas Melissa. MelissaDocumentos (~700L): - Sidebar com stats (Total / Tamanho / Tipos / Pendentes amber) + filtro Tipo (Select com TIPOS_DOCUMENTO 11 opcoes) + filtro Tag (Select dinamico com usedTags) + footer fixo Limpar filtros - Main: toolbar busca + lista de DocumentCard (componente reusado) - Modo "todos os pacientes" — patientId null. Upload/Gerar exigem abrir paciente especifico no prontuario (botoes nao aparecem). - Dialogs reusados: PreviewDialog + SignatureDialog + ShareDialog + ConfirmDialog (delete). MelissaDocumentosTemplates (~700L): - Layout 1-col empilhado, 3 views: list / create / edit - Header com botao "Novo template" (list) ou "Cancelar/Salvar" (create/edit) + back button - 2 sections distintas: "Templates padrao do sistema" (info-blue, click duplica) e "Meus templates" (accent, click edita + menu de acoes Duplicar/Editar/Desativar) - Cards em grid responsivo (auto-fill 280px), com badge "padrao"/ "inativo" e count de variaveis - DocumentTemplateEditor reusado pra create/edit - ConfirmDialog reusado MelissaRelatorios (~1100L): - Sidebar com 6 stats (Total / Realizadas verde / Faltas red / Canceladas warn / Agendadas info / Taxa realizacao) + filtro Periodo (button list: semana/mes/3meses/6meses) + filtro Status (Realizadas/Faltas/Canceladas/Agendadas com cores) + footer Limpar filtros - Main: card Grafico (Chart.js stacked bar agrupado por semana/mes) + card DataTable de sessoes filtradas (Data/Hora sortable / Paciente / Sessao / Modalidade / Status) - Empty states distintos: sem sessoes no periodo / sem resultado do filtro Logica preservada das paginas originais. Composables/services nao foram tocados — apenas adaptacao do chrome pra blueprint Melissa. DocumentsListPage / DocumentTemplatesPage / RelatoriosPage continuam intactas no layout Rail (/therapist/*, /admin/*). Wire-up MelissaLayout: imports + 3 render blocks + 'documentos', 'documentos-templates', 'relatorios' literais em NON_CONFIG_SLUGS; removidos de MELISSA_EMBED_KEYS. Entries removidos do EMBED_MAP em MelissaEmbed (resta apenas 'online-scheduling'). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
387043b3b2 |
MelissaFinanceiro + MelissaFinanceiroLancamentos nativas
Promove '/melissa/financeiro' e '/melissa/financeiro-lancamentos' do
embed pra paginas nativas, eliminando o triplo header.
MelissaFinanceiro (dashboard, ~700L):
- Layout 1-col empilhado (sem sidebar — so cards de resumo)
- Header com ícone wallet + titulo + badge mes corrente +
botao "Ver lancamentos" + Recarregar + Voltar
- Subheader explicativo
- 4 cards empilhados:
1. Quick stats grid (Recebido verde / Pendente amber / Vencido red /
Despesas neutral)
2. Card Grafico Receita x Despesa (Chart.js bar, 6 meses)
3. Card Projecao de Caixa (cobrancas em aberto, proximos 6 meses
com receita/despesa/saldo + count badge)
4. Card Ultimos lancamentos (DataTable 5 mais recentes)
- Click "Ver lancamentos" / "Ver todos" navega pra
/melissa/financeiro-lancamentos
MelissaFinanceiroLancamentos (lista, ~1100L):
- Blueprint tabular Melissa completo
- Header com botao "Lancamento manual" + Recarregar + Voltar
- Subheader
- Sidebar com __scroll + __footer fixo:
- Stats (Pendente amber / Vencido red / Pago verde / Total)
- Filtro Status (button list: Pendentes amber / Vencidos red /
Pagos green / Cancelados neutral) + X inline
- Filtro Tipo (Receita green / Despesa red) + X inline
- Filtro Paciente (Select com filter + identification_color dot)
+ X inline
- Filtro Periodo (DatePicker range vencimento) + X inline
- Footer fixo "Limpar filtros" (Transition fade+collapse)
- Main: DataTable lazy + paginator com 7 colunas (Paciente +
avatar / Sessao / Tipo / Valor + desconto / Vencimento / Status /
Acoes). Row overdue com bg vermelho tinted.
- Acoes por status:
- pending/overdue: botoes "Receber" (abre dialog pagamento) +
"Cancelar" (Confirm)
- paid: badge "metodo + data"
- cancelled: travessao
- Mobile: sidebar vira topo (max-height 50vh)
Dialogs preservados:
- Registrar pagamento (5 metodos com icones: pix/deposito/dinheiro/
cartao/convenio)
- Lancamento manual (Paciente opcional + Valor + Desconto + Valor
final read-only + Data vencimento + Metodo opcional + Obs)
Logica preservada do composable useFinancialRecords + RPCs
(get_financial_summary, list_financial_records, view
v_cashflow_projection, mark_as_paid, cancel_record,
create_manual_record).
FinanceiroDashboardPage e FinanceiroPage continuam intactas no
layout Rail (/admin/financeiro, /therapist/financeiro).
Wire-up: imports + render blocks + 'financeiro' e
'financeiro-lancamentos' em NON_CONFIG_SLUGS; removidos de
MELISSA_EMBED_KEYS. Entries removidos do EMBED_MAP em MelissaEmbed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f9145442ae |
MelissaNotificacoes nativa (mesmo design do LinkExterno)
Promove '/melissa/notificacoes' do embed pra pagina nativa Melissa,
eliminando o triplo header (layout + embed + hero sticky da
NotificationsHistoryPage interna).
Layout 2-col seguindo o mesmo blueprint Melissa:
- Header: titulo + count badge + pill amber "X nao lidas" quando
unreadCount > 0 + botao "Marcar todas lidas" (visivel se ha unread)
+ Recarregar + Voltar.
- Subheader explicativo.
- Sidebar (~280px) com __scroll + __footer fixo:
- Stat card (Total / Nao lidas amber / Lidas verde / Arquivadas)
- Filter card "Status" (button list: Todas / Nao lidas / Lidas /
Arquivadas) + X inline pra voltar pro padrao 'all'
- Filter card "Tipo" (button list: Agendamento red / Novo paciente
sky / Recorrencia amber / Sessao orange / Mensagem emerald —
cores espelham typeMap) + X inline
- Footer fixo "Limpar filtros" (Transition fade+collapse) — zera
busca + tipo + reseta status pra 'all'
- Main: toolbar com busca por titulo/descricao + lista de
notificacoes com row design preservado:
- Border-left colorido por tipo (--mn-row-color via inline style)
- Icone do tipo + avatar circular primary com iniciais
- Body: titulo + type pill colorido + arquivada pill (se aplicavel)
+ detail (2 lines clamp) + tempo relativo
- Hover actions: marcar lida/nao lida + arquivar/desarquivar +
remover (com ConfirmDialog)
- is-unread: bg primary tinted; is-archived: opacity 0.7
- Mobile (<1024px): sidebar vira topo (max-height 50vh), main fica
abaixo, actions sempre visiveis (sem hover).
Logica preservada da NotificationsHistoryPage:
- load() do supabase ('notifications' eq owner_id, limit 500)
- markRead/markUnread/archive/unarchive/remove + sync notifStore
- markAllRead em batch
- handleRowClick: inbound_message abre conversationDrawer (paciente
ou anonimo via from_number); outras com deeplink fazem
router.push (e fecham a Melissa pra navegar pra rota Rail);
todas marcam como lida automaticamente.
Wire-up MelissaLayout: import + render block + 'notificacoes'
literal em NON_CONFIG_SLUGS; removido de MELISSA_EMBED_KEYS. Entry
removido do EMBED_MAP no MelissaEmbed.
NotificationsHistoryPage continua intacta — segue funcionando no
layout Rail (/therapist/notifications, /admin/notificacoes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ee084c2918 |
Pacientes restore unificado + MelissaLinkExterno nativa
Trabalho de continuidade pós-blueprint: A) Botao "Restaurar" visivel direto na linha da PatientsListPage (layout Rail) quando paciente.status === 'Arquivado' — atalho pra usuarios que filtram por arquivados sem precisar abrir o menu de "..." (que ja tinha "Reativar" via PatientActionMenu). Icone pi-undo + label "Restaurar" + tooltip + click chama reactivatePatient do usePatientLifecycle. Aplicado tanto no DataTable desktop quanto nos cards mobile. B) Consolidacao: removido restorePatient do patientsRepository (era duplicado com reactivatePatient do usePatientLifecycle). MelissaPacientes agora consome reactivatePatient direto, fonte unica de verdade pra toda transicao de status pra 'Ativo'. C) MelissaLinkExterno (nova pagina nativa Melissa). Substitui o embed via MelissaEmbed que duplicava 3 headers (layout + embed + hero sticky da pagina interna). Lógica preservada (RPC issue_patient_invite + rotate_patient_invite_token_v2 + copy/openLink), so o chrome muda pra casar com o blueprint Melissa: 1 header com status pill (Link ativo/Gerando) + botao "Gerar novo link" + Recarregar + Voltar; subheader explicativo; body 2-col (esquerda card "Seu link publico" com InputGroup + 2 CTAs grandes + card "Mensagem pronta"; direita cards "Como funciona" + "Boas praticas"); mobile vira 1-col. PatientsExternalLinkPage continua intacta — segue funcionando no layout Rail. Wire-up no MelissaLayout: import + render block + 'link-externo' literal em NON_CONFIG_SLUGS; removido de MELISSA_EMBED_KEYS. Entry removido do EMBED_MAP no MelissaEmbed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
97b0ec1ec5 |
HANDOFF + log atualizados pra sessao 2026-05-06
- HANDOFF.md reescrito refletindo estado atual: working tree limpa, 5 commits criados na sessao, resumo do que foi feito (6 Melissa Pages blueprint + dialogs harmonizados + ConversationDrawer WhatsApp + bug fix de cores no MelissaPacientes), e o que continua pendente (A66 V2 design aguardando feedback + restore na PatientsListPage) - Obsidian/Brain/log.md: entrada da sessao 05-06 anexada com detalhes e referencias dos 5 commits Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
15103eded5 |
Cleanup: backups antigos removidos + dashboard config + HANDOFF/log
- Remove database-novo/backups/2026-03-27 e 2026-03-29 (deveriam estar no gitignore, mas haviam sido tracked antes) - Atualiza db.config.json + generate-dashboard.cjs + dashboard.html - HANDOFF.md atualizado com estado de 05-05 (sprint blueprint tabular + arquivamento de pacientes) - Obsidian/Brain/log.md: entrada da sessao 05-05 adicionada Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
98f7252dcd |
Melissa: 6 Pages aplicando blueprint + dialogs unificados + Conversa estilo WhatsApp
Sprint F (05-06). Blueprint tabular aplicado nas 6 paginas restantes;
dialogs harmonizados (FloatLabel + IconField + variant=filled + section
dividers, espelhando PatientsCadastroPage Identidade); ConversationDrawer
repaginado pra visual estilo WhatsApp.
Pages refatoradas (cada uma com subheader, sidebar __scroll + __footer
fixo "Limpar filtros", Xs inline pra zerar filtro individual, mobile
drawer com sticky footer):
- MelissaCompromissos: blueprint mantendo row design original (color
stripe + name + badges + descricao + meta inline). Filtros Status
(Ativos/Inativos) + Tipo (Nativos/Meus). Coluna Acoes frozen 140px
com toggle+pencil+trash.
- MelissaGrupos / MelissaTags: pattern completo + dialog "Pacientes
do grupo/tag" com lista vinculada via patient_group_patient /
patient_patient_tag. Avatar primary nos pacientes, header colorido
com cor da entidade, X de fechar igual .mc-close. Dialog de
criar/editar com FloatLabel + section dividers.
- MelissaMedicos: blueprint + dialog "Pacientes encaminhados" usando
cor primary do tema (medicos nao tem cor propria); dialog de
criar/editar com 4 secoes (Identificacao/Contato/Localizacao/Obs)
espelhando PatientsCadastroPage. Service ja tinha
fetchPatientsByMedicoNome (ILIKE em encaminhado_por).
- MelissaConversas: subheader, sidebar com bg-soft + border-right e
cards com sombra (mw-w--side), Limpar filtros global no footer fixo
(fix bug: filters era ref({...}) e eu lia filters.search direto, agora
usa .value), alerta de unlinked movido pro topo, kanban mobile com
min-height nas colunas pra mostrar mensagens.
- MelissaRecorrencias: subheader, button list de status (Ativas verde/
Encerradas vermelho/Todas) substitui SelectButton, busca por nome do
paciente, footer Limpar filtros, X inline no filtro Status.
ConversationDrawer redesign (WhatsApp-style):
- Header com avatar circular primary + iniciais + numero formatado
- Container de mensagens com bg "papel de parede" (color-mix com bege
esverdeado WA + radial-gradient pattern)
- Bolhas com cantos certos (top-left ou top-right zerado simulando
tail), sombra sutil, cores autenticas (#d9fdd3 light/#005c4b dark
outbound; #fff/#202c33 inbound), detecao dark via :global
- Time HH:MM + status overlay no canto inferior direito DENTRO do
balao; checks azuis quando lida (#53bdeb)
- Compose pill rounded-full + botao Send circular verde #00a884
- Removido fmtDateTime obsoleto (substituido por fmtTimeOnly)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
269b531158 |
Melissa: blueprint tabular + Cadastros/Agendamentos/Pacientes + restore
Sprint E (05-05). Blueprint tabular oficial pras paginas Melissa de
listagem (DataTable + sidebar com stats e filtros coloridos, view
toggle list/grade, subheader explicativo, mobile pencil+popover).
Novo arquivo:
- blueprints/melissa-table-page-blueprint.md (~530L, 18 secoes) —
referencia canonica MelissaCadastrosRecebidos
Paginas refatoradas/criadas:
- MelissaCadastrosRecebidos: refator pra blueprint (DataTable + frozen
action + view toggle + subheader)
- MelissaAgendamentosRecebidos (NOVO): substitui o embed via
MelissaEmbed; 4 status coloridos (Pendente/Autorizado/Convertido/
Recusado), 3 acoes condicionais (Recusar/Autorizar/Converter em
sessao), wired com AgendaEventDialog
- MelissaPacientes: refator parcial (subheader, sombras, status pills
coloridas, email/phone colunas proprias, mobile pencil+popover, fix
scroll mobile com min-height:0 na .mp-list, view toggle persistido,
tags/grupos color fix g.cor->g.color, restore de arquivados)
- MelissaEmbed: agendamentos-recebidos removido do EMBED_MAP
- MelissaLayout: wire-up MelissaAgendamentosRecebidos nativo
- composables/useMelissaPacientes + useMelissaPacientesAside ajustes
Restore de pacientes arquivados:
- patientsRepository: novo restorePatient(id, { tenantId })
- PatientsCadastroPage statusOpts: +Arquivado (fecha gap de
inconsistencia ao editar paciente arquivado)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6d9b36d592 |
A66 WIP: AgendaEventDialog quebrado em 5 composables + 265 specs + V2 esqueleto
Sub-sessao 1 entregue (composables): - agendaEventHelpers (262L) — utilitarios puros (date, format, parse) - useAgendaEventComposer (485L) — montagem do form + validacao - useAgendaEventActions (387L) — save/delete/cancel/move actions - useAgendaEventPickerBilling (378L) — pickers (terapeuta, servico, convenio) + calculo de billing - useAgendaEventLifecycle (474L) — open/close/dirty state + autosave - 5 specs em __tests__/ (75+76+28+43+43 = 265 testes), 495/495 passing AgendaEventDialog: 3522 -> 2632 linhas (-25%) consumindo os composables. Backup byte-identico em AgendaEventDialog.vue.bak pra rollback. Sub-sessao 2 entregue (esqueleto, NAO TESTADO): - AgendaEventDialogV2 (~1100L, 3 zonas: PACIENTE/QUANDO/O QUE) - Preview em /preview/agenda-dialog-v2 com 5 cenarios - Rota em routes.misc.js - User testou e nao gostou do design — aguarda feedback especifico pra iteracao na sub-sessao 3 (migracao nos 9 consumers). Dialogs auxiliares novos pro AgendaEventDialog: - InsurancePlanQuickCreateDialog (criar convenio inline) - ServiceQuickCreateDialog (criar tipo de sessao inline) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
957e912a7f |
Melissa polish + Prontuario Visao Geral + agenda historico
Sprints B (05-03) e C (05-04) acumulados: - NotificationDrawer/Item redesign (visual mais limpo, ações inline) - Dock pins compose (useMelissaDockPins) + cache store global (melissaCacheStore) - MelissaAgenda: timeline FullCalendar parity + cards resumo, histórico card com useMelissaAgendaHistorico, MelissaEventoPanel ajustado - useFeriados: cache opt-in pra evitar fetch redundante de feriados - PatientProntuario: aba Visão Geral nova; PatientConversationsTab polish - AgendaClinicMosaic / AgendaTerapeutaPage / useAgendaSettings: ajustes de paridade com Melissa - DocumentsListPage: pequenos ajustes - DB migration 20260504000001: fix do trigger pra status 'excluido' nas cancel_notifications Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
86311ef305 |
Melissa: hub Configuracoes + Embed + 9 Pages novas + dialog blueprint dark
Sprints 04-29 + 04-30 acumuladas. - MelissaConfiguracoes: hub 2-col com 6 grupos (Layout/Conta/Agenda/ Financeiro/WhatsApp/Sistema), tudo embedado via MelissaEmbed. - MelissaEmbed: wrapper generico que injeta layout-variant=melissa e remove cromos pra reaproveitar Pages tradicionais. - 9 Melissa Pages novas: CadastrosRecebidos, Compromissos, Configuracoes, Conversas, Embed, Grupos, Medicos, Recorrencias, Tags. - Dialog blueprint atualizado: bg-gray-100 (hardcoded light) -> bg-[var(--surface-ground)] (tema-aware). 22 dialogs migrados em 9 arquivos. Anti-pattern documentado. - PatientsCadastroPage: bug fix dropdown Grupo (optionLabel nome->name), toggle vertical/abas com persist localStorage, sticky margin-top. - Surface picker no popover do MelissaLayout (8 swatches). - useTopbarPlanMenu, useMelissaWhatsapp, useMelissaPacientesAside novos. - Migration: status agenda remarcado/confirmado. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
269c380d9c |
Wiki/grafo: graphify + wiki-brain setup compartilhado pra equipe
CLAUDE.md com 3 secoes (navegacao, regras de sessao, doc pra equipe). Vault Obsidian/Brain/ commitado pra time editar conhecimento curado. graphify-out/ no gitignore (regeneravel via /graphify src/). Binarios do Obsidian.exe ignorados, so vault Brain/ vai pro repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b331d68572 |
Test: classifySendError cobre 22 casos de erro de envio WhatsApp
Exporta classifySendError (era privada do store) pra poder testar isolada. A funcao e' deterministica e pura, entao spec direto vale mais que stub do supabase.functions.invoke. Cobertura: - 5xx downstream (502/503/504) -> banner "fora do ar" com 2 CTAs (Configurar + Comprar creditos), incluindo o case sem code - http_500 explicitamente NAO cai no ramo 5xx (e' catch geral, nao "downstream fora") — checagem de regressao - insufficient_credits, canal nao configurado/inativo (3 variacoes de string), credenciais evolution/twilio incompletas - evolution retornou X (com e sem status 5xx — confirma precedencia dos ramos), twilio_send_failed_<code> - auth (sessao expirou), forbidden (sem permissao) — ambos sem CTA - "Edge Function returned a non-2xx" wrapper do supabase-js - Fallback generico: code desconhecido com message custom; code+message vazios -> mensagem padrao - Robustez: case-insensitive (INSUFFICIENT_CREDITS -> reconhece), status nao-numerico -> null em vez de NaN, codes nao-string (undefined/number/object) nao quebram Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
76b58af9a1 |
Melissa: promove rota oficial + redirect automatico da home
Sai do estado "preview/sandbox" e liga o Melissa como layout real
ativavel pelo user em Configuracoes -> Profile.
Mudancas:
- routes.misc.js: /preview/melissa/:secao? -> /melissa/:secao?,
nome PreviewMelissa -> Melissa. Sem alias por compat (autorizado).
- router/index.js: novo beforeEach apos o supportGuard e antes do
applyGuards. Quando to.name e' therapist.dashboard ou admin.dashboard
E localStorage.layout_variant === 'melissa' E viewport >= 1280px,
redireciona pra { name: 'Melissa' }. Le do localStorage (gravado pelo
bootstrapUserSettings + setVariant) pra evitar esperar store do DB e
evitar flash do shell antes do redirect. Bypassa mobile pq Melissa
nao foi feito pra <xl e o effectiveVariant ja forca 'classic' la.
- MelissaLayout.vue: 2 chamadas router.push apontavam pra
'PreviewMelissa', agora 'Melissa'. Header doc atualizado.
- useMelissaPacientes.js: comment doc citando /preview/melissa
generalizado pra "sem session retorna vazio".
- ProfilePage.vue: card Melissa perde badge "Em construcao" e ganha
badge "Beta". Texto explicativo perde "navegacao completa ainda
nao esta integrada" e ganha "Ao salvar, sua proxima entrada na
home cai direto no Melissa". Link /preview/melissa -> /melissa.
Remove regra CSS .lv-card--wip orfa.
Tradeoff aceito: rotas especificas (/therapist/agenda etc.) seguem
no shell classico/rail. So a HOME do role e' interceptada pra /melissa.
Coerente com o desenho atual do MelissaLayout, que ja abre Agenda /
Pacientes / etc. como overlays internos via deep-link /melissa/<secao>.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
68d601e0f4 |
MelissaPacientes: title="" -> v-tooltip pra alinhar com MelissaAgenda
10 botoes da pagina (header, filtros side, acoes inline do card, fechar selecao da quick view) usavam o atributo HTML title nativo, fora da convencao do projeto. Substitui por v-tooltip do PrimeVue (auto-registrado via PrimeVueResolver) com posicao explicita por contexto: bottom no header, top nas acoes, left no close da detail. Sem mudanca funcional — apenas visual e de consistencia. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
629e7ce18e |
DB: melissa_prefs em user_settings + 'melissa' como layout_variant
Migration nova (database-novo/migrations/20260427000001_*):
- ALTER TABLE user_settings ADD COLUMN melissa_prefs jsonb DEFAULT '{}'
NOT NULL — guarda toqueTermino, overlayOpacity, bgImageOpacity, use24h,
cardsAtivos[] e cardsLayout. Sanitizacao no client antes do upsert.
- bgUrl (data URL da foto, MBs) NAO entra aqui — segue em localStorage
ate migrarmos pra Supabase Storage.
Schema canonico (tenants_multi_tenant.sql) atualizado em paralelo:
- mesma coluna melissa_prefs jsonb
- check de layout_variant agora aceita 'melissa' alem de 'classic' e
'rail' (precondicao pra plugar o tema Direcao B no preference real)
Leitura/escrita no client ainda pendente — feita em sessao separada.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
06bce11e1c |
Melissa: deep-link via URL + Pacientes (WIP) + cronometro reset
Roteamento por URL (substitui o ref local secaoAberta):
- routes.misc.js: rota vira /preview/melissa/:secao? — param opcional
- MelissaLayout.vue: secaoAberta agora e computed do route.params.secao,
validado contra SECOES (chave invalida -> null). abrirSecao/fecharSecao
fazem router.push em vez de mutar ref. Habilita back/forward, refresh
e deep-link tipo /preview/melissa/agenda.
Pagina Pacientes (WIP, ainda nao wireada no slot do Layout):
- src/layout/melissa/MelissaPacientes.vue (novo, ~? linhas) — fullscreen
3-col espelhando MelissaAgenda: aside esquerda com filtros (status /
grupos / tags), lista central com cards + busca, quick view direita
com KPIs do paciente selecionado + acoes.
- Carrega pacientes (todos os status), grupos/tags do tenant, vinculos
patient_groups + patient_tags + session counts em paralelo.
- Integra PatientProntuario (overlay), PatientCadastroDialog,
PatientCreatePopover + ComponentCadastroRapido, e
conversationDrawerStore (acao WhatsApp da quick view).
useMelissaPacientes ganha opcao { onlyActive }:
- default true (compat com cards do resumo / cronometro / eventos hoje
— so faz sentido com ativos)
- false retorna Ativo + Inativo + Arquivado, pra uso na pagina nova
- select agora inclui data_nascimento (necessario pros KPIs da quick view)
Cronometro: zera ao parar — terminou a sessao, fica pronto pra proxima
sem precisar reabrir o popover.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
7b67bd083a |
Melissa Agenda: breakpoint compact + drawer mobile teleportado
Dois pontos de quebra agora: - <xl (<=1279px) "compact": view-switcher (Dia/Semana/Mes/Lista) sai da toolbar e entra no menu "Acoes" com check icon no ativo. Filtros tambem migram pra dentro pra nao inflar a barra. - <lg (<=1023px) "mobile": .ma-side e .ma-widgets viajam pra fora do .ma-page via Teleport, num <aside class="ma-mobile-drawer"> sempre presente no DOM (v-show controla display) — garante target valido desde o mount. Botao "Menu" mobile-only aparece a esquerda do header. Backdrop entre drawer e .ma-page com Transition de fade. Bonus styles.scss: fix borda dupla do FullCalendar. .fc-scrollgrid em light mode mantinha borda externa que somada com a borda das celulas da ponta dava 2px na borda do calendario. Zera o contorno do contairner — celulas (td/th) ja desenham a grade visual. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dac3198873 |
Drawer WhatsApp: banner persistente em erros de envio
friendlySendError (string única) virou classifySendError, que devolve
{ code, status, message, hint, action, secondaryAction }. UI passa a
renderizar banner persistente no chat (não só toast efêmero) com título
+ dica explicativa + CTA contextual.
Casos cobertos:
- 502/503/504 -> "Servidor de WhatsApp fora do ar" + CTA Configurar +
CTA Comprar créditos (caso ainda não tenha contratado)
- insufficient_credits -> CTA Comprar créditos
- canal nao configurado / inativo -> CTA Configurar agora
- credenciais evolution incompletas -> CTA Configuracoes WhatsApp
- twilio credenciais incompletas -> sem CTA (fala pra contatar suporte)
- evolution retornou ... -> CTA Ver status
- twilio_send_failed... -> CTA Configuracoes WhatsApp
- auth -> "sessao expirou", sem CTA
- forbidden -> sem CTA
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
a57cf27a6a |
Fix TDZ no autosave do cadastro externo
O watch de scheduleProgressSave referenciava form.* antes da declaração do reactive form, violando TDZ e quebrando a página inteira no load. Move o watch pra depois da `const form = reactive(resetForm())`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ffcb8b17f9 |
Melissa Agenda: paridade com AgendaTerapeuta + responsivo mobile
Composable useMelissaAgenda (~1150 linhas, exclusivo Melissa): - Orquestra useAgendaEvents + useRecurrence + useDeterminedCommitments + useFeriados + useCommitmentServices - 7 cases de save (avulso, recorrente C, somente_este D, este_e_seguintes E, todos F, todos_sem_excecao G + tratamento de exclusion constraint) - 3 cases de delete (somente_este, este_e_seguintes, todos com encerrar série) - onCreateEvento (botão Agendar), onSelectTime com cap de 120min, persistMoveOrResize com confirm dialog descritivo e bold em datas/horas - Bloqueio: openBloqueioDialog(mode) com 4 modos MelissaLayout: - Provide composable via MELISSA_AGENDA_KEY (inject em MelissaAgenda) - Renderiza AgendaEventDialog + BloqueioDialog + ConfirmDialog - Slot #message v-html pra renderizar HTML em messages do confirm - onEditEvento liga panel ao dialog completo (B3 não-stub) MelissaAgenda: - Drop useMelissaEventosRange — eventos vêm do composable injetado - Drag/resize/select-to-create habilitados quando há composable - Cluster Paciente + Agendar (50/50 primary) - Toolbar: timeMode (24/12/Meu) + onlySessions + bloquear-menu (desktop) - Header: Pacientes (mobile-only, abre drawer) + Configurações + Fechar - Mobile <lg: aside + widgets viram drawer off-canvas (slide esquerda); calendar fullwidth; "Ações" menu mobile concentra timeMode/onlySessions/ bloquear; backdrop com click-outside MelissaEventoPanel (B3 estático-revisado): - Substitui panel inline que crashava em campos inexistentes - Action bar agrupada (status / paciente / geral) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ff3695fbb1 |
HANDOFF 2026-04-27: bug Teleport + B1 toolbar + B2 stats; B3 pendente teste
Sessão de domingo curta. Bug do chip resolvido pela manhã, polimento da Agenda à tarde (toolbar + stats interativos), à noite extração do MelissaEventoPanel novo (não testado em browser, fica pra amanhã). Working tree não commitado: B3 (MelissaEventoPanel novo + handlers no MelissaLayout + patient_id no normalize + defineExpose). Ver seção "PENDENTE DE TESTE" no HANDOFF pra plano de validação. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6a92735366 |
Melissa Agenda: toolbar polish + stats interativos com filtro
B1 — Toolbar - Cluster Hoje + chevrons num pill único (mais coeso) - Título com flex+ellipsis (some min-width:130px que truncava feio em view Mês/Lista) - Botão "Hoje" disabled visual (opacity 0.45) quando hoje cai no range visível — antes ficava idêntico, sem affordance - title="" → v-tooltip.top nos chevrons (memória: tooltips PrimeVue) - focus-visible com outline accent em todos os botões da toolbar - Visual refinado: padding/font-weight, view-btn ativo com box-shadow B2 — Stats interativos - Click no stat filtra fcEvents + sessoesHoje pelo predicado correspondente (Total/Sessões/Realizadas/Faltas — feriados continuam sempre) - Stat ativo ganha borda accent + bg color-mix - Stats com value=0 ficam disabled (cursor:not-allowed, opacity 0.4) - Click no stat ativo limpa o filtro - Chip flutuante "Filtrando: X" no canto sup direito do FC, click limpa - Tooltip dinâmico explicando a ação esperada Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f2b15ce0f7 |
HANDOFF + cleanup: bug Teleport resolvido, backups antigos removidos
- HANDOFF.md atualizado: bug do chip do cronômetro resolvido em 2026-04-27. Causa-raiz documentada (múltiplos Teleports compartilhando target + Transition>Element v-if gera comment placeholder VNode → emitsOptions:null no shouldUpdateComponent) e fix oficial (Transition envolvendo Teleport). - Backups locais 2026-03-23 removidos do índice (já estavam .gitignored, apenas saneamento). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1bcb969f72 |
Layout Melissa (Direção B): preview, /profile, Agenda, dock, cadastro
Sandbox completo do novo layout Win11 lockscreen-style. Não troca o
AppLayout atual — Fase 5 (router wire-up) fica pra sessão dedicada.
Estrutura
- src/layout/melissa/ — MelissaLayout (bg+ψ+overlays), MelissaCronometro,
MelissaAgenda (fullscreen), MelissaCard, MelissaMenu, MelissaBusca
- composables/useMelissaEventos.js — semana real do FC + range mensal
pros dots do mini-cal
- composables/useMelissaPacientes.js — agora retorna created_at p/ "novo"
- melissaToques.js — toques Web Audio do término
Rota e persistência
- /preview/melissa (sem auth, sem AppLayout)
- /account/profile ganha 3º card "Melissa" com badge "Em construção"
- bootstrapUserSettings + layout composable aceitam variant='melissa'
- Migration: CHECK constraint user_settings.layout_variant aceita 'melissa'
Light mode
- Gradiente Bloom flipa via CSS vars (--bloom-c1/c2/base-1/base-2)
Dark: 400/300/950 · Light: 200/100/0
- Cronômetro/Personalização: color: white → var(--m-text)
- Pílula psi-kbd ganha tokens --m-kbd-bg/--m-kbd-text
- Override mapeia text-X-200/300/400 → text-X-600 (17 cores Tailwind)
Agenda fullscreen
- Mini-cal funcional: click pula FC, range visível destacado, dots reais
- Feriados nacional/municipal/personalizado (rose/amber/violet)
- Dias fechados (workRules) cinza apagado, mutex feriado vence
- Card "Hoje" (stats+sessões) mesclado e movido pra sidebar esquerda
- ProximosFeriadosCard reaproveitado entre mini-cal e Hoje
- Avatar paciente: bg --m-accent-strong → --m-accent (saturado em light)
- Cores light: 12 substituições color:white → var(--m-text)
Dock taskbar Win11-style
- .melissa-dock 76px fixed bottom (CSS global, não scoped — Vue static
hoisting perderia data-v-{hash})
- ψ centralizado vertical na faixa (bottom:10px)
- Chip cronômetro teleportado pro dock + animação minimize macOS
(dialog encolhe + voa pro canto bottom-left, 340ms cubic-bezier)
- transform-origin: 96px calc(100% - 38px) (posição do chip no dock)
Pacientes na sidebar
- Botão fake "+" no topo abre PatientCreatePopover (rápido/completo/link)
- Reaproveita PatientCadastroDialog + ComponentCadastroRapido
- Pacientes criados nos últimos 7d sobem pro topo + badge "novo"
Dock contextual (ações do paciente selecionado)
- Avatar + nome + count + 5 ações (sessões/whatsapp/prontuário/editar/fechar)
- Teleportado pro .melissa-dock quando há paciente selecionado
- Em mobile, ações vivem em <Menu> kebab por linha
- Pattern <Transition><Teleport v-if> obrigatório (NUNCA o contrário)
pra evitar comment placeholder + emitsOptions:null no reconciler
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ab103ec88b |
Fix admin adjust créditos WhatsApp: clamp silencioso vira erro vermelho
- Severidade dos toasts de validação: warn → error (não selecionar tenant, valor < 1, > WA_ADJUST_MAX, nada removível, excede max removível) - Remove :max do <InputNumber> no formulário — antes ele clampeava silenciosamente o valor digitado pro máximo permitido, escondendo o erro. Agora deixa o usuário digitar e estourar o toast vermelho do submit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
463d71ce44 | HANDOFF 2026-04-23: resumo da sessão + roteiro de testes pra amanhã | ||
|
|
f1c97ee906 |
Dashboard SaaS ganha seção de receita de créditos WhatsApp (Asaas)
Fecha o gap de analytics que faltava: MRR/ARR de assinatura já existia, mas não havia visão de receita dos créditos WhatsApp comprados via Asaas. Banco (migration 20260423000011) — 4 RPCs saas_admin only: - saas_wa_credits_revenue_stats(from, to): total arrecadado, count de compras, tenants únicos, créditos vendidos, ticket médio. - saas_wa_credits_top_packages(from, to): ranking top 10 pacotes por revenue, consolida nome atual se pacote foi renomeado. - saas_wa_credits_usage_summary(): snapshot atual de lifetime_purchased vs lifetime_used vs current_balance + taxa de consumo. - saas_wa_credits_revenue_evolution(from, to, bucket_days): série temporal pra sparkline. Todas com check is_saas_admin() no início + SECURITY DEFINER. Frontend: - useSaasCreditsAnalytics composable orquestra as 4 RPCs em paralelo com seleção de período (30d/90d/6m/12m) que ajusta bucket_days automaticamente. - SaasCreditsRevenueCard.vue: 4 KPIs (receita + ticket médio, compras + tenants, créditos vendidos, % consumo global), sparkline SVG com indicador de tendência, ranking top 5 pacotes. - Integrado no SaasDashboard logo antes da tabela "Distribuição por plano". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b8ea292ef1 |
Grupo 8: agenda ↔ WhatsApp completo (8.2 lembrar manual, 8.3 status→msg, 8.4 lead)
=== 8.2 Botão "Lembrar paciente" na agenda ===
Edge nova send-session-reminder-manual:
- Recebe {event_id}, autoriza (member ativo do tenant), resolve template
lembrete_sessao (custom → default global), envia via Evolution, registra
outbound em conversation_messages + log em session_reminder_logs com
reminder_type='manual'.
- Reusa lógica do cron reminders (sanitização, fmt datas, render template)
mas sem janela/dedup — terapeuta pode redisparar quantas vezes quiser
(log usa UPSERT; UNIQUE (event_id, reminder_type) sobrescreve).
Migration 20260423000008 adiciona 'manual' ao CHECK constraint de
session_reminder_logs.reminder_type.
UI: botão verde pi-whatsapp no footer do AgendaEventDialog (só em edit
de sessão com paciente vinculado). Confirm dialog + toast + erros
amigáveis (no_phone, invalid_phone, no_active_channel, template_not_found,
forbidden, send_failed).
=== 8.3 Status sessão dispara mensagem ===
Migration 20260423000009 cria trigger AFTER UPDATE OF status em
agenda_eventos: quando status muda pra cancelado/remarcado/confirmado,
dispara edge send-session-status-notification via pg_net (não bloqueia
o UPDATE). Settings app.settings.supabase_url/service_role_key reusadas.
Edge nova send-session-status-notification:
- Body {event_id, old_status, new_status}
- STATUS_TEMPLATE_MAP: cancelado→cancelamento_sessao, remarcado→
remarcacao_sessao, confirmado→confirmacao_sessao.
- Respeita opt-out (conversation_optouts), canal ativo, template
existente (tenant-specific → global default). Skip silencioso em
caso de falta de config.
- Insere outbound em conversation_messages (sem log unique — múltiplas
mudanças de status geram múltiplas mensagens por design).
=== 8.4 Intake abandonado vira lead no CRM ===
Migration 20260423000010:
- Adiciona 'in_progress' e 'abandoned_lead' ao CHECK de
patient_intake_requests.status. Colunas last_progress_at e
lead_thread_key.
- RPC convert_abandoned_intake_to_lead(intake_id): cria mensagem
placeholder inbound no CRM do tenant (thread_key anon:{phone}) +
conversation_notes com resumo dos dados coletados + marca status.
Edge save-intake-progress:
- POST {token, nome_completo?, telefone?, email_principal?, ...}
- Whitelist de campos (ALLOWED_FIELDS) pra proteger contra POST
malicioso tentar setar status/owner/etc.
- Busca por token, set status='in_progress' se era 'new', atualiza
campos enviados + last_progress_at.
Edge convert-abandoned-intakes (cron):
- Body opcional {idle_minutes} (default 30).
- Varre patient_intake_requests status='in_progress' + last_progress_at
mais antigo que cutoff. Filtra só os com nome_completo OU telefone
(contato mínimo pra valer lead). Chama RPC pra cada um.
Hook no form público CadastroPacienteExterno:
- Watch em nome_completo, telefone, email_principal, onde_nos_conheceu
dispara scheduleProgressSave() com debounce 1.5s.
- savePartialProgress só chama a edge se tem nome OU telefone.
- Silent fail — autosave não é crítico.
Cron do convert-abandoned-intakes NÃO ativado automaticamente (igual
heartbeat/SLA). Template comentado não está na migration — admin
descomenta SELECT cron.schedule manualmente quando quiser ligar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
c2c42a1620 |
3.7 Bot auto-triagem WhatsApp: config por tenant + hook nos inbound
Bot que coleta nome, motivo de busca e preferências ANTES do paciente
entrar no fluxo humano. Terapeuta abre a conversa e já encontra
resumo em conversation_notes.
Banco (migration 20260423000007):
- conversation_bots: config 1 por tenant. enabled, greeting/closing
messages, steps (JSONB array de {prompt, variable, type}), trigger_mode
(new_contact | all_unassigned | keyword), trigger_keywords[],
idle_timeout_minutes, respect_optout.
Defaults vêm com 4 perguntas úteis: nome, motivo, modalidade,
horário preferido.
- conversation_bot_sessions: estado por thread. current_step,
collected_data JSONB, status (active | completed | abandoned_idle |
abandoned_manual | opted_out). UNIQUE parcial garante 1 ativa por
(tenant, thread).
- RLS: leitura tenant/saas_admin, escrita admins (config) + service_role
(sessions, só edge altera).
Shared (_shared/whatsapp-hooks.ts):
- maybeProcessBot: carrega config, busca sessão ativa, avança step
com resposta, envia próxima pergunta via SendFn. Ao esgotar steps,
envia closing + cria conversation_notes com resumo das variáveis
coletadas. Se humano assume (conversation_assignments preenchido),
sessão marca 'abandoned_manual' e bot sai.
- Trigger modes:
- 'new_contact' (default): só inicia pra thread sem histórico bot
E sem paciente vinculado (lead real).
- 'all_unassigned': qualquer thread sem assignee.
- 'keyword': matched contra lista; normalizeForMatch já existe.
Integração nos inbound (ambos providers):
- evolution-whatsapp-inbound: chama maybeProcessBot após opt-in/opt-out,
ANTES do auto-reply. Se bot processou, skip auto-reply (senão duas
respostas sobrepostas).
- twilio-whatsapp-inbound: idem, usando makeTwilioCreditedSendFn pra
cobrar crédito de cada mensagem enviada pelo bot.
UI (/configuracoes/conversas-bots):
- Toggle enabled + Select trigger_mode + (se keyword) chips de keywords.
- Textareas greeting/closing.
- Editor de steps: reordenar (up/down), remover, add, editor com prompt
e variable (regex /^[a-z_][a-z0-9_]*$/).
- Botão "Padrão" restaura mensagens/steps default.
- InputNumber idle_timeout + toggle respect_optout.
- Card inferior: últimas 30 sessões (7 dias) com status, contato,
nome coletado (primeiro campo), progresso (step X/N), início.
- Entrada na landing de configurações + rota /configuracoes/conversas-bots.
Caveat conhecido: a resolução de conversation_notes.created_by usa
o primeiro admin ativo do tenant (pickAnyAdmin). Pra uma v2 seria
ideal ter um user "bot" sintético dedicado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
4e4bac622c |
6.3 Reconnect automático Evolution antes de abrir incident
Fluxo novo no heartbeat-check quando threshold vence:
1. Verifica se reconnect está habilitado (metadata.heartbeat_reconnect_
enabled, default true) E se API respondeu (sem fetchError) E se
passou do cooldown de 10min desde a última tentativa.
2. POST /instance/restart/{instance} na Evolution.
3. Aguarda 3s pra estabilizar + rechecka connectionState.
4. Se state voltou pra 'open': restaura connected + limpa
first_unhealthy_at + incrementa heartbeat_reconnect_count + resolve
qualquer incident aberto. Retorna action='auto_reconnected'.
5. Senão: atualiza heartbeat_reconnect_last_at (respeita cooldown) e
abre incident normalmente com details.reconnect_attempted=true.
Anti-loop: 1 tentativa por ciclo (não retry), cooldown de 10min/channel
pra não martelar Evolution nem gerar restart infinito. Tentativas são
contadas em metadata.heartbeat_reconnect_count (auditoria futura).
UI em /configuracoes/whatsapp-pessoal ganha novo toggle no card de
Monitoramento: "Tentar reconectar automaticamente" (default ligado)
com explicação clara. Tenant pode desligar se preferir ser alertado
imediato sem tentativa.
Summary do endpoint agora inclui auto_reconnected count — útil pra
métricas de confiabilidade da Evolution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0f643817c2 |
Fix send-session-reminders comparava provider='evolution' mas DB guarda 'evolution_api'
Na migração do schema (20260421000007 / whatsapp_credits) o CHECK da coluna provider de notification_channels passou a aceitar apenas 'evolution_api' (com sufixo). O send-session-reminders continuou comparando pelo nome antigo, sempre caindo em skip 'unknown_provider'. Fix mapeia providerKind = 'evolution' quando channel.provider é 'evolution_api' ou 'evolution' — backward compat. Aplicado também no branch do twilio (sem mudança, só renomeia a variável). Validado end-to-end: lembrete 2h disparado pro paciente André Green chegou no WhatsApp do terapeuta (provider_message_id persistido em session_reminder_logs + outbound em conversation_messages). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
adf9208d2d |
Analytics 7.1: tempo médio de 1ª resposta WhatsApp no dashboard
Card novo pra clínica e terapeuta com 3 métricas + sparkline: - Tempo médio (e mediana) de 1ª resposta no período - Taxa de SLA cumprido — % de respostas dentro do threshold configurado - Contagem total de respostas no período - Sparkline da evolução com indicador de tendência (melhorando/piorando) - Ranking top 5 terapeutas (só no ClinicDashboard) Filtro de período: 7/30/90 dias (muda granularidade do bucket: 1/7/15 dias pra sparkline com ~5-6 pontos). Banco (migration 20260423000006): - Helper interno _first_response_runs: identifica "runs" de inbound (sequências do paciente sem outbound entre) e calcula delta até a próxima outbound. Evita contar múltiplas mensagens repetidas do paciente. responder_id vem de conversation_assignments. - first_response_stats: agregados (count, avg, median, min, max, sla_compliance_rate baseado em conversation_sla_rules). - first_response_by_therapist: ranking com avg e count por assigned_to. - first_response_evolution: série temporal com bucket alinhado a p_from (p_from + bucket_index * N days). Parâmetro p_bucket_days deixa o frontend escolher granularidade por período. Todas SECURITY DEFINER + GRANT authenticated/service_role. Filtro opcional por therapist_id nas funções que aplicam. Frontend: - useFirstResponseAnalytics composable wraps as 3 RPCs com cache via Promise.all paralelo. Helper formatSeconds (Ns/Xmin/Xh). - FirstResponseCard.vue renderiza sparkline SVG nativo (sem lib extra), cor da taxa SLA por threshold (verde ≥80%, âmbar ≥50%, vermelho). - Integrado em ClinicDashboard (visão global) e TherapistDashboard (filtrado por ownerId, sem ranking). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
36fbc02e9f |
Browser notification: click leva pro destino real (drawer ou rota)
Bug: onclick da Notification do browser (nativa do Chrome/Windows) fazia window.location.pathname = payload.deeplink direto, sem resolver alias semântico e sem abrir o drawer em alertas com thread_key. Como praticamente todos os nossos alertas do SLA vêm com deeplink '/conversas' (alias), o click na notificação do Chrome caía em NotFound. Fix: - fireBrowserNotification agora aceita um callback onClick e é exportada. - Removido o fireBrowserNotification hardcoded do subscribeRealtime do store (passa a ser responsabilidade do composable useNotifications). - useNotifications.onRealtimeNotification dispara toast + browser notif passando handleNotificationAction como handler. - handleNotificationAction: se tem thread_key → abre ConversationDrawer global direto na thread; senão resolve alias e router.push. Mesma lógica que já existe no toast e no clique do NotificationItem do sino. Agora os 3 pontos de click (toast, sininho, notificação nativa do OS) convergem pro mesmo comportamento. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
64e76343fc |
NotificationItem: resolve alias + botões inline "Conversa"/"Abrir"
3 melhorias no item de notificação do sininho: 1. handleRowClick: agora resolve alias (/conversas → /therapist|admin/ conversas) baseado em tenantStore.activeRole. Antes caía em NotFound quando o deeplink era /conversas ou /crm/conversas. 2. Se payload tem thread_key (alertas do SLA), o clique abre o drawer global diretamente na thread em vez de navegar — experiência similar à do botão do toast. Fallback pra deeplink se a thread sumiu. 3. typeMap ganha entrada 'system_alert' (ícone pi-exclamation-circle, borda vermelha). 4. Botões inline "Conversa" e "Abrir" aparecem embaixo do detail quando o payload tem thread_key ou deeplink — atalhos pras ações mais comuns sem precisar clicar na área do item. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f646efe522 |
Toast SLA: botão "Abrir conversa" abre drawer direto da thread
O alerta já vem com payload.thread_key vindo do edge conversation-sla- check. Agora o toast renderiza 2 botões lado a lado quando thread_key existe: - "Abrir conversa" (outlined) → abre ConversationDrawer global direto na thread, sem navegar de página. Usa o store global que já existe. - "Abrir CRM →" (solid) → fallback pra lista inteira via deeplink alias. openConversationDrawer busca o row da view conversation_threads pelo tenant+thread_key e delega pro conversationDrawerStore.openForThread. Se a thread sumiu (arquivada/paciente deletado), cai no fallback de navegar pra /conversas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5f51bc068e |
Fix deeplink /crm/conversas não existe; alias dinâmico por role
Bug: toast do SLA tinha deeplink /crm/conversas que caía em NotFound. As rotas reais são /therapist/conversas (terapeuta) e /admin/conversas (clinic_admin), contextuais por role. Fix: novo sistema de aliases em AppLayout.resolveDeeplink. DEEPLINK_ALIASES traduz links semânticos (ex: /conversas, /crm/conversas) pra rota real baseado em tenantStore.activeRole. Edge do SLA agora emite /conversas (alias) em vez de path hardcoded; frontend resolve. Padrão aplicável pras próximas features — basta registrar novo alias aqui quando a rota depender de contexto. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4026415401 |
Notifications: não redispara toast pra system_alert antigas após F5
Bug: a cada mount (F5, navegação), todas as system_alert não-lidas voltavam a disparar toast mesmo que o alerta já não fizesse mais sentido (ex: saldo baixo já restabelecido, mas notif histórica ainda não-lida reaparecia como toast sticky vermelho a cada reload). Fix: seed do set alertedIds marca TODAS as system_alert do load inicial como "já vistas nesta sessão". Alertas continuam no sino/drawer — o usuário vê que tem pendências, mas sem bombardeio de toasts repetidos. Toast só dispara pra alertas que chegarem depois do mount — seja via Realtime (novidade) ou via catch-up encontrando id ainda não no set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
771b636cee |
SLA de conversas WhatsApp (Grupo 3.4): config + detecção + alerta
Completa o Grupo 3 do CRM com alerta de conversa sem resposta além do tempo configurado — reutiliza o pipeline system_alert (toast vermelho sticky + sininho + drawer). Banco (migration 20260423000005): - conversation_sla_rules: 1 linha por tenant com threshold global (1-1440 min), respect_business_hours, business_hours_start/end, business_days (ISO 1=seg..7=dom), alert_scope (assigned_only|all), notify_admin_on_breach. Default: enabled=false. - conversation_sla_breaches: incidents com UNIQUE parcial (tenant_id, thread_key) WHERE resolved_at IS NULL — idempotência. - Trigger AFTER INSERT em conversation_messages resolve o breach automaticamente quando chega nova outbound na thread. - RPCs service_role: sla_open_breach (idempotente), sla_mark_notified. - RLS: membros do tenant leem; clinic_admin/tenant_admin/saas_admin escrevem na config; service_role escreve em breaches. Edge function conversation-sla-check (cron 5min): - Varre tenants com enabled=true. - Query conversation_threads onde last_message_direction='inbound' (+ assigned_to NOT NULL se scope='assigned_only'). - Se respect_business_hours: calcula businessMinutesElapsed em TS iterando dia por dia a interseção da janela [start,end] com [last_inbound_at, now], só em dias marcados em business_days. TZ fixa em America/Sao_Paulo via Intl.DateTimeFormat. - Se elapsed >= threshold: sla_open_breach (idempotente) + notifica assigned_to sempre + admins se notify_admin_on_breach (deduplicado via Set). - Anti-spam: só notifica 1x por incident (checa notified_at). - Notification leva deeplink pra /crm/conversas e payload.thread_key pro frontend destacar a conversa (fora de escopo deste commit). UI em /configuracoes/conversas-sla: - Toggle enabled + InputNumber threshold com preview "≈ Xh Ymin". - Toggle respect_business_hours → revela start/end + seletor de dias úteis (pills toggleáveis Seg..Dom, ISO order). - Select scope. - Toggle notify_admin_on_breach. - Card abaixo com breaches dos últimos 7 dias (status aberto/resolvido, thread_key, limite configurado no momento do breach, duração). - Adicionada na ConfiguracoesPage landing + rota /configuracoes/conversas-sla. Cron template comentado no fim da migration (mesmo padrão do heartbeat). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4441661f62 |
Toast system_alert: agregar no catch-up pra não empilhar enxurrada
Bug: acumulando N system_alert não-lidas, o refreshAndMaybeAlert (mount / visibilitychange / polling 60s) disparava N toasts de uma vez. Comum após recarregar a página com alertas pendentes do último teste. Fix: no catch-up, mostra só a notif mais recente, com sufixo "+N outros alertas no sino" no detail se houver múltiplas. As demais são marcadas no alertedIds pra não redisparar — continuam visíveis no sininho/drawer com badge. Eventos novos via Realtime seguem aparecendo individualmente (fluxo normal — o usuário está online vendo chegar). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6db06abfc2 |
Toast system_alert ganha botão de ação com deeplink
Novo <Toast group="system-alerts"> no AppLayout com template custom (vive no bloco global — persiste em qualquer layout/rota). Renderiza: - Ícone de alerta + título em bold - Detail em texto menor com opacity - Botão com deeplink quando payload.deeplink existe, severity danger Label do botão inferido do deeplink: - /configuracoes/creditos-whatsapp → "Ir pra loja" - /configuracoes/whatsapp-pessoal → "Ver conexão" - /configuracoes/whatsapp-oficial → "Ver canal oficial" - outros → "Abrir" (ou payload.actionLabel se vier explícito) Clique navega via router.push se é path interno, senão window.location.href. Toast continua sticky (24h) + closable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5c50db6704 |
Notifications: fallback de polling + catch-up ao focar a aba
Realtime em ambiente self-hosted às vezes perde eventos (WebSocket desconecta silenciosamente, JWT expira, sleep do SO, etc). Sem fallback, system_alert chega no DB mas toast nunca dispara — usuário só vê ao relogar ou recarregar. Três caminhos complementares agora: 1. Realtime (instantâneo, quando funciona) 2. visibilitychange — ao voltar pro foco da aba, recarrega notificações e dispara toast pras system_alert não-lidas ainda não exibidas 3. Polling a cada 60s como redundância Set alertedIds (in-memory por sessão) evita toast duplicado quando dois caminhos entregam a mesma notif. Seed inicial marca notifs já lidas/ arquivadas no mount pra não disparar retroativamente. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e409ba64ef |
Saldo baixo WhatsApp: trigger dispara notificação ao cruzar threshold
Fecha o loop do Marco B — tenant não zera mais saldo sem aviso. Nova função fn_whatsapp_low_balance_notify + trigger BEFORE UPDATE em whatsapp_credits_balance: - Dispara quando NEW.balance < NEW.low_balance_threshold e NEW.low_balance_alerted_at IS NULL - Insere system_alert pros stakeholders do tenant (owner do canal WhatsApp ativo + clinic_admin + tenant_admin, deduplicado via UNION) - Deeplink direto pra /configuracoes/creditos-whatsapp - Seta NEW.low_balance_alerted_at = now() pra anti-spam Reset do anti-spam já existia: add_whatsapp_credits seta low_balance_alerted_at=NULL ao creditar (purchase/topup/refund). Assim o ciclo completo funciona: cai abaixo → alerta → compra recrédita → cai de novo futuramente → alerta de novo. Toast no frontend já é sticky vermelho pra type='system_alert' (commit anterior). Config de threshold já existia na UI em /configuracoes/creditos-whatsapp. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
881fa16c27 |
Fluxo de reativação de canal WhatsApp + alerta toast sticky + notify owner
Cadeia de fixes descoberta ao testar o heartbeat 6.1 num tenant que migrou
de Evolution → Twilio e precisava voltar pro Evolution.
1. RLS notification_channels (migration 20260423000003)
- Policy antiga tinha `deleted_at IS NULL` como primeira condição AND,
bloqueando leitura de soft-deleted até pro próprio owner/saas_admin.
- Isso fazia o chooser nunca detectar "canal antigo pra reativar".
- Relaxada: owner/membro/saas_admin leem inclusive soft-deleted.
- Filtro de deleted_at fica no código aplicativo (todos os queries já
filtram explicitamente quando querem apenas ativos).
2. Edge function reactivate-notification-channel (nova)
- Espelho da deactivate existente; service_role bypass RLS.
- Aceita {channel_id} OU {tenant_id + provider}.
- Autoriza saas_admin OU membro ativo do tenant.
- Garante exclusividade: soft-deleta qualquer OUTRO canal ativo do
mesmo tenant+channel.
- Reseta metadata.first_unhealthy_at + connection_status=disconnected
(heartbeat começa do zero).
3. SaasWhatsappPage (/saas/whatsapp)
- loadChannel busca soft-deleted como fallback quando não tem ativo.
- saveCredentials detecta soft-deleted e chama reactivate edge,
depois atualiza credentials+display_name.
- Banner âmbar "Canal configurado anteriormente" + botão vira
"Reativar e salvar".
4. ConfiguracoesWhatsappPage tenant (/configuracoes/whatsapp-pessoal)
- loadCredentials busca soft-deleted como fallback.
- Card âmbar "WhatsApp Pessoal foi usado anteriormente" com botão
"Reativar WhatsApp Pessoal" em vez de mostrar apenas "chame o suporte".
5. ChooserPage (/configuracoes/whatsapp)
- Fix bug lateral: comparava activeProvider === 'evolution' (template)
com 'evolution_api' (DB) — card nunca mostrava estado ativo. Agora
normaliza via computed activeProviderKey.
- softDeletedByProvider map carregado no mount; cards que têm row
soft-deleted mostram "Reativar" em vez de "Ativar".
- handleChoose chama reactivate edge antes de goSetup se detecta
soft-deleted do provider escolhido.
6. whatsapp-heartbeat-check: notifica owner do channel + admins
- notifyChannelStakeholders substitui notifyTenantAdmins.
- Set dedupa o owner_id do channel + clinic_admin + tenant_admin.
- Em tenant solo: 1 notificação; em clínica com canal de terapeuta
específico: terapeuta (owner) + admin recebem; em clínica com canal
do próprio admin: 1 (owner=admin).
7. Toast frontend para system_alert
- notificationStore.subscribeRealtime aceita callback onInsert.
- useNotifications registra callback que dispara toast PrimeVue
(severity error, life 24h, closable) para type='system_alert'.
- Usuário precisa fechar manualmente — alerta crítico de infra
não pode sumir sozinho.
Cron heartbeat ativado em runtime local via cron.schedule()
(não vai neste commit — é config de ambiente, não migration).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e1f756ea82 |
Heartbeat WhatsApp Evolution (Grupo 6.1): detecção + incident + alerta admin
Detecta celular desconectado antes de falhar envios silenciosamente.
Banco (migration 20260423000002):
- Tabela whatsapp_connection_incidents (tenant_id, channel_id, kind,
started_at, resolved_at, duration_seconds, notified_at, details).
UNIQUE parcial garante no máximo 1 incident aberto por channel.
- RPCs whatsapp_heartbeat_open_incident (idempotente), _resolve_open_incidents
e _mark_notified. Service_role only.
- RLS: membros do tenant leem, saas_admin tudo.
- ALTER notifications.type pra aceitar 'system_alert' (usado pelo alerta).
Edge function whatsapp-heartbeat-check:
- Varre notification_channels provider=evolution_api e ativos.
- GET {api_url}/instance/connectionState/{instance} (timeout 8s, rewrite
localhost → host.docker.internal pra containers).
- Mapeia state pra connection_status (open/connecting/qr_pending/
disconnected/error), persiste + last_health_check.
- Lógica de threshold: marca first_unhealthy_at em metadata na primeira
falha; só abre incident após heartbeat_threshold_minutes (default 5).
- Notifica admins ativos (clinic_admin/tenant_admin) do tenant via
insert em notifications. Anti-spam: só notifica 1x por incident.
- Aceita ?channel_id=X pra check on-demand de um tenant específico.
UI tenant (/configuracoes/whatsapp-pessoal):
- Novo card "Monitoramento de conexão" com toggle alerts_enabled +
InputNumber threshold (1-60 min). Persiste em
notification_channels.metadata.
- Histórico últimos 7 dias: kind (tag colorida), aberto/resolvido,
início → fim, duração formatada (Ns/Xmin Ys/Nh Xmin).
UI SaaS (/saas/whatsapp):
- Badge "N incidents abertos" no header quando há algum.
- Botão "Verificar tudo agora" invoca a edge function e atualiza a lista.
- Tabela enriquecida: coluna Status ganha pill "Incident aberto",
colunas novas Última check e Incidents 7d (em laranja se > 0).
Cron template no final da migration (comentado — descomentar
cron.schedule pra ativar 2min periódico).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
f76a2e3033 |
Admin SaaS: ajuste manual de créditos WhatsApp (+/-) com proteção de compras
Fecha polimento do Marco B (créditos/Asaas) entregue em 21/04.
Nova RPC admin_adjust_whatsapp_credits(tenant, amount_signed, admin_id, note):
- |amount| <= 1000 por operação (anti dedo-gordo). Valores maiores → repetir.
- Em remoção (amount < 0), aplica regra FIFO cortesia primeiro:
removable = max(0, sum(topup_manual+adjustment+refund) - usage_total).
Créditos de 'purchase' (Asaas/PIX) são intocáveis — estorno real vai pelo
fluxo financeiro do Asaas.
- Protegida por is_saas_admin() — authenticated comum não consegue chamar.
- Registra como kind='adjustment' com amount signed (+ ou -).
Helper get_whatsapp_removable_balance(tenant) retorna {balance, removable,
protected_amount, topup_net, usage_total} pra UI mostrar breakdown.
Aba 4 (Pacotes WhatsApp):
- Desativação dispara ConfirmDialog com histórico (N compras, M tenants
distintos) + aviso forte se é o único pacote ativo + nota que créditos já
adquiridos continuam válidos.
- Fix visual: :key no ToggleSwitch força re-mount durante confirm pra não
desligar visualmente antes do accept.
Aba 5 (Topup → Ajuste):
- Substituído Select de kind por SelectButton Adicionar/Remover.
- InputNumber max 1000 · label e botão dinâmicos.
- Modo Remover: card laranja com breakdown removível/protegido, botão
vermelho, confirm obrigatório com saldo resultante.
- Error mapping friendly pt-BR pros códigos da RPC.
ConfirmDialog com v-html habilitado pra suportar <br><br> entre frases
e <strong>/cores. Inputs livres (row.name, tenantName) passam por
escapeHtml() antes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
2644e60bb6 |
CRM WhatsApp Grupo 3 completo + Marco A/B (Asaas) + admin SaaS + refactors polimórficos
Sessão 11+: fechamento do CRM de WhatsApp com dois providers (Evolution/Twilio),
sistema de créditos com Asaas/PIX, polimorfismo de telefones/emails, e integração
admin SaaS no /saas/addons existente.
═══════════════════════════════════════════════════════════════════════════
GRUPO 3 — WORKFLOW / CRM (completo)
═══════════════════════════════════════════════════════════════════════════
3.1 Tags · migration conversation_tags + seed de 5 system tags · composable
useConversationTags.js · popover + pills no drawer e nos cards do Kanban.
3.2 Atribuição de conversa a terapeuta · migration 20260421000012 com PK
(tenant_id, thread_key), UPSERT, RLS que valida assignee como membro ativo
do mesmo tenant · view conversation_threads expandida com assigned_to +
assigned_at · composable useConversationAssignment.js · drawer com Select
filtrável + botão "Assumir" · inbox com filtro aside (Todas/Minhas/Não
atribuídas) e chip do responsável em cada card (destaca "Eu" em azul).
3.3 Notas internas · migration conversation_notes · composable + seção
colapsável no drawer · apenas o criador pode editar/apagar (RLS).
3.5 Converter desconhecido em paciente · botão + dialog quick-cadastro ·
"Vincular existente" com Select filter de até 500 pacientes · cria
telefone WhatsApp (vinculado) via upsertWhatsappForExisting.
3.6 Histórico de conversa no prontuário · nova aba "Conversas" em
PatientProntuario.vue · PatientConversationsTab.vue com stats (total /
recebidas / enviadas / primeira / última), SelectButton de filtro, timeline
com bolhas por direção, mídia inline (imagem/áudio/vídeo/doc via signed
URL), indicadores ✓ ✓✓ de delivery, botão "Abrir no CRM".
═══════════════════════════════════════════════════════════════════════════
MARCO A — UNIFICAÇÃO WHATSAPP (dois providers mutuamente exclusivos)
═══════════════════════════════════════════════════════════════════════════
- Página chooser ConfiguracoesWhatsappChooserPage.vue com 2 cards (Pessoal/
Oficial), deactivate via edge function deactivate-notification-channel
- send-whatsapp-message refatorada com roteamento por provider; Twilio deduz
1 crédito antes do envio e refunda em falha
- Paridade Twilio (novo): módulo compartilhado supabase/functions/_shared/
whatsapp-hooks.ts com lógica provider-agnóstica (opt-in, opt-out, auto-
reply, schedule helpers em TZ São Paulo, makeTwilioCreditedSendFn que
envolve envio em dedução atômica + rollback). Consumido por Evolution E
Twilio inbound. Evolution refatorado (~290 linhas duplicadas removidas).
- Bucket privado whatsapp-media · decrypt via Evolution getBase64From
MediaMessage · upload com path tenant/yyyy/mm · signed URLs on-demand
═══════════════════════════════════════════════════════════════════════════
MARCO B — SISTEMA DE CRÉDITOS WHATSAPP + ASAAS
═══════════════════════════════════════════════════════════════════════════
Banco:
- Migration 20260421000007_whatsapp_credits (4 tabelas: balance,
transactions, packages, purchases) + RPCs add_whatsapp_credits e
deduct_whatsapp_credits (atômicas com SELECT FOR UPDATE)
- Migration 20260421000013_tenant_cpf_cnpj (coluna em tenants com CHECK
de 11 ou 14 dígitos)
Edge functions:
- create-whatsapp-credit-charge · Asaas v3 (sandbox + prod) · PIX com
QR code · getOrCreateAsaasCustomer patcha customer existente com CPF
quando está faltando
- asaas-webhook · recebe PAYMENT_RECEIVED/CONFIRMED e credita balance
Frontend (tenant):
- Página /configuracoes/creditos-whatsapp com saldo + loja + histórico
- Dialog de confirmação com CPF/CNPJ (validação via isValidCPF/CNPJ de
utils/validators, formatação on-blur, pré-fill de tenants.cpf_cnpj,
persiste no primeiro uso) · fallback sandbox 24971563792 REMOVIDO
- Composable useWhatsappCredits extrai erros amigáveis via
error.context.json()
Frontend (SaaS admin):
- Em /saas/addons (reuso do pattern existente, não criou página paralela):
- Aba 4 "Pacotes WhatsApp" — CRUD whatsapp_credit_packages com DataTable,
toggle is_active inline, dialog de edição com validação
- Aba 5 "Topup WhatsApp" — tenant Select com saldo ao vivo · RPC
add_whatsapp_credits com p_admin_id = auth.uid() (auditoria) · histórico
das últimas 20 transações topup/adjustment/refund
═══════════════════════════════════════════════════════════════════════════
GRUPO 2 — AUTOMAÇÃO
═══════════════════════════════════════════════════════════════════════════
2.3 Auto-reply · conversation_autoreply_settings + conversation_autoreply_
log · 3 modos de schedule (agenda das regras semanais, business_hours
custom, custom_window) · cooldown por thread · respeita opt-out · agora
funciona em Evolution E Twilio (hooks compartilhados)
2.4 Lembretes de sessão · conversation_session_reminders_settings +
_logs · edge send-session-reminders (cron) · janelas 24h e 2h antes ·
Twilio deduz crédito com rollback em falha
═══════════════════════════════════════════════════════════════════════════
GRUPO 5 — COMPLIANCE (LGPD Art. 18 §2)
═══════════════════════════════════════════════════════════════════════════
5.2 Opt-out · conversation_optouts + conversation_optout_keywords (10 system
seed + custom por tenant) · detecção por regex word-boundary e normalização
(lowercase + strip acentos + pontuação) · ack automático (deduz crédito em
Twilio) · opt-in via "voltar", "retornar", "reativar", "restart" ·
página /configuracoes/conversas-optouts com CRUD de keywords
═══════════════════════════════════════════════════════════════════════════
REFACTOR POLIMÓRFICO — TELEFONES + EMAILS
═══════════════════════════════════════════════════════════════════════════
- contact_types + contact_phones (entity_type + entity_id) — migration
20260421000008 · contact_email_types + contact_emails — 20260421000011
- Componentes ContactPhonesEditor.vue e ContactEmailsEditor.vue (add/edit/
remove com confirm, primary selector, WhatsApp linked badge)
- Composables useContactPhones.js + useContactEmails.js com
unsetOtherPrimaries() e validação
- Trocado em PatientsCadastroPage.vue e MedicosPage.vue (removidos campos
legados telefone/telefone_alternativo e email_principal/email_alternativo)
- Migration retroativa v2 (20260421000010) detecta conversation_messages
e cria/atualiza phone como WhatsApp vinculado
═══════════════════════════════════════════════════════════════════════════
POLIMENTO VISUAL + INFRA
═══════════════════════════════════════════════════════════════════════════
- Skeletons simplificados no dashboard do terapeuta
- Animações fade-up com stagger via [--delay:Xms] (fix specificity sobre
.dash-card box-shadow transition)
- ConfirmDialog com group="conversation-drawer" (evita montagem duplicada)
- Image preview PrimeVue com botão de download injetado via MutationObserver
(fetch + blob para funcionar cross-origin)
- Áudio/vídeo com preload="metadata" e controles de velocidade do browser
- friendlySendError() mapeia códigos do edge pra mensagens pt-BR via
error.context.json()
- Teleport #cfg-page-actions para ações globais de Configurações
- Brotli/Gzip + auto-import Vue/PrimeVue + bundle analyzer
- AppLayout consolidado (removidas duplicatas por área) + RouterPassthrough
- Removido console.trace debug que estava em watch de router e queries
Supabase (degradava perf pra todos)
- Realtime em conversation_messages via publication supabase_realtime
- Notifier global flutuante com beep + toggle mute (4 camadas: badge +
sino + popup + browser notification)
═══════════════════════════════════════════════════════════════════════════
MIGRATIONS NOVAS (13)
═══════════════════════════════════════════════════════════════════════════
20260420000001_patient_intake_invite_info_rpc
20260420000002_audit_logs_lgpd
20260420000003_audit_logs_unified_view
20260420000004_lgpd_export_patient_rpc
20260420000005_conversation_messages
20260420000005_search_global_rpc
20260420000006_conv_messages_notifications
20260420000007_notif_channels_saas_admin_insert
20260420000008_conv_messages_realtime
20260420000009_conv_messages_delivery_status
20260421000001_whatsapp_media_bucket
20260421000002_conversation_notes
20260421000003_conversation_tags
20260421000004_conversation_autoreply
20260421000005_conversation_optouts
20260421000006_session_reminders
20260421000007_whatsapp_credits
20260421000008_contact_phones
20260421000009_retroactive_whatsapp_link
20260421000010_retroactive_whatsapp_link_v2
20260421000011_contact_emails
20260421000012_conversation_assignments
20260421000013_tenant_cpf_cnpj
═══════════════════════════════════════════════════════════════════════════
EDGE FUNCTIONS NOVAS / MODIFICADAS
═══════════════════════════════════════════════════════════════════════════
Novas:
- _shared/whatsapp-hooks.ts (módulo compartilhado)
- asaas-webhook
- create-whatsapp-credit-charge
- deactivate-notification-channel
- evolution-webhook-provision
- evolution-whatsapp-inbound
- get-intake-invite-info
- notification-webhook
- send-session-reminders
- send-whatsapp-message
- submit-patient-intake
- twilio-whatsapp-inbound
═══════════════════════════════════════════════════════════════════════════
FRONTEND — RESUMO
═══════════════════════════════════════════════════════════════════════════
Composables novos: useAddonExtrato, useAuditoria, useAutoReplySettings,
useClinicKPIs, useContactEmails, useContactPhones, useConversationAssignment,
useConversationNotes, useConversationOptouts, useConversationTags,
useConversations, useLgpdExport, useSessionReminders, useWhatsappCredits
Stores: conversationDrawerStore
Componentes novos: ConversationDrawer, GlobalInboundNotifier, GlobalSearch,
ContactEmailsEditor, ContactPhonesEditor
Páginas novas: CRMConversasPage, PatientConversationsTab, AddonsExtratoPage,
AuditoriaPage, NotificationsHistoryPage, ConfiguracoesWhatsappChooserPage,
ConfiguracoesConversasAutoreplyPage, ConfiguracoesConversasOptoutsPage,
ConfiguracoesConversasTagsPage, ConfiguracoesCreditosWhatsappPage,
ConfiguracoesLembretesSessaoPage
Utils novos: addonExtratoExport, auditoriaExport, excelExport,
lgpdExportFormats
Páginas existentes alteradas: ClinicDashboard, PatientsCadastroPage,
PatientCadastroDialog, PatientsListPage, MedicosPage, PatientProntuario,
ConfiguracoesWhatsappPage, SaasWhatsappPage, ConfiguracoesRecursosExtrasPage,
ConfiguracoesPage, AgendaTerapeutaPage, AgendaClinicaPage, NotificationItem,
NotificationDrawer, AppLayout, AppTopbar, useMenuBadges,
patientsRepository, SaasAddonsPage (aba 4 + 5 WhatsApp)
Routes: routes.clinic, routes.configs, routes.therapist atualizados
Menus: clinic.menu, therapist.menu, saas.menu atualizados
═══════════════════════════════════════════════════════════════════════════
NOTAS
- Após subir, rodar supabase functions serve --no-verify-jwt
--env-file supabase/functions/.env pra carregar o módulo _shared
- WHATSAPP_SETUP.md reescrito (~400 linhas) com setup completo dos 3
providers + troubleshooting + LGPD
- HANDOFF.md atualizado com estado atual e próximos passos
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|