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
+396 -454
View File
@@ -15,538 +15,480 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useToast } from 'primevue/usetoast'
import { supabase } from '@/lib/supabase/client'
import { computed, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import Password from 'primevue/password'
import Chip from 'primevue/chip'
import Message from 'primevue/message'
import ProgressSpinner from 'primevue/progressspinner'
import Password from 'primevue/password';
import Chip from 'primevue/chip';
import Message from 'primevue/message';
import ProgressSpinner from 'primevue/progressspinner';
const route = useRoute()
const router = useRouter()
const toast = useToast()
const route = useRoute();
const router = useRouter();
const toast = useToast();
// ============================
// Form
// ============================
const email = ref('')
const password = ref('')
const loading = ref(false)
const email = ref('');
const password = ref('');
const loading = ref(false);
// validação simples (sem "viajar")
const emailOk = computed(() => /\S+@\S+\.\S+/.test(String(email.value || '').trim()))
const passwordOk = computed(() => String(password.value || '').length >= 6)
const canSubmit = computed(() => !loading.value && emailOk.value && passwordOk.value)
const emailOk = computed(() => /\S+@\S+\.\S+/.test(String(email.value || '').trim()));
const passwordOk = computed(() => String(password.value || '').length >= 6);
const canSubmit = computed(() => !loading.value && emailOk.value && passwordOk.value);
// ============================
// Query (plan / interval)
// ============================
const planFromQuery = computed(() => String(route.query.plan || '').trim().toLowerCase())
const intervalFromQuery = computed(() => String(route.query.interval || '').trim().toLowerCase())
const planFromQuery = computed(() =>
String(route.query.plan || '')
.trim()
.toLowerCase()
);
const intervalFromQuery = computed(() =>
String(route.query.interval || '')
.trim()
.toLowerCase()
);
function normalizeInterval (v) {
if (v === 'monthly') return 'month'
if (v === 'annual' || v === 'annually' || v === 'yearly') return 'year'
return v
function normalizeInterval(v) {
if (v === 'monthly') return 'month';
if (v === 'annual' || v === 'annually' || v === 'yearly') return 'year';
return v;
}
const intervalNormalized = computed(() => normalizeInterval(intervalFromQuery.value))
const intervalNormalized = computed(() => normalizeInterval(intervalFromQuery.value));
function isValidInterval (v) {
return v === 'month' || v === 'year'
function isValidInterval(v) {
return v === 'month' || v === 'year';
}
const hasPlanQuery = computed(() => !!planFromQuery.value && isValidInterval(intervalNormalized.value))
const hasPlanQuery = computed(() => !!planFromQuery.value && isValidInterval(intervalNormalized.value));
const intervalLabel = computed(() => {
if (intervalNormalized.value === 'year') return 'Anual'
if (intervalNormalized.value === 'month') return 'Mensal'
return ''
})
if (intervalNormalized.value === 'year') return 'Anual';
if (intervalNormalized.value === 'month') return 'Mensal';
return '';
});
// ============================
// Fetch pricing from v_public_pricing
// ============================
const selectedPlanRow = ref(null)
const pricingLoading = ref(false)
const selectedPlanRow = ref(null);
const pricingLoading = ref(false);
const selectedPlanName = computed(() => selectedPlanRow.value?.public_name || selectedPlanRow.value?.plan_name || null)
const selectedBadge = computed(() => selectedPlanRow.value?.badge || null)
const selectedDescription = computed(() => selectedPlanRow.value?.public_description || null)
const selectedPlanName = computed(() => selectedPlanRow.value?.public_name || selectedPlanRow.value?.plan_name || null);
const selectedBadge = computed(() => selectedPlanRow.value?.badge || null);
const selectedDescription = computed(() => selectedPlanRow.value?.public_description || null);
const bullets = computed(() => {
const b = selectedPlanRow.value?.bullets
return Array.isArray(b) ? b : []
})
const b = selectedPlanRow.value?.bullets;
return Array.isArray(b) ? b : [];
});
function amountForInterval (row, interval) {
if (!row) return null
const cents = interval === 'year' ? row.yearly_cents : row.monthly_cents
// fallback (se não existir preço no intervalo escolhido)
if (cents == null) return interval === 'year' ? row.monthly_cents : row.yearly_cents
return cents
function amountForInterval(row, interval) {
if (!row) return null;
const cents = interval === 'year' ? row.yearly_cents : row.monthly_cents;
// fallback (se não existir preço no intervalo escolhido)
if (cents == null) return interval === 'year' ? row.monthly_cents : row.yearly_cents;
return cents;
}
function currencyForInterval (row, interval) {
if (!row) return 'BRL'
const cur = interval === 'year' ? (row.yearly_currency || 'BRL') : (row.monthly_currency || 'BRL')
return cur || 'BRL'
function currencyForInterval(row, interval) {
if (!row) return 'BRL';
const cur = interval === 'year' ? row.yearly_currency || 'BRL' : row.monthly_currency || 'BRL';
return cur || 'BRL';
}
const amountCents = computed(() => amountForInterval(selectedPlanRow.value, intervalNormalized.value))
const currency = computed(() => currencyForInterval(selectedPlanRow.value, intervalNormalized.value))
const amountCents = computed(() => amountForInterval(selectedPlanRow.value, intervalNormalized.value));
const currency = computed(() => currencyForInterval(selectedPlanRow.value, intervalNormalized.value));
const formattedPrice = computed(() => {
if (amountCents.value == null) return null
try {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: currency.value || 'BRL' })
.format(Number(amountCents.value) / 100)
} catch {
return null
}
})
if (amountCents.value == null) return null;
try {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: currency.value || 'BRL' }).format(Number(amountCents.value) / 100);
} catch {
return null;
}
});
const showPlanCard = computed(() => hasPlanQuery.value && !!selectedPlanRow.value)
const showPlanCard = computed(() => hasPlanQuery.value && !!selectedPlanRow.value);
async function loadSelectedPlanRow () {
selectedPlanRow.value = null
if (!planFromQuery.value) return
async function loadSelectedPlanRow() {
selectedPlanRow.value = null;
if (!planFromQuery.value) return;
pricingLoading.value = true
try {
const { data, error } = await supabase
.from('v_public_pricing')
.select(
'plan_key, plan_name, public_name, public_description, badge, is_featured, is_visible, monthly_cents, yearly_cents, monthly_currency, yearly_currency, bullets'
)
.eq('plan_key', planFromQuery.value)
.eq('is_visible', true)
.maybeSingle()
pricingLoading.value = true;
try {
const { data, error } = await supabase
.from('v_public_pricing')
.select('plan_key, plan_name, public_name, public_description, badge, is_featured, is_visible, monthly_cents, yearly_cents, monthly_currency, yearly_currency, bullets')
.eq('plan_key', planFromQuery.value)
.eq('is_visible', true)
.maybeSingle();
if (error) throw error
if (!data) return
selectedPlanRow.value = data
} catch (err) {
console.error('[Signup] loadSelectedPlanRow:', err)
} finally {
pricingLoading.value = false
}
if (error) throw error;
if (!data) return;
selectedPlanRow.value = data;
} catch (err) {
console.error('[Signup] loadSelectedPlanRow:', err);
} finally {
pricingLoading.value = false;
}
}
onMounted(loadSelectedPlanRow)
onMounted(loadSelectedPlanRow);
watch(
() => [planFromQuery.value, intervalNormalized.value],
() => loadSelectedPlanRow()
)
() => [planFromQuery.value, intervalNormalized.value],
() => loadSelectedPlanRow()
);
// ============================
// subscription_intent (MODELO B: tenant)
// ============================
async function getActiveTenantIdForUser (userId) {
const { data, error } = await supabase
.from('tenant_members')
.select('tenant_id')
.eq('user_id', userId)
.eq('status', 'active')
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle()
async function getActiveTenantIdForUser(userId) {
const { data, error } = await supabase.from('tenant_members').select('tenant_id').eq('user_id', userId).eq('status', 'active').order('created_at', { ascending: false }).limit(1).maybeSingle();
if (error) throw error
return data?.tenant_id || null
if (error) throw error;
return data?.tenant_id || null;
}
async function createSubscriptionIntentAfterSignup (userId, preferredTenantId = null) {
if (!hasPlanQuery.value) return
if (!selectedPlanRow.value) return
if (amountCents.value == null) return
async function createSubscriptionIntentAfterSignup(userId, preferredTenantId = null) {
if (!hasPlanQuery.value) return;
if (!selectedPlanRow.value) return;
if (amountCents.value == null) return;
const tenantId = preferredTenantId || (await getActiveTenantIdForUser(userId))
if (!tenantId) throw new Error('Não encontrei tenant ativo para este usuário.')
const tenantId = preferredTenantId || (await getActiveTenantIdForUser(userId));
if (!tenantId) throw new Error('Não encontrei tenant ativo para este usuário.');
const payload = {
tenant_id: tenantId,
created_by_user_id: userId,
const payload = {
tenant_id: tenantId,
created_by_user_id: userId,
// opcional (se sua tabela ainda tem user_id)
user_id: userId,
// opcional (se sua tabela ainda tem user_id)
user_id: userId,
email: String(email.value || '').trim().toLowerCase() || null,
plan_key: selectedPlanRow.value.plan_key,
interval: intervalNormalized.value,
amount_cents: amountCents.value,
currency: currency.value || 'BRL',
status: 'new',
source: 'landing'
}
email:
String(email.value || '')
.trim()
.toLowerCase() || null,
plan_key: selectedPlanRow.value.plan_key,
interval: intervalNormalized.value,
amount_cents: amountCents.value,
currency: currency.value || 'BRL',
status: 'new',
source: 'landing'
};
const { error } = await supabase.from('subscription_intents').insert(payload)
if (error) throw error
const { error } = await supabase.from('subscription_intents').insert(payload);
if (error) throw error;
}
// ============================
// Nav
// ============================
function goLogin () {
router.push({
path: '/auth/login',
query: email.value ? { email: String(email.value).trim() } : undefined
})
function goLogin() {
router.push({
path: '/auth/login',
query: email.value ? { email: String(email.value).trim() } : undefined
});
}
function goBackPricing () {
// você usa /lp#pricing — mantive
router.push('/lp#pricing')
function goBackPricing() {
// você usa /lp#pricing — mantive
router.push('/lp#pricing');
}
// ============================
// Signup
// ============================
async function onSignup () {
if (!canSubmit.value) return
async function onSignup() {
if (!canSubmit.value) return;
loading.value = true
try {
const cleanEmail = String(email.value || '').trim().toLowerCase()
loading.value = true;
try {
const cleanEmail = String(email.value || '')
.trim()
.toLowerCase();
const { data, error } = await supabase.auth.signUp({
email: cleanEmail,
password: password.value
})
const { data, error } = await supabase.auth.signUp({
email: cleanEmail,
password: password.value
});
if (error) throw error
if (error) throw error;
const userId = data?.user?.id || null
const userId = data?.user?.id || null;
// ✅ Modelo B: garante tenant pessoal (não aborta se falhar)
let tenantId = null
if (userId) {
try {
const resTenant = await supabase.rpc('ensure_personal_tenant')
tenantId = resTenant?.data || null
} catch (e) {
console.warn('[Signup] ensure_personal_tenant falhou:', e)
}
// ✅ Modelo B: garante tenant pessoal (não aborta se falhar)
let tenantId = null;
if (userId) {
try {
const resTenant = await supabase.rpc('ensure_personal_tenant');
tenantId = resTenant?.data || null;
} catch (e) {
console.warn('[Signup] ensure_personal_tenant falhou:', e);
}
// ✅ intent (não quebra signup se falhar)
try {
await createSubscriptionIntentAfterSignup(userId, tenantId);
} catch (e) {
console.error('[Signup] subscription_intent failed:', e);
toast.add({
severity: 'warn',
summary: 'Conta criada',
detail: 'Não consegui registrar a intenção do plano. Você pode seguir normalmente.',
life: 4500
});
}
}
// ✅ intent (não quebra signup se falhar)
try {
await createSubscriptionIntentAfterSignup(userId, tenantId)
} catch (e) {
console.error('[Signup] subscription_intent failed:', e)
toast.add({
severity: 'warn',
summary: 'Conta criada',
detail: 'Não consegui registrar a intenção do plano. Você pode seguir normalmente.',
life: 4500
})
}
severity: 'success',
summary: 'Conta criada',
detail: 'Agora vamos para os próximos passos.',
life: 2500
});
router.push({
path: '/auth/welcome',
query: {
plan: planFromQuery.value || undefined,
interval: intervalNormalized.value || undefined
}
});
} catch (err) {
console.error(err);
const msg = String(err?.message || '');
const isAlreadyRegistered = err?.name === 'AuthApiError' && /User already registered/i.test(msg);
if (isAlreadyRegistered) {
toast.add({
severity: 'warn',
summary: 'Esse email já tem conta',
detail: 'Faça login para continuar.',
life: 4500
});
goLogin();
return;
}
toast.add({
severity: 'error',
summary: 'Erro ao criar conta',
detail: err?.message || 'Tente novamente.',
life: 4500
});
} finally {
loading.value = false;
}
toast.add({
severity: 'success',
summary: 'Conta criada',
detail: 'Agora vamos para os próximos passos.',
life: 2500
})
router.push({
path: '/auth/welcome',
query: {
plan: planFromQuery.value || undefined,
interval: intervalNormalized.value || undefined
}
})
} catch (err) {
console.error(err)
const msg = String(err?.message || '')
const isAlreadyRegistered = err?.name === 'AuthApiError' && /User already registered/i.test(msg)
if (isAlreadyRegistered) {
toast.add({
severity: 'warn',
summary: 'Esse email já tem conta',
detail: 'Faça login para continuar.',
life: 4500
})
goLogin()
return
}
toast.add({
severity: 'error',
summary: 'Erro ao criar conta',
detail: err?.message || 'Tente novamente.',
life: 4500
})
} finally {
loading.value = false
}
}
</script>
<template>
<div class="min-h-screen bg-[var(--surface-ground)] text-[var(--text-color)] flex items-center justify-center p-4">
<!-- fundo suave (noir glow) -->
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="absolute -top-28 -left-28 h-96 w-96 rounded-full blur-3xl opacity-60 bg-indigo-400/10" />
<div class="absolute top-24 -right-24 h-[28rem] w-[28rem] rounded-full blur-3xl opacity-60 bg-emerald-400/10" />
<div class="absolute -bottom-40 left-1/3 h-[34rem] w-[34rem] rounded-full blur-3xl opacity-60 bg-fuchsia-400/10" />
</div>
<div class="relative w-full max-w-6xl">
<div class="rounded-3xl border border-[var(--surface-border)] bg-[var(--surface-card)] shadow-sm overflow-hidden">
<div class="grid grid-cols-12">
<!-- LEFT -->
<div class="col-span-12 lg:col-span-6 p-6 md:p-10 bg-[color-mix(in_srgb,var(--surface-card),transparent_6%)] border-b lg:border-b-0 lg:border-r border-[var(--surface-border)]">
<div class="flex items-center gap-3">
<div class="h-11 w-11 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center">
<i class="pi pi-sparkles opacity-80 text-lg" />
</div>
<div class="min-w-0">
<div class="font-semibold leading-tight truncate">Psi Quasar</div>
<div class="text-xs text-[var(--text-color-secondary)] truncate">Gestão clínica sem ruído.</div>
</div>
</div>
<Divider class="my-6" />
<div class="text-3xl md:text-4xl font-semibold leading-tight">
Menos dispersão. Mais presença.
</div>
<div class="mt-3 text-sm md:text-base text-[var(--text-color-secondary)] max-w-lg leading-relaxed">
Crie sua conta e siga para o pagamento manual (PIX/boleto). Assim que confirmado, seu plano é ativado
e as funcionalidades liberadas.
</div>
<div class="mt-6 grid grid-cols-12 gap-3">
<div class="col-span-12 md:col-span-6">
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<div class="text-xs text-[var(--text-color-secondary)]">Agenda</div>
<div class="text-xl font-semibold mt-1">Organizada</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-1">encaixes, bloqueios e visão clara</div>
</div>
</div>
<div class="col-span-12 md:col-span-6">
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<div class="text-xs text-[var(--text-color-secondary)]">Financeiro</div>
<div class="text-xl font-semibold mt-1">Respirável</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-1">sem planilhas espalhadas</div>
</div>
</div>
<div class="col-span-12">
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-xs text-[var(--text-color-secondary)]">Prontuário</div>
<div class="font-semibold mt-1">Histórico por sessão</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-1">linha do tempo por paciente</div>
</div>
<i class="pi pi-file-edit opacity-60" />
</div>
</div>
</div>
</div>
<div class="mt-6 flex flex-wrap gap-2">
<Tag severity="secondary" value="Agenda online (PRO)" />
<Tag severity="secondary" value="Controle de sessões" />
<Tag severity="secondary" value="Financeiro integrado" />
<Tag severity="secondary" value="Clínica / multi-profissional" />
</div>
<div class="mt-6 text-xs text-[var(--text-color-secondary)]">
* Painel conceitual inspirado em layouts PrimeBlocks.
</div>
</div>
<!-- RIGHT -->
<div class="col-span-12 lg:col-span-6 p-6 md:p-10">
<div class="max-w-md mx-auto">
<div class="text-2xl font-semibold">Criar conta</div>
<div class="text-sm text-[var(--text-color-secondary)] mt-1">
tem conta?
<a class="cursor-pointer font-medium" @click.prevent="goLogin">Entrar</a>
</div>
<!-- Plano -->
<div class="mt-5">
<div v-if="pricingLoading" class="flex items-center gap-2 text-sm text-[var(--text-color-secondary)]">
<ProgressSpinner style="width: 18px; height: 18px" strokeWidth="4" />
Carregando plano
</div>
<Card v-else-if="showPlanCard" class="overflow-hidden rounded-[2rem]">
<template #content>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-xs text-[var(--text-color-secondary)]">Plano selecionado</div>
<div class="mt-1 flex items-center gap-2 flex-wrap">
<div class="text-lg font-semibold truncate">
{{ selectedPlanName }}
</div>
<Tag v-if="selectedPlanRow?.is_featured" severity="success" value="Popular" />
<Tag v-if="selectedBadge" severity="secondary" :value="selectedBadge" />
<Chip v-if="intervalLabel" :label="intervalLabel" />
</div>
<div class="mt-2 text-2xl font-semibold leading-none">
{{ formattedPrice || '—' }}
<span class="text-sm font-normal text-[var(--text-color-secondary)]">
/{{ intervalNormalized === 'month' ? 'mês' : 'ano' }}
</span>
</div>
<div v-if="selectedDescription" class="mt-2 text-sm text-[var(--text-color-secondary)] leading-relaxed">
{{ selectedDescription }}
</div>
</div>
<Button
icon="pi pi-pencil"
label="Trocar"
severity="secondary"
text
rounded
aria-label="Trocar plano"
@click="goBackPricing"
/>
</div>
<Divider class="my-4" />
<ul v-if="bullets.length" class="space-y-2 text-sm">
<li v-for="b in bullets" :key="b.id ?? b.text" class="flex items-start gap-2">
<i class="pi pi-check mt-1 text-emerald-500"></i>
<span :class="b.highlight ? 'font-semibold' : 'text-[var(--text-color-secondary)]'">
{{ b.text }}
</span>
</li>
</ul>
<Message v-else severity="info" class="mt-2">
Benefícios ainda não cadastrados para esse plano.
</Message>
</template>
</Card>
<Message v-else-if="hasPlanQuery && !selectedPlanRow" severity="warn" class="mb-0">
Não encontrei esse plano na vitrine pública. Você ainda pode criar a conta normalmente.
<div class="mt-2">
<Button
label="Ver planos"
icon="pi pi-credit-card"
severity="secondary"
outlined
class="w-full"
@click="goBackPricing"
/>
</div>
</Message>
<Message v-else severity="info" class="mb-0">
Você está criando a conta sem seleção de plano.
<div class="mt-2">
<Button
label="Ver planos"
icon="pi pi-credit-card"
severity="secondary"
outlined
class="w-full"
@click="goBackPricing"
/>
</div>
</Message>
<Message
v-if="hasPlanQuery && selectedPlanRow && amountCents == null && !pricingLoading"
severity="warn"
class="mt-3"
>
Esse plano não tem preço configurado para {{ intervalLabel }}. Você ainda pode criar a conta normalmente.
</Message>
</div>
<Divider class="my-6" />
<!-- Form -->
<div class="space-y-4">
<div>
<FloatLabel variant="on">
<InputText
id="signup_email"
v-model="email"
class="w-full"
autocomplete="email"
:disabled="loading"
@keydown.enter.prevent="onSignup"
/>
<label for="signup_email">Seu melhor e-mail</label>
</FloatLabel>
<div v-if="email && !emailOk" class="mt-2 text-xs text-orange-600">
Informe um e-mail válido.
</div>
</div>
<div>
<FloatLabel variant="on">
<Password
v-model="password"
inputId="signup_password"
toggleMask
:feedback="true"
autocomplete="new-password"
:disabled="loading"
@keydown.enter.prevent="onSignup"
:pt="{
root: { class: 'w-full' },
input: { class: 'w-full' }
}"
/>
<label for="signup_password">Senha</label>
</FloatLabel>
<div v-if="password && !passwordOk" class="mt-2 text-xs text-orange-600">
Use pelo menos 6 caracteres.
</div>
</div>
<Button
label="CRIAR CONTA"
class="w-full"
severity="success"
:loading="loading"
:disabled="!canSubmit"
icon="pi pi-arrow-right"
@click="onSignup"
/>
<div class="text-xs text-center text-[var(--text-color-secondary)]">
Ao criar a conta, registramos sua intenção de assinatura.
Pagamento é manual (PIX/boleto) por enquanto.
</div>
<div class="text-xs text-center">
<a class="cursor-pointer text-[var(--text-color-secondary)] hover:underline" @click.prevent="goLogin">
tenho conta entrar
</a>
</div>
</div>
</div>
</div>
<div class="min-h-screen bg-[var(--surface-ground)] text-[var(--text-color)] flex items-center justify-center p-4">
<!-- fundo suave (noir glow) -->
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div class="absolute -top-28 -left-28 h-96 w-96 rounded-full blur-3xl opacity-60 bg-indigo-400/10" />
<div class="absolute top-24 -right-24 h-[28rem] w-[28rem] rounded-full blur-3xl opacity-60 bg-emerald-400/10" />
<div class="absolute -bottom-40 left-1/3 h-[34rem] w-[34rem] rounded-full blur-3xl opacity-60 bg-fuchsia-400/10" />
</div>
</div>
<div class="text-center text-xs text-[var(--text-color-secondary)] mt-4">
Agência PSI gestão clínica sem ruído.
</div>
<div class="relative w-full max-w-6xl">
<div class="rounded-3xl border border-[var(--surface-border)] bg-[var(--surface-card)] shadow-sm overflow-hidden">
<div class="grid grid-cols-12">
<!-- LEFT -->
<div class="col-span-12 lg:col-span-6 p-6 md:p-10 bg-[color-mix(in_srgb,var(--surface-card),transparent_6%)] border-b lg:border-b-0 lg:border-r border-[var(--surface-border)]">
<div class="flex items-center gap-3">
<div class="h-11 w-11 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] grid place-items-center">
<i class="pi pi-sparkles opacity-80 text-lg" />
</div>
<div class="min-w-0">
<div class="font-semibold leading-tight truncate">Psi Quasar</div>
<div class="text-xs text-[var(--text-color-secondary)] truncate">Gestão clínica sem ruído.</div>
</div>
</div>
<Divider class="my-6" />
<div class="text-3xl md:text-4xl font-semibold leading-tight">Menos dispersão. Mais presença.</div>
<div class="mt-3 text-sm md:text-base text-[var(--text-color-secondary)] max-w-lg leading-relaxed">
Crie sua conta e siga para o pagamento manual (PIX/boleto). Assim que confirmado, seu plano é ativado e as funcionalidades liberadas.
</div>
<div class="mt-6 grid grid-cols-12 gap-3">
<div class="col-span-12 md:col-span-6">
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<div class="text-xs text-[var(--text-color-secondary)]">Agenda</div>
<div class="text-xl font-semibold mt-1">Organizada</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-1">encaixes, bloqueios e visão clara</div>
</div>
</div>
<div class="col-span-12 md:col-span-6">
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<div class="text-xs text-[var(--text-color-secondary)]">Financeiro</div>
<div class="text-xl font-semibold mt-1">Respirável</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-1">sem planilhas espalhadas</div>
</div>
</div>
<div class="col-span-12">
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-ground)] p-4">
<div class="flex items-start justify-between gap-3">
<div>
<div class="text-xs text-[var(--text-color-secondary)]">Prontuário</div>
<div class="font-semibold mt-1">Histórico por sessão</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-1">linha do tempo por paciente</div>
</div>
<i class="pi pi-file-edit opacity-60" />
</div>
</div>
</div>
</div>
<div class="mt-6 flex flex-wrap gap-2">
<Tag severity="secondary" value="Agenda online (PRO)" />
<Tag severity="secondary" value="Controle de sessões" />
<Tag severity="secondary" value="Financeiro integrado" />
<Tag severity="secondary" value="Clínica / multi-profissional" />
</div>
<div class="mt-6 text-xs text-[var(--text-color-secondary)]">* Painel conceitual inspirado em layouts PrimeBlocks.</div>
</div>
<!-- RIGHT -->
<div class="col-span-12 lg:col-span-6 p-6 md:p-10">
<div class="max-w-md mx-auto">
<div class="text-2xl font-semibold">Criar conta</div>
<div class="text-sm text-[var(--text-color-secondary)] mt-1">
tem conta?
<a class="cursor-pointer font-medium" @click.prevent="goLogin">Entrar</a>
</div>
<!-- Plano -->
<div class="mt-5">
<div v-if="pricingLoading" class="flex items-center gap-2 text-sm text-[var(--text-color-secondary)]">
<ProgressSpinner style="width: 18px; height: 18px" strokeWidth="4" />
Carregando plano
</div>
<Card v-else-if="showPlanCard" class="overflow-hidden rounded-[2rem]">
<template #content>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<div class="text-xs text-[var(--text-color-secondary)]">Plano selecionado</div>
<div class="mt-1 flex items-center gap-2 flex-wrap">
<div class="text-lg font-semibold truncate">
{{ selectedPlanName }}
</div>
<Tag v-if="selectedPlanRow?.is_featured" severity="success" value="Popular" />
<Tag v-if="selectedBadge" severity="secondary" :value="selectedBadge" />
<Chip v-if="intervalLabel" :label="intervalLabel" />
</div>
<div class="mt-2 text-2xl font-semibold leading-none">
{{ formattedPrice || '—' }}
<span class="text-sm font-normal text-[var(--text-color-secondary)]"> /{{ intervalNormalized === 'month' ? 'mês' : 'ano' }} </span>
</div>
<div v-if="selectedDescription" class="mt-2 text-sm text-[var(--text-color-secondary)] leading-relaxed">
{{ selectedDescription }}
</div>
</div>
<Button icon="pi pi-pencil" label="Trocar" severity="secondary" text rounded aria-label="Trocar plano" @click="goBackPricing" />
</div>
<Divider class="my-4" />
<ul v-if="bullets.length" class="space-y-2 text-sm">
<li v-for="b in bullets" :key="b.id ?? b.text" class="flex items-start gap-2">
<i class="pi pi-check mt-1 text-emerald-500"></i>
<span :class="b.highlight ? 'font-semibold' : 'text-[var(--text-color-secondary)]'">
{{ b.text }}
</span>
</li>
</ul>
<Message v-else severity="info" class="mt-2"> Benefícios ainda não cadastrados para esse plano. </Message>
</template>
</Card>
<Message v-else-if="hasPlanQuery && !selectedPlanRow" severity="warn" class="mb-0">
Não encontrei esse plano na vitrine pública. Você ainda pode criar a conta normalmente.
<div class="mt-2">
<Button label="Ver planos" icon="pi pi-credit-card" severity="secondary" outlined class="w-full" @click="goBackPricing" />
</div>
</Message>
<Message v-else severity="info" class="mb-0">
Você está criando a conta sem seleção de plano.
<div class="mt-2">
<Button label="Ver planos" icon="pi pi-credit-card" severity="secondary" outlined class="w-full" @click="goBackPricing" />
</div>
</Message>
<Message v-if="hasPlanQuery && selectedPlanRow && amountCents == null && !pricingLoading" severity="warn" class="mt-3">
Esse plano não tem preço configurado para {{ intervalLabel }}. Você ainda pode criar a conta normalmente.
</Message>
</div>
<Divider class="my-6" />
<!-- Form -->
<div class="space-y-4">
<div>
<FloatLabel variant="on">
<InputText id="signup_email" v-model="email" class="w-full" autocomplete="email" :disabled="loading" @keydown.enter.prevent="onSignup" />
<label for="signup_email">Seu melhor e-mail</label>
</FloatLabel>
<div v-if="email && !emailOk" class="mt-2 text-xs text-orange-600">Informe um e-mail válido.</div>
</div>
<div>
<FloatLabel variant="on">
<Password
v-model="password"
inputId="signup_password"
toggleMask
:feedback="true"
autocomplete="new-password"
:disabled="loading"
@keydown.enter.prevent="onSignup"
:pt="{
root: { class: 'w-full' },
input: { class: 'w-full' }
}"
/>
<label for="signup_password">Senha</label>
</FloatLabel>
<div v-if="password && !passwordOk" class="mt-2 text-xs text-orange-600">Use pelo menos 6 caracteres.</div>
</div>
<Button label="CRIAR CONTA" class="w-full" severity="success" :loading="loading" :disabled="!canSubmit" icon="pi pi-arrow-right" @click="onSignup" />
<div class="text-xs text-center text-[var(--text-color-secondary)]">Ao criar a conta, registramos sua intenção de assinatura. Pagamento é manual (PIX/boleto) por enquanto.</div>
<div class="text-xs text-center">
<a class="cursor-pointer text-[var(--text-color-secondary)] hover:underline" @click.prevent="goLogin"> tenho conta entrar </a>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="text-center text-xs text-[var(--text-color-secondary)] mt-4">Agência PSI gestão clínica sem ruído.</div>
</div>
</div>
</div>
</template>
</template>