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:
@@ -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>
|
||||
Reference in New Issue
Block a user