From 3f1786c9bfeaa1f1d813bc0c60745433257e0605 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 25 Mar 2026 08:39:45 -0300 Subject: [PATCH] =?UTF-8?q?+=20Menu=20Hover=20no=20Layout=20Rail,=20Twilio?= =?UTF-8?q?,=20Sms,=20Email,=20Templates,=20LNovo=20Layout=20Configura?= =?UTF-8?q?=C3=A7=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../001_twilio_whatsapp_subaccount.sql | 132 +++ src/assets/styles.scss | 44 +- .../agenda/AgendaQuickAddDialog.vue | 2 +- src/components/patients/PatientActionMenu.vue | 6 +- src/components/ui/PatientCreatePopover.vue | 4 +- .../agenda/pages/AgendaTerapeutaPage.vue | 58 +- .../pages/AgendamentosRecebidosPage.vue | 24 +- .../agenda/pages/CompromissosDeterminados.vue | 28 +- .../pages/FinanceiroDashboardPage.vue | 22 +- .../financeiro/pages/FinanceiroPage.vue | 30 +- src/features/patients/PatientsListPage.vue | 18 +- .../cadastro/PatientsCadastroPage.vue | 26 +- .../cadastro/PatientsExternalLinkPage.vue | 28 +- .../recebidos/CadastrosRecebidosPage.vue | 10 +- .../patients/grupos/GruposPacientesPage.vue | 26 +- .../patients/prontuario/PatientProntuario.vue | 6 +- src/features/patients/tags/TagsPage.vue | 30 +- src/layout/AppConfigurator.vue | 37 +- src/layout/AppLayout.vue | 1 + src/layout/AppMenuFooterPanel.vue | 26 +- src/layout/AppMenuItem.vue | 6 +- src/layout/AppRail.vue | 40 +- src/layout/AppRailPanel.vue | 33 +- src/layout/AppThemeBar.vue | 32 +- src/layout/ConfiguracoesPage.vue | 247 +++-- src/layout/composables/layout.js | 46 +- src/layout/configuracoes/BloqueiosPage.vue | 62 -- .../configuracoes/ConfiguracoesAgendaPage.vue | 9 - .../ConfiguracoesAgendadorPage.vue | 65 -- .../ConfiguracoesConveniosPage.vue | 78 -- .../ConfiguracoesDescontosPage.vue | 78 -- .../ConfiguracoesEmailTemplatesPage.vue | 67 -- .../ConfiguracoesExcecoesFinanceirasPage.vue | 65 -- .../ConfiguracoesMinhaEmpresaPage.vue | 69 -- .../ConfiguracoesPagamentoPage.vue | 69 -- .../ConfiguracoesPrecificacaoPage.vue | 78 -- .../ConfiguracoesTwilioWhatsappPage.vue | 460 ++++++++++ .../ConfiguracoesWhatsappPage.vue | 66 -- src/navigation/menus/saas.menu.js | 3 +- src/router/routes.configs.js | 5 + src/router/routes.saas.js | 6 + src/services/twilioWhatsappService.js | 181 ++++ src/stores/twilioWhatsappStore.js | 308 +++++++ src/views/pages/account/ProfilePage.vue | 2 +- src/views/pages/auth/Login.vue | 6 +- src/views/pages/auth/ResetPasswordPage.vue | 8 +- src/views/pages/auth/SecurityPage.vue | 10 +- .../pages/billing/ClinicMeuPlanoPage.vue | 20 +- .../pages/billing/TherapistMeuPlanoPage.vue | 16 +- .../pages/billing/TherapistUpgradePage.vue | 12 +- src/views/pages/billing/UpgradePage.vue | 22 +- .../notifications/SmsChannelSetupPage.vue | 2 +- src/views/pages/saas/SaasDocsPage.vue | 4 +- src/views/pages/saas/SaasFaqPage.vue | 8 +- src/views/pages/saas/SaasLoginCarousel.vue | 8 +- src/views/pages/saas/SaasSupportPage.vue | 4 +- .../pages/saas/SaasTwilioWhatsappPage.vue | 864 ++++++++++++++++++ src/views/pages/therapist/RelatoriosPage.vue | 10 +- .../pages/therapist/TherapistDashboard.vue | 32 +- 59 files changed, 2553 insertions(+), 1106 deletions(-) create mode 100644 database-novo/migrations/001_twilio_whatsapp_subaccount.sql create mode 100644 src/layout/configuracoes/ConfiguracoesTwilioWhatsappPage.vue create mode 100644 src/services/twilioWhatsappService.js create mode 100644 src/stores/twilioWhatsappStore.js create mode 100644 src/views/pages/saas/SaasTwilioWhatsappPage.vue diff --git a/database-novo/migrations/001_twilio_whatsapp_subaccount.sql b/database-novo/migrations/001_twilio_whatsapp_subaccount.sql new file mode 100644 index 0000000..4dc44a3 --- /dev/null +++ b/database-novo/migrations/001_twilio_whatsapp_subaccount.sql @@ -0,0 +1,132 @@ +-- ============================================================================= +-- AgenciaPsi — Migration 001: Twilio WhatsApp Subaccounts +-- ============================================================================= +-- Adiciona suporte a subcontas Twilio com número WhatsApp dedicado por tenant. +-- Cada clínica/terapeuta recebe sua própria subconta Twilio com número exclusivo. +-- ============================================================================= + +-- ── 1. Campos de subconta Twilio em notification_channels ────────────────── + +ALTER TABLE public.notification_channels + ADD COLUMN IF NOT EXISTS twilio_subaccount_sid text, + ADD COLUMN IF NOT EXISTS twilio_phone_number text, + ADD COLUMN IF NOT EXISTS twilio_phone_sid text, + ADD COLUMN IF NOT EXISTS webhook_url text, + ADD COLUMN IF NOT EXISTS cost_per_message_usd numeric(8,6) DEFAULT 0, + ADD COLUMN IF NOT EXISTS price_per_message_brl numeric(8,4) DEFAULT 0, + ADD COLUMN IF NOT EXISTS provisioned_at timestamp with time zone; + +COMMENT ON COLUMN public.notification_channels.twilio_subaccount_sid IS 'SID da subconta Twilio criada para este tenant'; +COMMENT ON COLUMN public.notification_channels.twilio_phone_number IS 'Número WhatsApp provisionado (E.164, ex: +5511999990000)'; +COMMENT ON COLUMN public.notification_channels.twilio_phone_sid IS 'SID do número de telefone na subconta Twilio'; +COMMENT ON COLUMN public.notification_channels.webhook_url IS 'URL do webhook configurada na Twilio para receber callbacks de status'; +COMMENT ON COLUMN public.notification_channels.cost_per_message_usd IS 'Custo real Twilio por mensagem WhatsApp (USD)'; +COMMENT ON COLUMN public.notification_channels.price_per_message_brl IS 'Valor cobrado do tenant por mensagem (BRL, inclui margem SaaS)'; +COMMENT ON COLUMN public.notification_channels.provisioned_at IS 'Timestamp do provisionamento da subconta'; + +-- Índice para busca rápida por subconta +CREATE INDEX IF NOT EXISTS idx_notification_channels_twilio_subaccount_sid + ON public.notification_channels (twilio_subaccount_sid) + WHERE twilio_subaccount_sid IS NOT NULL; + +-- ── 2. Tabela de consumo por subconta ───────────────────────────────────── + +CREATE TABLE IF NOT EXISTS public.twilio_subaccount_usage ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + tenant_id uuid NOT NULL, + channel_id uuid NOT NULL, + twilio_subaccount_sid text NOT NULL, + period_start date NOT NULL, + period_end date NOT NULL, + messages_sent integer DEFAULT 0 NOT NULL, + messages_delivered integer DEFAULT 0 NOT NULL, + messages_failed integer DEFAULT 0 NOT NULL, + cost_usd numeric(12,6) DEFAULT 0 NOT NULL, + cost_brl numeric(12,4) DEFAULT 0 NOT NULL, + revenue_brl numeric(12,4) DEFAULT 0 NOT NULL, + margin_brl numeric(12,4) GENERATED ALWAYS AS (revenue_brl - cost_brl) STORED, + usd_brl_rate numeric(8,4) DEFAULT 0, + synced_at timestamp with time zone DEFAULT now(), + created_at timestamp with time zone DEFAULT now() NOT NULL, + + CONSTRAINT twilio_subaccount_usage_pkey PRIMARY KEY (id), + CONSTRAINT twilio_subaccount_usage_channel_fk + FOREIGN KEY (channel_id) REFERENCES public.notification_channels(id) ON DELETE CASCADE, + CONSTRAINT twilio_subaccount_usage_period_check + CHECK (period_end >= period_start) +); + +COMMENT ON TABLE public.twilio_subaccount_usage IS + 'Consumo mensal de mensagens WhatsApp por subconta Twilio. Sincronizado via Edge Function.'; + +CREATE INDEX IF NOT EXISTS idx_twilio_usage_tenant_period + ON public.twilio_subaccount_usage (tenant_id, period_start DESC); + +CREATE INDEX IF NOT EXISTS idx_twilio_usage_channel + ON public.twilio_subaccount_usage (channel_id, period_start DESC); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_twilio_usage_unique_period + ON public.twilio_subaccount_usage (channel_id, period_start, period_end); + +ALTER TABLE public.twilio_subaccount_usage OWNER TO supabase_admin; + +-- ── 3. RLS: twilio_subaccount_usage ─────────────────────────────────────── + +ALTER TABLE public.twilio_subaccount_usage ENABLE ROW LEVEL SECURITY; + +-- Tenant vê apenas seu próprio consumo +CREATE POLICY "tenant_select_own_usage" + ON public.twilio_subaccount_usage + FOR SELECT + USING ( + tenant_id IN ( + SELECT tenant_id FROM public.tenant_members + WHERE user_id = auth.uid() + ) + ); + +-- Apenas service_role pode inserir/atualizar (via Edge Function) +CREATE POLICY "service_role_manage_usage" + ON public.twilio_subaccount_usage + FOR ALL + USING (auth.role() = 'service_role'); + +-- ── 4. RLS: notification_channels — acesso ao twilio_subaccount_sid ─────── +-- As políticas existentes já cobrem SELECT/UPDATE. Nenhuma alteração necessária. + +-- ── 5. View: resumo de subcontas para o painel SaaS admin ───────────────── + +CREATE OR REPLACE VIEW public.v_twilio_whatsapp_overview AS +SELECT + nc.id AS channel_id, + nc.tenant_id, + nc.owner_id, + nc.is_active, + nc.connection_status, + nc.display_name, + nc.twilio_subaccount_sid, + nc.twilio_phone_number, + nc.twilio_phone_sid, + nc.cost_per_message_usd, + nc.price_per_message_brl, + nc.provisioned_at, + nc.created_at, + nc.updated_at, + -- Uso do mês atual + COALESCE(u.messages_sent, 0) AS current_month_sent, + COALESCE(u.messages_delivered, 0) AS current_month_delivered, + COALESCE(u.messages_failed, 0) AS current_month_failed, + COALESCE(u.cost_usd, 0) AS current_month_cost_usd, + COALESCE(u.cost_brl, 0) AS current_month_cost_brl, + COALESCE(u.revenue_brl, 0) AS current_month_revenue_brl, + COALESCE(u.margin_brl, 0) AS current_month_margin_brl +FROM public.notification_channels nc +LEFT JOIN public.twilio_subaccount_usage u + ON u.channel_id = nc.id + AND u.period_start = date_trunc('month', CURRENT_DATE)::date +WHERE nc.channel = 'whatsapp' + AND nc.provider = 'twilio' + AND nc.deleted_at IS NULL; + +COMMENT ON VIEW public.v_twilio_whatsapp_overview IS + 'Visão consolidada de subcontas Twilio WhatsApp com uso do mês corrente.'; diff --git a/src/assets/styles.scss b/src/assets/styles.scss index f866186..f28a4d5 100644 --- a/src/assets/styles.scss +++ b/src/assets/styles.scss @@ -139,49 +139,7 @@ opacity: 0; transform: translateY(10px); } -/* ── Subheader de seção ──────────────────────────────── */ -.cfg-subheader { - display: flex; - align-items: center; - gap: 0.65rem; - padding: 0.875rem 1rem; - border-radius: 6px; - border: 1px solid color-mix(in srgb, var(--primary-color, #6366f1) 30%, transparent); - background: linear-gradient(135deg, color-mix(in srgb, var(--primary-color, #6366f1) 12%, var(--surface-card)) 0%, color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card)) 60%, var(--surface-card) 100%); - overflow: hidden; - position: relative; -} -/* Brilho sutil no canto */ -.cfg-subheader::before { - content: ''; - position: absolute; - top: -20px; - right: -20px; - width: 80px; - height: 80px; - border-radius: 50%; - background: color-mix(in srgb, var(--primary-color, #6366f1) 15%, transparent); - filter: blur(20px); - pointer-events: none; -} -.cfg-subheader__icon { - display: grid; - place-items: center; - flex-shrink: 0; - background: color-mix(in srgb, var(--primary-color, #6366f1) 20%, transparent); - color: var(--primary-color, #6366f1); -} -.cfg-subheader__title { - font-size: 0.95rem; - font-weight: 700; - color: var(--primary-color, #6366f1); - letter-spacing: -0.01em; -} -.cfg-subheader__sub { - font-size: 0.75rem; - color: var(--text-color-secondary); - opacity: 0.85; -} + .cfg-card__icon-wrap { border: 1px solid var(--surface-border); background: var(--surface-ground); diff --git a/src/components/agenda/AgendaQuickAddDialog.vue b/src/components/agenda/AgendaQuickAddDialog.vue index 3bc1cf7..2dc7a94 100644 --- a/src/components/agenda/AgendaQuickAddDialog.vue +++ b/src/components/agenda/AgendaQuickAddDialog.vue @@ -204,7 +204,7 @@ function skipProcedures() {