develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
9 changed files with 255 additions and 31 deletions
Showing only changes of commit 1a471357b7 - Show all commits

View File

@ -7,12 +7,13 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye } from "lucide-react";
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye, Users } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form";
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, Medico } from "@/lib/api";
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, buscarPacientesPorIds, Medico } from "@/lib/api";
import { listAssignmentsForUser } from '@/lib/assignment';
function normalizeMedico(m: any): Medico {
return {
@ -64,6 +65,10 @@ export default function DoutoresPage() {
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [viewingDoctor, setViewingDoctor] = useState<Medico | null>(null);
const [assignedDialogOpen, setAssignedDialogOpen] = useState(false);
const [assignedPatients, setAssignedPatients] = useState<any[]>([]);
const [assignedLoading, setAssignedLoading] = useState(false);
const [assignedDoctor, setAssignedDoctor] = useState<Medico | null>(null);
const [searchResults, setSearchResults] = useState<Medico[]>([]);
const [searchMode, setSearchMode] = useState(false);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
@ -179,7 +184,7 @@ export default function DoutoresPage() {
// Handler para o botão de busca
function handleClickBuscar() {
handleBuscarServidor();
void handleBuscarServidor();
}
useEffect(() => {
@ -253,6 +258,28 @@ export default function DoutoresPage() {
setViewingDoctor(doctor);
}
async function handleViewAssignedPatients(doctor: Medico) {
setAssignedDoctor(doctor);
setAssignedLoading(true);
setAssignedPatients([]);
try {
const assigns = await listAssignmentsForUser(String(doctor.user_id ?? doctor.id));
const patientIds = Array.isArray(assigns) ? assigns.map((a:any) => String(a.patient_id)).filter(Boolean) : [];
if (patientIds.length) {
const patients = await buscarPacientesPorIds(patientIds);
setAssignedPatients(patients || []);
} else {
setAssignedPatients([]);
}
} catch (e) {
console.warn('[DoutoresPage] erro ao carregar pacientes atribuídos:', e);
setAssignedPatients([]);
} finally {
setAssignedLoading(false);
setAssignedDialogOpen(true);
}
}
async function handleDelete(id: string) {
if (!confirm("Excluir este médico?")) return;
@ -326,9 +353,9 @@ export default function DoutoresPage() {
onKeyDown={handleSearchKeyDown}
/>
</div>
<Button
<Button
variant="secondary"
onClick={handleBuscarServidor}
onClick={() => void handleBuscarServidor()}
disabled={loading}
className="hover:bg-primary hover:text-white"
>
@ -394,11 +421,18 @@ export default function DoutoresPage() {
<span className="sr-only">Abrir menu</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleView(doctor)}>
<Eye className="mr-2 h-4 w-4" />
Ver
</DropdownMenuItem>
{/* Ver pacientes atribuídos ao médico */}
<DropdownMenuItem onClick={() => handleViewAssignedPatients(doctor)}>
<Users className="mr-2 h-4 w-4" />
Ver pacientes atribuídos
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
<Edit className="mr-2 h-4 w-4" />
Editar
@ -466,6 +500,36 @@ export default function DoutoresPage() {
<div className="text-sm text-muted-foreground">
Mostrando {displayedDoctors.length} {searchMode ? 'resultado(s) da busca' : `de ${doctors.length}`}
</div>
{/* Dialog para pacientes atribuídos */}
<Dialog open={assignedDialogOpen} onOpenChange={(open) => { if (!open) { setAssignedDialogOpen(false); setAssignedPatients([]); setAssignedDoctor(null); } }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Pacientes atribuídos{assignedDoctor ? ` - ${assignedDoctor.full_name}` : ''}</DialogTitle>
<DialogDescription>
Lista de pacientes atribuídos a este médico.
</DialogDescription>
</DialogHeader>
<div className="py-4">
{assignedLoading ? (
<div>Carregando pacientes...</div>
) : assignedPatients && assignedPatients.length ? (
<div className="space-y-2">
{assignedPatients.map((p:any) => (
<div key={p.id} className="p-2 border rounded">
<div className="font-medium">{p.full_name ?? p.nome ?? p.name ?? '(sem nome)'}</div>
<div className="text-xs text-muted-foreground">ID: {p.id} {p.cpf ? `• CPF: ${p.cpf}` : ''}</div>
</div>
))}
</div>
) : (
<div>Nenhum paciente atribuído encontrado.</div>
)}
</div>
<DialogFooter>
<Button onClick={() => setAssignedDialogOpen(false)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View File

@ -207,7 +207,7 @@ export default function PacientesPage() {
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
/>
</div>
<Button variant="secondary" onClick={handleBuscarServidor} className="hover:bg-primary hover:text-white">Buscar</Button>
<Button variant="secondary" onClick={() => void handleBuscarServidor()} className="hover:bg-primary hover:text-white">Buscar</Button>
<Button onClick={handleAdd}>
<Plus className="mr-2 h-4 w-4" />
Novo paciente

View File

@ -751,19 +751,37 @@ const ProfissionalPage = () => {
return;
}
// carregar relatórios para cada paciente encontrado (useReports não tem batch by multiple ids, então carregamos por paciente)
const allReports: any[] = [];
for (const pid of patientIds) {
try {
const rels = await import('@/lib/reports').then(m => m.listarRelatoriosPorPaciente(pid));
if (Array.isArray(rels)) allReports.push(...rels);
} catch (err) {
console.warn('[LaudoManager] falha ao carregar relatórios para paciente', pid, err);
// Tentar carregar todos os relatórios em uma única chamada usando in.(...)
try {
const reportsMod = await import('@/lib/reports');
if (typeof reportsMod.listarRelatoriosPorPacientes === 'function') {
const batch = await reportsMod.listarRelatoriosPorPacientes(patientIds);
if (mounted) setLaudos(batch || []);
} else {
// fallback: 请求 por paciente individual
const allReports: any[] = [];
for (const pid of patientIds) {
try {
const rels = await import('@/lib/reports').then(m => m.listarRelatoriosPorPaciente(pid));
if (Array.isArray(rels)) allReports.push(...rels);
} catch (err) {
console.warn('[LaudoManager] falha ao carregar relatórios para paciente', pid, err);
}
}
if (mounted) setLaudos(allReports);
}
}
if (mounted) {
setLaudos(allReports);
} catch (err) {
console.warn('[LaudoManager] erro ao carregar relatórios em batch, tentando por paciente individual', err);
const allReports: any[] = [];
for (const pid of patientIds) {
try {
const rels = await import('@/lib/reports').then(m => m.listarRelatoriosPorPaciente(pid));
if (Array.isArray(rels)) allReports.push(...rels);
} catch (e) {
console.warn('[LaudoManager] falha ao carregar relatórios para paciente', pid, e);
}
}
if (mounted) setLaudos(allReports);
}
} catch (e) {
console.warn('[LaudoManager] erro ao carregar laudos para pacientes atribuídos:', e);

View File

@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { useToast } from "@/hooks/use-toast";
import { assignRoleToUser, listAssignmentsForPatient, PatientAssignmentRole } from "@/lib/assignment";
import { useAuth } from '@/hooks/useAuth';
import { listarProfissionais } from "@/lib/api";
type Props = {
@ -20,6 +21,7 @@ type Props = {
export default function AssignmentForm({ patientId, open, onClose, onSaved }: Props) {
const { toast } = useToast();
const { user } = useAuth();
const [professionals, setProfessionals] = useState<any[]>([]);
const [selectedProfessional, setSelectedProfessional] = useState<string | null>(null);
// default to Portuguese role values expected by the backend
@ -52,8 +54,8 @@ export default function AssignmentForm({ patientId, open, onClose, onSaved }: Pr
if (!selectedProfessional) return toast({ title: 'Selecione um profissional', variant: 'default' });
setLoading(true);
try {
await assignRoleToUser({ patient_id: patientId, user_id: selectedProfessional, role });
toast({ title: 'Atribuição criada', variant: 'default' });
await assignRoleToUser({ patient_id: patientId, user_id: String(selectedProfessional), role, created_by: user?.id ?? null });
toast({ title: 'Atribuição criada', variant: 'default' });
onSaved && onSaved();
onClose();
} catch (err: any) {
@ -80,7 +82,8 @@ export default function AssignmentForm({ patientId, open, onClose, onSaved }: Pr
</SelectTrigger>
<SelectContent>
{professionals.map((p) => (
<SelectItem key={p.id} value={String(p.id)}>{p.full_name || p.name || p.email || p.id}</SelectItem>
// prefer the auth user id (p.user_id) when available; fallback to p.id
<SelectItem key={p.id} value={String(p.user_id ?? p.id)}>{p.full_name || p.name || p.email || p.user_id || p.id}</SelectItem>
))}
</SelectContent>
</Select>

View File

@ -173,6 +173,10 @@ export function DoctorRegistrationForm({
console.log("[DoctorForm] Carregando médico ID:", doctorId);
const medico = await buscarMedicoPorId(String(doctorId));
console.log("[DoctorForm] Dados recebidos do API:", medico);
if (!medico) {
console.warn('[DoctorForm] Médico não encontrado para ID:', doctorId);
return;
}
console.log("[DoctorForm] Campos principais:", {
full_name: medico.full_name,
crm: medico.crm,
@ -411,16 +415,35 @@ async function handleSubmit(ev: React.FormEvent) {
// 2. Cria usuário no Supabase Auth (direto via /auth/v1/signup)
console.log('🔐 Criando usuário de autenticação...');
try {
try {
const authResponse = await criarUsuarioMedico({
email: form.email,
full_name: form.full_name,
phone_mobile: form.celular || '',
});
if (authResponse.success && authResponse.user) {
console.log('✅ Usuário Auth criado:', authResponse.user.id);
// Attempt to link the created auth user id to the doctors record
try {
// savedDoctorProfile may be an array or object depending on API
const docId = (savedDoctorProfile && (savedDoctorProfile.id || (Array.isArray(savedDoctorProfile) ? savedDoctorProfile[0]?.id : undefined))) || null;
if (docId) {
console.log('[DoctorForm] Vinculando user_id ao médico:', { doctorId: docId, userId: authResponse.user.id });
// dynamic import to avoid circular deps in some bundlers
const api = await import('@/lib/api');
if (api && typeof api.vincularUserIdMedico === 'function') {
await api.vincularUserIdMedico(String(docId), String(authResponse.user.id));
console.log('[DoctorForm] user_id vinculado com sucesso.');
}
} else {
console.warn('[DoctorForm] Não foi possível determinar o ID do médico para vincular user_id. Doctor profile:', savedDoctorProfile);
}
} catch (linkErr) {
console.warn('[DoctorForm] Falha ao vincular user_id ao médico:', linkErr);
}
// 3. Exibe popup com credenciais
setCredentials({
email: authResponse.email,
@ -429,18 +452,18 @@ async function handleSubmit(ev: React.FormEvent) {
userType: 'médico',
});
setShowCredentialsDialog(true);
// 4. Limpa formulário
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
// 5. Notifica componente pai
onSaved?.(savedDoctorProfile);
} else {
throw new Error('Falha ao criar usuário de autenticação');
}
} catch (authError: any) {
console.error('❌ Erro ao criar usuário Auth:', authError);

View File

@ -296,6 +296,24 @@ export function PatientRegistrationForm({
});
setShowCredentialsDialog(true);
// Tenta vincular o user_id ao perfil do paciente recém-criado
try {
const apiMod = await import('@/lib/api');
const pacienteId = savedPatientProfile?.id || (savedPatientProfile && (savedPatientProfile as any).id);
const userId = (userResponse.user as any)?.id || (userResponse.user as any)?.user_id || (userResponse.user as any)?.id;
if (pacienteId && userId && typeof apiMod.vincularUserIdPaciente === 'function') {
console.log('[PatientForm] Vinculando user_id ao paciente:', pacienteId, userId);
try {
await apiMod.vincularUserIdPaciente(pacienteId, String(userId));
console.log('[PatientForm] user_id vinculado com sucesso ao paciente');
} catch (linkErr) {
console.warn('[PatientForm] Falha ao vincular user_id ao paciente:', linkErr);
}
}
} catch (dynErr) {
console.warn('[PatientForm] Não foi possível importar helper para vincular user_id:', dynErr);
}
// Limpa formulário mas NÃO fecha ainda - fechará quando o dialog de credenciais fechar
setForm(initial);
setPhotoPreview(null);

View File

@ -859,6 +859,39 @@ export async function criarMedico(input: MedicoInput): Promise<Medico> {
return Array.isArray(arr) ? arr[0] : (arr as Medico); // Retorno do médico
}
/**
* Vincula um user_id (auth user id) a um registro de médico existente.
* Retorna o médico atualizado.
*/
export async function vincularUserIdMedico(medicoId: string | number, userId: string): Promise<Medico> {
const url = `${REST}/doctors?id=eq.${encodeURIComponent(String(medicoId))}`;
const payload = { user_id: String(userId) };
const res = await fetch(url, {
method: 'PATCH',
headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'),
body: JSON.stringify(payload),
});
const arr = await parse<Medico[] | Medico>(res);
return Array.isArray(arr) ? arr[0] : (arr as Medico);
}
/**
* Vincula um user_id (auth user id) a um registro de paciente existente.
* Retorna o paciente atualizado.
*/
export async function vincularUserIdPaciente(pacienteId: string | number, userId: string): Promise<Paciente> {
const url = `${REST}/patients?id=eq.${encodeURIComponent(String(pacienteId))}`;
const payload = { user_id: String(userId) };
const res = await fetch(url, {
method: 'PATCH',
headers: withPrefer({ ...baseHeaders(), 'Content-Type': 'application/json' }, 'return=representation'),
body: JSON.stringify(payload),
});
const arr = await parse<Paciente[] | Paciente>(res);
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
}

View File

@ -13,14 +13,14 @@ export interface PatientAssignment {
user_id: string;
role: PatientAssignmentRole;
created_at: string;
created_by: string;
created_by: string | null;
}
export interface CreateAssignmentInput {
patient_id: string;
user_id: string;
role: PatientAssignmentRole;
created_by?: string;
created_by?: string | null;
}
// ===== CONSTANTES =====
@ -140,7 +140,7 @@ export async function listAssignmentsForPatient(patientId: string): Promise<Pati
*/
export async function listAssignmentsForUser(userId: string): Promise<PatientAssignment[]> {
console.log(`🔍 [ASSIGNMENT] Listando atribuições para o usuário: ${userId}`);
const url = `${ASSIGNMENTS_URL}?user_id=eq.${userId}`;
const url = `${ASSIGNMENTS_URL}?user_id=eq.${encodeURIComponent(userId)}`;
try {
const headers = getHeaders();

View File

@ -311,4 +311,69 @@ export async function listarRelatoriosPorMedico(idMedico: string): Promise<Repor
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do médico:', erro);
throw erro;
}
}
/**
* Lista relatórios para vários pacientes em uma única chamada (usa in.(...)).
* Retorna array vazio se nenhum id for fornecido.
*/
export async function listarRelatoriosPorPacientes(ids: string[]): Promise<Report[]> {
try {
if (!ids || !ids.length) return [];
// sanitize ids and remove empties
const cleaned = ids.map(i => String(i).trim()).filter(Boolean);
if (!cleaned.length) return [];
// monta cláusula in.(id1,id2,...)
const inClause = cleaned.join(',');
const url = `${BASE_API_RELATORIOS}?patient_id=in.(${inClause})`;
const headers = obterCabecalhos();
const masked = (headers as any)['Authorization'] ? '<<masked>>' : undefined;
console.debug('[listarRelatoriosPorPacientes] URL:', url);
console.debug('[listarRelatoriosPorPacientes] Headers (masked):', { ...headers, Authorization: masked ? '<<masked>>' : undefined });
const resposta = await fetch(url, { method: 'GET', headers });
const resultado = await tratarRespostaApi<Report[]>(resposta);
console.log('✅ [API RELATÓRIOS] Relatórios encontrados para pacientes:', resultado.length);
return resultado;
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios para vários pacientes:', erro);
throw erro;
}
}
/**
* Lista relatórios apenas para pacientes que foram atribuídos ao médico (userId).
* - Recupera as atribuições via `listAssignmentsForUser(userId)`
* - Extrai os patient_id e chama `listarRelatoriosPorPacientes` em batch
*/
export async function listarRelatoriosParaMedicoAtribuido(userId?: string): Promise<Report[]> {
try {
if (!userId) {
console.warn('[listarRelatoriosParaMedicoAtribuido] userId ausente, retornando array vazio');
return [];
}
console.log('[listarRelatoriosParaMedicoAtribuido] buscando assignments para user:', userId);
// importe dinamicamente para evitar possíveis ciclos
const assignmentMod = await import('./assignment');
const assigns = await assignmentMod.listAssignmentsForUser(String(userId));
if (!assigns || !Array.isArray(assigns) || assigns.length === 0) {
console.log('[listarRelatoriosParaMedicoAtribuido] nenhum paciente atribuído encontrado para user:', userId);
return [];
}
const patientIds = Array.from(new Set(assigns.map((a: any) => String(a.patient_id)).filter(Boolean)));
if (!patientIds.length) {
console.log('[listarRelatoriosParaMedicoAtribuido] nenhuma patient_id válida encontrada nas atribuições');
return [];
}
console.log('[listarRelatoriosParaMedicoAtribuido] carregando relatórios para pacientes:', patientIds);
const rels = await listarRelatoriosPorPacientes(patientIds);
return rels || [];
} catch (err) {
console.error('[listarRelatoriosParaMedicoAtribuido] erro:', err);
throw err;
}
}