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:
parent
34e2f4d05b
commit
d4cb5f98e0
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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...
|
||||
@ -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>
|
||||
)
|
||||
@ -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() {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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";
|
||||
|
||||
/*
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user