develop #83
@ -145,6 +145,11 @@ 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);
|
||||||
@ -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);
|
|
||||||
|
|
||||||
const filtered = sourceList.filter((d) => {
|
// 1) Busca
|
||||||
// Busca por nome
|
const afterSearch = !q
|
||||||
const byName = (d.full_name || "").toLowerCase().includes(q);
|
? sourceList
|
||||||
|
: sourceList.filter((d) => {
|
||||||
|
const byName = (d.full_name || "").toLowerCase().includes(q);
|
||||||
|
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);
|
||||||
|
const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q);
|
||||||
|
const match = byName || byCrm || byId || byEmail || byEspecialidade;
|
||||||
|
if (match) console.log('✅ Match encontrado:', d.full_name, d.id);
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
|
||||||
// Busca por CRM (remove formatação se necessário)
|
// 2) Filtros de localização e especialidade
|
||||||
const byCrm = qDigits.length >= 3 && (d.crm || "").replace(/\D/g, "").includes(qDigits);
|
const afterFilters = afterSearch.filter((d) => {
|
||||||
|
if (stateFilter && String(d.state) !== stateFilter) return false;
|
||||||
// Busca por ID (UUID completo ou parcial)
|
if (cityFilter && String(d.city) !== cityFilter) return false;
|
||||||
const byId = (d.id || "").toLowerCase().includes(q);
|
if (specialtyFilter && String(d.especialidade) !== specialtyFilter) return false;
|
||||||
|
return true;
|
||||||
// Busca por email
|
|
||||||
const byEmail = (d.email || "").toLowerCase().includes(q);
|
|
||||||
|
|
||||||
// Busca por especialidade
|
|
||||||
const byEspecialidade = (d.especialidade || "").toLowerCase().includes(q);
|
|
||||||
|
|
||||||
const match = byName || byCrm || byId || byEmail || byEspecialidade;
|
|
||||||
if (match) {
|
|
||||||
console.log('✅ Match encontrado:', d.full_name, d.id, 'por:', { byName, byCrm, byId, byEmail, byEspecialidade });
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (A–Z)</option>
|
||||||
|
<option value="name_desc">Nome (Z–A)</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
|
||||||
|
|||||||
@ -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, "");
|
|
||||||
|
|
||||||
return patients.filter((p) => {
|
// Busca
|
||||||
// Busca por nome
|
if (search.trim()) {
|
||||||
const byName = (p.full_name || "").toLowerCase().includes(q);
|
const q = search.toLowerCase().trim();
|
||||||
|
const qDigits = q.replace(/\D/g, "");
|
||||||
|
base = patients.filter((p) => {
|
||||||
|
const byName = (p.full_name || "").toLowerCase().includes(q);
|
||||||
|
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);
|
||||||
|
return byName || byCPF || byId || byEmail;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Busca por CPF (remove formatação)
|
// Filtros por UF e cidade
|
||||||
const byCPF = qDigits.length >= 3 && (p.cpf || "").replace(/\D/g, "").includes(qDigits);
|
const withLocation = base.filter((p) => {
|
||||||
|
if (stateFilter && String(p.state) !== stateFilter) return false;
|
||||||
// Busca por ID (UUID completo ou parcial)
|
if (cityFilter && String(p.city) !== cityFilter) return false;
|
||||||
const byId = (p.id || "").toLowerCase().includes(q);
|
return true;
|
||||||
|
|
||||||
// Busca por email
|
|
||||||
const byEmail = (p.email || "").toLowerCase().includes(q);
|
|
||||||
|
|
||||||
return byName || byCPF || byId || byEmail;
|
|
||||||
});
|
});
|
||||||
}, [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 (A–Z)</option>
|
||||||
|
<option value="name_desc">Nome (Z–A)</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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user