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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user