969 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { MoreHorizontal, Plus, Search, Edit, Trash2, ArrowLeft, Eye, Users } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { DoctorRegistrationForm } from "@/components/features/forms/doctor-registration-form";
import AvailabilityForm from '@/components/features/forms/availability-form'
import ExceptionForm from '@/components/features/forms/exception-form'
import { listarDisponibilidades, DoctorAvailability, deletarDisponibilidade, listarExcecoes, DoctorException, deletarExcecao } from '@/lib/api'
import { listarMedicos, excluirMedico, buscarMedicos, buscarMedicoPorId, buscarPacientesPorIds, Medico } from "@/lib/api";
import { listAssignmentsForUser } from '@/lib/assignment';
function normalizeMedico(m: any): Medico {
const normalizeSex = (v: any) => {
if (v === undefined) return null;
const s = String(v || '').trim().toLowerCase();
if (!s) return null;
const male = new Set(['m','masc','male','masculino','homem','h','1','mas']);
const female = new Set(['f','fem','female','feminino','mulher','mul','2','fem']);
const other = new Set(['o','outro','other','3','nb','nonbinary','nao binario','não binário']);
if (male.has(s)) return 'masculino';
if (female.has(s)) return 'feminino';
if (other.has(s)) return 'outro';
if (['masculino','feminino','outro'].includes(s)) return s;
return null;
};
const formatBirth = (v: any) => {
if (!v && typeof v !== 'string') return null;
const s = String(v || '').trim();
if (!s) return null;
const iso = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (iso) {
const [, y, mth, d] = iso;
return `${d.padStart(2,'0')}/${mth.padStart(2,'0')}/${y}`;
}
const ddmmyyyy = s.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (ddmmyyyy) return s;
const parsed = new Date(s);
if (!isNaN(parsed.getTime())) {
const d = String(parsed.getDate()).padStart(2,'0');
const mth = String(parsed.getMonth() + 1).padStart(2,'0');
const y = String(parsed.getFullYear());
return `${d}/${mth}/${y}`;
}
return null;
};
return {
id: String(m.id ?? m.uuid ?? ""),
full_name: m.full_name ?? m.nome ?? "", // 👈 Correção: usar full_name como padrão
nome_social: m.nome_social ?? m.social_name ?? null,
cpf: m.cpf ?? "",
rg: m.rg ?? m.document_number ?? null,
sexo: normalizeSex(m.sexo ?? m.sex ?? m.sexualidade ?? null),
data_nascimento: formatBirth(m.data_nascimento ?? m.birth_date ?? m.birthDate ?? null),
telefone: m.telefone ?? m.phone_mobile ?? "",
celular: m.celular ?? m.phone2 ?? null,
contato_emergencia: m.contato_emergencia ?? null,
email: m.email ?? "",
crm: m.crm ?? "",
estado_crm: m.estado_crm ?? m.crm_state ?? null,
rqe: m.rqe ?? null,
formacao_academica: m.formacao_academica ?? [],
curriculo_url: m.curriculo_url ?? null,
especialidade: m.especialidade ?? m.specialty ?? "",
observacoes: m.observacoes ?? m.notes ?? null,
foto_url: m.foto_url ?? null,
tipo_vinculo: m.tipo_vinculo ?? null,
dados_bancarios: m.dados_bancarios ?? null,
agenda_horario: m.agenda_horario ?? null,
valor_consulta: m.valor_consulta ?? null,
active: m.active ?? true,
cep: m.cep ?? "",
city: m.city ?? "",
complement: m.complement ?? null,
neighborhood: m.neighborhood ?? "",
number: m.number ?? "",
phone2: m.phone2 ?? null,
state: m.state ?? "",
street: m.street ?? "",
created_at: m.created_at ?? null,
created_by: m.created_by ?? null,
updated_at: m.updated_at ?? null,
updated_by: m.updated_by ?? null,
user_id: m.user_id ?? null,
};
}
function translateWeekday(w?: string) {
if (!w) return '';
const key = w.toString().toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '').replace(/[^a-z0-9]/g, '');
const map: Record<string, string> = {
'segunda': 'Segunda',
'terca': 'Terça',
'quarta': 'Quarta',
'quinta': 'Quinta',
'sexta': 'Sexta',
'sabado': 'Sábado',
'domingo': 'Domingo',
'monday': 'Segunda',
'tuesday': 'Terça',
'wednesday': 'Quarta',
'thursday': 'Quinta',
'friday': 'Sexta',
'saturday': 'Sábado',
'sunday': 'Domingo',
};
return map[key] ?? w;
}
export default function DoutoresPage() {
const [doctors, setDoctors] = useState<Medico[]>([]);
const [loading, setLoading] = useState(false);
const [search, setSearch] = useState("");
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [viewingDoctor, setViewingDoctor] = useState<Medico | null>(null);
const [assignedDialogOpen, setAssignedDialogOpen] = useState(false);
const [assignedPatients, setAssignedPatients] = useState<any[]>([]);
const [assignedLoading, setAssignedLoading] = useState(false);
const [assignedDoctor, setAssignedDoctor] = useState<Medico | null>(null);
const [availabilityOpenFor, setAvailabilityOpenFor] = useState<Medico | null>(null);
const [availabilityViewingFor, setAvailabilityViewingFor] = useState<Medico | null>(null);
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>([]);
const [availLoading, setAvailLoading] = useState(false);
const [editingAvailability, setEditingAvailability] = useState<DoctorAvailability | null>(null);
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
const [exceptionsLoading, setExceptionsLoading] = useState(false);
const [exceptionViewingFor, setExceptionViewingFor] = useState<Medico | null>(null);
const [exceptionOpenFor, setExceptionOpenFor] = useState<Medico | null>(null);
const [searchResults, setSearchResults] = useState<Medico[]>([]);
const [searchMode, setSearchMode] = useState(false);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
// Paginação
const [currentPage, setCurrentPage] = useState(1);
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() {
setLoading(true);
try {
const list = await listarMedicos({ limit: 50 });
const normalized = (list ?? []).map(normalizeMedico);
console.log('🏥 Médicos carregados:', normalized);
setDoctors(normalized);
} finally {
setLoading(false);
}
}
// Função para detectar se é um UUID válido
function isValidUUID(str: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
}
// Função para buscar médicos no servidor
async function handleBuscarServidor(termoBusca?: string) {
const termo = (termoBusca || search).trim();
if (!termo) {
setSearchMode(false);
setSearchResults([]);
return;
}
console.log('🔍 Buscando médico por:', termo);
setLoading(true);
try {
// Se parece com UUID, tenta busca direta por ID
if (isValidUUID(termo)) {
console.log('📋 Detectado UUID, buscando por ID...');
try {
const medico = await buscarMedicoPorId(termo);
const normalizado = normalizeMedico(medico);
console.log('✅ Médico encontrado por ID:', normalizado);
setSearchResults([normalizado]);
setSearchMode(true);
return;
} catch (error) {
console.log('❌ Não encontrado por ID, tentando busca geral...');
}
}
// Busca geral
const resultados = await buscarMedicos(termo);
const normalizados = resultados.map(normalizeMedico);
console.log('📋 Resultados da busca geral:', normalizados);
setSearchResults(normalizados);
setSearchMode(true);
} catch (error) {
console.error('❌ Erro na busca:', error);
setSearchResults([]);
setSearchMode(true);
} finally {
setLoading(false);
}
}
// Handler para mudança no campo de busca com busca automática
function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
const valor = e.target.value;
setSearch(valor);
// Limpa o timeout anterior se existir
if (searchTimeout) {
clearTimeout(searchTimeout);
}
// Se limpar a busca, volta ao modo normal
if (!valor.trim()) {
setSearchMode(false);
setSearchResults([]);
return;
}
// Busca automática com debounce ajustável
// Para IDs (UUID) longos, faz busca no servidor
// Para busca parcial, usa apenas filtro local
const isLikeUUID = valor.includes('-') && valor.length > 10;
const shouldSearchServer = isLikeUUID || valor.length >= 3;
if (shouldSearchServer) {
const debounceTime = isLikeUUID ? 300 : 500;
const newTimeout = setTimeout(() => {
handleBuscarServidor(valor);
}, debounceTime);
setSearchTimeout(newTimeout);
} else {
// Para termos curtos, apenas usa filtro local
setSearchMode(false);
setSearchResults([]);
}
}
// Handler para Enter no campo de busca
function handleSearchKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
if (e.key === 'Enter') {
e.preventDefault();
handleBuscarServidor();
}
}
// Handler para o botão de busca
function handleClickBuscar() {
void handleBuscarServidor();
}
useEffect(() => {
load();
}, []);
// Limpa o timeout quando o componente é desmontado
useEffect(() => {
return () => {
if (searchTimeout) {
clearTimeout(searchTimeout);
}
};
}, [searchTimeout]);
// 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(() => {
console.log('🔍 Filtro - search:', search, 'searchMode:', searchMode, 'doctors:', doctors.length, 'searchResults:', searchResults.length);
const q = search.toLowerCase().trim();
const qDigits = q.replace(/\D/g, "");
const sourceList = searchMode ? searchResults : doctors;
// 1) Busca
const afterSearch = !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;
});
// 2) Filtros de localização e especialidade
const afterFilters = afterSearch.filter((d) => {
if (stateFilter && String(d.state) !== stateFilter) return false;
if (cityFilter && String(d.city) !== cityFilter) return false;
if (specialtyFilter && String(d.especialidade) !== specialtyFilter) return false;
return true;
});
// 3) Ordenação
const sorted = [...afterFilters];
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
const paginatedDoctors = useMemo(() => {
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return displayedDoctors.slice(startIndex, endIndex);
}, [displayedDoctors, currentPage, itemsPerPage]);
const totalPages = Math.ceil(displayedDoctors.length / itemsPerPage);
// Reset página ao mudar busca/filtros/ordenação
useEffect(() => {
setCurrentPage(1);
}, [search, itemsPerPage, searchMode, stateFilter, cityFilter, specialtyFilter, sortBy]);
function handleAdd() {
setEditingId(null);
setShowForm(true);
}
function handleEdit(id: string) {
setEditingId(id);
setShowForm(true);
}
function handleView(doctor: Medico) {
setViewingDoctor(doctor);
}
async function handleViewAssignedPatients(doctor: Medico) {
setAssignedDoctor(doctor);
setAssignedLoading(true);
setAssignedPatients([]);
try {
const assigns = await listAssignmentsForUser(String(doctor.user_id ?? doctor.id));
const patientIds = Array.isArray(assigns) ? assigns.map((a:any) => String(a.patient_id)).filter(Boolean) : [];
if (patientIds.length) {
const patients = await buscarPacientesPorIds(patientIds);
setAssignedPatients(patients || []);
} else {
setAssignedPatients([]);
}
} catch (e) {
console.warn('[DoutoresPage] erro ao carregar pacientes atribuídos:', e);
setAssignedPatients([]);
} finally {
setAssignedLoading(false);
setAssignedDialogOpen(true);
}
}
async function reloadAvailabilities(doctorId?: string) {
if (!doctorId) return;
setAvailLoading(true);
try {
const list = await listarDisponibilidades({ doctorId, active: true });
setAvailabilities(list || []);
} catch (e) {
console.warn('Erro ao recarregar disponibilidades:', e);
} finally {
setAvailLoading(false);
}
}
async function handleDelete(id: string) {
if (!confirm("Excluir este médico?")) return;
await excluirMedico(id);
await load();
}
function handleSaved(savedDoctor?: Medico) {
setShowForm(false);
if (savedDoctor) {
const normalized = normalizeMedico(savedDoctor);
setDoctors((prev) => {
const i = prev.findIndex((d) => String(d.id) === String(normalized.id));
if (i < 0) {
// Novo médico → adiciona no topo
return [normalized, ...prev];
} else {
// Médico editado → substitui na lista
const clone = [...prev];
clone[i] = normalized;
return clone;
}
});
} else {
// fallback → recarrega tudo
load();
}
}
if (showForm) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => setShowForm(false)}>
<ArrowLeft className="h-4 w-4" />
</Button>
<h1 className="text-2xl font-bold">{editingId ? "Editar Médico" : "Novo Médico"}</h1>
</div>
<DoctorRegistrationForm
inline
mode={editingId ? "edit" : "create"}
doctorId={editingId}
onSaved={handleSaved}
onClose={() => setShowForm(false)}
/>
</div>
);
}
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div>
<h1 className="text-2xl font-bold">Médicos</h1>
<p className="text-muted-foreground">Gerencie os médicos da sua clínica</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<div className="flex gap-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
className="pl-8 w-80"
placeholder="Digite para buscar por ID, nome, CRM ou especialidade…"
value={search}
onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown}
/>
</div>
<Button
variant="secondary"
onClick={() => void handleBuscarServidor()}
disabled={loading}
className="hover:bg-primary hover:text-white"
>
Buscar
</Button>
{searchMode && (
<Button
variant="ghost"
onClick={() => {
setSearch("");
setSearchMode(false);
setSearchResults([]);
}}
>
Limpar
</Button>
)}
</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}>
<Plus className="mr-2 h-4 w-4" />
Novo Médico
</Button>
</div>
</div>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-primary hover:bg-primary">
<TableHead className="text-primary-foreground">Nome</TableHead>
<TableHead className="text-primary-foreground">Especialidade</TableHead>
<TableHead className="text-primary-foreground">CRM</TableHead>
<TableHead className="text-primary-foreground">Contato</TableHead>
<TableHead className="w-[100px] text-primary-foreground">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
Carregando
</TableCell>
</TableRow>
) : paginatedDoctors.length > 0 ? (
paginatedDoctors.map((doctor) => (
<TableRow key={doctor.id}>
<TableCell className="font-medium">{doctor.full_name}</TableCell>
<TableCell>
<Badge variant="outline">{doctor.especialidade}</Badge>
</TableCell>
<TableCell>{doctor.crm}</TableCell>
<TableCell>
<div className="flex flex-col">
<span>{doctor.email}</span>
<span className="text-sm text-muted-foreground">{doctor.telefone}</span>
</div>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="h-8 w-8 p-0 flex items-center justify-center rounded-md hover:bg-primary hover:text-white transition-colors">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Abrir menu</span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleView(doctor)}>
<Eye className="mr-2 h-4 w-4" />
Ver
</DropdownMenuItem>
{/* Ver pacientes atribuídos ao médico */}
<DropdownMenuItem onClick={() => handleViewAssignedPatients(doctor)}>
<Users className="mr-2 h-4 w-4" />
Ver pacientes atribuídos
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setAvailabilityOpenFor(doctor)}>
<Plus className="mr-2 h-4 w-4" />
Criar disponibilidade
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setExceptionOpenFor(doctor)}>
<Plus className="mr-2 h-4 w-4" />
Criar exceção
</DropdownMenuItem>
<DropdownMenuItem onClick={async () => {
setAvailLoading(true);
try {
const list = await listarDisponibilidades({ doctorId: doctor.id, active: true });
setAvailabilities(list || []);
setAvailabilityViewingFor(doctor);
} catch (e) {
console.warn('Erro ao listar disponibilidades:', e);
} finally {
setAvailLoading(false);
}
}}>
<Users className="mr-2 h-4 w-4" />
Ver disponibilidades
</DropdownMenuItem>
<DropdownMenuItem onClick={async () => {
setExceptionsLoading(true);
try {
const list = await listarExcecoes({ doctorId: doctor.id });
setExceptions(list || []);
setExceptionViewingFor(doctor);
} catch (e) {
console.warn('Erro ao listar exceções:', e);
} finally {
setExceptionsLoading(false);
}
}}>
<Users className="mr-2 h-4 w-4" />
Ver exceções
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(String(doctor.id))} className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
Nenhum médico encontrado
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* Controles de paginação */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Itens por página:</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(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={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<span className="text-sm text-muted-foreground">
Mostrando {paginatedDoctors.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, displayedDoctors.length)} de {displayedDoctors.length}
</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="hover:bg-primary! hover:text-white! transition-colors"
>
Primeira
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="hover:bg-primary! hover:text-white! transition-colors"
>
Anterior
</Button>
<span className="text-sm text-muted-foreground">
Página {currentPage} de {totalPages || 1}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
className="hover:bg-primary! hover:text-white! transition-colors"
>
Próxima
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
className="hover:bg-primary! hover:text-white! transition-colors"
>
Última
</Button>
</div>
</div>
{viewingDoctor && (
<Dialog open={!!viewingDoctor} onOpenChange={() => setViewingDoctor(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Detalhes do Médico</DialogTitle>
<DialogDescription>
Informações detalhadas de {viewingDoctor?.full_name}.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Nome</Label>
<span className="col-span-3 font-medium">{viewingDoctor?.full_name}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Especialidade</Label>
<span className="col-span-3">
<Badge variant="outline">{viewingDoctor?.especialidade}</Badge>
</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">CRM</Label>
<span className="col-span-3">{viewingDoctor?.crm}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Email</Label>
<span className="col-span-3">{viewingDoctor?.email}</span>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label className="text-right">Telefone</Label>
<span className="col-span-3">{viewingDoctor?.telefone}</span>
</div>
</div>
<DialogFooter>
<Button onClick={() => setViewingDoctor(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* Availability modal */}
{availabilityOpenFor && (
<AvailabilityForm
open={!!availabilityOpenFor}
onOpenChange={(open) => { if (!open) setAvailabilityOpenFor(null); }}
doctorId={availabilityOpenFor?.id}
onSaved={(saved) => { console.log('Disponibilidade salva', saved); setAvailabilityOpenFor(null); /* optionally reload list */ reloadAvailabilities(availabilityOpenFor?.id); }}
/>
)}
{exceptionOpenFor && (
<ExceptionForm
open={!!exceptionOpenFor}
onOpenChange={(open) => { if (!open) setExceptionOpenFor(null); }}
doctorId={exceptionOpenFor?.id}
onSaved={(saved) => { console.log('Exceção criada', saved); setExceptionOpenFor(null); /* reload availabilities in case a full-day block affects listing */ reloadAvailabilities(exceptionOpenFor?.id); }}
/>
)}
{/* Edit availability modal */}
{editingAvailability && (
<AvailabilityForm
open={!!editingAvailability}
onOpenChange={(open) => { if (!open) setEditingAvailability(null); }}
doctorId={editingAvailability?.doctor_id ?? availabilityViewingFor?.id}
availability={editingAvailability}
mode="edit"
onSaved={(saved) => { console.log('Disponibilidade atualizada', saved); setEditingAvailability(null); reloadAvailabilities(editingAvailability?.doctor_id ?? availabilityViewingFor?.id); }}
/>
)}
{/* Ver disponibilidades dialog */}
{availabilityViewingFor && (
<Dialog open={!!availabilityViewingFor} onOpenChange={(open) => { if (!open) { setAvailabilityViewingFor(null); setAvailabilities([]); } }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Disponibilidades - {availabilityViewingFor.full_name}</DialogTitle>
<DialogDescription>
Lista de disponibilidades públicas do médico selecionado.
</DialogDescription>
</DialogHeader>
<div className="py-4">
{availLoading ? (
<div>Carregando disponibilidades</div>
) : availabilities && availabilities.length ? (
<div className="space-y-2">
{availabilities.map((a) => (
<div key={String(a.id)} className="p-2 border rounded flex justify-between items-start">
<div>
<div className="font-medium">{translateWeekday(a.weekday)} {a.start_time} {a.end_time}</div>
<div className="text-xs text-muted-foreground">Duração: {a.slot_minutes} min Tipo: {a.appointment_type || '—'} {a.active ? 'Ativa' : 'Inativa'}</div>
</div>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={() => setEditingAvailability(a)}>Editar</Button>
<Button size="sm" variant="destructive" onClick={async () => {
if (!confirm('Excluir esta disponibilidade?')) return;
try {
await deletarDisponibilidade(String(a.id));
// reload
reloadAvailabilities(availabilityViewingFor?.id ?? a.doctor_id);
} catch (e) {
console.warn('Erro ao deletar disponibilidade:', e);
alert((e as any)?.message || 'Erro ao deletar disponibilidade');
}
}}>Excluir</Button>
</div>
</div>
))}
</div>
) : (
<div>Nenhuma disponibilidade encontrada.</div>
)}
</div>
<DialogFooter>
<Button onClick={() => { setAvailabilityViewingFor(null); setAvailabilities([]); }}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
{/* Ver exceções dialog */}
{exceptionViewingFor && (
<Dialog open={!!exceptionViewingFor} onOpenChange={(open) => { if (!open) { setExceptionViewingFor(null); setExceptions([]); } }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Exceções - {exceptionViewingFor.full_name}</DialogTitle>
<DialogDescription>
Lista de exceções (bloqueios/liberações) do médico selecionado.
</DialogDescription>
</DialogHeader>
<div className="py-4">
{exceptionsLoading ? (
<div>Carregando exceções</div>
) : exceptions && exceptions.length ? (
<div className="space-y-2">
{exceptions.map((ex) => (
<div key={String(ex.id)} className="p-2 border rounded flex justify-between items-start">
<div>
<div className="font-medium">{ex.date} {ex.start_time ? `${ex.start_time}` : ''} {ex.end_time ? `${ex.end_time}` : ''}</div>
<div className="text-xs text-muted-foreground">Tipo: {ex.kind} Motivo: {ex.reason || '—'}</div>
</div>
<div className="flex gap-2">
<Button size="sm" variant="destructive" onClick={async () => {
if (!confirm('Excluir esta exceção?')) return;
try {
await deletarExcecao(String(ex.id));
const list = await listarExcecoes({ doctorId: exceptionViewingFor?.id });
setExceptions(list || []);
} catch (e) {
console.warn('Erro ao deletar exceção:', e);
alert((e as any)?.message || 'Erro ao deletar exceção');
}
}}>Excluir</Button>
</div>
</div>
))}
</div>
) : (
<div>Nenhuma exceção encontrada.</div>
)}
</div>
<DialogFooter>
<Button onClick={() => { setExceptionViewingFor(null); setExceptions([]); }}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)}
<div className="text-sm text-muted-foreground">
{searchMode ? 'Resultado(s) da busca' : `Total de ${doctors.length} médico(s)`}
</div>
{/* Dialog para pacientes atribuídos */}
<Dialog open={assignedDialogOpen} onOpenChange={(open) => { if (!open) { setAssignedDialogOpen(false); setAssignedPatients([]); setAssignedDoctor(null); } }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Pacientes atribuídos{assignedDoctor ? ` - ${assignedDoctor.full_name}` : ''}</DialogTitle>
<DialogDescription>
Lista de pacientes atribuídos a este médico.
</DialogDescription>
</DialogHeader>
<div className="py-4">
{assignedLoading ? (
<div>Carregando pacientes...</div>
) : assignedPatients && assignedPatients.length ? (
<div className="space-y-2">
{assignedPatients.map((p:any) => (
<div key={p.id} className="p-2 border rounded">
<div className="font-medium">{p.full_name ?? p.nome ?? p.name ?? '(sem nome)'}</div>
<div className="text-xs text-muted-foreground">ID: {p.id} {p.cpf ? `• CPF: ${p.cpf}` : ''}</div>
</div>
))}
</div>
) : (
<div>Nenhum paciente atribuído encontrado.</div>
)}
</div>
<DialogFooter>
<Button onClick={() => setAssignedDialogOpen(false)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}