fix: menu acessibilidade todos os modos funcionais

This commit is contained in:
Pedro Araujo da Silveira 2025-11-04 16:20:04 -03:00
parent f2a9dc7b70
commit 60c8b5eaa9
7 changed files with 592 additions and 154 deletions

View File

@ -24,6 +24,8 @@ export function SecretaryAppointmentList() {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("Todos"); const [statusFilter, setStatusFilter] = useState("Todos");
const [typeFilter, setTypeFilter] = useState("Todos"); const [typeFilter, setTypeFilter] = useState("Todos");
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [modalMode, setModalMode] = useState<"create" | "edit">("create"); const [modalMode, setModalMode] = useState<"create" | "edit">("create");
const [selectedAppointment, setSelectedAppointment] = useState< const [selectedAppointment, setSelectedAppointment] = useState<
@ -152,6 +154,12 @@ export function SecretaryAppointmentList() {
return matchesSearch && matchesStatus && matchesType; return matchesSearch && matchesStatus && matchesType;
}); });
// Cálculos de paginação
const totalPages = Math.ceil(filteredAppointments.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedAppointments = filteredAppointments.slice(startIndex, endIndex);
const loadDoctorsAndPatients = async () => { const loadDoctorsAndPatients = async () => {
try { try {
const [patientsData, doctorsData] = await Promise.all([ const [patientsData, doctorsData] = await Promise.all([
@ -237,9 +245,15 @@ export function SecretaryAppointmentList() {
setSearchTerm(""); setSearchTerm("");
setStatusFilter("Todos"); setStatusFilter("Todos");
setTypeFilter("Todos"); setTypeFilter("Todos");
setCurrentPage(1);
loadAppointments(); loadAppointments();
}; };
// Reset página quando filtros mudarem
useEffect(() => {
setCurrentPage(1);
}, [searchTerm, statusFilter, typeFilter]);
const getStatusBadge = (status: string) => { const getStatusBadge = (status: string) => {
const statusMap: Record<string, { label: string; className: string }> = { const statusMap: Record<string, { label: string; className: string }> = {
confirmada: { confirmada: {
@ -293,8 +307,8 @@ export function SecretaryAppointmentList() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Consultas</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Consultas</h1>
<p className="text-gray-600 mt-1">Gerencie as consultas agendadas</p> <p className="text-gray-600 dark:text-gray-400 mt-1">Gerencie as consultas agendadas</p>
</div> </div>
<button <button
onClick={handleOpenCreateModal} onClick={handleOpenCreateModal}
@ -306,16 +320,16 @@ export function SecretaryAppointmentList() {
</div> </div>
{/* Search and Filters */} {/* Search and Filters */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 space-y-4">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="flex-1 relative"> <div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500" />
<input <input
type="text" type="text"
placeholder="Buscar consultas por paciente ou médico..." placeholder="Buscar consultas por paciente ou médico..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
/> />
</div> </div>
<button <button
@ -326,7 +340,7 @@ export function SecretaryAppointmentList() {
</button> </button>
<button <button
onClick={handleClear} onClick={handleClear}
className="px-6 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="px-6 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
> >
Limpar Limpar
</button> </button>
@ -334,11 +348,11 @@ export function SecretaryAppointmentList() {
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Status:</span> <span className="text-sm text-gray-600 dark:text-gray-400">Status:</span>
<select <select
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)} onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent" className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
> >
<option>Todos</option> <option>Todos</option>
<option>Confirmada</option> <option>Confirmada</option>
@ -348,11 +362,11 @@ export function SecretaryAppointmentList() {
</select> </select>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Tipo:</span> <span className="text-sm text-gray-600 dark:text-gray-400">Tipo:</span>
<select <select
value={typeFilter} value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)} onChange={(e) => setTypeFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent" className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
> >
<option>Todos</option> <option>Todos</option>
<option>Presencial</option> <option>Presencial</option>
@ -363,36 +377,36 @@ export function SecretaryAppointmentList() {
</div> </div>
{/* Table */} {/* Table */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200"> <thead className="bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<tr> <tr>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Paciente Paciente
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Médico Médico
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Data/Hora Data/Hora
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Tipo Tipo
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Status Status
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Ações Ações
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{loading ? ( {loading ? (
<tr> <tr>
<td <td
colSpan={6} colSpan={6}
className="px-6 py-12 text-center text-gray-500" className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
> >
Carregando consultas... Carregando consultas...
</td> </td>
@ -401,7 +415,7 @@ export function SecretaryAppointmentList() {
<tr> <tr>
<td <td
colSpan={6} colSpan={6}
className="px-6 py-12 text-center text-gray-500" className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
> >
{searchTerm || {searchTerm ||
statusFilter !== "Todos" || statusFilter !== "Todos" ||
@ -411,10 +425,10 @@ export function SecretaryAppointmentList() {
</td> </td>
</tr> </tr>
) : ( ) : (
filteredAppointments.map((appointment) => ( paginatedAppointments.map((appointment) => (
<tr <tr
key={appointment.id} key={appointment.id}
className="hover:bg-gray-50 transition-colors" className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
> >
<td className="px-6 py-4"> <td className="px-6 py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -425,11 +439,11 @@ export function SecretaryAppointmentList() {
color="blue" color="blue"
/> />
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{appointment.patient?.full_name || {appointment.patient?.full_name ||
"Paciente não encontrado"} "Paciente não encontrado"}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500 dark:text-gray-400">
{appointment.patient?.email || "—"} {appointment.patient?.email || "—"}
</p> </p>
</div> </div>
@ -444,7 +458,7 @@ export function SecretaryAppointmentList() {
color="green" color="green"
/> />
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{appointment.doctor?.full_name || {appointment.doctor?.full_name ||
"Médico não encontrado"} "Médico não encontrado"}
</p> </p>
@ -515,6 +529,62 @@ export function SecretaryAppointmentList() {
</table> </table>
</div> </div>
{/* Paginação */}
{filteredAppointments.length > 0 && (
<div className="flex items-center justify-between bg-white dark:bg-gray-800 px-6 py-4 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
Mostrando {startIndex + 1} até {Math.min(endIndex, filteredAppointments.length)} de {filteredAppointments.length} consultas
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Anterior
</button>
<div className="flex items-center gap-1">
{(() => {
const maxPagesToShow = 4;
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
// Ajusta startPage se estivermos próximos do fim
if (endPage - startPage < maxPagesToShow - 1) {
startPage = Math.max(1, endPage - maxPagesToShow + 1);
}
const pages = [];
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages.map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-2 text-sm rounded-lg transition-colors ${
currentPage === page
? "bg-green-600 text-white"
: "border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
}`}
>
{page}
</button>
));
})()}
</div>
<button
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Próxima
</button>
</div>
</div>
)}
{/* Modal de Criar Consulta */} {/* Modal de Criar Consulta */}
{showCreateModal && ( {showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">

View File

@ -70,6 +70,8 @@ export function SecretaryDoctorList({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [specialtyFilter, setSpecialtyFilter] = useState("Todas"); const [specialtyFilter, setSpecialtyFilter] = useState("Todas");
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
// Modal states // Modal states
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
@ -121,6 +123,12 @@ export function SecretaryDoctorList({
return matchesSearch && matchesSpecialty; return matchesSearch && matchesSpecialty;
}); });
// Cálculos de paginação
const totalPages = Math.ceil(filteredDoctors.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedDoctors = filteredDoctors.slice(startIndex, endIndex);
const handleSearch = () => { const handleSearch = () => {
loadDoctors(); loadDoctors();
}; };
@ -128,9 +136,15 @@ export function SecretaryDoctorList({
const handleClear = () => { const handleClear = () => {
setSearchTerm(""); setSearchTerm("");
setSpecialtyFilter("Todas"); setSpecialtyFilter("Todas");
setCurrentPage(1);
loadDoctors(); loadDoctors();
}; };
// Reset página quando filtros mudarem
useEffect(() => {
setCurrentPage(1);
}, [searchTerm, specialtyFilter]);
const handleNewDoctor = () => { const handleNewDoctor = () => {
setModalMode("create"); setModalMode("create");
setFormData({ setFormData({
@ -246,8 +260,8 @@ export function SecretaryDoctorList({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Médicos</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Médicos</h1>
<p className="text-gray-600 mt-1">Gerencie os médicos cadastrados</p> <p className="text-gray-600 dark:text-gray-400 mt-1">Gerencie os médicos cadastrados</p>
</div> </div>
<button <button
onClick={handleNewDoctor} onClick={handleNewDoctor}
@ -259,16 +273,16 @@ export function SecretaryDoctorList({
</div> </div>
{/* Search and Filters */} {/* Search and Filters */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 space-y-4">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="flex-1 relative"> <div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500" />
<input <input
type="text" type="text"
placeholder="Buscar médicos por nome ou CRM..." placeholder="Buscar médicos por nome ou CRM..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
/> />
</div> </div>
<button <button
@ -279,18 +293,18 @@ export function SecretaryDoctorList({
</button> </button>
<button <button
onClick={handleClear} onClick={handleClear}
className="px-6 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="px-6 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
> >
Limpar Limpar
</button> </button>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Especialidade:</span> <span className="text-sm text-gray-600 dark:text-gray-400">Especialidade:</span>
<select <select
value={specialtyFilter} value={specialtyFilter}
onChange={(e) => setSpecialtyFilter(e.target.value)} onChange={(e) => setSpecialtyFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent" className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
> >
<option>Todas</option> <option>Todas</option>
<option>Cardiologia</option> <option>Cardiologia</option>
@ -304,33 +318,33 @@ export function SecretaryDoctorList({
</div> </div>
{/* Table */} {/* Table */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200"> <thead className="bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<tr> <tr>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Médico Médico
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Especialidade Especialidade
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
CRM CRM
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Próxima Disponível Próxima Disponível
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Ações Ações
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{loading ? ( {loading ? (
<tr> <tr>
<td <td
colSpan={5} colSpan={5}
className="px-6 py-12 text-center text-gray-500" className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
> >
Carregando médicos... Carregando médicos...
</td> </td>
@ -339,7 +353,7 @@ export function SecretaryDoctorList({
<tr> <tr>
<td <td
colSpan={5} colSpan={5}
className="px-6 py-12 text-center text-gray-500" className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
> >
{searchTerm || specialtyFilter !== "Todas" {searchTerm || specialtyFilter !== "Todas"
? "Nenhum médico encontrado com esses filtros" ? "Nenhum médico encontrado com esses filtros"
@ -347,10 +361,10 @@ export function SecretaryDoctorList({
</td> </td>
</tr> </tr>
) : ( ) : (
filteredDoctors.map((doctor, index) => ( paginatedDoctors.map((doctor, index) => (
<tr <tr
key={doctor.id} key={doctor.id}
className="hover:bg-gray-50 transition-colors" className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
> >
<td className="px-6 py-4"> <td className="px-6 py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -362,20 +376,20 @@ export function SecretaryDoctorList({
{getInitials(doctor.full_name || "")} {getInitials(doctor.full_name || "")}
</div> </div>
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{formatDoctorName(doctor.full_name)} {formatDoctorName(doctor.full_name)}
</p> </p>
<p className="text-sm text-gray-500">{doctor.email}</p> <p className="text-sm text-gray-500 dark:text-gray-400">{doctor.email}</p>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-400">
{doctor.phone_mobile} {doctor.phone_mobile}
</p> </p>
</div> </div>
</div> </div>
</td> </td>
<td className="px-6 py-4 text-sm text-gray-700"> <td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{doctor.specialty || "—"} {doctor.specialty || "—"}
</td> </td>
<td className="px-6 py-4 text-sm text-gray-700"> <td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{doctor.crm || "—"} {doctor.crm || "—"}
</td> </td>
<td className="px-6 py-4 text-sm text-gray-700"> <td className="px-6 py-4 text-sm text-gray-700">
@ -431,6 +445,61 @@ export function SecretaryDoctorList({
</table> </table>
</div> </div>
{/* Paginação */}
{filteredDoctors.length > 0 && (
<div className="flex items-center justify-between bg-white dark:bg-gray-800 px-6 py-4 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
Mostrando {startIndex + 1} até {Math.min(endIndex, filteredDoctors.length)} de {filteredDoctors.length} médicos
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Anterior
</button>
<div className="flex items-center gap-1">
{(() => {
const maxPagesToShow = 4;
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
if (endPage - startPage < maxPagesToShow - 1) {
startPage = Math.max(1, endPage - maxPagesToShow + 1);
}
const pages = [];
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages.map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-2 text-sm rounded-lg transition-colors ${
currentPage === page
? "bg-green-600 text-white"
: "border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
}`}
>
{page}
</button>
));
})()}
</div>
<button
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Próxima
</button>
</div>
</div>
)}
{/* Modal de Formulário */} {/* Modal de Formulário */}
{showModal && ( {showModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">

View File

@ -460,22 +460,22 @@ export function SecretaryDoctorSchedule() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Agenda Médica</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Agenda Médica</h1>
<p className="text-gray-600 mt-1"> <p className="text-gray-600 dark:text-gray-400 mt-1">
Gerencie disponibilidades e exceções Gerencie disponibilidades e exceções
</p> </p>
</div> </div>
</div> </div>
{/* Doctor Selector */} {/* Doctor Selector */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Selecione o Médico Selecione o Médico
</label> </label>
<select <select
value={selectedDoctorId} value={selectedDoctorId}
onChange={(e) => setSelectedDoctorId(e.target.value)} onChange={(e) => setSelectedDoctorId(e.target.value)}
className="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
> >
{doctors.map((doctor) => ( {doctors.map((doctor) => (
<option key={doctor.id} value={doctor.id}> <option key={doctor.id} value={doctor.id}>
@ -486,27 +486,27 @@ export function SecretaryDoctorSchedule() {
</div> </div>
{/* Calendar */} {/* Calendar */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 capitalize"> <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 capitalize">
{formatMonthYear(currentDate)} {formatMonthYear(currentDate)}
</h2> </h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={goToToday} onClick={goToToday}
className="px-4 py-2 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300"
> >
Hoje Hoje
</button> </button>
<button <button
onClick={previousMonth} onClick={previousMonth}
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" className="p-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300"
> >
<ChevronLeft className="h-4 w-4" /> <ChevronLeft className="h-4 w-4" />
</button> </button>
<button <button
onClick={nextMonth} onClick={nextMonth}
className="p-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors" className="p-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-gray-700 dark:text-gray-300"
> >
<ChevronRight className="h-4 w-4" /> <ChevronRight className="h-4 w-4" />
</button> </button>
@ -517,15 +517,15 @@ export function SecretaryDoctorSchedule() {
<div className="mb-4 flex flex-wrap gap-3 text-xs"> <div className="mb-4 flex flex-wrap gap-3 text-xs">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-yellow-100 border border-yellow-300"></div> <div className="w-3 h-3 rounded bg-yellow-100 border border-yellow-300"></div>
<span className="text-gray-600">Solicitada</span> <span className="text-gray-600 dark:text-gray-400">Solicitada</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-green-100 border border-green-300"></div> <div className="w-3 h-3 rounded bg-green-100 border border-green-300"></div>
<span className="text-gray-600">Confirmada</span> <span className="text-gray-600 dark:text-gray-400">Confirmada</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-blue-100 border border-blue-300"></div> <div className="w-3 h-3 rounded bg-blue-100 border border-blue-300"></div>
<span className="text-gray-600">Concluída</span> <span className="text-gray-600 dark:text-gray-400">Concluída</span>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-3 h-3 rounded bg-red-100 border border-red-300"></div> <div className="w-3 h-3 rounded bg-red-100 border border-red-300"></div>
@ -537,11 +537,11 @@ export function SecretaryDoctorSchedule() {
</div> </div>
</div> </div>
<div className="grid grid-cols-7 gap-px bg-gray-200 border border-gray-200 rounded-lg overflow-hidden"> <div className="grid grid-cols-7 gap-px bg-gray-200 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden">
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => ( {["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
<div <div
key={day} key={day}
className="bg-gray-50 px-2 py-3 text-center text-sm font-semibold text-gray-700" className="bg-gray-50 dark:bg-gray-700 px-2 py-3 text-center text-sm font-semibold text-gray-700 dark:text-gray-300"
> >
{day} {day}
</div> </div>
@ -549,15 +549,15 @@ export function SecretaryDoctorSchedule() {
{calendarDays.map((day, index) => ( {calendarDays.map((day, index) => (
<div <div
key={index} key={index}
className={`bg-white p-2 min-h-[100px] ${ className={`bg-white dark:bg-gray-800 p-2 min-h-[100px] ${
day.isCurrentMonth ? "" : "opacity-40" day.isCurrentMonth ? "" : "opacity-40"
} ${ } ${
day.date.toDateString() === new Date().toDateString() day.date.toDateString() === new Date().toDateString()
? "bg-blue-50" ? "bg-blue-50 dark:bg-blue-900"
: "" : ""
}`} }`}
> >
<div className="text-sm text-gray-700 mb-1 font-medium"> <div className="text-sm text-gray-700 dark:text-gray-300 mb-1 font-medium">
{day.date.getDate()} {day.date.getDate()}
</div> </div>
@ -575,8 +575,8 @@ export function SecretaryDoctorSchedule() {
key={`exc-${i}`} key={`exc-${i}`}
className={`text-xs p-1 rounded mb-1 truncate ${ className={`text-xs p-1 rounded mb-1 truncate ${
exc.kind === "bloqueio" exc.kind === "bloqueio"
? "bg-red-100 text-red-800" ? "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
: "bg-purple-100 text-purple-800" : "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200"
}`} }`}
title={tooltipText} title={tooltipText}
> >
@ -598,14 +598,14 @@ export function SecretaryDoctorSchedule() {
key={`apt-${i}`} key={`apt-${i}`}
className={`text-xs p-1 rounded mb-1 truncate ${ className={`text-xs p-1 rounded mb-1 truncate ${
apt.status === "requested" apt.status === "requested"
? "bg-yellow-100 text-yellow-800" ? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
: apt.status === "confirmed" : apt.status === "confirmed"
? "bg-green-100 text-green-800" ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
: apt.status === "completed" : apt.status === "completed"
? "bg-blue-100 text-blue-800" ? "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
: apt.status === "cancelled" : apt.status === "cancelled"
? "bg-gray-100 text-gray-600" ? "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
: "bg-orange-100 text-orange-800" : "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"
}`} }`}
title={`${time} - ${apt.patient_id}`} title={`${time} - ${apt.patient_id}`}
> >

View File

@ -52,6 +52,8 @@ export function SecretaryPatientList({
const [insuranceFilter, setInsuranceFilter] = useState("Todos"); const [insuranceFilter, setInsuranceFilter] = useState("Todos");
const [showBirthdays, setShowBirthdays] = useState(false); const [showBirthdays, setShowBirthdays] = useState(false);
const [showVIP, setShowVIP] = useState(false); const [showVIP, setShowVIP] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
// Modal states // Modal states
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
@ -153,6 +155,12 @@ export function SecretaryPatientList({
return matchesSearch && matchesBirthday && matchesInsurance && matchesVIP; return matchesSearch && matchesBirthday && matchesInsurance && matchesVIP;
}); });
// Cálculos de paginação
const totalPages = Math.ceil(filteredPatients.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedPatients = filteredPatients.slice(startIndex, endIndex);
const handleSearch = () => { const handleSearch = () => {
loadPatients(); loadPatients();
}; };
@ -162,9 +170,15 @@ export function SecretaryPatientList({
setInsuranceFilter("Todos"); setInsuranceFilter("Todos");
setShowBirthdays(false); setShowBirthdays(false);
setShowVIP(false); setShowVIP(false);
setCurrentPage(1);
loadPatients(); loadPatients();
}; };
// Reset página quando filtros mudarem
useEffect(() => {
setCurrentPage(1);
}, [searchTerm, insuranceFilter, showBirthdays, showVIP]);
const handleNewPatient = () => { const handleNewPatient = () => {
setModalMode("create"); setModalMode("create");
setFormData({ setFormData({
@ -399,8 +413,8 @@ export function SecretaryPatientList({
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Pacientes</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Pacientes</h1>
<p className="text-gray-600 mt-1"> <p className="text-gray-600 dark:text-gray-400 mt-1">
Gerencie os pacientes cadastrados Gerencie os pacientes cadastrados
</p> </p>
</div> </div>
@ -414,16 +428,16 @@ export function SecretaryPatientList({
</div> </div>
{/* Search and Filters */} {/* Search and Filters */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 space-y-4">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="flex-1 relative"> <div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500" />
<input <input
type="text" type="text"
placeholder="Buscar pacientes por nome ou email..." placeholder="Buscar pacientes por nome ou email..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
/> />
</div> </div>
<button <button
@ -434,7 +448,7 @@ export function SecretaryPatientList({
</button> </button>
<button <button
onClick={handleClear} onClick={handleClear}
className="px-6 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="px-6 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
> >
Limpar Limpar
</button> </button>
@ -448,7 +462,7 @@ export function SecretaryPatientList({
onChange={(e) => setShowBirthdays(e.target.checked)} onChange={(e) => setShowBirthdays(e.target.checked)}
className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500" className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/> />
<span className="text-sm text-gray-700"> <span className="text-sm text-gray-700 dark:text-gray-300">
Aniversariantes do mês Aniversariantes do mês
</span> </span>
</label> </label>
@ -459,14 +473,14 @@ export function SecretaryPatientList({
onChange={(e) => setShowVIP(e.target.checked)} onChange={(e) => setShowVIP(e.target.checked)}
className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500" className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/> />
<span className="text-sm text-gray-700">Somente VIP</span> <span className="text-sm text-gray-700 dark:text-gray-300">Somente VIP</span>
</label> </label>
<div className="flex items-center gap-2 ml-auto"> <div className="flex items-center gap-2 ml-auto">
<span className="text-sm text-gray-600">Convênio:</span> <span className="text-sm text-gray-600 dark:text-gray-400">Convênio:</span>
<select <select
value={insuranceFilter} value={insuranceFilter}
onChange={(e) => setInsuranceFilter(e.target.value)} onChange={(e) => setInsuranceFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent" className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
> >
<option>Todos</option> <option>Todos</option>
<option>Particular</option> <option>Particular</option>
@ -479,30 +493,30 @@ export function SecretaryPatientList({
</div> </div>
{/* Table */} {/* Table */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200"> <thead className="bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<tr> <tr>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Paciente Paciente
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Próximo Atendimento Próximo Atendimento
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Convênio Convênio
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Ações Ações
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{loading ? ( {loading ? (
<tr> <tr>
<td <td
colSpan={4} colSpan={4}
className="px-6 py-12 text-center text-gray-500" className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
> >
Carregando pacientes... Carregando pacientes...
</td> </td>
@ -511,7 +525,7 @@ export function SecretaryPatientList({
<tr> <tr>
<td <td
colSpan={4} colSpan={4}
className="px-6 py-12 text-center text-gray-500" className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
> >
{searchTerm {searchTerm
? "Nenhum paciente encontrado com esse termo" ? "Nenhum paciente encontrado com esse termo"
@ -519,10 +533,10 @@ export function SecretaryPatientList({
</td> </td>
</tr> </tr>
) : ( ) : (
filteredPatients.map((patient, index) => ( paginatedPatients.map((patient, index) => (
<tr <tr
key={patient.id} key={patient.id}
className="hover:bg-gray-50 transition-colors" className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
> >
<td className="px-6 py-4"> <td className="px-6 py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -533,20 +547,20 @@ export function SecretaryPatientList({
color={getPatientColor(index)} color={getPatientColor(index)}
/> />
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{patient.full_name} {patient.full_name}
</p> </p>
<p className="text-sm text-gray-500">{patient.email}</p> <p className="text-sm text-gray-500 dark:text-gray-400">{patient.email}</p>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-400">
{patient.phone_mobile} {patient.phone_mobile}
</p> </p>
</div> </div>
</div> </div>
</td> </td>
<td className="px-6 py-4 text-sm text-gray-700"> <td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{/* TODO: Buscar próximo agendamento */} {/* TODO: Buscar próximo agendamento */}
</td> </td>
<td className="px-6 py-4 text-sm text-gray-700"> <td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{(patient as any).convenio || "Particular"} {(patient as any).convenio || "Particular"}
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-4">
@ -588,6 +602,61 @@ export function SecretaryPatientList({
</table> </table>
</div> </div>
{/* Paginação */}
{filteredPatients.length > 0 && (
<div className="flex items-center justify-between bg-white dark:bg-gray-800 px-6 py-4 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<div className="text-sm text-gray-700 dark:text-gray-300">
Mostrando {startIndex + 1} até {Math.min(endIndex, filteredPatients.length)} de {filteredPatients.length} pacientes
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Anterior
</button>
<div className="flex items-center gap-1">
{(() => {
const maxPagesToShow = 4;
let startPage = Math.max(1, currentPage - Math.floor(maxPagesToShow / 2));
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
if (endPage - startPage < maxPagesToShow - 1) {
startPage = Math.max(1, endPage - maxPagesToShow + 1);
}
const pages = [];
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return pages.map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`px-3 py-2 text-sm rounded-lg transition-colors ${
currentPage === page
? "bg-green-600 text-white"
: "border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
}`}
>
{page}
</button>
));
})()}
</div>
<button
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="px-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300"
>
Próxima
</button>
</div>
</div>
)}
{/* Modal de Formulário */} {/* Modal de Formulário */}
{showModal && ( {showModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">

View File

@ -335,8 +335,8 @@ export function SecretaryReportList() {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Relatórios</h1> <h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">Relatórios</h1>
<p className="text-gray-600 mt-1"> <p className="text-gray-600 dark:text-gray-400 mt-1">
Visualize e baixe relatórios do sistema Visualize e baixe relatórios do sistema
</p> </p>
</div> </div>
@ -350,16 +350,16 @@ export function SecretaryReportList() {
</div> </div>
{/* Search and Filters */} {/* Search and Filters */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6 space-y-4">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="flex-1 relative"> <div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 dark:text-gray-500" />
<input <input
type="text" type="text"
placeholder="Buscar relatórios..." placeholder="Buscar relatórios..."
value={searchTerm} value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)} onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent" className="w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400"
/> />
</div> </div>
<button <button
@ -370,7 +370,7 @@ export function SecretaryReportList() {
</button> </button>
<button <button
onClick={handleClear} onClick={handleClear}
className="px-6 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" className="px-6 py-2.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
> >
Limpar Limpar
</button> </button>
@ -378,11 +378,11 @@ export function SecretaryReportList() {
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-gray-600">Status:</span> <span className="text-sm text-gray-600 dark:text-gray-400">Status:</span>
<select <select
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)} onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent" className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
> >
<option value="">Todos</option> <option value="">Todos</option>
<option value="draft">Rascunho</option> <option value="draft">Rascunho</option>
@ -395,33 +395,33 @@ export function SecretaryReportList() {
</div> </div>
{/* Table */} {/* Table */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden"> <div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<table className="w-full"> <table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200"> <thead className="bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
<tr> <tr>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Relatório Relatório
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Status Status
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Criado Em Criado Em
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Solicitante Solicitante
</th> </th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider"> <th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
Ações Ações
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{loading ? ( {loading ? (
<tr> <tr>
<td <td
colSpan={5} colSpan={5}
className="px-6 py-12 text-center text-gray-500" className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
> >
Carregando relatórios... Carregando relatórios...
</td> </td>
@ -430,7 +430,7 @@ export function SecretaryReportList() {
<tr> <tr>
<td <td
colSpan={5} colSpan={5}
className="px-6 py-12 text-center text-gray-500" className="px-6 py-12 text-center text-gray-500 dark:text-gray-400"
> >
Nenhum relatório encontrado Nenhum relatório encontrado
</td> </td>
@ -439,18 +439,18 @@ export function SecretaryReportList() {
reports.map((report) => ( reports.map((report) => (
<tr <tr
key={report.id} key={report.id}
className="hover:bg-gray-50 transition-colors" className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
> >
<td className="px-6 py-4"> <td className="px-6 py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg"> <div className="p-2 bg-blue-100 dark:bg-blue-900 rounded-lg">
<FileText className="h-5 w-5 text-blue-600" /> <FileText className="h-5 w-5 text-blue-600 dark:text-blue-400" />
</div> </div>
<div> <div>
<p className="text-sm font-medium text-gray-900"> <p className="text-sm font-medium text-gray-900 dark:text-gray-100">
{report.order_number} {report.order_number}
</p> </p>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500 dark:text-gray-400">
{report.exam || "Sem exame"} {report.exam || "Sem exame"}
</p> </p>
</div> </div>
@ -460,12 +460,12 @@ export function SecretaryReportList() {
<span <span
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${ className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
report.status === "completed" report.status === "completed"
? "bg-green-100 text-green-800" ? "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
: report.status === "pending" : report.status === "pending"
? "bg-yellow-100 text-yellow-800" ? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
: report.status === "draft" : report.status === "draft"
? "bg-gray-100 text-gray-800" ? "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200"
: "bg-red-100 text-red-800" : "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
}`} }`}
> >
{report.status === "completed" {report.status === "completed"
@ -477,7 +477,7 @@ export function SecretaryReportList() {
: "Cancelado"} : "Cancelado"}
</span> </span>
</td> </td>
<td className="px-6 py-4 text-sm text-gray-700"> <td className="px-6 py-4 text-sm text-gray-700 dark:text-gray-300">
{formatDate(report.created_at)} {formatDate(report.created_at)}
</td> </td>
<td className="px-6 py-4 text-sm text-gray-700"> <td className="px-6 py-4 text-sm text-gray-700">

View File

@ -65,11 +65,32 @@ html.reduced-motion *::after {
scroll-behavior: auto !important; scroll-behavior: auto !important;
} }
/* Filtro de luz azul (aplica matiz e tonalidade amarelada) */ /* Filtro de luz azul (aplica overlay amarelada sem quebrar position: fixed) */
/* Filtro de luz azul (modo mais "padrão" com tom amarelado suave) */ html.low-blue-light {
html.low-blue-light body { position: relative;
/* Mais quente: mais sepia e matiz mais próximo do laranja */ }
filter: sepia(40%) hue-rotate(315deg) saturate(85%) brightness(98%);
html.low-blue-light::after {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(255, 220, 150, 0.25),
rgba(255, 200, 120, 0.25)
);
pointer-events: none;
z-index: 999999;
mix-blend-mode: multiply;
}
/* Garante que o menu de acessibilidade fique acima do filtro */
html.low-blue-light button[aria-label="Menu de Acessibilidade"],
html.low-blue-light [role="dialog"][aria-modal="true"] {
z-index: 9999999 !important;
} }
/* Modo foco: destaque reforçado no elemento focado, sem quebrar layout */ /* Modo foco: destaque reforçado no elemento focado, sem quebrar layout */
@ -137,50 +158,259 @@ html.focus-mode.dark *:focus-visible,
} }
} }
/* Estilos de Acessibilidade */ /* Estilos de Acessibilidade - Alto Contraste */
.high-contrast { .high-contrast {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
} }
.high-contrast body { .high-contrast body {
background-color: #000 !important; background-color: #000 !important;
color: #fff !important; color: #ffff00 !important;
} }
.high-contrast .bg-white { /* Backgrounds brancos/claros viram pretos */
.high-contrast .bg-white,
.high-contrast .bg-gray-50,
.high-contrast .bg-gray-100 {
background-color: #000 !important; background-color: #000 !important;
color: #fff !important; color: #ffff00 !important;
border: 2px solid #fff !important; border-color: #ffff00 !important;
} }
/* Backgrounds escuros ficam pretos */
.high-contrast .bg-gray-800,
.high-contrast .bg-gray-900 {
background-color: #000 !important;
color: #ffff00 !important;
}
/* Textos cinzas ficam amarelos */
.high-contrast .text-gray-400,
.high-contrast .text-gray-500,
.high-contrast .text-gray-600, .high-contrast .text-gray-600,
.high-contrast .text-gray-700, .high-contrast .text-gray-700,
.high-contrast .text-gray-800, .high-contrast .text-gray-800,
.high-contrast .text-gray-900 { .high-contrast .text-gray-900 {
color: #fff !important; color: #ffff00 !important;
} }
/* Textos brancos ficam amarelos */
.high-contrast .text-white,
.high-contrast .text-gray-100 {
color: #ffff00 !important;
}
/* Botões primários (verde/azul) */
.high-contrast .bg-blue-600, .high-contrast .bg-blue-600,
.high-contrast .bg-blue-500, .high-contrast .bg-blue-500,
.high-contrast .bg-green-600 { .high-contrast .bg-green-600,
.high-contrast .bg-green-700 {
background-color: #ffff00 !important; background-color: #ffff00 !important;
color: #000 !important; color: #000 !important;
border: 2px solid #000 !important;
font-weight: bold !important;
} }
.high-contrast a, /* Botões com bordas */
.high-contrast button:not(.bg-red-500) { .high-contrast .border-gray-300,
text-decoration: underline; .high-contrast .border-gray-600,
font-weight: bold; .high-contrast .border-gray-200,
.high-contrast .border-gray-700 {
border-color: #ffff00 !important;
} }
/* Links e botões secundários */
.high-contrast a {
color: #ffff00 !important;
text-decoration: underline !important;
font-weight: bold !important;
}
.high-contrast button {
border: 2px solid #ffff00 !important;
}
/* Inputs e selects */
.high-contrast input, .high-contrast input,
.high-contrast select, .high-contrast select,
.high-contrast textarea { .high-contrast textarea {
background-color: #fff !important; background-color: #fff !important;
color: #000 !important; color: #000 !important;
border: 3px solid #000 !important;
font-weight: bold !important;
}
.high-contrast input::placeholder,
.high-contrast textarea::placeholder {
color: #666 !important;
opacity: 1 !important;
}
/* Badges e status */
.high-contrast .bg-green-100,
.high-contrast .bg-blue-100,
.high-contrast .bg-yellow-100,
.high-contrast .bg-red-100,
.high-contrast .bg-purple-100,
.high-contrast .bg-orange-100 {
background-color: #ffff00 !important;
color: #000 !important;
border: 2px solid #000 !important; border: 2px solid #000 !important;
} }
/* Tabelas */
.high-contrast table {
border: 2px solid #ffff00 !important;
}
.high-contrast th,
.high-contrast td {
border: 1px solid #ffff00 !important;
}
.high-contrast thead {
background-color: #000 !important;
color: #ffff00 !important;
}
/* Hover states */
.high-contrast tr:hover,
.high-contrast .hover\:bg-gray-50:hover,
.high-contrast .hover\:bg-gray-100:hover {
background-color: #1a1a1a !important;
color: #ffff00 !important;
}
/* Icons devem ser visíveis */
.high-contrast svg {
color: #ffff00 !important;
stroke: currentColor;
}
/* Botões de ação com cores específicas */
.high-contrast .text-blue-600,
.high-contrast .text-green-600,
.high-contrast .text-orange-600,
.high-contrast .text-red-600,
.high-contrast .text-purple-600 {
color: #ffff00 !important;
}
.high-contrast .hover\:bg-blue-50:hover,
.high-contrast .hover\:bg-green-50:hover,
.high-contrast .hover\:bg-orange-50:hover,
.high-contrast .hover\:bg-red-50:hover {
background-color: #333 !important;
}
/* Divisores e bordas */
.high-contrast .divide-y > * {
border-color: #ffff00 !important;
}
/* Cards e containers */
.high-contrast .rounded-xl,
.high-contrast .rounded-lg {
border: 2px solid #ffff00 !important;
}
/* Modals e dialogs */
.high-contrast .shadow-xl,
.high-contrast .shadow-sm {
box-shadow: 0 0 0 3px #ffff00 !important;
}
/* Botões desabilitados */
.high-contrast button:disabled {
background-color: #333 !important;
color: #666 !important;
border-color: #666 !important;
opacity: 0.5 !important;
}
/* Paginação - página ativa */
.high-contrast .bg-green-600.text-white {
background-color: #ffff00 !important;
color: #000 !important;
border: 3px solid #000 !important;
}
/* Calendário - células cinzas */
.high-contrast .bg-gray-200 {
background-color: #000 !important;
color: #ffff00 !important;
}
/* Calendário - dias da semana e células */
.high-contrast .bg-gray-50,
.high-contrast .bg-gray-100 {
background-color: #000 !important;
color: #ffff00 !important;
}
/* Calendário - dia atual (azul claro) */
.high-contrast .bg-blue-50 {
background-color: #1a1a1a !important;
color: #ffff00 !important;
border: 3px solid #ffff00 !important;
}
/* Calendário - eventos/horários nas células */
.high-contrast .bg-blue-100,
.high-contrast .bg-green-100,
.high-contrast .text-blue-800,
.high-contrast .text-green-800,
.high-contrast .text-yellow-800,
.high-contrast .text-red-800,
.high-contrast .text-purple-800 {
background-color: #ffff00 !important;
color: #000 !important;
border: 2px solid #000 !important;
}
/* Calendário - Grid com divisórias amarelas */
.high-contrast .grid-cols-7 {
background-color: #ffff00 !important;
gap: 2px !important;
padding: 2px !important;
}
.high-contrast .grid-cols-7 > div {
background-color: #000 !important;
border: 2px solid #ffff00 !important;
color: #ffff00 !important;
}
/* Calendário - Background do grid */
.high-contrast .bg-gray-200.border.border-gray-200 {
background-color: #ffff00 !important;
border-color: #ffff00 !important;
}
/* Headers com fundo cinza */
.high-contrast .bg-gray-700 {
background-color: #000 !important;
color: #ffff00 !important;
}
/* Texto em fundos coloridos */
.high-contrast .text-blue-700,
.high-contrast .text-green-700,
.high-contrast .text-purple-700 {
color: #000 !important;
}
/* Garantir que backgrounds cinzas fiquem pretos */
.high-contrast [class*="bg-gray"] {
background-color: #000 !important;
color: #ffff00 !important;
}
/* Garantir que textos cinzas fiquem amarelos */
.high-contrast [class*="text-gray"] {
color: #ffff00 !important;
}
/* Modo Escuro Melhorado */ /* Modo Escuro Melhorado */
.dark { .dark {
color-scheme: dark; color-scheme: dark;

View File

@ -36,24 +36,24 @@ export default function PainelSecretaria() {
]; ];
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
{/* Header */} {/* Header */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-10"> <header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
<div className="max-w-[1400px] mx-auto px-6 py-4"> <div className="max-w-[1400px] mx-auto px-6 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-gray-900"> <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">
Painel da Secretaria Painel da Secretaria
</h1> </h1>
{user && ( {user && (
<p className="text-sm text-gray-600 mt-1"> <p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
Bem-vinda, {user.email} Bem-vinda, {user.email}
</p> </p>
)} )}
</div> </div>
<button <button
onClick={handleLogout} onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg transition-colors" className="flex items-center gap-2 px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
> >
<LogOut className="h-4 w-4" /> <LogOut className="h-4 w-4" />
Sair Sair
@ -63,7 +63,7 @@ export default function PainelSecretaria() {
</header> </header>
{/* Tabs Navigation */} {/* Tabs Navigation */}
<div className="bg-white border-b border-gray-200"> <div className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-[1400px] mx-auto px-6"> <div className="max-w-[1400px] mx-auto px-6">
<nav className="flex gap-2"> <nav className="flex gap-2">
{tabs.map((tab) => { {tabs.map((tab) => {
@ -75,8 +75,8 @@ export default function PainelSecretaria() {
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-3 border-b-2 transition-colors ${ className={`flex items-center gap-2 px-4 py-3 border-b-2 transition-colors ${
isActive isActive
? "border-green-600 text-green-600 font-medium" ? "border-green-600 text-green-600 dark:text-green-400 font-medium"
: "border-transparent text-gray-600 hover:text-gray-900 hover:border-gray-300" : "border-transparent text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600"
}`} }`}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />