dee89ccd84
Quando o profissional seleciona "Outro" no Tipo de registro, agora aparece um campo adicional pra informar o nome do conselho/instituicao livre (ex: APM, ABRAP, conselhos nao-listados). Migration 20260521000009 adiciona profiles.professional_registration_ type_other (text livre). Aplicada e marcada no _db_migrations. ProfilePage e MelissaPerfil: - form.professional_registration_type_other no reactive - SELECT/UPDATE inclui a nova coluna - UI condicional: campo aparece SOMENTE quando type === 'outro' - Preview ao vivo usa type_other no lugar de 'outro' quando aplicavel - Save limpa type_other automaticamente quando troca pra outro tipo DocumentGenerate.service.loadTherapistData puxa type_other da query. Quando profile.type='outro', terapeuta_registro_tipo recebe o valor livre (ex: 'APM 12345/SP' em vez de 'outro 12345/SP'). terapeuta_crp (legacy compat) continua so preenchido quando type RAW = 'CRP'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2258 lines
89 KiB
Vue
2258 lines
89 KiB
Vue
<script setup>
|
||
/*
|
||
* MelissaPerfil — Pagina nativa Melissa pra "Meu Perfil".
|
||
*
|
||
* Substitui o embed cfg-perfil que vivia dentro do MelissaConfiguracoes.
|
||
* Layout 2-col:
|
||
* - COL 1 (sidebar) — Gamificação (nivel + XP + badges + dicas) + Avatar
|
||
* (upload/remover) + footer Sair da conta
|
||
* - COL 2 (main) — Identidade + Contato + Bio + Sites e Redes
|
||
*
|
||
* Aparência (tema/cores) e Layout Variant ficaram fora — Aparencia ja
|
||
* vive no MelissaConfiguracoes "Layout Melissa" e Layout Variant nao
|
||
* faz sentido dentro do shell Melissa. Trocar senha vai pro cfg-seguranca.
|
||
*
|
||
* Logica de load/save espelhada do ProfilePage.vue (mesmas tabelas
|
||
* profiles + user_settings + auth.user_metadata, mesmo bucket avatars).
|
||
*/
|
||
import { ref, reactive, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||
import { useToast } from 'primevue/usetoast';
|
||
import { useConfirm } from 'primevue/useconfirm';
|
||
import { useRouter } from 'vue-router';
|
||
import { supabase } from '@/lib/supabase/client';
|
||
import { useTenantStore } from '@/stores/tenantStore';
|
||
import { useLayout } from '@/layout/composables/layout';
|
||
import MelissaConfigList from './MelissaConfigList.vue';
|
||
// InputText/Select/Textarea/InputMask/Skeleton/Tag/Button: auto via PrimeVueResolver
|
||
|
||
const emit = defineEmits(['close']);
|
||
|
||
const toast = useToast();
|
||
const confirm = useConfirm();
|
||
const router = useRouter();
|
||
const tenantStore = useTenantStore();
|
||
const { layoutConfig, setVariant, isDarkTheme, toggleDarkMode } = useLayout();
|
||
|
||
// Opções do CHECK constraint da migration 20260521000003 (CFP #5)
|
||
const REGISTRATION_TYPE_OPTIONS = [
|
||
{ value: '', label: '— Não informado —' },
|
||
{ value: 'CRP', label: 'CRP — Psicólogo(a)' },
|
||
{ value: 'CRM', label: 'CRM — Médico(a)' },
|
||
{ value: 'CRFa', label: 'CRFa — Fonoaudiólogo(a)' },
|
||
{ value: 'CREFITO', label: 'CREFITO — Fisioterapeuta / T.O.' },
|
||
{ value: 'CRESS', label: 'CRESS — Assistente Social' },
|
||
{ value: 'CRN', label: 'CRN — Nutricionista' },
|
||
{ value: 'RMS', label: 'RMS — Residência Multiprofissional' },
|
||
{ value: 'outro', label: 'Outro' }
|
||
];
|
||
|
||
const UF_OPTIONS = [
|
||
'AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB',
|
||
'PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO'
|
||
].map(uf => ({ value: uf, label: uf }));
|
||
|
||
function goSeguranca() {
|
||
// Página nativa Melissa — não vaza pra layout clássico
|
||
router.push('/melissa/seguranca');
|
||
}
|
||
|
||
// Troca de layout variant (classic/rail/melissa). Confirma + persiste +
|
||
// hard reload — sair do shell Melissa requer reload pq AppLayout não tem
|
||
// branch pra essa rota; quem renderiza Melissa é a rota /melissa separada.
|
||
const variantSwitchOpen = ref(false);
|
||
async function switchToVariant(v) {
|
||
if (!['classic', 'rail', 'melissa'].includes(v)) return;
|
||
if (layoutConfig.variant === v) return;
|
||
if (variantSwitchOpen.value) return;
|
||
variantSwitchOpen.value = true;
|
||
const labels = { classic: 'Clássico', rail: 'Rail', melissa: 'Melissa' };
|
||
confirm.require({
|
||
header: `Trocar para o layout ${labels[v]}`,
|
||
message: 'A página será recarregada para aplicar o novo layout. Confirma?',
|
||
icon: 'pi pi-th-large',
|
||
acceptLabel: 'Trocar e recarregar',
|
||
rejectLabel: 'Cancelar',
|
||
accept: async () => {
|
||
try {
|
||
setVariant(v);
|
||
if (userId.value) {
|
||
const { error } = await supabase
|
||
.from('user_settings')
|
||
.upsert(
|
||
{
|
||
user_id: userId.value,
|
||
layout_variant: v,
|
||
updated_at: new Date().toISOString()
|
||
},
|
||
{ onConflict: 'user_id' }
|
||
);
|
||
if (error) {
|
||
const msg = String(error.message || '');
|
||
const tolerant = /does not exist/i.test(msg) || /permission denied/i.test(msg) || /violates row-level security/i.test(msg);
|
||
if (!tolerant) throw error;
|
||
}
|
||
}
|
||
toast.add({ severity: 'info', summary: `Aplicando ${labels[v]}`, detail: 'Recarregando…', life: 1500 });
|
||
window.location.assign('/');
|
||
} catch (e) {
|
||
variantSwitchOpen.value = false;
|
||
toast.add({ severity: 'error', summary: 'Erro ao trocar layout', detail: e?.message || 'Tente novamente.', life: 4000 });
|
||
}
|
||
},
|
||
reject: () => { variantSwitchOpen.value = false; },
|
||
onHide: () => { variantSwitchOpen.value = false; }
|
||
});
|
||
}
|
||
|
||
const AVATAR_BUCKET = 'avatars';
|
||
|
||
// ── Breakpoints + drawer ───────────────────────────────────
|
||
const drawerOpen = ref(false);
|
||
const isMobile = ref(false);
|
||
let _mqMobile = null;
|
||
function _onMqMobileChange(e) {
|
||
isMobile.value = e.matches;
|
||
if (!e.matches) drawerOpen.value = false;
|
||
}
|
||
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
|
||
function fecharDrawer() { drawerOpen.value = false; }
|
||
|
||
// Toggle entre cards contextuais (default) e lista de configs (alterna
|
||
// inline na .mpr-side via v-if — sem overlay/popover, zero overhead).
|
||
const cfgOpen = ref(false);
|
||
function toggleCfg() { cfgOpen.value = !cfgOpen.value; }
|
||
function fecharCfg() { cfgOpen.value = false; }
|
||
|
||
// ── Estado ─────────────────────────────────────────────────
|
||
const loading = ref(true);
|
||
const saving = ref(false);
|
||
const dirty = ref(false);
|
||
const silentApplying = ref(true);
|
||
|
||
const userId = ref('');
|
||
const userEmail = ref('');
|
||
const userRole = ref(null);
|
||
|
||
const fieldErrors = reactive({
|
||
full_name: '',
|
||
nickname: '',
|
||
phone: ''
|
||
});
|
||
|
||
function clearErr(field) { fieldErrors[field] = ''; }
|
||
function markDirty() {
|
||
if (silentApplying.value) return;
|
||
dirty.value = true;
|
||
}
|
||
|
||
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: '',
|
||
// Registro profissional (CFP #5 — exigido pra emissão de recibos/laudos)
|
||
professional_registration_type: '',
|
||
professional_registration_type_other: '',
|
||
professional_registration_number: '',
|
||
professional_registration_uf: ''
|
||
});
|
||
|
||
const customSocials = ref([]);
|
||
|
||
function addCustomSocial() {
|
||
customSocials.value.push({ name: '', url: '' });
|
||
markDirty();
|
||
}
|
||
function removeCustomSocial(idx) {
|
||
customSocials.value.splice(idx, 1);
|
||
markDirty();
|
||
}
|
||
|
||
const ui = reactive({
|
||
avatarPreview: '',
|
||
avatarFile: null,
|
||
avatarFilePreviewUrl: ''
|
||
});
|
||
|
||
const fileInput = ref(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 / 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' }
|
||
];
|
||
|
||
// ── Iniciais (avatar fallback) ──────────────────────────────
|
||
const initials = computed(() => {
|
||
const name = form.full_name || userEmail.value || '';
|
||
const parts = String(name).trim().split(/\s+/).filter(Boolean);
|
||
const a = parts[0]?.[0] || 'U';
|
||
const b = parts.length > 1 ? parts[parts.length - 1][0] : '';
|
||
return (a + b).toUpperCase();
|
||
});
|
||
|
||
// ── Gamificação / Progresso ────────────────────────────────
|
||
const profileFields = computed(() => [
|
||
{ key: 'full_name', filled: !!form.full_name?.trim(), icon: 'pi pi-user', text: 'Preencha seu nome completo' },
|
||
{ key: 'nickname', filled: !!form.nickname?.trim(), icon: 'pi pi-comment', text: 'Escolha um nome de exibição' },
|
||
{ key: 'work_description', filled: !!form.work_description?.trim(), icon: 'pi pi-briefcase', text: 'Descreva seu trabalho' },
|
||
{ key: 'avatar', filled: !!(form.avatar_url?.trim() || ui.avatarFile), icon: 'pi pi-image', text: 'Adicione uma foto' },
|
||
{ key: 'bio', filled: !!form.bio?.trim(), icon: 'pi pi-pencil', text: 'Complete sua bio' },
|
||
{ key: 'phone', filled: !!form.phone?.trim()?.replace(/[^0-9]/g, ''), icon: 'pi pi-whatsapp', text: 'Informe seu WhatsApp' },
|
||
{ key: 'social', filled: !!(form.social_instagram || form.social_x || form.site_url || form.social_youtube || form.social_facebook || customSocials.value.length), icon: 'pi pi-share-alt', text: 'Adicione uma rede social' }
|
||
]);
|
||
|
||
const profileProgress = computed(() => {
|
||
const filled = profileFields.value.filter((f) => f.filled).length;
|
||
return Math.round((filled / profileFields.value.length) * 100);
|
||
});
|
||
|
||
const progressSuggestions = computed(() => profileFields.value.filter((f) => !f.filled));
|
||
|
||
const progressColor = computed(() => {
|
||
if (profileProgress.value >= 80) return '#10b981';
|
||
if (profileProgress.value >= 50) return '#f59e0b';
|
||
return '#ef4444';
|
||
});
|
||
|
||
const gameLevels = [
|
||
{ min: 0, max: 14, label: 'Iniciante', icon: '🌱', color: '#94a3b8' },
|
||
{ min: 15, max: 28, label: 'Aprendiz', icon: '🌿', color: '#60a5fa' },
|
||
{ min: 29, max: 42, label: 'Praticante', icon: '⚡', color: '#f59e0b' },
|
||
{ min: 43, max: 57, label: 'Avançado', icon: '🔥', color: '#f97316' },
|
||
{ min: 58, max: 71, label: 'Expert', icon: '💎', color: '#a78bfa' },
|
||
{ min: 72, max: 85, label: 'Mestre', icon: '🏆', color: '#10b981' },
|
||
{ min: 86, max: 100, label: 'Lendário', icon: '🌟', color: '#eab308' }
|
||
];
|
||
|
||
const currentLevel = computed(() =>
|
||
gameLevels.find((l) => profileProgress.value >= l.min && profileProgress.value <= l.max) || gameLevels[0]
|
||
);
|
||
const nextLevel = computed(() => {
|
||
const i = gameLevels.indexOf(currentLevel.value);
|
||
return i < gameLevels.length - 1 ? gameLevels[i + 1] : null;
|
||
});
|
||
const xpToNext = computed(() => (nextLevel.value ? nextLevel.value.min - profileProgress.value : 0));
|
||
|
||
const badges = computed(() => [
|
||
{ key: 'name', earned: !!form.full_name?.trim(), icon: '👤', label: 'Identificado' },
|
||
{ key: 'nick', earned: !!form.nickname?.trim(), icon: '✏️', label: 'Apelido' },
|
||
{ key: 'work', earned: !!form.work_description?.trim(), icon: '💼', label: 'Profissional' },
|
||
{ key: 'photo', earned: !!(form.avatar_url?.trim() || ui.avatarFile), icon: '📷', label: 'Fotogênico' },
|
||
{ key: 'bio', earned: !!form.bio?.trim(), icon: '📝', label: 'Eloquente' },
|
||
{ key: 'phone', earned: !!form.phone?.trim()?.replace(/[^0-9]/g, ''), icon: '📱', label: 'Conectado' },
|
||
{ key: 'social', earned: !!(form.social_instagram || form.social_x || form.site_url || form.social_youtube || form.social_facebook || customSocials.value.length), icon: '🌐', label: 'Social' }
|
||
]);
|
||
|
||
// ── Validação ──────────────────────────────────────────────
|
||
function validateRequired() {
|
||
const nameParts = form.full_name?.trim().split(/\s+/).filter(Boolean) || [];
|
||
fieldErrors.full_name =
|
||
nameParts.length === 0 ? 'Nome completo é obrigatório.'
|
||
: nameParts.length < 2 ? 'Informe seu nome e sobrenome.'
|
||
: '';
|
||
fieldErrors.nickname = form.nickname?.trim() ? '' : 'Nome de exibição é obrigatório.';
|
||
const digits = form.phone?.replace(/[^0-9]/g, '') || '';
|
||
fieldErrors.phone = digits.length >= 10 ? '' : 'WhatsApp é obrigatório.';
|
||
return !fieldErrors.full_name && !fieldErrors.nickname && !fieldErrors.phone;
|
||
}
|
||
|
||
// ── Avatar ─────────────────────────────────────────────────
|
||
function pickAvatar() { fileInput.value?.click(); }
|
||
|
||
function clearAvatarFile() {
|
||
ui.avatarFile = null;
|
||
if (ui.avatarFilePreviewUrl) {
|
||
try { URL.revokeObjectURL(ui.avatarFilePreviewUrl); } catch { /* ignore */ }
|
||
}
|
||
ui.avatarFilePreviewUrl = '';
|
||
if (fileInput.value) fileInput.value.value = '';
|
||
}
|
||
|
||
function onAvatarFileSelected(ev) {
|
||
const file = ev?.target?.files?.[0];
|
||
if (!file) return;
|
||
if (!file.type?.startsWith('image/')) {
|
||
toast.add({ severity: 'warn', summary: 'Arquivo inválido', detail: 'Escolha uma imagem (PNG/JPG/WebP).', life: 3500 });
|
||
clearAvatarFile();
|
||
return;
|
||
}
|
||
if (file.size > 5 * 1024 * 1024) {
|
||
toast.add({ severity: 'warn', summary: 'Arquivo grande', detail: 'Máximo recomendado: 5MB.', life: 3500 });
|
||
clearAvatarFile();
|
||
return;
|
||
}
|
||
ui.avatarFile = file;
|
||
if (ui.avatarFilePreviewUrl) {
|
||
try { URL.revokeObjectURL(ui.avatarFilePreviewUrl); } catch { /* ignore */ }
|
||
}
|
||
ui.avatarFilePreviewUrl = URL.createObjectURL(file);
|
||
ui.avatarPreview = ui.avatarFilePreviewUrl;
|
||
markDirty();
|
||
}
|
||
|
||
function removeAvatar() {
|
||
form.avatar_url = '';
|
||
ui.avatarPreview = '';
|
||
clearAvatarFile();
|
||
markDirty();
|
||
}
|
||
|
||
function extFromMime(mime) {
|
||
if (!mime) return 'png';
|
||
if (mime.includes('jpeg')) return 'jpg';
|
||
if (mime.includes('png')) return 'png';
|
||
if (mime.includes('webp')) return 'webp';
|
||
return 'png';
|
||
}
|
||
|
||
async function uploadAvatarIfNeeded() {
|
||
if (!ui.avatarFile) return null;
|
||
if (!userId.value) throw new Error('Sessão inválida para upload.');
|
||
const file = ui.avatarFile;
|
||
const ext = extFromMime(file.type);
|
||
const path = `${userId.value}/avatar-${Date.now()}.${ext}`;
|
||
const { error: upErr } = await supabase.storage
|
||
.from(AVATAR_BUCKET)
|
||
.upload(path, file, { upsert: true, contentType: file.type });
|
||
if (upErr) throw upErr;
|
||
const { data } = supabase.storage.from(AVATAR_BUCKET).getPublicUrl(path);
|
||
const url = data?.publicUrl;
|
||
if (!url) throw new Error('Upload ok, mas não consegui obter a URL pública.');
|
||
return url;
|
||
}
|
||
|
||
// ── Carregar / Salvar ──────────────────────────────────────
|
||
async function ensureProfileExists(uid) {
|
||
const { data: prof, error: selErr } = await supabase
|
||
.from('profiles').select('id, role').eq('id', uid).maybeSingle();
|
||
if (selErr) throw selErr;
|
||
if (prof?.id) return prof;
|
||
const { data: created, error: insErr } = await supabase
|
||
.from('profiles').insert({ id: uid, role: 'portal_user' }).select('id, role').single();
|
||
if (insErr) throw insErr;
|
||
return created;
|
||
}
|
||
|
||
async function loadProfile() {
|
||
silentApplying.value = true;
|
||
loading.value = true;
|
||
try {
|
||
const { data: u, error: uErr } = await supabase.auth.getUser();
|
||
if (uErr) throw uErr;
|
||
const user = u?.user;
|
||
if (!user) throw new Error('Você precisa estar logado.');
|
||
|
||
userId.value = user.id;
|
||
userEmail.value = user.email || '';
|
||
|
||
await ensureProfileExists(user.id);
|
||
|
||
const meta = user.user_metadata || {};
|
||
form.full_name = meta.full_name || '';
|
||
form.avatar_url = meta.avatar_url || '';
|
||
ui.avatarPreview = form.avatar_url;
|
||
|
||
const { data: prof, error: pErr } = await supabase
|
||
.from('profiles')
|
||
.select(
|
||
'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, professional_registration_type, professional_registration_type_other, professional_registration_number, professional_registration_uf'
|
||
)
|
||
.eq('id', user.id)
|
||
.maybeSingle();
|
||
|
||
if (!pErr && prof) {
|
||
userRole.value = prof.role || null;
|
||
form.full_name = prof.full_name ?? form.full_name;
|
||
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 ?? '';
|
||
form.professional_registration_type = prof.professional_registration_type ?? '';
|
||
form.professional_registration_type_other = prof.professional_registration_type_other ?? '';
|
||
form.professional_registration_number = prof.professional_registration_number ?? '';
|
||
form.professional_registration_uf = prof.professional_registration_uf ?? '';
|
||
customSocials.value = Array.isArray(prof.social_custom) ? prof.social_custom : [];
|
||
ui.avatarPreview = form.avatar_url;
|
||
}
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Não consegui carregar o perfil.', life: 6000 });
|
||
} finally {
|
||
loading.value = false;
|
||
silentApplying.value = false;
|
||
dirty.value = false;
|
||
}
|
||
}
|
||
|
||
async function saveAll() {
|
||
if (!validateRequired()) {
|
||
toast.add({
|
||
severity: 'warn',
|
||
summary: 'Campos obrigatórios',
|
||
detail: 'Preencha nome completo, nome de exibição e WhatsApp antes de salvar.',
|
||
life: 4000
|
||
});
|
||
return;
|
||
}
|
||
saving.value = true;
|
||
try {
|
||
if (ui.avatarFile) {
|
||
try {
|
||
const uploadedUrl = await uploadAvatarIfNeeded();
|
||
if (uploadedUrl) {
|
||
form.avatar_url = uploadedUrl;
|
||
ui.avatarPreview = uploadedUrl;
|
||
}
|
||
} catch (e) {
|
||
toast.add({
|
||
severity: 'warn',
|
||
summary: 'Avatar não subiu',
|
||
detail: `Não consegui enviar o arquivo (bucket "${AVATAR_BUCKET}"). (${e?.message || 'erro'})`,
|
||
life: 6500
|
||
});
|
||
}
|
||
}
|
||
|
||
const metaPayload = {
|
||
full_name: String(form.full_name || '').trim(),
|
||
avatar_url: String(form.avatar_url || '').trim() || null
|
||
};
|
||
const { error: upErr } = await supabase.auth.updateUser({ data: metaPayload });
|
||
if (upErr) throw upErr;
|
||
|
||
await ensureProfileExists(userId.value);
|
||
|
||
const profilePayload = {
|
||
full_name: metaPayload.full_name,
|
||
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),
|
||
// Registro profissional (CFP) — null se vazio
|
||
professional_registration_type: String(form.professional_registration_type || '').trim() || null,
|
||
// type_other só preenchido quando type === 'outro' (limpa quando muda)
|
||
professional_registration_type_other:
|
||
form.professional_registration_type === 'outro'
|
||
? (String(form.professional_registration_type_other || '').trim() || null)
|
||
: null,
|
||
professional_registration_number: String(form.professional_registration_number || '').trim() || null,
|
||
professional_registration_uf: String(form.professional_registration_uf || '').trim() || null
|
||
};
|
||
|
||
const { data: updatedProfile, error: pErr2 } = await supabase
|
||
.from('profiles')
|
||
.update(profilePayload)
|
||
.eq('id', userId.value)
|
||
.select('id')
|
||
.single();
|
||
|
||
if (pErr2) {
|
||
const msg = String(pErr2.message || '');
|
||
const tolerant = /does not exist/i.test(msg) || /column .* does not exist/i.test(msg)
|
||
|| /relation .* does not exist/i.test(msg) || /permission denied/i.test(msg)
|
||
|| /violates row-level security/i.test(msg);
|
||
if (!tolerant) throw pErr2;
|
||
}
|
||
if (!updatedProfile) throw new Error('Perfil não encontrado para atualização.');
|
||
|
||
clearAvatarFile();
|
||
dirty.value = false;
|
||
toast.add({ severity: 'success', summary: 'Salvo', detail: 'Perfil atualizado.', life: 2500 });
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro ao salvar', detail: e?.message || 'Não consegui salvar.', life: 6000 });
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
// ── Ancoras: badge/dica leva pra sessao do form ────────────
|
||
const SECTION_BY_FIELD = {
|
||
full_name: 'identidade',
|
||
nickname: 'identidade',
|
||
work_description: 'identidade',
|
||
name: 'identidade',
|
||
nick: 'identidade',
|
||
work: 'identidade',
|
||
avatar: 'avatar',
|
||
photo: 'avatar',
|
||
bio: 'bio',
|
||
phone: 'contato',
|
||
social: 'redes'
|
||
};
|
||
|
||
function scrollToSection(field) {
|
||
const sec = SECTION_BY_FIELD[field];
|
||
if (!sec) return;
|
||
nextTick(() => {
|
||
const target = document.getElementById('mpr-sec-' + sec);
|
||
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
});
|
||
}
|
||
|
||
// ── Sair da conta ──────────────────────────────────────────
|
||
function confirmSignOut() {
|
||
confirm.require({
|
||
header: 'Sair da conta',
|
||
message: 'Tem certeza que deseja sair? Você precisará fazer login novamente.',
|
||
icon: 'pi pi-sign-out',
|
||
acceptLabel: 'Sair',
|
||
rejectLabel: 'Cancelar',
|
||
acceptClass: 'p-button-danger',
|
||
accept: async () => {
|
||
try { await supabase.auth.signOut(); }
|
||
finally { router.push('/auth/login'); }
|
||
}
|
||
});
|
||
}
|
||
|
||
// ── Lifecycle ──────────────────────────────────────────────
|
||
onMounted(async () => {
|
||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
||
isMobile.value = _mqMobile.matches;
|
||
try { _mqMobile.addEventListener('change', _onMqMobileChange); }
|
||
catch { _mqMobile.addListener(_onMqMobileChange); }
|
||
}
|
||
await tenantStore.ensureLoaded();
|
||
await loadProfile();
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (_mqMobile) {
|
||
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
|
||
catch { _mqMobile.removeListener(_onMqMobileChange); }
|
||
}
|
||
clearAvatarFile();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<!-- Drawer mobile: hospeda a sidebar contextual da pagina (Sua
|
||
evolucao + Avatar + Sair) via Teleport target. A navegacao
|
||
entre configs vive no botao "Configuracoes" no topo da
|
||
.mpr-side, que abre o MelissaConfigPopover. -->
|
||
<Transition name="mpr-drawer-fade">
|
||
<div
|
||
v-show="isMobile && drawerOpen"
|
||
class="mpr-mobile-drawer"
|
||
:class="{ 'is-open': drawerOpen }"
|
||
>
|
||
<div id="mpr-mobile-drawer-target" class="mpr-mobile-drawer__scroll" />
|
||
</div>
|
||
</Transition>
|
||
<Transition name="mpr-drawer-fade">
|
||
<div
|
||
v-show="isMobile && drawerOpen"
|
||
class="mpr-mobile-drawer__backdrop"
|
||
@click="fecharDrawer"
|
||
/>
|
||
</Transition>
|
||
|
||
<section class="mpr-page">
|
||
<header class="mpr-page__head">
|
||
<button
|
||
class="mpr-menu-btn mpr-menu-btn--mobile-only"
|
||
v-tooltip.bottom="'Configurações & Evolução'"
|
||
@click="toggleDrawer"
|
||
>
|
||
<i class="pi pi-bars" />
|
||
<span>Menu Perfil</span>
|
||
</button>
|
||
<div class="mpr-page__title">
|
||
<i class="pi pi-user mpr-page__title-icon" />
|
||
<span>Meu Perfil</span>
|
||
<span class="mpr-page__count">{{ profileProgress }}%</span>
|
||
</div>
|
||
<div class="mpr-page__actions">
|
||
<button
|
||
class="mpr-act-btn mpr-act-btn--primary"
|
||
v-tooltip.bottom="'Salvar alterações'"
|
||
:disabled="!dirty || saving"
|
||
@click="saveAll"
|
||
>
|
||
<i :class="saving ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar</span>
|
||
</button>
|
||
<button class="mpr-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
|
||
<i class="pi pi-times" />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="mpr-subheader">
|
||
<i class="pi pi-info-circle mpr-subheader__icon" />
|
||
<span class="mpr-subheader__text">
|
||
Suas informações pessoais e indicadores de evolução do perfil. Quanto mais
|
||
campos preenchidos, mais alto o seu nível.
|
||
</span>
|
||
</div>
|
||
|
||
<div class="mpr-body">
|
||
<!-- ═══ COL 1: Sidebar (gamificação + avatar). Em desktop
|
||
fica em col 1 inline. Em mobile e teleportada pro
|
||
drawer "Menu Perfil" (abaixo do menu de configs). ═══ -->
|
||
<Teleport to="#mpr-mobile-drawer-target" :disabled="!isMobile">
|
||
<aside class="mpr-side">
|
||
<!-- Botao alterna entre cards (default) e lista
|
||
de configs. Click vira "Voltar" quando aberto. -->
|
||
<button
|
||
class="mpr-cfg-btn"
|
||
:class="{ 'is-open': cfgOpen }"
|
||
@click="toggleCfg"
|
||
>
|
||
<i :class="cfgOpen ? 'pi pi-arrow-left' : 'pi pi-cog'" />
|
||
<span>{{ cfgOpen ? 'Voltar' : 'Configurações' }}</span>
|
||
<i v-if="!cfgOpen" class="pi pi-chevron-down mpr-cfg-btn__chev" />
|
||
</button>
|
||
|
||
<!-- Estado: lista de configs -->
|
||
<div v-if="cfgOpen" class="mpr-side__scroll mpr-side__scroll--cfg">
|
||
<MelissaConfigList @select="fecharCfg" />
|
||
</div>
|
||
|
||
<!-- Estado: cards contextuais (default) -->
|
||
<div v-else class="mpr-side__scroll">
|
||
<!-- Card: Sua evolução -->
|
||
<div class="mpr-w mpr-w--side">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-star" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Sua evolução</div>
|
||
<div class="mpr-w__sub">Nível, conquistas e pendências</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mpr-w__body">
|
||
<div class="mpr-level">
|
||
<div
|
||
class="mpr-level__icon"
|
||
:style="{
|
||
backgroundColor: currentLevel.color + '22',
|
||
borderColor: currentLevel.color + '55',
|
||
color: currentLevel.color
|
||
}"
|
||
v-tooltip.top="currentLevel.label"
|
||
>{{ currentLevel.icon }}</div>
|
||
<div class="mpr-level__text">
|
||
<div class="mpr-level__name">
|
||
{{ currentLevel.label }}
|
||
<span
|
||
class="mpr-level__num"
|
||
:style="{
|
||
backgroundColor: currentLevel.color + '20',
|
||
color: currentLevel.color
|
||
}"
|
||
>Nível {{ gameLevels.indexOf(currentLevel) + 1 }}</span>
|
||
</div>
|
||
<div class="mpr-level__pct" :style="{ color: progressColor }">
|
||
{{ profileProgress }}% completo
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mpr-progress">
|
||
<div
|
||
class="mpr-progress__bar"
|
||
:style="{
|
||
width: profileProgress + '%',
|
||
backgroundColor: currentLevel.color
|
||
}"
|
||
/>
|
||
</div>
|
||
|
||
<p class="mpr-progress__hint">
|
||
<span v-if="nextLevel" :style="{ color: nextLevel.color }">
|
||
{{ xpToNext }}% até {{ nextLevel.label }} {{ nextLevel.icon }}
|
||
</span>
|
||
<span v-else style="color: #10b981">🎉 Perfil 100% completo!</span>
|
||
</p>
|
||
|
||
<!-- Badges -->
|
||
<div class="mpr-badges-head">Conquistas</div>
|
||
<div class="mpr-badges">
|
||
<button
|
||
v-for="badge in badges"
|
||
:key="badge.key"
|
||
type="button"
|
||
class="mpr-badge"
|
||
:class="badge.earned ? 'is-earned' : 'is-locked'"
|
||
v-tooltip.top="badge.earned ? badge.label : 'Bloqueado — ' + badge.label"
|
||
@click="scrollToSection(badge.key)"
|
||
>
|
||
<span class="mpr-badge__icon">{{ badge.icon }}</span>
|
||
<span class="mpr-badge__label">{{ badge.label }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Dicas (campos faltando) -->
|
||
<div v-if="progressSuggestions.length" class="mpr-tips">
|
||
<div class="mpr-tips__head">O que falta</div>
|
||
<button
|
||
v-for="(tip, i) in progressSuggestions"
|
||
:key="i"
|
||
type="button"
|
||
class="mpr-tip"
|
||
@click="scrollToSection(tip.key)"
|
||
>
|
||
<i :class="tip.icon" />
|
||
{{ tip.text }}
|
||
</button>
|
||
</div>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
|
||
<!-- Card: Avatar -->
|
||
<div id="mpr-sec-avatar" class="mpr-w mpr-w--side">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-image" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Avatar</div>
|
||
<div class="mpr-w__sub">Foto exibida no menu e cabeçalho</div>
|
||
</div>
|
||
</div>
|
||
<div class="mpr-w__body">
|
||
<div class="mpr-avatar">
|
||
<div class="mpr-avatar__preview">
|
||
<img v-if="ui.avatarPreview" :src="ui.avatarPreview" alt="Avatar" />
|
||
<span v-else class="mpr-avatar__initials">{{ initials }}</span>
|
||
</div>
|
||
<div class="mpr-avatar__actions">
|
||
<button class="mpr-btn mpr-btn--primary" @click="pickAvatar">
|
||
<i class="pi pi-upload" />
|
||
<span>{{ ui.avatarPreview ? 'Trocar' : 'Enviar' }}</span>
|
||
</button>
|
||
<button
|
||
v-if="ui.avatarPreview"
|
||
class="mpr-btn mpr-btn--danger"
|
||
@click="removeAvatar"
|
||
>
|
||
<i class="pi pi-times" />
|
||
<span>Remover</span>
|
||
</button>
|
||
</div>
|
||
<input
|
||
ref="fileInput"
|
||
type="file"
|
||
accept="image/*"
|
||
hidden
|
||
@change="onAvatarFileSelected"
|
||
/>
|
||
<div class="mpr-avatar__hint">JPG, PNG ou WebP. Máximo 5 MB.</div>
|
||
</div>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!cfgOpen" class="mpr-side__footer">
|
||
<button class="mpr-btn mpr-btn--danger mpr-btn--full" @click="confirmSignOut">
|
||
<i class="pi pi-sign-out" />
|
||
<span>Sair da conta</span>
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
</Teleport>
|
||
|
||
<!-- ═══ COL 2: Main (form) ═══ -->
|
||
<div class="mpr-main">
|
||
<!-- Skeleton enquanto carrega -->
|
||
<template v-if="loading">
|
||
<div class="mpr-w" v-for="n in 3" :key="`sk-${n}`">
|
||
<div class="mpr-w__body">
|
||
<Skeleton width="40%" height="20px" class="mb-3" />
|
||
<Skeleton v-for="m in 3" :key="`sk-${n}-${m}`" width="100%" height="40px" class="mb-2" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<!-- ── Identidade ── -->
|
||
<div id="mpr-sec-identidade" class="mpr-w">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-id-card" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Identidade</div>
|
||
<div class="mpr-w__sub">Nome, apelido e descrição profissional</div>
|
||
</div>
|
||
</div>
|
||
<div class="mpr-w__body">
|
||
<div class="mpr-grid">
|
||
<div class="mpr-field mpr-field--full">
|
||
<FloatLabel variant="on">
|
||
<InputText
|
||
id="mpr_name"
|
||
v-model="form.full_name"
|
||
class="w-full"
|
||
autocomplete="name"
|
||
:invalid="!!fieldErrors.full_name"
|
||
@input="markDirty(); clearErr('full_name')"
|
||
/>
|
||
<label for="mpr_name">Nome completo *</label>
|
||
</FloatLabel>
|
||
<small v-if="fieldErrors.full_name" class="mpr-err">{{ fieldErrors.full_name }}</small>
|
||
<small v-else class="mpr-hint">Aparece no menu, cabeçalhos e registros.</small>
|
||
</div>
|
||
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputText
|
||
id="mpr_nickname"
|
||
v-model="form.nickname"
|
||
class="w-full"
|
||
autocomplete="nickname"
|
||
:invalid="!!fieldErrors.nickname"
|
||
@input="markDirty(); clearErr('nickname')"
|
||
/>
|
||
<label for="mpr_nickname">Como deveríamos te chamar? *</label>
|
||
</FloatLabel>
|
||
<small v-if="fieldErrors.nickname" class="mpr-err">{{ fieldErrors.nickname }}</small>
|
||
<small v-else class="mpr-hint">Apelido ou nome preferido.</small>
|
||
</div>
|
||
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<Select
|
||
id="mpr_work"
|
||
v-model="form.work_description"
|
||
:options="workDescriptionOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
@change="markDirty"
|
||
/>
|
||
<label for="mpr_work">Como descreve seu trabalho?</label>
|
||
</FloatLabel>
|
||
<small class="mpr-hint">Exibido no seu perfil público.</small>
|
||
</div>
|
||
|
||
<div v-if="form.work_description === 'outro'" class="mpr-field mpr-field--full">
|
||
<FloatLabel variant="on">
|
||
<InputText
|
||
id="mpr_work_other"
|
||
v-model="form.work_description_other"
|
||
class="w-full"
|
||
@input="markDirty"
|
||
/>
|
||
<label for="mpr_work_other">Especifique a profissão</label>
|
||
</FloatLabel>
|
||
</div>
|
||
</div>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
|
||
<!-- ── Registro Profissional (CFP #5) ── -->
|
||
<div id="mpr-sec-registro" class="mpr-w">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-id-card" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Registro profissional</div>
|
||
<div class="mpr-w__sub">Conselho regional — exigido para emissão de recibos, atestados e laudos</div>
|
||
</div>
|
||
</div>
|
||
<div class="mpr-w__body">
|
||
<div class="mpr-grid">
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<Select
|
||
id="mpr_reg_type"
|
||
v-model="form.professional_registration_type"
|
||
:options="REGISTRATION_TYPE_OPTIONS"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
@change="markDirty"
|
||
/>
|
||
<label for="mpr_reg_type">Tipo de registro</label>
|
||
</FloatLabel>
|
||
<small class="mpr-hint">Conselho profissional ao qual você é vinculado.</small>
|
||
</div>
|
||
|
||
<!-- Campo livre quando tipo='outro' -->
|
||
<div v-if="form.professional_registration_type === 'outro'" class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputText
|
||
id="mpr_reg_type_other"
|
||
v-model="form.professional_registration_type_other"
|
||
class="w-full"
|
||
@input="markDirty"
|
||
/>
|
||
<label for="mpr_reg_type_other">Nome do conselho/instituição *</label>
|
||
</FloatLabel>
|
||
<small class="mpr-hint">Ex: APM, ABRAP, etc.</small>
|
||
</div>
|
||
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputText
|
||
id="mpr_reg_number"
|
||
v-model="form.professional_registration_number"
|
||
class="w-full"
|
||
:disabled="!form.professional_registration_type"
|
||
@input="markDirty"
|
||
/>
|
||
<label for="mpr_reg_number">Número do registro</label>
|
||
</FloatLabel>
|
||
<small class="mpr-hint">Ex: 06/12345</small>
|
||
</div>
|
||
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<Select
|
||
id="mpr_reg_uf"
|
||
v-model="form.professional_registration_uf"
|
||
:options="UF_OPTIONS"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
:disabled="!form.professional_registration_type"
|
||
:filter="true"
|
||
class="w-full"
|
||
@change="markDirty"
|
||
/>
|
||
<label for="mpr_reg_uf">UF</label>
|
||
</FloatLabel>
|
||
<small class="mpr-hint">Estado do conselho.</small>
|
||
</div>
|
||
|
||
<div v-if="form.professional_registration_type && form.professional_registration_number" class="mpr-field mpr-field--full">
|
||
<div class="mpr-preview-box">
|
||
<span class="mpr-preview-label">Aparecerá nos documentos como:</span>
|
||
<strong class="mpr-preview-value">
|
||
{{ form.professional_registration_type === 'outro'
|
||
? (form.professional_registration_type_other || 'Conselho não informado')
|
||
: form.professional_registration_type }}
|
||
{{ form.professional_registration_number }}{{ form.professional_registration_uf ? '/' + form.professional_registration_uf : '' }}
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
|
||
<!-- ── Contato ── -->
|
||
<div id="mpr-sec-contato" class="mpr-w">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-phone" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Contato</div>
|
||
<div class="mpr-w__sub">WhatsApp e e-mail de login</div>
|
||
</div>
|
||
</div>
|
||
<div class="mpr-w__body">
|
||
<div class="mpr-grid">
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputMask
|
||
id="mpr_phone"
|
||
v-model="form.phone"
|
||
mask="(99) 99999-9999"
|
||
class="w-full"
|
||
:invalid="!!fieldErrors.phone"
|
||
@input="markDirty(); clearErr('phone')"
|
||
/>
|
||
<label for="mpr_phone">WhatsApp *</label>
|
||
</FloatLabel>
|
||
<small v-if="fieldErrors.phone" class="mpr-err">{{ fieldErrors.phone }}</small>
|
||
<small v-else class="mpr-hint">Usado pra atendimento e suporte.</small>
|
||
</div>
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputText
|
||
id="mpr_email"
|
||
:value="userEmail"
|
||
readonly
|
||
placeholder=" "
|
||
class="w-full"
|
||
/>
|
||
<label for="mpr_email">E-mail (login)</label>
|
||
</FloatLabel>
|
||
<small class="mpr-hint">Pra trocar, vá em Segurança.</small>
|
||
</div>
|
||
</div>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
|
||
<!-- ── Bio ── -->
|
||
<div id="mpr-sec-bio" class="mpr-w">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-pencil" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Bio</div>
|
||
<div class="mpr-w__sub">Apresentação curta para o seu perfil público</div>
|
||
</div>
|
||
</div>
|
||
<div class="mpr-w__body">
|
||
<FloatLabel variant="on">
|
||
<Textarea
|
||
id="mpr_bio"
|
||
v-model="form.bio"
|
||
rows="4"
|
||
class="w-full"
|
||
@input="markDirty"
|
||
/>
|
||
<label for="mpr_bio">Conte um pouco sobre você</label>
|
||
</FloatLabel>
|
||
<small class="mpr-hint">Apresentação curta — aparece no seu perfil público.</small>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
|
||
<!-- ── Sites e Redes ── -->
|
||
<div id="mpr-sec-redes" class="mpr-w">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-share-alt" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Sites e Redes</div>
|
||
<div class="mpr-w__sub">Site, redes sociais e links customizados</div>
|
||
</div>
|
||
<button class="mpr-w__action" @click="addCustomSocial">
|
||
<i class="pi pi-plus" />
|
||
<span>Adicionar</span>
|
||
</button>
|
||
</div>
|
||
<div class="mpr-w__body">
|
||
<div class="mpr-grid">
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputText id="mpr_site" v-model="form.site_url" class="w-full" @input="markDirty" />
|
||
<label for="mpr_site">Site</label>
|
||
</FloatLabel>
|
||
</div>
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputText id="mpr_ig" v-model="form.social_instagram" class="w-full" @input="markDirty" />
|
||
<label for="mpr_ig">Instagram</label>
|
||
</FloatLabel>
|
||
</div>
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputText id="mpr_yt" v-model="form.social_youtube" class="w-full" @input="markDirty" />
|
||
<label for="mpr_yt">YouTube</label>
|
||
</FloatLabel>
|
||
</div>
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputText id="mpr_fb" v-model="form.social_facebook" class="w-full" @input="markDirty" />
|
||
<label for="mpr_fb">Facebook</label>
|
||
</FloatLabel>
|
||
</div>
|
||
<div class="mpr-field mpr-field--half">
|
||
<FloatLabel variant="on">
|
||
<InputText id="mpr_x" v-model="form.social_x" class="w-full" @input="markDirty" />
|
||
<label for="mpr_x">X (Twitter)</label>
|
||
</FloatLabel>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Customs -->
|
||
<div v-if="customSocials.length" class="mpr-customs">
|
||
<div
|
||
v-for="(s, idx) in customSocials"
|
||
:key="`cs-${idx}`"
|
||
class="mpr-custom"
|
||
>
|
||
<FloatLabel variant="on" class="flex-1">
|
||
<InputText
|
||
:id="`cs_name_${idx}`"
|
||
v-model="s.name"
|
||
class="w-full"
|
||
@input="markDirty"
|
||
/>
|
||
<label :for="`cs_name_${idx}`">Nome (ex: TikTok)</label>
|
||
</FloatLabel>
|
||
<FloatLabel variant="on" class="flex-1">
|
||
<InputText
|
||
:id="`cs_url_${idx}`"
|
||
v-model="s.url"
|
||
class="w-full"
|
||
@input="markDirty"
|
||
/>
|
||
<label :for="`cs_url_${idx}`">URL</label>
|
||
</FloatLabel>
|
||
<button
|
||
class="mpr-btn mpr-btn--danger mpr-btn--icon"
|
||
v-tooltip.left="'Remover'"
|
||
@click="removeCustomSocial(idx)"
|
||
>
|
||
<i class="pi pi-trash" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
|
||
<!-- ── Layout (variante de navegação) ── -->
|
||
<div id="mpr-sec-layout" class="mpr-w">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-th-large" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Layout</div>
|
||
<div class="mpr-w__sub">Estilo de navegação principal — troca exige reload</div>
|
||
</div>
|
||
</div>
|
||
<div class="mpr-w__body">
|
||
<div class="mpr-lv-grid">
|
||
<button
|
||
class="mpr-lv-card"
|
||
:class="{ 'mpr-lv-card--current': layoutConfig.variant === 'classic' }"
|
||
:disabled="layoutConfig.variant === 'classic'"
|
||
@click="switchToVariant('classic')"
|
||
>
|
||
<div class="mpr-lv-preview mpr-lv-preview--classic">
|
||
<div class="mpr-lv-sidebar" />
|
||
<div class="mpr-lv-main">
|
||
<div class="mpr-lv-bar" />
|
||
<div class="mpr-lv-line" />
|
||
<div class="mpr-lv-line mpr-lv-line--sm" />
|
||
</div>
|
||
</div>
|
||
<div class="mpr-lv-foot">
|
||
<div class="mpr-lv-radio">
|
||
<div v-if="layoutConfig.variant === 'classic'" class="mpr-lv-dot" />
|
||
</div>
|
||
<div>
|
||
<div class="mpr-lv-name">Clássico</div>
|
||
<div class="mpr-lv-sub">Sidebar lateral com menu completo</div>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="mpr-lv-card"
|
||
:class="{ 'mpr-lv-card--current': layoutConfig.variant === 'rail' }"
|
||
:disabled="layoutConfig.variant === 'rail'"
|
||
@click="switchToVariant('rail')"
|
||
>
|
||
<div class="mpr-lv-preview mpr-lv-preview--rail">
|
||
<div class="mpr-lv-rail" />
|
||
<div class="mpr-lv-panel" />
|
||
<div class="mpr-lv-main">
|
||
<div class="mpr-lv-bar" />
|
||
<div class="mpr-lv-line" />
|
||
<div class="mpr-lv-line mpr-lv-line--sm" />
|
||
</div>
|
||
</div>
|
||
<div class="mpr-lv-foot">
|
||
<div class="mpr-lv-radio">
|
||
<div v-if="layoutConfig.variant === 'rail'" class="mpr-lv-dot" />
|
||
</div>
|
||
<div>
|
||
<div class="mpr-lv-name">Rail</div>
|
||
<div class="mpr-lv-sub">Ícones no canto esquerdo + painel expansível. Disponível apenas no desktop.</div>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
|
||
<button
|
||
class="mpr-lv-card"
|
||
:class="{ 'mpr-lv-card--current': layoutConfig.variant === 'melissa' }"
|
||
:disabled="layoutConfig.variant === 'melissa'"
|
||
@click="switchToVariant('melissa')"
|
||
>
|
||
<div class="mpr-lv-preview mpr-lv-preview--melissa">
|
||
<div class="mpr-lv-melissa-bg" />
|
||
<div class="mpr-lv-melissa-dock" />
|
||
</div>
|
||
<div class="mpr-lv-foot">
|
||
<div class="mpr-lv-radio">
|
||
<div v-if="layoutConfig.variant === 'melissa'" class="mpr-lv-dot" />
|
||
</div>
|
||
<div>
|
||
<div class="mpr-lv-name">Melissa</div>
|
||
<div class="mpr-lv-sub">Lockscreen-style com dock central (atual)</div>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
|
||
<!-- ── Preferências (tema + aparência) ── -->
|
||
<div id="mpr-sec-preferencias" class="mpr-w">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-palette" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Preferências</div>
|
||
<div class="mpr-w__sub">Aparência do sistema</div>
|
||
</div>
|
||
</div>
|
||
<div class="mpr-w__body">
|
||
<div class="mpr-grid">
|
||
<div class="mpr-field mpr-field--full">
|
||
<label class="mpr-label">Tema</label>
|
||
<div class="mpr-theme-row">
|
||
<button
|
||
type="button"
|
||
class="mpr-lv-card mpr-theme-card"
|
||
:class="{ 'mpr-lv-card--current': !isDarkTheme }"
|
||
@click="isDarkTheme && toggleDarkMode()"
|
||
>
|
||
<i class="pi pi-sun mpr-theme-icon" style="color: #f59e0b;" />
|
||
<div class="mpr-theme-text">
|
||
<div class="mpr-lv-name">Claro</div>
|
||
<div class="mpr-lv-sub">Fundo branco</div>
|
||
</div>
|
||
<div class="mpr-lv-radio">
|
||
<div v-if="!isDarkTheme" class="mpr-lv-dot" />
|
||
</div>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="mpr-lv-card mpr-theme-card"
|
||
:class="{ 'mpr-lv-card--current': isDarkTheme }"
|
||
@click="!isDarkTheme && toggleDarkMode()"
|
||
>
|
||
<i class="pi pi-moon mpr-theme-icon" style="color: #6366f1;" />
|
||
<div class="mpr-theme-text">
|
||
<div class="mpr-lv-name">Escuro</div>
|
||
<div class="mpr-lv-sub">Menos fadiga visual</div>
|
||
</div>
|
||
<div class="mpr-lv-radio">
|
||
<div v-if="isDarkTheme" class="mpr-lv-dot" />
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<small class="mpr-hint">A preferência é salva no seu perfil.</small>
|
||
</div>
|
||
</div>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
|
||
<!-- ── Segurança ── -->
|
||
<div id="mpr-sec-seguranca" class="mpr-w">
|
||
<div class="mpr-w__head">
|
||
<div class="mpr-w__icon"><i class="pi pi-shield" /></div>
|
||
<div class="mpr-w__title">
|
||
<div class="mpr-w__title-text">Segurança</div>
|
||
<div class="mpr-w__sub">Senha e proteção da conta</div>
|
||
</div>
|
||
</div>
|
||
<div class="mpr-w__body">
|
||
<div class="mpr-grid">
|
||
<div class="mpr-field mpr-field--full">
|
||
<div class="mpr-info-row">
|
||
<div class="mpr-info-text">
|
||
<div class="mpr-info-title">E-mail de acesso</div>
|
||
<div class="mpr-info-value">{{ userEmail }}</div>
|
||
</div>
|
||
<small class="mpr-hint mpr-info-hint">Para trocar o e-mail, contate o suporte.</small>
|
||
</div>
|
||
</div>
|
||
<div class="mpr-field mpr-field--full">
|
||
<button type="button" class="mpr-lv-card mpr-action-card" @click="goSeguranca">
|
||
<i class="pi pi-key mpr-action-icon" />
|
||
<div class="mpr-action-text">
|
||
<div class="mpr-lv-name">Trocar senha</div>
|
||
<div class="mpr-lv-sub">Atualize sua senha de acesso ao sistema</div>
|
||
</div>
|
||
<i class="pi pi-arrow-right mpr-action-arrow" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div><!-- /.mpr-w__body -->
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* ═══════ Page chrome ═══════ */
|
||
.mpr-page {
|
||
position: absolute;
|
||
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) 6px;
|
||
z-index: 40;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--m-bg-medium);
|
||
backdrop-filter: blur(32px) saturate(160%);
|
||
-webkit-backdrop-filter: blur(32px) saturate(160%);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 18px;
|
||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
|
||
overflow: hidden;
|
||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
color: var(--m-text);
|
||
animation: mpr-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||
}
|
||
@keyframes mpr-page-enter {
|
||
from { opacity: 0; transform: scale(0.985); }
|
||
to { opacity: 1; transform: scale(1); }
|
||
}
|
||
|
||
.mpr-page__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 14px 18px;
|
||
border-bottom: 1px solid var(--m-border);
|
||
flex-shrink: 0;
|
||
gap: 10px;
|
||
}
|
||
.mpr-page__title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mpr-page__title-icon {
|
||
color: var(--p-primary-color);
|
||
font-size: 1.05rem;
|
||
}
|
||
.mpr-page__count {
|
||
margin-left: auto;
|
||
background: color-mix(in srgb, var(--p-primary-color) 18%, transparent);
|
||
color: var(--p-primary-color);
|
||
border-radius: 999px;
|
||
padding: 3px 10px;
|
||
font-size: 0.74rem;
|
||
font-weight: 700;
|
||
}
|
||
.mpr-page__actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.mpr-act-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 7px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
transition: background-color 120ms ease, border-color 120ms ease, opacity 120ms ease;
|
||
}
|
||
.mpr-act-btn:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
}
|
||
.mpr-act-btn--primary {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
.mpr-act-btn--primary:hover {
|
||
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
|
||
}
|
||
.mpr-act-btn:disabled {
|
||
opacity: 0.45;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.mpr-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: transparent;
|
||
border: 1px solid var(--m-border);
|
||
color: var(--m-text-muted);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: background-color 120ms ease, color 120ms ease;
|
||
}
|
||
.mpr-close:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
color: var(--m-text);
|
||
}
|
||
|
||
.mpr-menu-btn {
|
||
display: none;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 7px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
.mpr-menu-btn:hover { background: var(--m-bg-soft-hover); }
|
||
|
||
.mpr-subheader {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 10px 18px;
|
||
background: var(--m-bg-soft);
|
||
border-bottom: 1px solid var(--m-border);
|
||
color: var(--m-text-muted);
|
||
font-size: 0.78rem;
|
||
flex-shrink: 0;
|
||
}
|
||
.mpr-subheader__icon {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.85rem;
|
||
margin-top: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Body 2-col ═══════ */
|
||
.mpr-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Botao "Configuracoes" no topo da .mpr-side. Click alterna entre
|
||
cards (default) e lista de configs (estado is-open: vira "Voltar"). */
|
||
.mpr-cfg-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: calc(100% - 24px);
|
||
margin: 12px 12px 0;
|
||
padding: 10px 12px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 9px;
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.84rem;
|
||
font-weight: 600;
|
||
text-align: left;
|
||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||
flex-shrink: 0;
|
||
}
|
||
.mpr-cfg-btn:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
}
|
||
.mpr-cfg-btn.is-open {
|
||
background: color-mix(in srgb, var(--p-primary-color) 14%, transparent);
|
||
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
|
||
color: var(--p-primary-color);
|
||
}
|
||
.mpr-cfg-btn > i:first-child {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.92rem;
|
||
}
|
||
.mpr-cfg-btn > span {
|
||
flex: 1;
|
||
}
|
||
.mpr-cfg-btn__chev {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.7rem;
|
||
}
|
||
|
||
/* Variante do scroll quando renderiza a lista de configs em vez
|
||
dos cards — sem gap entre items (a propria lista controla). */
|
||
.mpr-side__scroll--cfg {
|
||
padding: 8px;
|
||
gap: 0;
|
||
}
|
||
|
||
/* COL 1: sidebar */
|
||
.mpr-side {
|
||
width: 320px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--m-bg-soft);
|
||
border-right: 1px solid var(--m-border);
|
||
overflow: hidden;
|
||
}
|
||
.mpr-side__scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--m-border-strong) transparent;
|
||
}
|
||
.mpr-side__scroll::-webkit-scrollbar { width: 5px; }
|
||
.mpr-side__scroll::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
.mpr-side__footer {
|
||
flex-shrink: 0;
|
||
padding: 12px;
|
||
background: var(--m-bg-medium);
|
||
border-top: 1px solid var(--m-border);
|
||
}
|
||
|
||
/* COL 2: main */
|
||
.mpr-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--m-border-strong) transparent;
|
||
}
|
||
.mpr-main::-webkit-scrollbar { width: 5px; }
|
||
.mpr-main::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Desktop (>=1024px): cards em 2 colunas (50/50) no main. Internal
|
||
grid colapsa pra 1 col pra nao ficar cramped. Cada card tem
|
||
min-height 300px + max-height 100% (do container) e overflow-y:
|
||
auto no body — adicionar redes/customs nao quebra layout, scroll
|
||
interno engata. Mesma logica nos cards da sidebar (Sua evolucao,
|
||
Avatar) que cresciam quando muitas badges/dicas apareciam. */
|
||
@media (min-width: 1024px) {
|
||
/* Fake dialog: largura adaptativa, alinhada a esquerda (apos o
|
||
config-aside global). Espelha pattern de Seguranca/Pagamento/etc:
|
||
- 1024–1012px : full-width (right: 6px) — overlap minimo
|
||
- 1012–2012px : width = 1000px fixo (right cresce com viewport)
|
||
- >= 2012px : width = ~50% do viewport (right: 50%) */
|
||
.mpr-page {
|
||
right: max(6px, min(50%, calc(100% - 1006px)));
|
||
}
|
||
|
||
/* Cards do main empilhados em 1-col (era grid 2-col antes).
|
||
.mpr-main herda flex-direction: column do base — cards crescem
|
||
com conteudo, sem scroll interno. Cada card ocupa width total
|
||
do dialog (max ~1000px). */
|
||
.mpr-main > .mpr-w {
|
||
flex-shrink: 0;
|
||
height: auto;
|
||
}
|
||
/* Cards da sidebar (Sua evolucao + Avatar): altura natural,
|
||
flex-shrink: 0 evita compressao quando total passa do
|
||
.mpr-side__scroll (scroll externo). */
|
||
.mpr-side > .mpr-side__scroll > .mpr-w--side {
|
||
display: flex;
|
||
flex-direction: column;
|
||
flex-shrink: 0;
|
||
height: auto;
|
||
}
|
||
}
|
||
|
||
/* ═══════ Card-base (mpr-w) — visual igual aos cards do MelissaFinanceiro:
|
||
head com icon-box colorido + titulo + subtitulo + border-bottom; body
|
||
com padding interno separado; box-shadow pra elevacao. */
|
||
.mpr-w {
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
/* Cards na sidebar — bg --m-bg-medium destacando do bg --m-bg-soft
|
||
da .mpr-side (espelha o pattern do MelissaPacientes .mp-w). */
|
||
.mpr-w--side {
|
||
background: var(--m-bg-medium);
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
|
||
}
|
||
.mpr-w__head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--m-border);
|
||
}
|
||
.mpr-w__icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 9px;
|
||
flex-shrink: 0;
|
||
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
|
||
color: var(--p-primary-color);
|
||
font-size: 1rem;
|
||
}
|
||
.mpr-w__icon > i { font-size: 0.95rem; }
|
||
.mpr-w__title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.mpr-w__title-text {
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
line-height: 1.2;
|
||
}
|
||
.mpr-w__sub {
|
||
font-size: 0.74rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 2px;
|
||
line-height: 1.3;
|
||
}
|
||
.mpr-w__action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 5px 10px;
|
||
border-radius: 7px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-medium);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-size: 0.74rem;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
transition: background-color 120ms ease;
|
||
}
|
||
.mpr-w__action:hover { background: var(--m-bg-soft-hover); }
|
||
.mpr-w__action > i { font-size: 0.72rem; color: var(--p-primary-color); }
|
||
|
||
/* Body do card — wrapper unificado pra padding/gap consistente */
|
||
.mpr-w__body {
|
||
padding: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* ═══════ Gamificação ═══════ */
|
||
.mpr-level {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
.mpr-level__icon {
|
||
width: 44px;
|
||
height: 44px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 12px;
|
||
border: 1px solid;
|
||
font-size: 1.5rem;
|
||
flex-shrink: 0;
|
||
}
|
||
.mpr-level__text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.mpr-level__name {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mpr-level__num {
|
||
font-size: 0.66rem;
|
||
font-weight: 700;
|
||
padding: 2px 7px;
|
||
border-radius: 999px;
|
||
}
|
||
.mpr-level__pct {
|
||
font-size: 0.74rem;
|
||
font-weight: 700;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.mpr-progress {
|
||
width: 100%;
|
||
height: 8px;
|
||
border-radius: 999px;
|
||
background: var(--m-bg-soft);
|
||
overflow: hidden;
|
||
}
|
||
.mpr-progress__bar {
|
||
height: 100%;
|
||
border-radius: 999px;
|
||
transition: width 600ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||
}
|
||
.mpr-progress__hint {
|
||
margin: 0;
|
||
font-size: 0.72rem;
|
||
color: var(--m-text-muted);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.mpr-badges-head {
|
||
font-size: 0.66rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--m-text-muted);
|
||
}
|
||
.mpr-badges {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
.mpr-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 4px 8px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--m-border);
|
||
font-size: 0.7rem;
|
||
font-family: inherit;
|
||
cursor: pointer;
|
||
transition: transform 120ms ease, background-color 120ms ease;
|
||
}
|
||
.mpr-badge:hover { transform: translateY(-1px); }
|
||
.mpr-badge.is-earned {
|
||
background: color-mix(in srgb, var(--p-primary-color) 14%, transparent);
|
||
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
|
||
color: var(--p-primary-color);
|
||
font-weight: 600;
|
||
}
|
||
.mpr-badge.is-locked {
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text-faint);
|
||
opacity: 0.6;
|
||
}
|
||
.mpr-badge__icon { font-size: 0.85rem; line-height: 1; }
|
||
.mpr-badge__label { line-height: 1; }
|
||
|
||
.mpr-tips {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid var(--m-border);
|
||
}
|
||
.mpr-tips__head {
|
||
font-size: 0.66rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--m-text-muted);
|
||
}
|
||
.mpr-tip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 8px;
|
||
border-radius: 6px;
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
color: var(--m-text-muted);
|
||
font-size: 0.72rem;
|
||
font-family: inherit;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease;
|
||
}
|
||
.mpr-tip:hover {
|
||
background: color-mix(in srgb, var(--p-primary-color) 12%, transparent);
|
||
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
|
||
color: var(--p-primary-color);
|
||
}
|
||
.mpr-tip > i { font-size: 0.7rem; color: var(--p-primary-color); }
|
||
|
||
/* ═══════ Avatar (sidebar) ═══════ */
|
||
.mpr-avatar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 4px 0 0;
|
||
}
|
||
.mpr-avatar__preview {
|
||
width: 96px;
|
||
height: 96px;
|
||
border-radius: 18px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
overflow: hidden;
|
||
display: grid;
|
||
place-items: center;
|
||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
|
||
}
|
||
.mpr-avatar__preview > img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
.mpr-avatar__initials {
|
||
font-size: 2rem;
|
||
font-weight: 800;
|
||
color: var(--m-text);
|
||
}
|
||
.mpr-avatar__actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
.mpr-avatar__hint {
|
||
font-size: 0.7rem;
|
||
color: var(--m-text-muted);
|
||
text-align: center;
|
||
}
|
||
|
||
/* ═══════ Buttons (sidebar utilitarios) ═══════ */
|
||
.mpr-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding: 7px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
transition: background-color 120ms ease;
|
||
}
|
||
.mpr-btn:hover { background: var(--m-bg-soft-hover); }
|
||
.mpr-btn--primary {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
.mpr-btn--primary:hover {
|
||
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
|
||
}
|
||
.mpr-btn--danger {
|
||
background: rgba(220, 38, 38, 0.12);
|
||
border-color: rgba(220, 38, 38, 0.40);
|
||
color: rgb(220, 38, 38);
|
||
}
|
||
.mpr-btn--danger:hover {
|
||
background: rgba(220, 38, 38, 0.22);
|
||
}
|
||
.mpr-btn--full { width: 100%; }
|
||
.mpr-btn--icon {
|
||
padding: 7px 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Form grid (main) ═══════ */
|
||
.mpr-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 14px 12px;
|
||
margin-top: 4px;
|
||
}
|
||
.mpr-field { min-width: 0; }
|
||
.mpr-field--full { grid-column: 1 / -1; }
|
||
.mpr-field--half { grid-column: span 1; }
|
||
|
||
.mpr-hint, .mpr-err {
|
||
display: block;
|
||
margin-top: 4px;
|
||
font-size: 0.7rem;
|
||
line-height: 1.3;
|
||
}
|
||
.mpr-hint { color: var(--m-text-muted); }
|
||
.mpr-err { color: rgb(220, 38, 38); font-weight: 500; }
|
||
|
||
/* ═══════ Custom socials ═══════ */
|
||
.mpr-customs {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
padding-top: 10px;
|
||
border-top: 1px dashed var(--m-border);
|
||
}
|
||
.mpr-custom {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: stretch;
|
||
}
|
||
.mpr-custom > .p-floatlabel,
|
||
.mpr-custom > div { min-width: 0; }
|
||
|
||
/* ═══════ Mobile drawer — agora hospeda o MENU GLOBAL DE CONFIGS
|
||
no topo + a sidebar contextual (Sua evolucao + Avatar) abaixo via
|
||
Teleport target. Em desktop o drawer fica oculto e o
|
||
MelissaLayout renderiza o menu fixado na esquerda. */
|
||
.mpr-mobile-drawer {
|
||
position: fixed;
|
||
top: 0; left: 0;
|
||
height: 100vh;
|
||
height: 100dvh;
|
||
width: min(360px, 88vw);
|
||
z-index: 80;
|
||
background: var(--m-bg-medium);
|
||
backdrop-filter: blur(28px) saturate(160%);
|
||
-webkit-backdrop-filter: blur(28px) saturate(160%);
|
||
border-right: 1px solid var(--m-border);
|
||
transform: translateX(-100%);
|
||
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||
color: var(--m-text);
|
||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mpr-mobile-drawer.is-open { transform: translateX(0); }
|
||
.mpr-mobile-drawer__scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
/* Aside teleportada pro drawer — perde adornos proprios */
|
||
.mpr-mobile-drawer__scroll .mpr-side {
|
||
flex: 1;
|
||
min-height: 0;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
background: transparent;
|
||
border-right: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mpr-mobile-drawer__scroll .mpr-cfg-btn {
|
||
margin: 12px 12px 0;
|
||
flex-shrink: 0;
|
||
}
|
||
.mpr-mobile-drawer__scroll .mpr-side__scroll {
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--m-border-strong) transparent;
|
||
}
|
||
.mpr-mobile-drawer__scroll .mpr-side__scroll::-webkit-scrollbar { width: 5px; }
|
||
.mpr-mobile-drawer__scroll .mpr-side__scroll::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
.mpr-mobile-drawer__scroll .mpr-w--side {
|
||
margin: 0;
|
||
flex-shrink: 0;
|
||
}
|
||
.mpr-mobile-drawer__scroll .mpr-side__footer {
|
||
flex-shrink: 0;
|
||
margin: 0;
|
||
padding: 12px;
|
||
background: var(--m-bg-medium);
|
||
border-top: 1px solid var(--m-border);
|
||
}
|
||
|
||
.mpr-mobile-drawer__backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
backdrop-filter: blur(4px);
|
||
-webkit-backdrop-filter: blur(4px);
|
||
z-index: 79;
|
||
}
|
||
.mpr-drawer-fade-enter-active,
|
||
.mpr-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||
.mpr-drawer-fade-enter-from,
|
||
.mpr-drawer-fade-leave-to { opacity: 0; }
|
||
|
||
/* ═══════ Mobile (<1024px) ═══════ */
|
||
@media (max-width: 1023px) {
|
||
.mpr-body {
|
||
flex-direction: column;
|
||
padding: 0;
|
||
}
|
||
/* .mpr-side foi Teleportada pro drawer — nao precisa de display: none
|
||
(Teleport ja a removeu do .mpr-body). Aplicar display: none aqui
|
||
escondia o elemento teleportado dentro do drawer. */
|
||
.mpr-main {
|
||
width: 100%;
|
||
padding: 8px;
|
||
}
|
||
/* Cards em mobile: altura natural por conteudo (sem stretch
|
||
vertical do flex column do .mpr-main, sem clip por overflow). */
|
||
.mpr-main .mpr-w {
|
||
height: auto;
|
||
flex: 0 0 auto;
|
||
align-self: stretch;
|
||
}
|
||
.mpr-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.mpr-field--half { grid-column: 1 / -1; }
|
||
.mpr-page__title > span:first-of-type { display: none; }
|
||
.mpr-page__title-icon { display: none; }
|
||
.mpr-menu-btn--mobile-only { display: inline-flex; }
|
||
/* Botao Salvar mostra o texto em mobile (acao primaria importante) */
|
||
.mpr-custom { flex-direction: column; gap: 8px; }
|
||
.mpr-custom .mpr-btn--icon { align-self: flex-end; }
|
||
}
|
||
|
||
/* ═══════ Layout variant cards ═══════ */
|
||
.mpr-lv-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
gap: 14px;
|
||
}
|
||
.mpr-lv-card {
|
||
background: var(--m-surface-2, var(--surface-card));
|
||
border: 1px solid var(--m-border, var(--surface-border));
|
||
border-radius: 10px;
|
||
padding: 12px;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
transition: border-color .15s, background .15s, transform .12s;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
color: var(--text-color);
|
||
}
|
||
.mpr-lv-card:hover:not(:disabled) {
|
||
border-color: var(--p-primary-500, #7c3aed);
|
||
background: var(--m-surface-hover, var(--surface-hover));
|
||
}
|
||
.mpr-lv-card:disabled {
|
||
cursor: default;
|
||
opacity: 0.9;
|
||
}
|
||
.mpr-lv-card--current {
|
||
border-color: var(--p-primary-500, #7c3aed);
|
||
box-shadow: 0 0 0 1px var(--p-primary-500, #7c3aed) inset;
|
||
}
|
||
.mpr-lv-preview {
|
||
height: 92px;
|
||
border-radius: 6px;
|
||
background: var(--m-surface-3, var(--surface-100));
|
||
border: 1px solid var(--m-border, var(--surface-border));
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: flex;
|
||
}
|
||
.mpr-lv-preview--classic .mpr-lv-sidebar {
|
||
width: 30%;
|
||
background: var(--p-primary-500, #7c3aed);
|
||
opacity: 0.85;
|
||
}
|
||
.mpr-lv-preview--rail .mpr-lv-rail {
|
||
width: 12%;
|
||
background: var(--p-primary-500, #7c3aed);
|
||
opacity: 0.85;
|
||
}
|
||
.mpr-lv-preview--rail .mpr-lv-panel {
|
||
width: 22%;
|
||
background: var(--p-primary-500, #7c3aed);
|
||
opacity: 0.35;
|
||
}
|
||
.mpr-lv-main {
|
||
flex: 1;
|
||
padding: 8px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
.mpr-lv-bar {
|
||
height: 8px;
|
||
background: var(--m-border-strong, var(--surface-300));
|
||
border-radius: 2px;
|
||
opacity: 0.6;
|
||
}
|
||
.mpr-lv-line {
|
||
height: 5px;
|
||
background: var(--m-border-strong, var(--surface-300));
|
||
border-radius: 2px;
|
||
opacity: 0.4;
|
||
}
|
||
.mpr-lv-line--sm { width: 65%; }
|
||
.mpr-lv-preview--melissa {
|
||
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
|
||
}
|
||
.mpr-lv-melissa-bg {
|
||
position: absolute;
|
||
inset: 8px;
|
||
border-radius: 4px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
}
|
||
.mpr-lv-melissa-dock {
|
||
position: absolute;
|
||
bottom: 8px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 60%;
|
||
height: 14px;
|
||
border-radius: 6px;
|
||
background: rgba(255, 255, 255, 0.25);
|
||
}
|
||
.mpr-lv-foot {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 9px;
|
||
}
|
||
.mpr-lv-radio {
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 9999px;
|
||
border: 1.5px solid var(--m-border-strong, var(--surface-400));
|
||
display: grid;
|
||
place-items: center;
|
||
flex-shrink: 0;
|
||
margin-top: 3px;
|
||
}
|
||
.mpr-lv-card--current .mpr-lv-radio {
|
||
border-color: var(--p-primary-500, #7c3aed);
|
||
}
|
||
.mpr-lv-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 9999px;
|
||
background: var(--p-primary-500, #7c3aed);
|
||
}
|
||
.mpr-lv-name {
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: var(--text-color);
|
||
line-height: 1.2;
|
||
}
|
||
.mpr-lv-sub {
|
||
font-size: 0.75rem;
|
||
color: var(--text-color-secondary);
|
||
line-height: 1.3;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* ═══════════ Registro Profissional — preview box ═══════════ */
|
||
.mpr-preview-box {
|
||
background: color-mix(in srgb, var(--p-primary-color) 7%, transparent);
|
||
border: 1px solid color-mix(in srgb, var(--p-primary-color) 22%, var(--surface-border));
|
||
border-radius: 8px;
|
||
padding: 10px 14px;
|
||
font-size: 0.88rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.mpr-preview-label {
|
||
color: var(--text-color-secondary);
|
||
}
|
||
.mpr-preview-value {
|
||
color: var(--text-color);
|
||
font-weight: 600;
|
||
letter-spacing: 0.01em;
|
||
}
|
||
|
||
/* ═══════════ Preferências — tema em 1 linha ═══════════ */
|
||
.mpr-theme-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
}
|
||
.mpr-theme-card {
|
||
padding: 14px 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
min-width: 0;
|
||
text-align: left;
|
||
}
|
||
.mpr-theme-icon {
|
||
font-size: 1.3rem;
|
||
flex-shrink: 0;
|
||
}
|
||
.mpr-theme-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
/* ═══════════ Segurança — info row + action card ═══════════ */
|
||
.mpr-info-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
padding: 14px;
|
||
border: 1px solid var(--surface-border);
|
||
border-radius: 8px;
|
||
background: var(--surface-ground, transparent);
|
||
}
|
||
.mpr-info-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.mpr-info-title {
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
color: var(--text-color);
|
||
}
|
||
.mpr-info-value {
|
||
color: var(--text-color-secondary);
|
||
font-size: 0.9rem;
|
||
}
|
||
.mpr-info-hint {
|
||
margin: 0;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.mpr-action-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
padding: 14px 18px;
|
||
width: 100%;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
.mpr-action-icon {
|
||
font-size: 1.4rem;
|
||
color: var(--text-color);
|
||
flex-shrink: 0;
|
||
}
|
||
.mpr-action-text {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.mpr-action-arrow {
|
||
opacity: 0.5;
|
||
flex-shrink: 0;
|
||
}
|
||
</style>
|