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";
|
import Link from "next/link";
|
||||||
|
|
||||||
// --- Imports do EventManager (NOVO) - MANTIDOS ---
|
// --- 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
|
import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback
|
||||||
|
|
||||||
// Imports mantidos
|
// Imports mantidos
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
import ProtectedRoute from "@/components/shared/ProtectedRoute";
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
import { Sidebar } from "@/components/layout/sidebar";
|
||||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { PagesHeader } from "@/components/features/dashboard/header";
|
import { PagesHeader } from "@/components/features/dashboard/header";
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import { AuthProvider } from "@/hooks/useAuth"
|
import { AuthProvider } from "@/hooks/useAuth"
|
||||||
import { ThemeProvider } from "@/components/theme-provider"
|
import { ThemeProvider } from "@/components/providers/theme-provider"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
@ -12,10 +12,10 @@ import { Label } from '@/components/ui/label'
|
|||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
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 { 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 { UploadAvatar } from '@/components/ui/upload-avatar'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
import ProtectedRoute from '@/components/shared/ProtectedRoute'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById } from '@/lib/api'
|
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)
|
if (localizacao) qs.set('local', localizacao)
|
||||||
// indicate navigation origin so destination can alter UX (e.g., show modal instead of redirect)
|
// indicate navigation origin so destination can alter UX (e.g., show modal instead of redirect)
|
||||||
qs.set('origin', 'paciente')
|
qs.set('origin', 'paciente')
|
||||||
return `/resultados?${qs.toString()}`
|
return `/paciente/resultados?${qs.toString()}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// derived lists for the page (computed after appointments state is declared)
|
// derived lists for the page (computed after appointments state is declared)
|
||||||
|
|||||||
@ -55,16 +55,17 @@ export default function ResultadosClient() {
|
|||||||
const params = useSearchParams()
|
const params = useSearchParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// Filtros/controles da UI
|
// Filtros/controles da UI - initialize with defaults to avoid hydration mismatch
|
||||||
const [tipoConsulta, setTipoConsulta] = useState<TipoConsulta>(
|
const [tipoConsulta, setTipoConsulta] = useState<TipoConsulta>('teleconsulta')
|
||||||
params?.get('tipo') === 'presencial' ? 'local' : 'teleconsulta'
|
const [especialidadeHero, setEspecialidadeHero] = useState<string>('Psicólogo')
|
||||||
)
|
|
||||||
const [especialidadeHero, setEspecialidadeHero] = useState<string>(params?.get('especialidade') || 'Psicólogo')
|
|
||||||
const [convenio, setConvenio] = useState<string>('Todos')
|
const [convenio, setConvenio] = useState<string>('Todos')
|
||||||
const [bairro, setBairro] = useState<string>('Todos')
|
const [bairro, setBairro] = useState<string>('Todos')
|
||||||
// Busca por nome do médico
|
// Busca por nome do médico
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||||
|
|
||||||
|
// Track if URL params have been synced to avoid race condition
|
||||||
|
const [paramsSync, setParamsSync] = useState(false)
|
||||||
|
|
||||||
// Estado dinâmico
|
// Estado dinâmico
|
||||||
const [patientId, setPatientId] = useState<string | null>(null)
|
const [patientId, setPatientId] = useState<string | null>(null)
|
||||||
const [medicos, setMedicos] = useState<Medico[]>([])
|
const [medicos, setMedicos] = useState<Medico[]>([])
|
||||||
@ -107,7 +108,20 @@ export default function ResultadosClient() {
|
|||||||
const [bookingSuccessOpen, setBookingSuccessOpen] = useState(false)
|
const [bookingSuccessOpen, setBookingSuccessOpen] = useState(false)
|
||||||
const [bookedWhenLabel, setBookedWhenLabel] = useState<string | null>(null)
|
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(() => {
|
useEffect(() => {
|
||||||
let mounted = true
|
let mounted = true
|
||||||
;(async () => {
|
;(async () => {
|
||||||
@ -127,10 +141,31 @@ export default function ResultadosClient() {
|
|||||||
return () => { mounted = false }
|
return () => { mounted = false }
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 2) Buscar médicos conforme especialidade selecionada
|
// 3) Initial doctors fetch on mount (one-time initialization)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If the user is actively searching by name, this effect should not run
|
let mounted = true
|
||||||
if (searchQuery && String(searchQuery).trim().length > 1) return
|
;(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
|
let mounted = true
|
||||||
;(async () => {
|
;(async () => {
|
||||||
@ -139,10 +174,15 @@ export default function ResultadosClient() {
|
|||||||
setMedicos([])
|
setMedicos([])
|
||||||
setAgendaByDoctor({})
|
setAgendaByDoctor({})
|
||||||
setAgendasExpandida({})
|
setAgendasExpandida({})
|
||||||
// termo de busca: usar a especialidade escolhida (fallback para string genérica)
|
// termo de busca: usar a especialidade escolhida
|
||||||
const termo = (especialidadeHero && especialidadeHero !== 'Veja mais') ? especialidadeHero : (params?.get('q') || 'medico')
|
const termo = (especialidadeHero && especialidadeHero !== 'Veja mais') ? especialidadeHero : 'medico'
|
||||||
const list = await buscarMedicos(termo).catch(() => [])
|
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
|
if (!mounted) return
|
||||||
|
console.log('[ResultadosClient] Doctors fetched:', list?.length || 0)
|
||||||
setMedicos(Array.isArray(list) ? list : [])
|
setMedicos(Array.isArray(list) ? list : [])
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showToast('error', e?.message || 'Falha ao buscar profissionais')
|
showToast('error', e?.message || 'Falha ao buscar profissionais')
|
||||||
@ -151,9 +191,9 @@ export default function ResultadosClient() {
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// 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(() => {
|
useEffect(() => {
|
||||||
let mounted = true
|
let mounted = true
|
||||||
const term = String(searchQuery || '').trim()
|
const term = String(searchQuery || '').trim()
|
||||||
@ -387,7 +427,7 @@ export default function ResultadosClient() {
|
|||||||
let start: Date
|
let start: Date
|
||||||
let end: Date
|
let end: Date
|
||||||
try {
|
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))) {
|
if (parts.length === 3 && parts.every((n) => !Number.isNaN(n))) {
|
||||||
const [y, m, d] = parts
|
const [y, m, d] = parts
|
||||||
start = new Date(y, m - 1, d, 0, 0, 0, 0)
|
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'],
|
5: ['5','fri','friday','sexta','sexta-feira'],
|
||||||
6: ['6','sat','saturday','sabado','sábado']
|
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) => {
|
const matched = (disponibilidades || []).filter((d: any) => {
|
||||||
try {
|
try {
|
||||||
const raw = String(d.weekday ?? d.weekday_name ?? d.day ?? d.day_of_week ?? '').toLowerCase()
|
const raw = String(d.weekday ?? d.weekday_name ?? d.day ?? d.day_of_week ?? '').toLowerCase()
|
||||||
if (!raw) return false
|
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.weekday === 'number' && d.weekday === weekdayNumber) return true
|
||||||
if (typeof d.day_of_week === 'number' && d.day_of_week === weekdayNumber) return true
|
if (typeof d.day_of_week === 'number' && d.day_of_week === weekdayNumber) return true
|
||||||
return false
|
return false
|
||||||
@ -441,7 +481,7 @@ export default function ResultadosClient() {
|
|||||||
const windows = matched.map((d: any) => {
|
const windows = matched.map((d: any) => {
|
||||||
const parseTime = (t?: string) => {
|
const parseTime = (t?: string) => {
|
||||||
if (!t) return { hh: 0, mm: 0, ss: 0 }
|
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 }
|
return { hh: parts[0] || 0, mm: parts[1] || 0, ss: parts[2] || 0 }
|
||||||
}
|
}
|
||||||
const s = parseTime(d.start_time)
|
const s = parseTime(d.start_time)
|
||||||
@ -488,8 +528,8 @@ export default function ResultadosClient() {
|
|||||||
cursorMs += perWindowStep * 60000
|
cursorMs += perWindowStep * 60000
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const lastBackendMs = backendSlotsInWindow[backendSlotsInWindow.length - 1]
|
const lastBackendMs = backendSlotsInWindow.at(-1)
|
||||||
let cursorMs = lastBackendMs + perWindowStep * 60000
|
let cursorMs = (lastBackendMs ?? 0) + perWindowStep * 60000
|
||||||
while (cursorMs <= lastStartMs) {
|
while (cursorMs <= lastStartMs) {
|
||||||
generatedSet.add(new Date(cursorMs).toISOString())
|
generatedSet.add(new Date(cursorMs).toISOString())
|
||||||
cursorMs += perWindowStep * 60000
|
cursorMs += perWindowStep * 60000
|
||||||
@ -682,7 +722,7 @@ export default function ResultadosClient() {
|
|||||||
<Toggle
|
<Toggle
|
||||||
pressed={tipoConsulta === 'teleconsulta'}
|
pressed={tipoConsulta === 'teleconsulta'}
|
||||||
onPressedChange={() => setTipoConsulta('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')}
|
tipoConsulta === 'teleconsulta' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
|
||||||
>
|
>
|
||||||
<Globe className="mr-2 h-4 w-4" />
|
<Globe className="mr-2 h-4 w-4" />
|
||||||
@ -691,7 +731,7 @@ export default function ResultadosClient() {
|
|||||||
<Toggle
|
<Toggle
|
||||||
pressed={tipoConsulta === 'local'}
|
pressed={tipoConsulta === 'local'}
|
||||||
onPressedChange={() => setTipoConsulta('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')}
|
tipoConsulta === 'local' ? 'bg-primary text-primary-foreground' : 'border border-primary/40 text-primary')}
|
||||||
>
|
>
|
||||||
<Building2 className="mr-2 h-4 w-4" />
|
<Building2 className="mr-2 h-4 w-4" />
|
||||||
@ -713,7 +753,7 @@ export default function ResultadosClient() {
|
|||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<Select value={bairro} onValueChange={setBairro}>
|
<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" />
|
<SelectValue placeholder="Bairro" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -778,6 +818,11 @@ export default function ResultadosClient() {
|
|||||||
|
|
||||||
{/* Lista de profissionais */}
|
{/* Lista de profissionais */}
|
||||||
<section className="space-y-4">
|
<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 && (
|
{loadingMedicos && (
|
||||||
<Card className="flex items-center justify-center border border-dashed border-border bg-card/60 p-12 text-muted-foreground">
|
<Card className="flex items-center justify-center border border-dashed border-border bg-card/60 p-12 text-muted-foreground">
|
||||||
Buscando profissionais...
|
Buscando profissionais...
|
||||||
@ -3,7 +3,7 @@ import ResultadosClient from './ResultadosClient'
|
|||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
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 />
|
<ResultadosClient />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
)
|
)
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { Header } from "@/components/layout/header"
|
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"
|
import { Footer } from "@/components/layout/footer"
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import SignatureCanvas from "react-signature-canvas";
|
import SignatureCanvas from "react-signature-canvas";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
import ProtectedRoute from "@/components/shared/ProtectedRoute";
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api";
|
import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api";
|
||||||
import { useReports } from "@/hooks/useReports";
|
import { useReports } from "@/hooks/useReports";
|
||||||
@ -12,7 +12,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select";
|
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 {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Header } from "@/components/layout/header"
|
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"
|
import { Footer } from "@/components/layout/footer"
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { EventCard } from "./EventCard";
|
|||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Event } from "@/components/event-manager";
|
import { Event } from "@/components/features/general/event-manager";
|
||||||
|
|
||||||
// Week View Component
|
// Week View Component
|
||||||
export function WeekView({
|
export function WeekView({
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Event } from "@/components/event-manager";
|
import { Event } from "@/components/features/general/event-manager";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|||||||
import { useState, useEffect, useRef } from "react"
|
import { useState, useEffect, useRef } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { SidebarTrigger } from "../../ui/sidebar"
|
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 }) {
|
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
||||||
const { logout, user } = useAuth();
|
const { logout, user } = useAuth();
|
||||||
|
|||||||
@ -32,7 +32,7 @@ import { getAvatarPublicUrl } from '@/lib/api';
|
|||||||
;
|
;
|
||||||
|
|
||||||
import { buscarCepAPI } from "@/lib/api";
|
import { buscarCepAPI } from "@/lib/api";
|
||||||
import { CredentialsDialog } from "@/components/credentials-dialog";
|
import { CredentialsDialog } from "@/components/features/general/credentials-dialog";
|
||||||
|
|
||||||
type FormacaoAcademica = {
|
type FormacaoAcademica = {
|
||||||
instituicao: string;
|
instituicao: string;
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import { getAvatarPublicUrl } from '@/lib/api';
|
|||||||
|
|
||||||
import { validarCPFLocal } from "@/lib/utils";
|
import { validarCPFLocal } from "@/lib/utils";
|
||||||
import { verificarCpfDuplicado } from "@/lib/api";
|
import { verificarCpfDuplicado } from "@/lib/api";
|
||||||
import { CredentialsDialog } from "@/components/credentials-dialog";
|
import { CredentialsDialog } from "@/components/features/general/credentials-dialog";
|
||||||
|
|
||||||
type Mode = "create" | "edit";
|
type Mode = "create" | "edit";
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import Link from "next/link";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Menu, X } from "lucide-react";
|
import { Menu, X } from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
|
import { SimpleThemeToggle } from "@/components/ui/simple-theme-toggle";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
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
|
// 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
|
// Mensagens amigáveis para erros comuns
|
||||||
let friendlyMessage = msg;
|
let friendlyMessage = msg;
|
||||||
@ -847,7 +848,7 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
|||||||
|
|
||||||
// Busca por ID se parece com UUID
|
// Busca por ID se parece com UUID
|
||||||
if (searchTerm.includes('-') && searchTerm.length > 10) {
|
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)
|
// 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)
|
// Busca por nome (usando ilike para busca case-insensitive)
|
||||||
|
// NOTA: apenas full_name existe, social_name foi removido
|
||||||
if (searchTerm.length >= 2) {
|
if (searchTerm.length >= 2) {
|
||||||
queries.push(`full_name=ilike.*${searchTerm}*`);
|
queries.push(`full_name=ilike.*${q}*`);
|
||||||
queries.push(`social_name=ilike.*${searchTerm}*`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca por email se contém @
|
// Busca por email se contém @
|
||||||
if (searchTerm.includes('@')) {
|
if (searchTerm.includes('@')) {
|
||||||
queries.push(`email=ilike.*${searchTerm}*`);
|
queries.push(`email=ilike.*${q}*`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const results: Paciente[] = [];
|
const results: Paciente[] = [];
|
||||||
@ -874,13 +875,8 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
|||||||
// Executa as buscas e combina resultados únicos
|
// Executa as buscas e combina resultados únicos
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
try {
|
try {
|
||||||
const [key, val] = String(query).split('=');
|
const url = `${REST}/patients?${query}&limit=10`;
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (key && val !== undefined) params.set(key, val);
|
|
||||||
params.set('limit', '10');
|
|
||||||
const url = `${REST}/patients?${params.toString()}`;
|
|
||||||
const headers = baseHeaders();
|
const headers = baseHeaders();
|
||||||
// Logs removidos por segurança
|
|
||||||
const res = await fetch(url, { method: "GET", headers });
|
const res = await fetch(url, { method: "GET", headers });
|
||||||
const arr = await parse<Paciente[]>(res);
|
const arr = await parse<Paciente[]>(res);
|
||||||
|
|
||||||
@ -893,7 +889,7 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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 searchTerm = termo.toLowerCase().trim();
|
||||||
const digitsOnly = searchTerm.replace(/\D/g, '');
|
const digitsOnly = searchTerm.replace(/\D/g, '');
|
||||||
// Do not pre-encode the searchTerm here; we'll let URLSearchParams handle encoding
|
const q = encodeURIComponent(searchTerm);
|
||||||
const q = searchTerm;
|
|
||||||
|
|
||||||
// Monta queries para buscar em múltiplos campos
|
// Monta queries para buscar em múltiplos campos
|
||||||
const queries = [];
|
const queries = [];
|
||||||
@ -1742,21 +1737,19 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
|||||||
|
|
||||||
// Busca por CRM (com e sem formatação)
|
// Busca por CRM (com e sem formatação)
|
||||||
if (digitsOnly.length >= 3) {
|
if (digitsOnly.length >= 3) {
|
||||||
queries.push(`crm=ilike.*${digitsOnly}*`);
|
queries.push(`crm=ilike.*${encodeURIComponent(digitsOnly)}*`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca por nome (usando ilike para busca case-insensitive)
|
// Busca por nome (usando ilike para busca case-insensitive)
|
||||||
|
// NOTA: apenas full_name existe na tabela, nome_social foi removido
|
||||||
if (searchTerm.length >= 2) {
|
if (searchTerm.length >= 2) {
|
||||||
queries.push(`full_name=ilike.*${q}*`);
|
queries.push(`full_name=ilike.*${q}*`);
|
||||||
queries.push(`nome_social=ilike.*${q}*`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca por email se contém @
|
// Busca por email se contém @
|
||||||
if (searchTerm.includes('@')) {
|
if (searchTerm.includes('@')) {
|
||||||
// Quando o usuário pesquisa por email (contendo '@'), limitar as queries apenas ao campo email.
|
// 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
|
queries.length = 0;
|
||||||
// erros de requisição (400) dependendo das colunas e políticas. Reduzimos o escopo para evitar 400s.
|
|
||||||
queries.length = 0; // limpar queries anteriores
|
|
||||||
queries.push(`email=ilike.*${q}*`);
|
queries.push(`email=ilike.*${q}*`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1764,8 +1757,6 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
|||||||
if (searchTerm.length >= 2) {
|
if (searchTerm.length >= 2) {
|
||||||
queries.push(`specialty=ilike.*${q}*`);
|
queries.push(`specialty=ilike.*${q}*`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug removido por segurança
|
|
||||||
|
|
||||||
const results: Medico[] = [];
|
const results: Medico[] = [];
|
||||||
const seenIds = new Set<string>();
|
const seenIds = new Set<string>();
|
||||||
@ -1773,15 +1764,8 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
|||||||
// Executa as buscas e combina resultados únicos
|
// Executa as buscas e combina resultados únicos
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
try {
|
try {
|
||||||
// Build the URL safely using URLSearchParams so special characters (like @) are encoded correctly
|
const url = `${REST}/doctors?${query}&limit=10`;
|
||||||
// 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 headers = baseHeaders();
|
const headers = baseHeaders();
|
||||||
// Logs removidos por segurança
|
|
||||||
const res = await fetch(url, { method: 'GET', headers });
|
const res = await fetch(url, { method: 'GET', headers });
|
||||||
const arr = await parse<Medico[]>(res);
|
const arr = await parse<Medico[]>(res);
|
||||||
|
|
||||||
@ -1794,7 +1778,7 @@ export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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> {
|
export async function buscarRelatorioPorId(id: string): Promise<Report> {
|
||||||
try {
|
try {
|
||||||
// Log removido por segurança
|
// Validar ID antes de fazer requisição
|
||||||
const resposta = await fetch(`${BASE_API_RELATORIOS}?id=eq.${id}`, {
|
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',
|
method: 'GET',
|
||||||
headers: obterCabecalhos(),
|
headers: obterCabecalhos(),
|
||||||
});
|
});
|
||||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||||
const relatorio = Array.isArray(resultado) && resultado.length > 0 ? resultado[0] : null;
|
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');
|
if (!relatorio) throw new Error('Relatório não encontrado');
|
||||||
return relatorio;
|
return relatorio;
|
||||||
} catch (erro) {
|
} catch (erro) {
|
||||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatório:', erro);
|
console.error('[REPORTS] Erro ao buscar relatório:', erro);
|
||||||
throw erro;
|
throw erro;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -259,39 +264,38 @@ export async function deletarRelatorio(id: string): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<Report[]> {
|
export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<Report[]> {
|
||||||
try {
|
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)
|
// 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}`;
|
let url = `${BASE_API_RELATORIOS}?patient_id=eq.${encodedId}`;
|
||||||
const headers = obterCabecalhos();
|
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, {
|
const resposta = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
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 eq returned results, return them. Otherwise retry using `in.(id)` which some setups prefer.
|
||||||
if (Array.isArray(resultado) && resultado.length) return resultado;
|
if (Array.isArray(resultado) && resultado.length) return resultado;
|
||||||
|
|
||||||
// Retry with in.(id) clause as a fallback
|
// Retry with in.(id) clause as a fallback
|
||||||
try {
|
try {
|
||||||
const inClause = encodeURIComponent(`(${String(idPaciente)})`);
|
const inClause = encodeURIComponent(`(${String(idPaciente).trim()})`);
|
||||||
const urlIn = `${BASE_API_RELATORIOS}?patient_id=in.${inClause}`;
|
const urlIn = `${BASE_API_RELATORIOS}?patient_id=in.${inClause}`;
|
||||||
// Log removido por segurança
|
|
||||||
const resp2 = await fetch(urlIn, { method: 'GET', headers });
|
const resp2 = await fetch(urlIn, { method: 'GET', headers });
|
||||||
const res2 = await tratarRespostaApi<Report[]>(resp2);
|
const res2 = await tratarRespostaApi<Report[]>(resp2);
|
||||||
// Log removido por segurança
|
|
||||||
return Array.isArray(res2) ? res2 : [];
|
return Array.isArray(res2) ? res2 : [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Log removido por segurança
|
// Fallback falhou, retornar vazio
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
|
||||||
} catch (erro) {
|
} catch (erro) {
|
||||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do paciente:', erro);
|
console.error('[REPORTS] Erro ao buscar relatórios do paciente:', erro);
|
||||||
throw erro;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,20 +304,24 @@ export async function listarRelatoriosPorPaciente(idPaciente: string): Promise<R
|
|||||||
*/
|
*/
|
||||||
export async function listarRelatoriosPorMedico(idMedico: string): Promise<Report[]> {
|
export async function listarRelatoriosPorMedico(idMedico: string): Promise<Report[]> {
|
||||||
try {
|
try {
|
||||||
console.log('👨⚕️ [API RELATÓRIOS] Buscando relatórios do médico:', idMedico);
|
// Validar ID antes de fazer requisição
|
||||||
const url = `${BASE_API_RELATORIOS}?requested_by=eq.${idMedico}`;
|
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();
|
const headers = obterCabecalhos();
|
||||||
// Logs removidos por segurança
|
|
||||||
const resposta = await fetch(url, {
|
const resposta = await fetch(url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: obterCabecalhos(),
|
headers: obterCabecalhos(),
|
||||||
});
|
});
|
||||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||||
// Log removido por segurança
|
return Array.isArray(resultado) ? resultado : [];
|
||||||
return resultado;
|
|
||||||
} catch (erro) {
|
} catch (erro) {
|
||||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios do médico:', erro);
|
console.error('[REPORTS] Erro ao buscar relatórios do médico:', erro);
|
||||||
throw erro;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,19 +336,17 @@ export async function listarRelatoriosPorPacientes(ids: string[]): Promise<Repor
|
|||||||
const cleaned = ids.map(i => String(i).trim()).filter(Boolean);
|
const cleaned = ids.map(i => String(i).trim()).filter(Boolean);
|
||||||
if (!cleaned.length) return [];
|
if (!cleaned.length) return [];
|
||||||
|
|
||||||
// monta cláusula in.(id1,id2,...)
|
// monta cláusula in.(id1,id2,...) com proper encoding
|
||||||
const inClause = cleaned.join(',');
|
const encodedIds = cleaned.map(id => encodeURIComponent(id)).join(',');
|
||||||
const url = `${BASE_API_RELATORIOS}?patient_id=in.(${inClause})`;
|
const url = `${BASE_API_RELATORIOS}?patient_id=in.(${encodedIds})`;
|
||||||
const headers = obterCabecalhos();
|
const headers = obterCabecalhos();
|
||||||
// Logs removidos por segurança
|
|
||||||
|
|
||||||
const resposta = await fetch(url, { method: 'GET', headers });
|
const resposta = await fetch(url, { method: 'GET', headers });
|
||||||
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
const resultado = await tratarRespostaApi<Report[]>(resposta);
|
||||||
// Log removido por segurança
|
return Array.isArray(resultado) ? resultado : [];
|
||||||
return resultado;
|
|
||||||
} catch (erro) {
|
} catch (erro) {
|
||||||
console.error('❌ [API RELATÓRIOS] Erro ao buscar relatórios para vários pacientes:', erro);
|
console.error('[REPORTS] Erro ao buscar relatórios para vários pacientes:', erro);
|
||||||
throw erro;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user