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
File diff suppressed because it is too large Load Diff
+250 -317
View File
@@ -15,365 +15,298 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useRouter } from 'vue-router';
import Password from 'primevue/password'
import { useToast } from 'primevue/usetoast'
import Password from 'primevue/password';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client'
import { supabase } from '@/lib/supabase/client';
const toast = useToast()
const router = useRouter()
const toast = useToast();
const router = useRouter();
const newPassword = ref('')
const confirmPassword = ref('')
const loading = ref(false)
const done = ref(false)
const newPassword = ref('');
const confirmPassword = ref('');
const loading = ref(false);
const done = ref(false);
// estado do link de recovery
const recoveryReady = ref(false)
const linkInvalid = ref(false)
const recoveryReady = ref(false);
const linkInvalid = ref(false);
function isStrongEnough (p) {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(p || '')
function isStrongEnough(p) {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(p || '');
}
const strengthOk = computed(() => isStrongEnough(newPassword.value))
const matchOk = computed(() => !!confirmPassword.value && newPassword.value === confirmPassword.value)
const strengthOk = computed(() => isStrongEnough(newPassword.value));
const matchOk = computed(() => !!confirmPassword.value && newPassword.value === confirmPassword.value);
const canSubmit = computed(() =>
recoveryReady.value && strengthOk.value && matchOk.value && !loading.value
)
const canSubmit = computed(() => recoveryReady.value && strengthOk.value && matchOk.value && !loading.value);
// barra de força: 04
const strengthScore = computed(() => {
const p = newPassword.value
if (!p) return 0
let s = 0
if (p.length >= 8) s++
if (/[A-Z]/.test(p)) s++
if (/[a-z]/.test(p)) s++
if (/\d/.test(p)) s++
return s
})
const p = newPassword.value;
if (!p) return 0;
let s = 0;
if (p.length >= 8) s++;
if (/[A-Z]/.test(p)) s++;
if (/[a-z]/.test(p)) s++;
if (/\d/.test(p)) s++;
return s;
});
const strengthLabel = computed(() => {
if (!newPassword.value) return ''
const labels = ['', 'Muito fraca', 'Fraca', 'Boa', 'Forte']
return labels[strengthScore.value] || 'Forte'
})
if (!newPassword.value) return '';
const labels = ['', 'Muito fraca', 'Fraca', 'Boa', 'Forte'];
return labels[strengthScore.value] || 'Forte';
});
const strengthColor = computed(() => {
const colors = ['', 'bg-red-500', 'bg-yellow-500', 'bg-blue-500', 'bg-emerald-500']
return colors[strengthScore.value] || 'bg-emerald-500'
})
const colors = ['', 'bg-red-500', 'bg-yellow-500', 'bg-blue-500', 'bg-emerald-500'];
return colors[strengthScore.value] || 'bg-emerald-500';
});
let unsubscribeFn = null
let unsubscribeFn = null;
async function syncRecoveryState () {
try {
const { data, error } = await supabase.auth.getSession()
if (error) throw error
if (data?.session) {
recoveryReady.value = true
} else {
linkInvalid.value = true
async function syncRecoveryState() {
try {
const { data, error } = await supabase.auth.getSession();
if (error) throw error;
if (data?.session) {
recoveryReady.value = true;
} else {
linkInvalid.value = true;
}
} catch {
linkInvalid.value = true;
}
} catch {
linkInvalid.value = true
}
}
onMounted(async () => {
await syncRecoveryState()
await syncRecoveryState();
const { data: listener } = supabase.auth.onAuthStateChange((event) => {
if (event === 'PASSWORD_RECOVERY' || event === 'SIGNED_IN') {
recoveryReady.value = true
linkInvalid.value = false
}
if (event === 'SIGNED_OUT') {
recoveryReady.value = false
}
})
const { data: listener } = supabase.auth.onAuthStateChange((event) => {
if (event === 'PASSWORD_RECOVERY' || event === 'SIGNED_IN') {
recoveryReady.value = true;
linkInvalid.value = false;
}
if (event === 'SIGNED_OUT') {
recoveryReady.value = false;
}
});
unsubscribeFn = () => listener?.subscription?.unsubscribe()
})
unsubscribeFn = () => listener?.subscription?.unsubscribe();
});
onBeforeUnmount(() => {
try { unsubscribeFn?.() } catch {}
})
try {
unsubscribeFn?.();
} catch {}
});
function goLogin () {
router.replace('/auth/login')
function goLogin() {
router.replace('/auth/login');
}
async function submit () {
if (!recoveryReady.value) {
toast.add({ severity: 'warn', summary: 'Link inválido', detail: 'Solicite um novo e-mail de redefinição.', life: 3500 })
return
}
if (!matchOk.value) {
toast.add({ severity: 'warn', summary: 'Confirmação', detail: 'As senhas não conferem.', life: 3000 })
return
}
if (!strengthOk.value) {
toast.add({ severity: 'warn', summary: 'Senha fraca', detail: 'Use no mínimo 8 caracteres com maiúscula, minúscula e número.', life: 4000 })
return
}
async function submit() {
if (!recoveryReady.value) {
toast.add({ severity: 'warn', summary: 'Link inválido', detail: 'Solicite um novo e-mail de redefinição.', life: 3500 });
return;
}
if (!matchOk.value) {
toast.add({ severity: 'warn', summary: 'Confirmação', detail: 'As senhas não conferem.', life: 3000 });
return;
}
if (!strengthOk.value) {
toast.add({ severity: 'warn', summary: 'Senha fraca', detail: 'Use no mínimo 8 caracteres com maiúscula, minúscula e número.', life: 4000 });
return;
}
loading.value = true
try {
const { error } = await supabase.auth.updateUser({ password: newPassword.value })
if (error) throw error
loading.value = true;
try {
const { error } = await supabase.auth.updateUser({ password: newPassword.value });
if (error) throw error;
done.value = true
try { await supabase.auth.signOut({ scope: 'global' }) } catch {}
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível redefinir a senha.', life: 4500 })
} finally {
loading.value = false
}
done.value = true;
try {
await supabase.auth.signOut({ scope: 'global' });
} catch {}
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível redefinir a senha.', life: 4500 });
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="min-h-screen w-full flex">
<div class="min-h-screen w-full flex">
<!-- ===== ESQUERDA: Painel de segurança ===== -->
<div class="hidden lg:flex lg:w-1/2 relative overflow-hidden flex-col">
<!-- Fundo gradiente -->
<div class="absolute inset-0 bg-gradient-to-br from-indigo-600 via-violet-600 to-purple-700" />
<!-- ===== ESQUERDA: Painel de segurança ===== -->
<div class="hidden lg:flex lg:w-1/2 relative overflow-hidden flex-col">
<!-- Fundo gradiente -->
<div class="absolute inset-0 bg-gradient-to-br from-indigo-600 via-violet-600 to-purple-700" />
<!-- Grade decorativa -->
<div class="absolute inset-0 opacity-[0.08]" style="background-image: linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px); background-size: 48px 48px" />
<!-- Grade decorativa -->
<div
class="absolute inset-0 opacity-[0.08]"
style="background-image: linear-gradient(to right, white 1px, transparent 1px), linear-gradient(to bottom, white 1px, transparent 1px); background-size: 48px 48px;"
/>
<!-- Orbs -->
<div class="absolute -top-40 -left-40 h-[32rem] w-[32rem] rounded-full bg-white/10 blur-3xl pointer-events-none" />
<div class="absolute bottom-0 right-0 h-80 w-80 rounded-full bg-violet-300/20 blur-3xl pointer-events-none" />
<!-- Orbs -->
<div class="absolute -top-40 -left-40 h-[32rem] w-[32rem] rounded-full bg-white/10 blur-3xl pointer-events-none" />
<div class="absolute bottom-0 right-0 h-80 w-80 rounded-full bg-violet-300/20 blur-3xl pointer-events-none" />
<div class="relative z-10 flex flex-col h-full p-10 xl:p-14">
<!-- Brand -->
<div class="flex items-center gap-3">
<div class="grid h-10 w-10 place-items-center rounded-xl bg-white/20 backdrop-blur-sm border border-white/20 shadow-lg">
<i class="pi pi-heart-fill text-white text-sm" />
</div>
<span class="text-white font-bold text-lg tracking-tight">Agência PSI</span>
</div>
<!-- Conteúdo central -->
<div class="flex-1 flex flex-col justify-center space-y-8">
<div class="grid h-16 w-16 place-items-center rounded-2xl bg-white/15 backdrop-blur-sm border border-white/20 shadow-lg">
<i class="pi pi-lock text-white text-2xl" />
</div>
<div class="space-y-3">
<h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">
Crie uma senha<br>que você não vai<br>esquecer.
</h2>
<p class="text-base text-white/70 leading-relaxed max-w-sm">
Escolha algo único e seguro. Uma boa senha protege seus dados e os de seus pacientes.
</p>
</div>
<!-- Dicas -->
<ul class="space-y-3">
<li class="flex items-start gap-3 text-sm text-white/70">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-emerald-400 flex-shrink-0" />
Mínimo de 8 caracteres
</li>
<li class="flex items-start gap-3 text-sm text-white/70">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-indigo-300 flex-shrink-0" />
Combine letras maiúsculas, minúsculas e números
</li>
<li class="flex items-start gap-3 text-sm text-white/70">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-fuchsia-300 flex-shrink-0" />
Não reutilize a mesma senha de outros serviços
</li>
<li class="flex items-start gap-3 text-sm text-white/70">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-amber-300 flex-shrink-0" />
Exemplo seguro: <span class="font-semibold text-white/90">"Noite#Calma7"</span>
</li>
</ul>
</div>
<!-- Rodapé esquerdo -->
<div class="flex items-center justify-between text-xs text-white/40">
<span>Agência PSI</span>
<span>Acesso seguro</span>
</div>
</div>
</div>
<!-- ===== DIREITA: Formulário ===== -->
<div class="flex-1 lg:w-1/2 flex flex-col min-h-screen bg-[var(--surface-ground)] overflow-y-auto relative">
<!-- Halos sutis -->
<div class="pointer-events-none absolute inset-0">
<div class="absolute top-10 right-10 h-64 w-64 rounded-full blur-3xl bg-indigo-400/5" />
<div class="absolute bottom-10 left-10 h-56 w-56 rounded-full blur-3xl bg-violet-400/5" />
</div>
<div class="relative flex flex-col flex-1 justify-center px-6 py-10 sm:px-10 lg:px-12 xl:px-16 w-full max-w-lg mx-auto">
<!-- Mobile: Brand -->
<div class="flex lg:hidden items-center gap-2 mb-8">
<div class="grid h-8 w-8 place-items-center rounded-lg bg-indigo-500/10 border border-indigo-500/20">
<i class="pi pi-heart-fill text-indigo-500 text-xs" />
</div>
<span class="text-[var(--text-color)] font-bold text-base tracking-tight">Agência PSI</span>
</div>
<!-- Estado: SUCESSO -->
<template v-if="done">
<div class="text-center space-y-6">
<div class="mx-auto grid h-20 w-20 place-items-center rounded-full bg-emerald-500/10 border border-emerald-500/20">
<i class="pi pi-check text-emerald-500 text-3xl" />
</div>
<div>
<h1 class="text-2xl font-bold text-[var(--text-color)]">Senha redefinida!</h1>
<p class="mt-2 text-sm text-[var(--text-color-secondary)] leading-relaxed">
Sua senha foi atualizada com sucesso.<br>Faça login novamente para continuar.
</p>
</div>
<Button
label="Ir para o login"
icon="pi pi-sign-in"
class="w-full"
@click="goLogin"
/>
</div>
</template>
<!-- Estado: LINK INVÁLIDO -->
<template v-else-if="linkInvalid">
<div class="text-center space-y-6">
<div class="mx-auto grid h-20 w-20 place-items-center rounded-full bg-red-500/10 border border-red-500/20">
<i class="pi pi-times text-red-500 text-3xl" />
</div>
<div>
<h1 class="text-2xl font-bold text-[var(--text-color)]">Link inválido</h1>
<p class="mt-2 text-sm text-[var(--text-color-secondary)] leading-relaxed">
Este link expirou ou foi utilizado.<br>Solicite um novo e-mail de redefinição.
</p>
</div>
<Button
label="Voltar para o login"
icon="pi pi-arrow-left"
severity="secondary"
outlined
class="w-full"
@click="goLogin"
/>
</div>
</template>
<!-- Estado: FORMULÁRIO -->
<template v-else>
<!-- Cabeçalho -->
<div class="mb-7">
<h1 class="text-3xl font-bold text-[var(--text-color)] leading-tight">Redefinir senha</h1>
<p class="mt-1.5 text-sm text-[var(--text-color-secondary)]">
Escolha uma senha nova e segura para sua conta.
</p>
</div>
<form class="space-y-5" @submit.prevent="submit">
<!-- Nova senha -->
<div>
<label class="block text-sm font-semibold text-[var(--text-color)] mb-1.5">Nova senha</label>
<Password
v-model="newPassword"
placeholder="Mínimo 8 caracteres"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full"
:disabled="loading"
/>
<!-- Barra de força -->
<div v-if="newPassword" class="mt-2 space-y-1">
<div class="flex gap-1">
<div
v-for="i in 4"
:key="i"
class="h-1 flex-1 rounded-full transition-all duration-300"
:class="i <= strengthScore ? strengthColor : 'bg-[var(--surface-border)]'"
/>
<div class="relative z-10 flex flex-col h-full p-10 xl:p-14">
<!-- Brand -->
<div class="flex items-center gap-3">
<div class="grid h-10 w-10 place-items-center rounded-xl bg-white/20 backdrop-blur-sm border border-white/20 shadow-lg">
<i class="pi pi-heart-fill text-white text-sm" />
</div>
<span class="text-white font-bold text-lg tracking-tight">Agência PSI</span>
</div>
<span class="text-xs" :class="{
'text-red-500': strengthScore === 1,
'text-yellow-500': strengthScore === 2,
'text-blue-500': strengthScore === 3,
'text-emerald-500': strengthScore === 4,
}">
{{ strengthLabel }}
</span>
</div>
<p v-else class="mt-1.5 text-xs text-[var(--text-color-secondary)]">
Dica: combine letras, números e símbolos.
</p>
<!-- Conteúdo central -->
<div class="flex-1 flex flex-col justify-center space-y-8">
<div class="grid h-16 w-16 place-items-center rounded-2xl bg-white/15 backdrop-blur-sm border border-white/20 shadow-lg">
<i class="pi pi-lock text-white text-2xl" />
</div>
<div class="space-y-3">
<h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">Crie uma senha<br />que você não vai<br />esquecer.</h2>
<p class="text-base text-white/70 leading-relaxed max-w-sm">Escolha algo único e seguro. Uma boa senha protege seus dados e os de seus pacientes.</p>
</div>
<!-- Dicas -->
<ul class="space-y-3">
<li class="flex items-start gap-3 text-sm text-white/70">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-emerald-400 flex-shrink-0" />
Mínimo de 8 caracteres
</li>
<li class="flex items-start gap-3 text-sm text-white/70">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-indigo-300 flex-shrink-0" />
Combine letras maiúsculas, minúsculas e números
</li>
<li class="flex items-start gap-3 text-sm text-white/70">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-fuchsia-300 flex-shrink-0" />
Não reutilize a mesma senha de outros serviços
</li>
<li class="flex items-start gap-3 text-sm text-white/70">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-amber-300 flex-shrink-0" />
Exemplo seguro: <span class="font-semibold text-white/90">"Noite#Calma7"</span>
</li>
</ul>
</div>
<!-- Rodapé esquerdo -->
<div class="flex items-center justify-between text-xs text-white/40">
<span>Agência PSI</span>
<span>Acesso seguro</span>
</div>
</div>
</div>
<!-- ===== DIREITA: Formulário ===== -->
<div class="flex-1 lg:w-1/2 flex flex-col min-h-screen bg-[var(--surface-ground)] overflow-y-auto relative">
<!-- Halos sutis -->
<div class="pointer-events-none absolute inset-0">
<div class="absolute top-10 right-10 h-64 w-64 rounded-full blur-3xl bg-indigo-400/5" />
<div class="absolute bottom-10 left-10 h-56 w-56 rounded-full blur-3xl bg-violet-400/5" />
</div>
<!-- Confirmar senha -->
<div>
<label class="block text-sm font-semibold text-[var(--text-color)] mb-1.5">Confirmar senha</label>
<Password
v-model="confirmPassword"
placeholder="Repita a nova senha"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full"
:disabled="loading"
/>
<div v-if="confirmPassword" class="mt-1.5 flex items-center gap-1.5 text-xs"
:class="matchOk ? 'text-emerald-500' : 'text-yellow-500'"
>
<i :class="matchOk ? 'pi pi-check' : 'pi pi-times'" />
{{ matchOk ? 'As senhas conferem.' : 'As senhas não conferem.' }}
</div>
<div class="relative flex flex-col flex-1 justify-center px-6 py-10 sm:px-10 lg:px-12 xl:px-16 w-full max-w-lg mx-auto">
<!-- Mobile: Brand -->
<div class="flex lg:hidden items-center gap-2 mb-8">
<div class="grid h-8 w-8 place-items-center rounded-lg bg-indigo-500/10 border border-indigo-500/20">
<i class="pi pi-heart-fill text-indigo-500 text-xs" />
</div>
<span class="text-[var(--text-color)] font-bold text-base tracking-tight">Agência PSI</span>
</div>
<!-- Estado: SUCESSO -->
<template v-if="done">
<div class="text-center space-y-6">
<div class="mx-auto grid h-20 w-20 place-items-center rounded-full bg-emerald-500/10 border border-emerald-500/20">
<i class="pi pi-check text-emerald-500 text-3xl" />
</div>
<div>
<h1 class="text-2xl font-bold text-[var(--text-color)]">Senha redefinida!</h1>
<p class="mt-2 text-sm text-[var(--text-color-secondary)] leading-relaxed">Sua senha foi atualizada com sucesso.<br />Faça login novamente para continuar.</p>
</div>
<Button label="Ir para o login" icon="pi pi-sign-in" class="w-full" @click="goLogin" />
</div>
</template>
<!-- Estado: LINK INVÁLIDO -->
<template v-else-if="linkInvalid">
<div class="text-center space-y-6">
<div class="mx-auto grid h-20 w-20 place-items-center rounded-full bg-red-500/10 border border-red-500/20">
<i class="pi pi-times text-red-500 text-3xl" />
</div>
<div>
<h1 class="text-2xl font-bold text-[var(--text-color)]">Link inválido</h1>
<p class="mt-2 text-sm text-[var(--text-color-secondary)] leading-relaxed">Este link expirou ou foi utilizado.<br />Solicite um novo e-mail de redefinição.</p>
</div>
<Button label="Voltar para o login" icon="pi pi-arrow-left" severity="secondary" outlined class="w-full" @click="goLogin" />
</div>
</template>
<!-- Estado: FORMULÁRIO -->
<template v-else>
<!-- Cabeçalho -->
<div class="mb-7">
<h1 class="text-3xl font-bold text-[var(--text-color)] leading-tight">Redefinir senha</h1>
<p class="mt-1.5 text-sm text-[var(--text-color-secondary)]">Escolha uma senha nova e segura para sua conta.</p>
</div>
<form class="space-y-5" @submit.prevent="submit">
<!-- Nova senha -->
<div>
<label class="block text-sm font-semibold text-[var(--text-color)] mb-1.5">Nova senha</label>
<Password v-model="newPassword" placeholder="Mínimo 8 caracteres" toggleMask :feedback="false" class="w-full" inputClass="w-full" :disabled="loading" />
<!-- Barra de força -->
<div v-if="newPassword" class="mt-2 space-y-1">
<div class="flex gap-1">
<div v-for="i in 4" :key="i" class="h-1 flex-1 rounded-full transition-all duration-300" :class="i <= strengthScore ? strengthColor : 'bg-[var(--surface-border)]'" />
</div>
<span
class="text-xs"
:class="{
'text-red-500': strengthScore === 1,
'text-yellow-500': strengthScore === 2,
'text-blue-500': strengthScore === 3,
'text-emerald-500': strengthScore === 4
}"
>
{{ strengthLabel }}
</span>
</div>
<p v-else class="mt-1.5 text-xs text-[var(--text-color-secondary)]">Dica: combine letras, números e símbolos.</p>
</div>
<!-- Confirmar senha -->
<div>
<label class="block text-sm font-semibold text-[var(--text-color)] mb-1.5">Confirmar senha</label>
<Password v-model="confirmPassword" placeholder="Repita a nova senha" toggleMask :feedback="false" class="w-full" inputClass="w-full" :disabled="loading" />
<div v-if="confirmPassword" class="mt-1.5 flex items-center gap-1.5 text-xs" :class="matchOk ? 'text-emerald-500' : 'text-yellow-500'">
<i :class="matchOk ? 'pi pi-check' : 'pi pi-times'" />
{{ matchOk ? 'As senhas conferem.' : 'As senhas não conferem.' }}
</div>
</div>
<!-- Botão -->
<Button type="submit" label="Atualizar senha" icon="pi pi-check" class="w-full" :loading="loading" :disabled="!canSubmit" />
</form>
<!-- Rodapé -->
<div class="mt-8 pt-6 border-t border-[var(--surface-border)]">
<button type="button" class="flex items-center gap-2 text-sm text-[var(--text-color-secondary)] hover:text-[var(--text-color)] transition-colors" @click="goLogin">
<i class="pi pi-arrow-left text-xs" />
Voltar para o login
</button>
<p class="mt-4 text-xs text-[var(--text-color-secondary)] leading-relaxed">Se você não solicitou essa redefinição, ignore o e-mail e certifique-se de que sua conta está segura.</p>
</div>
</template>
</div>
<!-- Botão -->
<Button
type="submit"
label="Atualizar senha"
icon="pi pi-check"
class="w-full"
:loading="loading"
:disabled="!canSubmit"
/>
</form>
<!-- Rodapé -->
<div class="mt-8 pt-6 border-t border-[var(--surface-border)]">
<button
type="button"
class="flex items-center gap-2 text-sm text-[var(--text-color-secondary)] hover:text-[var(--text-color)] transition-colors"
@click="goLogin"
>
<i class="pi pi-arrow-left text-xs" />
Voltar para o login
</button>
<p class="mt-4 text-xs text-[var(--text-color-secondary)] leading-relaxed">
Se você não solicitou essa redefinição, ignore o e-mail e certifique-se de que sua conta está segura.
</p>
</div>
</template>
</div>
</div>
</div>
</div>
</template>
+254 -313
View File
@@ -15,364 +15,305 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import Password from 'primevue/password'
import Button from 'primevue/button'
import { useToast } from 'primevue/usetoast'
import Password from 'primevue/password';
import Button from 'primevue/button';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client'
import { supabase } from '@/lib/supabase/client';
const toast = useToast()
const toast = useToast();
// ── Hero sticky ────────────────────────────────────────────
const headerEl = ref(null)
const headerSentinelRef = ref(null)
const headerStuck = ref(false)
let _observer = null
const headerEl = ref(null);
const headerSentinelRef = ref(null);
const headerStuck = ref(false);
let _observer = null;
const currentPassword = ref('')
const newPassword = ref('')
const confirmPassword = ref('')
const loading = ref(false)
const loadingReset = ref(false)
const mounted = ref(false)
const done = ref(false)
const currentPassword = ref('');
const newPassword = ref('');
const confirmPassword = ref('');
const loading = ref(false);
const loadingReset = ref(false);
const mounted = ref(false);
const done = ref(false);
// ── validações ────────────────────────────────────────────────────────────
function isStrongEnough (p) {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(p || '')
function isStrongEnough(p) {
return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/.test(p || '');
}
const strengthScore = computed(() => {
const p = newPassword.value
if (!p) return 0
let s = 0
if (p.length >= 8) s++
if (/[A-Z]/.test(p)) s++
if (/[a-z]/.test(p)) s++
if (/\d/.test(p)) s++
return s
})
const p = newPassword.value;
if (!p) return 0;
let s = 0;
if (p.length >= 8) s++;
if (/[A-Z]/.test(p)) s++;
if (/[a-z]/.test(p)) s++;
if (/\d/.test(p)) s++;
return s;
});
const strengthLabel = computed(() => {
const labels = ['', 'Muito fraca', 'Fraca', 'Boa', 'Forte']
return labels[strengthScore.value] || ''
})
const labels = ['', 'Muito fraca', 'Fraca', 'Boa', 'Forte'];
return labels[strengthScore.value] || '';
});
const strengthColor = computed(() => {
const colors = ['', 'bg-red-500', 'bg-yellow-500', 'bg-blue-500', 'bg-emerald-500']
return colors[strengthScore.value] || ''
})
const colors = ['', 'bg-red-500', 'bg-yellow-500', 'bg-blue-500', 'bg-emerald-500'];
return colors[strengthScore.value] || '';
});
const strengthTextColor = computed(() => {
const colors = ['', 'text-red-500', 'text-yellow-500', 'text-blue-500', 'text-emerald-500']
return colors[strengthScore.value] || ''
})
const colors = ['', 'text-red-500', 'text-yellow-500', 'text-blue-500', 'text-emerald-500'];
return colors[strengthScore.value] || '';
});
const strengthOk = computed(() => isStrongEnough(newPassword.value))
const matchOk = computed(() => !!confirmPassword.value && newPassword.value === confirmPassword.value)
const strengthOk = computed(() => isStrongEnough(newPassword.value));
const matchOk = computed(() => !!confirmPassword.value && newPassword.value === confirmPassword.value);
const canSubmit = computed(() =>
!!currentPassword.value && strengthOk.value && matchOk.value && !loading.value && !loadingReset.value
)
const canSubmit = computed(() => !!currentPassword.value && strengthOk.value && matchOk.value && !loading.value && !loadingReset.value);
// ── ações ─────────────────────────────────────────────────────────────────
function clearFields () {
currentPassword.value = ''
newPassword.value = ''
confirmPassword.value = ''
function clearFields() {
currentPassword.value = '';
newPassword.value = '';
confirmPassword.value = '';
}
async function hardLogout () {
try { await supabase.auth.signOut({ scope: 'global' }) } catch {}
try {
const keys = []
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i)
if (k?.startsWith('sb-') && k.includes('auth-token')) keys.push(k)
async function hardLogout() {
try {
await supabase.auth.signOut({ scope: 'global' });
} catch {}
try {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k?.startsWith('sb-') && k.includes('auth-token')) keys.push(k);
}
keys.forEach((k) => localStorage.removeItem(k));
} catch {}
try {
sessionStorage.removeItem('redirect_after_login');
} catch {}
window.location.replace('/auth/login');
}
async function changePassword() {
loading.value = true;
try {
const { data: uData, error: uErr } = await supabase.auth.getUser();
if (uErr) throw uErr;
const email = uData?.user?.email;
if (!email) throw new Error('Sessão inválida. Faça login novamente.');
// reautentica para confirmar senha atual
const { error: signError } = await supabase.auth.signInWithPassword({ email, password: currentPassword.value });
if (signError) throw new Error('Senha atual incorreta.');
// atualiza
const { error: upError } = await supabase.auth.updateUser({ password: newPassword.value });
if (upError) throw upError;
clearFields();
done.value = true;
toast.add({ severity: 'success', summary: 'Senha atualizada', detail: 'Por segurança, você será deslogado.', life: 2500 });
setTimeout(() => hardLogout(), 2600);
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível trocar a senha.', life: 4000 });
} finally {
loading.value = false;
}
keys.forEach(k => localStorage.removeItem(k))
} catch {}
try { sessionStorage.removeItem('redirect_after_login') } catch {}
window.location.replace('/auth/login')
}
async function changePassword () {
loading.value = true
try {
const { data: uData, error: uErr } = await supabase.auth.getUser()
if (uErr) throw uErr
const email = uData?.user?.email
if (!email) throw new Error('Sessão inválida. Faça login novamente.')
// reautentica para confirmar senha atual
const { error: signError } = await supabase.auth.signInWithPassword({ email, password: currentPassword.value })
if (signError) throw new Error('Senha atual incorreta.')
// atualiza
const { error: upError } = await supabase.auth.updateUser({ password: newPassword.value })
if (upError) throw upError
clearFields()
done.value = true
toast.add({ severity: 'success', summary: 'Senha atualizada', detail: 'Por segurança, você será deslogado.', life: 2500 })
setTimeout(() => hardLogout(), 2600)
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não foi possível trocar a senha.', life: 4000 })
} finally {
loading.value = false
}
}
onMounted(() => {
const rootMargin = `${document.querySelector('.l2-main') ? '0px' : '-56px'} 0px 0px 0px`
_observer = new IntersectionObserver(
([entry]) => { headerStuck.value = !entry.isIntersecting },
{ threshold: 0, rootMargin }
)
if (headerSentinelRef.value) _observer.observe(headerSentinelRef.value)
mounted.value = true
})
const rootMargin = `${document.querySelector('.l2-main') ? '0px' : '-56px'} 0px 0px 0px`;
_observer = new IntersectionObserver(
([entry]) => {
headerStuck.value = !entry.isIntersecting;
},
{ threshold: 0, rootMargin }
);
if (headerSentinelRef.value) _observer.observe(headerSentinelRef.value);
mounted.value = true;
});
onBeforeUnmount(() => { _observer?.disconnect() })
onBeforeUnmount(() => {
_observer?.disconnect();
});
async function sendResetEmail () {
loadingReset.value = true
try {
const { data: uData, error: uErr } = await supabase.auth.getUser()
if (uErr) throw uErr
async function sendResetEmail() {
loadingReset.value = true;
try {
const { data: uData, error: uErr } = await supabase.auth.getUser();
if (uErr) throw uErr;
const email = uData?.user?.email
if (!email) throw new Error('Sessão inválida. Faça login novamente.')
const email = uData?.user?.email;
if (!email) throw new Error('Sessão inválida. Faça login novamente.');
const redirectTo = `${window.location.origin}/auth/reset-password`
const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo })
if (error) throw error
const redirectTo = `${window.location.origin}/auth/reset-password`;
const { error } = await supabase.auth.resetPasswordForEmail(email, { redirectTo });
if (error) throw error;
toast.add({ severity: 'info', summary: 'E-mail enviado', detail: 'Verifique sua caixa de entrada para redefinir a senha.', life: 5000 })
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao enviar e-mail.', life: 4500 })
} finally {
loadingReset.value = false
}
toast.add({ severity: 'info', summary: 'E-mail enviado', detail: 'Verifique sua caixa de entrada para redefinir a senha.', life: 5000 });
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao enviar e-mail.', life: 4500 });
} finally {
loadingReset.value = false;
}
}
</script>
<template>
<!-- Sentinel -->
<div ref="headerSentinelRef" class="h-px" />
<!-- Sentinel -->
<div ref="headerSentinelRef" class="h-px" />
<!-- Hero sticky -->
<div
ref="headerEl"
class="sticky mx-auto max-w-2xl px-3 md:px-5 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5"
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
>
<!-- Blobs -->
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-12 bg-indigo-500/10" />
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-2 -left-20 bg-emerald-500/[0.08]" />
</div>
<div class="relative z-10 flex items-center gap-4">
<div class="flex items-center gap-3 flex-1 min-w-0">
<div
class="grid place-items-center w-10 h-10 rounded-[0.875rem] shrink-0"
style="background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent); color: var(--p-primary-500, #6366f1)"
>
<i class="pi pi-shield text-lg" />
<!-- Hero sticky -->
<div
ref="headerEl"
class="sticky mx-auto max-w-2xl px-3 md:px-5 mb-4 z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-5"
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
>
<!-- Blobs -->
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute rounded-full blur-[70px] w-72 h-72 -top-16 -right-12 bg-indigo-500/10" />
<div class="absolute rounded-full blur-[70px] w-80 h-80 top-2 -left-20 bg-emerald-500/[0.08]" />
</div>
<div class="min-w-0">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Segurança</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Gerencie o acesso e a senha da sua conta</div>
</div>
</div>
<span class="hidden xl:inline-flex items-center gap-2 text-[1rem] px-3 py-1.5 rounded-full border border-emerald-200 text-emerald-700 bg-emerald-50 shrink-0">
<span class="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
Sessão ativa
</span>
</div>
</div>
<div class="px-3 md:px-5 pb-8 flex justify-center">
<div class="w-full max-w-2xl space-y-4">
<!-- Card principal -->
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
<!-- Seção: Trocar senha -->
<div class="px-6 py-5 border-b border-[var(--surface-border)]">
<div class="flex items-center justify-between gap-4">
<div>
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Trocar senha</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">
Confirme sua senha atual e defina uma nova.
</div>
<div class="relative z-10 flex items-center gap-4">
<div class="flex items-center gap-3 flex-1 min-w-0">
<div class="grid place-items-center w-10 h-10 rounded-[0.875rem] shrink-0" style="background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent); color: var(--p-primary-500, #6366f1)">
<i class="pi pi-shield text-lg" />
</div>
<div class="min-w-0">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Segurança</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Gerencie o acesso e a senha da sua conta</div>
</div>
</div>
<span class="hidden sm:inline-flex items-center gap-1.5 rounded-full border border-[var(--surface-border)] bg-[var(--surface-ground)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)]">
<span class="h-1.5 w-1.5 rounded-full bg-emerald-400" />
sessão ativa
<span class="hidden xl:inline-flex items-center gap-2 text-[1rem] px-3 py-1.5 rounded-full border border-emerald-200 text-emerald-700 bg-emerald-50 shrink-0">
<span class="h-2 w-2 rounded-full bg-emerald-500 animate-pulse" />
Sessão ativa
</span>
</div>
</div>
<!-- Estado: concluído -->
<div v-if="done" class="px-6 py-10 text-center space-y-4">
<div class="mx-auto grid h-16 w-16 place-items-center rounded-full bg-emerald-500/10 border border-emerald-500/20">
<i class="pi pi-check text-emerald-500 text-2xl" />
</div>
<div>
<div class="font-semibold text-[var(--text-color)]">Senha atualizada!</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">Redirecionando para o login</div>
</div>
</div>
<!-- Estado: formulário -->
<div v-else class="px-6 py-6 space-y-5">
<!-- Senha atual -->
<div>
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Senha atual</div>
<Password
v-model="currentPassword"
placeholder="Digite sua senha atual"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full"
:disabled="loading || loadingReset"
/>
<div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">
Necessária para confirmar que é você.
</div>
</div>
<div class="h-px bg-[var(--surface-border)]" />
<!-- Nova senha -->
<div>
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Nova senha</div>
<Password
v-model="newPassword"
placeholder="Mínimo 8 caracteres"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full"
:disabled="loading || loadingReset"
/>
<!-- Barra de força -->
<div v-if="newPassword" class="mt-2 space-y-1">
<div class="flex gap-1">
<div
v-for="i in 4"
:key="i"
class="h-1 flex-1 rounded-full transition-all duration-300"
:class="i <= strengthScore ? strengthColor : 'bg-[var(--surface-border)]'"
/>
</div>
<span class="text-[1rem]" :class="strengthTextColor">{{ strengthLabel }}</span>
</div>
<div v-else class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">
Critérios: 8+ caracteres, maiúscula, minúscula e número.
</div>
</div>
<!-- Confirmar senha -->
<div>
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Confirmar nova senha</div>
<Password
v-model="confirmPassword"
placeholder="Repita a nova senha"
toggleMask
:feedback="false"
class="w-full"
inputClass="w-full"
:disabled="loading || loadingReset"
/>
<div
v-if="confirmPassword"
class="mt-1.5 flex items-center gap-1.5 text-[1rem]"
:class="matchOk ? 'text-emerald-500' : 'text-yellow-500'"
>
<i :class="matchOk ? 'pi pi-check' : 'pi pi-times'" />
{{ matchOk ? 'As senhas conferem.' : 'As senhas não conferem.' }}
</div>
</div>
<!-- Aviso -->
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)] px-4 py-3 flex items-start gap-3">
<i class="pi pi-info-circle text-[var(--text-color-secondary)] mt-0.5 flex-shrink-0" />
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">
Ao trocar sua senha, você será desconectado de todos os dispositivos por segurança.
</div>
</div>
<!-- Ações -->
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-between sm:items-center pt-1">
<Button
label="Enviar link por e-mail"
severity="secondary"
outlined
icon="pi pi-envelope"
:loading="loadingReset"
:disabled="loading || loadingReset"
@click="sendResetEmail"
/>
<Button
label="Trocar senha"
icon="pi pi-check"
:loading="loading"
:disabled="!canSubmit"
@click="changePassword"
/>
</div>
</div>
</div>
<!-- Card informativo: dicas -->
<div class="mt-4 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-6 py-5">
<div class="flex items-center gap-2 mb-3">
<i class="pi pi-lightbulb text-[var(--text-color-secondary)]" />
<span class="text-[1rem] font-semibold text-[var(--text-color)]">Boas práticas</span>
</div>
<ul class="space-y-2">
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-indigo-400 flex-shrink-0" />
Use pelo menos 8 caracteres com maiúscula, minúscula e número.
</li>
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-emerald-400 flex-shrink-0" />
Evite datas, nomes e sequências óbvias (1234, qwerty).
</li>
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-fuchsia-400 flex-shrink-0" />
Se estiver em computador compartilhado, encerre a sessão depois.
</li>
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-amber-400 flex-shrink-0" />
Não reutilize a mesma senha de outros serviços.
</li>
</ul>
</div>
</div>
</div>
<div class="px-3 md:px-4 pb-3">
<LoadedPhraseBlock v-if="mounted" />
</div>
<div class="px-3 md:px-5 pb-8 flex justify-center">
<div class="w-full max-w-2xl space-y-4">
<!-- Card principal -->
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
<!-- Seção: Trocar senha -->
<div class="px-6 py-5 border-b border-[var(--surface-border)]">
<div class="flex items-center justify-between gap-4">
<div>
<div class="text-[1rem] font-semibold text-[var(--text-color)]">Trocar senha</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Confirme sua senha atual e defina uma nova.</div>
</div>
<span class="hidden sm:inline-flex items-center gap-1.5 rounded-full border border-[var(--surface-border)] bg-[var(--surface-ground)] px-3 py-1 text-[1rem] text-[var(--text-color-secondary)]">
<span class="h-1.5 w-1.5 rounded-full bg-emerald-400" />
sessão ativa
</span>
</div>
</div>
<!-- Estado: concluído -->
<div v-if="done" class="px-6 py-10 text-center space-y-4">
<div class="mx-auto grid h-16 w-16 place-items-center rounded-full bg-emerald-500/10 border border-emerald-500/20">
<i class="pi pi-check text-emerald-500 text-2xl" />
</div>
<div>
<div class="font-semibold text-[var(--text-color)]">Senha atualizada!</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-1">Redirecionando para o login</div>
</div>
</div>
<!-- Estado: formulário -->
<div v-else class="px-6 py-6 space-y-5">
<!-- Senha atual -->
<div>
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Senha atual</div>
<Password v-model="currentPassword" placeholder="Digite sua senha atual" toggleMask :feedback="false" class="w-full" inputClass="w-full" :disabled="loading || loadingReset" />
<div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Necessária para confirmar que é você.</div>
</div>
<div class="h-px bg-[var(--surface-border)]" />
<!-- Nova senha -->
<div>
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Nova senha</div>
<Password v-model="newPassword" placeholder="Mínimo 8 caracteres" toggleMask :feedback="false" class="w-full" inputClass="w-full" :disabled="loading || loadingReset" />
<!-- Barra de força -->
<div v-if="newPassword" class="mt-2 space-y-1">
<div class="flex gap-1">
<div v-for="i in 4" :key="i" class="h-1 flex-1 rounded-full transition-all duration-300" :class="i <= strengthScore ? strengthColor : 'bg-[var(--surface-border)]'" />
</div>
<span class="text-[1rem]" :class="strengthTextColor">{{ strengthLabel }}</span>
</div>
<div v-else class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Critérios: 8+ caracteres, maiúscula, minúscula e número.</div>
</div>
<!-- Confirmar senha -->
<div>
<div class="text-[1rem] font-semibold text-[var(--text-color)] mb-1.5">Confirmar nova senha</div>
<Password v-model="confirmPassword" placeholder="Repita a nova senha" toggleMask :feedback="false" class="w-full" inputClass="w-full" :disabled="loading || loadingReset" />
<div v-if="confirmPassword" class="mt-1.5 flex items-center gap-1.5 text-[1rem]" :class="matchOk ? 'text-emerald-500' : 'text-yellow-500'">
<i :class="matchOk ? 'pi pi-check' : 'pi pi-times'" />
{{ matchOk ? 'As senhas conferem.' : 'As senhas não conferem.' }}
</div>
</div>
<!-- Aviso -->
<div class="rounded-md border border-[var(--surface-border)] bg-[var(--surface-ground)] px-4 py-3 flex items-start gap-3">
<i class="pi pi-info-circle text-[var(--text-color-secondary)] mt-0.5 flex-shrink-0" />
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">Ao trocar sua senha, você será desconectado de todos os dispositivos por segurança.</div>
</div>
<!-- Ações -->
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-between sm:items-center pt-1">
<Button label="Enviar link por e-mail" severity="secondary" outlined icon="pi pi-envelope" :loading="loadingReset" :disabled="loading || loadingReset" @click="sendResetEmail" />
<Button label="Trocar senha" icon="pi pi-check" :loading="loading" :disabled="!canSubmit" @click="changePassword" />
</div>
</div>
</div>
<!-- Card informativo: dicas -->
<div class="mt-4 rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-6 py-5">
<div class="flex items-center gap-2 mb-3">
<i class="pi pi-lightbulb text-[var(--text-color-secondary)]" />
<span class="text-[1rem] font-semibold text-[var(--text-color)]">Boas práticas</span>
</div>
<ul class="space-y-2">
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-indigo-400 flex-shrink-0" />
Use pelo menos 8 caracteres com maiúscula, minúscula e número.
</li>
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-emerald-400 flex-shrink-0" />
Evite datas, nomes e sequências óbvias (1234, qwerty).
</li>
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-fuchsia-400 flex-shrink-0" />
Se estiver em computador compartilhado, encerre a sessão depois.
</li>
<li class="flex items-start gap-2.5 text-[1rem] text-[var(--text-color-secondary)]">
<span class="mt-1 h-1.5 w-1.5 rounded-full bg-amber-400 flex-shrink-0" />
Não reutilize a mesma senha de outros serviços.
</li>
</ul>
</div>
</div>
</div>
<div class="px-3 md:px-4 pb-3">
<LoadedPhraseBlock v-if="mounted" />
</div>
</template>
<style scoped>
</style>
<style scoped></style>
+237 -279
View File
@@ -15,100 +15,109 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { supabase } from '@/lib/supabase/client'
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'
import Message from 'primevue/message';
import Chip from 'primevue/chip';
import ProgressSpinner from 'primevue/progressspinner';
const route = useRoute()
const router = useRouter()
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())
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
function normalizeInterval(v) {
if (v === 'monthly') return 'month';
if (v === 'annual' || v === 'yearly') return 'year';
return v;
}
const intervalNormalized = computed(() => normalizeInterval(intervalFromQuery.value))
const intervalNormalized = computed(() => normalizeInterval(intervalFromQuery.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 '';
});
const hasPlanQuery = computed(() => !!planFromQuery.value)
const hasPlanQuery = computed(() => !!planFromQuery.value);
// ============================
// Session (opcional para CTA melhor)
// ============================
const hasSession = ref(false)
const hasSession = ref(false);
async function checkSession () {
try {
const { data } = await supabase.auth.getSession()
hasSession.value = !!data?.session
} catch {
hasSession.value = 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 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)
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 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'
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 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
}
})
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
async function loadPlan() {
planRow.value = null;
if (!planFromQuery.value) return;
loading.value = true
try {
const { data, error } = await supabase
.from('v_public_pricing')
.select(`
loading.value = true;
try {
const { data, error } = await supabase
.from('v_public_pricing')
.select(
`
plan_key,
plan_name,
public_name,
@@ -120,239 +129,188 @@ async function loadPlan () {
monthly_currency,
yearly_currency,
is_visible
`)
.eq('plan_key', planFromQuery.value)
.eq('is_visible', true)
.maybeSingle()
`
)
.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
}
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 goLogin() {
router.push('/auth/login');
}
function goBackLanding () {
router.push('/lp')
function goBackLanding() {
router.push('/lp');
}
function goDashboard () {
router.push('/admin')
function goDashboard() {
router.push('/admin');
}
function goPricing () {
router.push('/lp#pricing')
function goPricing() {
router.push('/lp#pricing');
}
onMounted(async () => {
await checkSession()
await loadPlan()
})
await checkSession();
await loadPlan();
});
watch(
() => planFromQuery.value,
() => loadPlan()
)
() => 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 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>
</div>
</div>
</template>
</template>