Compare commits

...

1 Commits

Author SHA1 Message Date
fa9f7b7847 modificações nas tabelas 2025-10-22 20:12:25 -03:00
10 changed files with 1502 additions and 473 deletions

View File

@ -1,127 +1,198 @@
import API_KEY from '../components/utils/apiKeys';
import { Link } from 'react-router-dom';
import {useState, useEffect} from 'react'
import { Link } from 'react-router-dom';
import { useState, useEffect } from 'react';
import { useAuth } from '../components/utils/AuthProvider';
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js';
import TiptapViewer from './TiptapViewer';
import './styleMedico/DoctorRelatorioManager.css';
const DoctorRelatorioManager = () => {
const navigate = useNavigate()
const {getAuthorizationHeader} = useAuth();
let authHeader = getAuthorizationHeader()
const [RelatoriosFiltrados, setRelatorios] = useState([])
const [PacientesComRelatorios, setPacientesComRelatorios] = useState([])
const [showModal, setShowModal] = useState(false)
const [index, setIndex] = useState()
// 1º useEffect: Busca os dados dos pacientes após carregar os relatórios
useEffect( () => {
let pacientesDosRelatorios = []
const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth();
let authHeader = getAuthorizationHeader();
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
const [pacientesData, setPacientesData] = useState({});
const [showModal, setShowModal] = useState(false);
const [relatorioModal, setRelatorioModal] = useState(null);
const [termoPesquisa, setTermoPesquisa] = useState('');
const [filtroExame, setFiltroExame] = useState('');
const [examesDisponiveis, setExamesDisponiveis] = useState([]);
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
const ListarPacientes = async () => {
for (let i = 0; i < RelatoriosFiltrados.length; i++) {
let relatorio = RelatoriosFiltrados[i];
let paciente_id = relatorio.patient_id;
const paciente = await GetPatientByID(paciente_id, authHeader);
console.log(paciente)
if (paciente.length > 0) {
pacientesDosRelatorios.push(paciente[0]);
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
useEffect(() => {
const buscarPacientes = async () => {
const pacientesMap = {};
for (const relatorio of relatoriosOriginais) {
if (!pacientesMap[relatorio.patient_id]) {
try {
const paciente = await GetPatientByID(relatorio.patient_id, authHeader);
if (paciente && paciente.length > 0) {
pacientesMap[relatorio.patient_id] = paciente[0];
}
} catch (error) {
console.error('Erro ao buscar paciente:', error);
}
}
}
setPacientesComRelatorios(pacientesDosRelatorios);
setPacientesData(pacientesMap);
};
if (relatoriosOriginais.length > 0) {
buscarPacientes();
}
ListarPacientes()
}, [relatoriosOriginais, authHeader]);
}, [RelatoriosFiltrados, authHeader]);
// NOVO: useEffect para logar PacientesComRelatorios após a atualização
useEffect(() => {
console.log(PacientesComRelatorios, 'aqui')
}, [PacientesComRelatorios])
let resultados = relatoriosOriginais;
if (termoPesquisa.trim()) {
const termo = termoPesquisa.toLowerCase().trim();
resultados = resultados.filter(relatorio => {
const paciente = pacientesData[relatorio.patient_id];
if (!paciente) return false;
const nomeMatch = paciente.full_name?.toLowerCase().includes(termo);
const cpfMatch = paciente.cpf?.includes(termoPesquisa);
return nomeMatch || cpfMatch;
});
}
if (filtroExame.trim()) {
const termoExame = filtroExame.toLowerCase().trim();
resultados = resultados.filter(relatorio =>
relatorio.exam?.toLowerCase().includes(termoExame)
);
}
setRelatoriosFinais(resultados);
setPaginaAtual(1);
}, [termoPesquisa, filtroExame, relatoriosOriginais, pacientesData]);
// 2º useEffect: Busca a lista de relatórios
useEffect(() => {
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&status", requestOptions)
.then(response => response.json())
.then(data => { setRelatorios(data); console.log(data) })
.catch(error => console.log('error', error));
}, [authHeader])
const BaixarPDFdoRelatorio = (nome_paciente) => {
const elemento = document.getElementById("folhaA4"); // tua div do relatório
const opt = {
margin: 0,
filename: `relatorio_${nome_paciente || "paciente"}.pdf`,
html2canvas: { scale: 2 },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&status", requestOptions)
.then(response => response.json())
.then(data => {
setRelatoriosOriginais(data);
setRelatoriosFinais(data);
const examesUnicos = [...new Set(data.map(relatorio => relatorio.exam).filter(exam => exam))];
setExamesDisponiveis(examesUnicos);
})
.catch(error => console.log('error', error));
}, [authHeader]);
const totalPaginas = Math.ceil(relatoriosFinais.length / itensPorPagina);
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal);
const limparFiltros = () => {
setTermoPesquisa('');
setFiltroExame('');
setPaginaAtual(1);
};
html2pdf().set(opt).from(elemento).save();
const abrirModal = (relatorio) => {
setRelatorioModal(relatorio);
setShowModal(true);
};
const BaixarPDFdoRelatorio = (nome_paciente) => {
const elemento = document.getElementById("folhaA4");
const opt = {
margin: 0,
filename: `relatorio_${nome_paciente || "paciente"}.pdf`,
html2canvas: { scale: 2 },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
};
html2pdf().set(opt).from(elemento).save();
};
const irParaPagina = (pagina) => {
setPaginaAtual(pagina);
};
const avancarPagina = () => {
if (paginaAtual < totalPaginas) {
setPaginaAtual(paginaAtual + 1);
}
return (
};
const voltarPagina = () => {
if (paginaAtual > 1) {
setPaginaAtual(paginaAtual - 1);
}
};
const gerarNumerosPaginas = () => {
const paginas = [];
const paginasParaMostrar = 5;
let inicio = Math.max(1, paginaAtual - Math.floor(paginasParaMostrar / 2));
let fim = Math.min(totalPaginas, inicio + paginasParaMostrar - 1);
inicio = Math.max(1, fim - paginasParaMostrar + 1);
for (let i = inicio; i <= fim; i++) {
paginas.push(i);
}
return paginas;
};
return (
<div>
{showModal && (
<div className="modal" >
<div className="modal-dialog modal-tabela-relatorio">
<div className="modal-content">
<div className="modal-header text-white">
<h5 className="modal-title ">Relatório de {PacientesComRelatorios[index]?.full_name} </h5>
<button
type="button"
className="btn-close"
onClick={() => setShowModal(false)}
></button>
</div>
<div className="modal-body">
<div id="folhaA4">
<div id='header-relatorio'>
<p>Clinica Rise up</p>
<p>Dr - CRM/SP 123456</p>
<p>Avenida - (79) 9 4444-4444</p>
</div>
<div id='infoPaciente'>
<p>Paciente: {PacientesComRelatorios[index]?.full_name}</p>
<p>Data de nascimento: {PacientesComRelatorios[index]?.birth_date} </p>
<p>Data do exame: {}</p>
<p>Exame: {RelatoriosFiltrados[index]?.exam}</p>
{/* INÍCIO DA MUDANÇA (da resposta anterior) */}
<p style={{ marginTop: '15px', fontWeight: 'bold' }}>Conteúdo do Relatório:</p>
<TiptapViewer
htmlContent={
RelatoriosFiltrados[index]?.content ||
RelatoriosFiltrados[index]?.diagnosis ||
RelatoriosFiltrados[index]?.conclusion ||
'Relatório não preenchido.'
}
/>
{/* FIM DA MUDANÇA */}
</div>
<div>
<p>Dr {RelatoriosFiltrados[index]?.required_by}</p>
<p>Emitido em: 0</p>
</div>
{showModal && relatorioModal && (
<div className="modal" style={{ display: 'block', backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog modal-tabela-relatorio">
<div className="modal-content">
<div className="modal-header text-white">
<h5 className="modal-title">Relatório de {pacientesData[relatorioModal.patient_id]?.full_name} </h5>
<button type="button" className="btn-close" onClick={() => setShowModal(false)}></button>
</div>
<div className="modal-body">
<div id="folhaA4">
<div id='header-relatorio'>
<p>Clinica Rise up</p>
<p>Dr - CRM/SP 123456</p>
<p>Avenida - (79) 9 4444-4444</p>
</div>
<div id='infoPaciente'>
<p>Paciente: {pacientesData[relatorioModal.patient_id]?.full_name}</p>
<p>Data de nascimento: {pacientesData[relatorioModal.patient_id]?.birth_date} </p>
<p>Data do exame: { }</p>
<p>Exame: {relatorioModal.exam}</p>
<p style={{ marginTop: '15px', fontWeight: 'bold' }}>Conteúdo do Relatório:</p>
<TiptapViewer htmlContent={relatorioModal.content || relatorioModal.diagnosis || relatorioModal.conclusion || 'Relatório não preenchido.'} />
</div>
<div>
<p>Dr {relatorioModal.required_by}</p>
<p>Emitido em: 0</p>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name)}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button>
<button
type="button"
className="btn btn-primary"
onClick={() => {setShowModal(false)}}
>
Fechar
</button>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesData[relatorioModal.patient_id]?.full_name)}>
<i className='bi bi-file-pdf-fill'></i> baixar em pdf
</button>
<button type="button" className="btn btn-primary" onClick={() => setShowModal(false)}>Fechar</button>
</div>
</div>
</div>
</div>
)}
<div className="page-heading">
<h3>Lista de Relatórios</h3>
</div>
@ -132,9 +203,7 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
<div className="card-header d-flex justify-content-between align-items-center">
<h4 className="card-title mb-0">Relatórios Cadastrados</h4>
<Link to={'criar'}>
<button
className="btn btn-primary"
>
<button className="btn btn-primary">
<i className="bi bi-plus-circle"></i> Adicionar Relatório
</button>
</Link>
@ -142,97 +211,148 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
<div className="card-body">
<div className="card p-3 mb-3">
<h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i>{" "}
Filtros
<i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros
</h5>
<div
className="d-flex flex-nowrap align-items-center gap-2"
style={{ overflowX: "auto", paddingBottom: "6px" }}
>
<input
type="text"
className="form-control"
placeholder="Buscar por nome..."
style={{
minWidth: 250,
maxWidth: 300,
width: 260,
flex: "0 0 auto",
}}
/>
<div className="row">
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Buscar por nome ou CPF do paciente</label>
<input
type="text"
className="form-control"
placeholder="Digite nome ou CPF do paciente..."
value={termoPesquisa}
onChange={(e) => setTermoPesquisa(e.target.value)}
/>
</div>
</div>
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Filtrar por tipo de exame</label>
<input
type="text"
className="form-control"
placeholder="Digite o tipo de exame..."
value={filtroExame}
onChange={(e) => setFiltroExame(e.target.value)}
/>
</div>
</div>
<div className="col-md-2 d-flex align-items-end">
<button className="btn btn-outline-secondary w-100" onClick={limparFiltros}>
<i className="bi bi-arrow-clockwise"></i> Limpar
</button>
</div>
</div>
<div className="mt-2">
<div className="contador-relatorios">
{relatoriosFinais.length} DE {relatoriosOriginais.length} RELATÓRIOS ENCONTRADOS
</div>
</div>
</div>
<div className="table-responsive">
<table className="table table-striped table-hover">
<thead>
<tr>
<th>Paciente</th>
<th>CPF</th>
<th>Exame</th>
<th></th>
</tr>
</thead>
<tbody>
{RelatoriosFiltrados.length > 0 ? (
RelatoriosFiltrados.map((relatorio, index) => (
<tr key={relatorio.id}>
<td className='infos-paciente'>{PacientesComRelatorios[index]?.full_name}</td>
<td className='infos-paciente'>{PacientesComRelatorios[index]?.cpf}</td>
<td>{relatorio.exam}</td>
<td>
<div className="d-flex gap-2">
<button
className="btn btn-sm"
style={{
backgroundColor: "#E6F2FF",
color: "#004085",
}}
onClick={() => {
setShowModal(true); setIndex(index)
}}
>
{relatoriosPaginados.length > 0 ? (
relatoriosPaginados.map((relatorio) => {
const paciente = pacientesData[relatorio.patient_id];
return (
<tr key={relatorio.id}>
<td>{paciente?.full_name || 'Carregando...'}</td>
<td>{paciente?.cpf || 'Carregando...'}</td>
<td>{relatorio.exam}</td>
<td>
<div className="d-flex gap-2">
<button className="btn btn-sm btn-ver-detalhes" onClick={() => abrirModal(relatorio)}>
<i className="bi bi-eye me-1"></i> Ver Detalhes
</button>
<button
className="btn btn-sm"
style={{
backgroundColor: "#FFF3CD",
color: "#856404",
}}
onClick={() => {
// MANTIDO: Uso de string template para a navegação
navigate(`/medico/relatorios/${relatorio.id}/edit`)
}}
>
<button className="btn btn-sm btn-editar" onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<i className="bi bi-pencil me-1"></i> Editar
</button>
</div>
</td>
</tr>
))
</td>
</tr>
);
})
) : (
<tr>
<td colSpan="8" className="text-center">
Nenhum paciente encontrado.
<td colSpan="4" className="text-center py-4">
<div className="text-muted">
<i className="bi bi-search display-4"></i>
<p className="mt-2">Nenhum relatório encontrado com os filtros aplicados.</p>
{(termoPesquisa || filtroExame) && (
<button className="btn btn-outline-primary btn-sm mt-2" onClick={limparFiltros}>
Limpar filtros
</button>
)}
</div>
</td>
</tr>
)}
</tbody>
</table>
{relatoriosFinais.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, relatoriosFinais.length)} de {relatoriosFinais.length} itens
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div>
</div>
</div>
@ -240,6 +360,7 @@ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?patient_id&statu
</section>
</div>
</div>
)
}
export default DoctorRelatorioManager
);
};
export default DoctorRelatorioManager;

View File

@ -0,0 +1,31 @@
.contador-relatorios {
background-color: #1e3a8a;
color: white;
font-weight: bold;
font-size: 14px;
padding: 8px 12px;
border-radius: 4px;
display: inline-block;
}
.btn-ver-detalhes {
background-color: #E6F2FF;
color: #004085;
border: none;
}
.btn-ver-detalhes:hover {
background-color: #cce5ff;
color: #004085;
}
.btn-editar {
background-color: #FFF3CD;
color: #856404;
border: none;
}
.btn-editar:hover {
background-color: #ffeaa7;
color: #856404;
}

View File

@ -20,6 +20,11 @@
font-size: 24px;
cursor: pointer;
padding: 5px;
transition: transform 0.2s ease;
}
.phone-icon-container:hover {
transform: scale(1.1);
}
.phone-icon {
@ -33,75 +38,173 @@
}
.profile-picture-container {
width: 40px;
height: 40px;
width: 45px;
height: 45px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
border: 2px solid #ccc;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
border: 2px solid #007bff;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3);
transition: all 0.3s ease;
}
.profile-picture-container:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.4);
}
.profile-photo {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
display: block;
}
.profile-placeholder {
width: 100%;
height: 100%;
background-color: #A9A9A9;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.profile-placeholder::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
height: 60%;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="%23FFFFFF" d="M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm-45.7 48C79.8 304 0 383.8 0 482.3c0 16.7 13.5 30.2 30.2 30.2h387.6c16.7 0 30.2-13.5 30.2-30.2 0-98.5-79.8-178.3-178.3-178.3h-45.7z"/></svg>');
background-size: contain;
background-repeat: no-repeat;
opacity: 0.8;
.placeholder-icon {
font-size: 20px;
color: white;
}
.profile-dropdown {
position: absolute;
top: 50px;
top: 60px;
right: 0;
background-color: white;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border: 1px solid #e0e0e0;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
z-index: 1000;
min-width: 150px;
min-width: 180px;
overflow: hidden;
animation: dropdownFadeIn 0.2s ease-out;
}
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.dropdown-button {
background: none;
border: none;
padding: 10px 15px;
padding: 12px 16px;
text-align: left;
cursor: pointer;
font-size: 14px;
color: #333;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.dropdown-button:hover {
background-color: #f0f0f0;
background-color: #f8f9fa;
}
.logout-button {
color: #cc0000;
color: #dc3545;
border-top: 1px solid #f0f0f0;
}
.logout-button:hover {
background-color: #ffe0e0;
}
/* Modal de Logout */
.logout-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.logout-modal-content {
background-color: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 90%;
text-align: center;
}
.logout-modal-content h3 {
margin-bottom: 1rem;
color: #333;
font-size: 1.25rem;
}
.logout-modal-content p {
margin-bottom: 2rem;
color: #666;
line-height: 1.4;
}
.logout-modal-buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
.logout-cancel-button {
padding: 0.75rem 1.5rem;
border: 1px solid #ccc;
border-radius: 8px;
background-color: transparent;
color: #333;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.logout-cancel-button:hover {
background-color: #f0f0f0;
border-color: #999;
}
.logout-confirm-button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background-color: #dc3545;
color: white;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.logout-confirm-button:hover {
background-color: #c82333;
}
/* Suporte Card */
.suporte-card-overlay {
position: fixed;
top: 0;
@ -187,6 +290,7 @@
margin-bottom: 0;
}
/* Chat Online */
.chat-overlay {
position: fixed;
top: 0;
@ -246,6 +350,7 @@
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.fechar-chat:hover {
@ -260,6 +365,7 @@
display: flex;
flex-direction: column;
gap: 1rem;
background-color: #fafafa;
}
.mensagem {
@ -267,33 +373,53 @@
padding: 0.75rem;
border-radius: 12px;
position: relative;
animation: messageSlideIn 0.3s ease-out;
}
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.mensagem.usuario {
align-self: flex-end;
background-color: #e3f2fd;
background-color: #007bff;
color: white;
border-bottom-right-radius: 4px;
}
.mensagem.suporte {
align-self: flex-start;
background-color: #f5f5f5;
background-color: white;
border: 1px solid #e0e0e0;
border-bottom-left-radius: 4px;
}
.mensagem-texto {
margin-bottom: 0.25rem;
word-wrap: break-word;
line-height: 1.4;
}
.mensagem-hora {
font-size: 0.7rem;
color: #666;
opacity: 0.8;
text-align: right;
}
.mensagem.usuario .mensagem-hora {
color: rgba(255, 255, 255, 0.8);
}
.mensagem.suporte .mensagem-hora {
text-align: left;
color: #666;
}
.chat-input {
@ -313,93 +439,52 @@
outline: none;
font-size: 0.9rem;
background-color: white;
transition: border-color 0.2s;
}
.chat-campo:focus {
border-color: #1e3a8a;
border-color: #007bff;
}
.chat-enviar {
background-color: #1e3a8a;
background-color: #007bff;
color: white;
border: none;
padding: 0.75rem 1rem;
padding: 0.75rem 1.5rem;
border-radius: 20px;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.chat-enviar:hover {
background-color: #1e40af;
}
/* Modal de Logout */
.logout-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
background-color: #0056b3;
}
.logout-modal-content {
background-color: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 90%;
text-align: center;
}
/* Responsividade */
@media (max-width: 768px) {
.header-container {
padding: 10px 15px;
}
.logout-modal-content h3 {
margin-bottom: 1rem;
color: #333;
font-size: 1.25rem;
}
.right-corner-elements {
gap: 15px;
}
.logout-modal-content p {
margin-bottom: 2rem;
color: #666;
line-height: 1.4;
}
.profile-picture-container {
width: 40px;
height: 40px;
}
.logout-modal-buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
.suporte-card-container,
.chat-container {
margin-right: 10px;
margin-left: 10px;
}
.logout-cancel-button {
padding: 0.75rem 1.5rem;
border: 1px solid #ccc;
border-radius: 8px;
background-color: transparent;
color: #333;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.logout-cancel-button:hover {
background-color: #f0f0f0;
}
.logout-confirm-button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background-color: #dc3545;
color: white;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.logout-confirm-button:hover {
background-color: #c82333;
.suporte-card,
.chat-online {
width: calc(100vw - 20px);
max-width: none;
}
}

View File

@ -9,10 +9,31 @@ const Header = () => {
const [mensagem, setMensagem] = useState('');
const [mensagens, setMensagens] = useState([]);
const [showLogoutModal, setShowLogoutModal] = useState(false);
const [avatarUrl, setAvatarUrl] = useState(null);
const navigate = useNavigate();
const chatInputRef = useRef(null);
const mensagensContainerRef = useRef(null);
useEffect(() => {
const loadAvatar = () => {
const localAvatar = localStorage.getItem('user_avatar');
if (localAvatar) {
setAvatarUrl(localAvatar);
}
};
loadAvatar();
const handleStorageChange = () => {
loadAvatar();
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, []);
useEffect(() => {
if (isChatOpen && chatInputRef.current) {
chatInputRef.current.focus();
@ -25,7 +46,12 @@ const Header = () => {
}
}, [mensagens]);
// Funções de Logout (do seu código)
const handleLogoutCancel = () => {
setShowLogoutModal(false);
};
const handleLogoutClick = () => {
setShowLogoutModal(true);
setIsDropdownOpen(false);
@ -77,7 +103,7 @@ const Header = () => {
};
const clearAuthData = () => {
["token","authToken","userToken","access_token","user","auth","userData"].forEach(key => {
["token", "authToken", "userToken", "access_token", "user", "auth", "userData", "user_avatar"].forEach(key => {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
});
@ -91,8 +117,6 @@ const Header = () => {
}
};
const handleLogoutCancel = () => setShowLogoutModal(false);
const handleProfileClick = () => {
setIsDropdownOpen(!isDropdownOpen);
if (isSuporteCardOpen) setIsSuporteCardOpen(false);
@ -240,13 +264,27 @@ const Header = () => {
<div className="profile-section">
<div className="profile-picture-container" onClick={handleProfileClick}>
<div className="profile-placeholder"></div>
{avatarUrl ? (
<img
src={avatarUrl}
alt="Foto do perfil"
className="profile-photo"
/>
) : (
<div className="profile-placeholder">
<div className="placeholder-icon">👤</div>
</div>
)}
</div>
{isDropdownOpen && (
<div className="profile-dropdown">
<button type="button" onClick={handleViewProfile} className="dropdown-button">Ver Perfil</button>
<button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">Sair (Logout)</button>
<button type="button" onClick={handleViewProfile} className="dropdown-button">
Ver Perfil
</button>
<button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">
Sair (Logout)
</button>
</div>
)}
</div>

View File

@ -12,7 +12,6 @@ function TableDoctor() {
const [filtroEspecialidade, setFiltroEspecialidade] = useState("Todos");
const [filtroAniversariante, setFiltroAniversariante] = useState(false);
const [showFiltrosAvancados, setShowFiltrosAvancados] = useState(false);
const [filtroCidade, setFiltroCidade] = useState("");
const [filtroEstado, setFiltroEstado] = useState("");
@ -22,6 +21,9 @@ function TableDoctor() {
const [dataFinal, setDataFinal] = useState("");
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedDoctorId, setSelectedDoctorId] = useState(null);
@ -36,9 +38,9 @@ function TableDoctor() {
setIdadeMaxima("");
setDataInicial("");
setDataFinal("");
setPaginaAtual(1);
};
const deleteDoctor = async (id) => {
const authHeader = getAuthorizationHeader()
console.log(id, 'teu id')
@ -63,7 +65,6 @@ function TableDoctor() {
}
};
const ehAniversariante = (dataNascimento) => {
if (!dataNascimento) return false;
const hoje = new Date();
@ -75,7 +76,6 @@ function TableDoctor() {
);
};
const calcularIdade = (dataNascimento) => {
if (!dataNascimento) return 0;
const hoje = new Date();
@ -108,14 +108,12 @@ function TableDoctor() {
.catch(error => console.log('error', error));
}, [isAuthenticated, getAuthorizationHeader]);
const medicosFiltrados = Array.isArray(medicos) ? medicos.filter((medico) => {
const buscaNome = medico.full_name?.toLowerCase().includes(search.toLowerCase());
const buscaCPF = medico.cpf?.toLowerCase().includes(search.toLowerCase());
const buscaEmail = medico.email?.toLowerCase().includes(search.toLowerCase());
const passaBusca = search === "" || buscaNome || buscaCPF || buscaEmail;
const passaEspecialidade = filtroEspecialidade === "Todos" || medico.specialty === filtroEspecialidade;
const passaAniversario = filtroAniversariante
@ -132,23 +130,62 @@ function TableDoctor() {
const passaIdadeMinima = idadeMinima ? idade >= parseInt(idadeMinima) : true;
const passaIdadeMaxima = idadeMaxima ? idade <= parseInt(idadeMaxima) : true;
const passaDataInicial = dataInicial ?
medico.created_at && new Date(medico.created_at) >= new Date(dataInicial) : true;
const passaDataFinal = dataFinal ?
medico.created_at && new Date(medico.created_at) <= new Date(dataFinal) : true;
const resultado = passaBusca && passaEspecialidade && passaAniversario &&
passaCidade && passaEstado && passaIdadeMinima && passaIdadeMaxima &&
passaDataInicial && passaDataFinal;
return resultado;
}) : [];
const totalPaginas = Math.ceil(medicosFiltrados.length / itensPorPagina);
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const medicosPaginados = medicosFiltrados.slice(indiceInicial, indiceFinal);
const irParaPagina = (pagina) => {
setPaginaAtual(pagina);
};
const avancarPagina = () => {
if (paginaAtual < totalPaginas) {
setPaginaAtual(paginaAtual + 1);
}
};
const voltarPagina = () => {
if (paginaAtual > 1) {
setPaginaAtual(paginaAtual - 1);
}
};
const gerarNumerosPaginas = () => {
const paginas = [];
const paginasParaMostrar = 5;
let inicio = Math.max(1, paginaAtual - Math.floor(paginasParaMostrar / 2));
let fim = Math.min(totalPaginas, inicio + paginasParaMostrar - 1);
inicio = Math.max(1, fim - paginasParaMostrar + 1);
for (let i = inicio; i <= fim; i++) {
paginas.push(i);
}
return paginas;
};
useEffect(() => {
console.log(` Médicos totais: ${medicos.length}, Filtrados: ${medicosFiltrados.length}`);
}, [medicos, medicosFiltrados, search]);
setPaginaAtual(1);
}, [search, filtroEspecialidade, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal]);
return (
<>
@ -169,7 +206,6 @@ function TableDoctor() {
</div>
<div className="card-body">
<div className="card p-3 mb-3 table-doctor-filters">
<h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i>{" "}
@ -180,16 +216,15 @@ function TableDoctor() {
<input
type="text"
className="form-control"
placeholder="Buscar por nome ou CPF..."
placeholder="Buscar por nome, CPF ou email..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<small className="text-muted">
Digite o nome completo ou número do CPF
Digite o nome completo, CPF ou email
</small>
</div>
<div className="filtros-basicos">
<select
className="form-select filter-especialidade"
@ -213,7 +248,6 @@ function TableDoctor() {
</select>
<div className="filter-buttons-container">
<button
className={`btn filter-btn ${filtroAniversariante
? "btn-primary"
@ -243,13 +277,11 @@ function TableDoctor() {
</button>
</div>
{showFiltrosAvancados && (
<div className="mt-3 p-3 border rounded advanced-filters">
<h6 className="mb-3">Filtros Avançados</h6>
<div className="row g-3">
<div className="col-md-6">
<label className="form-label fw-bold">Cidade</label>
<input
@ -296,7 +328,6 @@ function TableDoctor() {
/>
</div>
{/* Data de Cadastro */}
<div className="col-md-6">
<label className="form-label fw-bold">Data inicial</label>
<input
@ -318,17 +349,21 @@ function TableDoctor() {
</div>
</div>
)}
<div className="mt-3">
<div className="contador-medicos">
{medicosFiltrados.length} DE {medicos.length} MÉDICOS ENCONTRADOS
</div>
</div>
</div>
{(search || filtroEspecialidade !== "Todos" || filtroAniversariante || // filtroVIP removido
{(search || filtroEspecialidade !== "Todos" || filtroAniversariante ||
filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
<div className="alert alert-info mb-3 filters-active">
<strong>Filtros ativos:</strong>
<div className="mt-1">
{search && <span className="badge bg-primary me-2">Busca: "{search}"</span>}
{filtroEspecialidade !== "Todos" && <span className="badge bg-primary me-2">Especialidade: {filtroEspecialidade}</span>}
{filtroAniversariante && <span className="badge bg-primary me-2">Aniversariantes</span>}
{filtroCidade && <span className="badge bg-primary me-2">Cidade: {filtroCidade}</span>}
{filtroEstado && <span className="badge bg-primary me-2">Estado: {filtroEstado}</span>}
@ -340,14 +375,6 @@ function TableDoctor() {
</div>
)}
<div className="mb-3">
<span className="badge results-badge">
{medicosFiltrados.length} de {medicos.length} médicos encontrados
</span>
</div>
<div className="table-responsive">
<table className="table table-striped table-hover table-doctor-table">
<thead>
@ -360,8 +387,8 @@ function TableDoctor() {
</tr>
</thead>
<tbody>
{medicosFiltrados.length > 0 ? (
medicosFiltrados.map((medico) => (
{medicosPaginados.length > 0 ? (
medicosPaginados.map((medico) => (
<tr key={medico.id}>
<td>
<div className="d-flex align-items-center">
@ -371,7 +398,6 @@ function TableDoctor() {
<i className="bi bi-gift"></i>
</span>
)}
</div>
</td>
<td>{medico.cpf}</td>
@ -410,13 +436,75 @@ function TableDoctor() {
))
) : (
<tr>
<td colSpan="5" className="empty-state">
Nenhum médico encontrado.
<td colSpan="5" className="text-center py-4">
<div className="text-muted">
<i className="bi bi-search display-4"></i>
<p className="mt-2">Nenhum médico encontrado com os filtros aplicados.</p>
{(search || filtroEspecialidade !== "Todos" || filtroAniversariante ||
filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
<button className="btn btn-outline-primary btn-sm mt-2" onClick={limparFiltros}>
Limpar filtros
</button>
)}
</div>
</td>
</tr>
)}
</tbody>
</table>
{/* Paginação */}
{medicosFiltrados.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, medicosFiltrados.length)} de {medicosFiltrados.length} médicos
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div>
</div>
</div>

View File

@ -1,38 +1,186 @@
// src/pages/ProfilePage.jsx
import React, { useState } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import "./style/ProfilePage.css";
const simulatedUserData = {
email: "admin@squad23.com",
role: "Administrador",
const MOCK_API_BASE_URL = "https://mock.apidog.com/m1/1053378-0-default";
const getLocalAvatar = () => localStorage.getItem('user_avatar');
const setLocalAvatar = (avatarData) => localStorage.setItem('user_avatar', avatarData);
const clearLocalAvatar = () => localStorage.removeItem('user_avatar');
const ROLES = {
ADMIN: "Administrador",
SECRETARY: "Secretária",
DOCTOR: "Médico",
FINANCIAL: "Financeiro"
};
const ProfilePage = () => {
const location = useLocation();
const navigate = useNavigate();
const getRoleFromPath = () => {
const getRoleFromPath = useCallback(() => {
const path = location.pathname;
if (path.includes("/admin")) return "Administrador";
if (path.includes("/secretaria")) return "Secretária";
if (path.includes("/medico")) return "Médico";
if (path.includes("/financeiro")) return "Financeiro";
return "Usuário Padrão";
};
if (path.includes("/admin")) return ROLES.ADMIN;
if (path.includes("/secretaria")) return ROLES.SECRETARY;
if (path.includes("/medico")) return ROLES.DOCTOR;
if (path.includes("/financeiro")) return ROLES.FINANCIAL;
return "Usuário";
}, [location.pathname]);
const userRole = simulatedUserData.role || getRoleFromPath();
const userEmail = simulatedUserData.email || "email.nao.encontrado@example.com";
const userRole = getRoleFromPath();
const [userName, setUserName] = useState("Admin Padrão");
const [userEmail, setUserEmail] = useState("admin@squad23.com");
const [avatarUrl, setAvatarUrl] = useState(null);
const [isEditingName, setIsEditingName] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState(null);
const handleNameKeyDown = (e) => {
if (e.key === "Enter") setIsEditingName(false);
useEffect(() => {
const handleEscKey = (event) => {
if (event.keyCode === 27) handleClose();
};
document.addEventListener('keydown', handleEscKey);
return () => document.removeEventListener('keydown', handleEscKey);
}, []);
useEffect(() => {
const loadProfileData = () => {
const localAvatar = getLocalAvatar();
if (localAvatar) {
setAvatarUrl(localAvatar);
}
};
loadProfileData();
}, []);
const handleNameSave = () => {
if (userName.trim() === "") {
setError("Nome não pode estar vazio");
return;
}
setIsEditingName(false);
setError(null);
};
const handleNameKeyDown = (event) => {
if (event.key === "Enter") handleNameSave();
if (event.key === "Escape") {
setUserName("Admin Padrão");
setIsEditingName(false);
setError(null);
}
};
const handleClose = () => navigate(-1);
const handleAvatarUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
setError(null);
const MAX_FILE_SIZE = 5 * 1024 * 1024;
const ACCEPTED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (file.size > MAX_FILE_SIZE) {
setError("Arquivo muito grande. Máximo 5MB.");
return;
}
if (!ACCEPTED_TYPES.includes(file.type)) {
setError("Tipo de arquivo não suportado. Use JPEG, PNG, GIF ou WebP.");
return;
}
setIsUploading(true);
try {
try {
const result = await uploadAvatarToMockAPI(file);
const newAvatarUrl = result.url || result.avatarUrl;
if (newAvatarUrl) {
setAvatarUrl(newAvatarUrl);
setLocalAvatar(newAvatarUrl);
console.log('Avatar enviado para API com sucesso');
return;
}
} catch (apiError) {
console.log('API não disponível, salvando localmente...');
}
const reader = new FileReader();
reader.onload = (e) => {
const imageDataUrl = e.target.result;
setLocalAvatar(imageDataUrl);
setAvatarUrl(imageDataUrl);
console.log('Avatar salvo localmente');
};
reader.readAsDataURL(file);
} catch (error) {
console.error('Erro no processamento:', error);
const reader = new FileReader();
reader.onload = (e) => {
const imageDataUrl = e.target.result;
setLocalAvatar(imageDataUrl);
setAvatarUrl(imageDataUrl);
};
reader.readAsDataURL(file);
} finally {
setIsUploading(false);
event.target.value = '';
}
};
const uploadAvatarToMockAPI = async (file) => {
const formData = new FormData();
formData.append("avatar", file);
const response = await fetch(`${MOCK_API_BASE_URL}/storage/v1/object/avatars/[path]`, {
method: "POST",
body: formData
});
if (!response.ok) {
return null;
}
return await response.json();
};
const clearAvatar = () => {
fetch(`${MOCK_API_BASE_URL}/storage/v1/object/avatars/[path]`, {
method: "DELETE"
}).catch(() => {
});
clearLocalAvatar();
setAvatarUrl(null);
};
return (
<div className="profile-overlay" role="dialog" aria-modal="true">
<div className="profile-modal">
@ -47,54 +195,108 @@ const ProfilePage = () => {
<div className="profile-content">
<div className="profile-left">
<div className="avatar-wrapper">
<div className="avatar-square" />
<button
className="avatar-edit-btn"
title="Editar foto"
aria-label="Editar foto"
type="button"
<div className="avatar-square">
{avatarUrl ? (
<img
src={avatarUrl}
alt="Avatar do usuário"
className="avatar-img"
onError={() => {
setAvatarUrl(null);
clearLocalAvatar();
}}
/>
) : (
<div className="avatar-placeholder">
{userName.split(' ').map(n => n[0]).join('').toUpperCase()}
</div>
)}
</div>
<label
className={`avatar-edit-btn ${isUploading ? 'uploading' : ''}`}
title="Alterar foto de perfil"
>
</button>
{isUploading ? 'Enviando...' : 'Alterar Foto'}
<input
type="file"
accept="image/*"
onChange={handleAvatarUpload}
disabled={isUploading}
style={{ display: "none" }}
/>
</label>
{isUploading && (
<p className="upload-status">
Processando imagem...
</p>
)}
</div>
</div>
<div className="profile-right">
<div className="profile-name-row">
{isEditingName ? (
<input
className="profile-name-input"
value={userName}
onChange={(e) => setUserName(e.target.value)}
onBlur={() => setIsEditingName(false)}
onKeyDown={handleNameKeyDown}
autoFocus
/>
<div className="name-edit-wrapper">
<input
className="profile-name-input"
value={userName}
onChange={(e) => setUserName(e.target.value)}
onBlur={handleNameSave}
onKeyDown={handleNameKeyDown}
autoFocus
maxLength={50}
/>
<div className="name-edit-hint">
Pressione Enter para salvar, ESC para cancelar
</div>
</div>
) : (
<h2 className="profile-username">{userName}</h2>
<h2 className="profile-username">
{userName}
</h2>
)}
<button
className="profile-edit-inline"
onClick={() => setIsEditingName(!isEditingName)}
aria-label="Editar nome"
type="button"
aria-label={isEditingName ? 'Cancelar edição' : 'Editar nome'}
>
{isEditingName ? 'Cancelar' : 'Editar'}
</button>
</div>
<p className="profile-email">
Email: <strong>{userEmail}</strong>
</p>
{error && (
<div className="error-message">
{error}
</div>
)}
<p className="profile-role">
Cargo: <strong>{userRole}</strong>
</p>
<div className="profile-info">
<p className="profile-email">
<span>Email:</span>
<strong>{userEmail}</strong>
</p>
<div className="profile-actions-row">
<button className="btn btn-close" onClick={handleClose}>
Fechar
<p className="profile-role">
<span>Cargo:</span>
<strong>{userRole}</strong>
</p>
</div>
<div className="profile-actions">
{avatarUrl && (
<button onClick={clearAvatar} className="btn btn-clear">
Remover Avatar
</button>
)}
<button
className="btn btn-close"
onClick={handleClose}
>
Fechar Perfil
</button>
</div>
</div>

View File

@ -21,6 +21,10 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
const [dataInicial, setDataInicial] = useState("");
const [dataFinal, setDataFinal] = useState("");
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedPatientId, setSelectedPatientId] = useState(null);
@ -155,6 +159,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
setIdadeMaxima("");
setDataInicial("");
setDataFinal("");
setPaginaAtual(1);
};
const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => {
@ -198,9 +203,50 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
return resultado;
}) : [];
const totalPaginas = Math.ceil(pacientesFiltrados.length / itensPorPagina);
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const pacientesPaginados = pacientesFiltrados.slice(indiceInicial, indiceFinal);
const irParaPagina = (pagina) => {
setPaginaAtual(pagina);
};
const avancarPagina = () => {
if (paginaAtual < totalPaginas) {
setPaginaAtual(paginaAtual + 1);
}
};
const voltarPagina = () => {
if (paginaAtual > 1) {
setPaginaAtual(paginaAtual - 1);
}
};
const gerarNumerosPaginas = () => {
const paginas = [];
const paginasParaMostrar = 5;
let inicio = Math.max(1, paginaAtual - Math.floor(paginasParaMostrar / 2));
let fim = Math.min(totalPaginas, inicio + paginasParaMostrar - 1);
inicio = Math.max(1, fim - paginasParaMostrar + 1);
for (let i = inicio; i <= fim; i++) {
paginas.push(i);
}
return paginas;
};
useEffect(() => {
console.log(` Pacientes totais: ${pacientes.length}, Filtrados: ${pacientesFiltrados.length}`);
}, [pacientes, pacientesFiltrados, search]);
setPaginaAtual(1);
}, [search, filtroConvenio, filtroVIP, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal]);
return (
<>
@ -255,6 +301,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
<button
className={`btn btn-sm ${filtroVIP ? "btn-primary" : "btn-outline-primary"}`}
onClick={() => setFiltroVIP(!filtroVIP)}
style={{ padding: "0.25rem 0.5rem" }}
>
<i className="bi bi-award me-1"></i> VIP
@ -359,6 +406,12 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</div>
</div>
)}
<div className="mt-3">
<div className="contador-pacientes">
{pacientesFiltrados.length} DE {pacientes.length} PACIENTES ENCONTRADOS
</div>
</div>
</div>
{(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante ||
@ -380,12 +433,6 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</div>
)}
<div className="mb-3">
<span className="badge results-badge">
{pacientesFiltrados.length} de {pacientes.length} pacientes encontrados
</span>
</div>
<div className="table-responsive">
<table className="table table-striped table-hover table-paciente-table">
<thead>
@ -398,8 +445,8 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</tr>
</thead>
<tbody>
{pacientesFiltrados.length > 0 ? (
pacientesFiltrados.map((paciente) => (
{pacientesPaginados.length > 0 ? (
pacientesPaginados.map((paciente) => (
<tr key={paciente.id}>
<td>
<div className="d-flex align-items-center patient-name-container">
@ -454,13 +501,75 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
))
) : (
<tr>
<td colSpan="5" className="empty-state">
Nenhum paciente encontrado.
<td colSpan="5" className="text-center py-4">
<div className="text-muted">
<i className="bi bi-search display-4"></i>
<p className="mt-2">Nenhum paciente encontrado com os filtros aplicados.</p>
{(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante ||
filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
<button className="btn btn-outline-primary btn-sm mt-2" onClick={limparFiltros}>
Limpar filtros
</button>
)}
</div>
</td>
</tr>
)}
</tbody>
</table>
{/* Paginação */}
{pacientesFiltrados.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, pacientesFiltrados.length)} de {pacientesFiltrados.length} pacientes
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
/* src/pages/ProfilePage.css */
/* Overlay que cobre toda a tela */
/* Overlay */
.profile-overlay {
position: fixed;
inset: 0;
@ -8,171 +8,318 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 20000; /* acima de header, vlibras, botões de acessibilidade */
z-index: 20000;
padding: 20px;
box-sizing: border-box;
}
/* Card central (estilo modal amplo parecido com a 4ª foto) */
/* Modal */
.profile-modal {
background: #ffffff;
border-radius: 10px;
padding: 18px;
width: min(1100px, 96%);
max-width: 1100px;
border-radius: 12px;
padding: 20px;
width: min(600px, 96%);
max-width: 600px;
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.5);
position: relative;
box-sizing: border-box;
overflow: visible;
}
/* Botão fechar (X) no canto do card */
/* Botão fechar */
.profile-close {
position: absolute;
top: 14px;
right: 14px;
top: 15px;
right: 15px;
background: none;
border: none;
font-size: 26px;
font-size: 24px;
color: #666;
cursor: pointer;
line-height: 1;
padding: 5px;
}
/* Conteúdo dividido em 2 colunas: esquerda avatar / direita infos */
.profile-close:hover {
color: #333;
}
/* Layout */
.profile-content {
display: flex;
gap: 28px;
gap: 30px;
align-items: flex-start;
padding: 22px 18px;
padding: 20px 10px;
}
/* Coluna esquerda - avatar */
/* Avatar */
.profile-left {
width: 220px;
width: 160px;
display: flex;
justify-content: center;
}
/* Avatar quadrado com sombra (estilo da foto 4) */
.avatar-wrapper {
position: relative;
width: 180px;
height: 180px;
width: 140px;
height: 140px;
}
.avatar-square {
width: 100%;
height: 100%;
border-radius: 8px;
background-color: #d0d0d0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 448 512'><path fill='%23FFFFFF' d='M224 256c70.7 0 128-57.3 128-128S294.7 0 224 0 96 57.3 96 128s57.3 128 128 128zm-45.7 48C79.8 304 0 383.8 0 482.3c0 16.7 13.5 30.2 30.2 30.2h387.6c16.7 0 30.2-13.5 30.2-30.2 0-98.5-79.8-178.3-178.3-178.3h-45.7z'/></svg>");
background-position: center;
background-repeat: no-repeat;
background-size: 55%;
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.avatar-placeholder {
font-size: 2rem;
font-weight: bold;
color: white;
}
/* Botão editar sobre o avatar — círculo pequeno */
.avatar-edit-btn {
position: absolute;
right: -8px;
bottom: -8px;
transform: translate(0, 0);
border: none;
background: #ffffff;
padding: 8px 9px;
padding: 8px;
border-radius: 50%;
box-shadow: 0 6px 14px rgba(0,0,0,0.18);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
cursor: pointer;
font-size: 0.95rem;
line-height: 1;
font-size: 0.9rem;
transition: all 0.2s ease;
}
/* Coluna direita - informações */
.avatar-edit-btn:hover {
background: #f0f0f0;
transform: scale(1.1);
}
.avatar-edit-btn.uploading {
background: #ffd700;
}
.upload-status {
position: absolute;
bottom: -25px;
left: 0;
right: 0;
text-align: center;
font-size: 0.8rem;
color: #666;
}
/* Informações */
.profile-right {
flex: 1;
min-width: 280px;
display: flex;
flex-direction: column;
justify-content: center;
min-width: 250px;
}
/* Nome e botão de editar inline */
.profile-name-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
margin-bottom: 15px;
}
.profile-username {
margin: 0;
font-size: 1.9rem;
font-size: 1.8rem;
color: #222;
font-weight: 600;
}
.profile-edit-inline {
background: none;
border: none;
cursor: pointer;
font-size: 1.05rem;
font-size: 1rem;
color: #444;
padding: 5px;
border-radius: 4px;
}
.profile-edit-inline:hover {
background: #f5f5f5;
}
/* Edição de nome */
.name-edit-wrapper {
width: 100%;
}
/* input de edição do nome */
.profile-name-input {
font-size: 1.6rem;
padding: 6px 8px;
border: 1px solid #e0e0e0;
padding: 5px 8px;
border: 2px solid #007bff;
border-radius: 6px;
width: 100%;
font-weight: 600;
color: #222;
}
.profile-name-input:focus {
outline: none;
border-color: #0056b3;
}
.name-edit-hint {
font-size: 0.75rem;
color: #666;
margin-top: 5px;
}
/* Informações do perfil */
.profile-info {
margin: 20px 0;
}
/* email/role */
.profile-email,
.profile-role {
margin: 6px 0;
margin: 8px 0;
color: #555;
font-size: 1rem;
}
.profile-role {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid #f1f1f1;
color: #333;
}
/* ações (apenas fechar aqui) */
.profile-actions-row {
display: flex;
gap: 12px;
margin-top: 18px;
gap: 8px;
}
.profile-email span,
.profile-role span {
color: #777;
min-width: 50px;
}
/* Mensagem de erro */
.error-message {
background-color: #fee;
color: #c33;
padding: 10px;
border-radius: 6px;
border: 1px solid #fcc;
margin: 15px 0;
font-size: 0.9rem;
}
/* Ações */
.profile-actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
/* botões */
.btn {
padding: 8px 14px;
border-radius: 8px;
border: 1px solid transparent;
padding: 10px 20px;
border-radius: 6px;
border: none;
cursor: pointer;
font-size: 0.95rem;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
}
.btn-close {
background: #f0f0f0;
color: #222;
border: 1px solid #e6e6e6;
}
/* responsividade */
@media (max-width: 880px) {
.btn-close:hover {
background: #e0e0e0;
}
.btn-clear {
background: #dc3545;
color: white;
}
.btn-clear:hover {
background: #c82333;
}
/* Responsividade */
@media (max-width: 680px) {
.profile-content {
flex-direction: column;
gap: 14px;
gap: 20px;
align-items: center;
text-align: center;
}
.profile-left {
width: 100%;
}
.avatar-wrapper {
margin: 0 auto;
}
.profile-email,
.profile-role {
justify-content: center;
}
.profile-actions {
justify-content: center;
}
.profile-left { width: 100%; }
.avatar-wrapper { width: 140px; height: 140px; }
.profile-right { width: 100%; text-align: center; }
}
@media (max-width: 480px) {
.profile-modal {
padding: 15px;
}
.profile-content {
padding: 10px 5px;
}
.profile-username {
font-size: 1.5rem;
}
.avatar-wrapper {
width: 120px;
height: 120px;
}
}.avatar-edit-btn {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background-color 0.2s;
}
.avatar-edit-btn:hover {
background: #0056b3;
}
.avatar-edit-btn.uploading {
background: #6c757d;
cursor: not-allowed;
}
.profile-edit-inline {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
margin-left: 10px;
}
.profile-edit-inline:hover {
background: #e9ecef;
}

View File

@ -1,4 +1,3 @@
.table-doctor-container {
line-height: 2.5;
}
@ -58,8 +57,6 @@
font-weight: 500;
}
.results-badge {
background-color: #1e3a8a;
color: white;
@ -75,7 +72,6 @@
font-size: 0.75em;
}
.btn-view {
background-color: #E6F2FF !important;
color: #004085 !important;
@ -115,7 +111,6 @@
border-color: #ED969E;
}
.advanced-filters {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
@ -132,7 +127,6 @@
font-size: 0.875rem;
}
.delete-modal .modal-header {
background-color: rgba(220, 53, 69, 0.1);
border-bottom: 1px solid rgba(220, 53, 69, 0.2);
@ -143,7 +137,6 @@
font-weight: 600;
}
.filter-especialidade {
min-width: 180px !important;
max-width: 200px;
@ -160,7 +153,6 @@
padding: 0.375rem 0.75rem;
}
.filtros-basicos {
display: flex;
flex-wrap: wrap;
@ -168,7 +160,6 @@
gap: 0.75rem;
}
@media (max-width: 768px) {
.table-doctor-table {
font-size: 0.875rem;
@ -207,7 +198,6 @@
}
}
.empty-state {
padding: 2rem;
text-align: center;
@ -224,7 +214,6 @@
padding: 0.4em 0.65em;
}
.table-doctor-table tbody tr {
transition: background-color 0.15s ease-in-out;
}
@ -234,3 +223,116 @@
.btn-delete {
transition: all 0.15s ease-in-out;
}
/* ===== ESTILOS PARA PAGINAÇÃO ===== */
.contador-medicos {
background-color: #1e3a8a;
color: white;
padding: 0.5em 0.75em;
font-size: 0.875em;
font-weight: 500;
border-radius: 0.375rem;
text-align: center;
display: inline-block;
}
/* Estilos para a paginação */
.pagination {
margin-bottom: 0;
}
.page-link {
color: #495057;
border: 1px solid #dee2e6;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.page-link:hover {
color: #1e3a8a;
background-color: #e9ecef;
border-color: #dee2e6;
}
.page-item.active .page-link {
background-color: #1e3a8a;
border-color: #1e3a8a;
color: white;
}
.page-item.disabled .page-link {
color: #6c757d;
background-color: #f8f9fa;
border-color: #dee2e6;
}
/* Ajustes para a seção de paginação */
.d-flex.justify-content-between.align-items-center {
border-top: 1px solid #dee2e6;
padding-top: 1rem;
margin-top: 1rem;
}
/* Estilos para empty state */
.text-center.py-4 .text-muted {
padding: 2rem;
}
.text-center.py-4 .bi-search {
font-size: 3rem;
opacity: 0.5;
}
.text-center.py-4 p {
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.text-center.py-4 td {
border-bottom: none;
padding: 2rem !important;
}
/* Responsividade para paginação */
@media (max-width: 768px) {
.d-flex.justify-content-between.align-items-center {
flex-direction: column;
gap: 1rem;
align-items: stretch !important;
}
.d-flex.justify-content-between.align-items-center > div {
justify-content: center !important;
}
.pagination {
flex-wrap: wrap;
justify-content: center;
}
.me-3.text-muted {
text-align: center;
margin-bottom: 0.5rem;
font-size: 0.8rem;
}
.contador-medicos {
font-size: 0.8rem;
padding: 0.4em 0.6em;
}
}
/* Ajuste para o select de itens por página */
.form-select.form-select-sm.w-auto {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
font-size: 0.875rem;
}
/* Melhorar a aparência dos badges de filtros ativos */
.filters-active .badge {
font-size: 0.75em;
padding: 0.4em 0.65em;
margin-bottom: 0.25rem;
}

View File

@ -1,4 +1,3 @@
.table-paciente-container {
line-height: 2.5;
}
@ -49,7 +48,6 @@
background-color: rgba(0, 0, 0, 0.025);
}
.insurance-badge {
background-color: #6c757d !important;
color: white !important;
@ -81,7 +79,6 @@
font-size: 0.75em;
}
.btn-view {
background-color: #E6F2FF !important;
color: #004085 !important;
@ -121,7 +118,6 @@
border-color: #ED969E;
}
.advanced-filters {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
@ -148,7 +144,6 @@
font-weight: 600;
}
.empty-state {
padding: 2rem;
text-align: center;
@ -165,7 +160,6 @@
padding: 0.4em 0.65em;
}
.table-paciente-table tbody tr {
transition: background-color 0.15s ease-in-out;
}
@ -176,7 +170,6 @@
transition: all 0.15s ease-in-out;
}
@media (max-width: 768px) {
.table-paciente-table {
font-size: 0.875rem;
@ -213,6 +206,7 @@
margin-left: 0 !important;
}
}
.compact-select {
font-size: 1.0rem;
padding: 0.45rem 0.5rem;
@ -227,8 +221,120 @@
white-space: nowrap;
}
.table-paciente-filters .d-flex {
align-items: center;
gap: 8px;
}
/* ===== ESTILOS PARA PAGINAÇÃO ===== */
.contador-pacientes {
background-color: #1e3a8a;
color: white;
padding: 0.5em 0.75em;
font-size: 0.875em;
font-weight: 500;
border-radius: 0.375rem;
text-align: center;
display: inline-block;
}
/* Estilos para a paginação */
.pagination {
margin-bottom: 0;
}
.page-link {
color: #495057;
border: 1px solid #dee2e6;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.page-link:hover {
color: #1e3a8a;
background-color: #e9ecef;
border-color: #dee2e6;
}
.page-item.active .page-link {
background-color: #1e3a8a;
border-color: #1e3a8a;
color: white;
}
.page-item.disabled .page-link {
color: #6c757d;
background-color: #f8f9fa;
border-color: #dee2e6;
}
/* Ajustes para a seção de paginação */
.d-flex.justify-content-between.align-items-center {
border-top: 1px solid #dee2e6;
padding-top: 1rem;
margin-top: 1rem;
}
/* Estilos para empty state */
.text-center.py-4 .text-muted {
padding: 2rem;
}
.text-center.py-4 .bi-search {
font-size: 3rem;
opacity: 0.5;
}
.text-center.py-4 p {
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.text-center.py-4 td {
border-bottom: none;
padding: 2rem !important;
}
/* Responsividade para paginação */
@media (max-width: 768px) {
.d-flex.justify-content-between.align-items-center {
flex-direction: column;
gap: 1rem;
align-items: stretch !important;
}
.d-flex.justify-content-between.align-items-center > div {
justify-content: center !important;
}
.pagination {
flex-wrap: wrap;
justify-content: center;
}
.me-3.text-muted {
text-align: center;
margin-bottom: 0.5rem;
font-size: 0.8rem;
}
.contador-pacientes {
font-size: 0.8rem;
padding: 0.4em 0.6em;
}
}
/* Ajuste para o select de itens por página */
.form-select.form-select-sm.w-auto {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
font-size: 0.875rem;
}
/* Melhorar a aparência dos badges de filtros ativos */
.filters-active .badge {
font-size: 0.75em;
padding: 0.4em 0.65em;
margin-bottom: 0.25rem;
}