Refatora filtros, corrige paginacao e normaliza especialidades
This commit is contained in:
parent
b9f8efb039
commit
f848ca7376
@ -1,18 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState, useCallback, useMemo } from "react"
|
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||||
import Link from "next/link"
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
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, Loader2 } from "lucide-react";
|
||||||
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 { 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";
|
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 {
|
interface Doctor {
|
||||||
id: number;
|
id: number;
|
||||||
@ -48,34 +51,41 @@ interface DoctorDetails {
|
|||||||
export default function DoctorsPage() {
|
export default function DoctorsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
// --- Estados de Dados ---
|
||||||
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// --- Estados de Modais ---
|
||||||
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
|
||||||
const [doctorDetails, setDoctorDetails] = useState<DoctorDetails | null>(null);
|
const [doctorDetails, setDoctorDetails] = useState<DoctorDetails | null>(null);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [doctorToDeleteId, setDoctorToDeleteId] = useState<number | null>(null);
|
const [doctorToDeleteId, setDoctorToDeleteId] = useState<number | null>(null);
|
||||||
|
|
||||||
// --- Estados para Filtros ---
|
// --- Estados de Filtro e Busca ---
|
||||||
const [searchTerm, setSearchTerm] = useState(""); // <--- 2. Novo estado para a busca
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [specialtyFilter, setSpecialtyFilter] = useState("all");
|
const [filters, setFilters] = useState({
|
||||||
const [statusFilter, setStatusFilter] = useState("all");
|
specialty: "all",
|
||||||
|
status: "all"
|
||||||
|
});
|
||||||
|
|
||||||
// --- Estados para Paginação ---
|
// --- Estados de Paginação ---
|
||||||
const [itemsPerPage, setItemsPerPage] = useState(10);
|
const [itemsPerPage, setItemsPerPage] = useState(10);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
// 1. Buscar Médicos na API
|
||||||
const fetchDoctors = useCallback(async () => {
|
const fetchDoctors = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const data: Doctor[] = await doctorsService.list();
|
const data: Doctor[] = await doctorsService.list();
|
||||||
|
// Mockando status para visualização (conforme original)
|
||||||
const dataWithStatus = data.map((doc, index) => ({
|
const dataWithStatus = data.map((doc, index) => ({
|
||||||
...doc,
|
...doc,
|
||||||
status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo",
|
status: index % 3 === 0 ? "Inativo" : index % 2 === 0 ? "Férias" : "Ativo",
|
||||||
}));
|
}));
|
||||||
setDoctors(dataWithStatus || []);
|
setDoctors(dataWithStatus || []);
|
||||||
setCurrentPage(1);
|
// Não resetamos a página aqui para manter a navegação fluida se apenas recarregar dados
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Erro ao carregar lista de médicos:", e);
|
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.");
|
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();
|
||||||
}, [fetchDoctors]);
|
}, [fetchDoctors]);
|
||||||
|
|
||||||
const openDetailsDialog = async (doctor: Doctor) => {
|
// 2. Gerar lista única de especialidades (Normalizada)
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const uniqueSpecialties = useMemo(() => {
|
const uniqueSpecialties = useMemo(() => {
|
||||||
const specialties = doctors.map((doctor) => doctor.specialty).filter(Boolean);
|
return getUniqueSpecialties(doctors);
|
||||||
return [...new Set(specialties)];
|
|
||||||
}, [doctors]);
|
}, [doctors]);
|
||||||
|
|
||||||
// --- 3. Atualização da Lógica de Filtragem ---
|
// 3. Lógica de Filtragem Centralizada
|
||||||
const filteredDoctors = doctors.filter((doctor) => {
|
const filteredDoctors = useMemo(() => {
|
||||||
const specialtyMatch = specialtyFilter === "all" || doctor.specialty === specialtyFilter;
|
return doctors.filter((doctor) => {
|
||||||
const statusMatch = statusFilter === "all" || doctor.status === statusFilter;
|
// Normaliza a especialidade do médico atual para comparar
|
||||||
|
const normalizedDocSpecialty = normalizeSpecialty(doctor.specialty);
|
||||||
// Lógica da barra de pesquisa
|
|
||||||
const searchLower = searchTerm.toLowerCase();
|
// Filtros exatos
|
||||||
const nameMatch = doctor.full_name?.toLowerCase().includes(searchLower);
|
const specialtyMatch = filters.specialty === "all" || normalizedDocSpecialty === filters.specialty;
|
||||||
const phoneMatch = doctor.phone_mobile?.includes(searchLower);
|
const statusMatch = filters.status === "all" || doctor.status === filters.status;
|
||||||
// Opcional: buscar também por CRM se desejar
|
|
||||||
const crmMatch = doctor.crm?.toLowerCase().includes(searchLower);
|
// 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 totalPages = Math.ceil(filteredDoctors.length / itemsPerPage);
|
||||||
const indexOfLastItem = currentPage * itemsPerPage;
|
const indexOfLastItem = currentPage * itemsPerPage;
|
||||||
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
|
||||||
const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem);
|
const currentItems = filteredDoctors.slice(indexOfFirstItem, indexOfLastItem);
|
||||||
|
|
||||||
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
|
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
|
||||||
|
const goToPrevPage = () => setCurrentPage((prev) => Math.max(1, prev - 1));
|
||||||
const goToPrevPage = () => {
|
const goToNextPage = () => setCurrentPage((prev) => Math.min(totalPages, prev + 1));
|
||||||
setCurrentPage((prev) => Math.max(1, prev - 1));
|
|
||||||
};
|
|
||||||
|
|
||||||
const goToNextPage = () => {
|
|
||||||
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVisiblePageNumbers = (totalPages: number, currentPage: number) => {
|
const getVisiblePageNumbers = (totalPages: number, currentPage: number) => {
|
||||||
const pages: number[] = [];
|
const pages: number[] = [];
|
||||||
@ -186,9 +181,42 @@ export default function DoctorsPage() {
|
|||||||
|
|
||||||
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
|
const visiblePageNumbers = getVisiblePageNumbers(totalPages, currentPage);
|
||||||
|
|
||||||
const handleItemsPerPageChange = (value: string) => {
|
// --- Handlers de Ações (Detalhes e Delete) ---
|
||||||
setItemsPerPage(Number(value));
|
const openDetailsDialog = (doctor: Doctor) => {
|
||||||
setCurrentPage(1);
|
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 (
|
return (
|
||||||
@ -202,67 +230,43 @@ export default function DoctorsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* --- Filtros e Barra de Pesquisa Atualizada --- */}
|
{/* --- NOVO COMPONENTE DE FILTRO --- */}
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-3 bg-white p-3 sm:p-4 rounded-lg border border-gray-200">
|
<FilterBar
|
||||||
|
searchTerm={searchTerm}
|
||||||
{/* Barra de Pesquisa (Estilo similar à foto) */}
|
onSearch={handleSearch}
|
||||||
<div className="relative w-full md:flex-1">
|
activeFilters={filters}
|
||||||
<Filter className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
onFilterChange={handleFilterChange}
|
||||||
<Input
|
onClearFilters={handleClearFilters}
|
||||||
placeholder="Buscar por nome ou telefone..."
|
searchPlaceholder="Buscar por nome, CRM ou telefone..."
|
||||||
value={searchTerm}
|
filters={[
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
{
|
||||||
className="pl-10 w-full bg-gray-50 border-gray-200 focus:bg-white transition-colors"
|
key: "specialty",
|
||||||
/>
|
label: "Especialidade",
|
||||||
|
options: uniqueSpecialties
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "Status",
|
||||||
|
options: ["Ativo", "Férias", "Inativo"]
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Seletor de Itens por Página (Filho do FilterBar) */}
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<Select onValueChange={handleItemsPerPageChange} defaultValue={String(itemsPerPage)}>
|
||||||
|
<SelectTrigger className="w-[70px]">
|
||||||
|
<SelectValue placeholder="10" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="5">5</SelectItem>
|
||||||
|
<SelectItem value="10">10</SelectItem>
|
||||||
|
<SelectItem value="20">20</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
</FilterBar>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-3 w-full md:w-auto">
|
{/* Tabela de Médicos */}
|
||||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
||||||
<Select value={specialtyFilter} onValueChange={setSpecialtyFilter}>
|
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
|
||||||
<SelectValue placeholder="Especialidade" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Todas Especialidades</SelectItem>
|
|
||||||
{uniqueSpecialties.map((specialty) => (
|
|
||||||
<SelectItem key={specialty} value={specialty}>
|
|
||||||
{specialty}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
||||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
|
||||||
<SelectTrigger className="w-full sm:w-[150px]">
|
|
||||||
<SelectValue placeholder="Status" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="all">Status: Todos</SelectItem>
|
|
||||||
<SelectItem value="Ativo">Ativo</SelectItem>
|
|
||||||
<SelectItem value="Férias">Férias</SelectItem>
|
|
||||||
<SelectItem value="Inativo">Inativo</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="hidden lg:block">
|
|
||||||
<Select onValueChange={handleItemsPerPageChange} defaultValue={String(itemsPerPage)}>
|
|
||||||
<SelectTrigger className="w-[70px]">
|
|
||||||
<SelectValue placeholder="10" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="5">5</SelectItem>
|
|
||||||
<SelectItem value="10">10</SelectItem>
|
|
||||||
<SelectItem value="20">20</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Tabela de Médicos (Visível em Telas Médias e Maiores) */}
|
|
||||||
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden hidden md:block">
|
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden hidden md:block">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-8 text-center text-gray-500">
|
<div className="p-8 text-center text-gray-500">
|
||||||
@ -296,10 +300,12 @@ export default function DoctorsPage() {
|
|||||||
<tr key={doctor.id} className="hover:bg-gray-50 transition">
|
<tr key={doctor.id} className="hover:bg-gray-50 transition">
|
||||||
<td className="px-4 py-3 font-medium text-gray-900">
|
<td className="px-4 py-3 font-medium text-gray-900">
|
||||||
{doctor.full_name}
|
{doctor.full_name}
|
||||||
<div className="text-xs text-gray-400 md:hidden">{doctor.phone_mobile}</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-500 hidden sm:table-cell">{doctor.crm}</td>
|
<td className="px-4 py-3 text-gray-500 hidden sm:table-cell">{doctor.crm}</td>
|
||||||
<td className="px-4 py-3 text-gray-500 hidden md:table-cell">{doctor.specialty}</td>
|
<td className="px-4 py-3 text-gray-500 hidden md:table-cell">
|
||||||
|
{/* Exibe Especialidade Normalizada */}
|
||||||
|
{normalizeSpecialty(doctor.specialty)}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-500 hidden lg:table-cell">
|
<td className="px-4 py-3 text-gray-500 hidden lg:table-cell">
|
||||||
<span className={`px-2 py-1 rounded-full text-xs ${
|
<span className={`px-2 py-1 rounded-full text-xs ${
|
||||||
doctor.status === 'Ativo' ? 'bg-green-100 text-green-800' :
|
doctor.status === 'Ativo' ? 'bg-green-100 text-green-800' :
|
||||||
@ -348,7 +354,7 @@ export default function DoctorsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cards de Médicos (Visível Apenas em Telas Pequenas) */}
|
{/* Cards de Médicos (Mobile) */}
|
||||||
<div className="bg-white rounded-lg border border-gray-200 shadow-md p-4 block md:hidden">
|
<div className="bg-white rounded-lg border border-gray-200 shadow-md p-4 block md:hidden">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="p-8 text-center text-gray-500">
|
<div className="p-8 text-center text-gray-500">
|
||||||
@ -371,7 +377,7 @@ export default function DoctorsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="font-semibold text-gray-900">{doctor.full_name}</div>
|
<div className="font-semibold text-gray-900">{doctor.full_name}</div>
|
||||||
<div className="text-xs text-gray-500 mb-1">{doctor.phone_mobile}</div>
|
<div className="text-xs text-gray-500 mb-1">{doctor.phone_mobile}</div>
|
||||||
<div className="text-sm text-gray-600">{doctor.specialty}</div>
|
<div className="text-sm text-gray-600">{normalizeSpecialty(doctor.specialty)}</div>
|
||||||
<div className="text-xs mt-1">
|
<div className="text-xs mt-1">
|
||||||
<span className={`px-2 py-0.5 rounded-full text-xs ${
|
<span className={`px-2 py-0.5 rounded-full text-xs ${
|
||||||
doctor.status === 'Ativo' ? 'bg-green-100 text-green-800' :
|
doctor.status === 'Ativo' ? 'bg-green-100 text-green-800' :
|
||||||
@ -446,7 +452,7 @@ export default function DoctorsPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Dialogs (Exclusão e Detalhes) mantidos igual ao original... */}
|
{/* Dialogs (Exclusão e Detalhes) */}
|
||||||
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
|
|||||||
115
components/ui/filter-bar.tsx
Normal file
115
components/ui/filter-bar.tsx
Normal file
@ -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<string, string>;
|
||||||
|
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 (
|
||||||
|
<div className={`flex flex-col md:flex-row items-start md:items-center gap-3 bg-white p-4 rounded-lg border border-gray-200 ${className}`}>
|
||||||
|
|
||||||
|
{/* Barra de Pesquisa */}
|
||||||
|
<div className="relative w-full md:flex-1">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
|
<Input
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => onSearch(e.target.value)}
|
||||||
|
className="pl-10 w-full bg-gray-50 border-gray-200 focus:bg-white transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filtros Dinâmicos (Selects) */}
|
||||||
|
<div className="flex flex-wrap items-center gap-3 w-full md:w-auto">
|
||||||
|
{filters.map((filter) => (
|
||||||
|
<div key={filter.key} className="w-full sm:w-auto">
|
||||||
|
<Select
|
||||||
|
value={activeFilters[filter.key] || "all"}
|
||||||
|
onValueChange={(value) => onFilterChange(filter.key, value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
|
<SelectValue placeholder={filter.label} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Todos: {filter.label}</SelectItem>
|
||||||
|
{filter.options.map((opt) => {
|
||||||
|
// Suporta tanto array de strings quanto array de objetos {label, value}
|
||||||
|
const value = typeof opt === 'string' ? opt : opt.value;
|
||||||
|
const label = typeof opt === 'string' ? opt : opt.label;
|
||||||
|
return (
|
||||||
|
<SelectItem key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Botão de Limpar Filtros */}
|
||||||
|
{hasActiveFilters && onClearFilters && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={onClearFilters}
|
||||||
|
className="text-gray-500 hover:text-red-600"
|
||||||
|
title="Limpar filtros"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Botões Extras (ex: Novo Médico, Paginação) passados como children */}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
94
lib/normalization.ts
Normal file
94
lib/normalization.ts
Normal file
@ -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<string, string> = {
|
||||||
|
// --- 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<string>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user