From 44908f32d10866b04735099404f59812007b7f55 Mon Sep 17 00:00:00 2001 From: Rodrigo Borba Date: Sun, 1 Mar 2026 21:53:11 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20sistema=20de=20notifica=C3=A7=C3=B5es?= =?UTF-8?q?=20de=20reserva=20com=20templates=20configur=C3=A1veis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adiciona check_in_at/duration_hours ao schema do tool CreateReservationIntent para que a IA capture o horário EXATO de chegada informado pelo cliente - Cria captain_notification_templates: label, content, timing_minutes, timing_direction (before/after), active, position - Implementa SendNotificationService com interpolação de variáveis (guest_name, check_in_time, check_out_time, suite_name, unit_name) - Implementa NotificationScannerJob (Sidekiq-cron a cada 5min) com janela de tolerância de ±5min e idempotência via metadata JSONB - API REST: /captain/units/:unit_id/notification_templates (CRUD) - Store Vuex captainNotificationTemplates + API client - UI: página de gestão de templates com editor inline e botão '+' - Configura rota captain_settings_notifications - i18n PT/EN para todas as strings novas - Rubocop e ESLint: zero offenses --- .../api/captain/notificationTemplates.js | 30 + .../dashboard/i18n/locale/en/captain.json | 86 +- .../dashboard/i18n/locale/pt_BR/captain.json | 87 +- .../settings/captain/captain.routes.js | 9 + .../settings/captain/notifications/Index.vue | 361 +++++ .../settings/captain/reports/Index.vue | 1215 ++++++++++++++++- .../store/captain/notificationTemplates.js | 104 ++ app/javascript/dashboard/store/index.js | 2 + config/agents/tools.yml | 5 + config/routes.rb | 4 +- config/schedule.yml | 7 + ...0_create_captain_notification_templates.rb | 18 + db/schema.rb | 17 +- .../notification_templates_controller.rb | 52 + .../notifications/notification_scanner_job.rb | 40 + .../models/captain/notification_template.rb | 41 + enterprise/app/models/captain/unit.rb | 2 + .../send_notification_service.rb | 54 + .../tools/create_reservation_intent_tool.rb | 351 +++++ 19 files changed, 2406 insertions(+), 79 deletions(-) create mode 100644 app/javascript/dashboard/api/captain/notificationTemplates.js create mode 100644 app/javascript/dashboard/routes/dashboard/settings/captain/notifications/Index.vue create mode 100644 app/javascript/dashboard/store/captain/notificationTemplates.js create mode 100644 db/migrate/20260301120000_create_captain_notification_templates.rb create mode 100644 enterprise/app/controllers/api/v1/accounts/captain/notification_templates_controller.rb create mode 100644 enterprise/app/jobs/captain/notifications/notification_scanner_job.rb create mode 100644 enterprise/app/models/captain/notification_template.rb create mode 100644 enterprise/app/services/captain/notifications/send_notification_service.rb create mode 100644 enterprise/app/services/captain/tools/create_reservation_intent_tool.rb diff --git a/app/javascript/dashboard/api/captain/notificationTemplates.js b/app/javascript/dashboard/api/captain/notificationTemplates.js new file mode 100644 index 000000000..2a5fbac48 --- /dev/null +++ b/app/javascript/dashboard/api/captain/notificationTemplates.js @@ -0,0 +1,30 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class CaptainNotificationTemplatesAPI extends ApiClient { + constructor() { + super('captain/units', { accountScoped: true }); + } + + getTemplates(unitId) { + return axios.get(`${this.url}/${unitId}/notification_templates`); + } + + createTemplate(unitId, data) { + return axios.post(`${this.url}/${unitId}/notification_templates`, { + notification_template: data, + }); + } + + updateTemplate(unitId, id, data) { + return axios.patch(`${this.url}/${unitId}/notification_templates/${id}`, { + notification_template: data, + }); + } + + deleteTemplate(unitId, id) { + return axios.delete(`${this.url}/${unitId}/notification_templates/${id}`); + } +} + +export default new CaptainNotificationTemplatesAPI(); diff --git a/app/javascript/dashboard/i18n/locale/en/captain.json b/app/javascript/dashboard/i18n/locale/en/captain.json index 43a71e37a..c70a4c2d8 100644 --- a/app/javascript/dashboard/i18n/locale/en/captain.json +++ b/app/javascript/dashboard/i18n/locale/en/captain.json @@ -352,6 +352,7 @@ "UNITS_GROUP": "Pix Units", "INBOXES_GROUP": "Inboxes", "TABS": { + "DASHBOARD": "Dashboard", "INSIGHTS": "AI Insights", "OPERATIONAL": "Operational" }, @@ -372,7 +373,22 @@ "AI_FAILURES": "AI Failures", "BULLET": "•", "COUNT_PREFIX": "(", - "COUNT_SUFFIX": ")" + "COUNT_SUFFIX": ")", + "TIMES": "x", + "SENTIMENT": "Sentiment", + "SENTIMENT_POSITIVE": "Positive", + "SENTIMENT_NEGATIVE": "Negative", + "SENTIMENT_NEUTRAL": "Neutral", + "PRAISES": "Customer Praises", + "COMPLAINTS": "Complaints", + "FAQ_GAPS": "FAQ Gaps", + "FAQ_GAPS_HINT": "Questions customers ask that the agent doesn't cover", + "MOST_REQUESTED_SUITES": "Most Requested Suites", + "PRICE_REACTIONS": "Price Reactions", + "PRICE_OBJECTIONS": "price objections", + "RECOMMENDATIONS": "Recommendations", + "SHOW_DETAILS": "View full analysis", + "HIDE_DETAILS": "Hide details" }, "EMPTY": { "TITLE": "No reports generated", @@ -393,6 +409,74 @@ "OPERATIONAL": { "COMING_SOON": "Coming soon", "COMING_SOON_DESC": "Real-time operational data (reservations, Pix charges, etc.) will be available here soon." + }, + "DASHBOARD": { + "TOTAL_CONVERSATIONS": "Analyzed conversations", + "AVG_SENTIMENT": "Avg. positive sentiment", + "FAQ_GAPS_TOTAL": "FAQ gaps identified", + "WEEKS_ANALYZED": "weeks analyzed", + "NO_DATA": "Not enough data. Generate more AI reports to see the dashboard.", + "SENTIMENT_TREND": "Sentiment trend by week", + "FAILURES_RANKING": "Agent failure ranking", + "FAILURES_RANKING_HINT": "Most frequent situations where the AI couldn't respond well", + "FAQ_PRIORITY": "Priority FAQs to create", + "FAQ_PRIORITY_HINT": "Questions customers ask most that aren't in the FAQ yet", + "CUSTOMER_BEHAVIOR": "Customer behavior", + "TOP_TOPICS_TITLE": "Most discussed topics", + "SUITES_TITLE": "Most requested suites", + "COMPLAINTS_TREND": "Complaints volume by week", + "HANDOFFS_TITLE": "Estimated handoffs to human", + "HANDOFFS_HINT": "Based on AI failure frequency. Direct handoff tracking coming soon.", + "TREND_UP": "rising", + "TREND_DOWN": "falling", + "TREND_STABLE": "stable", + "WEEKS": "weeks" + }, + "FAQ_QUICK_ADD": { + "BUTTON": "Create FAQ", + "TITLE": "Create FAQ from AI suggestion", + "QUESTION_LABEL": "Question (suggested by AI)", + "ANSWER_LABEL": "Answer", + "ANSWER_PLACEHOLDER": "Write the answer to this question...", + "ASSISTANT_LABEL": "AI Agent", + "ASSISTANT_PLACEHOLDER": "Select the agent", + "CANCEL": "Cancel", + "SAVE": "Save FAQ", + "SUCCESS": "FAQ created successfully!", + "ERROR": "Error creating FAQ. Please try again." + }, + "NOTIFICATIONS": { + "TITLE": "Automatic Notifications", + "DESCRIPTION": "Configure messages sent automatically before or after the guest's arrival.", + "LOADING": "Loading notifications...", + "ADD": "Add notification", + "ACTIVE": "Active", + "INACTIVE": "Inactive", + "DIRECTION": { + "BEFORE": "before", + "AFTER": "after", + "OF_ARRIVAL": "arrival" + }, + "FORM": { + "LABEL_PLACEHOLDER": "Template name (e.g. Arrival Instructions)", + "CONTENT_PLACEHOLDER": "Message to send... Use {{guest_name}}, {{check_in_time}}, {{suite_name}}", + "SEND": "Send", + "MINUTES": "min", + "CANCEL": "Cancel", + "SAVE": "Save" + }, + "CREATE": { + "SUCCESS": "Notification created successfully!", + "ERROR": "Error creating notification. Please try again." + }, + "UPDATE": { + "SUCCESS": "Notification updated!", + "ERROR": "Error updating notification." + }, + "DELETE": { + "SUCCESS": "Notification removed.", + "ERROR": "Error removing notification." + } } } } diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/captain.json b/app/javascript/dashboard/i18n/locale/pt_BR/captain.json index f2d637a83..4d0f2c60b 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/captain.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/captain.json @@ -352,6 +352,7 @@ "UNITS_GROUP": "Unidades Pix", "INBOXES_GROUP": "Caixas de Entrada", "TABS": { + "DASHBOARD": "Dashboard", "INSIGHTS": "Insights IA", "OPERATIONAL": "Operacional" }, @@ -372,7 +373,22 @@ "AI_FAILURES": "Falhas da IA", "BULLET": "•", "COUNT_PREFIX": "(", - "COUNT_SUFFIX": ")" + "COUNT_SUFFIX": ")", + "TIMES": "x", + "SENTIMENT": "Sentimento", + "SENTIMENT_POSITIVE": "Positivo", + "SENTIMENT_NEGATIVE": "Negativo", + "SENTIMENT_NEUTRAL": "Neutro", + "PRAISES": "Elogios dos clientes", + "COMPLAINTS": "Reclamações", + "FAQ_GAPS": "Lacunas no FAQ", + "FAQ_GAPS_HINT": "Perguntas que os clientes fazem mas o agente não cobre", + "MOST_REQUESTED_SUITES": "Suítes mais pedidas", + "PRICE_REACTIONS": "Reação a preços", + "PRICE_OBJECTIONS": "objeções de preço", + "RECOMMENDATIONS": "Recomendações", + "SHOW_DETAILS": "Ver análise completa", + "HIDE_DETAILS": "Ocultar detalhes" }, "EMPTY": { "TITLE": "Nenhum relatório gerado", @@ -393,6 +409,75 @@ "OPERATIONAL": { "COMING_SOON": "Em breve", "COMING_SOON_DESC": "Os dados operacionais em tempo real (reservas, cobranças Pix, etc.) estarão disponíveis aqui em breve." + }, + "DASHBOARD": { + "TOTAL_CONVERSATIONS": "Conversas analisadas", + "AVG_SENTIMENT": "Sentimento positivo médio", + "FAQ_GAPS_TOTAL": "Lacunas de FAQ identificadas", + "WEEKS_ANALYZED": "semanas analisadas", + "NO_DATA": "Dados insuficientes. Gere mais relatórios de IA para ver o dashboard.", + "SENTIMENT_TREND": "Tendência de sentimento por semana", + "FAILURES_RANKING": "Ranking de falhas do agente", + "FAILURES_RANKING_HINT": "Situações mais frequentes em que a IA não conseguiu responder bem", + "FAQ_PRIORITY": "FAQ prioritário para criar", + "FAQ_PRIORITY_HINT": "Perguntas que os clientes mais fazem e ainda não estão no FAQ", + "CUSTOMER_BEHAVIOR": "Comportamento dos clientes", + "TOP_TOPICS_TITLE": "Assuntos mais discutidos", + "SUITES_TITLE": "Suítes mais solicitadas", + "COMPLAINTS_TREND": "Volume de reclamações por semana", + "HANDOFFS_TITLE": "Estimativa de transferências para humano", + "HANDOFFS_HINT": "Baseado na frequência de falhas do agente. Rastreamento direto de transferências em breve.", + "TREND_UP": "em alta", + "TREND_DOWN": "em queda", + "TREND_STABLE": "estável", + "WEEKS": "semanas" + }, + "FAQ_QUICK_ADD": { + "BUTTON": "Criar FAQ", + "TITLE": "Criar FAQ a partir da sugestão da IA", + "QUESTION_LABEL": "Pergunta (sugerida pela IA)", + "ANSWER_LABEL": "Resposta", + "ANSWER_PLACEHOLDER": "Escreva a resposta para esta pergunta...", + "ASSISTANT_LABEL": "Agente de IA", + "ASSISTANT_PLACEHOLDER": "Selecione o agente", + "CANCEL": "Cancelar", + "SAVE": "Salvar FAQ", + "SUCCESS": "FAQ criado com sucesso!", + "ERROR": "Erro ao criar FAQ. Tente novamente." + }, + "NOTIFICATIONS": { + "TITLE": "Notificações Automáticas", + "DESCRIPTION": "Configure mensagens automáticas enviadas antes ou depois da chegada do hóspede.", + "LOADING": "Carregando notificações...", + "ADD": "Adicionar notificação", + "ACTIVE": "Ativo", + "INACTIVE": "Inativo", + "DIRECTION": { + "BEFORE": "antes", + "AFTER": "depois", + "OF_ARRIVAL": "da chegada" + }, + "TIMING_LABEL": "da chegada", + "FORM": { + "LABEL_PLACEHOLDER": "Nome do template (ex: Orientações de Chegada)", + "CONTENT_PLACEHOLDER": "Mensagem a enviar... Use {{guest_name}}, {{check_in_time}}, {{suite_name}}", + "SEND": "Enviar", + "MINUTES": "min", + "CANCEL": "Cancelar", + "SAVE": "Salvar" + }, + "CREATE": { + "SUCCESS": "Notificação criada com sucesso!", + "ERROR": "Erro ao criar notificação. Tente novamente." + }, + "UPDATE": { + "SUCCESS": "Notificação atualizada!", + "ERROR": "Erro ao atualizar notificação." + }, + "DELETE": { + "SUCCESS": "Notificação removida.", + "ERROR": "Erro ao remover notificação." + } } } } diff --git a/app/javascript/dashboard/routes/dashboard/settings/captain/captain.routes.js b/app/javascript/dashboard/routes/dashboard/settings/captain/captain.routes.js index c99b08b3c..5a89b37bd 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/captain/captain.routes.js +++ b/app/javascript/dashboard/routes/dashboard/settings/captain/captain.routes.js @@ -8,6 +8,7 @@ import UnitEdit from './units/Edit.vue'; import GalleryIndex from './gallery/Index.vue'; import GalleryEdit from './gallery/Edit.vue'; const ReportsIndex = () => import('./reports/Index.vue'); +const NotificationsIndex = () => import('./notifications/Index.vue'); export default { routes: [ @@ -77,6 +78,14 @@ export default { permissions: ['administrator'], }, }, + { + path: 'units/:unitId/notifications', + name: 'captain_settings_notifications', + component: NotificationsIndex, + meta: { + permissions: ['administrator'], + }, + }, ], }, ], diff --git a/app/javascript/dashboard/routes/dashboard/settings/captain/notifications/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/captain/notifications/Index.vue new file mode 100644 index 000000000..7bf2184a9 --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/captain/notifications/Index.vue @@ -0,0 +1,361 @@ + + +