Suspensão temporária do agendamento via feature flag

TLDR: uma flag scheduling_suspension que, ligada, exibe o TopBanner com o aviso “O agendamento em nosso sistema precisará ser suspenso por 24h.” e faz todo agendador do front se comportar como se o terapeuta não tivesse horários disponíveis — para clientes, terapeutas e visitantes deslogados.

Contexto

O agendamento precisará ser suspenso por 24h. Durante esse período os usuários devem ser avisados e não podem conseguir marcar nem reagendar sessões, e a suspensão precisa ser ligada e desligada sem deploy.

O TopBanner (#505, #510) já existe, mas saiu do Header no #547 junto com a flag COST_REDUCTION_PHASE_ONE, e o MobileMenu perdeu o ajuste de posição do popover quando o banner está visível.

Todo fluxo de escolha de horário passa pelo hook useScheduler (src/hooks/useScheduler.ts), via UseSchedulerProvider:

  • perfil público do terapeuta (PublicProfile → ProfileScheduler)
  • modal de agendamento do card de terapeuta (TerapeutaCard → withScheduler)
  • “Agendar Nova Sessão” em Meus Terapeutas (pages/app/meus-terapeutas/[id].tsx)
  • “Reagendar Sessão” no painel, Minhas Sessões e Minha Agenda (ReschedulingSession)
  • “Agendar sessão” do voucher na carteira (ReschedulingVoucher)

Quando availableDays está vazio, esses agendadores já têm uma variante própria: exibem “Sem horários disponíveis para estas datas.” (NO_AVAILABLE_DAYS) e o botão “Confirmar Agendamento” fica desabilitado. A suspensão reaproveita essa variante em vez de esconder telas.

Os flags vêm do trgclub-api (GET /features/:user_id, sem autenticação), que lista as features do Flipper. Hoje FeatureFlag::FlipperAdapter#user_features retorna [] quando não há usuário, e o useFeature do front só busca flags com usuário logado — então visitantes deslogados nunca recebem flag ligada.

Objetivos

  • Uma única flag scheduling_suspension (Flag.SCHEDULING_SUSPENSION) controla o banner e a suspensão.
  • Banner — com a flag ligada, o Header renderiza o TopBanner:
    • description: “O agendamento em nosso sistema precisará ser suspenso por 24h.”
    • sem link (label e href passam a ser opcionais no TopBanner)
    • chave de persistência muda de top-banner-storage para top-banner-storage-v2, para reaparecer a quem dispensou o banner antigo
    • regras atuais do useTopBanner mantidas (só autenticado, fora das rotas excluídas, dismiss persistido)
  • Responsividade — com a flag ligada e o banner visível, o popover do MobileMenu volta a usar marginTop: 1.5rem; abaixo de 391px, marginTop: 2rem e maxHeight: calc(100vh - 10rem). Caso contrário, 1rem.
  • Agendamento — com a flag ligada, useScheduler não busca disponibilidade e retorna availableDays vazio. Todos os agendadores listados no contexto passam a exibir a variante “Sem horários disponíveis para estas datas.” e não permitem confirmar.
  • Deslogados — a flag também vale para visitantes sem login (perfil público /terapeuta/[slug]):
    • front: useFeature passa a buscar /features/anonymous quando não há usuário, com queryKey ["features", userId || "anonymous"]
    • backend (trgclub-api, PR separado): FlipperAdapter#user_features retorna as features habilitadas globalmente (Flipper.enabled?(feature)) quando não há usuário
    • a flag precisa estar ligada globalmente (“Fully enabled”) no Flipper para valer para deslogados

Fora de escopo

  • Criar/ligar a flag no Flipper — feito manualmente em /features_ui do trgclub-api.
  • Esconder menus, botões ou páginas (Minha Agenda, Minha Disponibilidade, carteira, checkout): continuam visíveis; só os horários aparecem como indisponíveis.
  • Tela de disponibilidade do terapeuta (/app/minha-disponibilidade) e onboarding de disponibilidade: não mudam — o terapeuta segue podendo configurar, só ninguém consegue marcar.
  • Bloqueio no backend (criação de meeting, reagendamento, voucher, checkout por URL já montada) — a suspensão é apenas de interface.
  • Alterar estilos do banner, rotas excluídas ou a página /novidades.

Mudanças

trgclub-web

  • src/services/feature/types.ts — SCHEDULING_SUSPENSION = "scheduling_suspension" no enum Flag.
  • src/infra/FeatureManager/useFeature.ts — buscar /features/anonymous sem usuário; queryKey ["features", userId || "anonymous"]; remover enabled: !!userId.
  • src/hooks/useScheduler.ts — ler useFeature().isEnabled(Flag.SCHEDULING_SUSPENSION); quando ligada, query de disponibilidade com enabled: false e availableDays retornado como [].
  • src/components/TopBanner/index.tsx — label e href opcionais; S.Link só renderiza quando ambos existem.
  • src/stores/topBanner/topBanner.ts — name: 'top-banner-storage-v2'.
  • src/components/Header/Header.tsx — TopBanner dentro de <Feature flag={Flag.SCHEDULING_SUSPENSION}><Feature.On>, logo após {withFlags && <TRGFlags />}.
  • src/components/MobileMenu/MobileMenu.tsx — MobileMenuWithPopover volta a calcular isEnabled(Flag.SCHEDULING_SUSPENSION) && shouldShow e monta o PopoverComponent com React.useMemo, aplicando os offsets acima.
  • Testes: useScheduler (flag ligada → sem fetch e availableDays vazio; desligada → comportamento atual), useFeature (anônimo busca /features/anonymous), TopBanner (com e sem link), topBanner.test.ts se referenciar a chave.

trgclub-api (PR separado)

  • app/services/feature_flag/flipper_adapter.rb — sem usuário, user_features retorna all_features.select { |f| Flipper.enabled?(f) }.
  • Spec cobrindo usuário ausente com feature ligada globalmente e com feature ligada só por ator.

Como verificar

  • Flag desligada ou inexistente: nenhum banner; agendadores mostram horários normalmente; MobileMenu com marginTop: 1rem.
  • Flag ligada globalmente:
    • logado (cliente ou terapeuta) em /app/...: banner aparece sem link; ao fechar, some e não volta após reload; quem tinha fechado o banner antigo vê o novo
    • perfil público /terapeuta/[slug] logado e deslogado: “Sem horários disponíveis para estas datas.” e “Confirmar Agendamento” desabilitado
    • modal do card de terapeuta, “Agendar Nova Sessão”, “Reagendar Sessão” (painel, Minhas Sessões, Minha Agenda) e voucher da carteira: mesma variante sem horários
    • MobileMenu com banner visível: popover abaixo do banner sem sobreposição em desktop e em viewport < 391px
  • Desligar a flag restaura os horários sem deploy (após o staleTime de flags ou reload).
  • Testes, lint e typecheck sem erros nos dois repos.

Documentação

  • Criar .project/docs/rules/scheduling/scheduling_suspension.md descrevendo a regra da flag scheduling_suspension (o que muda para cliente, terapeuta e deslogado) e registrar no índice .project/docs/README.md.