refactor(structure): Organizes the structure of the app and components folders

- Reorganizes the components folder into ui, layout, features, shared, and providers for better modularity.
- Groups routes in the app folder using a route group (auth).
- Updates all imports to reflect the new file structure.
This commit is contained in:
M-Gabrielly 2025-11-05 18:06:13 -03:00
parent 34e2f4d05b
commit d4cb5f98e0
29 changed files with 137 additions and 102 deletions

View File

@ -6,7 +6,7 @@ import dynamic from "next/dynamic";
import Link from "next/link";
// --- Imports do EventManager (NOVO) - MANTIDOS ---
import { EventManager, type Event } from "@/components/event-manager";
import { EventManager, type Event } from "@/components/features/general/event-manager";
import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback
// Imports mantidos

View File

@ -1,5 +1,5 @@
import type React from "react";
import ProtectedRoute from "@/components/ProtectedRoute";
import ProtectedRoute from "@/components/shared/ProtectedRoute";
import { Sidebar } from "@/components/layout/sidebar";
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { PagesHeader } from "@/components/features/dashboard/header";

View File

@ -1,7 +1,7 @@
import type React from "react"
import type { Metadata } from "next"
import { AuthProvider } from "@/hooks/useAuth"
import { ThemeProvider } from "@/components/theme-provider"
import { ThemeProvider } from "@/components/providers/theme-provider"
import "./globals.css"
export const metadata: Metadata = {

View File

@ -12,10 +12,10 @@ import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react'
import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
import { SimpleThemeToggle } from '@/components/ui/simple-theme-toggle'
import { UploadAvatar } from '@/components/ui/upload-avatar'
import Link from 'next/link'
import ProtectedRoute from '@/components/ProtectedRoute'
import ProtectedRoute from '@/components/shared/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById } from '@/lib/api'
@ -632,7 +632,7 @@ export default function PacientePage() {
if (localizacao) qs.set('local', localizacao)
// indicate navigation origin so destination can alter UX (e.g., show modal instead of redirect)
qs.set('origin', 'paciente')
return `/resultados?${qs.toString()}`
return `/paciente/resultados?${qs.toString()}`
}
// derived lists for the page (computed after appointments state is declared)

View File

@ -55,16 +55,17 @@ export default function ResultadosClient() {
const params = useSearchParams()
const router = useRouter()
// Filtros/controles da UI
const [tipoConsulta, setTipoConsulta] = useState<TipoConsulta>(
params?.get('tipo') === 'presencial' ? 'local' : 'teleconsulta'
)
const [especialidadeHero, setEspecialidadeHero] = useState<string>(params?.get('especialidade') || 'Psicólogo')
// Filtros/controles da UI - initialize with defaults to avoid hydration mismatch
const [tipoConsulta, setTipoConsulta] = useState<TipoConsulta>('teleconsulta')
const [especialidadeHero, setEspecialidadeHero] = useState<string>('Psicólogo')
const [convenio, setConvenio] = useState<string>('Todos')
const [bairro, setBairro] = useState<string>('Todos')
// Busca por nome do médico
const [searchQuery, setSearchQuery] = useState<string>('')
// Track if URL params have been synced to avoid race condition
const [paramsSync, setParamsSync] = useState(false)
// Estado dinâmico
const [patientId, setPatientId] = useState<string | null>(null)
const [medicos, setMedicos] = useState<Medico[]>([])
@ -107,7 +108,20 @@ export default function ResultadosClient() {
const [bookingSuccessOpen, setBookingSuccessOpen] = useState(false)
const [bookedWhenLabel, setBookedWhenLabel] = useState<string | null>(null)
// 1) Obter patientId a partir do usuário autenticado (email -> patients)
// 1) Sincronize URL params with state after client mount (prevent hydration mismatch)
useEffect(() => {
if (!params) return
const tipoParam = params.get('tipo')
if (tipoParam === 'presencial') setTipoConsulta('local')
const especialidadeParam = params.get('especialidade')
if (especialidadeParam) setEspecialidadeHero(especialidadeParam)
// Mark params as synced
setParamsSync(true)
}, [params])
// 2) Fetch patient ID from auth
useEffect(() => {
let mounted = true
;(async () => {
@ -127,10 +141,31 @@ export default function ResultadosClient() {
return () => { mounted = false }
}, [])
// 2) Buscar médicos conforme especialidade selecionada
// 3) Initial doctors fetch on mount (one-time initialization)
useEffect(() => {
// If the user is actively searching by name, this effect should not run
if (searchQuery && String(searchQuery).trim().length > 1) return
let mounted = true
;(async () => {
try {
setLoadingMedicos(true)
console.log('[ResultadosClient] Initial doctors fetch starting')
const list = await buscarMedicos('medico').catch((err) => {
console.error('[ResultadosClient] Initial fetch error:', err)
return []
})
if (!mounted) return
console.log('[ResultadosClient] Initial fetch completed, got:', list?.length || 0, 'doctors')
setMedicos(Array.isArray(list) ? list : [])
} finally {
if (mounted) setLoadingMedicos(false)
}
})()
return () => { mounted = false }
}, [])
// 4) Re-fetch doctors when especialidade changes (after initial sync)
useEffect(() => {
// Skip if this is the initial render or if user is searching by name
if (!paramsSync || (searchQuery && String(searchQuery).trim().length > 1)) return
let mounted = true
;(async () => {
@ -139,10 +174,15 @@ export default function ResultadosClient() {
setMedicos([])
setAgendaByDoctor({})
setAgendasExpandida({})
// termo de busca: usar a especialidade escolhida (fallback para string genérica)
const termo = (especialidadeHero && especialidadeHero !== 'Veja mais') ? especialidadeHero : (params?.get('q') || 'medico')
const list = await buscarMedicos(termo).catch(() => [])
// termo de busca: usar a especialidade escolhida
const termo = (especialidadeHero && especialidadeHero !== 'Veja mais') ? especialidadeHero : 'medico'
console.log('[ResultadosClient] Fetching doctors with term:', termo)
const list = await buscarMedicos(termo).catch((err) => {
console.error('[ResultadosClient] buscarMedicos error:', err)
return []
})
if (!mounted) return
console.log('[ResultadosClient] Doctors fetched:', list?.length || 0)
setMedicos(Array.isArray(list) ? list : [])
} catch (e: any) {
showToast('error', e?.message || 'Falha ao buscar profissionais')
@ -151,9 +191,9 @@ export default function ResultadosClient() {
}
})()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [especialidadeHero])
}, [especialidadeHero, paramsSync])
// Debounced search by doctor name. When searchQuery is non-empty (>=2 chars), call buscarMedicos
// 5) Debounced search by doctor name
useEffect(() => {
let mounted = true
const term = String(searchQuery || '').trim()
@ -387,7 +427,7 @@ export default function ResultadosClient() {
let start: Date
let end: Date
try {
const parts = String(dateOnly).split('-').map((p) => Number(p))
const parts = String(dateOnly).split('-').map(Number)
if (parts.length === 3 && parts.every((n) => !Number.isNaN(n))) {
const [y, m, d] = parts
start = new Date(y, m - 1, d, 0, 0, 0, 0)
@ -425,12 +465,12 @@ export default function ResultadosClient() {
5: ['5','fri','friday','sexta','sexta-feira'],
6: ['6','sat','saturday','sabado','sábado']
}
const allowed = (weekdayNames[weekdayNumber] || []).map(s => String(s).toLowerCase())
const allowed = new Set((weekdayNames[weekdayNumber] || []).map(s => String(s).toLowerCase()))
const matched = (disponibilidades || []).filter((d: any) => {
try {
const raw = String(d.weekday ?? d.weekday_name ?? d.day ?? d.day_of_week ?? '').toLowerCase()
if (!raw) return false
if (allowed.includes(raw)) return true
if (allowed.has(raw)) return true
if (typeof d.weekday === 'number' && d.weekday === weekdayNumber) return true
if (typeof d.day_of_week === 'number' && d.day_of_week === weekdayNumber) return true
return false
@ -441,7 +481,7 @@ export default function ResultadosClient() {
const windows = matched.map((d: any) => {
const parseTime = (t?: string) => {
if (!t) return { hh: 0, mm: 0, ss: 0 }
const parts = String(t).split(':').map((p) => Number(p))
const parts = String(t).split(':').map(Number)
return { hh: parts[0] || 0, mm: parts[1] || 0, ss: parts[2] || 0 }
}
const s = parseTime(d.start_time)
@ -488,8 +528,8 @@ export default function ResultadosClient() {
cursorMs += perWindowStep * 60000
}
} else {
const lastBackendMs = backendSlotsInWindow[backendSlotsInWindow.length - 1]
let cursorMs = lastBackendMs + perWindowStep * 60000
const lastBackendMs = backendSlotsInWindow.at(-1)
let cursorMs = (lastBackendMs ?? 0) + perWindowStep * 60000
while (cursorMs <= lastStartMs) {
generatedSet.add(new Date(cursorMs).toISOString())
cursorMs += perWindowStep * 60000
@ -682,7 +722,7 @@ export default function ResultadosClient() {
<Toggle
pressed={tipoConsulta === 'teleconsulta'}
onPressedChange={() => setTipoConsulta('teleconsulta')}
className={cn('rounded-full px-4 py-[10px] text-sm font-medium transition hover:bg-primary hover:text-primary-foreground focus-visible:ring-2 focus-visible:ring-primary/60 active:scale-[0.97]',
className={cn('rounded-full px-4 py-2.5 text-sm font-medium transition hover:bg-primary hover:text-primary-foreground focus-visible:ring-2 focus-visible:ring-primary/60 active:scale-[0.97]',
tipoConsulta === 'teleconsulta' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
>
<Globe className="mr-2 h-4 w-4" />
@ -691,7 +731,7 @@ export default function ResultadosClient() {
<Toggle
pressed={tipoConsulta === 'local'}
onPressedChange={() => setTipoConsulta('local')}
className={cn('rounded-full px-4 py-[10px] text-sm font-medium transition hover:bg-primary hover:text-primary-foreground focus-visible:ring-2 focus-visible:ring-primary/60 active:scale-[0.97]',
className={cn('rounded-full px-4 py-2.5 text-sm font-medium transition hover:bg-primary hover:text-primary-foreground focus-visible:ring-2 focus-visible:ring-primary/60 active:scale-[0.97]',
tipoConsulta === 'local' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
>
<Building2 className="mr-2 h-4 w-4" />
@ -713,7 +753,7 @@ export default function ResultadosClient() {
</Select>
<Select value={bairro} onValueChange={setBairro}>
<SelectTrigger className="h-10 min-w-[160px] rounded-full border border-primary/40 bg-primary/10 text-primary transition duration-200 hover:border-primary! focus:ring-2 focus:ring-primary cursor-pointer">
<SelectTrigger className="h-10 min-w-40 rounded-full border border-primary/40 bg-primary/10 text-primary transition duration-200 hover:border-primary! focus:ring-2 focus:ring-primary cursor-pointer">
<SelectValue placeholder="Bairro" />
</SelectTrigger>
<SelectContent>
@ -778,6 +818,11 @@ export default function ResultadosClient() {
{/* Lista de profissionais */}
<section className="space-y-4">
{/* Debug card */}
<div className="text-xs text-muted-foreground p-2 bg-muted/30 rounded">
Status: loading={loadingMedicos} | medicos={medicos.length} | profissionais={profissionais.length} | especialidade={especialidadeHero} | paramsSync={paramsSync}
</div>
{loadingMedicos && (
<Card className="flex items-center justify-center border border-dashed border-border bg-card/60 p-12 text-muted-foreground">
Buscando profissionais...

View File

@ -3,7 +3,7 @@ import ResultadosClient from './ResultadosClient'
export default function Page() {
return (
<Suspense fallback={<div className="min-h-screen">Carregando...</div>}>
<Suspense fallback={<div className="min-h-screen flex items-center justify-center"><span>Carregando...</span></div>}>
<ResultadosClient />
</Suspense>
)

View File

@ -1,5 +1,5 @@
import { Header } from "@/components/layout/header"
import { HeroSection } from "@/components/hero-section"
import { HeroSection } from "@/components/features/general/hero-section"
import { Footer } from "@/components/layout/footer"
export default function HomePage() {

View File

@ -2,7 +2,7 @@
import React, { useState, useRef, useEffect } from "react";
import SignatureCanvas from "react-signature-canvas";
import Link from "next/link";
import ProtectedRoute from "@/components/ProtectedRoute";
import ProtectedRoute from "@/components/shared/ProtectedRoute";
import { useAuth } from "@/hooks/useAuth";
import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api";
import { useReports } from "@/hooks/useReports";
@ -12,7 +12,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
import { SimpleThemeToggle } from "@/components/ui/simple-theme-toggle";
import {
Table,
TableBody,

View File

@ -1,5 +1,5 @@
import { Header } from "@/components/layout/header"
import { AboutSection } from "@/components/about-section"
import { AboutSection } from "@/components/features/general/about-section"
import { Footer } from "@/components/layout/footer"
export default function AboutPage() {

View File

@ -3,7 +3,7 @@ import { EventCard } from "./EventCard";
import { Card } from "@/components/ui/card";
// Types
import { Event } from "@/components/event-manager";
import { Event } from "@/components/features/general/event-manager";
// Week View Component
export function WeekView({

View File

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { Event } from "@/components/event-manager";
import { Event } from "@/components/features/general/event-manager";
import { cn } from "@/lib/utils";
/*

View File

@ -8,7 +8,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useState, useEffect, useRef } from "react"
import { useRouter } from "next/navigation"
import { SidebarTrigger } from "../../ui/sidebar"
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
import { SimpleThemeToggle } from "@/components/ui/simple-theme-toggle";
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
const { logout, user } = useAuth();

View File

@ -32,7 +32,7 @@ import { getAvatarPublicUrl } from '@/lib/api';
;
import { buscarCepAPI } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
import { CredentialsDialog } from "@/components/features/general/credentials-dialog";
type FormacaoAcademica = {
instituicao: string;

View File

@ -30,7 +30,7 @@ import { getAvatarPublicUrl } from '@/lib/api';
import { validarCPFLocal } from "@/lib/utils";
import { verificarCpfDuplicado } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
import { CredentialsDialog } from "@/components/features/general/credentials-dialog";
type Mode = "create" | "edit";

View File

@ -5,7 +5,7 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Menu, X } from "lucide-react";
import { usePathname } from "next/navigation";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
import { SimpleThemeToggle } from "@/components/ui/simple-theme-toggle";
export function Header() {
const [isMenuOpen, setIsMenuOpen] = useState(false);

View File

@ -742,7 +742,8 @@ async function parse<T>(res: Response): Promise<T> {
}
// For other errors, log a concise error and try to produce a friendly message
console.error('[API ERROR] Status:', res.status, json ? 'JSON response' : 'no-json', rawText ? 'raw body present' : 'no raw body');
const endpoint = res.url ? new URL(res.url).pathname : 'unknown';
console.error('[API ERROR] Status:', res.status, 'Endpoint:', endpoint, json ? 'JSON response' : 'no-json', rawText ? 'raw body present' : 'no raw body', 'Message:', msg || 'N/A');
// Mensagens amigáveis para erros comuns
let friendlyMessage = msg;
@ -847,7 +848,7 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
// Busca por ID se parece com UUID
if (searchTerm.includes('-') && searchTerm.length > 10) {
queries.push(`id=eq.${searchTerm}`);
queries.push(`id=eq.${encodeURIComponent(searchTerm)}`);
}
// Busca por CPF (com e sem formatação)
@ -858,14 +859,14 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
}
// Busca por nome (usando ilike para busca case-insensitive)
// NOTA: apenas full_name existe, social_name foi removido
if (searchTerm.length >= 2) {
queries.push(`full_name=ilike.*${searchTerm}*`);
queries.push(`social_name=ilike.*${searchTerm}*`);
queries.push(`full_name=ilike.*${q}*`);
}
// Busca por email se contém @
if (searchTerm.includes('@')) {
queries.push(`email=ilike.*${searchTerm}*`);
queries.push(`email=ilike.*${q}*`);
}
const results: Paciente[] = [];
@ -874,13 +875,8 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
// Executa as buscas e combina resultados únicos
for (const query of queries) {
try {
const [key, val] = String(query).split('=');
const params = new URLSearchParams();
if (key && val !== undefined) params.set(key, val);
params.set('limit', '10');
const url = `${REST}/patients?${params.toString()}`;
const url = `${REST}/patients?${query}&limit=10`;
const headers = baseHeaders();
// Logs removidos por segurança
const res = await fetch(url, { method: "GET", headers });
const arr = await parse<Paciente[]>(res);
@ -893,7 +889,7 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
}
}
} catch (error) {
console.warn(`Erro na busca com query: ${query}`, error);
console.warn(`[API] Erro na busca de pacientes com query: ${query}`, error);
}
}
@ -1729,8 +1725,7 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
const searchTerm = termo.toLowerCase().trim();
const digitsOnly = searchTerm.replace(/\D/g, '');
// Do not pre-encode the searchTerm here; we'll let URLSearchParams handle encoding
const q = searchTerm;
const q = encodeURIComponent(searchTerm);
// Monta queries para buscar em múltiplos campos
const queries = [];
@ -1742,21 +1737,19 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
// Busca por CRM (com e sem formatação)
if (digitsOnly.length >= 3) {
queries.push(`crm=ilike.*${digitsOnly}*`);
queries.push(`crm=ilike.*${encodeURIComponent(digitsOnly)}*`);
}
// Busca por nome (usando ilike para busca case-insensitive)
// NOTA: apenas full_name existe na tabela, nome_social foi removido
if (searchTerm.length >= 2) {
queries.push(`full_name=ilike.*${q}*`);
queries.push(`nome_social=ilike.*${q}*`);
}
// Busca por email se contém @
if (searchTerm.includes('@')) {
// Quando o usuário pesquisa por email (contendo '@'), limitar as queries apenas ao campo email.
// Em alguns esquemas de banco / views, buscar por outros campos com um email pode provocar
// erros de requisição (400) dependendo das colunas e políticas. Reduzimos o escopo para evitar 400s.
queries.length = 0; // limpar queries anteriores
queries.length = 0;
queries.push(`email=ilike.*${q}*`);
}
@ -1764,8 +1757,6 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
if (searchTerm.length >= 2) {
queries.push(`specialty=ilike.*${q}*`);
}
// Debug removido por segurança
const results: Medico[] = [];
const seenIds = new Set<string>();
@ -1773,15 +1764,8 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
// Executa as buscas e combina resultados únicos
for (const query of queries) {
try {
// Build the URL safely using URLSearchParams so special characters (like @) are encoded correctly
// query is like 'nome_social=ilike.*something*' -> split into key/value
const [key, val] = String(query).split('=');
const params = new URLSearchParams();
if (key && val !== undefined) params.set(key, val);
params.set('limit', '10');
const url = `${REST}/doctors?${params.toString()}`;
const url = `${REST}/doctors?${query}&limit=10`;
const headers = baseHeaders();
// Logs removidos por segurança
const res = await fetch(url, { method: 'GET', headers });
const arr = await parse<Medico[]>(res);
@ -1794,7 +1778,7 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
}
}
} catch (error) {
console.warn(`Erro na busca com query: ${query}`, error);
console.warn(`[API] Erro na busca de médicos com query: ${query}`, error);
}
}

View File

@ -162,18 +162,23 @@ export async function listarRelatorios(filtros?: { patient_id?: string; status?:
*/
export async function buscarRelatorioPorId(id: string): Promise<Report> {
try {
// Log removido por segurança
const resposta = await fetch(`${BASE_API_RELATORIOS}?id=eq.${id}`, {
// Validar ID antes de fazer requisição
if (!id || typeof id !== 'string' || id.trim() === '') {
console.warn('[REPORTS] ID vazio ou inválido ao buscar relatório');
throw new Error('ID de relatório inválido');
}
const encodedId = encodeURIComponent(id.trim());
const resposta = await fetch(`${BASE_API_RELATORIOS}?id=eq.${encodedId}`, {
method: 'GET',
headers: obterCabecalhos(),
});
const resultado = await tratarRespostaApi<Report[]>(resposta);
const relatorio = Array.isArray(resultado) && resultado.length > 0 ? resultado[0] : null;
// Log removido por segurança
if (!relatorio) throw new Error('Relatório não encontrado');
return relatorio;
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatório:', erro);
console.error('[REPORTS] Erro ao buscar relatório:', erro);
throw erro;
}
}
@ -259,39 +264,38 @@ export async function deletarRelatorio(id: string): Promise<void> {
*/
export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<Report[]> {
try {
// Logs removidos por segurança
// Validar ID antes de fazer requisição
if (!idPaciente || typeof idPaciente !== 'string' || idPaciente.trim() === '') {
console.warn('[REPORTS] ID paciente vazio ou inválido ao listar relatórios');
return [];
}
// Try a strict eq lookup first (encode the id)
const encodedId = encodeURIComponent(String(idPaciente));
const encodedId = encodeURIComponent(String(idPaciente).trim());
let url = `${BASE_API_RELATORIOS}?patient_id=eq.${encodedId}`;
const headers = obterCabecalhos();
const masked = (headers as any)['Authorization'] ? `${String((headers as any)['Authorization']).slice(0,6)}...${String((headers as any)['Authorization']).slice(-6)}` : null;
// Logs removidos por segurança
const resposta = await fetch(url, {
method: 'GET',
headers,
});
const resultado = await tratarRespostaApi<Report[]>(resposta);
// Log removido por segurança
// If eq returned results, return them. Otherwise retry using `in.(id)` which some setups prefer.
if (Array.isArray(resultado) && resultado.length) return resultado;
// Retry with in.(id) clause as a fallback
try {
const inClause = encodeURIComponent(`(${String(idPaciente)})`);
const inClause = encodeURIComponent(`(${String(idPaciente).trim()})`);
const urlIn = `${BASE_API_RELATORIOS}?patient_id=in.${inClause}`;
// Log removido por segurança
const resp2 = await fetch(urlIn, { method: 'GET', headers });
const res2 = await tratarRespostaApi<Report[]>(resp2);
// Log removido por segurança
return Array.isArray(res2) ? res2 : [];
} catch (e) {
// Log removido por segurança
// Fallback falhou, retornar vazio
return [];
}
return [];
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do paciente:', erro);
throw erro;
console.error('[REPORTS] Erro ao buscar relatórios do paciente:', erro);
return [];
}
}
@ -300,20 +304,24 @@ export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<R
*/
export async function listarRelatoriosPorMedico(idMedico: string): Promise<Report[]> {
try {
console.log('👨‍⚕️ [API RELATÓRIOS] Buscando relatórios do médico:', idMedico);
const url = `${BASE_API_RELATORIOS}?requested_by=eq.${idMedico}`;
// Validar ID antes de fazer requisição
if (!idMedico || typeof idMedico !== 'string' || idMedico.trim() === '') {
console.warn('[REPORTS] ID médico vazio ou inválido ao listar relatórios');
return [];
}
const encodedId = encodeURIComponent(idMedico.trim());
const url = `${BASE_API_RELATORIOS}?requested_by=eq.${encodedId}`;
const headers = obterCabecalhos();
// Logs removidos por segurança
const resposta = await fetch(url, {
method: 'GET',
headers: obterCabecalhos(),
});
const resultado = await tratarRespostaApi<Report[]>(resposta);
// Log removido por segurança
return resultado;
return Array.isArray(resultado) ? resultado : [];
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do médico:', erro);
throw erro;
console.error('[REPORTS] Erro ao buscar relatórios do médico:', erro);
return [];
}
}
@ -328,19 +336,17 @@ export async function listarRelatoriosPorPacientes(ids: string[]): Promise<Repor
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})`;
// monta cláusula in.(id1,id2,...) com proper encoding
const encodedIds = cleaned.map(id => encodeURIComponent(id)).join(',');
const url = `${BASE_API_RELATORIOS}?patient_id=in.(${encodedIds})`;
const headers = obterCabecalhos();
// Logs removidos por segurança
const resposta = await fetch(url, { method: 'GET', headers });
const resultado = await tratarRespostaApi<Report[]>(resposta);
// Log removido por segurança
return resultado;
return Array.isArray(resultado) ? resultado : [];
} catch (erro) {
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios para vários pacientes:', erro);
throw erro;
console.error('[REPORTS] Erro ao buscar relatórios para vários pacientes:', erro);
return [];
}
}