Files
agenciapsilmno/src/views/pages/auth/Welcome.vue
T

358 lines
13 KiB
Vue

<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/views/pages/auth/Welcome.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { supabase } from '@/lib/supabase/client'
import Message from 'primevue/message'
import Chip from 'primevue/chip'
import ProgressSpinner from 'primevue/progressspinner'
const route = useRoute()
const router = useRouter()
// ============================
// Query
// ============================
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 === 'yearly') return 'year'
return v
}
const intervalNormalized = computed(() => normalizeInterval(intervalFromQuery.value))
const intervalLabel = computed(() => {
if (intervalNormalized.value === 'year') return 'Anual'
if (intervalNormalized.value === 'month') return 'Mensal'
return ''
})
const hasPlanQuery = computed(() => !!planFromQuery.value)
// ============================
// Session (opcional para CTA melhor)
// ============================
const hasSession = ref(false)
async function checkSession () {
try {
const { data } = await supabase.auth.getSession()
hasSession.value = !!data?.session
} catch {
hasSession.value = false
}
}
// ============================
// Pricing
// ============================
const loading = ref(false)
const planRow = ref(null)
const planName = computed(() => planRow.value?.public_name || planRow.value?.plan_name || null)
const planDescription = computed(() => planRow.value?.public_description || null)
function amountForInterval (row, interval) {
if (!row) return null
const cents = interval === 'year' ? row.yearly_cents : row.monthly_cents
// fallback inteligente: se não houver preço nesse intervalo, tenta o outro
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'
}
const amountCents = computed(() => amountForInterval(planRow.value, intervalNormalized.value))
const currency = computed(() => currencyForInterval(planRow.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
}
})
async function loadPlan () {
planRow.value = null
if (!planFromQuery.value) return
loading.value = true
try {
const { data, error } = await supabase
.from('v_public_pricing')
.select(`
plan_key,
plan_name,
public_name,
public_description,
badge,
is_featured,
monthly_cents,
yearly_cents,
monthly_currency,
yearly_currency,
is_visible
`)
.eq('plan_key', planFromQuery.value)
.eq('is_visible', true)
.maybeSingle()
if (error) throw error
if (data) planRow.value = data
} catch (err) {
console.error('[Welcome] loadPlan:', err)
} finally {
loading.value = false
}
}
function goLogin () {
router.push('/auth/login')
}
function goBackLanding () {
router.push('/lp')
}
function goDashboard () {
router.push('/admin')
}
function goPricing () {
router.push('/lp#pricing')
}
onMounted(async () => {
await checkSession()
await loadPlan()
})
watch(
() => planFromQuery.value,
() => loadPlan()
)
</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">
Bem-vindo(a).
</div>
<div class="mt-3 text-sm md:text-base text-[var(--text-color-secondary)] max-w-lg leading-relaxed">
Sua conta foi criada e sua intenção de assinatura foi registrada.
Agora o caminho é simples: instruções de pagamento confirmação ativação do plano.
</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)]">1) Pagamento</div>
<div class="text-xl font-semibold mt-1">Manual</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-1">PIX ou boleto</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)]">2) Confirmação</div>
<div class="text-xl font-semibold mt-1">Rápida</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-1">verificação e liberação</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)]">3) Plano ativo</div>
<div class="font-semibold mt-1">Recursos liberados</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-1">
entitlements PRO quando confirmado
</div>
</div>
<i class="pi pi-verified opacity-60" />
</div>
</div>
</div>
</div>
<div class="mt-6 flex flex-wrap gap-2">
<Tag severity="secondary" value="Sem cobrança automática" />
<Tag severity="secondary" value="Ativação após confirmação" />
<Tag severity="secondary" value="Gateway depois, sem retrabalho" />
</div>
<div class="mt-6 text-xs text-[var(--text-color-secondary)]">
* Boas-vindas inspirada 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">Conta criada 🎉</div>
<div class="text-sm text-[var(--text-color-secondary)] mt-1 leading-relaxed">
Você pode entrar. Se o seu plano for PRO, ele será ativado após confirmação do pagamento.
</div>
<div class="mt-5">
<Message severity="success" class="mb-3">
Intenção de assinatura registrada.
</Message>
<div v-if="loading" class="flex items-center gap-2 text-sm text-[var(--text-color-secondary)]">
<ProgressSpinner style="width: 18px; height: 18px" strokeWidth="4" />
Carregando detalhes do plano
</div>
<Card v-else 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)]">Resumo</div>
<div v-if="hasPlanQuery" class="mt-1 flex items-center gap-2 flex-wrap">
<div class="text-lg font-semibold truncate">
{{ planName || 'Plano' }}
</div>
<Tag v-if="planRow?.is_featured" severity="success" value="Popular" />
<Tag v-if="planRow?.badge" severity="secondary" :value="planRow.badge" />
<Chip v-if="intervalLabel" :label="intervalLabel" />
</div>
<div v-else class="mt-1">
<div class="text-lg font-semibold">Sem plano selecionado</div>
<div class="text-sm text-[var(--text-color-secondary)] mt-1">
Você pode escolher um plano agora ou seguir no FREE.
</div>
</div>
<div v-if="hasPlanQuery" class="mt-2 text-2xl font-semibold leading-none">
{{ formattedPrice || '' }}
<span v-if="intervalLabel" class="text-sm font-normal text-[var(--text-color-secondary)]">
/{{ intervalNormalized === 'month' ? 'mês' : 'ano' }}
</span>
</div>
<div v-if="planDescription" class="mt-2 text-sm text-[var(--text-color-secondary)] leading-relaxed">
{{ planDescription }}
</div>
<Message v-if="hasPlanQuery && !planRow" severity="warn" class="mt-3">
Não encontrei esse plano na vitrine pública. Você pode continuar normalmente.
</Message>
</div>
</div>
<Divider class="my-4" />
<Message severity="info" class="mb-0">
Próximo passo: você receberá instruções de pagamento (PIX/boleto).
Assim que confirmado, sua assinatura será ativada.
</Message>
<div v-if="!hasPlanQuery" class="mt-3">
<Button
label="Escolher um plano"
icon="pi pi-credit-card"
severity="secondary"
outlined
class="w-full"
@click="goPricing"
/>
</div>
</template>
</Card>
</div>
<div class="mt-5 gap-2">
<Button
v-if="hasSession"
label="Ir para o painel"
class="w-full mb-2"
icon="pi pi-arrow-right"
@click="goDashboard"
/>
<Button
v-else
label="Ir para login"
class="w-full mb-2"
icon="pi pi-sign-in"
@click="goLogin"
/>
<Button
label="Voltar para a página inicial"
severity="secondary"
outlined
class="w-full"
icon="pi pi-arrow-left"
@click="goBackLanding"
/>
</div>
<div class="text-center text-xs text-[var(--text-color-secondary)] mt-4">
Psi Quasar gestão clínica sem ruído.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>