Files
agenciapsilmno/src/features/setup/SetupWizardPage - Copia.vue

1808 lines
109 KiB
Vue

<!--
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/features/setup/SetupWizardPage.vue
| Data: 2026
| Local: São Carlos/SP Brasil
|--------------------------------------------------------------------------
| © 2026 Todos os direitos reservados
|--------------------------------------------------------------------------
-->
<script setup>
import { useInsurancePlans } from '@/features/agenda/composables/useInsurancePlans';
import { useServices } from '@/features/agenda/composables/useServices';
import { useLayout } from '@/layout/composables/layout';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import { applyThemeEngine } from '@/theme/theme.options';
import InputMask from 'primevue/inputmask';
import Step from 'primevue/step';
import StepList from 'primevue/steplist';
import StepPanel from 'primevue/steppanel';
import StepPanels from 'primevue/steppanels';
import Stepper from 'primevue/stepper';
import { useToast } from 'primevue/usetoast';
import { computed, onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const toast = useToast();
const { toggleDarkMode, isDarkTheme, layoutConfig } = useLayout();
function isDarkNow() {
return document.documentElement.classList.contains('app-dark');
}
async function toggleTheme() {
toggleDarkMode();
try {
const {
data: { user }
} = await supabase.auth.getUser();
if (!user?.id) return;
await supabase.from('user_settings').upsert({ user_id: user.id, theme_mode: isDarkNow() ? 'dark' : 'light' }, { onConflict: 'user_id' });
} catch {}
}
const tenant = useTenantStore();
const { services, load: loadServices, save: saveService, remove: removeSvc } = useServices();
const { plans: insurancePlans, load: loadPlans, save: savePlanItem, removeDefinitivo: removePlanItem } = useInsurancePlans();
const uid = ref(null);
const tenantId = computed(() => tenant.activeTenantId);
const isClinicRole = computed(() => {
const r = tenant.activeRole || '';
return r === 'clinic_admin' || r.startsWith('clinic') || (r === 'tenant_admin' && tenant.memberships?.find((m) => m.tenant_id === tenant.activeTenantId)?.kind?.startsWith('clinic'));
});
const agendaRoute = computed(() => (isClinicRole.value ? '/admin/agenda/clinica' : '/therapist/agenda'));
const configRoute = computed(() => (isClinicRole.value ? '/admin' : '/configuracoes'));
const userEmail = ref('');
const done = ref(false);
// ── 3 STEPS ───────────────────────────────────────────────────────────────
const steps = [
{ id: 'voce', label: 'Você', icon: 'pi-user', color: '#60A5FA', macroAt: 40, description: 'Seu perfil básico.' },
{ id: 'negocio', label: 'Negócio', icon: 'pi-building', color: '#4ADE80', macroAt: 70, description: 'Crie seu negócio.' },
{ id: 'atendimento', label: 'Atendimento', icon: 'pi-heart', color: '#A78BFA', macroAt: 100, description: 'Como você atende.' }
];
const currentIdx = ref(0);
const currentStep = computed(() => steps[currentIdx.value]);
const firstName = computed(() => perfil.value.fullName?.split(' ')[0] || 'você');
async function navigateTo(idx) {
if (idx < 0 || idx >= steps.length || idx === currentIdx.value) return;
if (idx > currentIdx.value) {
const id = currentStep.value?.id;
if (id === 'voce') {
if (!validateStepForward('voce')) return;
await saveVoce(true);
}
if (id === 'negocio') {
if (!validateStepForward('negocio')) return;
await saveNegocio(true);
}
if (id === 'atendimento' && atendimento.value.mode) await saveAtendimento(true);
}
currentIdx.value = idx;
}
// ── STEP STATES ────────────────────────────────────────────────────────────
const stepStates = ref({ voce: 'not_started', negocio: 'not_started', atendimento: 'not_started' });
function markStarted(id) {
if (stepStates.value[id] === 'not_started') stepStates.value[id] = 'started';
}
function markSaved(id) {
stepStates.value[id] = 'saved';
}
// ── HYBRID PROGRESS ────────────────────────────────────────────────────────
const progressPct = computed(() => {
let p = 0;
if (stepStates.value.voce === 'saved') {
p += 40;
} else {
const filled = [perfil.value.fullName.trim().split(/\s+/).filter(Boolean).length >= 2, !!perfil.value.nickname.trim(), String(perfil.value.whatsapp || '').replace(/\D/g, '').length >= 10].filter(Boolean).length;
p += Math.round((filled / 3) * 40);
}
if (stepStates.value.negocio === 'saved') {
p += 30;
} else if (stepStates.value.voce === 'saved') {
const filled = [!!negocio.value.name.trim(), !!negocio.value.type].filter(Boolean).length;
p += Math.round((filled / 2) * 30);
}
if (stepStates.value.atendimento === 'saved') {
p += 30;
} else if (stepStates.value.negocio === 'saved' && atendimento.value.mode) {
p += 30;
}
let bonus = 0;
if (perfil.value.avatarPreview) bonus += 10;
if (perfil.value.bio?.trim()) bonus += 5;
if (layoutConfig.primary && layoutConfig.primary !== 'noir') bonus += 2;
if (isDarkTheme.value) bonus += 2;
if (negocio.value.logoPreview) bonus += 10;
if (negocio.value.logradouro?.trim() && negocio.value.cidade?.trim()) bonus += 5;
if (negocio.value.phone?.trim()) bonus += 3;
if (negocio.value.siteUrl?.trim() || negocio.value.instagram?.trim()) bonus += 2;
return Math.min(130, p + bonus);
});
const progressMsg = computed(() => {
const p = progressPct.value;
if (p > 100) return 'Perfil otimizado! 🎉';
if (p >= 100) return 'Tudo pronto! ✨';
if (p >= 70) return 'Quase lá! Último passo 🚀';
if (p >= 40) return 'Bom começo! Configure seu negócio';
if (p > 0) return 'Preenchendo seu perfil...';
return 'Vamos começar! 👋';
});
const canFinish = computed(() => steps.every((s) => stepStates.value[s.id] === 'saved'));
const finishing = ref(false);
const saving = ref({ voce: false, negocio: false, atendimento: false });
const isSaving = computed(() => saving.value[currentStep.value?.id] ?? false);
async function saveCurrentStep() {
const id = currentStep.value?.id;
if (id === 'voce') return saveVoce();
if (id === 'negocio') return saveNegocio();
if (id === 'atendimento') return saveAtendimento();
}
// ── AUTOSAVE ───────────────────────────────────────────────────────────────
let autoSaveTimer = null;
function scheduleAutosave(stepId) {
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => {
if (stepId === 'voce' && canSaveVoce.value) saveVoce(true);
if (stepId === 'negocio' && canSaveNegocio.value) saveNegocio(true);
if (stepId === 'atendimento' && atendimento.value.mode) saveAtendimento(true);
}, 2500);
}
// ── THEME / COLORS ─────────────────────────────────────────────────────────
const swRootEl = ref(null);
const primaryColors = [
{ name: 'noir', palette: { 500: 'currentColor' } },
{ name: 'emerald', palette: { 500: '#10b981' } },
{ name: 'green', palette: { 500: '#22c55e' } },
{ name: 'lime', palette: { 500: '#84cc16' } },
{ name: 'orange', palette: { 500: '#f97316' } },
{ name: 'amber', palette: { 500: '#f59e0b' } },
{ name: 'yellow', palette: { 500: '#eab308' } },
{ name: 'teal', palette: { 500: '#14b8a6' } },
{ name: 'cyan', palette: { 500: '#06b6d4' } },
{ name: 'sky', palette: { 500: '#0ea5e9' } },
{ name: 'blue', palette: { 500: '#3b82f6' } },
{ name: 'indigo', palette: { 500: '#6366f1' } },
{ name: 'violet', palette: { 500: '#8b5cf6' } },
{ name: 'purple', palette: { 500: '#a855f7' } },
{ name: 'fuchsia', palette: { 500: '#d946ef' } },
{ name: 'pink', palette: { 500: '#ec4899' } },
{ name: 'rose', palette: { 500: '#f43f5e' } }
];
function getPrimaryHex(name) {
if (!name || name === 'noir') return '#18181b';
return primaryColors.find((c) => c.name === name)?.palette['500'] || '#6154e8';
}
function applyWizardPrimary(name) {
const el = swRootEl.value;
if (!el) return;
const hex = getPrimaryHex(name);
const r = parseInt(hex.slice(1, 3), 16),
g = parseInt(hex.slice(3, 5), 16),
b = parseInt(hex.slice(5, 7), 16);
el.style.setProperty('--sw-primary', hex);
el.style.setProperty('--sw-primary-dim', `rgba(${r},${g},${b},0.12)`);
el.style.setProperty('--sw-primary-border', `rgba(${r},${g},${b},0.30)`);
el.style.setProperty('--sw-primary-rgb', `${r},${g},${b}`);
}
watch(() => layoutConfig.primary, applyWizardPrimary);
function updatePrimaryColor(pc) {
layoutConfig.primary = pc.name;
applyThemeEngine(layoutConfig);
applyWizardPrimary(pc.name);
markStarted('voce');
scheduleAutosave('voce');
}
const themeOptions = [
{ label: 'Claro', value: 'light' },
{ label: 'Escuro', value: 'dark' }
];
const themeModel = computed(() => (isDarkTheme.value ? 'dark' : 'light'));
async function onThemeChange(val) {
if ((val === 'dark') !== isDarkNow()) toggleDarkMode();
try {
const {
data: { user }
} = await supabase.auth.getUser();
if (!user?.id) return;
await supabase.from('user_settings').upsert({ user_id: user.id, theme_mode: val }, { onConflict: 'user_id' });
} catch {}
markStarted('voce');
scheduleAutosave('voce');
}
// ── PERFIL (VOCÊ) ──────────────────────────────────────────────────────────
const avatarInput = ref(null);
const perfil = ref({
fullName: '',
nickname: '',
workDescription: '',
workDescriptionOther: '',
bio: '',
whatsapp: '',
avatarUrl: null,
avatarPreview: null,
avatarFile: null
});
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', value: 'psicologo_escolar' },
{ label: 'Psicólogo(a) Hospitalar', value: 'psicologo_hospitalar' },
{ label: 'Coach / Mentor(a)', value: 'coach_mentor' },
{ label: 'Terapeuta Holístico(a)', value: 'terapeuta_holistico' },
{ label: 'Outro', value: 'outro' }
];
const initials = computed(() => {
const p = String(perfil.value.fullName || '')
.trim()
.split(/\s+/)
.filter(Boolean);
if (!p.length) return '?';
return p.length === 1 ? p[0].slice(0, 2).toUpperCase() : (p[0][0] + p[p.length - 1][0]).toUpperCase();
});
const canSaveVoce = computed(() => perfil.value.fullName.trim().length > 0 && perfil.value.nickname.trim().length > 0 && String(perfil.value.whatsapp || '').replace(/\D/g, '').length >= 10);
// ── VALIDAÇÃO INLINE ────────────────────────────────────────────────────────
const fieldErrors = ref({});
function clearErr(key) {
fieldErrors.value = { ...fieldErrors.value, [key]: null };
}
function validateStepForward(id) {
fieldErrors.value = {};
if (id === 'voce') {
const e = {};
const nameParts = perfil.value.fullName.trim().split(/\s+/).filter(Boolean);
if (!nameParts.length) e.fullName = 'Nome completo é obrigatório.';
else if (nameParts.length < 2) e.fullName = 'Informe nome e sobrenome.';
if (!perfil.value.nickname.trim()) e.nickname = 'Apelido é obrigatório.';
if (String(perfil.value.whatsapp || '').replace(/\D/g, '').length < 10) e.whatsapp = 'WhatsApp é obrigatório.';
fieldErrors.value = e;
return !Object.keys(e).length;
}
if (id === 'negocio') {
const e = {};
if (!negocio.value.name.trim()) e.negocioName = 'Nome do negócio é obrigatório.';
if (!negocio.value.type) e.negocioType = 'Selecione o tipo do negócio.';
fieldErrors.value = e;
return !Object.keys(e).length;
}
return true;
}
function triggerAvatarUpload() {
avatarInput.value?.click();
}
function onAvatarChange(e) {
const file = e.target.files?.[0];
if (!file) return;
perfil.value.avatarFile = file;
perfil.value.avatarPreview = URL.createObjectURL(file);
markStarted('voce');
scheduleAutosave('voce');
}
// ── NEGÓCIO ────────────────────────────────────────────────────────────────
const logoInput = ref(null);
const negocio = ref({
name: '',
type: '',
logoFile: null,
logoPreview: null,
logoUrl: null,
cep: '',
logradouro: '',
numero: '',
complemento: '',
bairro: '',
cidade: '',
estado: '',
phone: '',
email: '',
siteUrl: '',
instagram: ''
});
const fetchingCep = ref(false);
async function fetchCep(cepRaw) {
const cep = String(cepRaw ?? '').replace(/\D/g, '');
if (cep.length !== 8) return null;
const res = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
const data = await res.json();
if (!data || data.erro) return null;
return data;
}
async function onCepBlur() {
fetchingCep.value = true;
try {
const d = await fetchCep(negocio.value.cep);
if (!d) return;
negocio.value.logradouro = d.logradouro || negocio.value.logradouro;
negocio.value.bairro = d.bairro || negocio.value.bairro;
negocio.value.cidade = d.localidade || negocio.value.cidade;
negocio.value.estado = d.uf || negocio.value.estado;
if (!negocio.value.complemento) negocio.value.complemento = d.complemento || '';
markStarted('negocio');
scheduleAutosave('negocio');
} catch (_) {
} finally {
fetchingCep.value = false;
}
}
const businessTypes = [
{ label: 'Consultório', value: 'consultorio', icon: 'pi-user' },
{ label: 'Clínica', value: 'clinica', icon: 'pi-building' },
{ label: 'Instituto', value: 'instituto', icon: 'pi-home' },
{ label: 'Grupo', value: 'grupo', icon: 'pi-users' }
];
const negocioInitials = computed(() => {
const p = String(negocio.value.name || '')
.trim()
.split(/\s+/)
.filter(Boolean);
if (!p.length) return '?';
return p.length === 1 ? p[0].slice(0, 2).toUpperCase() : (p[0][0] + p[p.length - 1][0]).toUpperCase();
});
const canSaveNegocio = computed(() => negocio.value.name.trim().length > 0 && negocio.value.type.length > 0);
function triggerLogoUpload() {
logoInput.value?.click();
}
function onLogoChange(e) {
const file = e.target.files?.[0];
if (!file) return;
negocio.value.logoFile = file;
negocio.value.logoPreview = URL.createObjectURL(file);
markStarted('negocio');
scheduleAutosave('negocio');
}
// ── ATENDIMENTO ────────────────────────────────────────────────────────────
const atendimento = ref({ mode: null });
const isConveniado = computed(() => atendimento.value.mode === 'convenio' || atendimento.value.mode === 'ambos');
const isParticular = computed(() => atendimento.value.mode === 'particular' || atendimento.value.mode === 'ambos');
const atendimentoModes = [
{ value: 'particular', label: 'Particular', icon: 'pi-wallet', desc: 'Apenas atendimentos privados.' },
{ value: 'convenio', label: 'Convênio', icon: 'pi-id-card', desc: 'Aceito planos de saúde.' },
{ value: 'ambos', label: 'Ambos', icon: 'pi-check-square', desc: 'Particular e convênio.' }
];
const newService = ref({ id: null, name: '', price: null, duration_min: 50 });
const newPlan = ref({ id: null, name: '', notes: '' });
function editService(svc) {
newService.value = { id: svc.id, name: svc.name, price: svc.price, duration_min: svc.duration_min ?? 50 };
}
function cancelEditService() {
newService.value = { id: null, name: '', price: null, duration_min: 50 };
}
function formatCurrency(v) {
if (v == null) return '—';
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(v);
}
// ── CONFETTI ───────────────────────────────────────────────────────────────
function confettiStyle(i) {
const colors = ['#7C6AF7', '#60A5FA', '#4ADE80', '#FB923C', '#F43F5E', '#C084FC', '#FCD34D'];
return { '--x': `${Math.random() * 100}%`, '--color': colors[i % colors.length], '--delay': `${(i * 0.12).toFixed(2)}s`, '--rot': `${Math.floor(Math.random() * 360)}deg` };
}
// ── LOAD ───────────────────────────────────────────────────────────────────
onMounted(async () => {
await tenant.ensureLoaded();
const {
data: { user }
} = await supabase.auth.getUser();
uid.value = user?.id || null;
if (!uid.value) return;
userEmail.value = user.email || '';
const { data: cfg } = await supabase.from('agenda_configuracoes').select('setup_concluido, setup_clinica_concluido, atendimento_mode').eq('owner_id', uid.value).maybeSingle();
if (cfg && (cfg.setup_concluido || cfg.setup_clinica_concluido || !!cfg.atendimento_mode)) {
router.replace('/pages/notfound');
return;
}
applyWizardPrimary(layoutConfig.primary);
await Promise.all([loadVoce(), loadNegocio(), loadAtendimento()]);
});
async function loadVoce() {
const { data } = await supabase.from('profiles').select('full_name,avatar_url,bio,nickname,work_description,work_description_other,phone').eq('id', uid.value).maybeSingle();
if (!data) return;
perfil.value.fullName = data.full_name || '';
perfil.value.bio = data.bio || '';
perfil.value.nickname = data.nickname || '';
perfil.value.workDescription = data.work_description || '';
perfil.value.workDescriptionOther = data.work_description_other || '';
perfil.value.whatsapp = data.phone || '';
perfil.value.avatarUrl = data.avatar_url || null;
perfil.value.avatarPreview = data.avatar_url || null;
if (data.full_name && data.nickname && data.phone) markSaved('voce');
else if (data.full_name) markStarted('voce');
}
async function loadNegocio() {
if (!tenantId.value) return;
// Fonte única: company_profiles
const { data } = await supabase.from('company_profiles').select('nome_fantasia,tipo_empresa,logo_url,cep,logradouro,numero,complemento,bairro,cidade,estado,telefone,email,site,redes_sociais').eq('tenant_id', tenantId.value).maybeSingle();
if (!data) return;
negocio.value.name = data.nome_fantasia || '';
negocio.value.type = data.tipo_empresa || '';
let resolvedLogo = data.logo_url || null;
if (resolvedLogo && !resolvedLogo.startsWith('http')) {
const {
data: { publicUrl }
} = supabase.storage.from('logos').getPublicUrl(resolvedLogo);
resolvedLogo = publicUrl;
}
negocio.value.logoUrl = resolvedLogo;
negocio.value.logoPreview = resolvedLogo;
negocio.value.cep = data.cep || '';
negocio.value.logradouro = data.logradouro || '';
negocio.value.numero = data.numero || '';
negocio.value.complemento = data.complemento || '';
negocio.value.bairro = data.bairro || '';
negocio.value.cidade = data.cidade || '';
negocio.value.estado = data.estado || '';
negocio.value.phone = data.telefone || '';
negocio.value.email = data.email || '';
negocio.value.siteUrl = data.site || '';
// instagram: busca em redes_sociais[]
const redes = Array.isArray(data.redes_sociais) ? data.redes_sociais : [];
const igRede = redes.find((r) => /instagram/i.test(r.name || ''));
negocio.value.instagram = igRede?.url || '';
if (data.nome_fantasia && data.tipo_empresa) markSaved('negocio');
else if (data.nome_fantasia) markStarted('negocio');
}
async function loadAtendimento() {
if (!uid.value) return;
const { data } = await supabase.from('agenda_configuracoes').select('atendimento_mode').eq('owner_id', uid.value).maybeSingle();
if (data?.atendimento_mode) {
atendimento.value.mode = data.atendimento_mode;
markSaved('atendimento');
}
await loadServices(uid.value);
await loadPlans(uid.value);
}
// ── SAVE VOCE ──────────────────────────────────────────────────────────────
async function saveVoce(silent = false) {
if (!canSaveVoce.value) {
if (!silent) toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Nome completo, apelido e WhatsApp são obrigatórios.', life: 3500 });
return;
}
saving.value.voce = true;
try {
let avatarUrl = perfil.value.avatarUrl;
if (perfil.value.avatarFile) {
const ext = perfil.value.avatarFile.name.split('.').pop();
const path = `${uid.value}/avatar.${ext}`;
const { error: upErr } = await supabase.storage.from('avatars').upload(path, perfil.value.avatarFile, { upsert: true });
if (upErr) throw upErr;
const {
data: { publicUrl }
} = supabase.storage.from('avatars').getPublicUrl(path);
avatarUrl = publicUrl;
perfil.value.avatarUrl = avatarUrl;
}
const wd = String(perfil.value.workDescription || '').trim() || null;
await supabase
.from('profiles')
.update({
full_name: perfil.value.fullName.trim(),
bio: perfil.value.bio.trim() || null,
avatar_url: avatarUrl || null,
nickname: perfil.value.nickname.trim() || null,
work_description: wd,
work_description_other: wd === 'outro' ? perfil.value.workDescriptionOther.trim() || null : null,
phone: perfil.value.whatsapp || null
})
.eq('id', uid.value);
await supabase.from('user_settings').upsert(
{
user_id: uid.value,
primary_color: layoutConfig.primary || 'noir',
theme_mode: isDarkNow() ? 'dark' : 'light',
updated_at: new Date().toISOString()
},
{ onConflict: 'user_id' }
);
markSaved('voce');
if (!silent) toast.add({ severity: 'success', summary: 'Perfil salvo!', detail: `Progresso: ${progressPct.value}%`, life: 2500 });
} catch (e) {
if (!silent) toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
} finally {
saving.value.voce = false;
}
}
// ── SAVE NEGOCIO ──────────────────────────────────────────────────────────
async function saveNegocio(silent = false) {
if (!canSaveNegocio.value) {
if (!silent) toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Nome e tipo do negócio são obrigatórios.', life: 3000 });
return;
}
if (!tenantId.value) {
if (!silent) toast.add({ severity: 'error', summary: 'Erro', detail: 'Tenant não identificado. Tente recarregar a página.', life: 4000 });
return;
}
saving.value.negocio = true;
try {
// Upload logo se selecionado
let logoUrl = negocio.value.logoUrl;
if (negocio.value.logoFile) {
const ext = negocio.value.logoFile.name.split('.').pop();
const path = `${tenantId.value}/logo.${ext}`;
const { error: upErr } = await supabase.storage.from('logos').upload(path, negocio.value.logoFile, { upsert: true });
if (upErr) throw upErr;
const {
data: { publicUrl }
} = supabase.storage.from('logos').getPublicUrl(path);
logoUrl = publicUrl;
negocio.value.logoUrl = logoUrl;
}
// Instagram salvo em redes_sociais[]
const redes = [];
if (negocio.value.instagram?.trim()) {
redes.push({ name: 'Instagram', url: negocio.value.instagram.trim() });
}
// Fonte única: company_profiles (upsert garante insert ou update)
const { error } = await supabase.from('company_profiles').upsert(
{
tenant_id: tenantId.value,
nome_fantasia: negocio.value.name.trim() || null,
tipo_empresa: negocio.value.type || null,
logo_url: logoUrl || null,
cep: negocio.value.cep?.trim() || null,
logradouro: negocio.value.logradouro?.trim() || null,
numero: negocio.value.numero?.trim() || null,
complemento: negocio.value.complemento?.trim() || null,
bairro: negocio.value.bairro?.trim() || null,
cidade: negocio.value.cidade?.trim() || null,
estado: negocio.value.estado?.trim() || null,
telefone: negocio.value.phone?.trim() || null,
email: negocio.value.email?.trim() || null,
site: negocio.value.siteUrl?.trim() || null,
redes_sociais: redes
},
{ onConflict: 'tenant_id' }
);
if (error) throw error;
markSaved('negocio');
if (!silent) toast.add({ severity: 'success', summary: 'Negócio salvo!', detail: `Progresso: ${progressPct.value}%`, life: 2500 });
} catch (e) {
if (!silent) toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
} finally {
saving.value.negocio = false;
}
}
// ── SAVE ATENDIMENTO ──────────────────────────────────────────────────────
async function saveAtendimento(silent = false) {
if (!atendimento.value.mode) {
if (!silent) toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Selecione como você atende seus clientes.', life: 3000 });
return;
}
saving.value.atendimento = true;
try {
if (isParticular.value && services.value.length === 0) {
await saveService({ name: 'Atendimento padrão', price: 0, duration_min: 50, owner_id: uid.value, tenant_id: tenantId.value });
await loadServices(uid.value);
}
const { error } = await supabase.from('agenda_configuracoes').upsert(
{
owner_id: uid.value,
tenant_id: tenantId.value,
atendimento_mode: atendimento.value.mode,
updated_at: new Date().toISOString()
},
{ onConflict: 'owner_id' }
);
if (error) throw error;
markSaved('atendimento');
if (!silent) toast.add({ severity: 'success', summary: 'Atendimento configurado!', detail: '100% concluído — pronto para ativar!', life: 3000 });
} catch (e) {
if (!silent) toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
} finally {
saving.value.atendimento = false;
}
}
// ── SERVICES & PLANS ──────────────────────────────────────────────────────
async function saveServiceItem() {
if (!newService.value.name.trim()) {
toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Nome obrigatório.', life: 3000 });
return;
}
try {
const { id: svcId, ...svcFields } = newService.value;
const payload = { ...svcFields, price: svcFields.price ?? 0, owner_id: uid.value, tenant_id: tenantId.value };
if (svcId) payload.id = svcId;
await saveService(payload);
await loadServices(uid.value);
cancelEditService();
if (atendimento.value.mode) await saveAtendimento(true);
toast.add({ severity: 'success', summary: svcId ? 'Serviço atualizado' : 'Serviço adicionado', life: 2000 });
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
}
}
async function removeServiceItem(id) {
try {
await removeSvc(id);
toast.add({ severity: 'info', summary: 'Serviço removido', life: 2000 });
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
}
}
async function savePlanItemWizard() {
if (!newPlan.value.name.trim()) {
toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Nome obrigatório.', life: 3000 });
return;
}
try {
await savePlanItem({ ...newPlan.value, owner_id: uid.value, tenant_id: tenantId.value });
newPlan.value = { id: null, name: '', notes: '' };
await loadPlans(uid.value);
if (atendimento.value.mode) await saveAtendimento(true);
toast.add({ severity: 'success', summary: 'Convênio adicionado', life: 2000 });
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
}
}
async function removePlanItemWizard(id) {
try {
await removePlanItem(id);
await loadPlans(uid.value);
toast.add({ severity: 'info', summary: 'Convênio removido', life: 2000 });
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
}
}
// ── FINISH ─────────────────────────────────────────────────────────────────
async function onFinish() {
if (!canFinish.value) return;
finishing.value = true;
try {
const now = new Date().toISOString();
const { error: finErr } = await supabase.from('agenda_configuracoes').upsert({ owner_id: uid.value, tenant_id: tenantId.value, setup_concluido: true, setup_concluido_em: now }, { onConflict: 'owner_id' });
if (finErr) throw finErr;
done.value = true;
} catch (e) {
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 4000 });
} finally {
finishing.value = false;
}
}
</script>
<template>
<div ref="swRootEl" class="sw-root relative min-h-screen w-full flex flex-col items-center overflow-x-hidden bg-[--sw-bg] text-[--sw-text] transition-colors duration-300">
<!-- BG Blobs -->
<div class="fixed inset-0 pointer-events-none z-0 overflow-hidden" aria-hidden="true">
<div class="sw-blob sw-blob--a absolute rounded-full opacity-[0.38] dark:opacity-[0.42]" />
<div class="sw-blob sw-blob--b absolute rounded-full opacity-[0.38] dark:opacity-[0.42]" />
<div class="sw-noise absolute inset-0 opacity-30" />
</div>
<!-- Theme toggle -->
<button
class="fixed top-5 right-5 z-[300] w-10 h-10 rounded-full bg-white/85 dark:bg-white/5 backdrop-blur-xl border border-black/10 dark:border-white/10 text-black/50 dark:text-white/50 flex items-center justify-center cursor-pointer transition-all duration-200 hover:text-[--sw-primary] hover:border-[--sw-primary-border] hover:shadow-[0_0_0_3px_var(--sw-primary-dim)]"
@click="toggleTheme"
:title="isDarkTheme ? 'Modo claro' : 'Modo escuro'"
>
<i :class="['pi text-sm', isDarkTheme ? 'pi-sun' : 'pi-moon']" />
</button>
<!-- DONE SCREEN -->
<Transition name="fade">
<div v-if="done" class="fixed inset-0 z-50 flex items-center justify-center bg-[--sw-bg]/80 backdrop-blur-lg">
<!-- Confetti -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<span v-for="i in 32" :key="i" class="sw-confetti" :style="confettiStyle(i)" />
</div>
<div class="relative bg-white/90 dark:bg-white/[0.06] backdrop-blur-xl border border-black/10 dark:border-white/10 rounded-3xl p-10 max-w-lg w-full mx-4 text-center shadow-2xl overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-br from-white/30 to-transparent dark:from-white/5 pointer-events-none rounded-3xl" />
<div class="relative z-10">
<div class="text-6xl mb-4">🎉</div>
<h1 class="text-3xl font-black tracking-tight mb-2">Tudo pronto, {{ firstName }}!</h1>
<p class="text-black/50 dark:text-white/50 mb-6">Seu sistema está ativo e pronto para uso.</p>
<div class="flex flex-wrap justify-center gap-2 mb-8">
<span v-for="s in steps" :key="s.id" v-show="stepStates[s.id] === 'saved'" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold bg-[--sw-primary-dim] text-[--sw-primary]">
<i :class="`pi ${s.icon}`" /> {{ s.label }}
</span>
</div>
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<button class="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-xl font-semibold text-white bg-[--sw-primary] hover:brightness-110 transition-all shadow-lg cursor-pointer" @click="router.push(agendaRoute)">
<i class="pi pi-calendar" /> Ir para a Agenda
</button>
<button
class="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-xl font-semibold text-[--sw-primary] bg-[--sw-primary-dim] hover:bg-[--sw-primary-border] transition-all cursor-pointer"
@click="router.push(configRoute)"
>
<i class="pi pi-cog" /> Configurações
</button>
</div>
</div>
</div>
</div>
</Transition>
<!-- WIZARD -->
<div v-if="!done" class="relative z-[1] w-full min-h-screen flex flex-col items-center px-4 lg:px-8 pt-10 pb-32 max-w-7xl mx-auto">
<!-- Header -->
<header class="text-center w-full mb-8 animate-[fadeDown_0.55s_ease_both]">
<p class="text-[0.7rem] uppercase tracking-[0.18em] text-[--sw-primary] opacity-80 mb-2">Agência PSI · Ativação</p>
<h1 class="text-[clamp(1.8rem,4.5vw,3rem)] font-black tracking-tighter leading-none mb-2">
👋 Olá, <span class="bg-gradient-to-br from-violet-400 to-blue-400 bg-clip-text text-transparent">{{ perfil.fullName || 'bem-vindo' }}</span>
</h1>
<h4 class="text-[clamp(0.95rem,2.2vw,1.15rem)] font-normal text-black/50 dark:text-white/50">Vamos ativar seu sistema em 3 passos rápidos.</h4>
</header>
<!-- Stepper 2 colunas -->
<Stepper :value="currentIdx" @update:value="navigateTo($event)" class="w-full">
<div class="flex flex-col lg:flex-row gap-6 w-full">
<!-- COLUNA ESQUERDA: Steps + Progress -->
<div class="lg:w-72 shrink-0 flex flex-col gap-4 sticky top-6 self-start">
<StepList class="!flex-col !border-0 !bg-transparent !p-0 gap-2">
<Step v-for="(step, idx) in steps" :key="step.id" :value="idx" :as-child="true">
<template #default="{ active, activateCallback }">
<button
@click="navigateTo(idx)"
class="group w-full flex items-center gap-3 px-4 py-3.5 rounded-2xl border transition-all duration-200 text-left cursor-pointer"
:class="[
active
? 'bg-white/90 dark:bg-white/[0.08] border-[--sw-primary-border] shadow-lg shadow-[--sw-primary-dim]'
: 'bg-white/50 dark:bg-white/[0.03] border-black/5 dark:border-white/5 hover:bg-white/70 dark:hover:bg-white/[0.06]',
stepStates[step.id] === 'saved' && !active ? '!border-emerald-300/40 dark:!border-emerald-500/30' : ''
]"
>
<!-- Icon circle -->
<div
class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0 transition-all"
:class="[
stepStates[step.id] === 'saved'
? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400'
: active
? 'bg-[--sw-primary-dim] text-[--sw-primary]'
: 'bg-black/5 dark:bg-white/5 text-black/30 dark:text-white/30'
]"
>
<i v-if="stepStates[step.id] === 'saved'" class="pi pi-check text-sm" />
<i v-else :class="`pi ${step.icon} text-sm`" />
</div>
<!-- Text -->
<div class="min-w-0 flex-1">
<div class="text-sm font-semibold truncate" :class="active ? 'text-[--sw-primary]' : ''">
{{ step.label }}
</div>
<div class="text-xs text-black/40 dark:text-white/40 truncate">{{ step.description }}</div>
</div>
<!-- Badge -->
<span
class="inline-flex text-[0.65rem] font-bold px-2 py-0.5 rounded-full shrink-0"
:class="stepStates[step.id] === 'saved' ? 'bg-emerald-500/15 text-emerald-600 dark:text-emerald-400' : 'bg-black/5 dark:bg-white/5 text-black/30 dark:text-white/30'"
>
{{ stepStates[step.id] === 'saved' ? '✓' : `${idx + 1}/3` }}
</span>
</button>
</template>
</Step>
</StepList>
<!-- Progress + autosave indicator -->
<div class="space-y-3 mt-2">
<!-- Progress bar -->
<div>
<div class="h-2.5 rounded-full bg-black/5 dark:bg-white/5 overflow-hidden">
<div class="h-full rounded-full bg-[var(--primary-color)] transition-all duration-500 ease-out" :style="{ width: Math.min(100, progressPct) + '%' }" />
</div>
<div class="flex justify-between items-center mt-1.5">
<span class="text-sm text-black/50 dark:text-white/50">{{ progressMsg }}</span>
<span class="text-sm font-bold" :class="progressPct > 100 ? 'text-amber-500' : 'text-[--sw-primary]'"> {{ Math.min(100, progressPct) }}%<span v-if="progressPct > 100" class="text-amber-500">+</span> </span>
</div>
</div>
<!-- Autosave hint -->
<div class="flex items-center gap-2 text-sm text-black/40 dark:text-white/40 px-1">
<i v-if="isSaving" class="pi pi-spin pi-spinner" />
<i v-else-if="stepStates[currentStep.id] === 'saved'" class="pi pi-check-circle text-emerald-500" />
<i v-else class="pi pi-bolt" />
{{ isSaving ? 'Salvando...' : stepStates[currentStep.id] === 'saved' ? 'Etapa salva' : 'Autosave ativo' }}
</div>
</div>
</div>
<!-- COLUNA DIREITA: Painel de conteúdo -->
<StepPanels class="flex-1 min-w-0 !border-0 !bg-transparent !p-0">
<!-- STEP 1: VOCÊ -->
<StepPanel :value="0">
<!-- Hero header -->
<div class="sw-hero relative overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-3 py-2.5 mb-3">
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-blue-300/10" />
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-indigo-500/[0.09]" />
</div>
<div class="relative z-1 flex items-center gap-3">
<div class="grid place-items-center w-9 h-9 rounded-md shrink-0 bg-blue-500/[0.12] text-blue-500">
<i class="pi pi-user text-base" />
</div>
<div class="min-w-0">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Você</div>
<div class="text-xs text-[var(--text-color-secondary)] flex items-center gap-1 mt-px">
<i class="pi pi-user opacity-50 shrink-0" />
<span>Seu perfil básico nome, foto e preferências.</span>
</div>
</div>
</div>
</div>
<!-- Conteúdo -->
<div class="grid grid-cols-1 xl:grid-cols-[1fr_280px] gap-4">
<!-- Form col -->
<div class="flex flex-col gap-3">
<!-- Card: Foto de perfil -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-camera text-xs opacity-50" />
<span>Foto de perfil</span>
<span class="text-[0.65rem] font-bold text-amber-500 bg-amber-500/10 px-1.5 py-0.5 rounded-full ml-auto">+10%</span>
</div>
<div class="flex items-center gap-4 p-4">
<div
class="relative w-16 h-16 rounded-2xl overflow-hidden cursor-pointer group border-2 border-dashed border-[var(--surface-border)] hover:border-[--sw-primary-border] transition-all"
@click="triggerAvatarUpload"
>
<img v-if="perfil.avatarPreview" :src="perfil.avatarPreview" class="w-full h-full object-cover" />
<span v-else class="w-full h-full flex items-center justify-center text-sm font-bold text-[var(--text-color-secondary)] bg-[var(--surface-hover)]">{{ initials }}</span>
<div class="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="pi pi-camera text-white" />
</div>
</div>
<input ref="avatarInput" type="file" accept="image/*" class="hidden" @change="onAvatarChange" />
<p class="text-xs text-[var(--text-color-secondary)]">Clique para upload · JPG/PNG até 5MB</p>
</div>
</div>
<!-- Card: Identificação -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-id-card text-xs opacity-50" />
<span>Identificação</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 p-4">
<div>
<label class="sw-label">Nome completo <span class="text-red-500">*</span></label>
<InputText
v-model="perfil.fullName"
placeholder="Ex: Leonardo Marques"
class="w-full"
:invalid="!!fieldErrors.fullName"
@input="
markStarted('voce');
scheduleAutosave('voce');
clearErr('fullName');
"
/>
<Message v-if="fieldErrors.fullName" severity="error" size="small" variant="simple">{{ fieldErrors.fullName }}</Message>
</div>
<div>
<label class="sw-label">Como te chamamos? <span class="text-red-500">*</span></label>
<InputText
v-model="perfil.nickname"
placeholder="Ex: Leo"
class="w-full"
:invalid="!!fieldErrors.nickname"
@input="
markStarted('voce');
scheduleAutosave('voce');
clearErr('nickname');
"
/>
<Message v-if="fieldErrors.nickname" severity="error" size="small" variant="simple">{{ fieldErrors.nickname }}</Message>
</div>
</div>
</div>
<!-- Card: Contato -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-phone text-xs opacity-50" />
<span>Contato</span>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 p-4">
<div>
<label class="sw-label">WhatsApp <span class="text-red-500">*</span></label>
<InputMask
v-model="perfil.whatsapp"
mask="(99) 99999-9999"
:autoClear="false"
class="w-full"
placeholder="(99) 99999-9999"
:invalid="!!fieldErrors.whatsapp"
@update:modelValue="
markStarted('voce');
scheduleAutosave('voce');
clearErr('whatsapp');
"
/>
<Message v-if="fieldErrors.whatsapp" severity="error" size="small" variant="simple">{{ fieldErrors.whatsapp }}</Message>
<Message severity="secondary" size="small" variant="simple" class="mt-1">
<i class="pi pi-lock text-[0.6rem] mr-1" /> Não se preocupe, esse número é utilizado apenas para recuperação de conta ou contato da equipe. Ele não será exibido publicamente.
</Message>
</div>
<div>
<label class="sw-label">E-mail</label>
<InputText :modelValue="userEmail" class="w-full" disabled />
</div>
</div>
</div>
<!-- Card: Profissão -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-briefcase text-xs opacity-50" />
<span>Profissão</span>
</div>
<div class="flex flex-col gap-4 p-4">
<div>
<label class="sw-label">O que melhor descreve seu trabalho?</label>
<Select v-model="perfil.workDescription" :options="workDescriptionOptions" optionLabel="label" optionValue="value" class="w-full" placeholder="Selecione..." @change="markStarted('voce')" />
</div>
<Transition name="fade">
<div v-if="perfil.workDescription === 'outro'">
<label class="sw-label">Informe sua atuação</label>
<InputText v-model="perfil.workDescriptionOther" placeholder="Descreva brevemente..." class="w-full" @input="markStarted('voce')" />
</div>
</Transition>
<div>
<label class="sw-label">
Bio curta
<span class="text-[0.65rem] font-bold text-amber-500 bg-amber-500/10 px-1.5 py-0.5 rounded-full ml-1">+5%</span>
</label>
<Textarea
v-model="perfil.bio"
:maxlength="300"
rows="3"
placeholder="Conte um pouco sobre você..."
class="w-full"
@input="
markStarted('voce');
scheduleAutosave('voce');
"
/>
<span class="text-xs text-[var(--text-color-secondary)] float-right mt-0.5">{{ perfil.bio.length }}/300</span>
</div>
</div>
</div>
<!-- Card: Aparência -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-palette text-xs opacity-50" />
<span>Aparência</span>
<span class="text-[0.65rem] font-bold text-amber-500 bg-amber-500/10 px-1.5 py-0.5 rounded-full ml-auto">+4%</span>
</div>
<div class="flex flex-col gap-4 p-4">
<div>
<label class="sw-label">Cor principal</label>
<div class="flex flex-wrap gap-1.5 mt-1">
<button
v-for="pc in primaryColors"
:key="pc.name"
type="button"
:title="pc.name"
class="w-7 h-7 rounded-full border-2 transition-all cursor-pointer hover:scale-110"
:class="layoutConfig.primary === pc.name ? 'border-[--sw-primary] scale-110 shadow-lg' : 'border-transparent'"
:style="{ backgroundColor: pc.name === 'noir' ? 'var(--text-color,#18181b)' : pc.palette['500'] }"
@click="updatePrimaryColor(pc)"
/>
</div>
</div>
<div>
<label class="sw-label">Tema</label>
<SelectButton :modelValue="themeModel" :options="themeOptions" optionLabel="label" optionValue="value" :allowEmpty="false" @update:modelValue="onThemeChange" class="mt-1" />
</div>
</div>
</div>
</div>
<!-- Preview col -->
<div class="hidden xl:block space-y-4">
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-eye text-xs opacity-50" />
<span>Como pacientes te veem</span>
</div>
<div class="p-5 text-center">
<div class="w-20 h-20 rounded-2xl mx-auto mb-3 overflow-hidden bg-[var(--surface-ground)]">
<img v-if="perfil.avatarPreview" :src="perfil.avatarPreview" class="w-full h-full object-cover" />
<span v-else class="w-full h-full flex items-center justify-center text-lg font-bold text-[var(--text-color-secondary)]">{{ initials }}</span>
</div>
<div class="font-bold text-sm text-[var(--text-color)]">{{ perfil.fullName || 'Seu nome' }}</div>
<div v-if="perfil.nickname" class="text-xs text-[--sw-primary] mt-0.5">{{ perfil.nickname }}</div>
<div class="text-xs text-[var(--text-color-secondary)] mt-2 leading-relaxed">{{ perfil.bio || 'Sua bio aparecerá aqui...' }}</div>
<div class="mt-3 inline-flex items-center gap-1 text-[0.65rem] text-emerald-600 dark:text-emerald-400 bg-emerald-500/10 px-2 py-1 rounded-full"><i class="pi pi-verified" /> Profissional verificado</div>
</div>
</div>
<!-- Micro checklist -->
<div class="space-y-2">
<div
v-for="item in [
{ done: perfil.avatarPreview, label: 'Foto de perfil', pts: '+10%' },
{ done: perfil.bio?.trim(), label: 'Bio curta', pts: '+5%' },
{ done: layoutConfig.primary && layoutConfig.primary !== 'noir', label: 'Cor personalizada', pts: '+2%' },
{ done: isDarkNow(), label: 'Tema escuro', pts: '+2%' }
]"
:key="item.label"
class="flex items-center gap-2 text-xs px-2 py-1.5 rounded-lg transition-colors"
:class="item.done ? 'text-emerald-600 dark:text-emerald-400' : 'text-[var(--text-color-secondary)]'"
>
<i :class="item.done ? 'pi pi-check-circle' : 'pi pi-circle'" />
<span class="flex-1">{{ item.label }}</span>
<span class="font-bold text-amber-500">{{ item.pts }}</span>
</div>
</div>
</div>
</div>
</StepPanel>
<!-- ══════════════ STEP 2: NEGÓCIO ══════════════ -->
<StepPanel :value="1">
<!-- Hero header -->
<div class="sw-hero relative overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-3 py-2.5 mb-3">
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-emerald-300/10" />
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-indigo-500/[0.09]" />
</div>
<div class="relative z-1 flex items-center gap-3">
<div class="grid place-items-center w-9 h-9 rounded-md shrink-0 bg-emerald-500/[0.12] text-emerald-500">
<i class="pi pi-building text-base" />
</div>
<div class="min-w-0">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Negócio</div>
<div class="text-xs text-[var(--text-color-secondary)] flex items-center gap-1 mt-px">
<i class="pi pi-building opacity-50 shrink-0" />
<span>Dados do seu consultório ou clínica.</span>
</div>
</div>
</div>
</div>
<!-- Conteúdo -->
<div class="grid grid-cols-1 xl:grid-cols-[1fr_280px] gap-4">
<div class="flex flex-col gap-3">
<!-- Card: Logo -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-image text-xs opacity-50" />
<span>Logomarca</span>
<span class="text-[0.65rem] font-bold text-amber-500 bg-amber-500/10 px-1.5 py-0.5 rounded-full ml-auto">+10%</span>
</div>
<div class="flex items-center gap-4 p-4">
<div
class="relative w-16 h-16 rounded-2xl overflow-hidden cursor-pointer group border-2 border-dashed border-[var(--surface-border)] hover:border-[--sw-primary-border] transition-all"
@click="triggerLogoUpload"
>
<img v-if="negocio.logoPreview" :src="negocio.logoPreview" class="w-full h-full object-cover" />
<span v-else class="w-full h-full flex items-center justify-center text-sm font-bold text-[var(--text-color-secondary)] bg-[var(--surface-hover)]">{{ negocioInitials }}</span>
<div class="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
<i class="pi pi-upload text-white" />
</div>
</div>
<input ref="logoInput" type="file" accept="image/*" class="hidden" @change="onLogoChange" />
<p class="text-xs text-[var(--text-color-secondary)]">PNG/SVG recomendado · até 2MB</p>
</div>
</div>
<!-- Card: Identificação -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-building text-xs opacity-50" />
<span>Identificação</span>
</div>
<div class="flex flex-col gap-4 p-4">
<div>
<label class="sw-label">Nome do negócio <span class="text-red-500">*</span></label>
<InputText
v-model="negocio.name"
placeholder="Ex: Consultório Bem Estar"
class="w-full"
:invalid="!!fieldErrors.negocioName"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
clearErr('negocioName');
"
/>
<Message v-if="fieldErrors.negocioName" severity="error" size="small" variant="simple">{{ fieldErrors.negocioName }}</Message>
</div>
<div>
<label class="sw-label">Tipo de negócio <span class="text-red-500">*</span></label>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 mt-1" :class="{ 'ring-2 ring-red-500/30 rounded-xl p-1': !!fieldErrors.negocioType }">
<button
v-for="bt in businessTypes"
:key="bt.value"
type="button"
class="relative flex flex-col items-center gap-1.5 py-3 px-2 rounded-xl border transition-all cursor-pointer text-center"
:class="
negocio.type === bt.value
? 'bg-[--sw-primary-dim] border-[--sw-primary-border] text-[--sw-primary]'
: 'bg-[var(--surface-hover)] border-[var(--surface-border)] hover:border-[--sw-primary-border]'
"
@click="
negocio.type = bt.value;
markStarted('negocio');
scheduleAutosave('negocio');
clearErr('negocioType');
"
>
<i :class="`pi ${bt.icon} text-lg`" />
<span class="text-xs font-medium">{{ bt.label }}</span>
<i v-if="negocio.type === bt.value" class="pi pi-check-circle absolute top-1.5 right-1.5 text-xs text-[--sw-primary]" />
</button>
</div>
<Message v-if="fieldErrors.negocioType" severity="error" size="small" variant="simple">{{ fieldErrors.negocioType }}</Message>
</div>
</div>
</div>
<!-- Card: Endereço -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-map-marker text-xs opacity-50" />
<span>Endereço</span>
<span class="text-[0.65rem] font-bold text-amber-500 bg-amber-500/10 px-1.5 py-0.5 rounded-full ml-auto">+5%</span>
</div>
<div class="p-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label class="sw-label">CEP</label>
<div class="relative">
<InputMask v-model="negocio.cep" mask="99999-999" :autoClear="false" placeholder="00000-000" class="w-full" @blur="onCepBlur" @update:modelValue="markStarted('negocio')" />
<i v-if="fetchingCep" class="pi pi-spin pi-spinner absolute right-3 top-1/2 -translate-y-1/2 text-xs text-[--sw-primary]" />
</div>
</div>
<div class="sm:col-span-2">
<label class="sw-label">Logradouro</label>
<InputText
v-model="negocio.logradouro"
placeholder="Rua, Avenida..."
class="w-full"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
</div>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 mt-3">
<div>
<label class="sw-label">Número</label>
<InputText
v-model="negocio.numero"
placeholder="123"
class="w-full"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
</div>
<div>
<label class="sw-label">Complemento</label>
<InputText
v-model="negocio.complemento"
placeholder="Sala 1"
class="w-full"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
</div>
<div>
<label class="sw-label">Bairro</label>
<InputText
v-model="negocio.bairro"
placeholder="Bairro"
class="w-full"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
</div>
<div class="col-span-2 sm:col-span-1">
<label class="sw-label">Cidade / UF</label>
<div class="flex gap-2">
<InputText
v-model="negocio.cidade"
placeholder="Cidade"
class="w-full"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
<InputText
v-model="negocio.estado"
placeholder="UF"
class="w-16 shrink-0"
maxlength="2"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Card: Contato e Presença Digital -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-globe text-xs opacity-50" />
<span>Contato e Presença Digital</span>
<span class="text-[0.65rem] font-bold text-amber-500 bg-amber-500/10 px-1.5 py-0.5 rounded-full ml-auto">+5%</span>
</div>
<div class="p-4 flex flex-col gap-4">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="sw-label">Telefone</label>
<InputMask
v-model="negocio.phone"
mask="(99) 99999-9999"
:autoClear="false"
placeholder="(11) 99999-9999"
class="w-full"
@update:modelValue="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
</div>
<div>
<label class="sw-label">E-mail de contato</label>
<InputText
v-model="negocio.email"
type="email"
placeholder="contato@clinica.com"
class="w-full"
:invalid="negocio.email.length > 0 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(negocio.email)"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
<Message v-if="negocio.email.length > 0 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(negocio.email)" severity="error" size="small" variant="simple">E-mail inválido.</Message>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label class="sw-label">Site</label>
<InputText
v-model="negocio.siteUrl"
type="url"
placeholder="https://seunegocio.com"
class="w-full"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
</div>
<div>
<label class="sw-label">Instagram</label>
<InputText
v-model="negocio.instagram"
placeholder="@seuperfil"
class="w-full"
@input="
markStarted('negocio');
scheduleAutosave('negocio');
"
/>
</div>
</div>
</div>
</div>
</div>
<!-- Preview col -->
<div class="hidden xl:block space-y-4">
<div class="text-xs font-semibold text-[var(--text-color-secondary)] uppercase tracking-wider">Seu negócio</div>
<div class="sw-card">
<div class="p-5 text-center">
<div class="w-20 h-20 rounded-2xl mx-auto mb-3 overflow-hidden bg-[var(--surface-ground)]">
<img v-if="negocio.logoPreview" :src="negocio.logoPreview" class="w-full h-full object-cover" />
<span v-else class="w-full h-full flex items-center justify-center text-lg font-bold text-[var(--text-color-secondary)]">{{ negocioInitials }}</span>
</div>
<div class="font-bold text-sm text-[var(--text-color)]">{{ negocio.name || 'Nome do Negócio' }}</div>
<div v-if="negocio.type" class="text-xs text-[--sw-primary] mt-0.5">
{{ businessTypes.find((b) => b.value === negocio.type)?.label }}
</div>
<div v-if="negocio.logradouro || negocio.cidade" class="text-xs text-[var(--text-color-secondary)] mt-2 flex items-center justify-center gap-1">
<i class="pi pi-map-marker text-[0.6rem]" />
{{ [negocio.logradouro, negocio.numero, negocio.bairro, negocio.cidade, negocio.estado].filter(Boolean).join(', ') }}
</div>
</div>
<div class="space-y-2">
<div
v-for="item in [
{ done: negocio.logoPreview, label: 'Logo', pts: '+10%' },
{ done: negocio.logradouro?.trim() && negocio.cidade?.trim(), label: 'Endereço', pts: '+5%' },
{ done: negocio.phone?.trim(), label: 'Telefone', pts: '+3%' },
{ done: negocio.siteUrl?.trim() || negocio.instagram?.trim(), label: 'Site / Instagram', pts: '+2%' }
]"
:key="item.label"
class="flex items-center gap-2 text-xs px-2 py-1.5 rounded-lg transition-colors"
:class="item.done ? 'text-emerald-600 dark:text-emerald-400' : 'text-[var(--text-color-secondary)]'"
>
<i :class="item.done ? 'pi pi-check-circle' : 'pi pi-circle'" />
<span class="flex-1">{{ item.label }}</span>
<span class="font-bold text-amber-500">{{ item.pts }}</span>
</div>
</div>
</div>
</div>
</div>
</StepPanel>
<!-- ══════════════ STEP 3: ATENDIMENTO ══════════════ -->
<StepPanel :value="2">
<!-- Hero header -->
<div class="sw-hero relative overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-3 py-2.5 mb-3">
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-violet-300/10" />
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-indigo-500/[0.09]" />
</div>
<div class="relative z-1 flex items-center gap-3">
<div class="grid place-items-center w-9 h-9 rounded-md shrink-0 bg-violet-500/[0.12] text-violet-500">
<i class="pi pi-heart text-base" />
</div>
<div class="min-w-0">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Atendimento</div>
<div class="text-xs text-[var(--text-color-secondary)] flex items-center gap-1 mt-px">
<i class="pi pi-heart opacity-50 shrink-0" />
<span>Modalidade, serviços e convênios.</span>
</div>
</div>
</div>
</div>
<!-- Conteúdo -->
<div class="grid grid-cols-1 xl:grid-cols-[1fr_280px] gap-4">
<div class="flex flex-col gap-3">
<!-- Card: Modalidade -->
<div class="sw-card">
<div class="sw-card__head">
<i class="pi pi-question-circle text-xs opacity-50" />
<span>Modalidade</span>
</div>
<div class="p-4">
<label class="sw-label">Como você atende seus clientes? <span class="text-red-500">*</span></label>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-1">
<button
v-for="m in atendimentoModes"
:key="m.value"
type="button"
class="relative flex items-center gap-3 p-4 rounded-xl border transition-all cursor-pointer text-left"
:class="atendimento.mode === m.value ? 'bg-[--sw-primary-dim] border-[--sw-primary-border]' : 'bg-[var(--surface-hover)] border-[var(--surface-border)] hover:border-[--sw-primary-border]'"
@click="
atendimento.mode = m.value;
saveAtendimento(true);
"
>
<i :class="`pi ${m.icon} text-lg`" :style="atendimento.mode === m.value ? 'color: var(--sw-primary)' : ''" />
<div class="min-w-0">
<div class="text-sm font-semibold" :class="atendimento.mode === m.value ? 'text-[--sw-primary]' : ''">{{ m.label }}</div>
<div class="text-xs text-[var(--text-color-secondary)]">{{ m.desc }}</div>
</div>
<i v-if="atendimento.mode === m.value" class="pi pi-check-circle absolute top-2 right-2 text-xs text-[--sw-primary]" />
</button>
</div>
</div>
</div>
<!-- Card: Serviços -->
<Transition name="fade">
<div v-if="isParticular" class="sw-card">
<div class="sw-card__head">
<i class="pi pi-tag text-xs opacity-50" />
<span>Serviços</span>
</div>
<div class="p-4 space-y-3">
<div v-if="services.length" class="space-y-2">
<div v-for="svc in services" :key="svc.id" class="flex items-center gap-3 p-3 rounded-xl bg-[var(--surface-hover)] border border-[var(--surface-border)]">
<div class="flex-1 min-w-0">
<span class="text-sm font-medium text-[var(--text-color)]">{{ svc.name }}</span>
<span class="text-xs text-[--sw-primary] ml-2">{{ formatCurrency(svc.price) }}</span>
<span v-if="svc.duration_min" class="text-xs text-[var(--text-color-secondary)] ml-2">{{ svc.duration_min }}min</span>
</div>
<div class="flex gap-1">
<button
class="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--text-color-secondary)] hover:text-[--sw-primary] hover:bg-[--sw-primary-dim] transition-all cursor-pointer"
@click="editService(svc)"
>
<i class="pi pi-pencil text-xs" />
</button>
<button
class="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--text-color-secondary)] hover:text-red-500 hover:bg-red-500/10 transition-all cursor-pointer"
@click="removeServiceItem(svc.id)"
>
<i class="pi pi-trash text-xs" />
</button>
</div>
</div>
</div>
<div v-else class="flex items-center gap-2 text-xs text-[var(--text-color-secondary)] p-3 rounded-xl bg-[var(--surface-ground)]">
<i class="pi pi-tag" /><span>Criaremos um serviço padrão ao salvar</span>
</div>
<div class="pt-2 space-y-3">
<label class="sw-label">{{ newService.id ? 'Editar serviço' : 'Novo serviço' }}</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="sw-label">Nome</label>
<InputText v-model="newService.name" placeholder="Ex: Psicoterapia Individual" class="w-full" @input="markStarted('atendimento')" />
</div>
<div>
<label class="sw-label">Preço</label>
<InputNumber v-model="newService.price" mode="currency" currency="BRL" locale="pt-BR" :min="0" fluid @update:modelValue="markStarted('atendimento')" />
</div>
</div>
<div class="flex gap-2">
<button
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-sm font-semibold text-white bg-[--sw-primary] hover:brightness-110 transition-all cursor-pointer"
@click="saveServiceItem"
>
<i class="pi pi-plus text-xs" /> {{ newService.id ? 'Atualizar' : 'Adicionar' }}
</button>
<button
v-if="newService.id"
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-sm font-semibold text-[--sw-primary] bg-[--sw-primary-dim] hover:bg-[--sw-primary-border] transition-all cursor-pointer"
@click="cancelEditService"
>
Cancelar
</button>
</div>
</div>
<div v-if="services.length === 0" class="flex items-center gap-2 text-xs text-blue-600 dark:text-blue-400 bg-blue-500/10 p-3 rounded-xl">
<i class="pi pi-info-circle" />
Ao salvar, criaremos automaticamente um <b>Atendimento padrão</b> (50 min).
</div>
</div>
</div>
</Transition>
<!-- Card: Convênios -->
<Transition name="fade">
<div v-if="isConveniado" class="sw-card">
<div class="sw-card__head">
<i class="pi pi-heart text-xs opacity-50" />
<span>Convênios</span>
</div>
<div class="p-4 space-y-3">
<div v-if="insurancePlans.length" class="space-y-2">
<div v-for="plan in insurancePlans" :key="plan.id" class="flex items-center gap-3 p-3 rounded-xl bg-[var(--surface-hover)] border border-[var(--surface-border)]">
<div class="flex-1 min-w-0">
<span class="text-sm font-medium text-[var(--text-color)]">{{ plan.name }}</span>
<span v-if="plan.notes" class="text-xs text-[var(--text-color-secondary)] ml-2">{{ plan.notes }}</span>
</div>
<button
class="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--text-color-secondary)] hover:text-red-500 hover:bg-red-500/10 transition-all cursor-pointer"
@click="removePlanItemWizard(plan.id)"
>
<i class="pi pi-trash text-xs" />
</button>
</div>
</div>
<div v-else class="flex items-center gap-2 text-xs text-[var(--text-color-secondary)] p-3 rounded-xl bg-[var(--surface-ground)]"><i class="pi pi-heart" /><span>Nenhum convênio ainda</span></div>
<div class="pt-2 space-y-3">
<label class="sw-label">Adicionar convênio</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="sw-label">Nome</label>
<InputText v-model="newPlan.name" placeholder="Ex: Unimed" class="w-full" @input="markStarted('atendimento')" />
</div>
<div>
<label class="sw-label">Observação</label>
<InputText v-model="newPlan.notes" placeholder="Opcional" class="w-full" />
</div>
</div>
<button
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-sm font-semibold text-white bg-[--sw-primary] hover:brightness-110 transition-all cursor-pointer"
@click="savePlanItemWizard"
>
<i class="pi pi-plus text-xs" /> Adicionar
</button>
</div>
</div>
</div>
</Transition>
</div>
<!-- Preview col -->
<div class="hidden xl:block space-y-4">
<div class="text-xs font-semibold text-[var(--text-color-secondary)] uppercase tracking-wider">Resumo</div>
<div class="sw-card">
<div class="p-5">
<div v-if="atendimento.mode" class="flex items-center gap-2 text-sm font-semibold text-[--sw-primary] mb-3">
<i :class="`pi ${atendimentoModes.find((m) => m.value === atendimento.mode)?.icon}`" />
{{ atendimentoModes.find((m) => m.value === atendimento.mode)?.label }}
</div>
<div v-else class="text-xs text-[var(--text-color-secondary)]">Selecione como você atende</div>
<div v-if="isParticular && services.length" class="mt-3">
<div class="text-xs font-semibold text-[var(--text-color-secondary)] mb-1.5">Serviços</div>
<div v-for="s in services" :key="s.id" class="flex justify-between text-xs py-1">
<span class="text-[var(--text-color)]">{{ s.name }}</span>
<span class="text-[--sw-primary] font-medium">{{ formatCurrency(s.price) }}</span>
</div>
</div>
<div v-if="isConveniado && insurancePlans.length" class="mt-3">
<div class="text-xs font-semibold text-[var(--text-color-secondary)] mb-1.5">Convênios</div>
<div v-for="p in insurancePlans" :key="p.id" class="flex justify-between items-center text-xs py-1">
<span class="text-[var(--text-color)]">{{ p.name }}</span>
<i class="pi pi-heart text-[--sw-primary] text-[0.6rem]" />
</div>
</div>
</div>
</div>
</div>
</div>
</StepPanel>
</StepPanels>
</div>
</Stepper>
</div>
<!-- ════════════════════ FOOTER FIXO ════════════════════ -->
<div v-if="!done" class="fixed bottom-0 left-0 right-0 z-50 bg-white/90 dark:bg-zinc-950/90 backdrop-blur-xl border-t border-black/10 dark:border-white/10">
<div class="max-w-7xl mx-auto px-4 lg:px-8 py-3">
<div class="flex items-center justify-between gap-3">
<button
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all cursor-pointer bg-[var(--primary-color)] text-white shadow-md hover:brightness-110 disabled:opacity-30 disabled:cursor-not-allowed"
:disabled="currentIdx === 0"
@click="navigateTo(currentIdx - 1)"
>
<i class="pi pi-chevron-left text-xs" /> Anterior
</button>
<!-- Autosave indicator (center) -->
<div class="flex items-center gap-2 text-xs text-black/40 dark:text-white/40">
<i v-if="isSaving" class="pi pi-spin pi-spinner" />
<i v-else-if="stepStates[currentStep.id] === 'saved'" class="pi pi-check-circle text-emerald-500" />
<i v-else class="pi pi-bolt" />
<span class="hidden sm:inline">{{ isSaving ? 'Salvando...' : stepStates[currentStep.id] === 'saved' ? 'Salvo' : 'Autosave' }}</span>
</div>
<button
v-if="currentIdx === steps.length - 1"
class="inline-flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-bold transition-all cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed"
:class="canFinish ? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/30 hover:brightness-110 animate-pulse' : 'bg-black/5 dark:bg-white/5 text-black/30 dark:text-white/30'"
:disabled="!canFinish || finishing"
@click="onFinish"
>
<i :class="finishing ? 'pi pi-spin pi-spinner' : 'pi pi-check-circle'" />
{{ finishing ? 'Ativando...' : 'Ativar Sistema' }}
</button>
<button
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-semibold transition-all cursor-pointer bg-[var(--primary-color)] text-white shadow-md hover:brightness-110 disabled:opacity-30 disabled:cursor-not-allowed"
:disabled="currentIdx === steps.length - 1"
@click="navigateTo(currentIdx + 1)"
>
Próximo <i class="pi pi-chevron-right text-xs" />
</button>
</div>
</div>
</div>
</div>
</template>
<style>
/* ── CSS Variables (dynamic via JS) ── */
.sw-root {
--sw-bg: #f1f0fa;
--sw-text: #18181b;
--sw-primary: #6154e8;
--sw-primary-dim: rgba(97, 84, 232, 0.1);
--sw-primary-border: rgba(97, 84, 232, 0.28);
--sw-primary-rgb: 97, 84, 232;
}
:where([class*='app-dark']) .sw-root {
--sw-bg: #0c0b1a;
--sw-text: #f0f0f5;
--sw-primary: #7c6af7;
--sw-primary-dim: rgba(124, 106, 247, 0.1);
--sw-primary-border: rgba(124, 106, 247, 0.28);
--sw-primary-rgb: 124, 106, 247;
}
/* ── Background Blobs ── */
.sw-blob {
filter: blur(130px);
transition: opacity 0.4s;
}
.sw-blob--a {
width: 720px;
height: 720px;
top: -220px;
left: -200px;
background: radial-gradient(circle, #8b7cf8 0%, transparent 70%);
animation: blobFloat 14s ease-in-out infinite alternate;
}
:where([class*='app-dark']) .sw-blob--a {
background: radial-gradient(circle, #5b47e0 0%, transparent 70%);
}
.sw-blob--b {
width: 620px;
height: 620px;
bottom: -160px;
right: -140px;
background: radial-gradient(circle, #60a5fa 0%, transparent 70%);
animation: blobFloat 18s ease-in-out infinite alternate-reverse;
}
:where([class*='app-dark']) .sw-blob--b {
background: radial-gradient(circle, #1e6fa8 0%, transparent 70%);
}
.sw-noise {
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
background-size: 200px;
}
/* ── Keyframes ── */
@keyframes blobFloat {
from {
transform: translate(0, 0) scale(1);
}
to {
transform: translate(45px, 35px) scale(1.09);
}
}
@keyframes fadeDown {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes confettiFall {
0% {
transform: translateY(-100vh) rotate(var(--rot, 0deg));
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(calc(var(--rot, 0deg) + 720deg));
opacity: 0;
}
}
/* ── Confetti ── */
.sw-confetti {
position: absolute;
top: -10px;
left: var(--x, 50%);
width: 8px;
height: 8px;
background: var(--color, #7c6af7);
border-radius: 2px;
animation: confettiFall 3s ease-in var(--delay, 0s) forwards;
}
/* ── Transitions ── */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* ── PrimeVue Stepper overrides ── */
.sw-root .p-stepper {
background: transparent;
border: none;
padding: 0;
}
.sw-root .p-steplist {
background: transparent;
border: none;
padding: 0;
}
.sw-root .p-steppanels {
background: transparent;
border: none;
padding: 0;
}
.sw-root .p-stepper-separator {
display: none;
}
.sw-root .p-steppanel {
background: transparent;
}
.sw-root .p-steppanel-content {
padding: 0;
}
/* ── Card pattern (espelhando cfg-card de ConfiguracoesMinhaEmpresaPage) ── */
.sw-card {
border: 1px solid var(--surface-border);
border-radius: 8px;
background: var(--surface-card);
overflow: hidden;
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow, 0 0 #0000), var(--tw-inset-ring-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.sw-card__head {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--surface-border);
background: var(--surface-ground);
font-size: 0.82rem;
font-weight: 700;
color: var(--text-color);
}
.sw-card__head i {
font-size: 0.8rem;
color: var(--primary-color, #6366f1);
opacity: 0.7;
}
.sw-hero {
--tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1));
box-shadow: var(--tw-inset-shadow, 0 0 #0000), var(--tw-inset-ring-shadow, 0 0 #0000), var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.sw-label {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-color-secondary);
display: block;
margin-bottom: 0.25rem;
}
</style>