registro profissional: campo livre quando tipo='outro'

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>
This commit is contained in:
Leonardo
2026-05-21 11:26:21 -03:00
parent 6a8ee52ad8
commit dee89ccd84
4 changed files with 78 additions and 8 deletions
@@ -0,0 +1,21 @@
-- ============================================================================
-- Compliance CFP #5 — campo livre quando tipo de registro = 'outro'
-- ----------------------------------------------------------------------------
-- Migration 20260521000003 adicionou professional_registration_type com CHECK
-- limitado a 8 valores (CRP/CRM/CRFa/CREFITO/CRESS/CRN/RMS/outro). Quando o
-- profissional escolhe 'outro', precisa informar qual conselho/instituição
-- (ex: associações privadas, conselhos não-listados).
--
-- Esta migration adiciona professional_registration_type_other (text livre),
-- que só é preenchido quando type = 'outro'.
-- ============================================================================
BEGIN;
ALTER TABLE public.profiles
ADD COLUMN IF NOT EXISTS professional_registration_type_other text;
COMMENT ON COLUMN public.profiles.professional_registration_type_other IS
'Nome livre do conselho/instituição quando professional_registration_type = ''outro''. Aparece em recibos/laudos no lugar do tipo padrão.';
COMMIT;
+25 -2
View File
@@ -160,6 +160,7 @@ const form = reactive({
social_x: '', social_x: '',
// Registro profissional (CFP #5 — exigido pra emissão de recibos/laudos) // Registro profissional (CFP #5 — exigido pra emissão de recibos/laudos)
professional_registration_type: '', professional_registration_type: '',
professional_registration_type_other: '',
professional_registration_number: '', professional_registration_number: '',
professional_registration_uf: '' professional_registration_uf: ''
}); });
@@ -372,7 +373,7 @@ async function loadProfile() {
const { data: prof, error: pErr } = await supabase const { data: prof, error: pErr } = await supabase
.from('profiles') .from('profiles')
.select( .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_number, professional_registration_uf' '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) .eq('id', user.id)
.maybeSingle(); .maybeSingle();
@@ -392,6 +393,7 @@ async function loadProfile() {
form.social_facebook = prof.social_facebook ?? ''; form.social_facebook = prof.social_facebook ?? '';
form.social_x = prof.social_x ?? ''; form.social_x = prof.social_x ?? '';
form.professional_registration_type = prof.professional_registration_type ?? ''; 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_number = prof.professional_registration_number ?? '';
form.professional_registration_uf = prof.professional_registration_uf ?? ''; form.professional_registration_uf = prof.professional_registration_uf ?? '';
customSocials.value = Array.isArray(prof.social_custom) ? prof.social_custom : []; customSocials.value = Array.isArray(prof.social_custom) ? prof.social_custom : [];
@@ -463,6 +465,11 @@ async function saveAll() {
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 // Registro profissional (CFP) — null se vazio
professional_registration_type: String(form.professional_registration_type || '').trim() || null, 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_number: String(form.professional_registration_number || '').trim() || null,
professional_registration_uf: String(form.professional_registration_uf || '').trim() || null professional_registration_uf: String(form.professional_registration_uf || '').trim() || null
}; };
@@ -894,6 +901,20 @@ onBeforeUnmount(() => {
<small class="mpr-hint">Conselho profissional ao qual você é vinculado.</small> <small class="mpr-hint">Conselho profissional ao qual você é vinculado.</small>
</div> </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"> <div class="mpr-field mpr-field--half">
<FloatLabel variant="on"> <FloatLabel variant="on">
<InputText <InputText
@@ -930,7 +951,9 @@ onBeforeUnmount(() => {
<div class="mpr-preview-box"> <div class="mpr-preview-box">
<span class="mpr-preview-label">Aparecerá nos documentos como:</span> <span class="mpr-preview-label">Aparecerá nos documentos como:</span>
<strong class="mpr-preview-value"> <strong class="mpr-preview-value">
{{ form.professional_registration_type }} {{ 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 : '' }} {{ form.professional_registration_number }}{{ form.professional_registration_uf ? '/' + form.professional_registration_uf : '' }}
</strong> </strong>
</div> </div>
+6 -3
View File
@@ -148,7 +148,7 @@ export async function loadTherapistData() {
const { data: profile } = await supabase const { data: profile } = await supabase
.from('profiles') .from('profiles')
.select('full_name, phone, professional_registration_type, professional_registration_number, professional_registration_uf') .select('full_name, phone, professional_registration_type, professional_registration_type_other, professional_registration_number, professional_registration_uf')
.eq('id', ownerId) .eq('id', ownerId)
.single(); .single();
@@ -156,7 +156,10 @@ export async function loadTherapistData() {
const { data: userData } = await supabase.auth.getUser(); const { data: userData } = await supabase.auth.getUser();
const email = userData?.user?.email || ''; const email = userData?.user?.email || '';
const tipo = profile?.professional_registration_type || ''; const tipoRaw = profile?.professional_registration_type || '';
const tipoOther = profile?.professional_registration_type_other || '';
// Quando type='outro', usa o nome livre do conselho/instituição
const tipo = tipoRaw === 'outro' ? tipoOther : tipoRaw;
const numero = profile?.professional_registration_number || ''; const numero = profile?.professional_registration_number || '';
const uf = profile?.professional_registration_uf || ''; const uf = profile?.professional_registration_uf || '';
const registro = formatRegistroProfissional({ tipo, numero, uf }); const registro = formatRegistroProfissional({ tipo, numero, uf });
@@ -173,7 +176,7 @@ export async function loadTherapistData() {
// o número/UF (sem prefixo) pra não duplicar com o "CRP" já no HTML. // o número/UF (sem prefixo) pra não duplicar com o "CRP" já no HTML.
// Quando o registro não é CRP, retorna vazio (template visualmente errado // Quando o registro não é CRP, retorna vazio (template visualmente errado
// pede pra usar {{terapeuta_registro}}). // pede pra usar {{terapeuta_registro}}).
terapeuta_crp: tipo === 'CRP' ? (uf ? `${numero}/${uf}` : numero) : '' terapeuta_crp: tipoRaw === 'CRP' ? (uf ? `${numero}/${uf}` : numero) : ''
}; };
} }
+26 -3
View File
@@ -105,6 +105,7 @@ const form = reactive({
// Registro profissional (CFP #5 — exigido pra emissão de recibos/laudos) // Registro profissional (CFP #5 — exigido pra emissão de recibos/laudos)
professional_registration_type: '', professional_registration_type: '',
professional_registration_type_other: '',
professional_registration_number: '', professional_registration_number: '',
professional_registration_uf: '', professional_registration_uf: '',
@@ -634,7 +635,7 @@ async function loadProfile() {
const { data: prof, error: pErr } = await supabase const { data: prof, error: pErr } = await supabase
.from('profiles') .from('profiles')
.select( .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, professional_registration_type, professional_registration_number, professional_registration_uf' '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_type_other, professional_registration_number, professional_registration_uf'
) )
.eq('id', user.id) .eq('id', user.id)
.maybeSingle(); .maybeSingle();
@@ -655,6 +656,7 @@ async function loadProfile() {
form.social_x = prof.social_x ?? ''; form.social_x = prof.social_x ?? '';
form.professional_registration_type = prof.professional_registration_type ?? ''; 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_number = prof.professional_registration_number ?? '';
form.professional_registration_uf = prof.professional_registration_uf ?? ''; form.professional_registration_uf = prof.professional_registration_uf ?? '';
@@ -738,6 +740,11 @@ async function saveAll() {
// Registro profissional (CFP) — null se vazio // Registro profissional (CFP) — null se vazio
professional_registration_type: String(form.professional_registration_type || '').trim() || null, 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_number: String(form.professional_registration_number || '').trim() || null,
professional_registration_uf: String(form.professional_registration_uf || '').trim() || null professional_registration_uf: String(form.professional_registration_uf || '').trim() || null
}; };
@@ -747,7 +754,7 @@ async function saveAll() {
.update(profilePayload) .update(profilePayload)
.eq('id', userId.value) .eq('id', userId.value)
.select( .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, professional_registration_type, professional_registration_number, professional_registration_uf, 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_type_other, professional_registration_number, professional_registration_uf, updated_at'
) )
.single(); .single();
@@ -1173,6 +1180,20 @@ onBeforeUnmount(() => {
<div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Conselho profissional ao qual você é vinculado.</div> <div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Conselho profissional ao qual você é vinculado.</div>
</div> </div>
<!-- Campo livre quando tipo='outro' -->
<div v-if="form.professional_registration_type === 'outro'" class="col-span-12 md:col-span-6">
<FloatLabel variant="on">
<InputText
id="prof_registration_type_other"
v-model="form.professional_registration_type_other"
class="w-full"
@input="markDirty"
/>
<label for="prof_registration_type_other">Nome do conselho/instituição <span class="text-red-400">*</span></label>
</FloatLabel>
<div class="mt-1.5 text-[1rem] text-[var(--text-color-secondary)]">Ex: APM (Associação Paulista de Medicina), ABRAP, etc.</div>
</div>
<!-- Número --> <!-- Número -->
<div class="col-span-7 md:col-span-3"> <div class="col-span-7 md:col-span-3">
<FloatLabel variant="on"> <FloatLabel variant="on">
@@ -1212,7 +1233,9 @@ onBeforeUnmount(() => {
<div class="rounded-md border border-[var(--c-border)] bg-[var(--c-dim)] p-3 text-[0.9rem]"> <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> <span class="text-[var(--text-color-secondary)] mr-2">Aparecerá nos documentos como:</span>
<strong class="text-[var(--text-color)]"> <strong class="text-[var(--text-color)]">
{{ form.professional_registration_type }} {{ 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 : '' }} {{ form.professional_registration_number }}{{ form.professional_registration_uf ? '/' + form.professional_registration_uf : '' }}
</strong> </strong>
</div> </div>