358 lines
13 KiB
Vue
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ê já 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> |