From d4cb5f98e0cd1f33ed3fe907ae9ebd60c8745b99 Mon Sep 17 00:00:00 2001 From: M-Gabrielly Date: Wed, 5 Nov 2025 18:06:13 -0300 Subject: [PATCH] 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. --- .../app/{ => (auth)}/login-admin/page.tsx | 0 .../app/{ => (auth)}/login-paciente/page.tsx | 0 susconecta/app/{ => (auth)}/login/page.tsx | 0 .../app/(main-routes)/calendar/page.tsx | 2 +- susconecta/app/(main-routes)/layout.tsx | 2 +- susconecta/app/layout.tsx | 2 +- susconecta/app/paciente/page.tsx | 6 +- .../resultados/ResultadosClient.tsx | 91 ++++++++++++++----- .../app/{ => paciente}/resultados/page.tsx | 2 +- susconecta/app/page.tsx | 2 +- susconecta/app/profissional/page.tsx | 4 +- susconecta/app/sobre/page.tsx | 2 +- .../features/Calendario/Calendar.tsx | 2 +- .../features/Calendario/EventCard.tsx | 2 +- .../components/features/dashboard/header.tsx | 2 +- .../forms/doctor-registration-form.tsx | 2 +- .../forms/patient-registration-form.tsx | 2 +- .../{ => features/general}/about-section.tsx | 0 .../general}/calendarComponente/page.tsx | 0 .../general}/credentials-dialog.tsx | 0 .../{ => features/general}/event-manager.tsx | 0 .../{ => features/general}/hero-section.tsx | 0 susconecta/components/layout/header.tsx | 2 +- .../{ => providers}/theme-provider.tsx | 0 .../{ => shared}/ProtectedRoute.tsx | 0 .../{ => ui}/simple-theme-toggle.tsx | 0 .../components/{ => ui}/theme-toggle.tsx | 0 susconecta/lib/api.ts | 44 +++------ susconecta/lib/reports.ts | 70 +++++++------- 29 files changed, 137 insertions(+), 102 deletions(-) rename susconecta/app/{ => (auth)}/login-admin/page.tsx (100%) rename susconecta/app/{ => (auth)}/login-paciente/page.tsx (100%) rename susconecta/app/{ => (auth)}/login/page.tsx (100%) rename susconecta/app/{ => paciente}/resultados/ResultadosClient.tsx (93%) rename susconecta/app/{ => paciente}/resultados/page.tsx (60%) rename susconecta/components/{ => features/general}/about-section.tsx (100%) rename susconecta/{app => components/features/general}/calendarComponente/page.tsx (100%) rename susconecta/components/{ => features/general}/credentials-dialog.tsx (100%) rename susconecta/components/{ => features/general}/event-manager.tsx (100%) rename susconecta/components/{ => features/general}/hero-section.tsx (100%) rename susconecta/components/{ => providers}/theme-provider.tsx (100%) rename susconecta/components/{ => shared}/ProtectedRoute.tsx (100%) rename susconecta/components/{ => ui}/simple-theme-toggle.tsx (100%) rename susconecta/components/{ => ui}/theme-toggle.tsx (100%) diff --git a/susconecta/app/login-admin/page.tsx b/susconecta/app/(auth)/login-admin/page.tsx similarity index 100% rename from susconecta/app/login-admin/page.tsx rename to susconecta/app/(auth)/login-admin/page.tsx diff --git a/susconecta/app/login-paciente/page.tsx b/susconecta/app/(auth)/login-paciente/page.tsx similarity index 100% rename from susconecta/app/login-paciente/page.tsx rename to susconecta/app/(auth)/login-paciente/page.tsx diff --git a/susconecta/app/login/page.tsx b/susconecta/app/(auth)/login/page.tsx similarity index 100% rename from susconecta/app/login/page.tsx rename to susconecta/app/(auth)/login/page.tsx diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx index d7d6b5a..062a035 100644 --- a/susconecta/app/(main-routes)/calendar/page.tsx +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -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 diff --git a/susconecta/app/(main-routes)/layout.tsx b/susconecta/app/(main-routes)/layout.tsx index 50b946b..a673113 100644 --- a/susconecta/app/(main-routes)/layout.tsx +++ b/susconecta/app/(main-routes)/layout.tsx @@ -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"; diff --git a/susconecta/app/layout.tsx b/susconecta/app/layout.tsx index b7b71f1..4f6ef6b 100644 --- a/susconecta/app/layout.tsx +++ b/susconecta/app/layout.tsx @@ -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 = { diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index 2d012b7..da64595 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -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) diff --git a/susconecta/app/resultados/ResultadosClient.tsx b/susconecta/app/paciente/resultados/ResultadosClient.tsx similarity index 93% rename from susconecta/app/resultados/ResultadosClient.tsx rename to susconecta/app/paciente/resultados/ResultadosClient.tsx index 632668c..e179122 100644 --- a/susconecta/app/resultados/ResultadosClient.tsx +++ b/susconecta/app/paciente/resultados/ResultadosClient.tsx @@ -55,16 +55,17 @@ export default function ResultadosClient() { const params = useSearchParams() const router = useRouter() - // Filtros/controles da UI - const [tipoConsulta, setTipoConsulta] = useState( - params?.get('tipo') === 'presencial' ? 'local' : 'teleconsulta' - ) - const [especialidadeHero, setEspecialidadeHero] = useState(params?.get('especialidade') || 'Psicólogo') + // Filtros/controles da UI - initialize with defaults to avoid hydration mismatch + const [tipoConsulta, setTipoConsulta] = useState('teleconsulta') + const [especialidadeHero, setEspecialidadeHero] = useState('Psicólogo') const [convenio, setConvenio] = useState('Todos') const [bairro, setBairro] = useState('Todos') // Busca por nome do médico const [searchQuery, setSearchQuery] = useState('') + // Track if URL params have been synced to avoid race condition + const [paramsSync, setParamsSync] = useState(false) + // Estado dinâmico const [patientId, setPatientId] = useState(null) const [medicos, setMedicos] = useState([]) @@ -107,7 +108,20 @@ export default function ResultadosClient() { const [bookingSuccessOpen, setBookingSuccessOpen] = useState(false) const [bookedWhenLabel, setBookedWhenLabel] = useState(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() { 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')} > @@ -691,7 +731,7 @@ export default function ResultadosClient() { 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')} > @@ -713,7 +753,7 @@ export default function ResultadosClient() {