Commit Graph

185 Commits

Author SHA1 Message Date
VPS 4 Builder 2d0396fcb1 feat(timezone): configuração de fuso horário por workspace (Fase 1)
Fundação do multi-fuso: cada clínica define seu timezone (Configurações → Perfil da
Unidade → Fuso horário). Migration clinicas.timezone (default America/Sao_Paulo);
GET/PUT /api/clinicas/:id/timezone (whitelist de fusos do Brasil; PUT só dono/admin).

Base para a Fase 2 (usar o fuso no cálculo de slots, no "agora" da Secretária e na
exibição da agenda) — que corrige o -1h observado (IA confirmou 09:30, agenda mostrou
08:30) causado por satélite=UTC-3, motor=UTC e navegador=UTC-4 com horário naive.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 02:27:34 +02:00
VPS 4 Builder c8443a146a fix(nw): handoff é ação de inbox (não Secretária) + botão do dono p/ horário da clínica
1) 403 ao "desligar a IA no chat": guardAreaAccess tratava TODO /sec/* como config da
   Secretária (exige can_secretaria). Mas /sec/handoff é assumir/soltar o CHAT = INBOX.
   Reclassificado p/ can_inbox — a recep (can_inbox, sem can_secretaria) volta a assumir
   o chat. /sec/agent|nodes|slots e /secretaria/* seguem exigindo can_secretaria.
   (session-power global segue can_secretaria — o dono delega se quiser.)

2) Duplicação de horário (parte 2): o DONO agora tem, no modal, o card + botão p/
   configurar o "Horário da clínica" (clinicas_horarios) em Horários de Funcionamento
   (aba clínica). Completude do dono passa a exigir esse horário. Backend:
   comunicacaoCompleto(p, {ehDentista,jornadaOk,ehDono,horarioClinicaOk}); GET devolve
   horario_clinica_configurado. Verificado: dono da Consultt sem horário → gate exige.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 00:34:04 +02:00
VPS 4 Builder e9f8a24a93 fix(comunicacao-interna): remove duplicação de agenda — fonte única (jornada real)
A seção "Agenda/Horários de atendimento" do modal era uma 3ª fonte de horário que a
Secretária NÃO lia (ela agenda por clinicas_horarios + dentistas_horarios). Removida:
- Modal cuida só de contato (endereço + números + janelas de envio). Para dentista,
  mostra o STATUS da jornada real e um botão que abre "Horários de Funcionamento"
  (HorariosConfig, aba dentistas) por cima; ao fechar, recarrega.
- Completude do DENTISTA passa a exigir a jornada REAL configurada (dentistas_horarios),
  não mais a agenda do modal. GET recomputa `completo` ao vivo + devolve jornada_configurada.
- Backend: comunicacaoCompleto(p, ehDentista, jornadaOk) + helper jornadaConfigurada.

Verificado: dentista com jornada → completo; query bate com dentistas_horarios real.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 00:14:32 +02:00
VPS 4 Builder b253feb4aa feat(comunicacao-interna): notificação de número compartilhado + agenda de contatos
Opção (b) — mesma pessoa/número servindo a papéis diferentes, roteando por situação:
- GET /api/comunicacao/numero-lookup: ao digitar um número, avisa se ele já é de outra
  conta/papel da clínica (ex.: número na conta dentista aparece ao editar a de dono).
  Modal mostra banner "pode compartilhar — descreva as situações; a Secretária roteia".
- Bridge GET /contatos: agenda de contatos internos (nome, papel, situações de cada
  número, janela atual) p/ a Secretária rotear por assunto. Não expõe o número cru.

Verificado E2E: lookup como dono acha o nº na conta dentista; /contatos lista papel+situações.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 22:48:42 +02:00
VPS 4 Builder eafc1578e3 fix(app): importa useRef — corrige tela branca do gate progressivo
O gate de comunicação interna usava useRef sem importá-lo, quebrando o render
inteiro do App (ReferenceError → tela branca).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:49:46 +02:00
VPS 4 Builder 8b9776f081 feat(comunicacao-interna): gate progressivo + agenda só p/ dentista
- Gate progressivo: modal ganha "Lembrar mais tarde" e reaparece a cada login;
  a partir da 4ª aparição vira bloqueio até preencher (contador em localStorage
  por clínica; ref evita contagem dupla no StrictMode).
- Agenda passa a ser exigida/exibida só para DENTISTA (quem atende). Backend:
  comunicacaoCompleto(p, ehDentista) via vinculos.role; GET devolve `role`.
  Não-dentista (dono/funcionário) completa só com endereço + números.

Verificado: dono completa sem agenda; dentista exige agenda; role no GET ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:40:54 +02:00
VPS 4 Builder 9fd078e36f feat(comunicacao-interna): pré-preenche endereço a partir da clínica
Perfil sem endereço próprio → GET /api/comunicacao sugere o endereço já cadastrado
da clínica (só confirmar, não redigitar) + flag endereco_da_clinica. Modal mostra
aviso "puxamos o endereço da clínica — é só conferir". Resolve a fricção do dono,
cuja clínica já tem endereço.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:18:01 +02:00
VPS 4 Builder d33ebd3f76 feat(comunicacao-interna): entrada na Gestão de Equipe
Botão "Comunicação interna" em cada membro (gestores) abre o mesmo modal full-screen
para preencher/editar o perfil de contato daquele usuário (usuarioId = m.id).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:16:50 +02:00
VPS 4 Builder dcf33befec feat(comunicacao-interna): perfil de contato de não-pacientes + gate obrigatório
Base da "comunicação interna": cada não-paciente (dentista/funcionário) cadastra
endereço, MÚLTIPLOS números de WhatsApp e a disponibilidade de agenda.

Backend:
- Tabela pessoa_comunicacao (usuario_id+clinica_id): endereco/numeros/agenda JSONB + completo.
- GET/PUT /api/comunicacao/:usuarioId ('me' = usuário atual). Autoriza self OU gestor
  (dono/admin). Recalcula `completo` no PUT (regra: endereço + ≥1 número com descrição
  e ≥1 janela de horário + agenda decidida; se aberta, exige ≥1 janela).

Frontend:
- ComunicacaoInternaModal (full-screen): Endereço (com ViaCEP), Números (cada um com
  descrição + situações + janelas de horário permitido p/ a Secretária enviar msg),
  Agenda (abrir/fechar + horários de atendimento). Editor de janelas reutilizável.
- Gate no App.tsx: todo não-paciente com clínica ativa cujo perfil está incompleto vê
  o modal FORÇADO em todas as telas (espelha o gate de troca de senha) até completar.

Verificado E2E: PUT incompleto->completo=false; PUT completo->true; GET reflete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:13:14 +02:00
VPS 4 Builder 343ca560dc feat(salas): contrato de locação — modelo padrão + aceite na reserva
Backend:
- Nova coluna salas.contrato (TEXT, migration ADD COLUMN IF NOT EXISTS).
- INSERT/UPDATE de sala aceitam `contrato`; marketplace SELECT devolve s.contrato
  (minhas salas já via s.*).

Frontend (SalasPlugin):
- CONTRATO_MODELO: modelo padrão cuja cláusula 2 deixa TODOS os procedimentos sob
  inteira responsabilidade do locatário; o dono edita por sala.
- SalaForm ganha textarea "Contrato de locação" pré-preenchida com o modelo +
  botão "Restaurar modelo".
- Passo Pagamento: botão "Ler contrato de locação" abre modal com o texto
  (contrato da sala, ou o modelo) + checkbox de aceite. "Solicitar Reserva" só
  habilita após aceitar; o aceite é registrado nas observações da reserva.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:10:40 +02:00
VPS 4 Builder d6aa1cdeac feat(salas): modelo "Dia" com seleção de dias avulsos (não-consecutivos)
Antes o modelo Dia só permitia um intervalo consecutivo (início + N dias).
Agora permite escolher dias avulsos (ex.: 1 e 5, 3 e 20):

- Clique no calendário alterna (marca/desmarca) cada dia — estado diasMulti[].
- Card mostra chips dos dias selecionados (removíveis; dia ocupado fica âmbar) +
  dica "cada dia vira uma reserva (08h–18h)".
- Total = valor_diaria × nº de dias; resumo lista os dias.
- handleReserve envia uma reserva por dia; reporta quantas foram criadas e
  quantas falharam (ex.: já ocupado).
- Travas de navegação/botão passam a usar temSelecao (Dia não usa f.inicio).
- Semana/Mês seguem com stepper de quantidade; Hora/Período inalterados.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 04:25:49 +02:00
VPS 4 Builder c78c745f3f feat(salas): reserva de múltiplos dias/semanas/meses + controles visíveis
Calendário (passo 2):
- qtd vira "quantidade" genérica por modelo: Hora=horas (1–12), Dia=dias (1–30),
  Semana=semanas (1–8), Mês=meses (1–12). computeRange estende o fim conforme a
  quantidade; total = valor × quantidade (Turno segue unitário).
- O intervalo escolhido (diária/semanal/mensal) é realçado no calendário e some
  ao trocar de modelo (qtd volta a 1).
- Controles (Início/Duração/Período/Quantidade) que ficavam escondidos abaixo do
  calendário agora vivem num card destacado, com cabeçalho do dia/intervalo
  selecionado e auto-scroll (scrollIntoView) ao escolher o dia.
- Resumo ganha linha "Quantidade" e a nota do total é genérica (N un × valor).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 04:14:50 +02:00
VPS 4 Builder 8ec1e6204d feat(salas): move o modelo de locação para o passo "Dados"
- Seletor de modelo (Hora/Período/Dia/Semana/Mês) sai do passo Calendário e
  passa a ficar no passo Dados — o usuário escolhe o modelo primeiro, depois
  o calendário/horário reagem à escolha.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 04:02:24 +02:00
VPS 4 Builder 69a0bce4d7 fix(salas): renomeia modelos (Hora/Período/Dia/Semana/Mês) + calendário completo
- Rótulos dos modelos: Turno→Período, Diária→Dia, Semanal→Semana, Mensal→Mês
  (ids inalterados). Label do controle "Turno" → "Período".
- Calendário cortado no wizard: as células aspect-square deixavam a grade alta
  demais p/ a área do slide; troca p/ altura fixa h-9 — a grade fica compacta e
  aparece completa.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 03:51:26 +02:00
VPS 4 Builder f24a5a82e8 feat(salas): reserva vira wizard de 4 passos em slide
Modal de reservar sala reorganizado num wizard (formato slide, translateX) com
stepper clicável p/ voltar:
1) Dados — reservar como (pessoal/unidade) + profissional + observações.
2) Calendário — modelo (cartões c/ preço) + calendário mensal + horário/turno.
3) Resumo — resumo detalhado (sala, modelo, período, total) + histórico de
   reservas da sala.
4) Pagamento — total, forma de pagamento (PIX/Dinheiro/Transferência/Cartão) e
   como funciona; a forma escolhida vai nas observações do pedido.
Navegação Voltar/Próximo (Próximo bloqueado no calendário até escolher o dia).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 03:46:02 +02:00
VPS 4 Builder dab74de207 feat(salas): modal de reserva com calendário mensal + seleção de modelo por cartão
Substitui os dois datetime-local crus por um calendário mensal profissional
(navegação de mês, dia selecionável, dias passados desabilitados, dias ocupados
com marcador, destaque de intervalo p/ semanal/mensal). Modelo de locação vira
grade de cartões com o preço de cada (Hora/Turno/Diária/Semanal/Mensal, só os que
a sala oferece). Controles conforme o modelo: hora (início + duração), turno
(manhã/tarde/noite); diária/semanal/mensal calculam o período. Resumo do período
+ ocupados do dia selecionado. início/fim derivados do calendário (backend igual).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 03:34:36 +02:00
VPS 4 Builder 5a790ff000 fix(salas): alugar-salas ↔ minhas-salas não trocava o conteúdo (só a URL)
As duas rotas renderizam o mesmo SalasPlugin com prop `view` diferente; o `tab`
vinha do useState (só no mount) e não reagia à troca de `view`. Agora um efeito
reseta a aba ativa quando `view` muda.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 03:07:02 +02:00
VPS 4 Builder 29f9139b6c feat(ui): centraliza estilo de filtros/busca (components/filterStyles) + SearchInput
Fonte única para os campos de filtro (inputNew/labelNew) e um componente
SearchInput padrão (ícone + input arredondado, encaminha ref/props). Profissionais
e Salas passam a importar do módulo (dedupe). Aplica SearchInput na busca do
Orçamentos como 1ª adoção. Buscas que são autocomplete com dropdown (Pacientes,
Relatórios) ficam para conversão cuidadosa caso a caso (não quebrar o dropdown).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 02:36:17 +02:00
VPS 4 Builder e0b37797b9 feat(salas): filtros do alugar-salas iguais ao marketplace de profissionais + fix própria sala
- Área de filtros de /alugar-salas restilizada igual ao marketplace de
  profissionais: card com busca no topo, filtros recolhíveis (Ocultar/Mostrar),
  slider de raio com badge KM, barra de ações (Limpar + Buscar) e busca por
  texto (nome/cidade/bairro) sobre o resultado.
- Fix: o marketplace listava as PRÓPRIAS salas (e as da unidade ativa) — agora
  exclui owner_usuario_id do usuário e clinica_id da unidade ativa. Não se aluga
  a própria sala.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 02:26:22 +02:00
VPS 4 Builder 041803ebb3 feat(ui): padroniza abas no segmented control (componente SegmentedTabs)
Cria components/SegmentedTabs.tsx (controle segmentado padrão, responsivo, com
cor de destaque + ícone + badge) e converte as barras de aba que usavam sublinhado
ou pills soltos: Comissões, Glosas, Lab Clientes, Prótese (OS + cotações),
Contratos, Profissionais (marketplace) e HorariosConfig. Telas que já usavam o
visual (Gestão de Equipe, Notificações, RX, Salas) ficam como referência.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:26:28 +02:00
VPS 4 Builder 3ca89ab21c feat(salas): melhora visual e distribuição de Minhas Salas / Alugar / Marketplace
- Abas viram segmented control: no mobile ocupam a largura toda e distribuem igual;
  no desktop encolhem ao conteúdo (some quando há só 1 aba).
- "Cadastrar Sala" ganha toolbar (contagem à esquerda, botão à direita no desktop,
  largura cheia no mobile) em vez de botão solto.
- Filtros do marketplace: grid 2/3/6 colunas (distribui uniforme) e BUSCAR full-width
  no mobile.
- Cards de sala: 3 colunas em telas largas (xl).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:01:51 +02:00
VPS 4 Builder ffcace0133 fix(superadmin): item "Secretária IA" faltava no allowlist do menu do superadmin
O menuItems do Sidebar filtra por um allowlist de ids p/ o superadmin, e
'superadmin-secretaria' não estava incluído — por isso a página de motivos não
aparecia no menu. Adicionado ao allowlist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:55:44 +02:00
VPS 4 Builder 65244c983f feat(superadmin+wa-secretaria): motivos do botão "desativar para todos os chats"
Botão global no /wa-secretaria agora abre modal com textarea de motivo (obrigatório
p/ desativar) + histórico, em vez de um confirm. Página super admin "Secretária IA"
passa a listar os motivos por ESCOPO: "Todos os chats" (global) e "Uma sessão",
mesclando os dois logs do motor (/secretaria/power/log + /session-power/log).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:49:34 +02:00
VPS 4 Builder d8d8617a6c fix(wa-inbox): esvazia a lista de chats quando não há instância ativa
Complementa o fix anterior: no mount, loadChats podia rodar com o activeInstanceId
persistido (stale) antes de loadInstances validar, deixando os chats na tela mesmo
após o reset. Agora o efeito faz setChats([]) quando não há instância ativa.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:24:05 +02:00
VPS 4 Builder f343d8e7a7 fix(wa-inbox): não vazar sessão/chats de outro usuário (limpa estado local no logout)
O /wa-inbox persistia activeInstanceId + inboxCache:v1:* no localStorage e o logout
não limpava — então o próximo login (ex.: superadmin) herdava a instância e via os
chats antigos de outra sessão. Agora: clearInboxLocalState() no logout, na expiração
de sessão e na troca de workspace; e loadInstances reseta activeInstanceId se ele não
estiver na lista de instâncias acessíveis (guarda defensiva).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:21:25 +02:00
VPS 4 Builder b1ddfbf349 fix(superadmin): página Secretária IA mostra só motivo/ação/quando (sem dados da clínica)
Remove colunas Número e Quem (telefone/pessoa da clínica) da tabela — a página é
contexto GERAL para melhorar a IA, não para ver dados de uma clínica. Ajusta o texto.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:15:22 +02:00
VPS 4 Builder b67fa15c55 feat(superadmin): página "Secretária IA" com motivos de liga/desliga por sessão
Nova aba no super admin (/superadmin/secretaria): tabela com quem ligou/desligou a
Secretária de cada número, quando e o MOTIVO do desligamento. Endpoint
/api/superadmin/secretaria-desligamentos (superadminGuard) busca o log global no
motor (resolve um e-mail de dono pareado; log é global). Item no Sidebar + rotas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:02:39 +02:00
VPS 4 Builder cfeac891fb feat(wa-inbox): botão liga/desliga da Secretária por sessão na thin sidebar
Botão Power na thin sidebar do /wa-inbox age SÓ na sessão/número aberto (verde=on,
vermelho pulsante=off). Abre modal com textarea de motivo (obrigatório p/ desligar),
histórico de quem ligou/desligou e aviso de rate-limit (1x/hora). secSessionPowerApi
+ SessaoSecretariaPower.tsx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:39:41 +02:00
VPS 4 Builder f30b0ab732 feat(agenda): linha da meia-hora dividindo cada hora em 2 (estilo Google)
Grade de 30min com rótulo só na hora (slotDuration 30min + slotLabelInterval 1h);
a linha de :30 fica tracejada/fraca (fc-timegrid-slot-minor), distinta da linha
cheia da hora — como na agenda do Google.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:34:02 +02:00
VPS 4 Builder ddcc639ab6 feat(agenda): coluna "Agenda Clínica" no desktop com a estrutura do mobile
As ações da coluna de controles no desktop eram só uma fileira de ícones sem
rótulo (difícil saber o que era cada um). Agora o desktop herda a estrutura do
mobile: botões empilhados com ícone + nome (Atividade da equipe, Pedidos pendentes,
Configurações, Meus Horários, etc.), badges reposicionados junto ao ícone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:22:04 +02:00
VPS 4 Builder 40bed96515 feat(newwhats): agenda inteligente — especialidades, situações, dúvidas, checklist do dono e pendências
Satélite da Secretária IA:
- /contexto (dentistas+especialidades+situações+cautelas) e /duvida (encaminhar
  ao humano sem agendar); agenda_pedidos ganha tipo=duvida + resumo.
- Situações da clínica: tabela agenda_situacoes + config (dono) + aba na UI.
- Checklist do dono (checklist-defs.js + agenda_checklist): perguntas Sim/Não
  conferidas contra o banco (dentistas/pacientes) → pendências de config +
  cautelas p/ a IA não chutar.
- Área de PENDÊNCIAS DO DONO (PendenciasDono.tsx): engloba config + fila
  operacional (horários/dúvidas); botão+badge no /wa-secretaria (só o dono).
- Fila humana (PedidosPendentes): cards de dúvida + resolver; /resolver.
- Botão DESLIGAR global da Secretária no ThinNav (secPowerApi, visível a todos).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:40:51 +02:00
VPS 4 Builder ca2cfa1f35 feat(agenda): janela IA-livre vs estendida (zona cinza) + fila da secretária humana
- Duplicar horário para todos os dias (botão na grade).
- Janela do dentista com DOIS limites: "IA agenda livre" [inicio,fim] e "aceito
  confirmar também" (mais cedo/mais tarde) — colunas hora_inicio_flex/hora_fim_flex.
- Zona cinza: quando o cliente pede um horário fora da janela livre, a Secretária IA
  NÃO agenda — registra um pedido (POST /pedido) e diz "vou confirmar e retorno".
- Fila da secretária humana: tabela agenda_pedidos + endpoints (listar/confirmar/
  recusar); tela PedidosPendentes (confirmar → vira agendamento real, com escolha do
  dentista); botão com BADGE de contagem na Agenda (poll a cada 30 min) para staff.

Testado em dev: dentista configura flex; IA registra pedido de 8h30 e responde "vou
verificar com a secretária"; humana confirma → agendamento criado.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 05:29:25 +02:00
VPS 4 Builder 773e8d4bf2 fix(agenda): fuso automático (America/Sao_Paulo) + dentista configura seus horários
- TZ=America/Sao_Paulo no backend (compose): a agenda deixa de rodar em UTC —
  horários/slots passam a ser Brasília automaticamente (corrige deslocamento de 3h).
- Acesso do DENTISTA: novo botão "Meus Horários" na agenda do dentista, abrindo
  a config direto nos horários/folgas DELE (HorariosConfig com initialTab/soDentista,
  restrito ao próprio registro). Antes só admin/donoclinica alcançavam a config.
- Config da clínica: gate passa a incluir donoconsultorio (dono de consultório).

Obs.: AgendaView.tsx também traz trabalho acumulado da agenda que estava no
working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 04:44:38 +02:00
VPS 4 Builder 0a324bf8c8 feat(secretaria): folga do dentista por data (A) + salas alugadas na agenda (B)
A — Folga/férias do dentista por DATA: tabela dentistas_folgas; o dentista (ou o
dono) marca dias em que não atende; o /slots pula o dentista nessas datas.
Endpoints /dentista/:id/folgas (GET/POST/DELETE) + UI na aba Dentistas.

B — Salas alugadas: o dentista com sala_reserva aprovada passa a ter horários
oferecidos pela Secretária no quarto (disponibilidade = janela da reserva,
casada pelo usuario_id). /slots ganha passada de salas (slot com sala_id +
local); /book grava agendamentos.sala_id (coluna aditiva). Geração passa a usar
CANDIDATE_CAP e ordena por horário (clínica e salas competem).

Testado em dev: folga pula os dias do dentista; reserva de sala gera slots com o
nome da sala.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 02:12:50 +02:00
VPS 4 Builder 77dec56dd0 feat(secretaria): horários de funcionamento + feriados na agenda/Secretária
A Secretária passa a saber quando a clínica está aberta e a NÃO oferecer
horário/agendar em dia fechado (domingo, fora do horário, feriado).

- feriados.js: feriados nacionais BR (fixos + móveis via Páscoa) + resolver
  com exceções da clínica.
- agenda-schema.js: tabelas clinicas_horarios (horário do dono) e feriados
  (exceções/fechamentos), idempotentes.
- agenda-config.js: CRUD com ownership (horário/feriado da clínica = dono;
  horário do dentista = ele mesmo ou o dono), em /api/nw/agenda-config.
- agenda-bridge.js: GET /status (aberto hoje?, motivo, próximo aberto, grade,
  feriados) + /slots agora = clínica ∩ dentista − feriado.
- HorariosConfig.tsx + botão no AgendaSettingsModal: telas de configuração
  (abas Clínica e Dentistas).

Testado em dev: a Secretária ofereceu "amanhã" ao pedirem "hoje" num domingo,
com os horários configurados da clínica.

Obs.: index.js também traz trabalho acumulado do satélite (proxy de mídia via
Wasabi/storage) que estava no working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 01:23:27 +02:00
VPS 4 Builder 03711e6280 feat(secretaria): agente/cérebro por número na aba Números
Adiciona o seletor "Agente / Cérebro deste número" no NumbersPanel: cada
número de WhatsApp passa a apontar para um agente (sec_numbers.agent_id),
tornando a Secretária, os nós do cérebro e a agenda separados POR SESSÃO.
Mostra o cérebro vinculado em cada card; tipos SecNumber.agent_id e
CalendarSlot.instance_id + param instance_id no calendar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 19:48:29 +02:00
VPS 4 Builder 09edeb9558 feat(newwhats): prévia via thumbnail Wasabi + guard de mídia irrecuperável
build-and-promote / build (push) Has been skipped
build-and-promote / promote (push) Successful in 1m42s
- MessageBubble: carrega <url>.thumb.webp na prévia (fallback ao full via onError)
- mediaUnavailable proativo via flag media_recoverable (evita 400 em mídia legada)
- useNewInboxBridge: mapeia mediaRecoverable -> media_recoverable

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:05:42 +02:00
VPS 4 Builder 8b1801bd9c feat(agenda): layout em 2 colunas no desktop + sidebar recolhível
- AgendaView: controles migrados do header horizontal para uma coluna lateral
  esquerda (AGENDAR, ícones de ação em faixa horizontal de tamanho igual,
  presença, legenda de dentistas); calendário em flex-1 ocupando a altura toda
- Mobile revisto: cabeçalho compacto (título + AGENDAR) e calendário flex-1
- Sidebar do sistema recolhível no desktop (App: estado persistido + botão
  flutuante para reexibir); toggle fica ao lado do título "AGENDA CLÍNICA"
- Toolbar do FullCalendar compactada (título/botões/cabeçalhos menores)
- Título da agenda responsivo (não gera scroll horizontal na coluna estreita)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 02:32:54 +02:00
VPS 4 Builder c2a1f8f701 fix(newwhats): resolve conversa por 9º dígito e nome do paciente no slot da agenda
- WhatsChatDrawer: busca o chat pelos últimos 8 dígitos (o WhatsApp grava o jid BR
  sem o 9º dígito, ex. 556799591687), evitando falso "sem conversa"
- AgendaView: slot usa pacienteNome || pacientenome (backend retorna minúsculo),
  corrigindo o card que não exibia o nome do paciente

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 00:29:02 +02:00
VPS 4 Builder 21bebd3256 feat(newwhats): ownership de sessões, auto-provisionamento, assinatura e drawer na agenda
- Drawer de WhatsApp focado (WhatsChatDrawer) aberto do slot do agendamento, com
  dropdown de números do paciente + grupo familiar e fluxo de iniciar conversa
- Auto-provisionamento de conta no motor: eager no cadastro/convite + self-heal no
  proxy (404 "conta não encontrada" → provisiona e refaz o request)
- Ownership por sessão (wa_session_authz): dono do workspace (owner_id ou vínculo
  donoclinica/donoconsultorio) tem poder total; delega re-scan, excluir sessão,
  apagar conversa/mensagem, acesso ao Inbox e à Secretária — por conta
- Enforcement no proxy: guardSessionAction (scan/rescan/delete sessão),
  guardAreaAccess (inbox/secretaria), guardInboxDestructive (apagar conversa/msg)
- Endpoints /api/nw/authz (dono gerencia) e /api/nw/access (acesso agregado)
- Assinatura do operador nas mensagens (*Nome*, desambiguação de homônimos → *Rui C.*)
- UI: painel "Gerenciar acesso" com 5 toggles; menu e botões de apagar condicionados
- Proxy de mídia /api/nw/media + re-download sob demanda

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:26:31 +02:00
VPS 4 Builder 372431c0a1 style(newwhats): painel de agentes +20% no desktop e tela cheia no mobile
Painel do meio (selecionar agente) passa de 280px para 336px (md:) e, no mobile,
ocupa a tela (flex-1) com o painel principal oculto (hidden md:flex).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 05:20:03 +02:00
VPS 4 Builder ecf52d961a feat(newwhats): envia x-nw-clinica do workspace ativo (modelo de canal)
nwClient inclui o header x-nw-clinica (id do SCOREODONTO_ACTIVE_WORKSPACE) e o
proxy REST o encaminha ao motor — assim as ações manuais escopam a ponte de
agenda para a clínica do workspace ativo (por-requisição).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 05:19:44 +02:00
VPS 4 Builder 3e1001b7c7 style(newwhats): /wa-sessions com sidebar do scoreodonto + tema branco
- wa-sessions sai do isStandaloneView → renderiza com a sidebar (área de
  administração); wa-inbox/wa-secretaria seguem em tela cheia.
- SessionsView convertida do dark para o tema branco/profissional (bg-gray-50/
  white, text-gray-*, badges claros, botão Conectar em brand); backdrops de
  modal e botões coloridos preservados.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 03:55:16 +02:00
VPS 4 Builder 821ae591d4 style(newwhats): tema branco/profissional na Secretária (/wa-secretaria)
Converte a SecretariaView do dark para o tema claro do scoreodonto
(bg-gray-50/white, text-gray-*, border-gray-*), preservando botões brand,
backdrops de modal e acentos.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:42:34 +02:00
VPS 4 Builder 092635be5a fix(realtime): guarda cleanup de WebSocket em CONNECTING (StrictMode)
Fechar o socket ainda em CONNECTING (double-mount do StrictMode em dev) gerava
"WebSocket is closed before the connection is established". closeSocket() adia o
close para o onopen e neutraliza handlers; reconexão por queda inesperada intacta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 20:54:24 +02:00
VPS 4 Builder 3042ddca38 feat(newwhats): frontend do plugin (Inbox/Sessions/Secretária) em tela cheia
- /wa-inbox, /wa-sessions e /wa-secretaria rodam sem a sidebar do scoreodonto
  (isStandaloneView); cada tela tem seu "Voltar" (Secretária ganhou botão na ThinNav).
- SessionsView: banner "Você já possui acesso ao WhatsApp" quando há sessão ativa.
- avatares: usa a URL do CDN (contactAvatarUrl/instance.avatar) direto; Avatar
  exibe sem depender de version; self-heal no onError re-busca a foto atual.
- deps: framer-motion, immer. Dev override com HMR (docker-compose.dev.yml).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 20:54:24 +02:00
VPS 4 Builder 872acd800a fix(sidebar): itens WHATSAPP/SECRETÁRIA usam isPluginActive (ALWAYS_ON)
getActivePlugins() não inclui ALWAYS_ON_PLUGINS; o gate dos itens checava
activePluginIds.includes('newwhats') (sempre falso). Troca para isPluginActive
('newwhats'), que considera ALWAYS_ON — agora os menus aparecem.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:14:59 +02:00
VPS 4 Builder 13af039555 feat(newwhats): pareamento por TOKEN — cole 1 token e auto-configura
- Backend POST /api/nw/config/token (superadmin): decodifica nwpair_<base64url({u,k,s})>,
  VALIDA no motor (/api/ext/v1/sessions) e salva URL+chave+webhook. Token inválido/
  sem conexão é rejeitado antes de salvar.
- Frontend: campo 'Token de pareamento' como caminho principal (textarea + Aplicar);
  estado da conexão; clínica no dropdown (local); campos manuais em 'configuração
  manual' (avançado).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 19:05:13 +02:00
VPS 4 Builder abbd9374b4 feat(newwhats): config DB-backed + UI dedicada (dropdown de clínicas, testar conexão)
Backend:
- config.js: config lida da tabela settings (id=newwhats_config) com fallback ENV;
  loadConfigFromDb/saveConfigToDb + cache.
- index.js: endpoints superadmin GET/PUT /api/nw/config (secrets nunca em claro;
  branco mantém valor), GET /api/nw/clinicas (dropdown), GET /api/nw/status (testar
  conexão). server.js passa { superadminGuard }.
Frontend:
- PluginsView: NewwhatsConfigModal dedicado (carrega/salva no backend, dropdown de
  clínicas em vez de ID cru, botão Testar conexão). Substitui o form genérico p/ newwhats.

Resolve UX ruim: nada de digitar ID de clínica; config passa a funcionar de fato.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 18:55:54 +02:00
VPS 4 Builder b031117925 feat(frontend): plugin NewWhats — Inbox + Secretária (gating por papel)
- PluginsView: registra o plugin 'newwhats' (config só no catálogo, exclusivo do
  superadmin via view 'plugins' — já gateada em isViewAllowed).
- pluginRegistry: 'newwhats' em ALWAYS_ON (ativação por plugin é local/localStorage
  e não propaga; ALWAYS_ON garante Inbox/Secretária para TODOS os usuários).
- App.tsx: views wa-inbox/wa-secretaria (ViewKey, rotas, render); gating
  isPluginActive('newwhats') libera a qualquer usuário.
- Sidebar: itens WHATSAPP + SECRETÁRIA IA quando o plugin está ativo.
- Novas views consomem o proxy /api/nw/v1/* (motor ext-api). Estágio 1: funcional
  (lista/thread/envio; ask da secretária), visual aproximado do motor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 17:57:46 +02:00