melissa/perfil: 3 cards novos — registro CFP + preferencias + seguranca

Espelha as melhorias do ProfilePage no perfil nativo Melissa
(/melissa/perfil), com 4 changes:

1. Card "Registro Profissional" (id=mpr-sec-registro, antes do
   card Layout): Select tipo + Number + Select UF + preview ao vivo
   "Aparecera nos documentos como: CRP 06/12345/SP". 3 colunas de
   migration 20260521000003 wire-up no load + save.

2. Card "Layout" — sub do Rail atualizado pra mensagem solicitada:
   "Icones no canto esquerdo + painel expansivel. Disponivel apenas
   no desktop."

3. Card "Preferencias" (id=mpr-sec-preferencias, depois do Layout):
   toggle Tema Claro vs Escuro com cards visuais + sun/moon icons.
   Usa isDarkTheme + toggleDarkMode do useLayout.

4. Card "Seguranca" (id=mpr-sec-seguranca, ultimo): mostra e-mail
   atual readonly + botao "Trocar senha" que navega pra
   /account/security.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-21 11:04:32 -03:00
parent ae1e1388b9
commit 20d2b3aee4
+182 -5
View File
@@ -31,7 +31,29 @@ const toast = useToast();
const confirm = useConfirm();
const router = useRouter();
const tenantStore = useTenantStore();
const { layoutConfig, setVariant } = useLayout();
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() {
router.push('/account/security');
}
// Troca de layout variant (classic/rail/melissa). Confirma + persiste +
// hard reload — sair do shell Melissa requer reload pq AppLayout não tem
@@ -134,7 +156,11 @@ const form = reactive({
social_instagram: '',
social_youtube: '',
social_facebook: '',
social_x: ''
social_x: '',
// Registro profissional (CFP #5 — exigido pra emissão de recibos/laudos)
professional_registration_type: '',
professional_registration_number: '',
professional_registration_uf: ''
});
const customSocials = ref([]);
@@ -345,7 +371,7 @@ async function loadProfile() {
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'
'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_number, professional_registration_uf'
)
.eq('id', user.id)
.maybeSingle();
@@ -364,6 +390,9 @@ async function loadProfile() {
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_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;
}
@@ -430,7 +459,11 @@ async function saveAll() {
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)
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,
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
@@ -984,6 +1017,63 @@ onBeforeUnmount(() => {
</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" style="background: rgba(14,165,233,0.1); color: #0ea5e9"><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-col-6">
<label class="mpr-label">Tipo de registro</label>
<Select
v-model="form.professional_registration_type"
:options="REGISTRATION_TYPE_OPTIONS"
optionLabel="label"
optionValue="value"
class="w-full"
@update:modelValue="markDirty"
/>
<small class="mpr-hint">Conselho profissional ao qual você é vinculado.</small>
</div>
<div class="mpr-field mpr-col-3">
<label class="mpr-label">Número</label>
<InputText
v-model="form.professional_registration_number"
class="w-full"
:disabled="!form.professional_registration_type"
@input="markDirty"
/>
<small class="mpr-hint">Ex: 06/12345</small>
</div>
<div class="mpr-field mpr-col-3">
<label class="mpr-label">UF</label>
<Select
v-model="form.professional_registration_uf"
:options="UF_OPTIONS"
optionLabel="label"
optionValue="value"
:disabled="!form.professional_registration_type"
:filter="true"
class="w-full"
@update:modelValue="markDirty"
/>
<small class="mpr-hint">Estado do conselho.</small>
</div>
<div v-if="form.professional_registration_type && form.professional_registration_number" class="mpr-col-12">
<div style="background: rgba(14,165,233,0.08); border: 1px solid rgba(14,165,233,0.2); border-radius: 6px; padding: 10px 14px; font-size: 0.9rem;">
<span style="opacity: 0.7; margin-right: 8px;">Aparecerá nos documentos como:</span>
<strong>{{ form.professional_registration_type }} {{ form.professional_registration_number }}{{ form.professional_registration_uf ? '/' + form.professional_registration_uf : '' }}</strong>
</div>
</div>
</div>
</div>
</div>
<!-- Layout (variante de navegação) -->
<div id="mpr-sec-layout" class="mpr-w">
<div class="mpr-w__head">
@@ -1041,7 +1131,7 @@ onBeforeUnmount(() => {
</div>
<div>
<div class="mpr-lv-name">Rail</div>
<div class="mpr-lv-sub">Mini rail + painel expansível, full-width</div>
<div class="mpr-lv-sub">Ícones no canto esquerdo + painel expansível. Disponível apenas no desktop.</div>
</div>
</div>
</button>
@@ -1069,6 +1159,93 @@ onBeforeUnmount(() => {
</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" style="background: rgba(168,85,247,0.1); color: #a855f7"><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 tema e densidade</div>
</div>
</div>
<div class="mpr-w__body">
<div class="mpr-grid">
<div class="mpr-field mpr-col-12">
<label class="mpr-label">Tema</label>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button
type="button"
class="mpr-lv-card"
:class="{ 'mpr-lv-card--current': !isDarkTheme }"
style="padding: 14px 18px; flex: 1; min-width: 180px; display: flex; align-items: center; gap: 12px;"
@click="isDarkTheme && toggleDarkMode()"
>
<i class="pi pi-sun" style="font-size: 1.3rem; color: #f59e0b;" />
<div style="text-align: left;">
<div class="mpr-lv-name">Claro</div>
<div class="mpr-lv-sub">Fundo branco, contraste alto</div>
</div>
<div class="mpr-lv-radio" style="margin-left: auto;">
<div v-if="!isDarkTheme" class="mpr-lv-dot" />
</div>
</button>
<button
type="button"
class="mpr-lv-card"
:class="{ 'mpr-lv-card--current': isDarkTheme }"
style="padding: 14px 18px; flex: 1; min-width: 180px; display: flex; align-items: center; gap: 12px;"
@click="!isDarkTheme && toggleDarkMode()"
>
<i class="pi pi-moon" style="font-size: 1.3rem; color: #6366f1;" />
<div style="text-align: left;">
<div class="mpr-lv-name">Escuro</div>
<div class="mpr-lv-sub">Fundo escuro, menos fadiga visual</div>
</div>
<div class="mpr-lv-radio" style="margin-left: auto;">
<div v-if="isDarkTheme" class="mpr-lv-dot" />
</div>
</button>
</div>
<small class="mpr-hint">A preferência é salva no seu perfil e segue você em qualquer navegador.</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" style="background: rgba(239,68,68,0.1); color: #ef4444"><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-col-12">
<div style="display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding: 14px; border: 1px solid var(--m-border, rgba(0,0,0,0.1)); border-radius: 8px;">
<div style="flex: 1; min-width: 200px;">
<div style="font-weight: 600; margin-bottom: 4px;">E-mail de acesso</div>
<div style="opacity: 0.7; font-size: 0.9rem;">{{ userEmail }}</div>
</div>
<small class="mpr-hint" style="margin: 0;">Para trocar o e-mail, contate o suporte.</small>
</div>
</div>
<div class="mpr-field mpr-col-12">
<button type="button" class="mpr-lv-card" style="display: flex; align-items: center; gap: 14px; padding: 14px 18px; width: 100%; text-align: left;" @click="goSeguranca">
<i class="pi pi-key" style="font-size: 1.4rem; color: #ef4444;" />
<div style="flex: 1;">
<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" style="opacity: 0.5;" />
</button>
</div>
</div>
</div><!-- /.mpr-w__body -->
</div>
</template>
</div>
</div>