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
+407 -481
View File
@@ -15,558 +15,484 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useToast } from 'primevue/usetoast'
import { supabase } from '@/lib/supabase/client'
import {
createSupportSession,
listActiveSupportSessions,
listSessionHistory,
revokeSupportSession,
buildSupportUrl,
} from '@/support/supportSessionService'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import { createSupportSession, listActiveSupportSessions, listSessionHistory, revokeSupportSession, buildSupportUrl } from '@/support/supportSessionService';
const TAG = '[SaasSupportPage]'
const toast = useToast()
const TAG = '[SaasSupportPage]';
const toast = useToast();
// ── Tabs ──────────────────────────────────────────────────────────────────────
const activeTab = ref(0)
const activeTab = ref(0);
// ── Estado — Nova Sessão ──────────────────────────────────────────────────────
const selectedTenantId = ref(null)
const ttlMinutes = ref(60)
const sessionNote = ref('')
const creating = ref(false)
const generatedUrl = ref(null)
const generatedData = ref(null)
const selectedTenantId = ref(null);
const ttlMinutes = ref(60);
const sessionNote = ref('');
const creating = ref(false);
const generatedUrl = ref(null);
const generatedData = ref(null);
// ── Estado — Listas ───────────────────────────────────────────────────────────
const loadingTenants = ref(false)
const loadingSessions = ref(false)
const loadingHistory = ref(false)
const revokingToken = ref(null)
const loadingTenants = ref(false);
const loadingSessions = ref(false);
const loadingHistory = ref(false);
const revokingToken = ref(null);
const tenants = ref([])
const tenantMap = ref({})
const activeSessions = ref([])
const sessionHistory = ref([])
const tenants = ref([]);
const tenantMap = ref({});
const activeSessions = ref([]);
const sessionHistory = ref([]);
// ── TTL Options ───────────────────────────────────────────────────────────────
const ttlOptions = [
{ label: '15 minutos', value: 15 },
{ label: '30 minutos', value: 30 },
{ label: '1 hora', value: 60 },
{ label: '2 horas', value: 120 },
]
{ label: '15 minutos', value: 15 },
{ label: '30 minutos', value: 30 },
{ label: '1 hora', value: 60 },
{ label: '2 horas', value: 120 }
];
// ── Countdown tick ────────────────────────────────────────────────────────────
const _now = ref(Date.now())
let _tickTimer = null
const _now = ref(Date.now());
let _tickTimer = null;
function startTick () {
if (_tickTimer) return
_tickTimer = setInterval(() => { _now.value = Date.now() }, 10_000)
function startTick() {
if (_tickTimer) return;
_tickTimer = setInterval(() => {
_now.value = Date.now();
}, 10_000);
}
onBeforeUnmount(() => { if (_tickTimer) clearInterval(_tickTimer) })
onBeforeUnmount(() => {
if (_tickTimer) clearInterval(_tickTimer);
});
// ── Computed ──────────────────────────────────────────────────────────────────
const expiresLabel = computed(() => {
if (!generatedData.value?.expires_at) return ''
return new Date(generatedData.value.expires_at).toLocaleString('pt-BR')
})
if (!generatedData.value?.expires_at) return '';
return new Date(generatedData.value.expires_at).toLocaleString('pt-BR');
});
const tokenPreview = computed(() => {
if (!generatedData.value?.token) return ''
const t = generatedData.value.token
return `${t.slice(0, 8)}${t.slice(-8)}`
})
if (!generatedData.value?.token) return '';
const t = generatedData.value.token;
return `${t.slice(0, 8)}${t.slice(-8)}`;
});
const activeSessionCount = computed(() => activeSessions.value.length)
const activeSessionCount = computed(() => activeSessions.value.length);
// ── Lifecycle ─────────────────────────────────────────────────────────────────
onMounted(async () => {
console.log(`${TAG} montado`)
await loadTenants()
await loadActiveSessions()
startTick()
})
console.log(`${TAG} montado`);
await loadTenants();
await loadActiveSessions();
startTick();
});
// ── Tenants ───────────────────────────────────────────────────────────────────
async function loadTenants () {
loadingTenants.value = true
console.log(`${TAG} loadTenants`)
try {
const { data, error } = await supabase
.from('tenants')
.select('id, name, kind')
.order('name', { ascending: true })
async function loadTenants() {
loadingTenants.value = true;
console.log(`${TAG} loadTenants`);
try {
const { data, error } = await supabase.from('tenants').select('id, name, kind').order('name', { ascending: true });
if (error) throw error
if (error) throw error;
const list = data || []
console.log(`${TAG} ${list.length} tenant(s) carregado(s)`)
const list = data || [];
console.log(`${TAG} ${list.length} tenant(s) carregado(s)`);
tenantMap.value = Object.fromEntries(list.map(t => [t.id, t.name || t.id]))
tenants.value = list.map(t => ({
value: t.id,
label: `${t.name} (${t.kind ?? 'tenant'})`,
}))
} catch (e) {
console.error(`${TAG} loadTenants ERRO`, e)
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 })
} finally {
loadingTenants.value = false
}
tenantMap.value = Object.fromEntries(list.map((t) => [t.id, t.name || t.id]));
tenants.value = list.map((t) => ({
value: t.id,
label: `${t.name} (${t.kind ?? 'tenant'})`
}));
} catch (e) {
console.error(`${TAG} loadTenants ERRO`, e);
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
} finally {
loadingTenants.value = false;
}
}
// ── Sessões ativas ─────────────────────────────────────────────────────────────
async function loadActiveSessions () {
loadingSessions.value = true
console.log(`${TAG} loadActiveSessions`)
try {
activeSessions.value = await listActiveSupportSessions()
console.log(`${TAG} ${activeSessions.value.length} sessão(ões) ativa(s)`)
} catch (e) {
console.error(`${TAG} loadActiveSessions ERRO`, e)
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 })
} finally {
loadingSessions.value = false
}
async function loadActiveSessions() {
loadingSessions.value = true;
console.log(`${TAG} loadActiveSessions`);
try {
activeSessions.value = await listActiveSupportSessions();
console.log(`${TAG} ${activeSessions.value.length} sessão(ões) ativa(s)`);
} catch (e) {
console.error(`${TAG} loadActiveSessions ERRO`, e);
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
} finally {
loadingSessions.value = false;
}
}
// ── Histórico ─────────────────────────────────────────────────────────────────
async function loadHistory () {
if (loadingHistory.value) return
loadingHistory.value = true
console.log(`${TAG} loadHistory`)
try {
sessionHistory.value = await listSessionHistory(100)
console.log(`${TAG} histórico: ${sessionHistory.value.length} registro(s)`)
} catch (e) {
console.error(`${TAG} loadHistory ERRO`, e)
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 })
} finally {
loadingHistory.value = false
}
async function loadHistory() {
if (loadingHistory.value) return;
loadingHistory.value = true;
console.log(`${TAG} loadHistory`);
try {
sessionHistory.value = await listSessionHistory(100);
console.log(`${TAG} histórico: ${sessionHistory.value.length} registro(s)`);
} catch (e) {
console.error(`${TAG} loadHistory ERRO`, e);
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
} finally {
loadingHistory.value = false;
}
}
// ── Criar sessão ──────────────────────────────────────────────────────────────
async function handleCreate () {
if (!selectedTenantId.value) return
creating.value = true
generatedUrl.value = null
generatedData.value = null
async function handleCreate() {
if (!selectedTenantId.value) return;
creating.value = true;
generatedUrl.value = null;
generatedData.value = null;
console.log(`${TAG} handleCreate`, { tenantId: selectedTenantId.value, ttlMinutes: ttlMinutes.value, note: sessionNote.value || '(sem nota)' })
console.log(`${TAG} handleCreate`, { tenantId: selectedTenantId.value, ttlMinutes: ttlMinutes.value, note: sessionNote.value || '(sem nota)' });
try {
const result = await createSupportSession(selectedTenantId.value, ttlMinutes.value, sessionNote.value)
generatedData.value = result
generatedUrl.value = buildSupportUrl(result.token)
try {
const result = await createSupportSession(selectedTenantId.value, ttlMinutes.value, sessionNote.value);
generatedData.value = result;
generatedUrl.value = buildSupportUrl(result.token);
console.log(`${TAG} sessão criada com sucesso`, { token: `${result.token.slice(0,8)}`, expires_at: result.expires_at })
console.log(`${TAG} sessão criada com sucesso`, { token: `${result.token.slice(0, 8)}`, expires_at: result.expires_at });
toast.add({ severity: 'success', summary: 'Sessão criada', detail: 'URL de suporte gerada com sucesso.', life: 4000 })
await loadActiveSessions()
} catch (e) {
console.error(`${TAG} handleCreate ERRO`, e)
toast.add({ severity: 'error', summary: 'Erro ao criar sessão', detail: e?.message, life: 5000 })
} finally {
creating.value = false
}
toast.add({ severity: 'success', summary: 'Sessão criada', detail: 'URL de suporte gerada com sucesso.', life: 4000 });
await loadActiveSessions();
} catch (e) {
console.error(`${TAG} handleCreate ERRO`, e);
toast.add({ severity: 'error', summary: 'Erro ao criar sessão', detail: e?.message, life: 5000 });
} finally {
creating.value = false;
}
}
// ── Revogar ───────────────────────────────────────────────────────────────────
async function handleRevoke (token) {
revokingToken.value = token
console.log(`${TAG} handleRevoke token=${token.slice(0, 8)}`)
try {
await revokeSupportSession(token)
toast.add({ severity: 'success', summary: 'Sessão revogada', life: 3000 })
if (generatedData.value?.token === token) {
generatedUrl.value = null
generatedData.value = null
async function handleRevoke(token) {
revokingToken.value = token;
console.log(`${TAG} handleRevoke token=${token.slice(0, 8)}`);
try {
await revokeSupportSession(token);
toast.add({ severity: 'success', summary: 'Sessão revogada', life: 3000 });
if (generatedData.value?.token === token) {
generatedUrl.value = null;
generatedData.value = null;
}
await loadActiveSessions();
if (sessionHistory.value.length) await loadHistory();
} catch (e) {
console.error(`${TAG} handleRevoke ERRO`, e);
toast.add({ severity: 'error', summary: 'Erro ao revogar', detail: e?.message, life: 4000 });
} finally {
revokingToken.value = null;
}
await loadActiveSessions()
if (sessionHistory.value.length) await loadHistory()
} catch (e) {
console.error(`${TAG} handleRevoke ERRO`, e)
toast.add({ severity: 'error', summary: 'Erro ao revogar', detail: e?.message, life: 4000 })
} finally {
revokingToken.value = null
}
}
// ── Copiar ────────────────────────────────────────────────────────────────────
function copyUrl (url) {
if (!url) return
navigator.clipboard.writeText(url)
console.log(`${TAG} URL copiada`)
toast.add({ severity: 'info', summary: 'Copiado!', detail: 'URL copiada para a área de transferência.', life: 2000 })
function copyUrl(url) {
if (!url) return;
navigator.clipboard.writeText(url);
console.log(`${TAG} URL copiada`);
toast.add({ severity: 'info', summary: 'Copiado!', detail: 'URL copiada para a área de transferência.', life: 2000 });
}
// ── Tab change ────────────────────────────────────────────────────────────────
function onTabChange (e) {
const idx = e.index ?? e
activeTab.value = idx
console.log(`${TAG} tab mudou para ${idx}`)
if (idx === 2 && sessionHistory.value.length === 0) loadHistory()
function onTabChange(e) {
const idx = e.index ?? e;
activeTab.value = idx;
console.log(`${TAG} tab mudou para ${idx}`);
if (idx === 2 && sessionHistory.value.length === 0) loadHistory();
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function tenantName (id) {
return tenantMap.value[id] || id
function tenantName(id) {
return tenantMap.value[id] || id;
}
function formatDate (iso) {
if (!iso) return '-'
return new Date(iso).toLocaleString('pt-BR')
function formatDate(iso) {
if (!iso) return '-';
return new Date(iso).toLocaleString('pt-BR');
}
function remainingLabel (iso) {
_now.value // dependência reativa
if (!iso) return '-'
const diff = new Date(iso) - Date.now()
if (diff <= 0) return 'Expirada'
const min = Math.floor(diff / 60000)
const h = Math.floor(min / 60)
const m = min % 60
if (h > 0) return `${h}h ${m}min`
return `${min} min`
function remainingLabel(iso) {
_now.value; // dependência reativa
if (!iso) return '-';
const diff = new Date(iso) - Date.now();
if (diff <= 0) return 'Expirada';
const min = Math.floor(diff / 60000);
const h = Math.floor(min / 60);
const m = min % 60;
if (h > 0) return `${h}h ${m}min`;
return `${min} min`;
}
function isExpiringSoon (iso) {
if (!iso) return false
const diff = (new Date(iso) - Date.now()) / 60000
return diff > 0 && diff < 15
function isExpiringSoon(iso) {
if (!iso) return false;
const diff = (new Date(iso) - Date.now()) / 60000;
return diff > 0 && diff < 15;
}
function sessionStatusSeverity (session) {
if (session._expired) return 'danger'
if (isExpiringSoon(session.expires_at)) return 'warning'
return 'success'
function sessionStatusSeverity(session) {
if (session._expired) return 'danger';
if (isExpiringSoon(session.expires_at)) return 'warning';
return 'success';
}
function sessionStatusLabel (session) {
if (session._expired) return 'Expirada'
if (isExpiringSoon(session.expires_at)) return 'Expirando'
return 'Ativa'
function sessionStatusLabel(session) {
if (session._expired) return 'Expirada';
if (isExpiringSoon(session.expires_at)) return 'Expirando';
return 'Ativa';
}
</script>
<template>
<!-- Sentinel -->
<div class="h-px" />
<!-- Sentinel -->
<div class="h-px" />
<!-- Hero sticky -->
<div
class="sticky mx-3 md:mx-4 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5"
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
>
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-20 bg-orange-400/10" />
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-10 -left-24 bg-fuchsia-400/10" />
<!-- Hero sticky -->
<div class="sticky mx-3 md:mx-4 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5" :style="{ top: 'var(--layout-sticky-top, 56px)' }">
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-20 bg-orange-400/10" />
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-10 -left-24 bg-fuchsia-400/10" />
</div>
<div class="relative z-10 flex items-center justify-between gap-3 flex-wrap">
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center justify-center w-10 h-10 rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)] flex-shrink-0">
<i class="pi pi-headphones text-[var(--text-color)]" />
</div>
<div class="min-w-0">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Suporte Técnico</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Gere e gerencie links seguros de acesso em modo debug</div>
</div>
</div>
<Tag v-if="activeSessionCount > 0" :value="`${activeSessionCount} sessão${activeSessionCount > 1 ? 'ões' : ''} ativa${activeSessionCount > 1 ? 's' : ''}`" severity="warning" />
</div>
</div>
<div class="relative z-10 flex items-center justify-between gap-3 flex-wrap">
<div class="flex items-center gap-3 min-w-0">
<div class="flex items-center justify-center w-10 h-10 rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)] flex-shrink-0">
<i class="pi pi-headphones text-[var(--text-color)]" />
</div>
<div class="min-w-0">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Suporte Técnico</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Gere e gerencie links seguros de acesso em modo debug</div>
</div>
</div>
<Tag
v-if="activeSessionCount > 0"
:value="`${activeSessionCount} sessão${activeSessionCount > 1 ? 'ões' : ''} ativa${activeSessionCount > 1 ? 's' : ''}`"
severity="warning"
/>
<!-- content -->
<div class="px-3 md:px-4 pb-8 flex flex-col gap-4">
<!-- Tabs -->
<TabView @tab-change="onTabChange">
<!-- Tab 0: Nova Sessão -->
<TabPanel header="Nova Sessão">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5 pt-2">
<!-- Formulário -->
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<div class="text-[1rem] font-semibold flex items-center gap-2 mb-4">
<i class="pi pi-plus-circle text-[var(--primary-color)]" />
Configurar acesso de suporte
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label class="text-[1rem] font-medium">Selecionar Cliente (Tenant)</label>
<Select
v-model="selectedTenantId"
:options="tenants"
option-label="label"
option-value="value"
placeholder="Buscar tenant..."
filter
:loading="loadingTenants"
class="w-full"
empty-filter-message="Nenhum tenant encontrado"
/>
</div>
<div class="flex flex-col gap-1">
<label class="text-[1rem] font-medium">Duração do Acesso</label>
<Select v-model="ttlMinutes" :options="ttlOptions" option-label="label" option-value="value" class="w-full" />
</div>
<div class="flex flex-col gap-1">
<label class="text-[1rem] font-medium">
Nota / Motivo
<span class="text-[var(--text-color-secondary)] font-normal">(opcional)</span>
</label>
<InputText v-model="sessionNote" placeholder="Ex: cliente reportou erro na agenda de recorrência" class="w-full" />
</div>
<Button label="Ativar Modo Suporte" icon="pi pi-shield" severity="warning" :loading="creating" :disabled="!selectedTenantId" class="w-full" @click="handleCreate" />
</div>
</div>
<!-- URL Gerada -->
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<div class="text-[1rem] font-semibold flex items-center gap-2 mb-4">
<i class="pi pi-link text-[var(--primary-color)]" />
URL Gerada
</div>
<div v-if="generatedUrl" class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label class="text-[1rem] font-medium">Link de Acesso</label>
<div class="flex gap-2">
<InputText :value="generatedUrl" readonly class="flex-1 font-mono text-[1rem]" />
<Button icon="pi pi-copy" severity="secondary" outlined v-tooltip.top="'Copiar URL'" @click="copyUrl(generatedUrl)" />
</div>
</div>
<div class="flex items-center gap-2 text-[1rem]">
<i class="pi pi-clock text-orange-500" />
<span class="text-[var(--text-color-secondary)]">Expira em:</span>
<strong>{{ expiresLabel }}</strong>
</div>
<div class="flex items-center gap-2 text-[1rem] text-[var(--text-color-secondary)] font-mono">
<i class="pi pi-key" />
<span>{{ tokenPreview }}</span>
</div>
<div v-if="sessionNote" class="flex items-start gap-2 text-[1rem] text-[var(--text-color-secondary)]">
<i class="pi pi-comment mt-0.5 flex-shrink-0" />
<span class="italic">{{ sessionNote }}</span>
</div>
<Message severity="info" :closable="false">
<div class="text-[1rem]">Envie este link ao terapeuta ou acesse diretamente para monitorar os logs da agenda em tempo real.</div>
</Message>
</div>
<div v-else class="flex flex-col items-center justify-center py-12 text-[var(--text-color-secondary)] gap-3">
<i class="pi pi-shield text-4xl opacity-25" />
<div class="text-[1rem]">Nenhuma sessão gerada ainda</div>
</div>
</div>
</div>
</TabPanel>
<!-- Tab 1: Sessões Ativas -->
<TabPanel>
<template #header>
<span class="flex items-center gap-2">
Sessões Ativas
<Badge v-if="activeSessionCount > 0" :value="String(activeSessionCount)" severity="warning" />
</span>
</template>
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 mt-2">
<div class="flex items-center justify-between mb-4">
<div class="text-[1rem] font-semibold flex items-center gap-2">
<i class="pi pi-circle-fill text-green-500" />
Sessões em vigor
</div>
<Button icon="pi pi-refresh" severity="secondary" outlined size="small" :loading="loadingSessions" label="Atualizar" @click="loadActiveSessions" />
</div>
<DataTable :value="activeSessions" :loading="loadingSessions" empty-message="Nenhuma sessão ativa no momento" size="small" striped-rows>
<Column header="Tenant" style="min-width: 200px">
<template #body="{ data }">
<div class="flex flex-col gap-0.5">
<span class="font-medium text-[1rem]">{{ tenantName(data.tenant_id) }}</span>
<span class="font-mono text-[1rem] text-[var(--text-color-secondary)]">{{ data.tenant_id }}</span>
</div>
</template>
</Column>
<Column header="Token">
<template #body="{ data }">
<span class="font-mono text-[1rem] text-[var(--text-color-secondary)]">{{ data.token.slice(0, 12) }}</span>
</template>
</Column>
<Column header="Restante" style="min-width: 100px">
<template #body="{ data }">
<span :class="isExpiringSoon(data.expires_at) ? 'text-orange-500 font-semibold' : ''">
{{ remainingLabel(data.expires_at) }}
</span>
</template>
</Column>
<Column header="Criada em">
<template #body="{ data }">
<span class="text-[1rem] text-[var(--text-color-secondary)]">{{ formatDate(data.created_at) }}</span>
</template>
</Column>
<Column header="Nota">
<template #body="{ data }">
<span v-if="data._note" class="text-[1rem] italic text-[var(--text-color-secondary)]">{{ data._note }}</span>
<span v-else class="text-[1rem] text-[var(--text-color-secondary)]"></span>
</template>
</Column>
<Column header="Ações" style="width: 110px">
<template #body="{ data }">
<div class="flex gap-2">
<Button icon="pi pi-copy" size="small" severity="secondary" outlined v-tooltip.top="'Copiar URL'" @click="copyUrl(buildSupportUrl(data.token))" />
<Button icon="pi pi-trash" size="small" severity="danger" outlined v-tooltip.top="'Revogar'" :loading="revokingToken === data.token" @click="handleRevoke(data.token)" />
</div>
</template>
</Column>
</DataTable>
</div>
</TabPanel>
<!-- Tab 2: Histórico -->
<TabPanel header="Histórico">
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 mt-2">
<div class="flex items-center justify-between mb-4">
<div class="text-[1rem] font-semibold flex items-center gap-2">
<i class="pi pi-history text-[var(--primary-color)]" />
Últimas 100 sessões
</div>
<Button icon="pi pi-refresh" severity="secondary" outlined size="small" :loading="loadingHistory" label="Carregar" @click="loadHistory" />
</div>
<DataTable :value="sessionHistory" :loading="loadingHistory" empty-message="Clique em Carregar para ver o histórico" size="small" striped-rows paginator :rows="20">
<Column header="Status" style="width: 110px">
<template #body="{ data }">
<Tag :value="sessionStatusLabel(data)" :severity="sessionStatusSeverity(data)" />
</template>
</Column>
<Column header="Tenant" style="min-width: 180px">
<template #body="{ data }">
<div class="flex flex-col gap-0.5">
<span class="font-medium text-[1rem]">{{ tenantName(data.tenant_id) }}</span>
<span class="font-mono text-[1rem] text-[var(--text-color-secondary)]">{{ data.tenant_id }}</span>
</div>
</template>
</Column>
<Column header="Token">
<template #body="{ data }">
<span class="font-mono text-[1rem] text-[var(--text-color-secondary)]">{{ data.token.slice(0, 12) }}</span>
</template>
</Column>
<Column header="Criada em" sortable field="created_at">
<template #body="{ data }">
<span class="text-[1rem] text-[var(--text-color-secondary)]">{{ formatDate(data.created_at) }}</span>
</template>
</Column>
<Column header="Expirava em">
<template #body="{ data }">
<span class="text-[1rem] text-[var(--text-color-secondary)]">{{ formatDate(data.expires_at) }}</span>
</template>
</Column>
<Column header="Nota">
<template #body="{ data }">
<span v-if="data._note" class="text-[1rem] italic text-[var(--text-color-secondary)]">{{ data._note }}</span>
<span v-else class="text-[1rem] text-[var(--text-color-secondary)]"></span>
</template>
</Column>
<Column header="" style="width: 60px">
<template #body="{ data }">
<Button v-if="!data._expired" icon="pi pi-trash" size="small" severity="danger" outlined v-tooltip.top="'Revogar'" :loading="revokingToken === data.token" @click="handleRevoke(data.token)" />
</template>
</Column>
</DataTable>
</div>
</TabPanel>
</TabView>
</div>
</div>
<!-- content -->
<div class="px-3 md:px-4 pb-8 flex flex-col gap-4">
<!-- Tabs -->
<TabView @tab-change="onTabChange">
<!-- Tab 0: Nova Sessão -->
<TabPanel header="Nova Sessão">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5 pt-2">
<!-- Formulário -->
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<div class="text-[1rem] font-semibold flex items-center gap-2 mb-4">
<i class="pi pi-plus-circle text-[var(--primary-color)]" />
Configurar acesso de suporte
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label class="text-[1rem] font-medium">Selecionar Cliente (Tenant)</label>
<Select
v-model="selectedTenantId"
:options="tenants"
option-label="label"
option-value="value"
placeholder="Buscar tenant..."
filter
:loading="loadingTenants"
class="w-full"
empty-filter-message="Nenhum tenant encontrado"
/>
</div>
<div class="flex flex-col gap-1">
<label class="text-[1rem] font-medium">Duração do Acesso</label>
<Select
v-model="ttlMinutes"
:options="ttlOptions"
option-label="label"
option-value="value"
class="w-full"
/>
</div>
<div class="flex flex-col gap-1">
<label class="text-[1rem] font-medium">
Nota / Motivo
<span class="text-[var(--text-color-secondary)] font-normal">(opcional)</span>
</label>
<InputText
v-model="sessionNote"
placeholder="Ex: cliente reportou erro na agenda de recorrência"
class="w-full"
/>
</div>
<Button
label="Ativar Modo Suporte"
icon="pi pi-shield"
severity="warning"
:loading="creating"
:disabled="!selectedTenantId"
class="w-full"
@click="handleCreate"
/>
</div>
</div>
<!-- URL Gerada -->
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<div class="text-[1rem] font-semibold flex items-center gap-2 mb-4">
<i class="pi pi-link text-[var(--primary-color)]" />
URL Gerada
</div>
<div v-if="generatedUrl" class="flex flex-col gap-4">
<div class="flex flex-col gap-1">
<label class="text-[1rem] font-medium">Link de Acesso</label>
<div class="flex gap-2">
<InputText :value="generatedUrl" readonly class="flex-1 font-mono text-[1rem]" />
<Button icon="pi pi-copy" severity="secondary" outlined v-tooltip.top="'Copiar URL'" @click="copyUrl(generatedUrl)" />
</div>
</div>
<div class="flex items-center gap-2 text-[1rem]">
<i class="pi pi-clock text-orange-500" />
<span class="text-[var(--text-color-secondary)]">Expira em:</span>
<strong>{{ expiresLabel }}</strong>
</div>
<div class="flex items-center gap-2 text-[1rem] text-[var(--text-color-secondary)] font-mono">
<i class="pi pi-key" />
<span>{{ tokenPreview }}</span>
</div>
<div v-if="sessionNote" class="flex items-start gap-2 text-[1rem] text-[var(--text-color-secondary)]">
<i class="pi pi-comment mt-0.5 flex-shrink-0" />
<span class="italic">{{ sessionNote }}</span>
</div>
<Message severity="info" :closable="false">
<div class="text-[1rem]">Envie este link ao terapeuta ou acesse diretamente para monitorar os logs da agenda em tempo real.</div>
</Message>
</div>
<div v-else class="flex flex-col items-center justify-center py-12 text-[var(--text-color-secondary)] gap-3">
<i class="pi pi-shield text-4xl opacity-25" />
<div class="text-[1rem]">Nenhuma sessão gerada ainda</div>
</div>
</div>
</div>
</TabPanel>
<!-- Tab 1: Sessões Ativas -->
<TabPanel>
<template #header>
<span class="flex items-center gap-2">
Sessões Ativas
<Badge v-if="activeSessionCount > 0" :value="String(activeSessionCount)" severity="warning" />
</span>
</template>
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 mt-2">
<div class="flex items-center justify-between mb-4">
<div class="text-[1rem] font-semibold flex items-center gap-2">
<i class="pi pi-circle-fill text-green-500" />
Sessões em vigor
</div>
<Button
icon="pi pi-refresh"
severity="secondary"
outlined
size="small"
:loading="loadingSessions"
label="Atualizar"
@click="loadActiveSessions"
/>
</div>
<DataTable
:value="activeSessions"
:loading="loadingSessions"
empty-message="Nenhuma sessão ativa no momento"
size="small"
striped-rows
>
<Column header="Tenant" style="min-width: 200px">
<template #body="{ data }">
<div class="flex flex-col gap-0.5">
<span class="font-medium text-[1rem]">{{ tenantName(data.tenant_id) }}</span>
<span class="font-mono text-[1rem] text-[var(--text-color-secondary)]">{{ data.tenant_id }}</span>
</div>
</template>
</Column>
<Column header="Token">
<template #body="{ data }">
<span class="font-mono text-[1rem] text-[var(--text-color-secondary)]">{{ data.token.slice(0, 12) }}</span>
</template>
</Column>
<Column header="Restante" style="min-width: 100px">
<template #body="{ data }">
<span :class="isExpiringSoon(data.expires_at) ? 'text-orange-500 font-semibold' : ''">
{{ remainingLabel(data.expires_at) }}
</span>
</template>
</Column>
<Column header="Criada em">
<template #body="{ data }">
<span class="text-[1rem] text-[var(--text-color-secondary)]">{{ formatDate(data.created_at) }}</span>
</template>
</Column>
<Column header="Nota">
<template #body="{ data }">
<span v-if="data._note" class="text-[1rem] italic text-[var(--text-color-secondary)]">{{ data._note }}</span>
<span v-else class="text-[1rem] text-[var(--text-color-secondary)]"></span>
</template>
</Column>
<Column header="Ações" style="width: 110px">
<template #body="{ data }">
<div class="flex gap-2">
<Button icon="pi pi-copy" size="small" severity="secondary" outlined v-tooltip.top="'Copiar URL'" @click="copyUrl(buildSupportUrl(data.token))" />
<Button icon="pi pi-trash" size="small" severity="danger" outlined v-tooltip.top="'Revogar'" :loading="revokingToken === data.token" @click="handleRevoke(data.token)" />
</div>
</template>
</Column>
</DataTable>
</div>
</TabPanel>
<!-- Tab 2: Histórico -->
<TabPanel header="Histórico">
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5 mt-2">
<div class="flex items-center justify-between mb-4">
<div class="text-[1rem] font-semibold flex items-center gap-2">
<i class="pi pi-history text-[var(--primary-color)]" />
Últimas 100 sessões
</div>
<Button
icon="pi pi-refresh"
severity="secondary"
outlined
size="small"
:loading="loadingHistory"
label="Carregar"
@click="loadHistory"
/>
</div>
<DataTable
:value="sessionHistory"
:loading="loadingHistory"
empty-message="Clique em Carregar para ver o histórico"
size="small"
striped-rows
paginator
:rows="20"
>
<Column header="Status" style="width: 110px">
<template #body="{ data }">
<Tag :value="sessionStatusLabel(data)" :severity="sessionStatusSeverity(data)" />
</template>
</Column>
<Column header="Tenant" style="min-width: 180px">
<template #body="{ data }">
<div class="flex flex-col gap-0.5">
<span class="font-medium text-[1rem]">{{ tenantName(data.tenant_id) }}</span>
<span class="font-mono text-[1rem] text-[var(--text-color-secondary)]">{{ data.tenant_id }}</span>
</div>
</template>
</Column>
<Column header="Token">
<template #body="{ data }">
<span class="font-mono text-[1rem] text-[var(--text-color-secondary)]">{{ data.token.slice(0, 12) }}</span>
</template>
</Column>
<Column header="Criada em" sortable field="created_at">
<template #body="{ data }">
<span class="text-[1rem] text-[var(--text-color-secondary)]">{{ formatDate(data.created_at) }}</span>
</template>
</Column>
<Column header="Expirava em">
<template #body="{ data }">
<span class="text-[1rem] text-[var(--text-color-secondary)]">{{ formatDate(data.expires_at) }}</span>
</template>
</Column>
<Column header="Nota">
<template #body="{ data }">
<span v-if="data._note" class="text-[1rem] italic text-[var(--text-color-secondary)]">{{ data._note }}</span>
<span v-else class="text-[1rem] text-[var(--text-color-secondary)]"></span>
</template>
</Column>
<Column header="" style="width: 60px">
<template #body="{ data }">
<Button
v-if="!data._expired"
icon="pi pi-trash"
size="small"
severity="danger"
outlined
v-tooltip.top="'Revogar'"
:loading="revokingToken === data.token"
@click="handleRevoke(data.token)"
/>
</template>
</Column>
</DataTable>
</div>
</TabPanel>
</TabView>
</div>
</template>