Atualização semanal
This commit is contained in:
parent
15dfdc61e5
commit
c180e9a5c9
6245
package-lock.json
generated
6245
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,7 @@
|
||||
"react-flatpickr": "^4.0.11",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-input-mask": "^2.0.4",
|
||||
"react-is": "^19.2.0",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-router-dom": "^7.9.2",
|
||||
"react-scripts": "5.0.1",
|
||||
|
||||
@ -1,96 +1,164 @@
|
||||
// src/PagesMedico/DoctorRelatorioManager.jsx
|
||||
import API_KEY from '../components/utils/apiKeys';
|
||||
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 { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
|
||||
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();
|
||||
const authHeader = getAuthorizationHeader();
|
||||
const [RelatoriosFiltrados, setRelatorios] = useState([]);
|
||||
const [PacientesComRelatorios, setPacientesComRelatorios] = useState([]);
|
||||
const [MedicosComRelatorios, setMedicosComRelatorios] = useState([]);
|
||||
let authHeader = getAuthorizationHeader();
|
||||
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
|
||||
const [pacientesData, setPacientesData] = useState({});
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [index, setIndex] = useState();
|
||||
const [relatorioModal, setRelatorioModal] = useState(null);
|
||||
const [termoPesquisa, setTermoPesquisa] = useState('');
|
||||
const [filtroExame, setFiltroExame] = useState('');
|
||||
const [examesDisponiveis, setExamesDisponiveis] = useState([]);
|
||||
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
|
||||
|
||||
|
||||
const [paginaAtual, setPaginaAtual] = useState(1);
|
||||
const [itensPorPagina, setItensPorPagina] = useState(10);
|
||||
|
||||
// busca lista de relatórios
|
||||
useEffect(() => {
|
||||
const fetchReports = async () => {
|
||||
try {
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append('apikey', API_KEY);
|
||||
myHeaders.append('Authorization', authHeader);
|
||||
var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
|
||||
|
||||
const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions);
|
||||
const data = await res.json();
|
||||
setRelatorios(data || []);
|
||||
} catch (err) {
|
||||
console.error('Erro listar relatórios', err);
|
||||
setRelatorios([]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
setPacientesData(pacientesMap);
|
||||
};
|
||||
fetchReports();
|
||||
if (relatoriosOriginais.length > 0) {
|
||||
buscarPacientes();
|
||||
}
|
||||
}, [relatoriosOriginais, authHeader]);
|
||||
|
||||
useEffect(() => {
|
||||
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]);
|
||||
|
||||
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 => {
|
||||
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]);
|
||||
|
||||
// depois que RelatoriosFiltrados mudar, busca pacientes e médicos correspondentes
|
||||
useEffect(() => {
|
||||
const fetchRelData = async () => {
|
||||
const pacientes = [];
|
||||
const medicos = [];
|
||||
for (let i = 0; i < RelatoriosFiltrados.length; i++) {
|
||||
const rel = RelatoriosFiltrados[i];
|
||||
// paciente
|
||||
try {
|
||||
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
|
||||
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
|
||||
} catch (err) {
|
||||
pacientes.push(null);
|
||||
}
|
||||
// médico: tenta created_by ou requested_by id se existir
|
||||
try {
|
||||
const doctorId = rel.created_by || rel.requested_by || null;
|
||||
if (doctorId) {
|
||||
// se created_by é id (uuid) usamos GetDoctorByID, senão se requested_by for nome, guardamos nome
|
||||
const docRes = await GetDoctorByID(doctorId, authHeader);
|
||||
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
|
||||
} else {
|
||||
medicos.push({ full_name: rel.requested_by || '' });
|
||||
}
|
||||
} catch (err) {
|
||||
medicos.push({ full_name: rel.requested_by || '' });
|
||||
}
|
||||
}
|
||||
setPacientesComRelatorios(pacientes);
|
||||
setMedicosComRelatorios(medicos);
|
||||
};
|
||||
if (RelatoriosFiltrados.length > 0) fetchRelData();
|
||||
else {
|
||||
setPacientesComRelatorios([]);
|
||||
setMedicosComRelatorios([]);
|
||||
}
|
||||
}, [RelatoriosFiltrados, 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);
|
||||
};
|
||||
|
||||
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" } };
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
{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 {PacientesComRelatorios[index]?.full_name}</h5>
|
||||
<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">
|
||||
@ -100,32 +168,34 @@ const DoctorRelatorioManager = () => {
|
||||
<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: {RelatoriosFiltrados[index]?.due_at || ''}</p>
|
||||
{/* Exibe conteúdo salvo (content_html) */}
|
||||
<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={RelatoriosFiltrados[index]?.content_html || RelatoriosFiltrados[index]?.content || 'Relatório não preenchido.'} />
|
||||
<TiptapViewer htmlContent={relatorioModal.content || relatorioModal.diagnosis || relatorioModal.conclusion || 'Relatório não preenchido.'} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>Dr {MedicosComRelatorios[index]?.full_name || RelatoriosFiltrados[index]?.requested_by}</p>
|
||||
<p>Emitido em: {RelatoriosFiltrados[index]?.created_at || '—'}</p>
|
||||
<p>Dr {relatorioModal.required_by}</p>
|
||||
<p>Emitido em: 0</p>
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
<div className="page-heading">
|
||||
<h3>Lista de Relatórios</h3>
|
||||
</div>
|
||||
<div className="page-content">
|
||||
<section className="row">
|
||||
<div className="col-12">
|
||||
@ -133,14 +203,51 @@ const DoctorRelatorioManager = () => {
|
||||
<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"><i className="bi bi-plus-circle"></i> Adicionar Relatório</button>
|
||||
<button className="btn btn-primary">
|
||||
<i className="bi bi-plus-circle"></i> Adicionar Relatório
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<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</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" }} />
|
||||
<h5 className="mb-3">
|
||||
<i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros
|
||||
</h5>
|
||||
<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>
|
||||
|
||||
@ -149,36 +256,104 @@ const DoctorRelatorioManager = () => {
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Paciente</th>
|
||||
<th>Doutor</th>
|
||||
<th>CPF</th>
|
||||
<th>Exame</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{RelatoriosFiltrados.length > 0 ? (
|
||||
RelatoriosFiltrados.map((relatorio, idx) => (
|
||||
<tr key={relatorio.id}>
|
||||
<td className='infos-paciente'>{PacientesComRelatorios[idx]?.full_name}</td>
|
||||
<td className='infos-paciente'>{MedicosComRelatorios[idx]?.full_name || relatorio.requested_by || '-'}</td>
|
||||
<td>
|
||||
<div className="d-flex gap-2">
|
||||
<button className="btn btn-sm" style={{ backgroundColor: "#E6F2FF", color: "#004085" }} onClick={() => { setShowModal(true); setIndex(idx); }}>
|
||||
<i className="bi bi-eye me-1"></i> Ver Detalhes
|
||||
</button>
|
||||
|
||||
<button className="btn btn-sm" style={{ backgroundColor: "#FFF3CD", color: "#856404" }} onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
|
||||
<i className="bi bi-pencil me-1"></i> Editar
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
{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 btn-editar" onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
|
||||
<i className="bi bi-pencil me-1"></i> Editar
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr><td colSpan="8" className="text-center">Nenhum paciente encontrado.</td></tr>
|
||||
<tr>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
{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>
|
||||
</div>
|
||||
@ -188,4 +363,4 @@ const DoctorRelatorioManager = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default DoctorRelatorioManager;
|
||||
export default DoctorRelatorioManager;
|
||||
31
src/PagesMedico/styleMedico/DoctorRelatorioManager.css
Normal file
31
src/PagesMedico/styleMedico/DoctorRelatorioManager.css
Normal 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;
|
||||
}
|
||||
@ -12,18 +12,18 @@ const FormNovaConsulta = ({ onCancel, onSave, setAgendamento, agendamento }) =>
|
||||
console.log(agendamento, 'aqui2')
|
||||
|
||||
const [sessoes,setSessoes] = useState(1)
|
||||
const [tempoBaseConsulta, setTempoBaseConsulta] = useState(30); // NOVO: Tempo base da consulta em minutos
|
||||
|
||||
const [tempoBaseConsulta, setTempoBaseConsulta] = useState(30);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [anexos, setAnexos] = useState([]);
|
||||
const [loadingAnexos, setLoadingAnexos] = useState(false);
|
||||
const [acessibilidade, setAcessibilidade] = useState({cadeirante:false,idoso:false,gravida:false,bebe:false, autista:false })
|
||||
|
||||
// Estado para controlar o modal
|
||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||
|
||||
const [todosProfissionais, setTodosProfissionais] = useState([])
|
||||
const [profissionaisFiltrados, setProfissionaisFiltrados] = useState([]);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const [todosProfissionais, setTodosProfissionais] = useState([])
|
||||
const [profissionaisFiltrados, setProfissionaisFiltrados] = useState([]);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
|
||||
const [horarioInicio, setHorarioInicio] = useState('');
|
||||
const [horarioTermino, setHorarioTermino] = useState('');
|
||||
@ -40,8 +40,6 @@ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
.replace(/(\d{3})(\d{1,2})$/, '$1-$2');
|
||||
}
|
||||
|
||||
|
||||
|
||||
const handleChange = (e) => {
|
||||
const {value, name} = e.target;
|
||||
console.log(value, name, agendamento)
|
||||
@ -87,105 +85,93 @@ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const ChamarMedicos = async () => {
|
||||
const Medicos = await GetAllDoctors(authHeader)
|
||||
setTodosProfissionais(Medicos)
|
||||
}
|
||||
ChamarMedicos();
|
||||
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
myHeaders.append("apikey", API_KEY)
|
||||
myHeaders.append("Authorization", `Bearer ${authHeader.split(' ')[1]}`);
|
||||
|
||||
var raw = JSON.stringify({
|
||||
"doctor_id": agendamento.doctor_id,
|
||||
"start_date": agendamento.dataAtendimento,
|
||||
"end_date": `${agendamento.dataAtendimento}T23:59:59.999Z`,
|
||||
|
||||
});
|
||||
|
||||
var requestOptions = {
|
||||
method: 'POST',
|
||||
headers: myHeaders,
|
||||
body: raw,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
fetch("https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/get-available-slots", requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(result => {console.log(result); sethorariosDisponiveis(result)})
|
||||
.catch(error => console.log('error', error));
|
||||
|
||||
}, [agendamento.dataAtendimento, agendamento.doctor_id])
|
||||
|
||||
|
||||
// FUNÇÃO DE BUSCA E FILTRAGEM
|
||||
const handleSearchProfissional = (e) => {
|
||||
const term = e.target.value;
|
||||
handleChange(e);
|
||||
// 2. Lógica de filtragem:
|
||||
if (term.trim() === '') {
|
||||
setProfissionaisFiltrados([]);
|
||||
setIsDropdownOpen(false);
|
||||
return;
|
||||
useEffect(() => {
|
||||
const ChamarMedicos = async () => {
|
||||
const Medicos = await GetAllDoctors(authHeader)
|
||||
setTodosProfissionais(Medicos)
|
||||
}
|
||||
// Adapte o nome da propriedade (ex: 'nome', 'full_name')
|
||||
const filtered = todosProfissionais.filter(p =>
|
||||
p.full_name.toLowerCase().includes(term.toLowerCase())
|
||||
);
|
||||
|
||||
setProfissionaisFiltrados(filtered);
|
||||
setIsDropdownOpen(filtered.length > 0); // Abre se houver resultados
|
||||
};
|
||||
ChamarMedicos();
|
||||
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("Content-Type", "application/json");
|
||||
myHeaders.append("apikey", API_KEY)
|
||||
myHeaders.append("Authorization", `Bearer ${authHeader.split(' ')[1]}`);
|
||||
|
||||
// FUNÇÃO PARA SELECIONAR UM ITEM DO DROPDOWN
|
||||
const handleSelectProfissional = async (profissional) => {
|
||||
setAgendamento(prev => ({
|
||||
...prev,
|
||||
doctor_id: profissional.id,
|
||||
nome_medico: profissional.full_name
|
||||
}));
|
||||
// 2. Fecha o dropdown
|
||||
setProfissionaisFiltrados([]);
|
||||
setIsDropdownOpen(false);
|
||||
var raw = JSON.stringify({
|
||||
"doctor_id": agendamento.doctor_id,
|
||||
"start_date": agendamento.dataAtendimento,
|
||||
"end_date": `${agendamento.dataAtendimento}T23:59:59.999Z`,
|
||||
});
|
||||
|
||||
var requestOptions = {
|
||||
method: 'POST',
|
||||
headers: myHeaders,
|
||||
body: raw,
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
|
||||
const formatarHora = (datetimeString) => {
|
||||
return datetimeString.substring(11, 16);
|
||||
fetch("https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/get-available-slots", requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(result => {console.log(result); sethorariosDisponiveis(result)})
|
||||
.catch(error => console.log('error', error));
|
||||
|
||||
}, [agendamento.dataAtendimento, agendamento.doctor_id])
|
||||
|
||||
// FUNÇÃO DE BUSCA E FILTRAGEM
|
||||
const handleSearchProfissional = (e) => {
|
||||
const term = e.target.value;
|
||||
handleChange(e);
|
||||
if (term.trim() === '') {
|
||||
setProfissionaisFiltrados([]);
|
||||
setIsDropdownOpen(false);
|
||||
return;
|
||||
}
|
||||
const filtered = todosProfissionais.filter(p =>
|
||||
p.full_name.toLowerCase().includes(term.toLowerCase())
|
||||
);
|
||||
|
||||
setProfissionaisFiltrados(filtered);
|
||||
setIsDropdownOpen(filtered.length > 0);
|
||||
};
|
||||
|
||||
// FUNÇÃO PARA SELECIONAR UM ITEM DO DROPDOWN
|
||||
const handleSelectProfissional = async (profissional) => {
|
||||
setAgendamento(prev => ({
|
||||
...prev,
|
||||
doctor_id: profissional.id,
|
||||
nome_medico: profissional.full_name
|
||||
}));
|
||||
setProfissionaisFiltrados([]);
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const formatarHora = (datetimeString) => {
|
||||
return datetimeString.substring(11, 16);
|
||||
};
|
||||
|
||||
const opcoesDeHorario = horariosDisponiveis?.slots?.map(item => ({
|
||||
|
||||
value: formatarHora(item.datetime),
|
||||
label: formatarHora(item.datetime),
|
||||
disabled: !item.available
|
||||
value: formatarHora(item.datetime),
|
||||
label: formatarHora(item.datetime),
|
||||
disabled: !item.available
|
||||
}));
|
||||
|
||||
const calcularHorarioTermino = (inicio, sessoes, tempoBase) => {
|
||||
if (!inicio || inicio.length !== 5 || !inicio.includes(':')) return '';
|
||||
const calcularHorarioTermino = (inicio, sessoes, tempoBase) => {
|
||||
if (!inicio || inicio.length !== 5 || !inicio.includes(':')) return '';
|
||||
|
||||
const [horas, minutos] = inicio.split(':').map(Number);
|
||||
const minutosInicio = (horas * 60) + minutos;
|
||||
const duracaoTotalMinutos = sessoes * tempoBase;
|
||||
const minutosTermino = minutosInicio + duracaoTotalMinutos;
|
||||
const [horas, minutos] = inicio.split(':').map(Number);
|
||||
const minutosInicio = (horas * 60) + minutos;
|
||||
const duracaoTotalMinutos = sessoes * tempoBase;
|
||||
const minutosTermino = minutosInicio + duracaoTotalMinutos;
|
||||
|
||||
const horaTermino = Math.floor(minutosTermino / 60) % 24;
|
||||
const minutoTermino = minutosTermino % 60;
|
||||
const horaTermino = Math.floor(minutosTermino / 60) % 24;
|
||||
const minutoTermino = minutosTermino % 60;
|
||||
|
||||
const formatar = (num) => String(num).padStart(2, '0');
|
||||
const formatar = (num) => String(num).padStart(2, '0');
|
||||
|
||||
return `${formatar(horaTermino)}:${formatar(minutoTermino)}`;
|
||||
return `${formatar(horaTermino)}:${formatar(minutoTermino)}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Recalcula o horário de término sempre que houver alteração nas variáveis dependentes
|
||||
const novoTermino = calcularHorarioTermino(horarioInicio, sessoes, tempoBaseConsulta);
|
||||
setHorarioTermino(novoTermino);
|
||||
|
||||
@ -195,191 +181,209 @@ const calcularHorarioTermino = (inicio, sessoes, tempoBase) => {
|
||||
}));
|
||||
}, [horarioInicio, sessoes, tempoBaseConsulta, setAgendamento]);
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
alert("Agendamento salvo!");
|
||||
onSave({...agendamento, horarioInicio:horarioInicio})
|
||||
// Mostra o modal de sucesso
|
||||
setShowSuccessModal(true);
|
||||
};
|
||||
|
||||
// Função para fechar o modal e chamar onSave
|
||||
const handleCloseModal = () => {
|
||||
setShowSuccessModal(false);
|
||||
onSave({...agendamento, horarioInicio:horarioInicio});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-container">
|
||||
|
||||
{/* Modal de Sucesso - Estilo igual ao do PatientForm */}
|
||||
{showSuccessModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Sucesso</h5>
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="modal-close-btn"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
<p className="modal-message">
|
||||
Agendamento salvo com sucesso!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="modal-confirm-btn"
|
||||
>
|
||||
Fechar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="form-agendamento" onSubmit={handleSubmit}>
|
||||
<h2 className="section-title">Informações do paciente</h2>
|
||||
|
||||
<div className="campos-informacoes-paciente" id="informacoes-paciente-linha-um">
|
||||
|
||||
<div className="campos-informacoes-paciente" id="informacoes-paciente-linha-um">
|
||||
<div className="campo-de-input">
|
||||
<label>CPF do paciente</label>
|
||||
<input type="text" name="paciente_cpf" placeholder="000.000.000-00" onChange={handleChange} value={agendamento.paciente_cpf}/>
|
||||
|
||||
<input type="text" name="paciente_cpf" placeholder="000.000.000-00" onChange={handleChange} value={agendamento.paciente_cpf}/>
|
||||
</div>
|
||||
|
||||
<div className="campo-de-input">
|
||||
<label>Nome *</label>
|
||||
<input type="text" name="paciente_nome" value={agendamento.paciente_nome} placeholder="Insira o nome do paciente" required onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="campos-informacoes-paciente" id="informacoes-paciente-linha-tres">
|
||||
|
||||
<div >
|
||||
<label>Convênio</label>
|
||||
<select name="convenio" onChange={handleChange}>
|
||||
<option value="publico">Público</option>
|
||||
<option value="unimed">Unimed</option>
|
||||
<option value="bradesco_saude">Bradesco Saúde</option>
|
||||
<option value="hapvida">Hapvida</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="campos-informacoes-paciente" id="informacoes-paciente-linha-tres">
|
||||
<div>
|
||||
<label>Convênio</label>
|
||||
<select name="convenio" onChange={handleChange}>
|
||||
<option value="publico">Público</option>
|
||||
<option value="unimed">Unimed</option>
|
||||
<option value="bradesco_saude">Bradesco Saúde</option>
|
||||
<option value="hapvida">Hapvida</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 className="section-title">Informações do atendimento</h2>
|
||||
|
||||
|
||||
<div className="campo-informacoes-atendimento">
|
||||
|
||||
<div className="campo-de-input-container"> {/* NOVO CONTAINER PAI */}
|
||||
<div className="campo-de-input">
|
||||
<label>Nome do profissional *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="nome_medico" // Use o nome correto da propriedade no estado `agendamento`
|
||||
onChange={handleSearchProfissional}
|
||||
value={agendamento?.nome_medico}
|
||||
autoComplete="off" // Ajuda a evitar o autocomplete nativo do navegador
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="campo-de-input-container">
|
||||
<div className="campo-de-input">
|
||||
<label>Nome do profissional *</label>
|
||||
<input
|
||||
type="text"
|
||||
name="nome_medico"
|
||||
onChange={handleSearchProfissional}
|
||||
value={agendamento?.nome_medico}
|
||||
autoComplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* DROPDOWN - RENDERIZAÇÃO CONDICIONAL */}
|
||||
{isDropdownOpen && profissionaisFiltrados.length > 0 && (
|
||||
<div className='dropdown-profissionais'>
|
||||
{profissionaisFiltrados.map((profissional) => (
|
||||
<div
|
||||
key={profissional.id} // Use o ID do profissional
|
||||
{isDropdownOpen && profissionaisFiltrados.length > 0 && (
|
||||
<div className='dropdown-profissionais'>
|
||||
{profissionaisFiltrados.map((profissional) => (
|
||||
<div
|
||||
key={profissional.id}
|
||||
className='dropdown-item'
|
||||
onClick={() => handleSelectProfissional(profissional)}
|
||||
>
|
||||
>
|
||||
{profissional.full_name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tipo_atendimento">
|
||||
<div className="tipo_atendimento">
|
||||
<label>Tipo de atendimento *</label>
|
||||
<select onChange={handleChange} name="tipo_atendimento" >
|
||||
<select onChange={handleChange} name="tipo_atendimento">
|
||||
<option value="presencial" selected>Presencial</option>
|
||||
<option value="teleconsulta">Teleconsulta</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<section id="informacoes-atendimento-segunda-linha">
|
||||
<section id="informacoes-atendimento-segunda-linha-esquerda">
|
||||
|
||||
<div className="campo-informacoes-atendimento">
|
||||
|
||||
|
||||
<div className="campo-de-input">
|
||||
<label>Data *</label>
|
||||
<input type="date" name="dataAtendimento" onChange={handleChange} required />
|
||||
<div className="campo-informacoes-atendimento">
|
||||
<div className="campo-de-input">
|
||||
<label>Data *</label>
|
||||
<input type="date" name="dataAtendimento" onChange={handleChange} required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="linha">
|
||||
{/* Dropdown de Início (Não modificado) */}
|
||||
<div className="campo-de-input">
|
||||
<label htmlFor="inicio">Início *</label>
|
||||
<select
|
||||
id="inicio"
|
||||
name="inicio"
|
||||
required
|
||||
value={horarioInicio}
|
||||
onChange={(e) => setHorarioInicio(e.target.value)}
|
||||
>
|
||||
<option value="" disabled>Selecione a hora de início</option>
|
||||
{opcoesDeHorario?.map((opcao, index) => (
|
||||
<option
|
||||
key={index}
|
||||
value={opcao.value}
|
||||
disabled={opcao.disabled}
|
||||
|
||||
<div className="linha">
|
||||
<div className="campo-de-input">
|
||||
<label htmlFor="inicio">Início *</label>
|
||||
<select
|
||||
id="inicio"
|
||||
name="inicio"
|
||||
required
|
||||
value={horarioInicio}
|
||||
onChange={(e) => setHorarioInicio(e.target.value)}
|
||||
>
|
||||
{opcao.label}
|
||||
{opcao.disabled && " (Indisponível)"}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<option value="" disabled>Selecione a hora de início</option>
|
||||
{opcoesDeHorario?.map((opcao, index) => (
|
||||
<option
|
||||
key={index}
|
||||
value={opcao.value}
|
||||
disabled={opcao.disabled}
|
||||
>
|
||||
{opcao.label}
|
||||
{opcao.disabled && " (Indisponível)"}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* SELETOR DE SESSÕES MODIFICADO */}
|
||||
{/* Removemos o 'label' para evitar o desalinhamento e colocamos o texto acima */}
|
||||
<div className='seletor-wrapper'>
|
||||
<label>Número de Sessões *</label> {/* Novo label para o seletor */}
|
||||
<div className='sessao-contador'>
|
||||
<button
|
||||
type="button" /* Adicionado para evitar submissão de formulário */
|
||||
onClick={() => {if(sessoes === 0)return; else(setSessoes(sessoes - 1))}}
|
||||
disabled={sessoes === 0} /* Desabilita o botão no limite */
|
||||
>
|
||||
<i className="bi bi-chevron-compact-left"></i>
|
||||
</button>
|
||||
|
||||
<p className='sessao-valor'>{sessoes}</p> {/* Adicionada classe para estilização */}
|
||||
|
||||
<button
|
||||
type="button" /* Adicionado para evitar submissão de formulário */
|
||||
onClick={() => {if(sessoes === 3 )return; else(setSessoes(sessoes + 1))}}
|
||||
disabled={sessoes === 3} /* Desabilita o botão no limite */
|
||||
>
|
||||
<i className="bi bi-chevron-compact-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="campo-de-input">
|
||||
<label htmlFor="termino">Término *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="termino"
|
||||
name="termino"
|
||||
value={horarioTermino || '— —'}
|
||||
readOnly
|
||||
className="horario-termino-readonly"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<div className='seletor-wrapper'>
|
||||
<label>Número de Sessões *</label>
|
||||
<div className='sessao-contador'>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {if(sessoes === 0)return; else(setSessoes(sessoes - 1))}}
|
||||
disabled={sessoes === 0}
|
||||
>
|
||||
<i className="bi bi-chevron-compact-left"></i>
|
||||
</button>
|
||||
|
||||
<p className='sessao-valor'>{sessoes}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {if(sessoes === 3 )return; else(setSessoes(sessoes + 1))}}
|
||||
disabled={sessoes === 3}
|
||||
>
|
||||
<i className="bi bi-chevron-compact-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="informacoes-atendimento-segunda-linha-direita">
|
||||
|
||||
<div className="campo-de-input">
|
||||
<label htmlFor="termino">Término *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="termino"
|
||||
name="termino"
|
||||
value={horarioTermino || '— —'}
|
||||
readOnly
|
||||
className="horario-termino-readonly"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="campo-de-input">
|
||||
<label>Observações</label>
|
||||
<textarea name="observacoes" rows="4" cols="1"></textarea>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
|
||||
<section className="informacoes-atendimento-segunda-linha-direita">
|
||||
<div className="campo-de-input">
|
||||
<label>Observações</label>
|
||||
<textarea name="observacoes" rows="4" cols="1"></textarea>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<div className="form-actions">
|
||||
<button type="submit" className="btn-primary">Salvar agendamento</button>
|
||||
<div className="form-actions">
|
||||
<button type="submit" className="btn-primary">Salvar agendamento</button>
|
||||
<button type="button" className="btn-cancel" onClick={onCancel}>Cancelar</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="campo-de-input-check">
|
||||
<input className="form-check-input form-custom-check" type="checkbox" name="status" onChange={handleChange} />
|
||||
<label className="form-check-label checkbox-label" htmlFor="status">
|
||||
Adicionar a fila de espera
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="campo-de-input-check">
|
||||
<input className="form-check-input form-custom-check" type="checkbox" name="status" onChange={handleChange} />
|
||||
<label className="form-check-label checkbox-label" htmlFor="status">
|
||||
Adicionar a fila de espera
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -43,8 +43,6 @@ svg{
|
||||
font-family: 'Material Symbols Outlined';
|
||||
font-size: 20px;
|
||||
color:black
|
||||
|
||||
|
||||
}
|
||||
|
||||
.form-container {
|
||||
@ -152,7 +150,6 @@ svg{
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
|
||||
.cardconsulta-infosecundaria{
|
||||
font-size: small;
|
||||
}
|
||||
@ -166,10 +163,8 @@ svg{
|
||||
.campo-de-input{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
|
||||
#informacoes-atendimento-segunda-linha{
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
@ -185,13 +180,13 @@ textarea{
|
||||
.campos-informacoes-paciente,
|
||||
.campo-informacoes-atendimento {
|
||||
display: flex;
|
||||
gap: 16px; /* espaço entre campos */
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.campo-de-input {
|
||||
flex: 1; /* todos os filhos ocupam mesmo espaço */
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column; /* mantém label em cima do input */
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#informacoes-atendimento-segunda-linha-esquerda select[name="unidade"]{
|
||||
@ -213,7 +208,7 @@ select[name=solicitante]{
|
||||
.form-container {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
margin: 0; /* >>> sem espaço para encostar no topo <<< */
|
||||
margin: 0;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
@ -308,24 +303,21 @@ html[data-bs-theme="dark"] svg {
|
||||
|
||||
/* CONTAINER PAI - ESSENCIAL PARA POSICIONAMENTO */
|
||||
.campo-de-input-container {
|
||||
position: relative; /* Define o contexto para o dropdown */
|
||||
/* ... outros estilos de layout (display, margin, etc.) ... */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ESTILO DA LISTA DROPDOWN */
|
||||
.dropdown-profissionais {
|
||||
position: absolute; /* Flutua em relação ao pai (.campo-de-input-container) */
|
||||
top: 100%; /* Começa logo abaixo do input */
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%; /* Ocupa toda a largura do container pai */
|
||||
|
||||
/* Estilos visuais */
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100; /* Alto z-index para garantir que fique acima de outros elementos */
|
||||
z-index: 100;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* ESTILO DE CADA ITEM DO DROPDOWN */
|
||||
@ -335,67 +327,47 @@ html[data-bs-theme="dark"] svg {
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f0f0f0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.tipo_atendimento{
|
||||
margin-left: 3rem;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 1. Estilização Básica e Tamanho (Estado Padrão - Antes de Clicar) */
|
||||
.checkbox-customs {
|
||||
/* Remove a aparência padrão do navegador/Bootstrap */
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
/* Define o tamanho desejado */
|
||||
width: 1.2rem; /* Ajuste conforme o seu gosto (ex: 1.2rem = 19.2px) */
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
|
||||
/* Define o visual "branco com borda preta" */
|
||||
background-color: #fff; /* Fundo branco */
|
||||
border: 1px solid #000; /* Borda preta de 1px */
|
||||
border-radius: 0.25rem; /* Borda levemente arredondada (opcional, imita Bootstrap) */
|
||||
|
||||
/* Centraliza o 'check' (quando aparecer) */
|
||||
background-color: #fff;
|
||||
border: 1px solid #000;
|
||||
border-radius: 0.25rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
cursor: pointer; /* Indica que é clicável */
|
||||
|
||||
/* Adiciona a transição suave */
|
||||
transition: all 0.5s ease; /* Transição em 0.5 segundos para todas as propriedades */
|
||||
cursor: pointer;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
/* 2. Estilização no Estado Clicado (:checked) */
|
||||
.checkbox-customs:checked {
|
||||
/* Quando clicado, mantém o fundo branco (se quiser mudar, altere aqui) */
|
||||
background-color: #fff;
|
||||
|
||||
/* Se você quiser que a borda mude de cor ao clicar, altere aqui. */
|
||||
/* border-color: #007bff; */ /* Exemplo: borda azul */
|
||||
}
|
||||
|
||||
/* 3. Ocultar o 'Check' Padrão e Criar um Check Customizado */
|
||||
/* O Bootstrap/Navegador insere um ícone de 'check'. Vamos controlá-lo com background-image. */
|
||||
.checkbox-customs:checked {
|
||||
/* Este código do Bootstrap usa um SVG para o ícone de 'check' */
|
||||
/* Aqui, estamos forçando o ícone de 'check' a ser preto para combinar com a borda preta. */
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
|
||||
|
||||
/* Garante que o ícone fique centralizado e preencha o espaço */
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* Container dos três elementos na linha */
|
||||
.linha {
|
||||
display: flex;
|
||||
align-items: flex-end; /* Garante que os campos de input e o seletor fiquem alinhados pela base */
|
||||
gap: 20px; /* Espaçamento entre os campos */
|
||||
align-items: flex-end;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* ------------------------------------------- */
|
||||
@ -403,72 +375,155 @@ html[data-bs-theme="dark"] svg {
|
||||
/* ------------------------------------------- */
|
||||
|
||||
.seletor-wrapper {
|
||||
/* Garante que o label e o contador fiquem alinhados verticalmente com os selects */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sessao-contador {
|
||||
/* Estilo de "campo de input" */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
/* Cores e Bordas */
|
||||
background-color: #e9ecef; /* Cor cinza claro dos inputs do Bootstrap */
|
||||
border: 1px solid #ced4da; /* Borda sutil */
|
||||
border-radius: 0.25rem; /* Bordas arredondadas (Padrão Bootstrap) */
|
||||
|
||||
/* Garante a mesma altura dos selects */
|
||||
height: 40px; /* Ajuste este valor para corresponder à altura exata do seu select */
|
||||
width: 100px; /* Largura ajustável */
|
||||
padding: 0 5px; /* Padding interno */
|
||||
|
||||
background-color: #e9ecef;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 0.25rem;
|
||||
height: 40px;
|
||||
width: 100px;
|
||||
padding: 0 5px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sessao-valor {
|
||||
/* Estilo do número de sessões */
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
font-size: 1.1rem; /* Um pouco maior que o texto dos selects */
|
||||
color: #007bff; /* Cor azul destacada (como na sua imagem) */
|
||||
font-size: 1.1rem;
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.sessao-contador button {
|
||||
/* Estilo dos botões de chevron */
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 2px;
|
||||
color: #495057; /* Cor do ícone */
|
||||
font-size: 1.5rem; /* Aumenta o tamanho dos ícones do chevron */
|
||||
line-height: 1; /* Alinha o ícone verticalmente */
|
||||
color: #495057;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.sessao-contador button:hover:not(:disabled) {
|
||||
color: #007bff; /* Cor azul ao passar o mouse */
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.sessao-contador button:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #adb5bd; /* Cor mais clara quando desabilitado */
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
/* ------------------------------------------- */
|
||||
/* GARANTINDO COERÊNCIA NOS SELECTS */
|
||||
/* ESTILOS DO MODAL DE SUCESSO */
|
||||
/* ------------------------------------------- */
|
||||
|
||||
.campo-de-input select {
|
||||
/* Se seus selects estiverem com estilos diferentes, este bloco garante que eles se pareçam */
|
||||
/* com o seletor de sessões (se já usarem classes do Bootstrap, podem não precisar disso) */
|
||||
background-color: #e9ecef; /* Fundo cinza claro */
|
||||
border: 1px solid #ced4da; /* Borda sutil */
|
||||
border-radius: 0.25rem;
|
||||
height: 40px; /* Garante a mesma altura do sessao-contador */
|
||||
/* Adicione mais estilos do seu input/select se necessário (ex: font-size, padding) */
|
||||
.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: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
animation: modalAppear 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #28a745;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-body p {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-footer .btn-primary {
|
||||
background: #28a745;
|
||||
padding: 10px 24px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-footer .btn-primary:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
/* Estilos para tema escuro do modal */
|
||||
html[data-bs-theme="dark"] .modal-content {
|
||||
background: #232323 !important;
|
||||
color: #e0e0e0 !important;
|
||||
border: 1px solid #404053 !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] .modal-header h3 {
|
||||
color: #4ade80 !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] .modal-body p {
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] .modal-footer .btn-primary {
|
||||
background: #16a34a !important;
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] .modal-footer .btn-primary:hover {
|
||||
background: #15803d !important;
|
||||
}
|
||||
|
||||
/* Campo de término readonly */
|
||||
.horario-termino-readonly {
|
||||
background-color: #f8f9fa;
|
||||
color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
html[data-bs-theme="dark"] .horario-termino-readonly {
|
||||
background-color: #2d3748 !important;
|
||||
color: #a0aec0 !important;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
.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: 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;
|
||||
/* Responsividade */
|
||||
@media (max-width: 768px) {
|
||||
.header-container {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.right-corner-elements {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.profile-picture-container {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.suporte-card-container,
|
||||
.chat-container {
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.suporte-card,
|
||||
.chat-online {
|
||||
width: calc(100vw - 20px);
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -100,7 +124,7 @@ const Header = () => {
|
||||
};
|
||||
|
||||
const handleViewProfile = () => {
|
||||
navigate('/perfil');
|
||||
navigate('/perfil');
|
||||
setIsDropdownOpen(false);
|
||||
};
|
||||
|
||||
@ -160,7 +184,7 @@ const Header = () => {
|
||||
'Já encaminhei sua solicitação para nossa equipe técnica.',
|
||||
'Vou ajudar você a resolver isso!'
|
||||
];
|
||||
|
||||
|
||||
const respostaSuporte = {
|
||||
id: Date.now() + 1,
|
||||
texto: respostas[Math.floor(Math.random() * respostas.length)],
|
||||
@ -176,21 +200,21 @@ const Header = () => {
|
||||
<div className="suporte-card">
|
||||
<h2 className="suporte-titulo">Suporte</h2>
|
||||
<p className="suporte-subtitulo">Entre em contato conosco através dos canais abaixo</p>
|
||||
|
||||
|
||||
<div className="contato-item">
|
||||
<div className="contato-info">
|
||||
<div className="contato-nome">Email</div>
|
||||
<div className="contato-descricao">suporte@mediconnect.com</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="contato-item">
|
||||
<div className="contato-info">
|
||||
<div className="contato-nome">Telefone</div>
|
||||
<div className="contato-descricao">(11) 3333-4444</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="contato-item clickable" onClick={handleChatClick}>
|
||||
<div className="contato-info">
|
||||
<div className="contato-nome">Chat Online</div>
|
||||
@ -206,7 +230,7 @@ const Header = () => {
|
||||
<h3 className="chat-titulo">Chat de Suporte</h3>
|
||||
<button type="button" className="fechar-chat" onClick={handleCloseChat}>×</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="chat-mensagens" ref={mensagensContainerRef}>
|
||||
{mensagens.map((msg) => (
|
||||
<div key={msg.id} className={`mensagem ${msg.remetente}`}>
|
||||
@ -215,7 +239,7 @@ const Header = () => {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<form className="chat-input" onSubmit={handleEnviarMensagem}>
|
||||
<input
|
||||
ref={chatInputRef}
|
||||
@ -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>
|
||||
|
||||
@ -260,16 +260,6 @@ function Sidebar({ menuItems }) {
|
||||
})}
|
||||
|
||||
{/* Logout */}
|
||||
<li className="sidebar-item">
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-link btn"
|
||||
onClick={handleLogoutClick}
|
||||
>
|
||||
<i className="bi bi-box-arrow-right"></i>
|
||||
<span>Sair (Logout)</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<TrocardePerfis />
|
||||
</ul>
|
||||
|
||||
75
src/components/TrocardePerfis.css
Normal file
75
src/components/TrocardePerfis.css
Normal file
@ -0,0 +1,75 @@
|
||||
|
||||
.container-perfis-toggle {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 300px;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
cursor: pointer;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.acesso-text {
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
.perfil-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 20px 20px 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.perfil-item {
|
||||
padding: 10px 15px;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.perfil-item:hover {
|
||||
background-color: #0056b3;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.perfil-item:focus {
|
||||
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.5);
|
||||
}
|
||||
|
||||
.perfil-item:active {
|
||||
background-color: #004085;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.no-profiles {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-style: italic;
|
||||
}
|
||||
@ -1,31 +1,58 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { UserInfos } from "./utils/Functions-Endpoints/General";
|
||||
import { useAuth } from "./utils/AuthProvider";
|
||||
import "../pages/style/TrocardePerfis.css";
|
||||
import "./TrocardePerfis.css";
|
||||
|
||||
const ToggleIcon = ({ isOpen }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ transition: 'transform 0.3s', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
const TrocardePerfis = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { getAuthorizationHeader } = useAuth();
|
||||
|
||||
const [selectedProfile, setSelectedProfile] = useState("");
|
||||
const [showProfiles, setShowProfiles] = useState([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const authHeader = getAuthorizationHeader();
|
||||
setSelectedProfile(location.pathname || "");
|
||||
const userInfo = await UserInfos(authHeader);
|
||||
setShowProfiles(userInfo?.roles || []);
|
||||
try {
|
||||
const userInfo = await UserInfos(authHeader);
|
||||
setShowProfiles(userInfo?.roles || []);
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar informações do usuário:", error);
|
||||
setShowProfiles([]);
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [location.pathname, getAuthorizationHeader]);
|
||||
}, [getAuthorizationHeader]);
|
||||
|
||||
const handleSelectChange = (e) => {
|
||||
const route = e.target.value;
|
||||
setSelectedProfile(route);
|
||||
if (route) navigate(route);
|
||||
const handleProfileClick = (route) => {
|
||||
if (route) {
|
||||
navigate(route);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(prev => !prev);
|
||||
};
|
||||
|
||||
const options = [
|
||||
@ -40,20 +67,47 @@ const TrocardePerfis = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container-perfis">
|
||||
<p className="acesso-text">Acesso aos módulos:</p>
|
||||
<select
|
||||
className="perfil-select"
|
||||
value={selectedProfile}
|
||||
onChange={handleSelectChange}
|
||||
<div className="container-perfis-toggle">
|
||||
|
||||
<div
|
||||
className="toggle-button"
|
||||
onClick={handleToggle}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleToggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">Selecionar perfil</option>
|
||||
{options.map((opt) => (
|
||||
<option key={opt.key} value={opt.route}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="acesso-text">Acesso aos módulos</span>
|
||||
<ToggleIcon isOpen={isOpen} />
|
||||
</div>
|
||||
|
||||
{isOpen && (
|
||||
<div className="perfil-list">
|
||||
{options.length > 0 ? (
|
||||
options.map((opt) => (
|
||||
<div
|
||||
key={opt.key}
|
||||
className="perfil-item"
|
||||
onClick={() => handleProfileClick(opt.route)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleProfileClick(opt.route);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="no-profiles">Nenhum perfil disponível.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -734,7 +734,7 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
||||
</div>
|
||||
|
||||
{/* BOTÕES DE AÇÃO */}
|
||||
<div className="actions-container">
|
||||
<div className="btns-container">
|
||||
<button
|
||||
className="btn btn-success btn-submit"
|
||||
onClick={handleSubmit}
|
||||
|
||||
@ -632,7 +632,7 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
||||
)}
|
||||
|
||||
{/* BOTÕES DE AÇÃO */}
|
||||
<div className="actions-container">
|
||||
<div className="btns-container">
|
||||
<button className="btn btn-success btn-submit" onClick={handleSubmit} disabled={isLoading}>
|
||||
{isLoading ? 'Salvando...' : 'Salvar Paciente'}
|
||||
</button>
|
||||
@ -642,6 +642,7 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,48 +1,56 @@
|
||||
import API_KEY from '../apiKeys';
|
||||
|
||||
const GetDoctorByID = async (ID, authHeader) => {
|
||||
var myHeaders = new Headers();
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append('apikey', API_KEY);
|
||||
if (authHeader) myHeaders.append('Authorization', authHeader);
|
||||
|
||||
const requestOptions = { method: 'GET', redirect: 'follow', headers: myHeaders };
|
||||
const res = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${ID}`, requestOptions);
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
redirect: 'follow',
|
||||
headers: myHeaders,
|
||||
};
|
||||
|
||||
const res = await fetch(
|
||||
`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${ID}`,
|
||||
requestOptions
|
||||
);
|
||||
const DictMedico = await res.json();
|
||||
return DictMedico;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const GetAllDoctors = async (authHeader) => {
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("apikey", API_KEY);
|
||||
myHeaders.append("Authorization", authHeader);
|
||||
const myHeaders = new Headers();
|
||||
myHeaders.append('apikey', API_KEY);
|
||||
if (authHeader) myHeaders.append('Authorization', authHeader);
|
||||
|
||||
var requestOptions = {
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
headers: myHeaders,
|
||||
redirect: 'follow'
|
||||
redirect: 'follow',
|
||||
};
|
||||
|
||||
const result = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions)
|
||||
const DictMedicos = await result.json()
|
||||
return DictMedicos
|
||||
}
|
||||
|
||||
|
||||
const result = await fetch(
|
||||
'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?select=id,full_name,crm&limit=500',
|
||||
requestOptions
|
||||
);
|
||||
const DictMedicos = await result.json();
|
||||
return DictMedicos;
|
||||
};
|
||||
|
||||
const GetDoctorByName = async (nome, authHeader) => {
|
||||
const Medicos = await GetAllDoctors(authHeader)
|
||||
|
||||
for (let i = 0; i < Medicos.length; i++) {
|
||||
const Medicos = await GetAllDoctors(authHeader);
|
||||
|
||||
if (Medicos[i].full_name === nome) {
|
||||
console.log('Medico encontrado:', Medicos[i]);
|
||||
return Medicos[i];
|
||||
}
|
||||
else{console.log("nada encontrado")}
|
||||
}
|
||||
|
||||
for (let i = 0; i < Medicos.length; i++) {
|
||||
if (Medicos[i].full_name === nome) {
|
||||
console.log('Médico encontrado:', Medicos[i]);
|
||||
return Medicos[i];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
console.log('Nenhum médico encontrado com o nome:', nome);
|
||||
return null;
|
||||
};
|
||||
|
||||
export {GetDoctorByID, GetDoctorByName, GetAllDoctors}
|
||||
export { GetDoctorByID, GetDoctorByName, GetAllDoctors };
|
||||
|
||||
@ -101,7 +101,7 @@ const DisponibilidadesDoctorPage = () => {
|
||||
Disponibilidades por Médico
|
||||
</h1>
|
||||
|
||||
<Link
|
||||
{/* <Link
|
||||
to={rotaGerenciar}
|
||||
className="btn-primary"
|
||||
style={{
|
||||
@ -113,7 +113,7 @@ const DisponibilidadesDoctorPage = () => {
|
||||
}}
|
||||
>
|
||||
+ Gerenciar Disponibilidades
|
||||
</Link>
|
||||
</Link> */}
|
||||
</div>
|
||||
|
||||
<div className="atendimento-eprocura">
|
||||
|
||||
@ -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();
|
||||
@ -104,18 +104,16 @@ function TableDoctor() {
|
||||
|
||||
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(result => {setMedicos(result); console.log(result)})
|
||||
.then(result => setMedicos(result))
|
||||
.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>
|
||||
|
||||
@ -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>
|
||||
@ -104,4 +306,4 @@ const ProfilePage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
export default ProfilePage;
|
||||
@ -3,10 +3,10 @@ import { Link } from "react-router-dom";
|
||||
import API_KEY from "../components/utils/apiKeys";
|
||||
import { useAuth } from "../components/utils/AuthProvider";
|
||||
import "./style/TablePaciente.css";
|
||||
import ModalErro from "../components/utils/fetchErros/ModalErro";
|
||||
|
||||
function TablePaciente({ setCurrentPage, setPatientID }) {
|
||||
|
||||
const { getAuthorizationHeader, isAuthenticated, RefreshingToken } = useAuth();
|
||||
const { getAuthorizationHeader, isAuthenticated } = useAuth();
|
||||
|
||||
const [pacientes, setPacientes] = useState([]);
|
||||
const [search, setSearch] = useState("");
|
||||
@ -21,13 +21,13 @@ 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);
|
||||
|
||||
const [showModalError, setShowModalError] = useState(false);
|
||||
|
||||
const [ ErrorInfo, setErrorInfo] = useState({})
|
||||
|
||||
const GetAnexos = async (id) => {
|
||||
var myHeaders = new Headers();
|
||||
myHeaders.append("Authorization", "Bearer <token>");
|
||||
@ -104,6 +104,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const authHeader = getAuthorizationHeader()
|
||||
|
||||
console.log(authHeader, 'aqui autorização')
|
||||
@ -117,47 +118,10 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
|
||||
redirect: 'follow'
|
||||
};
|
||||
|
||||
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions)
|
||||
.then(response => {
|
||||
|
||||
// 1. VERIFICAÇÃO DO STATUS HTTP (Se não for 2xx)
|
||||
if (!response.ok) {
|
||||
|
||||
return response.json().then(errorData => {
|
||||
|
||||
const errorObject = {
|
||||
httpStatus: response.status,
|
||||
apiCode: errorData.code,
|
||||
message: errorData.message || response.statusText,
|
||||
details: errorData.details,
|
||||
hint: errorData.hint
|
||||
};
|
||||
|
||||
console.error("ERRO DETALHADO:", errorObject);
|
||||
throw errorObject;
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Se a resposta for OK (2xx), processamos o JSON normalmente
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
// 4. Bloco de SUCESSO
|
||||
setPacientes(result);
|
||||
console.log("Sucesso:", result);
|
||||
|
||||
// IMPORTANTE: Se o modal estava aberto, feche-o no sucesso
|
||||
setShowModalError(false);
|
||||
})
|
||||
.catch(error => {
|
||||
// 5. Bloco de ERRO (Captura erros de rede ou o erro lançado pelo 'throw')
|
||||
//console.error('Falha na requisição:', error.message);
|
||||
if(error.httpStatus === 401){
|
||||
RefreshingToken()
|
||||
}
|
||||
setErrorInfo(error)
|
||||
setShowModalError(true);
|
||||
});
|
||||
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(result => setPacientes(result))
|
||||
.catch(error => console.log('error', error));
|
||||
}, [isAuthenticated, getAuthorizationHeader]);
|
||||
|
||||
const ehAniversariante = (dataNascimento) => {
|
||||
@ -195,6 +159,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
|
||||
setIdadeMaxima("");
|
||||
setDataInicial("");
|
||||
setDataFinal("");
|
||||
setPaginaAtual(1);
|
||||
};
|
||||
|
||||
const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => {
|
||||
@ -238,13 +203,53 @@ 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 (
|
||||
<>
|
||||
<ModalErro showModal={showModalError} setShowModal={setShowModalError} ErrorData={ErrorInfo}/>
|
||||
<div className="page-heading">
|
||||
<h3>Lista de Pacientes</h3>
|
||||
</div>
|
||||
@ -295,7 +300,8 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
|
||||
</select>
|
||||
|
||||
<button
|
||||
className={`btn btn-sm ${filtroVIP ? "btn-primary" : "btn-outline-primary"}`}
|
||||
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
|
||||
@ -400,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 ||
|
||||
@ -421,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>
|
||||
@ -439,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">
|
||||
@ -495,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>
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
}
|
||||
|
||||
.modal-header-success {
|
||||
background-color: #28a745 !important;
|
||||
background-color: #1e3a8a !important;
|
||||
}
|
||||
|
||||
.modal-header-error {
|
||||
@ -104,12 +104,12 @@
|
||||
}
|
||||
|
||||
.modal-button-success {
|
||||
background-color: #28a745;
|
||||
background-color: #1e3a8a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-button-success:hover {
|
||||
background-color: #218838;
|
||||
background-color: #1e3a8a;
|
||||
}
|
||||
|
||||
.modal-button-error {
|
||||
@ -186,10 +186,8 @@
|
||||
outline: 2px solid #0056b3;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Garantir que as cores dos cabeçalhos sejam aplicadas */
|
||||
.modal-overlay .modal-container .modal-header.modal-header-success {
|
||||
background-color: #28a745 !important;
|
||||
background-color: #1e3a8a !important;
|
||||
}
|
||||
|
||||
.modal-overlay .modal-container .modal-header.modal-header-error {
|
||||
@ -258,7 +256,7 @@
|
||||
}
|
||||
|
||||
.modal-header-success {
|
||||
background-color: #006400 !important;
|
||||
background-color: #1e3a8a !important;
|
||||
}
|
||||
|
||||
.modal-header-error {
|
||||
|
||||
@ -91,7 +91,7 @@
|
||||
}
|
||||
|
||||
.modal-header.success {
|
||||
background-color: #28a745 !important;
|
||||
background-color: #1e3a8a !important;
|
||||
}
|
||||
|
||||
.modal-header.error {
|
||||
@ -168,11 +168,11 @@
|
||||
}
|
||||
|
||||
.modal-confirm-button.success {
|
||||
background-color: #28a745 !important;
|
||||
background-color: #1e3a8a !important;
|
||||
}
|
||||
|
||||
.modal-confirm-button.success:hover {
|
||||
background-color: #218838 !important;
|
||||
background-color: #1e3a8a !important;
|
||||
}
|
||||
|
||||
.modal-confirm-button.error {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
.table-doctor-container {
|
||||
line-height: 2.5;
|
||||
}
|
||||
@ -49,7 +48,7 @@
|
||||
background-color: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
|
||||
.specialty-badge {
|
||||
background-color: #1e3a8a !important;
|
||||
color: white !important;
|
||||
@ -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;
|
||||
}
|
||||
@ -233,4 +222,116 @@
|
||||
.btn-edit,
|
||||
.btn-delete {
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
.d-flex.justify-content-between.align-items-center {
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding-top: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
.form-select.form-select-sm.w-auto {
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
|
||||
.filters-active .badge {
|
||||
font-size: 0.75em;
|
||||
padding: 0.4em 0.65em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user