profile: UI dos campos de registro profissional (CFP #5)

Gap detectado em teste manual: migration 20260521000003 adicionou
as 3 colunas (professional_registration_type/_number/_uf) e o
DocumentGenerate.service.loadTherapistData ja le delas, mas a UI
de edicao nao foi criada.

ProfilePage.vue ganha novo card "Registro Profissional" (id=
registro-profissional, cor #0ea5e9 ciano, antes do card de Redes
Sociais):
- Select tipo (CRP/CRM/CRFa/CREFITO/CRESS/CRN/RMS/outro — mesmas
  opcoes do CHECK constraint)
- InputText numero
- Select UF (27 estados, filterable)
- Preview: "Aparecera nos documentos como: CRP 06/12345/SP"
- Numero e UF disabled enquanto tipo nao escolhido

Wire-up: SELECT/UPDATE do profile agora incluem as 3 colunas.
form.* tem defaults vazios.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-21 10:54:51 -03:00
parent 4024469952
commit ae1e1388b9
+118 -3
View File
@@ -103,6 +103,11 @@ const form = reactive({
bio: '',
phone: '',
// Registro profissional (CFP #5 — exigido pra emissão de recibos/laudos)
professional_registration_type: '',
professional_registration_number: '',
professional_registration_uf: '',
site_url: '',
social_instagram: '',
social_youtube: '',
@@ -117,6 +122,24 @@ const form = reactive({
notify_news: false
});
// Opções do CHECK constraint da migration 20260521000003
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 }));
const customSocials = ref([]);
function addCustomSocial() {
@@ -611,7 +634,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, language, timezone, notify_system_email, notify_reminders, notify_news'
'role, full_name, avatar_url, phone, bio, nickname, work_description, work_description_other, site_url, social_instagram, social_youtube, social_facebook, social_x, social_custom, language, timezone, notify_system_email, notify_reminders, notify_news, professional_registration_type, professional_registration_number, professional_registration_uf'
)
.eq('id', user.id)
.maybeSingle();
@@ -631,6 +654,10 @@ async function loadProfile() {
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 ?? '';
if (Array.isArray(prof.social_custom)) {
customSocials.value = prof.social_custom;
}
@@ -707,7 +734,12 @@ async function saveAll() {
notify_system_email: !!form.notify_system_email,
notify_reminders: !!form.notify_reminders,
notify_news: !!form.notify_news
notify_news: !!form.notify_news,
// 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
@@ -715,7 +747,7 @@ async function saveAll() {
.update(profilePayload)
.eq('id', userId.value)
.select(
'id, role, full_name, avatar_url, phone, bio, nickname, work_description, work_description_other, site_url, social_instagram, social_youtube, social_facebook, social_x, social_custom, language, timezone, notify_system_email, notify_reminders, notify_news, updated_at'
'id, role, full_name, avatar_url, phone, bio, nickname, work_description, work_description_other, site_url, social_instagram, social_youtube, social_facebook, social_x, social_custom, language, timezone, notify_system_email, notify_reminders, notify_news, professional_registration_type, professional_registration_number, professional_registration_uf, updated_at'
)
.single();
@@ -1105,6 +1137,89 @@ onBeforeUnmount(() => {
</div>
</div>
<!-- REGISTRO PROFISSIONAL (CFP #5) -->
<div
id="registro-profissional"
class="pcard relative overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] p-6 scroll-mt-20 transition-[border-color] duration-[220ms]"
style="--c: #0ea5e9; --c-dim: rgba(14, 165, 233, 0.08); --c-border: rgba(14, 165, 233, 0.2)"
>
<div class="pcard__shine" />
<div class="flex items-center gap-2.5 mb-3.5">
<div class="w-9 h-9 rounded-[0.625rem] shrink-0 flex items-center justify-center" style="background: var(--c-dim); border: 1px solid var(--c-border); color: var(--c)"><i class="pi pi-id-card" /></div>
<span class="text-[1rem] font-extrabold uppercase tracking-[0.14em] opacity-75" style="color: var(--c)">Registro Profissional</span>
</div>
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)] mb-1">Conselho regional</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">Exigido para emissão de recibos, atestados e laudos. Aparecerá no rodapé dos documentos.</div>
<div class="h-px bg-[var(--surface-border)] my-5" />
<div class="grid grid-cols-12 gap-4">
<!-- Tipo de conselho -->
<div class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<Select
id="prof_registration_type"
v-model="form.professional_registration_type"
:options="REGISTRATION_TYPE_OPTIONS"
optionLabel="label"
optionValue="value"
class="w-full"
@update:modelValue="markDirty"
/>
<label for="prof_registration_type">Tipo de registro</label>
</FloatLabel>
<div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Conselho profissional ao qual você é vinculado.</div>
</div>
<!-- Número -->
<div class="col-span-7 md:col-span-3">
<FloatLabel variant="on">
<InputText
id="prof_registration_number"
v-model="form.professional_registration_number"
class="w-full"
:disabled="!form.professional_registration_type"
@input="markDirty"
/>
<label for="prof_registration_number">Número</label>
</FloatLabel>
<div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Ex: 06/12345</div>
</div>
<!-- UF -->
<div class="col-span-5 md:col-span-3">
<FloatLabel variant="on">
<Select
id="prof_registration_uf"
v-model="form.professional_registration_uf"
:options="UF_OPTIONS"
optionLabel="label"
optionValue="value"
:disabled="!form.professional_registration_type"
class="w-full"
:filter="true"
@update:modelValue="markDirty"
/>
<label for="prof_registration_uf">UF</label>
</FloatLabel>
<div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Estado do conselho.</div>
</div>
<!-- Preview -->
<div v-if="form.professional_registration_type && form.professional_registration_number" class="col-span-12">
<div class="rounded-md border border-[var(--c-border)] bg-[var(--c-dim)] p-3 text-[0.9rem]">
<span class="text-[var(--text-color-secondary)] mr-2">Aparecerá nos documentos como:</span>
<strong class="text-[var(--text-color)]">
{{ form.professional_registration_type }}
{{ form.professional_registration_number }}{{ form.professional_registration_uf ? '/' + form.professional_registration_uf : '' }}
</strong>
</div>
</div>
</div>
</div>
<!-- 02 SITES E REDES SOCIAIS -->
<div
id="redes-sociais"