compliance CFP: #5 registro profissional + #9 especialidades

ROADMAP Fase 1.2 (Compliance basico BR). Item #5: profiles ganha
3 colunas (professional_registration_type/number/uf) com CHECK
constraint dos conselhos comuns (CRP, CRM, CRFa, CREFITO, CRESS,
CRN, RMS, outro). Item #9: catalogo public.specialties + join
M:N profile_specialties + RLS. Seed seed_050 popula 33
especialidades is_system=true (clinica, jurídica, neuropsicologia,
ABA, TCC, psicanalise etc). Service specialtiesService.js no
src/services pra consumo na UI.

Item #8 (nome social) ja estava integrado. #6 (consent forms UI)
e #7 (assinatura no portal) adiados — schemas document_templates
e document_signatures existem, falta workflow UI dedicado.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-21 04:21:03 -03:00
parent de3898878a
commit cd67f7e9f5
4 changed files with 319 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
/*
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Arquivo: src/services/specialtiesService.js
|
| Service pra catálogo de especialidades + manage as escolhidas do profile.
| ROADMAP item #9 (Compliance CFP).
|
| Schema: ver migrations/20260521000004_specialties.sql + seed_050.
|--------------------------------------------------------------------------
*/
import { supabase } from '@/lib/supabase/client';
const SPECIALTY_SELECT = 'id, key, name, category, is_system, active';
const PROFILE_SPECIALTY_SELECT = 'profile_id, specialty_id, other_label, created_at';
/**
* Lista catálogo de especialidades ativas. Ordem: category, name.
*
* @param {Object} [opts]
* @param {string} [opts.category] - filtra por categoria (psicologia, abordagem, publico, tema, outro)
*/
export async function listSpecialties({ category } = {}) {
let q = supabase.from('specialties').select(SPECIALTY_SELECT).eq('active', true).order('category', { ascending: true }).order('name', { ascending: true });
if (category) q = q.eq('category', category);
const { data, error } = await q;
if (error) throw error;
return data || [];
}
/**
* Lê especialidades do profile passado (default: user logado via auth.uid()).
*
* @param {string} [profileId]
*/
export async function getProfileSpecialties(profileId = null) {
let pid = profileId;
if (!pid) {
const { data: userData } = await supabase.auth.getUser();
pid = userData?.user?.id;
if (!pid) throw new Error('Sessão inválida.');
}
const { data, error } = await supabase
.from('profile_specialties')
.select(`${PROFILE_SPECIALTY_SELECT}, specialty:specialties (${SPECIALTY_SELECT})`)
.eq('profile_id', pid);
if (error) throw error;
return (data || []).map((r) => ({
...r,
// flatten — UI espera campos do specialty direto
key: r.specialty?.key,
name: r.specialty?.name,
category: r.specialty?.category
}));
}
/**
* Substitui completamente as especialidades do user (delete + insert pattern).
* Other label preenchido se key === 'outra'.
*
* @param {Array<{specialty_id, other_label?}>} specialties
* @param {string} [profileId]
*/
export async function setProfileSpecialties(specialties, profileId = null) {
let pid = profileId;
if (!pid) {
const { data: userData } = await supabase.auth.getUser();
pid = userData?.user?.id;
if (!pid) throw new Error('Sessão inválida.');
}
// 1. Delete existentes
const { error: delErr } = await supabase.from('profile_specialties').delete().eq('profile_id', pid);
if (delErr) throw delErr;
// 2. Insert novos (skip se array vazio)
if (!specialties?.length) return [];
const rows = specialties.map((s) => ({
profile_id: pid,
specialty_id: s.specialty_id,
other_label: s.other_label ? String(s.other_label).trim() || null : null
}));
const { data, error } = await supabase.from('profile_specialties').insert(rows).select(PROFILE_SPECIALTY_SELECT);
if (error) throw error;
return data || [];
}
/**
* Lista profiles que têm uma especialidade específica (perfil público / busca).
* Use só pra contextos onde tenant_admin/saas tem permissão de ver outros profiles.
*
* @param {string} specialtyKey - ex: 'tcc', 'psicanalise'
*/
export async function listProfilesBySpecialty(specialtyKey) {
if (!specialtyKey) return [];
// 1. Pega specialty.id pela key
const { data: spec } = await supabase.from('specialties').select('id').eq('key', specialtyKey).eq('active', true).maybeSingle();
if (!spec) return [];
// 2. Lê profile_specialties + join profiles
const { data, error } = await supabase.from('profile_specialties').select('profile_id, profiles!profile_id (id, full_name, avatar_url, bio, professional_registration_type, professional_registration_number, professional_registration_uf)').eq('specialty_id', spec.id);
if (error) throw error;
return (data || []).map((r) => r.profiles).filter(Boolean);
}