Files
agenciapsilmno/src/views/pages/HomeCards.vue
T
Leonardo 27467bbb68 M1: features/medicos + features/insurance + ComponentCadastroRapido refactor
Modulo 1 da Fase 1 de padronizacao. Novos features/medicos (services
+ composable useMedicos) e features/insurance (idem). 3 cadastros
rapidos (medicos, convenios, ComponentCadastroRapido + Insurance
PlanQuickCreateDialog) migrados pra usar os composables novos —
zero supabase.from() em UI components. TEST_ACCOUNTS extraido pra
src/config/devTestAccounts.js. Topbar ganhou switcher de layout
+ atalhos M1 via novo useTopbarDevMenuExtras. M1.6 MelissaLayout
90 imports deferida pra sessao dedicada (memoria padronizacao_sweep).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 04:19:57 -03:00

1510 lines
49 KiB
Vue

<!-- src/views/pages/HomeCards.vue -->
<script setup>
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { supabase } from '../../lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import { TEST_ACCOUNTS } from '@/config/devTestAccounts';
const router = useRouter();
const tenant = useTenantStore();
const checking = ref(true);
const userEmail = ref('');
const role = ref(null);
const globalRole = ref(null);
const memberships = ref([]);
const activeTenantId = ref(null);
const activeRole = ref(null);
const isDev = import.meta.env.DEV;
const storageTenantId = ref(null);
const storageTenant = ref(null);
const storageCurrentTenantId = ref(null);
const PROFILE_CARDS = [
{
key: 'patient',
index: '01',
label: 'Paciente',
description: 'Documentos, histórico de sessões e interações com seu terapeuta.',
icon: 'pi-user',
color: '#4ADE80',
colorDim: 'rgba(74,222,128,0.08)',
colorBorder: 'rgba(74,222,128,0.2)',
tag: 'Portal'
},
{
key: 'therapist',
index: '02',
label: 'Terapeuta',
description: 'Agenda, prontuários, evolução clínica e gestão de pacientes.',
icon: 'pi-calendar',
color: '#60A5FA',
colorDim: 'rgba(96,165,250,0.08)',
colorBorder: 'rgba(96,165,250,0.2)',
tag: 'Clínico'
},
{
key: 'supervisor',
index: '03',
label: 'Supervisor',
description: 'Supervisione sessões, evolução dos pacientes e indicadores da clínica.',
icon: 'pi-eye',
color: '#C084FC',
colorDim: 'rgba(192,132,252,0.08)',
colorBorder: 'rgba(192,132,252,0.2)',
tag: 'Supervisão'
},
{
key: 'clinic_admin',
index: '04',
label: 'Clínica',
description: 'Gestão de terapeutas, salas, secretaria e configurações.',
icon: 'pi-building',
color: '#A78BFA',
colorDim: 'rgba(167,139,250,0.08)',
colorBorder: 'rgba(167,139,250,0.2)',
tag: 'Gestão'
},
{
key: 'editor',
index: '05',
label: 'Editor',
description: 'Crie e gerencie cursos, módulos e conteúdos da plataforma de microlearning.',
icon: 'pi-pencil',
color: '#FB923C',
colorDim: 'rgba(251,146,60,0.08)',
colorBorder: 'rgba(251,146,60,0.2)',
tag: 'Conteúdo'
},
{
key: 'saas',
index: '06',
label: 'SaaS',
description: 'Visão global da plataforma: tenants, assinaturas e saúde.',
icon: 'pi-shield',
color: '#F43F5E',
colorDim: 'rgba(244,63,94,0.08)',
colorBorder: 'rgba(244,63,94,0.2)',
tag: 'SaaS'
}
];
const QA_EXTRA_USERS = computed(() => [
{ key: 'therapist2', label: 'Terapeuta 2', ...TEST_ACCOUNTS.therapist2 },
{ key: 'therapist3', label: 'Terapeuta 3', ...TEST_ACCOUNTS.therapist3 },
{ key: 'secretary', label: 'Secretária', ...TEST_ACCOUNTS.secretary }
]);
function setLoginPrefill(acc) {
if (!isDev || !acc?.email) return;
sessionStorage.setItem('login_prefill_email', String(acc.email).trim().toLowerCase());
sessionStorage.setItem('login_prefill_password', String(acc.password || ''));
}
async function goQaPrefill(key) {
const acc = TEST_ACCOUNTS[key];
if (!acc) return;
sessionStorage.setItem('intended_area', 'therapist');
setLoginPrefill(acc);
router.push('/auth/login');
}
const envUsers = ref([]);
function buildEnvUsers() {
return [
{ key: 'patient', label: 'Paciente', tag: 'portal', ...TEST_ACCOUNTS.patient },
{ key: 'therapist', label: 'Terapeuta', tag: 'therapist', ...TEST_ACCOUNTS.therapist },
{ key: 'supervisor', label: 'Supervisor', tag: 'supervisor', ...TEST_ACCOUNTS.supervisor },
{ key: 'clinic_admin', label: 'Clínica Full', tag: 'clinic', ...TEST_ACCOUNTS.clinic_admin },
{ key: 'editor', label: 'Editor', tag: 'editor', ...TEST_ACCOUNTS.editor },
{ key: 'saas', label: 'SaaS Master', tag: 'saas', ...TEST_ACCOUNTS.saas }
].map((r) => ({ ...r, passwordDev: isDev ? r.password : null }));
}
const customUsers = ref([]);
const customUsersLoading = ref(false);
const intentLeads = ref([]);
const intentLeadsLoading = ref(false);
const authUsers = ref([]);
const authUsersLoading = ref(false);
function inferTypeLabel(row) {
const r = row?.tenant_role || row?.global_role || '';
if (r === 'clinic_admin' || r === 'tenant_admin') return 'Clínica';
if (r === 'therapist') return 'Terapeuta';
if (r === 'patient' || r === 'portal_user') return 'Paciente';
if (r === 'saas_admin') return 'SaaS';
if (r === 'tenant_member') return 'Membro';
return r || '—';
}
async function loadCustomUsers() {
customUsersLoading.value = true;
try {
const { data, error } = await supabase.rpc('dev_list_custom_users');
if (error) throw error;
customUsers.value = Array.isArray(data) ? data : [];
} catch {
customUsers.value = [];
} finally {
customUsersLoading.value = false;
}
}
async function loadIntentLeads() {
intentLeadsLoading.value = true;
try {
const { data, error } = await supabase.rpc('dev_list_intent_leads');
if (error) throw error;
intentLeads.value = Array.isArray(data) ? data : [];
} catch {
intentLeads.value = [];
} finally {
intentLeadsLoading.value = false;
}
}
async function loadAuthUsers() {
authUsersLoading.value = true;
try {
const { data, error } = await supabase.rpc('dev_list_auth_users', { p_limit: 50 });
if (error) throw error;
authUsers.value = Array.isArray(data) ? data : [];
} catch {
authUsers.value = [];
} finally {
authUsersLoading.value = false;
}
}
function roleToPath(r) {
if (r === 'clinic_admin' || r === 'tenant_admin' || r === 'admin') return '/admin';
if (r === 'therapist') return '/therapist';
if (r === 'supervisor') return '/supervisor';
if (r === 'patient' || r === 'portal_user') return '/portal';
if (r === 'saas_admin') return '/saas';
return '/';
}
function readStorageDebug() {
try {
storageTenantId.value = localStorage.getItem('tenant_id');
storageCurrentTenantId.value = localStorage.getItem('currentTenantId');
storageTenant.value = localStorage.getItem('tenant') || null;
} catch (_) {}
}
async function fetchGlobalRole() {
const { data: userData } = await supabase.auth.getUser();
const uid = userData?.user?.id;
if (!uid) return null;
const { data } = await supabase.from('profiles').select('role').eq('id', uid).single();
return data?.role || null;
}
async function syncTenantRole() {
await tenant.loadSessionAndTenant();
role.value = tenant.activeRole || null;
activeTenantId.value = tenant.activeTenantId || null;
activeRole.value = tenant.activeRole || null;
return role.value;
}
async function fetchMyTenants() {
try {
const { data, error } = await supabase.rpc('my_tenants');
if (error) return [];
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
async function go(area) {
const { data: sessionData } = await supabase.auth.getSession();
const session = sessionData?.session;
if (session) {
userEmail.value = session.user?.email || userEmail.value || '';
globalRole.value = await fetchGlobalRole();
memberships.value = await fetchMyTenants();
if (globalRole.value === 'saas_admin') {
readStorageDebug();
return router.push('/saas');
}
if (globalRole.value === 'portal_user') {
try {
['tenant_id', 'tenant', 'currentTenantId'].forEach((k) => localStorage.removeItem(k));
} catch (_) {}
readStorageDebug();
return router.push('/portal');
}
const r = role.value || (await syncTenantRole());
readStorageDebug();
if (!r) return router.push('/auth/login');
return router.push(roleToPath(r));
}
sessionStorage.setItem('intended_area', area);
if (isDev) {
const acc = TEST_ACCOUNTS[area];
if (acc) {
sessionStorage.setItem('login_prefill_email', acc.email);
sessionStorage.setItem('login_prefill_password', acc.password);
} else {
sessionStorage.removeItem('login_prefill_email');
sessionStorage.removeItem('login_prefill_password');
}
}
router.push('/auth/login');
}
async function goMyPanel() {
globalRole.value = await fetchGlobalRole();
memberships.value = await fetchMyTenants();
if (globalRole.value === 'saas_admin') {
readStorageDebug();
return router.push('/saas');
}
if (globalRole.value === 'portal_user') {
try {
['tenant_id', 'tenant', 'currentTenantId'].forEach((k) => localStorage.removeItem(k));
} catch (_) {}
readStorageDebug();
return router.push('/portal');
}
const r = role.value || (await syncTenantRole());
readStorageDebug();
if (!r) return router.push('/auth/login');
router.push(roleToPath(r));
}
async function logout() {
try {
await supabase.auth.signOut();
} finally {
role.value = null;
globalRole.value = null;
memberships.value = [];
activeTenantId.value = null;
activeRole.value = null;
userEmail.value = '';
customUsers.value = [];
intentLeads.value = [];
authUsers.value = [];
sessionStorage.removeItem('redirect_after_login');
sessionStorage.removeItem('intended_area');
try {
['tenant_id', 'tenant', 'currentTenantId'].forEach((k) => localStorage.removeItem(k));
} catch (_) {}
readStorageDebug();
router.replace('/');
}
}
const sessionSummary = computed(() => ({
email: userEmail.value || '',
globalRole: globalRole.value,
activeTenantId: activeTenantId.value,
activeRole: activeRole.value,
memberships: memberships.value
}));
onMounted(async () => {
try {
envUsers.value = buildEnvUsers();
const { data: sessionData } = await supabase.auth.getSession();
const session = sessionData?.session;
readStorageDebug();
if (session) {
userEmail.value = session.user?.email || '';
globalRole.value = await fetchGlobalRole();
memberships.value = await fetchMyTenants();
if (globalRole.value === 'saas_admin') {
await loadCustomUsers();
await loadIntentLeads();
await loadAuthUsers();
}
if (globalRole.value === 'portal_user') {
try {
['tenant_id', 'tenant', 'currentTenantId'].forEach((k) => localStorage.removeItem(k));
} catch (_) {}
readStorageDebug();
router.replace('/portal');
return;
}
role.value = await syncTenantRole();
readStorageDebug();
if (role.value) {
router.replace(roleToPath(role.value));
return;
}
}
} finally {
checking.value = false;
}
});
</script>
<template>
<!-- Loading -->
<div v-if="checking" class="hc-loading">
<div class="hc-loading__ring">
<div class="hc-loading__dot" />
<div class="hc-loading__dot" />
<div class="hc-loading__dot" />
</div>
</div>
<div v-else class="hc-root">
<!-- Fundo -->
<div class="hc-bg" aria-hidden="true">
<div class="hc-bg__noise" />
<div class="hc-bg__line hc-bg__line--1" />
<div class="hc-bg__line hc-bg__line--2" />
<div class="hc-bg__line hc-bg__line--3" />
<div class="hc-bg__spot hc-bg__spot--a" />
<div class="hc-bg__spot hc-bg__spot--b" />
</div>
<div class="hc-wrap">
<!-- NAV -->
<nav class="hc-nav">
<div class="hc-nav__brand">
<span class="hc-nav__logo">Ψ</span>
<span class="hc-nav__name">Agência PSI</span>
</div>
<div class="hc-nav__right">
<span class="hc-nav__env">DEV</span>
<template v-if="userEmail">
<span class="hc-nav__sep" />
<span class="hc-nav__email">{{ userEmail }}</span>
<button class="hc-nav__logout" @click="logout" title="Sair">
<i class="pi pi-sign-out" />
</button>
</template>
</div>
</nav>
<!-- HERO -->
<header class="hc-hero">
<div class="hc-hero__eyebrow">Plataforma de saúde mental</div>
<h1 class="hc-hero__title">
<span class="hc-hero__title-line">Escolha seu</span>
<span class="hc-hero__title-line hc-hero__title-line--accent">perfil de acesso</span>
</h1>
<p class="hc-hero__sub">Selecione como você vai usar a plataforma hoje. Você será autenticado e redirecionado automaticamente.</p>
</header>
<!-- SESSÃO ATIVA -->
<div v-if="userEmail" class="hc-session">
<div class="hc-session__pill">
<span class="hc-session__dot" />
Sessão ativa
</div>
<div class="hc-session__email">{{ sessionSummary.email }}</div>
<div class="hc-session__meta">
<code>{{ sessionSummary.globalRole || '—' }}</code>
<span v-if="sessionSummary.activeRole"
> <code>{{ sessionSummary.activeRole }}</code></span
>
</div>
<div class="hc-session__actions">
<button class="hc-btn hc-btn--primary" @click="goMyPanel"><i class="pi pi-arrow-right" /> Ir para meu painel</button>
<button class="hc-btn hc-btn--ghost" @click="logout"><i class="pi pi-sign-out" /> Sair</button>
</div>
<!-- Debug colapsável -->
<details class="hc-debug">
<summary class="hc-debug__toggle"><i class="pi pi-code" /> Inspecionar sessão</summary>
<div class="hc-debug__grid">
<div class="hc-debug__cell">
<span class="hc-debug__label">profiles.role</span>
<code>{{ sessionSummary.globalRole || '—' }}</code>
</div>
<div class="hc-debug__cell">
<span class="hc-debug__label">tenant.activeRole</span>
<code>{{ sessionSummary.activeRole || '—' }}</code>
</div>
<div class="hc-debug__cell">
<span class="hc-debug__label">tenant.activeTenantId</span>
<code class="hc-debug__break">{{ sessionSummary.activeTenantId || '—' }}</code>
</div>
<div class="hc-debug__cell hc-debug__cell--wide">
<span class="hc-debug__label">my_tenants()</span>
<div v-if="sessionSummary.memberships?.length">
<code v-for="(m, i) in sessionSummary.memberships" :key="i" class="hc-debug__block"> {{ m.tenant_id }} · {{ m.role }} · {{ m.status }} · {{ m.kind }} </code>
</div>
<code v-else></code>
</div>
<div class="hc-debug__cell hc-debug__cell--wide">
<span class="hc-debug__label">localStorage</span>
<code>tenant_id: {{ storageTenantId || '—' }} · currentTenantId: {{ storageCurrentTenantId || '—' }}</code>
</div>
</div>
</details>
</div>
<!-- CARDS DE PERFIL -->
<div class="hc-cards">
<button
v-for="(card, i) in PROFILE_CARDS"
:key="card.key"
class="hc-card"
:style="{
'--c': card.color,
'--c-dim': card.colorDim,
'--c-border': card.colorBorder,
animationDelay: `${i * 80}ms`
}"
@click="go(card.key)"
>
<div class="hc-card__num">{{ card.index }}</div>
<div class="hc-card__shine" />
<div class="hc-card__head">
<div class="hc-card__icon">
<i :class="`pi ${card.icon}`" />
</div>
<span class="hc-card__tag">{{ card.tag }}</span>
</div>
<div class="hc-card__body">
<div class="hc-card__title">{{ card.label }}</div>
<div class="hc-card__desc">{{ card.description }}</div>
</div>
<div class="hc-card__foot">Entrar <i class="pi pi-arrow-right" /></div>
</button>
</div>
<!-- DEV: CREDENCIAIS -->
<details class="hc-panel" open>
<summary class="hc-panel__head">
<div class="hc-panel__title"><i class="pi pi-key" /> Credenciais de desenvolvimento</div>
<span class="hc-panel__badge">{{ envUsers.length }} contas</span>
</summary>
<div class="hc-panel__body">
<div class="hc-table-wrap">
<table class="hc-table">
<thead>
<tr>
<th>Perfil</th>
<th>E-mail</th>
<th>Tipo</th>
<th v-if="isDev">Senha</th>
<th>Acesso rápido</th>
</tr>
</thead>
<tbody>
<tr v-for="u in envUsers" :key="u.key">
<td class="hc-table__name">{{ u.label }}</td>
<td>
<code class="hc-table__code">{{ u.email }}</code>
</td>
<td>
<span :class="`hc-badge hc-badge--${u.tag}`">{{ u.tag }}</span>
</td>
<td v-if="isDev">
<code class="hc-table__pass">{{ u.passwordDev || '—' }}</code>
</td>
<td>
<button class="hc-quick" @click="goQaPrefill(u.key)"><i class="pi pi-sign-in" /> Entrar</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- QA extras -->
<div v-if="QA_EXTRA_USERS.length" class="hc-extras">
<span class="hc-extras__label">QA extras</span>
<button v-for="u in QA_EXTRA_USERS" :key="u.key" class="hc-quick hc-quick--sm" @click="goQaPrefill(u.key)"><i class="pi pi-user" /> {{ u.label }}</button>
</div>
</div>
</details>
<!-- SEÇÕES SAAS ADMIN -->
<template v-if="userEmail && sessionSummary.globalRole === 'saas_admin'">
<!-- Usuários criados -->
<details class="hc-panel">
<summary class="hc-panel__head">
<div class="hc-panel__title"><i class="pi pi-users" /> Usuários criados</div>
<button class="hc-btn hc-btn--xs" :disabled="customUsersLoading" @click.stop="loadCustomUsers">
<i :class="['pi', customUsersLoading ? 'pi-spin pi-spinner' : 'pi-refresh']" />
</button>
</summary>
<div class="hc-panel__body">
<div class="hc-table-wrap">
<table class="hc-table">
<thead>
<tr>
<th>Tipo</th>
<th>E-mail</th>
<th>Role global</th>
<th>Role tenant</th>
<th>Senha DEV</th>
<th>Criado</th>
</tr>
</thead>
<tbody>
<tr v-for="u in customUsers" :key="u.user_id">
<td class="hc-table__name">{{ inferTypeLabel(u) }}</td>
<td>
<code class="hc-table__code">{{ u.email }}</code>
</td>
<td>
<code class="hc-table__mono">{{ u.global_role || '—' }}</code>
</td>
<td>
<code class="hc-table__mono">{{ u.tenant_role || '—' }}</code>
</td>
<td>
<code class="hc-table__pass">{{ u.password_dev || '—' }}</code>
</td>
<td>
<code class="hc-table__mono">{{ u.created_at ? new Date(u.created_at).toLocaleString('pt-BR') : '—' }}</code>
</td>
</tr>
<tr v-if="!customUsers.length">
<td colspan="6" class="hc-table__empty">Nenhum usuário adicional.</td>
</tr>
</tbody>
</table>
</div>
</div>
</details>
<!-- auth.users -->
<details class="hc-panel">
<summary class="hc-panel__head">
<div class="hc-panel__title"><i class="pi pi-database" /> auth.users</div>
<button class="hc-btn hc-btn--xs" :disabled="authUsersLoading" @click.stop="loadAuthUsers">
<i :class="['pi', authUsersLoading ? 'pi-spin pi-spinner' : 'pi-refresh']" />
</button>
</summary>
<div class="hc-panel__body">
<div class="hc-table-wrap">
<table class="hc-table">
<thead>
<tr>
<th>ID</th>
<th>E-mail</th>
<th>Criado</th>
</tr>
</thead>
<tbody>
<tr v-for="u in authUsers" :key="u.id">
<td>
<code class="hc-table__mono hc-table__break">{{ u.id }}</code>
</td>
<td>
<code class="hc-table__code">{{ u.email }}</code>
</td>
<td>
<code class="hc-table__mono">{{ u.created_at ? new Date(u.created_at).toLocaleString('pt-BR') : '—' }}</code>
</td>
</tr>
<tr v-if="!authUsers.length">
<td colspan="3" class="hc-table__empty">Nenhum usuário.</td>
</tr>
</tbody>
</table>
</div>
</div>
</details>
<!-- Leads -->
<details class="hc-panel">
<summary class="hc-panel__head">
<div class="hc-panel__title"><i class="pi pi-inbox" /> Intenções sem conta (leads)</div>
<button class="hc-btn hc-btn--xs" :disabled="intentLeadsLoading" @click.stop="loadIntentLeads">
<i :class="['pi', intentLeadsLoading ? 'pi-spin pi-spinner' : 'pi-refresh']" />
</button>
</summary>
<div class="hc-panel__body">
<div class="hc-table-wrap">
<table class="hc-table">
<thead>
<tr>
<th>E-mail</th>
<th>Plano</th>
<th>Intervalo</th>
<th>Status</th>
<th>Tenant</th>
<th>Última intenção</th>
</tr>
</thead>
<tbody>
<tr v-for="l in intentLeads" :key="l.email">
<td>
<code class="hc-table__code">{{ l.email }}</code>
</td>
<td>
<code class="hc-table__mono">{{ l.plan_key || '—' }}</code>
</td>
<td>
<code class="hc-table__mono">{{ l.billing_interval || '—' }}</code>
</td>
<td>
<code class="hc-table__mono">{{ l.status || '—' }}</code>
</td>
<td>
<code class="hc-table__mono hc-table__break">{{ l.tenant_id || '—' }}</code>
</td>
<td>
<code class="hc-table__mono">{{ l.last_intent_at ? new Date(l.last_intent_at).toLocaleString('pt-BR') : '—' }}</code>
</td>
</tr>
<tr v-if="!intentLeads.length">
<td colspan="6" class="hc-table__empty">Nenhuma intenção órfã.</td>
</tr>
</tbody>
</table>
</div>
</div>
</details>
</template>
<!-- FOOTER -->
<footer class="hc-footer">
<span class="hc-footer__psi">Ψ</span>
Agência PSI · Ambiente de desenvolvimento
</footer>
</div>
</div>
</template>
<style scoped>
/* ─────────────────────────────────────────────────
TOKENS
───────────────────────────────────────────────── */
.hc-root {
--bg: var(--surface-ground, #0d0d10);
--surface: var(--surface-card, #131317);
--border: var(--surface-border, rgba(255, 255, 255, 0.07));
--text: var(--text-color, #eeeef2);
--muted: var(--text-color-secondary, rgba(238, 238, 242, 0.45));
--dim: rgba(238, 238, 242, 0.18);
--primary: var(--primary-color, #7c6af7);
--r: 16px;
--r-lg: 24px;
font-family: 'DM Sans', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
position: relative;
overflow-x: hidden;
}
/* ─────────────────────────────────────────────────
LOADING
───────────────────────────────────────────────── */
.hc-loading {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: var(--surface-ground, #0d0d10);
}
.hc-loading__ring {
display: flex;
gap: 6px;
}
.hc-loading__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--primary-color, #7c6af7);
animation: bounce 1s ease-in-out infinite;
}
.hc-loading__dot:nth-child(2) {
animation-delay: 0.15s;
}
.hc-loading__dot:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes bounce {
0%,
80%,
100% {
transform: translateY(0);
opacity: 0.4;
}
40% {
transform: translateY(-6px);
opacity: 1;
}
}
/* ─────────────────────────────────────────────────
BG
───────────────────────────────────────────────── */
.hc-bg {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
}
.hc-bg__noise {
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
background-size: 256px 256px;
opacity: 0.6;
}
.hc-bg__line {
position: absolute;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.06), transparent);
}
.hc-bg__line--1 {
top: 180px;
}
.hc-bg__line--2 {
top: 50%;
}
.hc-bg__line--3 {
bottom: 200px;
}
.hc-bg__spot {
position: absolute;
border-radius: 50%;
filter: blur(100px);
pointer-events: none;
}
.hc-bg__spot--a {
width: 700px;
height: 700px;
top: -200px;
right: -200px;
background: radial-gradient(circle, rgba(124, 106, 247, 0.12), transparent 65%);
animation: flt 20s ease-in-out infinite alternate;
}
.hc-bg__spot--b {
width: 600px;
height: 600px;
bottom: -150px;
left: -150px;
background: radial-gradient(circle, rgba(74, 222, 128, 0.08), transparent 65%);
animation: flt 26s ease-in-out infinite alternate-reverse;
}
@keyframes flt {
from {
transform: translate(0, 0) scale(1);
}
to {
transform: translate(40px, 30px) scale(1.08);
}
}
/* ─────────────────────────────────────────────────
LAYOUT
───────────────────────────────────────────────── */
.hc-wrap {
position: relative;
z-index: 1;
max-width: 1080px;
margin: 0 auto;
padding: 32px 24px 80px;
display: flex;
flex-direction: column;
gap: 28px;
}
/* ─────────────────────────────────────────────────
NAV
───────────────────────────────────────────────── */
.hc-nav {
display: flex;
align-items: center;
justify-content: space-between;
}
.hc-nav__brand {
display: flex;
align-items: center;
gap: 10px;
}
.hc-nav__logo {
font-size: 1.4rem;
font-weight: 700;
color: var(--primary);
line-height: 1;
text-shadow: 0 0 20px rgba(124, 106, 247, 0.5);
}
.hc-nav__name {
font-size: 0.9rem;
font-weight: 600;
letter-spacing: -0.01em;
opacity: 0.85;
}
.hc-nav__right {
display: flex;
align-items: center;
gap: 10px;
}
.hc-nav__env {
font-size: 0.6rem;
font-weight: 800;
letter-spacing: 0.14em;
text-transform: uppercase;
padding: 3px 9px;
border-radius: 100px;
background: rgba(124, 106, 247, 0.12);
border: 1px solid rgba(124, 106, 247, 0.25);
color: #a78bfa;
}
.hc-nav__sep {
width: 1px;
height: 14px;
background: var(--border);
}
.hc-nav__email {
font-size: 0.72rem;
color: var(--muted);
}
.hc-nav__logout {
width: 28px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border);
background: transparent;
color: var(--muted);
cursor: pointer;
transition: all 0.15s;
}
.hc-nav__logout:hover {
background: rgba(255, 255, 255, 0.06);
color: var(--text);
}
/* ─────────────────────────────────────────────────
HERO
───────────────────────────────────────────────── */
.hc-hero {
padding: 20px 0 4px;
animation: fadeUp 0.5s ease both;
}
.hc-hero__eyebrow {
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.14em;
color: var(--primary);
opacity: 0.7;
margin-bottom: 12px;
}
.hc-hero__title {
display: flex;
flex-direction: column;
gap: 2px;
margin: 0 0 16px;
}
.hc-hero__title-line {
font-size: clamp(2rem, 5vw, 3.2rem);
font-weight: 700;
letter-spacing: -0.04em;
line-height: 1.1;
color: var(--text);
}
.hc-hero__title-line--accent {
color: transparent;
background: linear-gradient(135deg, #a78bfa, #60a5fa 50%, #4ade80);
-webkit-background-clip: text;
background-clip: text;
}
.hc-hero__sub {
font-size: 0.9rem;
color: var(--muted);
margin: 0;
max-width: 480px;
line-height: 1.6;
}
/* ─────────────────────────────────────────────────
SESSÃO
───────────────────────────────────────────────── */
.hc-session {
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: var(--surface);
padding: 20px 24px;
display: flex;
flex-direction: column;
gap: 12px;
animation: fadeUp 0.35s ease both;
}
.hc-session__pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #4ade80;
opacity: 0.85;
}
.hc-session__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #4ade80;
box-shadow: 0 0 8px #4ade80;
animation: pulse-dot 2s ease-in-out infinite;
}
@keyframes pulse-dot {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
.hc-session__email {
font-size: 0.9rem;
font-weight: 500;
}
.hc-session__meta {
display: flex;
gap: 8px;
align-items: center;
flex-wrap: wrap;
font-size: 0.72rem;
color: var(--muted);
}
.hc-session__meta code {
background: rgba(255, 255, 255, 0.06);
padding: 1px 7px;
border-radius: 5px;
border: 1px solid var(--border);
font-size: 0.7rem;
}
.hc-session__actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
/* ─────────────────────────────────────────────────
DEBUG
───────────────────────────────────────────────── */
.hc-debug {
border-top: 1px solid var(--border);
padding-top: 14px;
}
.hc-debug__toggle {
font-size: 0.72rem;
color: var(--muted);
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 6px;
list-style: none;
margin-bottom: 14px;
}
.hc-debug__toggle::-webkit-details-marker {
display: none;
}
.hc-debug__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.hc-debug__cell {
background: rgba(0, 0, 0, 0.25);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.hc-debug__cell--wide {
grid-column: 1 / -1;
}
.hc-debug__label {
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--dim);
}
.hc-debug__block {
display: block;
font-size: 0.68rem;
color: var(--muted);
word-break: break-all;
}
.hc-debug__break {
word-break: break-all;
}
@media (max-width: 580px) {
.hc-debug__grid {
grid-template-columns: 1fr;
}
}
/* ─────────────────────────────────────────────────
CARDS
───────────────────────────────────────────────── */
.hc-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
@media (max-width: 860px) {
.hc-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 460px) {
.hc-cards {
grid-template-columns: 1fr;
}
}
.hc-card {
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 18px;
padding: 22px 20px 18px;
border-radius: var(--r-lg);
border: 1px solid var(--border);
background: var(--surface);
cursor: pointer;
text-align: left;
transition:
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.22s ease,
box-shadow 0.22s ease;
animation: fadeUp 0.5s ease both;
outline: none;
}
.hc-card:hover {
transform: translateY(-5px);
border-color: var(--c-border);
box-shadow:
0 16px 48px -12px color-mix(in srgb, var(--c) 18%, transparent),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.hc-card:hover .hc-card__shine {
opacity: 1;
}
.hc-card:hover .hc-card__num {
opacity: 0.07;
}
.hc-card:hover .hc-card__foot {
opacity: 1;
color: var(--c);
}
.hc-card:focus-visible {
outline: 2px solid var(--c);
outline-offset: 2px;
}
/* número decorativo */
.hc-card__num {
position: absolute;
top: 12px;
right: 16px;
font-size: 3.5rem;
font-weight: 900;
letter-spacing: -0.05em;
line-height: 1;
color: var(--text);
opacity: 0.03;
transition: opacity 0.22s ease;
pointer-events: none;
user-select: none;
}
/* shine */
.hc-card__shine {
position: absolute;
inset: 0;
border-radius: inherit;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
background: radial-gradient(ellipse at 25% 0%, color-mix(in srgb, var(--c) 10%, transparent), transparent 55%);
}
.hc-card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.hc-card__icon {
width: 38px;
height: 38px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.95rem;
background: var(--c-dim);
border: 1px solid var(--c-border);
color: var(--c);
transition: background 0.2s;
}
.hc-card:hover .hc-card__icon {
background: color-mix(in srgb, var(--c) 18%, transparent);
}
.hc-card__tag {
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--c);
opacity: 0.6;
}
.hc-card__body {
flex: 1;
}
.hc-card__title {
font-size: 1rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--text);
margin-bottom: 8px;
}
.hc-card__desc {
font-size: 0.78rem;
line-height: 1.6;
color: var(--muted);
}
.hc-card__foot {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 5px;
font-size: 0.75rem;
font-weight: 600;
color: var(--muted);
opacity: 0.6;
transition:
opacity 0.2s,
color 0.2s;
}
/* ─────────────────────────────────────────────────
PANELS (DEV sections)
───────────────────────────────────────────────── */
.hc-panel {
border: 1px solid var(--border);
border-radius: var(--r-lg);
background: var(--surface);
overflow: hidden;
animation: fadeUp 0.5s ease both;
}
.hc-panel__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
cursor: pointer;
user-select: none;
list-style: none;
border-bottom: 1px solid transparent;
transition: border-color 0.15s;
}
.hc-panel[open] .hc-panel__head {
border-bottom-color: var(--border);
}
.hc-panel__head::-webkit-details-marker {
display: none;
}
.hc-panel__title {
font-size: 0.78rem;
font-weight: 600;
color: var(--text);
opacity: 0.8;
display: flex;
align-items: center;
gap: 7px;
}
.hc-panel__title .pi {
font-size: 0.72rem;
opacity: 0.55;
}
.hc-panel__badge {
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 2px 8px;
border-radius: 100px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--muted);
}
.hc-panel__body {
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* ─────────────────────────────────────────────────
TABELA
───────────────────────────────────────────────── */
.hc-table-wrap {
overflow-x: auto;
}
.hc-table {
width: 100%;
border-collapse: collapse;
font-size: 0.75rem;
}
.hc-table thead tr {
border-bottom: 1px solid var(--border);
}
.hc-table th {
padding: 8px 14px;
text-align: left;
font-size: 0.62rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.09em;
color: var(--dim);
white-space: nowrap;
}
.hc-table td {
padding: 9px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
vertical-align: middle;
}
.hc-table tbody tr:last-child td {
border-bottom: none;
}
.hc-table tbody tr:hover {
background: rgba(255, 255, 255, 0.02);
}
.hc-table__name {
font-weight: 600;
color: var(--text);
opacity: 0.88;
}
.hc-table__code {
font-size: 0.7rem;
color: var(--muted);
background: none;
padding: 0;
}
.hc-table__mono {
font-size: 0.68rem;
color: var(--muted);
background: none;
padding: 0;
font-family: monospace;
}
.hc-table__pass {
font-size: 0.68rem;
color: #86efac;
background: none;
padding: 0;
font-family: monospace;
}
.hc-table__break {
word-break: break-all;
}
.hc-table__empty {
text-align: center;
padding: 16px !important;
color: var(--muted);
font-size: 0.72rem;
opacity: 0.6;
}
/* BADGES */
.hc-badge {
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 2px 8px;
border-radius: 100px;
}
.hc-badge--patient {
background: rgba(74, 222, 128, 0.1);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.2);
}
.hc-badge--therapist {
background: rgba(96, 165, 250, 0.1);
color: #60a5fa;
border: 1px solid rgba(96, 165, 250, 0.2);
}
.hc-badge--supervisor {
background: rgba(192, 132, 252, 0.1);
color: #c084fc;
border: 1px solid rgba(192, 132, 252, 0.2);
}
.hc-badge--clinic {
background: rgba(167, 139, 250, 0.1);
color: #a78bfa;
border: 1px solid rgba(167, 139, 250, 0.2);
}
.hc-badge--editor {
background: rgba(251, 146, 60, 0.1);
color: #fb923c;
border: 1px solid rgba(251, 146, 60, 0.2);
}
.hc-badge--saas {
background: rgba(244, 63, 94, 0.1);
color: #f43f5e;
border: 1px solid rgba(244, 63, 94, 0.2);
}
.hc-badge--portal {
background: rgba(74, 222, 128, 0.1);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.2);
}
/* EXTRAS */
.hc-extras {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.hc-extras__label {
font-size: 0.62rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--dim);
}
/* ─────────────────────────────────────────────────
BOTÕES
───────────────────────────────────────────────── */
.hc-btn {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 16px;
border-radius: 10px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.15s;
white-space: nowrap;
}
.hc-btn--primary {
background: var(--primary, #7c6af7);
color: #fff;
}
.hc-btn--primary:hover {
filter: brightness(1.12);
}
.hc-btn--ghost {
background: transparent;
color: var(--muted);
border: 1px solid var(--border);
}
.hc-btn--ghost:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--text);
}
.hc-btn--xs {
padding: 4px 10px;
font-size: 0.7rem;
border-radius: 7px;
background: rgba(255, 255, 255, 0.05);
color: var(--muted);
border: 1px solid var(--border);
}
.hc-btn--xs:hover {
background: rgba(255, 255, 255, 0.08);
}
.hc-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.hc-quick {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
border-radius: 8px;
font-size: 0.72rem;
font-weight: 600;
cursor: pointer;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
color: var(--muted);
transition: all 0.15s;
white-space: nowrap;
}
.hc-quick:hover {
background: rgba(124, 106, 247, 0.12);
border-color: rgba(124, 106, 247, 0.3);
color: #a78bfa;
}
.hc-quick--sm {
padding: 3px 10px;
font-size: 0.68rem;
}
/* ─────────────────────────────────────────────────
FOOTER
───────────────────────────────────────────────── */
.hc-footer {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 0.7rem;
color: var(--dim);
padding-top: 8px;
}
.hc-footer__psi {
font-weight: 700;
color: var(--primary);
opacity: 0.4;
}
/* ─────────────────────────────────────────────────
ANIMAÇÃO
───────────────────────────────────────────────── */
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>