develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
2 changed files with 256 additions and 63 deletions
Showing only changes of commit add30c54a3 - Show all commits

View File

@ -145,7 +145,12 @@ export default function DoutoresPage() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10); const [itemsPerPage, setItemsPerPage] = useState(10);
// NOVO: Ordenação e filtros
const [sortBy, setSortBy] = useState<"name_asc" | "name_desc" | "recent" | "oldest">("name_asc");
const [stateFilter, setStateFilter] = useState<string>("");
const [cityFilter, setCityFilter] = useState<string>("");
const [specialtyFilter, setSpecialtyFilter] = useState<string>("");
async function load() { async function load() {
setLoading(true); setLoading(true);
try { try {
@ -272,47 +277,87 @@ export default function DoutoresPage() {
}; };
}, [searchTimeout]); }, [searchTimeout]);
// Lista de médicos a exibir (busca ou filtro local) // NOVO: Opções dinâmicas
const stateOptions = useMemo(
() =>
Array.from(
new Set((doctors || []).map((d) => (d.state || "").trim()).filter(Boolean)),
).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" })),
[doctors],
);
const cityOptions = useMemo(() => {
const base = (doctors || []).filter((d) => !stateFilter || String(d.state) === stateFilter);
return Array.from(
new Set(base.map((d) => (d.city || "").trim()).filter(Boolean)),
).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" }));
}, [doctors, stateFilter]);
const specialtyOptions = useMemo(
() =>
Array.from(
new Set((doctors || []).map((d) => (d.especialidade || "").trim()).filter(Boolean)),
).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" })),
[doctors],
);
// NOVO: Índice para ordenação por "tempo" (ordem de carregamento)
const indexById = useMemo(() => {
const map = new Map<string, number>();
(doctors || []).forEach((d, i) => map.set(String(d.id), i));
return map;
}, [doctors]);
// Lista de médicos a exibir com busca + filtros + ordenação
const displayedDoctors = useMemo(() => { const displayedDoctors = useMemo(() => {
console.log('🔍 Filtro - search:', search, 'searchMode:', searchMode, 'doctors:', doctors.length, 'searchResults:', searchResults.length); console.log('🔍 Filtro - search:', search, 'searchMode:', searchMode, 'doctors:', doctors.length, 'searchResults:', searchResults.length);
// Se não tem busca, mostra todos os médicos
if (!search.trim()) return doctors;
const q = search.toLowerCase().trim(); const q = search.toLowerCase().trim();
const qDigits = q.replace(/\D/g, ""); const qDigits = q.replace(/\D/g, "");
// Se estamos em modo de busca (servidor), filtra os resultados da busca
const sourceList = searchMode ? searchResults : doctors; const sourceList = searchMode ? searchResults : doctors;
console.log('🔍 Usando sourceList:', searchMode ? 'searchResults' : 'doctors', '- tamanho:', sourceList.length);
// 1) Busca
const filtered = sourceList.filter((d) => { const afterSearch = !q
// Busca por nome ? sourceList
const byName = (d.full_name || "").toLowerCase().includes(q); : sourceList.filter((d) => {
const byName = (d.full_name || "").toLowerCase().includes(q);
// Busca por CRM (remove formatação se necessário) const byCrm = qDigits.length >= 3 && (d.crm || "").replace(/\D/g, "").includes(qDigits);
const byCrm = qDigits.length >= 3 && (d.crm || "").replace(/\D/g, "").includes(qDigits); const byId = (d.id || "").toLowerCase().includes(q);
const byEmail = (d.email || "").toLowerCase().includes(q);
// Busca por ID (UUID completo ou parcial) const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q);
const byId = (d.id || "").toLowerCase().includes(q); const match = byName || byCrm || byId || byEmail || byEspecialidade;
if (match) console.log('✅ Match encontrado:', d.full_name, d.id);
// Busca por email return match;
const byEmail = (d.email || "").toLowerCase().includes(q); });
// Busca por especialidade // 2) Filtros de localização e especialidade
const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q); const afterFilters = afterSearch.filter((d) => {
if (stateFilter && String(d.state) !== stateFilter) return false;
const match = byName || byCrm || byId || byEmail || byEspecialidade; if (cityFilter && String(d.city) !== cityFilter) return false;
if (match) { if (specialtyFilter && String(d.especialidade) !== specialtyFilter) return false;
console.log('✅ Match encontrado:', d.full_name, d.id, 'por:', { byName, byCrm, byId, byEmail, byEspecialidade }); return true;
}
return match;
}); });
console.log('🔍 Resultados filtrados:', filtered.length); // 3) Ordenação
return filtered; const sorted = [...afterFilters];
}, [doctors, search, searchMode, searchResults]); if (sortBy === "name_asc" || sortBy === "name_desc") {
sorted.sort((a, b) => {
const an = (a.full_name || "").trim();
const bn = (b.full_name || "").trim();
const cmp = an.localeCompare(bn, "pt-BR", { sensitivity: "base" });
return sortBy === "name_asc" ? cmp : -cmp;
});
} else if (sortBy === "recent" || sortBy === "oldest") {
sorted.sort((a, b) => {
const ia = indexById.get(String(a.id)) ?? 0;
const ib = indexById.get(String(b.id)) ?? 0;
return sortBy === "recent" ? ia - ib : ib - ia;
});
}
console.log('🔍 Resultados filtrados:', sorted.length);
return sorted;
}, [doctors, search, searchMode, searchResults, stateFilter, cityFilter, specialtyFilter, sortBy, indexById]);
// Dados paginados // Dados paginados
const paginatedDoctors = useMemo(() => { const paginatedDoctors = useMemo(() => {
@ -323,10 +368,10 @@ export default function DoutoresPage() {
const totalPages = Math.ceil(displayedDoctors.length / itemsPerPage); const totalPages = Math.ceil(displayedDoctors.length / itemsPerPage);
// Reset para página 1 quando mudar a busca ou itens por página // Reset página ao mudar busca/filtros/ordenação
useEffect(() => { useEffect(() => {
setCurrentPage(1); setCurrentPage(1);
}, [search, itemsPerPage, searchMode]); }, [search, itemsPerPage, searchMode, stateFilter, cityFilter, specialtyFilter, sortBy]);
function handleAdd() { function handleAdd() {
setEditingId(null); setEditingId(null);
@ -440,7 +485,7 @@ export default function DoutoresPage() {
<p className="text-muted-foreground">Gerencie os médicos da sua clínica</p> <p className="text-muted-foreground">Gerencie os médicos da sua clínica</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-wrap">
<div className="flex gap-2"> <div className="flex gap-2">
<div className="relative"> <div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
@ -473,6 +518,59 @@ export default function DoutoresPage() {
</Button> </Button>
)} )}
</div> </div>
{/* NOVO: Ordenar por */}
<select
aria-label="Ordenar por"
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="name_asc">Nome (AZ)</option>
<option value="name_desc">Nome (ZA)</option>
<option value="recent">Mais recentes (carregamento)</option>
<option value="oldest">Mais antigos (carregamento)</option>
</select>
{/* NOVO: Especialidade */}
<select
aria-label="Filtrar por especialidade"
value={specialtyFilter}
onChange={(e) => setSpecialtyFilter(e.target.value)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todas as especialidades</option>
{specialtyOptions.map((sp) => (
<option key={sp} value={sp}>{sp}</option>
))}
</select>
{/* NOVO: Estado (UF) */}
<select
aria-label="Filtrar por estado"
value={stateFilter}
onChange={(e) => { setStateFilter(e.target.value); setCityFilter(""); }}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todos os estados</option>
{stateOptions.map((uf) => (
<option key={uf} value={uf}>{uf}</option>
))}
</select>
{/* NOVO: Cidade (dependente do estado) */}
<select
aria-label="Filtrar por cidade"
value={cityFilter}
onChange={(e) => setCityFilter(e.target.value)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todas as cidades</option>
{cityOptions.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
<Button onClick={handleAdd} disabled={loading}> <Button onClick={handleAdd} disabled={loading}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Novo Médico Novo Médico

View File

@ -1,4 +1,3 @@
"use client"; "use client";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@ -54,6 +53,11 @@ export default function PacientesPage() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10); const [itemsPerPage, setItemsPerPage] = useState(10);
// Ordenação e filtros adicionais
const [sortBy, setSortBy] = useState<"name_asc" | "name_desc" | "recent" | "oldest">("name_asc");
const [stateFilter, setStateFilter] = useState<string>("");
const [cityFilter, setCityFilter] = useState<string>("");
async function loadAll() { async function loadAll() {
try { try {
setLoading(true); setLoading(true);
@ -77,27 +81,72 @@ export default function PacientesPage() {
loadAll(); loadAll();
}, []); }, []);
// Opções dinâmicas para Estado e Cidade
const stateOptions = useMemo(
() =>
Array.from(
new Set((patients || []).map((p) => (p.state || "").trim()).filter(Boolean)),
).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" })),
[patients],
);
const cityOptions = useMemo(() => {
const base = (patients || []).filter((p) => !stateFilter || String(p.state) === stateFilter);
return Array.from(
new Set(base.map((p) => (p.city || "").trim()).filter(Boolean)),
).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" }));
}, [patients, stateFilter]);
// Índice para ordenar por "tempo" (ordem de carregamento)
const indexById = useMemo(() => {
const map = new Map<string, number>();
(patients || []).forEach((p, i) => map.set(String(p.id), i));
return map;
}, [patients]);
// Substitui o filtered anterior: aplica busca + filtros + ordenação
const filtered = useMemo(() => { const filtered = useMemo(() => {
if (!search.trim()) return patients; let base = patients;
const q = search.toLowerCase().trim();
const qDigits = q.replace(/\D/g, ""); // Busca
if (search.trim()) {
return patients.filter((p) => { const q = search.toLowerCase().trim();
// Busca por nome const qDigits = q.replace(/\D/g, "");
const byName = (p.full_name || "").toLowerCase().includes(q); base = patients.filter((p) => {
const byName = (p.full_name || "").toLowerCase().includes(q);
// Busca por CPF (remove formatação) const byCPF = qDigits.length >= 3 && (p.cpf || "").replace(/\D/g, "").includes(qDigits);
const byCPF = qDigits.length >= 3 && (p.cpf || "").replace(/\D/g, "").includes(qDigits); const byId = (p.id || "").toLowerCase().includes(q);
const byEmail = (p.email || "").toLowerCase().includes(q);
// Busca por ID (UUID completo ou parcial) return byName || byCPF || byId || byEmail;
const byId = (p.id || "").toLowerCase().includes(q); });
}
// Busca por email
const byEmail = (p.email || "").toLowerCase().includes(q); // Filtros por UF e cidade
const withLocation = base.filter((p) => {
return byName || byCPF || byId || byEmail; if (stateFilter && String(p.state) !== stateFilter) return false;
if (cityFilter && String(p.city) !== cityFilter) return false;
return true;
}); });
}, [patients, search]);
// Ordenação
const sorted = [...withLocation];
if (sortBy === "name_asc" || sortBy === "name_desc") {
sorted.sort((a, b) => {
const an = (a.full_name || "").trim();
const bn = (b.full_name || "").trim();
const cmp = an.localeCompare(bn, "pt-BR", { sensitivity: "base" });
return sortBy === "name_asc" ? cmp : -cmp;
});
} else if (sortBy === "recent" || sortBy === "oldest") {
sorted.sort((a, b) => {
const ia = indexById.get(String(a.id)) ?? 0;
const ib = indexById.get(String(b.id)) ?? 0;
return sortBy === "recent" ? ia - ib : ib - ia;
});
}
return sorted;
}, [patients, search, stateFilter, cityFilter, sortBy, indexById]);
// Dados paginados // Dados paginados
const paginatedData = useMemo(() => { const paginatedData = useMemo(() => {
@ -108,10 +157,10 @@ export default function PacientesPage() {
const totalPages = Math.ceil(filtered.length / itemsPerPage); const totalPages = Math.ceil(filtered.length / itemsPerPage);
// Reset para página 1 quando mudar a busca ou itens por página // Reset página ao mudar filtros/ordenadores
useEffect(() => { useEffect(() => {
setCurrentPage(1); setCurrentPage(1);
}, [search, itemsPerPage]); }, [search, itemsPerPage, stateFilter, cityFilter, sortBy]);
function handleAdd() { function handleAdd() {
setEditingId(null); setEditingId(null);
@ -214,7 +263,8 @@ export default function PacientesPage() {
<p className="text-muted-foreground">Gerencie os pacientes</p> <p className="text-muted-foreground">Gerencie os pacientes</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-wrap">
{/* Busca */}
<div className="relative"> <div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input <Input
@ -225,7 +275,52 @@ export default function PacientesPage() {
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()} onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
/> />
</div> </div>
<Button variant="secondary" onClick={() => void handleBuscarServidor()} className="hover:bg-primary hover:text-white">Buscar</Button> <Button variant="secondary" onClick={() => void handleBuscarServidor()} className="hover:bg-primary hover:text-white">
Buscar
</Button>
{/* Ordenar por */}
<select
aria-label="Ordenar por"
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="name_asc">Nome (AZ)</option>
<option value="name_desc">Nome (ZA)</option>
<option value="recent">Mais recentes (carregamento)</option>
<option value="oldest">Mais antigos (carregamento)</option>
</select>
{/* Estado (UF) */}
<select
aria-label="Filtrar por estado"
value={stateFilter}
onChange={(e) => {
setStateFilter(e.target.value);
setCityFilter("");
}}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todos os estados</option>
{stateOptions.map((uf) => (
<option key={uf} value={uf}>{uf}</option>
))}
</select>
{/* Cidade (dependente do estado) */}
<select
aria-label="Filtrar por cidade"
value={cityFilter}
onChange={(e) => setCityFilter(e.target.value)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todas as cidades</option>
{cityOptions.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
<Button onClick={handleAdd}> <Button onClick={handleAdd}>
<Plus className="mr-2 h-4 w-4" /> <Plus className="mr-2 h-4 w-4" />
Novo paciente Novo paciente