Documentos Pacientes, Template Documentos Pacientes Saas, Documentos prontuários, Documentos Externos, Visualização Externa, Permissão de Visualização, Render Otimização
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/services/Medicos.service.js
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
|
||||
async function getOwnerId() {
|
||||
const { data, error } = await supabase.auth.getUser();
|
||||
if (error) throw error;
|
||||
const uid = data?.user?.id;
|
||||
if (!uid) throw new Error('Sessão inválida.');
|
||||
return uid;
|
||||
}
|
||||
|
||||
async function getActiveTenantId(uid) {
|
||||
const { data, error } = await supabase
|
||||
.from('tenant_members')
|
||||
.select('tenant_id')
|
||||
.eq('user_id', uid)
|
||||
.eq('status', 'active')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(1)
|
||||
.single();
|
||||
if (error) throw error;
|
||||
if (!data?.tenant_id) throw new Error('Tenant não encontrado.');
|
||||
return data.tenant_id;
|
||||
}
|
||||
|
||||
function normalizeNome(s) {
|
||||
return String(s || '').trim().toLowerCase().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
function isUniqueViolation(err) {
|
||||
if (!err) return false;
|
||||
if (err.code === '23505') return true;
|
||||
return /duplicate key value violates unique constraint/i.test(String(err.message || ''));
|
||||
}
|
||||
|
||||
// ── List ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Lista médicos ativos do owner com contagem de pacientes encaminhados.
|
||||
* A contagem é feita buscando quantos patients possuem o nome do médico
|
||||
* no campo `encaminhado_por` (text).
|
||||
*/
|
||||
export async function listMedicosWithPatientCounts() {
|
||||
const ownerId = await getOwnerId();
|
||||
|
||||
const { data: medicos, error } = await supabase
|
||||
.from('medicos')
|
||||
.select('id, nome, crm, especialidade, telefone_profissional, telefone_pessoal, email, clinica, cidade, estado, observacoes, ativo, owner_id, tenant_id, created_at, updated_at')
|
||||
.eq('owner_id', ownerId)
|
||||
.eq('ativo', true)
|
||||
.order('nome', { ascending: true });
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Busca pacientes do owner para contar encaminhamentos por médico
|
||||
const { data: patients, error: pErr } = await supabase
|
||||
.from('patients')
|
||||
.select('id, encaminhado_por')
|
||||
.eq('owner_id', ownerId);
|
||||
|
||||
if (pErr) throw pErr;
|
||||
|
||||
const countMap = new Map();
|
||||
for (const med of medicos || []) {
|
||||
countMap.set(med.id, 0);
|
||||
}
|
||||
|
||||
for (const p of patients || []) {
|
||||
const enc = String(p.encaminhado_por || '').toLowerCase();
|
||||
if (!enc) continue;
|
||||
for (const med of medicos || []) {
|
||||
const nomeLower = med.nome.toLowerCase();
|
||||
if (enc.includes(nomeLower)) {
|
||||
countMap.set(med.id, (countMap.get(med.id) || 0) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (medicos || []).map((m) => ({
|
||||
...m,
|
||||
patients_count: countMap.get(m.id) || 0
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Create ───────────────────────────────────────────────────
|
||||
|
||||
export async function createMedico(payload) {
|
||||
const ownerId = await getOwnerId();
|
||||
const tenantId = await getActiveTenantId(ownerId);
|
||||
|
||||
const nome = String(payload.nome || '').trim();
|
||||
if (!nome) throw new Error('Nome do médico é obrigatório.');
|
||||
|
||||
const row = {
|
||||
owner_id: ownerId,
|
||||
tenant_id: tenantId,
|
||||
nome,
|
||||
crm: String(payload.crm || '').trim() || null,
|
||||
especialidade: payload.especialidade || null,
|
||||
telefone_profissional: payload.telefone_profissional || null,
|
||||
telefone_pessoal: payload.telefone_pessoal || null,
|
||||
email: String(payload.email || '').trim() || null,
|
||||
clinica: String(payload.clinica || '').trim() || null,
|
||||
cidade: String(payload.cidade || '').trim() || null,
|
||||
estado: String(payload.estado || '').trim() || null,
|
||||
observacoes: String(payload.observacoes || '').trim() || null,
|
||||
ativo: true
|
||||
};
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('medicos')
|
||||
.insert(row)
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (isUniqueViolation(error)) throw new Error('Já existe um médico com este CRM.');
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Update ───────────────────────────────────────────────────
|
||||
|
||||
export async function updateMedico(id, payload) {
|
||||
const ownerId = await getOwnerId();
|
||||
|
||||
if (!id) throw new Error('ID inválido.');
|
||||
const nome = String(payload.nome || '').trim();
|
||||
if (!nome) throw new Error('Nome do médico é obrigatório.');
|
||||
|
||||
const row = {
|
||||
nome,
|
||||
crm: String(payload.crm || '').trim() || null,
|
||||
especialidade: payload.especialidade || null,
|
||||
telefone_profissional: payload.telefone_profissional || null,
|
||||
telefone_pessoal: payload.telefone_pessoal || null,
|
||||
email: String(payload.email || '').trim() || null,
|
||||
clinica: String(payload.clinica || '').trim() || null,
|
||||
cidade: String(payload.cidade || '').trim() || null,
|
||||
estado: String(payload.estado || '').trim() || null,
|
||||
observacoes: String(payload.observacoes || '').trim() || null,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('medicos')
|
||||
.update(row)
|
||||
.eq('id', id)
|
||||
.eq('owner_id', ownerId)
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
if (isUniqueViolation(error)) throw new Error('Já existe um médico com este CRM.');
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Delete (soft) ────────────────────────────────────────────
|
||||
|
||||
export async function deleteMedico(id) {
|
||||
const ownerId = await getOwnerId();
|
||||
if (!id) throw new Error('ID inválido.');
|
||||
|
||||
const { error } = await supabase
|
||||
.from('medicos')
|
||||
.update({ ativo: false, updated_at: new Date().toISOString() })
|
||||
.eq('id', id)
|
||||
.eq('owner_id', ownerId);
|
||||
|
||||
if (error) throw error;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Pacientes de um médico ───────────────────────────────────
|
||||
|
||||
/**
|
||||
* Busca pacientes do owner cujo campo `encaminhado_por` contém o nome do médico.
|
||||
*/
|
||||
export async function fetchPatientsByMedicoNome(medicoNome) {
|
||||
const ownerId = await getOwnerId();
|
||||
const nomeLower = String(medicoNome || '').trim().toLowerCase();
|
||||
if (!nomeLower) return [];
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('patients')
|
||||
.select('id, nome_completo, email_principal, telefone, avatar_url, encaminhado_por')
|
||||
.eq('owner_id', ownerId)
|
||||
.ilike('encaminhado_por', `%${nomeLower}%`);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return (data || [])
|
||||
.map((p) => ({
|
||||
id: p.id,
|
||||
full_name: p.nome_completo || '—',
|
||||
email: p.email_principal || '—',
|
||||
phone: p.telefone || '—',
|
||||
avatar_url: p.avatar_url || null,
|
||||
encaminhado_por: p.encaminhado_por || ''
|
||||
}))
|
||||
.sort((a, b) => String(a.full_name).localeCompare(String(b.full_name), 'pt-BR'));
|
||||
}
|
||||
Reference in New Issue
Block a user