Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions
@@ -0,0 +1,279 @@
<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/layout/configuracoes/ConfiguracoesCanaisPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
const router = useRouter();
const toast = useToast();
const tenantStore = useTenantStore();
const userId = ref(null);
const tenantId = ref(null);
const loading = ref(true);
// ── WhatsApp ──────────────────────────────────────────────────
const whatsapp = ref({
configured: false,
status: null, // 'open' | 'close' | 'connecting' | null
checking: false,
credentials: null
});
// ── SMS ───────────────────────────────────────────────────────
const sms = ref({
hasCredits: false,
balance: 0,
isActive: false
});
// ── Email ─────────────────────────────────────────────────────
const email = ref({
templatesCount: 0
});
// ── Computed status cards ─────────────────────────────────────
const channels = computed(() => [
{
key: 'whatsapp',
label: 'WhatsApp',
icon: 'pi pi-whatsapp',
description: 'Envie lembretes e confirmações via WhatsApp automaticamente.',
route: '/configuracoes/whatsapp',
tag: whatsappTag.value,
details: whatsappDetails.value
},
{
key: 'sms',
label: 'SMS',
icon: 'pi pi-comment',
description: 'Envie SMS para pacientes com créditos pré-pagos.',
route: '/configuracoes/sms',
tag: smsTag.value,
details: smsDetails.value
},
{
key: 'email',
label: 'E-mail',
icon: 'pi pi-envelope',
description: 'Personalize os e-mails enviados automaticamente aos pacientes.',
route: '/configuracoes/email-templates',
tag: emailTag.value,
details: emailDetails.value
}
]);
const whatsappTag = computed(() => {
if (whatsapp.value.checking) return { label: 'Verificando...', severity: 'secondary', icon: 'pi pi-spin pi-spinner' };
if (!whatsapp.value.configured) return { label: 'Não configurado', severity: 'secondary', icon: 'pi pi-info-circle' };
switch (whatsapp.value.status) {
case 'open':
return { label: 'Conectado', severity: 'success', icon: 'pi pi-check-circle' };
case 'connecting':
return { label: 'Conectando...', severity: 'warn', icon: 'pi pi-spin pi-spinner' };
default:
return { label: 'Desconectado', severity: 'danger', icon: 'pi pi-times-circle' };
}
});
const whatsappDetails = computed(() => {
if (!whatsapp.value.configured) return 'Configure as credenciais da Evolution API para conectar.';
if (whatsapp.value.status === 'open') return 'Canal ativo e enviando mensagens.';
return 'Canal configurado mas desconectado. Reconecte pelo QR Code.';
});
const smsTag = computed(() => {
if (!sms.value.hasCredits) return { label: 'Sem créditos', severity: 'secondary', icon: 'pi pi-info-circle' };
if (sms.value.balance <= 0) return { label: 'Sem saldo', severity: 'danger', icon: 'pi pi-times-circle' };
if (sms.value.balance <= 10) return { label: `${sms.value.balance} créditos`, severity: 'warn', icon: 'pi pi-exclamation-triangle' };
return { label: `${sms.value.balance} créditos`, severity: 'success', icon: 'pi pi-check-circle' };
});
const smsDetails = computed(() => {
if (!sms.value.hasCredits) return 'Adquira créditos SMS em Recursos Extras para ativar o canal.';
if (sms.value.balance <= 0) return 'Saldo zerado. Os envios de SMS estão pausados.';
return `Saldo de ${sms.value.balance} créditos disponíveis para envio.`;
});
const emailTag = computed(() => {
return { label: 'Ativo', severity: 'success', icon: 'pi pi-check-circle' };
});
const emailDetails = computed(() => {
if (email.value.templatesCount > 0) {
return `${email.value.templatesCount} template(s) personalizados.`;
}
return 'Usando templates padrão. Personalize se desejar.';
});
// ── Load ──────────────────────────────────────────────────────
async function loadUser() {
const {
data: { user }
} = await supabase.auth.getUser();
if (!user) return;
userId.value = user.id;
tenantId.value = tenantStore.activeTenantId || user.id;
}
async function loadWhatsApp() {
if (!tenantId.value) return;
let { data } = await supabase.from('notification_channels').select('credentials, connection_status').eq('tenant_id', tenantId.value).eq('channel', 'whatsapp').is('deleted_at', null).maybeSingle();
// Fallback owner_id
if (!data && userId.value && userId.value !== tenantId.value) {
const fb = await supabase.from('notification_channels').select('credentials, connection_status').eq('owner_id', userId.value).eq('channel', 'whatsapp').is('deleted_at', null).maybeSingle();
data = fb.data;
}
if (data?.credentials) {
whatsapp.value.configured = true;
whatsapp.value.credentials = data.credentials;
// Tenta verificar status real via Evolution API
whatsapp.value.checking = true;
try {
const res = await fetch(`${data.credentials.api_url}/instance/fetchInstances`, {
headers: { apikey: data.credentials.api_key }
});
if (res.ok) {
const instances = await res.json();
const inst = Array.isArray(instances) ? instances.find((i) => i.instance?.instanceName === data.credentials.instance_name) : null;
whatsapp.value.status = inst?.instance?.status || 'close';
} else {
whatsapp.value.status = 'close';
}
} catch {
whatsapp.value.status = 'close';
} finally {
whatsapp.value.checking = false;
}
}
}
async function loadSms() {
if (!tenantId.value) return;
const { data } = await supabase.from('addon_credits').select('balance, is_active').eq('tenant_id', tenantId.value).eq('addon_type', 'sms').eq('is_active', true).maybeSingle();
if (data) {
sms.value.hasCredits = true;
sms.value.balance = data.balance || 0;
sms.value.isActive = data.is_active;
}
}
async function loadEmail() {
if (!tenantId.value) return;
const { count } = await supabase.from('email_templates_tenant').select('id', { count: 'exact', head: true }).eq('tenant_id', tenantId.value);
email.value.templatesCount = count || 0;
}
function goTo(route) {
router.push(route);
}
// ── Init ──────────────────────────────────────────────────────
onMounted(async () => {
await loadUser();
await Promise.all([loadWhatsApp(), loadSms(), loadEmail()]);
loading.value = false;
});
</script>
<template>
<div class="flex flex-col gap-5">
<!-- Header -->
<Card>
<template #title>
<div class="flex items-center gap-2">
<i class="pi pi-bell text-xl" />
Canais de Notificação
</div>
</template>
<template #subtitle>Visão geral dos canais de comunicação com seus pacientes.</template>
</Card>
<!-- Loading -->
<div v-if="loading" class="flex items-center justify-center py-12 text-surface-500"><i class="pi pi-spin pi-spinner mr-2 text-xl" /> Verificando canais...</div>
<!-- Channel Cards -->
<div v-else class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div v-for="ch in channels" :key="ch.key" class="border border-surface rounded-xl p-5 flex flex-col gap-4 cursor-pointer hover:shadow-md transition-shadow" @click="goTo(ch.route)">
<!-- Header -->
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center">
<i :class="ch.icon" class="text-xl text-primary" />
</div>
<span class="font-semibold text-lg">{{ ch.label }}</span>
</div>
<Tag :value="ch.tag.label" :severity="ch.tag.severity" :icon="ch.tag.icon" />
</div>
<!-- Descrição -->
<p class="text-sm text-surface-500 m-0">{{ ch.description }}</p>
<!-- Status detalhe -->
<div class="text-xs text-surface-400 mt-auto">
{{ ch.details }}
</div>
<!-- Link -->
<div class="flex justify-end">
<Button label="Configurar" icon="pi pi-arrow-right" iconPos="right" size="small" text @click.stop="goTo(ch.route)" />
</div>
</div>
</div>
<!-- Resumo rápido -->
<Card>
<template #title>Como funciona</template>
<template #content>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2 font-semibold">
<i class="pi pi-whatsapp text-green-500" />
WhatsApp
</div>
<p class="text-surface-500 m-0">Conecte via Evolution API e QR Code. Mensagens automáticas de lembrete, confirmação e cancelamento.</p>
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2 font-semibold">
<i class="pi pi-comment text-blue-500" />
SMS
</div>
<p class="text-surface-500 m-0">Funciona com créditos pré-pagos. Adquira pacotes em Recursos Extras. Ideal para pacientes sem WhatsApp.</p>
</div>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2 font-semibold">
<i class="pi pi-envelope text-orange-500" />
E-mail
</div>
<p class="text-surface-500 m-0">Ativo por padrão. Personalize os templates de e-mail com o visual da sua clínica.</p>
</div>
</div>
</template>
</Card>
</div>
</template>