diff --git a/app/manager/home/page.tsx b/app/manager/home/page.tsx index fe49726..db15f2d 100644 --- a/app/manager/home/page.tsx +++ b/app/manager/home/page.tsx @@ -1,18 +1,21 @@ "use client"; -import React, { useEffect, useState, useCallback, useMemo } from "react" -import Link from "next/link" +import React, { useEffect, useState, useCallback, useMemo } from "react"; +import Link from "next/link"; import { useRouter } from "next/navigation"; -import { Button } from "@/components/ui/button" -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { Input } from "@/components/ui/input" // <--- 1. Importação adicionada -import { Edit, Trash2, Eye, Calendar, Filter, Loader2, Search } from "lucide-react" // <--- Adicionado ícone Search -import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Edit, Trash2, Eye, Calendar, Loader2 } from "lucide-react"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; -import { doctorsService } from "services/doctorsApi.mjs"; +// Imports dos Serviços +import { doctorsService } from "@/services/doctorsApi.mjs"; import Sidebar from "@/components/Sidebar"; +// --- NOVOS IMPORTS (Certifique-se que criou os arquivos no passo anterior) --- +import { FilterBar } from "@/components/ui/filter-bar"; +import { normalizeSpecialty, getUniqueSpecialties } from "@/lib/normalization"; interface Doctor { id: number; @@ -48,34 +51,41 @@ interface DoctorDetails { export default function DoctorsPage() { const router = useRouter(); + // --- Estados de Dados --- const [doctors, setDoctors] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + + // --- Estados de Modais --- const [detailsDialogOpen, setDetailsDialogOpen] = useState(false); const [doctorDetails, setDoctorDetails] = useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [doctorToDeleteId, setDoctorToDeleteId] = useState(null); - // --- Estados para Filtros --- - const [searchTerm, setSearchTerm] = useState(""); // <--- 2. Novo estado para a busca - const [specialtyFilter, setSpecialtyFilter] = useState("all"); - const [statusFilter, setStatusFilter] = useState("all"); + // --- Estados de Filtro e Busca --- + const [searchTerm, setSearchTerm] = useState(""); + const [filters, setFilters] = useState({ + specialty: "all", + status: "all" + }); - // --- Estados para Paginação --- + // --- Estados de Paginação --- const [itemsPerPage, setItemsPerPage] = useState(10); const [currentPage, setCurrentPage] = useState(1); + // 1. Buscar Médicos na API const fetchDoctors = useCallback(async () => { setLoading(true); setError(null); try { const data: Doctor[] = await doctorsService.list(); + // Mockando status para visualização (conforme original) const dataWithStatus = data.map((doc, index) => ({ ...doc, status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo", })); setDoctors(dataWithStatus || []); - setCurrentPage(1); + // Não resetamos a página aqui para manter a navegação fluida se apenas recarregar dados } catch (e: any) { console.error("Erro ao carregar lista de médicos:", e); setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API."); @@ -89,78 +99,63 @@ export default function DoctorsPage() { fetchDoctors(); }, [fetchDoctors]); - const openDetailsDialog = async (doctor: Doctor) => { - setDetailsDialogOpen(true); - setDoctorDetails({ - nome: doctor.full_name, - crm: doctor.crm, - especialidade: doctor.specialty, - contato: { celular: doctor.phone_mobile ?? undefined }, - endereco: { cidade: doctor.city ?? undefined, estado: doctor.state ?? undefined }, - status: doctor.status || "Ativo", - convenio: "Particular", - vip: false, - ultimo_atendimento: "N/A", - proximo_atendimento: "N/A", - }); - }; - - const handleDelete = async () => { - if (doctorToDeleteId === null) return; - setLoading(true); - try { - await doctorsService.delete(doctorToDeleteId); - setDeleteDialogOpen(false); - setDoctorToDeleteId(null); - await fetchDoctors(); - } catch (e) { - console.error("Erro ao excluir:", e); - alert("Erro ao excluir médico."); - } finally { - setLoading(false); - } - }; - - const openDeleteDialog = (doctorId: number) => { - setDoctorToDeleteId(doctorId); - setDeleteDialogOpen(true); - }; - + // 2. Gerar lista única de especialidades (Normalizada) const uniqueSpecialties = useMemo(() => { - const specialties = doctors.map((doctor) => doctor.specialty).filter(Boolean); - return [...new Set(specialties)]; + return getUniqueSpecialties(doctors); }, [doctors]); - // --- 3. Atualização da Lógica de Filtragem --- - const filteredDoctors = doctors.filter((doctor) => { - const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter; - const statusMatch = statusFilter === "all" || doctor.status === statusFilter; - - // Lógica da barra de pesquisa - const searchLower = searchTerm.toLowerCase(); - const nameMatch = doctor.full_name?.toLowerCase().includes(searchLower); - const phoneMatch = doctor.phone_mobile?.includes(searchLower); - // Opcional: buscar também por CRM se desejar - const crmMatch = doctor.crm?.toLowerCase().includes(searchLower); + // 3. Lógica de Filtragem Centralizada + const filteredDoctors = useMemo(() => { + return doctors.filter((doctor) => { + // Normaliza a especialidade do médico atual para comparar + const normalizedDocSpecialty = normalizeSpecialty(doctor.specialty); + + // Filtros exatos + const specialtyMatch = filters.specialty === "all" || normalizedDocSpecialty === filters.specialty; + const statusMatch = filters.status === "all" || doctor.status === filters.status; + + // Busca textual (Nome, Telefone, CRM) + const searchLower = searchTerm.toLowerCase(); + const nameMatch = doctor.full_name?.toLowerCase().includes(searchLower); + const phoneMatch = doctor.phone_mobile?.includes(searchLower); + const crmMatch = doctor.crm?.toLowerCase().includes(searchLower); - const searchMatch = searchTerm === "" || nameMatch || phoneMatch || crmMatch; + return specialtyMatch && statusMatch && (searchTerm === "" || nameMatch || phoneMatch || crmMatch); + }); + }, [doctors, filters, searchTerm]); - return specialtyMatch && statusMatch && searchMatch; - }); + // --- Handlers de Controle (Com Reset de Paginação) --- + const handleSearch = (term: string) => { + setSearchTerm(term); + setCurrentPage(1); // Correção: Reseta para página 1 ao buscar + }; + + const handleFilterChange = (key: string, value: string) => { + setFilters(prev => ({ ...prev, [key]: value })); + setCurrentPage(1); // Correção: Reseta para página 1 ao filtrar + }; + + const handleClearFilters = () => { + setSearchTerm(""); + setFilters({ specialty: "all", status: "all" }); + setCurrentPage(1); // Correção: Reseta para página 1 ao limpar + }; + + const handleItemsPerPageChange = (value: string) => { + setItemsPerPage(Number(value)); + setCurrentPage(1); + }; + + // --- Lógica de Paginação --- const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage); const indexOfLastItem = currentPage * itemsPerPage; const indexOfFirstItem = indexOfLastItem - itemsPerPage; const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem); + const paginate = (pageNumber: number) => setCurrentPage(pageNumber); - - const goToPrevPage = () => { - setCurrentPage((prev) => Math.max(1, prev - 1)); - }; - - const goToNextPage = () => { - setCurrentPage((prev) => Math.min(totalPages, prev + 1)); - }; + const goToPrevPage = () => setCurrentPage((prev) => Math.max(1, prev - 1)); + const goToNextPage = () => setCurrentPage((prev) => Math.min(totalPages, prev + 1)); const getVisiblePageNumbers = (totalPages: number, currentPage: number) => { const pages: number[] = []; @@ -186,9 +181,42 @@ export default function DoctorsPage() { const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage); - const handleItemsPerPageChange = (value: string) => { - setItemsPerPage(Number(value)); - setCurrentPage(1); + // --- Handlers de Ações (Detalhes e Delete) --- + const openDetailsDialog = (doctor: Doctor) => { + setDetailsDialogOpen(true); + setDoctorDetails({ + nome: doctor.full_name, + crm: doctor.crm, + especialidade: normalizeSpecialty(doctor.specialty), // Exibe normalizado + contato: { celular: doctor.phone_mobile ?? undefined }, + endereco: { cidade: doctor.city ?? undefined, estado: doctor.state ?? undefined }, + status: doctor.status || "Ativo", + convenio: "Particular", + vip: false, + ultimo_atendimento: "N/A", + proximo_atendimento: "N/A", + }); + }; + + const openDeleteDialog = (doctorId: number) => { + setDoctorToDeleteId(doctorId); + setDeleteDialogOpen(true); + }; + + const handleDelete = async () => { + if (doctorToDeleteId === null) return; + setLoading(true); + try { + await doctorsService.delete(doctorToDeleteId); + setDeleteDialogOpen(false); + setDoctorToDeleteId(null); + await fetchDoctors(); + } catch (e) { + console.error("Erro ao excluir:", e); + alert("Erro ao excluir médico."); + } finally { + setLoading(false); + } }; return ( @@ -202,67 +230,43 @@ export default function DoctorsPage() { - {/* --- Filtros e Barra de Pesquisa Atualizada --- */} -
- - {/* Barra de Pesquisa (Estilo similar à foto) */} -
- - setSearchTerm(e.target.value)} - className="pl-10 w-full bg-gray-50 border-gray-200 focus:bg-white transition-colors" - /> + {/* --- NOVO COMPONENTE DE FILTRO --- */} + + {/* Seletor de Itens por Página (Filho do FilterBar) */} +
+
+
-
-
- -
- -
- -
- -
- -
-
-
- - {/* Tabela de Médicos (Visível em Telas Médias e Maiores) */} + {/* Tabela de Médicos */}
{loading ? (
@@ -296,10 +300,12 @@ export default function DoctorsPage() { {doctor.full_name} -
{doctor.phone_mobile}
{doctor.crm} - {doctor.specialty} + + {/* Exibe Especialidade Normalizada */} + {normalizeSpecialty(doctor.specialty)} + - {/* Cards de Médicos (Visível Apenas em Telas Pequenas) */} + {/* Cards de Médicos (Mobile) */}
{loading ? (
@@ -371,7 +377,7 @@ export default function DoctorsPage() {
{doctor.full_name}
{doctor.phone_mobile}
-
{doctor.specialty}
+
{normalizeSpecialty(doctor.specialty)}
)} - {/* Dialogs (Exclusão e Detalhes) mantidos igual ao original... */} + {/* Dialogs (Exclusão e Detalhes) */} diff --git a/components/ui/filter-bar.tsx b/components/ui/filter-bar.tsx new file mode 100644 index 0000000..d7064b0 --- /dev/null +++ b/components/ui/filter-bar.tsx @@ -0,0 +1,115 @@ +"use client"; + +import React from "react"; +import { Search, Filter, X } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +export interface FilterOption { + label: string; + value: string; +} + +export interface FilterConfig { + key: string; // O nome do estado que vai guardar esse valor (ex: 'specialty') + label: string; // O placeholder do select (ex: 'Especialidade') + options: FilterOption[] | string[]; // Opções do dropdown +} + +interface FilterBarProps { + onSearch: (term: string) => void; + searchTerm: string; + searchPlaceholder?: string; + filters?: FilterConfig[]; + activeFilters: Record; + onFilterChange: (key: string, value: string) => void; + onClearFilters?: () => void; + className?: string; + children?: React.ReactNode; // Para botões extras (ex: "Novo Médico", paginação) +} + +export function FilterBar({ + onSearch, + searchTerm, + searchPlaceholder = "Pesquisar...", + filters = [], + activeFilters, + onFilterChange, + onClearFilters, + children, + className, +}: FilterBarProps) { + + // Verifica se tem algum filtro ativo para mostrar o botão de limpar + const hasActiveFilters = + searchTerm !== "" || + Object.values(activeFilters).some(val => val !== "all" && val !== ""); + + return ( +
+ + {/* Barra de Pesquisa */} +
+ + onSearch(e.target.value)} + className="pl-10 w-full bg-gray-50 border-gray-200 focus:bg-white transition-colors" + /> +
+ + {/* Filtros Dinâmicos (Selects) */} +
+ {filters.map((filter) => ( +
+ +
+ ))} + + {/* Botão de Limpar Filtros */} + {hasActiveFilters && onClearFilters && ( + + )} + + {/* Botões Extras (ex: Novo Médico, Paginação) passados como children */} + {children} +
+
+ ); +} \ No newline at end of file diff --git a/lib/normalization.ts b/lib/normalization.ts new file mode 100644 index 0000000..038b23a --- /dev/null +++ b/lib/normalization.ts @@ -0,0 +1,94 @@ +// lib/normalization.ts + +/** + * Mapa de normalização. + * A chave é o termo "sujo" (em minúsculo) e o valor é o termo "Canônico" (Bonito). + */ +const SPECIALTY_MAPPING: Record = { + // --- Cardiologia --- + "cardiologista": "Cardiologia", + "cardio": "Cardiologia", + "cardiologia": "Cardiologia", + + // --- Dermatologia --- + "dermatologista": "Dermatologia", + "dermato": "Dermatologia", + "dermatologia": "Dermatologia", + + // --- Ortopedia --- + "ortopedista": "Ortopedia", + "ortopedia": "Ortopedia", + + // --- Ginecologia --- + "ginecologista": "Ginecologia", + "ginecologia": "Ginecologia", + "ginecologistaa": "Ginecologia", // Erro de digitação comum + "gineco": "Ginecologia", + + // --- Pediatria --- + "pediatra": "Pediatria", + "pediatria": "Pediatria", + + // --- Clínica Geral (Onde estava o erro) --- + "clinico geral": "Clínica Geral", + "clínico geral": "Clínica Geral", + "clinica geral": "Clínica Geral", + "clínica geral": "Clínica Geral", // <--- ADICIONADO + "geral": "Clínica Geral", + "medico geral": "Clínica Geral", + "médico geral": "Clínica Geral", + + // --- Neurologia --- + "neurologista": "Neurologia", + "neurologia": "Neurologia", + "neuro": "Neurologia", + "neurocirurgiao": "Neurocirurgia", + "neurocirurgião": "Neurocirurgia", + + // --- Limpeza de Lixo / Outros --- + "asdw": "Outros", + "teste": "Outros", + "n/a": "Não Informado", // <--- Transforma o "N/A" da imagem + "na": "Não Informado", +}; + +/** + * Recebe uma especialidade suja e retorna a versão limpa. + */ +export function normalizeSpecialty(raw: string | null | undefined): string { + if (!raw) return "Não Informado"; + + // Remove espaços extras e joga para minúsculo + const lower = raw.trim().toLowerCase(); + + // Se for uma string vazia ou traço + if (lower === "" || lower === "-") return "Não Informado"; + + // Verifica no mapa + if (SPECIALTY_MAPPING[lower]) { + return SPECIALTY_MAPPING[lower]; + } + + // Fallback: Capitaliza a primeira letra de cada palavra + // Ex: "cirurgia plastica" -> "Cirurgia Plastica" + return lower.replace(/\b\w/g, (l) => l.toUpperCase()); +} + +/** + * Extrai uma lista única de especialidades normalizadas. + */ +export function getUniqueSpecialties(items: any[]): string[] { + const specialties = new Set(); + + items.forEach(item => { + // Normaliza antes de adicionar ao Set + const normalized = normalizeSpecialty(item.specialty); + + // Só adiciona se não for "Não Informado" ou "Outros" (Opcional: remova o if se quiser mostrar tudo) + if (normalized && normalized !== "Não Informado") { + specialties.add(normalized); + } + }); + + return Array.from(specialties).sort(); +} \ No newline at end of file