Commit Graph

4603 Commits

Author SHA1 Message Date
Rodribm10
d831ee4d33 feat(reports): Painel Diretoria — Onda 1A (leitura)
Primeira onda do roadmap de indicadores executivos do Grupo Nova. Mede
ADOÇÃO DO CANAL DIGITAL, não a operação total — banner explícito alerta
que reservas fechadas manualmente na recepção ainda não estão capturadas
(Onda 1B vai adicionar marcação manual via botão na conversa).

Backend:
- V2::Reports::ConversionFunnelBuilder — leads (novo/retorno/total),
  reservas (criadas != draft, pagas in active/completed/confirmed),
  taxas de conversão. Filtro opcional por inbox.
- V2::Reports::InboxBenchmarkingBuilder — uma linha por inbox com
  brand_name (via Captain::UnitInbox -> Unit -> Brand)
- Endpoints GET /reports/conversion_funnel e /reports/inbox_benchmarking
- RSpec do ConversionFunnelBuilder

Frontend:
- Rota top-level Reports → Painel Diretoria
- DirectoryDashboard.vue: banner de adoção + filtros + cards + funil + tabela
  benchmarking agrupada por marca com variação vs média
- API client getConversionFunnel + getInboxBenchmarking
- i18n EN + PT

Memórias suporte: feedback_metricas_adocao_canal.md + project_painel_diretoria_roadmap.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:44:59 -03:00
Rodribm10
617eadbfe4 feat(reports): breakdown auto/manual no BotReports + nomes corretos
Engrenagem fechada e nomes que casam com o que a métrica mede:

Cards superiores (taxas):
- "Tempo de resolução" -> "Resolvidas pelo bot %"
  Tooltip: bot resolveu sozinho (sem humano via Chatwoot ou WhatsApp) / total
- "Taxa de entrega" -> "Transferidas pra humano %"
  Tooltip: agora soma auto (Jasmine chamou) + manual (humano respondeu sem
  handoff explícito). Junto com a resolução, fecha ~100%.

Cards de detalhe (segunda linha, contagem absoluta):
- "Resolvidas pelo bot" — quantas o bot fechou sozinho
- "Transferência automática (Jasmine)" — bot_handoff explícito (loop, timeout,
  max turns, intent)
- "Tomada manual (agente)" — humano respondeu (UI ou WhatsApp echo) SEM a
  Jasmine ter chamado bot_handoff. Era o "bucket fantasma" antes.

Backend:
- BotMetricsBuilder.metrics inclui bot_resolutions_count, auto_handoffs_count,
  manual_takeovers_count
- handoff_rate agora é (auto + manual) / total — daí a engrenagem fechar
- manual_takeovers_count: conversas com mensagem outgoing humana
  (sender_type='User' OR NULL) MENOS as que tiveram conversation_bot_handoff

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 12:01:50 -03:00
Rodribm10
e9b8b6e587 feat(reports): filtro por inbox no Relatório do Bot
Hoje as métricas e séries do BotReports agregam toda a conta — não dá pra ver
"a Jasmine da PrimeAL está errando mais que a do Qnn01". Cada unidade tem
prompt próprio, então um sintoma localizado precisa de medição localizada.

Backend:
- Inbox#has_many :reporting_events (relação inversa que faltava)
- BotMetricsBuilder aceita inbox_id e filtra bot_conversations + base_reporting_events
- bot_metrics endpoint passa inbox_id pelos params permitidos
- count_report_builder já suporta scope=inbox; agora funciona pra
  bot_resolutions_count e bot_handoffs_count graças à relação acima

Frontend:
- BotReports.vue: ReportFilters com filter-type='inboxes' e dropdown ativo
- Quando uma inbox é escolhida, requestPayload inclui inboxId/type/id e os
  fetches (BotMetrics + ReportContainer) passam o filtro
- API client getBotMetrics aceita inboxId; getBotSummary aceita type+id
- Sem inbox selecionada: comportamento antigo (agregação da conta)

Bonus na rake task de retroativo:
- rebuild_bot_resolved.rake: Message.unscope(:order) pra evitar conflito
  PG::InvalidColumnReference (DISTINCT + ORDER BY default scope)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:47:46 -03:00
Rodribm10
7cd2ea1258 fix(reports): tratar resposta humana via WhatsApp como interação humana
Bug do BotReports descoberto pelo Rodrigo:
- A regra de "conversation_bot_resolved" só desqualificava conversas com
  outgoing.sender_type='User' (atendente respondendo pelo Chatwoot UI)
- Mas mensagem outgoing vinda do webhook WhatsApp com IsFromMe=true (atendente
  respondeu direto pelo celular do hotel) é gravada com sender=nil
- Resultado: a Jasmine ganhava crédito mesmo quando humano respondia fora
  do Chatwoot. Taxa de resolução pelo bot inflada.

Fix prospectivo:
- ReportingEventListener#create_bot_resolved_event agora desqualifica via
  human_outgoing_messages? (sender_type='User' OU sender_type IS NULL)
- Captain::Assistant (a Jasmine) usa sender_type='Captain::Assistant' e segue
  fora do filtro, como antes
- Spec novo cobrindo o caso WhatsApp echo

Retroativo:
- lib/tasks/rebuild_bot_resolved.rake — task idempotente que purga
  reporting_events de conversation_bot_resolved gerados sob a regra antiga.
- DRY-RUN por padrão, APPLY=true pra deletar, ACCOUNT_ID pra restringir,
  SNAPSHOT_PATH pra trilha de auditoria

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 11:35:08 -03:00
Rodribm10
429567495f fix(reports): inbox_id reativo + nome da inbox visível na tab Novas × Retorno
Bugs originais (tabela Reports → Inbox → unidade específica → Novas × Retorno):
1. Backend recebia sempre inbox_id=1 — useFunctionGetter('inboxes/getInboxById', route.params.id)
   passava string crua, não Ref reativa, então o getter ficava travado no ID inicial
2. UX: tab não mostrava qual unidade estava sendo filtrada

Correções:
- InboxReportsShow.vue: passa inboxIdParam computed pra useFunctionGetter (reativo agora)
- Passa inbox.name como prop pro InboxLeadsReport
- InboxLeadsReport.vue: header com título + label "Caixa de entrada: <nome>" no topo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 09:14:44 -03:00
Rodribm10
3897db325e feat(reports): aba "Novas × Retorno" no Inbox Report
Mede por inbox/período: leads novos (1ª conversa do contato em qualquer
inbox da rede), retorno (conversa anterior resolved há >24h) e outras
(conversa anterior open ou resolved <24h). Categorias somadas batem com
o conversations_count nativo do report — bucket "outras" garante o
fechamento.

- Novo builder V2::Reports::InboxLeadsSummaryBuilder com CTE única
- Endpoint GET /api/v2/accounts/:id/reports/inbox_leads_summary
- Tabs no InboxReportsShow (Visão Geral | Novas × Retorno)
- Componente InboxLeadsReport com 3 metric cards + barras empilhadas
- API client + Pinia (state/getters/actions/mutations)
- i18n en + pt_BR
- RSpec do builder cobrindo classificação e isolamento por inbox

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:22:43 -03:00
Rodribm10
a682668f4f fix(wuzapi): força data URI octet-stream pra send_file
Wuzapi rejeita data URI com mime real (application/pdf) e exige
'data:application/octet-stream;base64,'. O tipo é inferido do FileName.
Imagens continuam usando o mime original.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 06:45:26 -03:00
Rodribm10
b97a56d43c fix(wuzapi): payload de /chat/send/document espera Document, não Body
Wuzapi retornou 400 'missing Document in Payload'. O campo correto pra
PDF é Document + FileName. Mantém Body/Filename pra fallback em versões
antigas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 06:38:11 -03:00
Rodribm10
5e7447f1d8 fix(wuzapi): troca /chat/send/file por /chat/send/document pra PDF
Wuzapi (asternic) atual usa /chat/send/document pra arquivos. O endpoint
/chat/send/file não existe (404), então PDF nunca chegava — propagado
como ECONNRESET pelo proxy. Mantém os antigos como fallback.

Também trata Errno::ECONNRESET como ConnectionError no http handler pra
ativar a cadeia de fallback se voltar a acontecer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 06:27:28 -03:00
Rodribm10
74aea5e2d5 fix(agents): converte Parameters pra Hash antes de chamar any?
ActionController::Parameters não tem .any?; chamar sem to_h quebra
com NoMethodError (500) ao salvar agente. Bug introduzido em b69fa21e5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:27:11 -03:00
Rodribm10
b69fa21e53 feat(aggressive-alert): filtro por inbox configurável por admin
Admin configura em Account Settings → Agents → Editar quais inboxes
disparam o banner agressivo (reopened + inatividade) pra cada agente.

- user.ui_settings.aggressive_alert_inbox_ids: null (todas) | [] (nenhuma) | [1,2,3]
- Filtro aplicado no actionCable.feedInactivityTracker, maybeTriggerAggressiveAlert
  e no hydrate do AggressiveConversationBanner
- Backend aceita ui_settings no agents#update e serializa em _agent.json.jbuilder
- UI no EditAgent com toggle "todas inboxes" + multiselect

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 11:51:00 -03:00
Rodribm10
49a21c845b fix(aggressive-alert): usa last_non_activity_message no hidrate + debug flag
O hidrate do inactivityAlertTracker lia conv.messages pra achar a última
mensagem do cliente. Mas o serializer da listagem só expõe 1 mensagem nesse
array (e pode ser uma activity, que é filtrada), então conversas em 'open'
com cliente esperando resposta não entravam no tracker e o banner de 5/15/28
minutos nunca disparava.

Fix: findLastNonActivityMessage agora usa conv.last_non_activity_message
primeiro (campo dedicado do payload, já pré-filtrado pelo backend) e só
cai em conv.messages como fallback.

Também adicionada flag de debug opt-in em window.__AGGRESSIVE_DEBUG__ pra
facilitar diagnóstico futuro do tracker direto do DevTools.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 08:41:00 -03:00
Rodribm10
6aa328e329 fix(aggressive-alert): hidrata tracker de inatividade no boot
Bug reportado em 2026-04-24: só o banner vermelho status→open
funcionava. 5/15/28min nunca disparavam.

Causa: o tracker só era alimentado via websocket ao vivo
(message.created). Se a msg do cliente chegou ANTES da aba carregar
(ou depois de F5), o tracker ficava vazio, setInterval nunca começava,
thresholds nunca disparavam.

Fix:
- Nova função `hydrateFromConversations(convs)` no tracker. Varre
  conversas em 'open', pega a última msg não-activity, se for de
  Contact registra com timestamp REAL (msg.created_at), não Date.now().
  Isso fecha o gap de tempo: se o cliente falou 7min atrás, o YELLOW
  já dispara na hora.
- AggressiveConversationBanner.vue tem agora `watch: allConversations`
  chamando hydrate toda vez que a lista muda (boot + F5 + navegação).
- parseCreatedAt() suporta Unix seconds + ISO.
- findLastNonActivityMessage() ignora mensagens de sistema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 08:00:38 -03:00
Rodribm10
d1fa5335e1 fix(whatsapp): remove janela de 24h pra providers Web (wuzapi, evolution)
A regra de 24h é da Meta Cloud API. Providers que conectam via
WhatsApp Web (baileys, zapi — já isentos; wuzapi, evolution — agora
isentos) permitem mensagem livre a qualquer momento.

Antes: agente via "Você só pode responder a esta conversa usando um
modelo de mensagem devido a Restrições de janela de mensagem de 24 horas"
e ficava bloqueado de digitar.

Agora: MessageWindowService.messaging_window retorna nil pros 4
providers Web, o que faz can_reply? retornar true incondicionalmente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 07:27:53 -03:00
Rodribm10
4b0e8c314e feat(aggressive-alert): escalada amarelo/laranja/vermelho + toggles
Banner agressivo passa de uma notificação só ("status→open") pra
um sistema de escalada baseado em inatividade quando o cliente é
o último a falar.

Níveis:
- 5 min sem resposta  → AMARELO, sem som
- 15 min sem resposta → LARANJA, beep 1x + notificação do SO
- 28 min sem resposta → VERMELHO pulsante + som em loop infinito
- status→open (reabertura) → VERMELHO imediato

Por conversa, o banner mostra um item com nome do contato, inbox
e contexto ("reabriu agora" / "15 min sem resposta"). Headline
grande e explicação clara sobre como limpa.

Comportamento do × dismiss:
- Antes: apagava o alerta de vez. Agente podia "fingir que viu".
- Agora: esconde temporariamente. Volta quando escalar (próximo
  threshold) ou nova mensagem. A única forma de LIMPAR de vez é
  responder o cliente (tracker detecta msg outgoing do User ou
  AgentBot e chama dismissForReply).

Permissões:
- account.settings.aggressive_alert_enabled (master switch admin)
- user.ui_settings.aggressive_alert_enabled (toggle do próprio agente)
- Default true pros dois; um false em qualquer bloqueia alertas.

Settings UI:
- Conta → General: novo card "Alerta agressivo (master switch)"
- Perfil do usuário: novo card "Receber alertas agressivos"

Arquivos:
- helper/aggressiveAlert.js: multi-level state, hide vs dismiss-for-reply
- helper/inactivityAlertTracker.js: timer único, thresholds declarativos
- helper/actionCable.js: hook em onMessageCreated (feed tracker) +
  isAggressiveAlertEnabled() + limpa tracker em status_changed != open
- components/app/AggressiveConversationBanner.vue: variantes de cor,
  headline grande, explanation, × temp-hide
- account.rb + accounts_controller.rb: store_accessor + permitted
- settings UI components (account + profile): switches auto-persist

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 20:49:24 -03:00
Rodribm10
f35c3ea821 fix(aggressive-alert): dispara em qualquer transição pra 'open'
Race condition: quando o próprio usuário reabre a conversa, o dispatch
HTTP (toggleStatus) comita CHANGE_CONVERSATION_STATUS no Vuex ANTES do
broadcast actionCable chegar. Aí o check previousStatus === 'open'
bloqueava o alerta porque o store já estava em status=open.

Broadcast conversation.status_changed (app/listeners/action_cable_listener.rb
linha 103) só é emitido em transição real. Conversa nova entra via
onConversationCreated, não por status_changed. Não precisa do lookup.

Removido: getConversationById + guarda early-return por previousStatus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:13:57 -03:00
Rodribm10
2b9ada259e feat(dashboard): aggressive alert on conversation reopening
Banner persistente + som em loop + OS notification + title flash
+ vibração mobile quando conversa transiciona pra 'open' vindo de
pending/snoozed/resolved. Exige interação pra dismissar — atendente
não perde evento de reabertura.

- AggressiveConversationBanner.vue: banner full-width no topo,
  dismissable, mostra nome do contato + inbox + status anterior
- aggressiveAlert.js: manager do som (loop infinito), title flash
  (intervalo 1s), Notification API (requireInteraction: true),
  navigator.vibrate (padrão 500-200-500-200-500)
- actionCable.onStatusChange: detecta transição pra 'open' e dispara
  trigger via BUS_EVENTS (só se status anterior ≠ open, pra não
  alertar conversa nova criada já em open)
- i18n pt_BR + en: chaves de notificação (title/body/dismiss)
- busEvents: AGGRESSIVE_ALERT_TRIGGER + AGGRESSIVE_ALERT_DISMISS

Camada 1 da feature. Camada 2 (escalation SMS/WhatsApp se não
dismissar em X segundos) fica pra outro PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:03:26 -03:00
Rodribm10
34d42dfbbd feat: remove Notificações Automáticas + ajusta assinatura WhatsApp
1. Captain — remove feature Notificações Automáticas (captain_notification_templates)
   Feature órfã: 0 registros em prod, substituída pela Jornada do Cliente
   (Lifecycle). Sem dependências fora dela própria. Removido:
   - Vue: routes/settings/captain/notifications/ + entry no captain.routes.js
   - Sidebar: item "Notifications" do Captain menu
   - Store: modules captainNotificationTemplates + import no store/index
   - API: api/captain/notificationTemplates.js
   - Controller: api/v1/accounts/captain/notification_templates_controller
   - Model: Captain::NotificationTemplate
   - Job: enterprise/app/jobs/captain/notifications/
   - Routes: resources :notification_templates no config/routes.rb
   - i18n: chaves CAPTAIN_SETTINGS.NOTIFICATIONS + SIDEBAR.CAPTAIN_NOTIFICATIONS
     em pt_BR e en (captain.json + settings.json)

   Tabela captain_notification_templates mantida (0 rows, sem consumidores).
   Se quiser drop, criar migration separada depois.

2. WhatsApp — tira colchetes do prefixo de assinatura
   Era: *[ Jasmine(PrimeAL) ]*\ncontent
   Agora: *Jasmine(PrimeAL)*\ncontent
   Afeta wuzapi_service (outgoing) + incoming_message_wuzapi_service (echo)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 15:34:45 -03:00
Rodribm10
b457e84c2f fix(captain): route embeddings to legacy OpenAI + retry transient errors
Resolve duas camadas de problema identificadas em teste end-to-end:

1. Embeddings falhavam com HTTP 404 (/codex/v1/embeddings não existe).
   Solução: Captain::Llm::EmbeddingService sempre usa OpenAI tradicional
   via Llm::Config.with_api_key(legacy_settings). ProviderConfig expõe
   legacy_openai_settings pra isso.

2. Servidor Codex ocasionalmente responde com response.failed +
   code=server_error (instabilidade transitória). Client agora retenta
   até 2x com backoff exponencial (0.5s, 1.5s) em erros retryable:
   HTTP 5xx, server_error no response.failed, ou stream inacabado.

Outras correções nesta etapa:
- Scenario#agent_model: em modo Codex, ignora CAPTAIN_OPEN_AI_MODEL_SCENARIO
  (que pode ter gpt-4o legado) e usa ProviderConfig.model.
- ExtractionService/ContradictionCheckerService/TranslateQueryService:
  trocam constantes hardcoded gpt-4o-mini/gpt-4.1-nano por
  ProviderConfig.light_model (respeitando o provider ativo).
- ProviderConfig.DEFAULT_CODEX_MODEL agora é gpt-5.2 (reconhecido pelo
  RubyLLM; gpt-5.4 não está no catalog do gem).

Validado ponta-a-ponta: WhatsApp → Chatwoot → Jasmine → handoff Daniela
→ faq_lookup com embedding OK → resposta com preços corretos.

Docs em docs/captain-codex-oauth.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:42:31 -03:00
Rodribm10
928b1ec6b9 feat(captain): Codex OAuth auth module + proxy controller
Implementa Fases 1+2 do plano Captain Codex OAuth.

Fase 1 — Auth módulo:
- Migration captain_codex_credentials (tokens AR-encrypted)
- Model Captain::CodexCredential (singleton-ish com .current)
- Captain::Codex::AuthService com device flow completo:
  start_device_login, poll_once, exchange_for_credential,
  valid_access_token (auto-refresh), refresh!
- Rake task captain:codex:{login,status,refresh}
- Sidekiq job Captain::Codex::RefreshTokensJob rodando a cada 30min

Fase 2 — Proxy Chat Completions → Responses:
- Captain::Codex::Translator (chat ↔ responses, tools, tool_calls)
- Captain::Codex::Client (streaming SSE → agregado)
- Api::Internal::CodexProxyController expondo
  POST /codex/v1/chat/completions
- 10 specs do Translator passando

Próximo: Fase 3 (feature flag + fallback) e reconfiguração dos
clientes RubyLLM/Agents/ruby-openai pra apontarem pro proxy quando
CAPTAIN_LLM_PROVIDER=openai_codex_oauth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:07:01 -03:00
Rodribm10
6fa2f621fa feat(retention): UI layer — badge, filters, cohort matrix, KPI dashboard
- RetentionSummaryBadge in the "Previous conversations" sidebar:
  tiered status (First contact / Active / Recurring / Sleeping /
  At risk / Inactive) + counts of interactions, one-shots, Pix.

- Retention tab in Captain Reports: KpiCards, FlowCard, CohortMatrix
  (12x13 heatmap with CSV export).

- Five new filters on the contacts list: recurring, last interaction,
  days since, interactions count, reservations paid.

- Full pt_BR + en i18n under CAPTAIN_REPORTS.RETENTION.*

- Spec for InteractionCalculatorService covering gap behavior,
  one-shot classification, internal-label exclusion, multi-conversation
  grouping across the 30h window.

- Docs: docs/captain-retention-indicators.md with business rules,
  column reference, endpoint shape, and backup SQL queries.
2026-04-22 10:30:19 -03:00
Rodribm10
aed6d62640 feat(retention): summary KPIs + cohort endpoints
Exposes two JSON endpoints under /api/v1/accounts/:id/captain/reports:
- GET /retention — aggregate KPIs (active/recurring/sleeping/at-risk/
  churned, new vs returned in period, Pix generated/paid/conversion,
  retention rates at 30d and 90d)
- GET /retention/cohort — monthly cohort matrix, 12 months lookback,
  12 months of offset. Each cell is % of the cohort that interacted in
  month M+N. SQL-aggregated with DATE_TRUNC + DISTINCT so it is a
  single query even on large histories.
2026-04-22 09:59:21 -03:00
Rodribm10
cfffea9c16 feat(captain): semantic memory fixes + roleta + reclamações + analytics
Consolida o trabalho desta branch de abril/2026 em um bloco pronto pra
testar em staging antes do merge pra main.

## Correções de memória semântica
- ExtractionService: Princípio Zero + Regra de Ouro (ação consumada vs intenção).
- Cenário Daniela_Reservas: Passo 0 de classificação (consulta/intenção/fora).

## Roleta da Sorte (end-to-end)
- Schema Supabase + 7 RPCs atômicas (server-side, idempotentes).
- Services: Offer, Redeem, WeeklyReport.
- Jobs: OfferRouletteJob (hook em ConfirmationService após Pix pago),
  NotifyRevealed + Scheduler de fallback.
- Tool manual GenerateRoletaLinkTool + endpoint público /roleta/notify.
- Dashboard /captain/roleta com Resgate + Relatório + anomaly detection.

## Cenário Reclamacoes_Ouvidoria
- Triagem P1-P4, framework LAST, Three-level listening, Self-check.
- Sem compensação material, detecção de cliente frustrado eleva prioridade.

## Analytics
- Funil de conversão /captain/funnel: 5 etapas via regex, zero LLM.
- Detector de churn via ChurnOutreach* (cron dias úteis 10h-17h BRT).

## Trabalho pré-existente incluído
- Captain Executive Reports (ceo_digest, mattermost_delivery).
- get_reserva_preco_tool, Lifecycle ajustes, Reservations UI polimentos.

## Outros
- .gitignore: patterns pra credenciais.
- Migrations de scenarios idempotentes.
- i18n completa pt_BR+en pra roleta/funnel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:36:25 -03:00
Rodribm10
6ecafd30c6 feat(captain-memory): redesign Contact Memories UI with type badges + relative time + fix i18n keys 2026-04-19 07:38:50 -03:00
Rodribm10
b07486c430 feat(captain-memory): wire Contact Memories section into conversation sidebar 2026-04-19 07:30:30 -03:00
Rodribm10
2f7d8edd92 feat(captain-memory): add Contact Memory UI component + API client + i18n
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 01:47:56 -03:00
Rodribm10
2bf68e5be8 feat(captain-memory): add feature flag helpers on Account 2026-04-18 22:10:10 -03:00
Rodribm10
1c21b8d815 fix: guard landing host sync when inbox has no portal
Inboxes without portal_id were crashing with NoMethodError on save,
blocking landing host creation via UI for any inbox without a portal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 22:49:35 -03:00
Rodribm10
2e9551a0f3 feat(lifecycle): rules tab with templates, wizard and variable autocomplete
Implements Task 15: Rules.vue (template grid + rules table), RuleWizardDialog.vue
(4-step wizard: Quando/Pra quem/O quê/Revisão) and MessageEditor.vue (textarea with
{{ variable }} autocomplete). Adds WIZARD.CANCEL, OFFSET_UNIT_LABEL, STEP_LABELS and
REVIEW i18n keys in en and pt_BR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 11:15:00 -03:00
Rodribm10
94fdb5c318 feat(lifecycle): settings tab with guards form and concierge per unit
Replaces stub Settings.vue with full implementation: anti-spam guard
form (quiet hours, interval, pause-on-reply, opt-out label) and a
collapsible ConciergeUnitCard per unit (inbox selector, persona name,
knowledge base, key-value variables). Adds CONCIERGE_CONFIGURED /
CONCIERGE_NOT_CONFIGURED i18n keys to en + pt_BR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 11:05:40 -03:00
Rodribm10
ae4647d1c2 feat(lifecycle): history tab with paginated list and preview modal
Implements Task 13 — replaces the stub History.vue with a real paginated
table filtered by status, and adds DeliveryPreviewModal to show rendered_body.
Also extends i18n keys (TOTAL, PAGINATION, MODAL labels) in en + pt_BR.
2026-04-15 10:57:56 -03:00
Rodribm10
ad2255aba4 feat(lifecycle): sidebar entry for Jornada do Cliente 2026-04-15 10:53:33 -03:00
Rodribm10
65a76ed59d feat(lifecycle): parent view with TabBar + 3 stub children routes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 10:51:09 -03:00
Rodribm10
bc85ec0a67 feat(lifecycle): Pinia/Vuex stores for rules/config/deliveries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 10:46:20 -03:00
Rodribm10
b69053ae62 feat(lifecycle): API clients for rules/config/deliveries + concierge update
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 10:42:51 -03:00
Rodribm10
1459655243 feat(lifecycle): i18n keys for Jornada do Cliente UI
Adds CAPTAIN_LIFECYCLE block (en + pt_BR) to captain.json with full
key set for Rules, Wizard, Settings, History and sidebar entry.

Also stages pre-existing uncommitted additions to captain.json from
prior work (KPI, PILLS, QUICK_DATE, CARD, ACTIONS extras for
CAPTAIN_RESERVATIONS) — those were already in the working tree and
belong to the same feature branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 10:39:50 -03:00
Rodribm10
fa1dd8b6cb feat(lifecycle): expose concierge config update on UnitsController
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 10:35:03 -03:00
Rodribm10
cb67a1063d fix(lifecycle): move stub controllers from non-enterprise to enterprise path
Os stubs de lifecycle criados na task anterior estavam em app/controllers/
causando futura colisão de redefinição de classe quando os controllers reais
forem implementados em enterprise/app/controllers/ (tasks 4-6). Move os 3
stubs para o enterprise path onde vivem todos os controllers Captain.

Routing spec: 7 examples, 0 failures

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 10:13:33 -03:00
Rodribm10
fbc91e2fa8 feat(lifecycle): add REST routes for rules, config, deliveries, concierge
Wires 3 new captain namespace resources (lifecycle_rules, lifecycle_config,
lifecycle_deliveries) and a member action `patch :concierge` on units.
Includes stub controllers (to be expanded in Tasks 4-7) and passing routing spec.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 10:11:39 -03:00
Rodribm10
b29b35465b feat(lifecycle): add Account associations for lifecycle models 2026-04-15 10:03:01 -03:00
Rodribm10
23a17599c4 feat(wuzapi): dispatch interactive messages (buttons/list/url_button)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 01:30:31 -03:00
Rodribm10
7a203ccb6d feat(wuzapi): add send_buttons, send_list, send_url_button methods
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 01:28:00 -03:00
Rodribm10
a892e65300 fix(ui): dropdown Vincular Unidade Pix mostra unit vinculada em cascata
Existem 3 mecanismos de vinculo inbox-unit no fork:
  1. captain_inboxes.captain_unit_id (fluxo Captain)
  2. captain_unit_inboxes (tabela join pura, usada pela UI nova)
  3. captain_units.inbox_id (legado direto)

O serializer lia so o #1 e retornava null nas outras, fazendo a UI
mostrar 'Nenhuma unidade vinculada' mesmo quando havia vinculo via
#2 ou #3. Agora o jbuilder cai em cascata nas 3 fontes.
2026-04-14 20:07:33 -03:00
Rodribm10
996704350b feat: Captain::Reservation callback cria nota interna automaticamente
Cobre ambos os caminhos (generate_pix_tool e PublicReservationsController):
toda reserva criada recebe um after_create_commit que posta uma mensagem
privada na conversa com os detalhes (suite, check-in, valores, ID).

Remove a criacao duplicada do PublicReservationsController.
2026-04-14 19:53:21 -03:00
Rodribm10
7c9411a0b0 feat: persiste metadados do cliente em custom_attributes + tool blindada
- Controller grava cpf/ultima_suite/ultima_permanencia/ultima_reserva_em/total_reservas
  em contact.custom_attributes (aparece no painel lateral do Chatwoot)
- GenerateReservationLinkTool exige marca/unidade/categoria/permanencia/checkin_at;
  retorna erro se Jasmine chamar sem esses dados
2026-04-14 13:26:02 -03:00
Rodribm10
6e1b80002e feat: adiciona label aguardando_pagamento ao criar reserva (fecha fase 2+3) 2026-04-14 10:24:37 -03:00
Rodribm10
9dabaaa505 fix: usa phone digits como source_id (whatsapp inbox exige E.164 sem +)
Smoke test revelou que o inbox do tipo whatsapp valida source_id com
regex ^\d{1,15}\z. Trocar UUID por telefone em digitos (phone_digits)
e normalizar phone_number pra +phone_digits antes de criar o contato.
2026-04-14 10:01:50 -03:00
Rodribm10
5ff3a70474 feat: implementa POST create (contact + conversa + reserva + pix) e GET status 2026-04-13 23:50:26 -03:00
Rodribm10
e9a5e734ff feat: rota + controller esqueleto PublicReservations com token auth 2026-04-13 23:49:11 -03:00
Rodrigo Borba
cdc5149866 Automate landing promotion sync to captain docs/faqs with cascade cleanup 2026-03-04 19:30:05 -03:00