Setup Wizard

This commit is contained in:
Leonardo
2026-03-14 19:09:44 -03:00
parent 587079e414
commit ee09b30987
16 changed files with 25276 additions and 62 deletions

View File

@@ -123,8 +123,9 @@
<div class="prof-card__sep" />
<div class="grid grid-cols-12 gap-4">
<!-- Nome -->
<div class="col-span-12 md:col-span-7">
<!-- Nome completo -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<InputText id="prof_name" v-model="form.full_name" class="w-full" autocomplete="name" @input="markDirty" />
<label for="prof_name">Nome completo</label>
@@ -132,8 +133,34 @@
<small class="prof-hint">Aparece no menu, cabeçalhos e registros.</small>
</div>
<!-- Como a Agência PSI deveria te chamar? -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<InputText id="prof_nickname" v-model="form.nickname" class="w-full" autocomplete="nickname" @input="markDirty" />
<label for="prof_nickname">Como a Agência PSI deveria te chamar?</label>
</FloatLabel>
<small class="prof-hint">Apelido ou nome preferido para comunicação.</small>
</div>
<!-- O que melhor descreve seu trabalho? -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<Select
id="prof_work_desc"
v-model="form.work_description"
:options="workDescriptionOptions"
optionLabel="label"
optionValue="value"
class="w-full"
@change="markDirty"
/>
<label for="prof_work_desc">O que melhor descreve seu trabalho?</label>
</FloatLabel>
<small class="prof-hint">Exibido no seu perfil público.</small>
</div>
<!-- E-mail -->
<div class="col-span-12 md:col-span-5">
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<InputText id="prof_email" :modelValue="userEmail" class="w-full" disabled />
<label for="prof_email">E-mail</label>
@@ -141,27 +168,44 @@
<small class="prof-hint">Gerenciado pelo Supabase Auth.</small>
</div>
<!-- Informe seu trabalho (somente quando 'outro') -->
<Transition name="prof-slide">
<div v-if="form.work_description === 'outro'" class="col-span-12">
<FloatLabel variant="on">
<InputText
id="prof_work_other"
v-model="form.work_description_other"
class="w-full"
autocomplete="off"
@input="markDirty"
/>
<label for="prof_work_other">Informe qual é o seu trabalho</label>
</FloatLabel>
<small class="prof-hint">Descreva brevemente sua atuação profissional.</small>
</div>
</Transition>
<!-- Bio -->
<div class="col-span-12 md:col-span-7">
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<Textarea
id="prof_bio"
v-model="form.bio"
class="w-full"
rows="5"
maxlength="2000"
maxlength="300"
@input="markDirty"
/>
<label for="prof_bio">Bio</label>
</FloatLabel>
<div class="prof-hint flex justify-between">
<span>Breve descrição sobre você.</span>
<span>{{ (form.bio || '').length }}/2000</span>
<span>{{ (form.bio || '').length }}/300</span>
</div>
</div>
<!-- Telefone -->
<div class="col-span-12 md:col-span-5">
<!-- Whatsapp -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<InputMask
id="prof_phone"
@@ -171,20 +215,195 @@
:autoClear="false"
@update:modelValue="markDirty"
/>
<label for="prof_phone">Telefone</label>
<label for="prof_phone">Whatsapp</label>
</FloatLabel>
<small class="prof-hint">Opcional.</small>
</div>
</div>
</div>
<!-- 02 AVATAR -->
<!-- 02 SITES E REDES SOCIAIS -->
<div
id="redes-sociais"
class="prof-card scroll-mt-20"
style="--c:#E879F9;--c-dim:rgba(232,121,249,0.08);--c-border:rgba(232,121,249,0.2)"
>
<div class="prof-card__num">02</div>
<div class="prof-card__shine" />
<div class="prof-card__head">
<div class="prof-card__icon"><i class="pi pi-share-alt" /></div>
<span class="prof-card__tag">Sites e Redes Sociais</span>
</div>
<div class="prof-card__title">Seus Sites e Redes Sociais</div>
<div class="prof-card__subtitle">Links exibidos no seu perfil público.</div>
<div class="prof-card__sep" />
<div class="grid grid-cols-12 gap-4">
<!-- Site -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-globe" />
<InputText
id="prof_site"
v-model="form.site_url"
class="w-full"
type="url"
@input="markDirty"
/>
</IconField>
<label for="prof_site">Endereço do site</label>
</FloatLabel>
<small class="prof-hint">Ex: https://seuperfil.com.br</small>
</div>
<!-- Instagram -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-instagram" />
<InputText
id="prof_instagram"
v-model="form.social_instagram"
class="w-full"
@input="markDirty"
/>
</IconField>
<label for="prof_instagram">Instagram</label>
</FloatLabel>
<small class="prof-hint">Ex: @seuperfil</small>
</div>
<!-- Youtube -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-youtube" />
<InputText
id="prof_youtube"
v-model="form.social_youtube"
class="w-full"
@input="markDirty"
/>
</IconField>
<label for="prof_youtube">YouTube</label>
</FloatLabel>
<small class="prof-hint">Ex: @seucanal</small>
</div>
<!-- Facebook -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-facebook" />
<InputText
id="prof_facebook"
v-model="form.social_facebook"
class="w-full"
@input="markDirty"
/>
</IconField>
<label for="prof_facebook">Facebook</label>
</FloatLabel>
<small class="prof-hint">Ex: /suapagina</small>
</div>
<!-- X -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<IconField>
<InputIcon class="pi pi-twitter" />
<InputText
id="prof_x"
v-model="form.social_x"
class="w-full"
@input="markDirty"
/>
</IconField>
<label for="prof_x">X (Twitter)</label>
</FloatLabel>
<small class="prof-hint">Ex: @seuuser</small>
</div>
</div>
<!-- Outras redes -->
<div class="mt-5">
<div class="flex items-center justify-between mb-3">
<div>
<div class="text-sm font-semibold text-[var(--text-color)]">Outras redes ou links</div>
<div class="text-xs text-[var(--text-color-secondary)]">Adicione qualquer outra rede social, podcast, link ou perfil.</div>
</div>
<Button
icon="pi pi-plus"
label="Adicionar"
severity="secondary"
size="small"
outlined
class="rounded-full"
@click="addCustomSocial"
/>
</div>
<div v-if="customSocials.length" class="flex flex-col gap-3">
<div
v-for="(item, idx) in customSocials"
:key="idx"
class="flex items-center gap-2"
>
<FloatLabel variant="on" class="flex-1">
<InputText
:id="`prof_cs_name_${idx}`"
v-model="item.name"
class="w-full"
@input="markDirty"
/>
<label :for="`prof_cs_name_${idx}`">Nome da rede</label>
</FloatLabel>
<FloatLabel variant="on" class="flex-[2]">
<IconField>
<InputIcon class="pi pi-link" />
<InputText
:id="`prof_cs_url_${idx}`"
v-model="item.url"
class="w-full"
@input="markDirty"
/>
</IconField>
<label :for="`prof_cs_url_${idx}`">URL / usuário</label>
</FloatLabel>
<Button
icon="pi pi-trash"
severity="danger"
text
rounded
size="small"
v-tooltip.top="'Remover'"
@click="removeCustomSocial(idx)"
/>
</div>
</div>
<p v-else class="text-xs text-[var(--text-color-secondary)] italic mt-1">
Nenhuma rede adicional cadastrada ainda.
</p>
</div>
</div>
<!-- 03 AVATAR -->
<div
id="avatar"
class="prof-card scroll-mt-20"
style="--c:#4ADE80;--c-dim:rgba(74,222,128,0.08);--c-border:rgba(74,222,128,0.2)"
>
<div class="prof-card__num">02</div>
<div class="prof-card__num">03</div>
<div class="prof-card__shine" />
<div class="prof-card__head">
@@ -272,7 +491,7 @@
class="prof-card scroll-mt-20"
style="--c:#A78BFA;--c-dim:rgba(167,139,250,0.08);--c-border:rgba(167,139,250,0.2)"
>
<div class="prof-card__num">03</div>
<div class="prof-card__num">04</div>
<div class="prof-card__shine" />
<div class="prof-card__head">
@@ -404,7 +623,7 @@
class="prof-card scroll-mt-20"
style="--c:#FB923C;--c-dim:rgba(251,146,60,0.08);--c-border:rgba(251,146,60,0.2)"
>
<div class="prof-card__num">04</div>
<div class="prof-card__num">05</div>
<div class="prof-card__shine" />
<div class="prof-card__head">
@@ -526,13 +745,13 @@
</div>
</div>
<!-- 05 SEGURANÇA -->
<!-- 07 SEGURANÇA -->
<div
id="seguranca"
class="prof-card scroll-mt-20"
style="--c:#F87171;--c-dim:rgba(248,113,113,0.08);--c-border:rgba(248,113,113,0.2)"
>
<div class="prof-card__num">05</div>
<div class="prof-card__num">07</div>
<div class="prof-card__shine" />
<div class="prof-card__head">
@@ -623,6 +842,7 @@ const { setVariant } = _useLayout()
import Textarea from 'primevue/textarea'
import InputMask from 'primevue/inputmask'
import Checkbox from 'primevue/checkbox'
import Select from 'primevue/select'
import { supabase } from '@/lib/supabase/client'
import { useLayout } from '@/layout/composables/layout'
@@ -661,10 +881,19 @@ const ui = reactive({
// Perfil
const form = reactive({
full_name: '',
nickname: '',
work_description: '',
work_description_other: '',
avatar_url: '',
bio: '',
phone: '',
site_url: '',
social_instagram: '',
social_youtube: '',
social_facebook: '',
social_x: '',
language: 'pt-BR',
timezone: 'America/Sao_Paulo',
@@ -673,13 +902,41 @@ const form = reactive({
notify_news: false
})
const customSocials = ref([])
function addCustomSocial () {
customSocials.value.push({ name: '', url: '' })
markDirty()
}
function removeCustomSocial (idx) {
customSocials.value.splice(idx, 1)
markDirty()
}
const workDescriptionOptions = [
{ label: 'Psicólogo(a) Clínico(a)', value: 'psicologo_clinico' },
{ label: 'Psicanalista', value: 'psicanalista' },
{ label: 'Psiquiatra', value: 'psiquiatra' },
{ label: 'Psicoterapeuta', value: 'psicoterapeuta' },
{ label: 'Neuropsicólogo(a)', value: 'neuropsicologo' },
{ label: 'Psicólogo(a) Organizacional', value: 'psicologo_organizacional' },
{ label: 'Psicólogo(a) Escolar / Educacional', value: 'psicologo_escolar' },
{ label: 'Psicólogo(a) Hospitalar', value: 'psicologo_hospitalar' },
{ label: 'Psicólogo(a) Jurídico(a)', value: 'psicologo_juridico' },
{ label: 'Coach / Mentor(a)', value: 'coach_mentor' },
{ label: 'Terapeuta Holístico(a)', value: 'terapeuta_holistico' },
{ label: 'Outro', value: 'outro' },
]
const sections = [
{ id: 'conta', label: 'Conta', icon: 'pi pi-user' },
{ id: 'avatar', label: 'Avatar', icon: 'pi pi-image' },
{ id: 'layout', label: 'Aparência', icon: 'pi pi-palette' },
{ id: 'preferencias', label: 'Preferências', icon: 'pi pi-sliders-h' },
{ id: 'seguranca', label: 'Segurança', icon: 'pi pi-shield' },
{ id: 'layout-variant', label: 'Layout', icon: 'pi pi-th-large' }
{ id: 'conta', label: 'Conta', icon: 'pi pi-user' },
{ id: 'redes-sociais', label: 'Sites e Redes', icon: 'pi pi-share-alt' },
{ id: 'avatar', label: 'Avatar', icon: 'pi pi-image' },
{ id: 'layout', label: 'Aparência', icon: 'pi pi-palette' },
{ id: 'preferencias', label: 'Preferências', icon: 'pi pi-sliders-h' },
{ id: 'layout-variant', label: 'Layout', icon: 'pi pi-th-large' },
{ id: 'seguranca', label: 'Segurança', icon: 'pi pi-shield' },
]
const activeSection = ref('conta')
@@ -968,13 +1225,15 @@ async function loadUserSettings (uid) {
if (settings.surface_color && !safeEq(settings.surface_color, layoutConfig.surface)) layoutConfig.surface = settings.surface_color
if (settings.menu_mode && !safeEq(settings.menu_mode, layoutConfig.menuMode)) {
layoutConfig.menuMode = settings.menu_mode
try { changeMenuMode?.(settings.menu_mode) } catch {
try { changeMenuMode?.({ value: settings.menu_mode }) } catch {}
}
// Não chama changeMenuMode() — ela reseta staticMenuInactive e outros estados,
// fazendo a sidebar desaparecer ao entrar na página.
}
// layout variant
if (settings.layout_variant === 'rail' || settings.layout_variant === 'classic') {
// layout variant — só aplica se mudou, para não resetar o estado do layout
if (
(settings.layout_variant === 'rail' || settings.layout_variant === 'classic') &&
settings.layout_variant !== layoutConfig.variant
) {
setVariant(settings.layout_variant)
}
@@ -1026,7 +1285,7 @@ async function loadProfile () {
const { data: prof, error: pErr } = await supabase
.from('profiles')
.select('full_name, avatar_url, phone, bio, language, timezone, notify_system_email, notify_reminders, notify_news')
.select('full_name, avatar_url, phone, bio, nickname, work_description, work_description_other, site_url, social_instagram, social_youtube, social_facebook, social_x, social_custom, language, timezone, notify_system_email, notify_reminders, notify_news')
.eq('id', user.id)
.maybeSingle()
@@ -1035,6 +1294,18 @@ async function loadProfile () {
form.avatar_url = prof.avatar_url ?? form.avatar_url
form.phone = prof.phone ?? ''
form.bio = prof.bio ?? ''
form.nickname = prof.nickname ?? ''
form.work_description = prof.work_description ?? ''
form.work_description_other = prof.work_description_other ?? ''
form.site_url = prof.site_url ?? ''
form.social_instagram = prof.social_instagram ?? ''
form.social_youtube = prof.social_youtube ?? ''
form.social_facebook = prof.social_facebook ?? ''
form.social_x = prof.social_x ?? ''
if (Array.isArray(prof.social_custom)) {
customSocials.value = prof.social_custom
}
form.language = prof.language ?? form.language
form.timezone = prof.timezone ?? form.timezone
@@ -1087,6 +1358,15 @@ async function saveAll () {
avatar_url: metaPayload.avatar_url,
phone: String(form.phone || '').trim() || null,
bio: String(form.bio || '').trim() || null,
nickname: String(form.nickname || '').trim() || null,
work_description: String(form.work_description || '').trim() || null,
work_description_other: form.work_description === 'outro' ? (String(form.work_description_other || '').trim() || null) : null,
site_url: String(form.site_url || '').trim() || null,
social_instagram: String(form.social_instagram || '').trim() || null,
social_youtube: String(form.social_youtube || '').trim() || null,
social_facebook: String(form.social_facebook || '').trim() || null,
social_x: String(form.social_x || '').trim() || null,
social_custom: customSocials.value.filter(s => s.name || s.url),
language: form.language || 'pt-BR',
timezone: form.timezone || 'America/Sao_Paulo',
@@ -1100,7 +1380,7 @@ async function saveAll () {
.from('profiles')
.update(profilePayload)
.eq('id', userId.value)
.select('id, role, full_name, avatar_url, phone, bio, language, timezone, notify_system_email, notify_reminders, notify_news, updated_at')
.select('id, role, full_name, avatar_url, phone, bio, nickname, work_description, work_description_other, site_url, social_instagram, social_youtube, social_facebook, social_x, social_custom, language, timezone, notify_system_email, notify_reminders, notify_news, updated_at')
.single()
if (pErr2) {
@@ -1644,4 +1924,39 @@ onBeforeUnmount(() => {
from { opacity: 0; transform: translateY(14px); }
to { opacity: 1; transform: translateY(0); }
}
/* ─── Social addons ─────────────────────────────────────── */
.social-addon {
display: flex; align-items: center; justify-content: center;
width: 2.75rem; flex-shrink: 0;
font-size: 0.95rem;
}
.social-addon--site { color: var(--text-color-secondary); }
.social-addon--instagram { color: #E1306C; }
.social-addon--youtube { color: #FF0000; }
.social-addon--facebook { color: #1877F2; }
.social-addon--x { color: var(--text-color); }
/* FloatLabel com inputgroup: label offset pelo addon */
.social-float-label {
left: 2.85rem !important;
}
/* Fix FloatLabel wrapping inputgroup */
.p-floatlabel:has(.p-inputgroup) { display: block; }
.p-floatlabel .p-inputgroup { width: 100%; }
.p-floatlabel .p-inputgroup .p-inputtext {
border-radius: 0 var(--p-inputtext-border-radius, 8px) var(--p-inputtext-border-radius, 8px) 0 !important;
}
/* ─── Transition "outro" ────────────────────────────────── */
.prof-slide-enter-active,
.prof-slide-leave-active {
transition: opacity 0.2s ease, max-height 0.25s ease, margin 0.2s ease;
max-height: 6rem; overflow: hidden;
}
.prof-slide-enter-from,
.prof-slide-leave-to {
opacity: 0; max-height: 0; margin: 0;
}
</style>