27467bbb68
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>
1510 lines
49 KiB
Vue
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>
|