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