Mudanças design

This commit is contained in:
Caio Miguel Lima Nunes 2025-10-30 17:23:23 -03:00
commit 77b934636f
24 changed files with 2006 additions and 7019 deletions

View File

@ -1,7 +1,7 @@
# .env # .env
# Cole o token de acesso aqui # Cole o token de acesso aqui
WHATSAPP_TOKEN=EAAVZA9C5Lx9IBPjITD8IZCZCeGRBIACX9PInHcNHxuhmp5vK7t40Yn0kc9ZC4YeKx1ZC69tnc1MtcQFWCptQimDvQIIvugiw7BNdi0ak1COfBmIZAMAkzskVkk5qhG9WnMsVmZBEoy9AXcbI53vbqSQooZCCN7LkOhbigZCaZC3VqfLnrmIzKZBC0QhzdSzTpvfQYHocDAzCS8ejf2o6WVSXYlqJEOuLzFEkvtGR6eLvNQi6QZDZD WHATSAPP_TOKEN=EAAVZA9C5Lx9IBP0kF76Yy5GJquZCOkQZCtnsLDYJZCLRfZA7BrOsZBPBk7BODsDuU1r5qYNu5vsRFlI1tNZBlnQpWXsZCZBrkqTygGphqQLZCvikGDyZBEFEyknkWM9oadz1xVtAA65JKXFbGFIJWhmFMOgauWXZC072CSkApe5UZCVGZCZAqc5we1TqCcFBvLqWnUexosBRIEb8kSThWlEDheHNoP7MrjwNcYaNBczmFmhq9aPqKm6jCgjwqjZBI0jVLjdooKkZCanaz9ZA3ZBIfNbyq8FOYUI
# Cole o ID do número de telefone aqui # Cole o ID do número de telefone aqui
WHATSAPP_PHONE_NUMBER_ID=806117442588831 WHATSAPP_PHONE_NUMBER_ID=806117442588831

6237
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,7 @@
"react-flatpickr": "^4.0.11", "react-flatpickr": "^4.0.11",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"react-is": "^19.2.0",
"react-quill": "^2.0.0", "react-quill": "^2.0.0",
"react-router-dom": "^7.9.2", "react-router-dom": "^7.9.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",

View File

@ -21,7 +21,7 @@ app.post("/api/chat", async (req, res) => {
messages: [ messages: [
{ {
role: "system", role: "system",
content: "Você é o assistente virtual do site Mediconnect. Responda de forma amigável e informativa, explicando sobre o funcionamento do site, cadastro, agendamento, e suporte técnico.", content: "Você é a assistente virtual do site Mediconnect, chamada Ágatha. Responda de forma amigável e informativa, explicando sobre o funcionamento do site, cadastro, agendamento, e suporte técnico.",
}, },
{ role: "user", content: message }, { role: "user", content: message },
], ],

View File

@ -1,4 +1,3 @@
// src/PagesMedico/DoctorRelatorioManager.jsx
import API_KEY from '../components/utils/apiKeys'; import API_KEY from '../components/utils/apiKeys';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
@ -8,16 +7,34 @@ import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js'; import html2pdf from 'html2pdf.js';
import TiptapViewer from './TiptapViewer'; import TiptapViewer from './TiptapViewer';
import './styleMedico/DoctorRelatorioManager.css';
const DoctorRelatorioManager = () => { const DoctorRelatorioManager = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth(); const { getAuthorizationHeader } = useAuth();
const authHeader = getAuthorizationHeader(); let authHeader = getAuthorizationHeader();
const [RelatoriosFiltrados, setRelatorios] = useState([]);
const [PacientesComRelatorios, setPacientesComRelatorios] = useState([]); const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
const [MedicosComRelatorios, setMedicosComRelatorios] = useState([]); const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
const [pacientesData, setPacientesData] = useState({});
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [showModal, setShowModal] = useState(false); 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 [modalIndex, setModalIndex] = useState(0);
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const totalPaginas = Math.ceil(relatoriosFinais.length / itensPorPagina);
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal);
useEffect(() => { useEffect(() => {
let mounted = true; let mounted = true;
@ -39,10 +56,18 @@ const DoctorRelatorioManager = () => {
const unique = Array.from(uniqueMap.values()) const unique = Array.from(uniqueMap.values())
.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)); .sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
if (mounted) setRelatorios(unique); if (mounted) {
setRelatoriosOriginais(unique);
setRelatoriosFiltrados(unique);
setRelatoriosFinais(unique);
}
} catch (err) { } catch (err) {
console.error('Erro listar relatórios', err); console.error('Erro listar relatórios', err);
if (mounted) setRelatorios([]); if (mounted) {
setRelatoriosOriginais([]);
setRelatoriosFiltrados([]);
setRelatoriosFinais([]);
}
} }
}; };
@ -61,8 +86,8 @@ const DoctorRelatorioManager = () => {
const fetchRelData = async () => { const fetchRelData = async () => {
const pacientes = []; const pacientes = [];
const medicos = []; const medicos = [];
for (let i = 0; i < RelatoriosFiltrados.length; i++) { for (let i = 0; i < relatoriosFiltrados.length; i++) {
const rel = RelatoriosFiltrados[i]; const rel = relatoriosFiltrados[i];
try { try {
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader); const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes); pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
@ -84,12 +109,25 @@ const DoctorRelatorioManager = () => {
setPacientesComRelatorios(pacientes); setPacientesComRelatorios(pacientes);
setMedicosComRelatorios(medicos); setMedicosComRelatorios(medicos);
}; };
if (RelatoriosFiltrados.length > 0) fetchRelData(); if (relatoriosFiltrados.length > 0) fetchRelData();
else { else {
setPacientesComRelatorios([]); setPacientesComRelatorios([]);
setMedicosComRelatorios([]); setMedicosComRelatorios([]);
} }
}, [RelatoriosFiltrados, authHeader]); }, [relatoriosFiltrados, authHeader]);
const abrirModal = (relatorio, index) => {
setRelatorioModal(relatorio);
setModalIndex(index);
setShowModal(true);
};
// Função para limpar filtros
const limparFiltros = () => {
setTermoPesquisa('');
setFiltroExame('');
setRelatoriosFinais(relatoriosOriginais);
};
const BaixarPDFdoRelatorio = (nome_paciente, idx) => { const BaixarPDFdoRelatorio = (nome_paciente, idx) => {
const elemento = document.getElementById(`folhaA4-${idx}`); const elemento = document.getElementById(`folhaA4-${idx}`);
@ -106,20 +144,51 @@ const DoctorRelatorioManager = () => {
html2pdf().set(opt).from(elemento).save(); 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 ( return (
<div> <div>
{showModal && ( {showModal && (
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowModal(false)}> <div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowModal(false)}>
{/* aqui: classe modal-dialog-square para ficar quadrado */}
<div className="modal-dialog modal-dialog-square" role="document" onClick={(e) => e.stopPropagation()}> <div className="modal-dialog modal-dialog-square" role="document" onClick={(e) => e.stopPropagation()}>
<div className="modal-content"> <div className="modal-content">
<div className="modal-header custom-modal-header"> <div className="modal-header custom-modal-header">
<h5 className="modal-title">Relatório de {PacientesComRelatorios[index]?.full_name}</h5> <h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name}</h5>
<button type="button" className="btn-close modal-close-btn" aria-label="Close" onClick={() => setShowModal(false)}></button> <button type="button" className="btn-close modal-close-btn" aria-label="Close" onClick={() => setShowModal(false)}></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<div id={`folhaA4-${index}`} className="folhaA4"> <div id={`folhaA4-${modalIndex}`} className="folhaA4">
<div id='header-relatorio' style={{ textAlign: 'center', marginBottom: 24 }}> <div id='header-relatorio' style={{ textAlign: 'center', marginBottom: 24 }}>
<p style={{ margin: 0 }}>Clinica Rise up</p> <p style={{ margin: 0 }}>Clinica Rise up</p>
<p style={{ margin: 0 }}>Dr - CRM/SP 123456</p> <p style={{ margin: 0 }}>Dr - CRM/SP 123456</p>
@ -127,25 +196,25 @@ const DoctorRelatorioManager = () => {
</div> </div>
<div id='infoPaciente' style={{ padding: '0 6px' }}> <div id='infoPaciente' style={{ padding: '0 6px' }}>
<p><strong>Paciente:</strong> {PacientesComRelatorios[index]?.full_name}</p> <p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name}</p>
<p><strong>Data de nascimento:</strong> {PacientesComRelatorios[index]?.birth_date || '—'}</p> <p><strong>Data de nascimento:</strong> {pacientesComRelatorios[modalIndex]?.birth_date || '—'}</p>
<p><strong>Data do exame:</strong> {RelatoriosFiltrados[index]?.due_at || '—'}</p> <p><strong>Data do exame:</strong> {relatoriosFiltrados[modalIndex]?.due_at || '—'}</p>
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p> <p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
<div className="tiptap-viewer-wrapper"> <div className="tiptap-viewer-wrapper">
<TiptapViewer htmlContent={RelatoriosFiltrados[index]?.content_html || RelatoriosFiltrados[index]?.content || 'Relatório não preenchido.'} /> <TiptapViewer htmlContent={relatoriosFiltrados[modalIndex]?.content_html || relatoriosFiltrados[modalIndex]?.content || 'Relatório não preenchido.'} />
</div> </div>
</div> </div>
<div style={{ marginTop: 20, padding: '0 6px' }}> <div style={{ marginTop: 20, padding: '0 6px' }}>
<p>Dr {MedicosComRelatorios[index]?.full_name || RelatoriosFiltrados[index]?.requested_by}</p> <p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFiltrados[modalIndex]?.requested_by}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {RelatoriosFiltrados[index]?.created_at || '—'}</p> <p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatoriosFiltrados[modalIndex]?.created_at || '—'}</p>
</div> </div>
</div> </div>
</div> </div>
<div className="modal-footer custom-modal-footer"> <div className="modal-footer custom-modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name, index)}> <button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name, modalIndex)}>
<i className='bi bi-file-pdf-fill'></i> baixar em pdf <i className='bi bi-file-pdf-fill'></i> baixar em pdf
</button> </button>
<button type="button" className="btn btn-outline-secondary" onClick={() => { setShowModal(false) }}> <button type="button" className="btn btn-outline-secondary" onClick={() => { setShowModal(false) }}>
@ -157,7 +226,6 @@ const DoctorRelatorioManager = () => {
</div> </div>
)} )}
{/* restante da página (lista) permanece igual */}
<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"> <div className="page-content">
<section className="row"> <section className="row">
@ -166,14 +234,51 @@ const DoctorRelatorioManager = () => {
<div className="card-header d-flex justify-content-between align-items-center"> <div className="card-header d-flex justify-content-between align-items-center">
<h4 className="card-title mb-0">Relatórios Cadastrados</h4> <h4 className="card-title mb-0">Relatórios Cadastrados</h4>
<Link to={'criar'}> <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> </Link>
</div> </div>
<div className="card-body"> <div className="card-body">
<div className="card p-3 mb-3"> <div className="card p-3 mb-3">
<h5 className="mb-3"><i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros</h5> <h5 className="mb-3">
<div className="d-flex flex-nowrap align-items-center gap-2" style={{ overflowX: "auto", paddingBottom: "6px" }}> <i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros
<input type="text" className="form-control" placeholder="Buscar por nome..." style={{ minWidth: 250, maxWidth: 300, width: 260, flex: "0 0 auto" }} /> </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>
</div> </div>
@ -182,36 +287,91 @@ const DoctorRelatorioManager = () => {
<thead> <thead>
<tr> <tr>
<th>Paciente</th> <th>Paciente</th>
<th>Doutor</th> <th>CPF</th>
<th>Exame</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{RelatoriosFiltrados.length > 0 ? ( {relatoriosPaginados.length > 0 ? (
RelatoriosFiltrados.map((relatorio, idx) => ( relatoriosPaginados.map((relatorio, index) => {
<tr key={relatorio.id}> const paciente = pacientesData[relatorio.patient_id];
<td className='infos-paciente'>{PacientesComRelatorios[idx]?.full_name}</td> return (
<td className='infos-paciente'>{MedicosComRelatorios[idx]?.full_name || relatorio.requested_by || '-'}</td> <tr key={relatorio.id}>
<td> <td>{paciente?.full_name || 'Carregando...'}</td>
<div className="d-flex gap-2"> <td>{paciente?.cpf || 'Carregando...'}</td>
<button className="btn btn-sm" style={{ backgroundColor: "#E6F2FF", color: "#004085" }} onClick={() => { setShowModal(true); setIndex(idx); }}> <td>{relatorio.exam}</td>
<i className="bi bi-eye me-1"></i> Ver Detalhes <td>
</button> <div className="d-flex gap-2">
<button className="btn btn-sm btn-ver-detalhes" onClick={() => abrirModal(relatorio, index)}>
<button className="btn btn-sm" style={{ backgroundColor: "#FFF3CD", color: "#856404" }} onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}> <i className="bi bi-eye me-1"></i> Ver Detalhes
<i className="bi bi-pencil me-1"></i> Editar </button>
</button> <button className="btn btn-sm btn-editar" onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
</div> <i className="bi bi-pencil me-1"></i> Editar
</td> </button>
</tr> </div>
)) </td>
</tr>
);
})
) : ( ) : (
<tr><td colSpan="3" className="text-center">Nenhum paciente encontrado.</td></tr> <tr><td colSpan="4" className="text-center">Nenhum relatório encontrado.</td></tr>
)} )}
</tbody> </tbody>
</table> </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> </div>
</div> </div>
@ -221,4 +381,4 @@ const DoctorRelatorioManager = () => {
); );
}; };
export default DoctorRelatorioManager; export default DoctorRelatorioManager;

View File

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

View File

@ -1,178 +1,142 @@
import InputMask from "react-input-mask"; import InputMask from "react-input-mask";
import "./style/formagendamentos.css"; import "./style/formagendamentos.css";
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { GetPatientByCPF } from "../utils/Functions-Endpoints/Patient"; import { GetPatientByCPF } from "../utils/Functions-Endpoints/Patient";
import { GetDoctorByName, GetAllDoctors } from "../utils/Functions-Endpoints/Doctor"; import { GetAllDoctors } from "../utils/Functions-Endpoints/Doctor";
import { useAuth } from "../utils/AuthProvider"; import { useAuth } from "../utils/AuthProvider";
import API_KEY from "../utils/apiKeys"; import API_KEY from "../utils/apiKeys";
const FormNovaConsulta = ({ onCancel, onSave, setAgendamento, agendamento }) => { const FormNovaConsulta = ({ onCancel, onSave, setAgendamento, agendamento }) => {
const {getAuthorizationHeader} = useAuth() const { getAuthorizationHeader } = useAuth();
console.log(agendamento, 'aqui2')
const [sessoes,setSessoes] = useState(1)
const [tempoBaseConsulta, setTempoBaseConsulta] = useState(30); // NOVO: Tempo base da consulta em minutos
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 })
const [todosProfissionais, setTodosProfissionais] = useState([])
const [profissionaisFiltrados, setProfissionaisFiltrados] = useState([]);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [sessoes, setSessoes] = useState(1);
const [tempoBaseConsulta] = useState(30);
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [todosProfissionais, setTodosProfissionais] = useState([]);
const [profissionaisFiltrados, setProfissionaisFiltrados] = useState([]);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [horarioInicio, setHorarioInicio] = useState(''); const [horarioInicio, setHorarioInicio] = useState('');
const [horarioTermino, setHorarioTermino] = useState(''); const [horarioTermino, setHorarioTermino] = useState('');
const [horariosDisponiveis, sethorariosDisponiveis] = useState([]);
const [horariosDisponiveis, sethorariosDisponiveis] = useState([]) const authHeader = getAuthorizationHeader();
let authHeader = getAuthorizationHeader()
const FormatCPF = (valor) => { const FormatCPF = (valor) => {
const digits = String(valor).replace(/\D/g, '').slice(0, 11); const digits = String(valor).replace(/\D/g, '').slice(0, 11);
return digits return digits
.replace(/(\d{3})(\d)/, '$1.$2') .replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d)/, '$1.$2') .replace(/(\d{3})(\d)/, '$1.$2')
.replace(/(\d{3})(\d{1,2})$/, '$1-$2'); .replace(/(\d{3})(\d{1,2})$/, '$1-$2');
} };
const handleChange = (e) => { const handleChange = (e) => {
const {value, name} = e.target; const { value, name } = e.target;
console.log(value, name, agendamento)
if(name === 'email'){ if (name === 'email') {
setAgendamento({...agendamento, contato:{ setAgendamento(prev => ({
...agendamento.contato, ...prev,
email:value contato: { ...prev.contato, email: value }
}})} }));
else if(name === 'status'){ } else if (name === 'status') {
if(agendamento.status==='requested'){ setAgendamento(prev => ({
setAgendamento((prev) => ({ ...prev,
...prev, status: prev.status === 'requested' ? 'confirmed' : 'requested'
status:'confirmed', }));
})); } else if (name === 'paciente_cpf') {
}else if(agendamento.status === 'confirmed'){ const cpfFormatted = FormatCPF(value);
console.log(value) const fetchPatient = async () => {
setAgendamento((prev) => ({ const patientData = await GetPatientByCPF(cpfFormatted, authHeader);
...prev, if (patientData) {
status:'requested', setAgendamento(prev => ({
})); ...prev,
}} paciente_nome: patientData.full_name,
patient_id: patientData.id
else if(name === 'paciente_cpf'){ }));
}
let cpfFormatted = FormatCPF(value) };
const fetchPatient = async () => { setAgendamento(prev => ({ ...prev, paciente_cpf: cpfFormatted }));
let patientData = await GetPatientByCPF(cpfFormatted, authHeader); fetchPatient();
if (patientData) { } else if (name === 'convenio') {
setAgendamento((prev) => ({ setAgendamento(prev => ({ ...prev, insurance_provider: value }));
...prev, } else {
paciente_nome: patientData.full_name, setAgendamento(prev => ({ ...prev, [name]: value }));
patient_id: patientData.id
}));
}}
setAgendamento(prev => ({ ...prev, cpf: cpfFormatted }))
fetchPatient()
}else if(name==='convenio'){
setAgendamento({...agendamento,insurance_provider:value})
} }
else{ };
setAgendamento({...agendamento,[name]:value})
} const ChamarMedicos = useCallback(async () => {
} const Medicos = await GetAllDoctors(authHeader);
setTodosProfissionais(Medicos);
}, [authHeader]);
// Se estiver na página de edição esse useEffect pega o horario de inicio para coloca-lo no horarioInicio
useEffect(() => { useEffect(() => {
console.log("Horario", ) ChamarMedicos();
setHorarioInicio(formatarHora(agendamento.scheduled_at)) }, [ChamarMedicos]);
}, [])
useEffect(() => {
if (!agendamento.dataAtendimento || !agendamento.doctor_id) return;
useEffect(() => { const myHeaders = new Headers();
const ChamarMedicos = async () => { myHeaders.append("Content-Type", "application/json");
const Medicos = await GetAllDoctors(authHeader) myHeaders.append("apikey", API_KEY);
setTodosProfissionais(Medicos) myHeaders.append("Authorization", `Bearer ${authHeader.split(' ')[1]}`);
}
ChamarMedicos();
var myHeaders = new Headers(); const raw = JSON.stringify({
myHeaders.append("Content-Type", "application/json"); doctor_id: agendamento.doctor_id,
myHeaders.append("apikey", API_KEY) start_date: agendamento.dataAtendimento,
myHeaders.append("Authorization", `Bearer ${authHeader.split(' ')[1]}`); end_date: `${agendamento.dataAtendimento}T23:59:59.999Z`,
});
var raw = JSON.stringify({ const requestOptions = {
"doctor_id": agendamento.doctor_id, method: 'POST',
"start_date": agendamento.dataAtendimento, headers: myHeaders,
"end_date": `${agendamento.dataAtendimento}T23:59:59.999Z`, body: raw,
};
});
var requestOptions = { fetch("https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/get-available-slots", requestOptions)
method: 'POST', .then(response => response.json())
headers: myHeaders, .then(result => sethorariosDisponiveis(result))
body: raw, .catch(error => console.log('error', error));
redirect: 'follow' }, [agendamento.dataAtendimento, agendamento.doctor_id, authHeader]);
};
fetch("https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/get-available-slots", requestOptions) const handleSearchProfissional = (e) => {
.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; const term = e.target.value;
handleChange(e); handleChange(e);
// 2. Lógica de filtragem:
if (term.trim() === '') { if (term.trim() === '') {
setProfissionaisFiltrados([]); setProfissionaisFiltrados([]);
setIsDropdownOpen(false); setIsDropdownOpen(false);
return; return;
} }
// Adapte o nome da propriedade (ex: 'nome', 'full_name')
const filtered = todosProfissionais.filter(p => const filtered = todosProfissionais.filter(p =>
p.full_name.toLowerCase().includes(term.toLowerCase()) p.full_name.toLowerCase().includes(term.toLowerCase())
); );
setProfissionaisFiltrados(filtered); setProfissionaisFiltrados(filtered);
setIsDropdownOpen(filtered.length > 0); // Abre se houver resultados setIsDropdownOpen(filtered.length > 0);
}; };
const handleSelectProfissional = (profissional) => {
// FUNÇÃO PARA SELECIONAR UM ITEM DO DROPDOWN
const handleSelectProfissional = async (profissional) => {
setAgendamento(prev => ({ setAgendamento(prev => ({
...prev, ...prev,
doctor_id: profissional.id, doctor_id: profissional.id,
nome_medico: profissional.full_name nome_medico: profissional.full_name
})); }));
// 2. Fecha o dropdown
setProfissionaisFiltrados([]); setProfissionaisFiltrados([]);
setIsDropdownOpen(false); setIsDropdownOpen(false);
}; };
const formatarHora = (datetimeString) => {
const formatarHora = (datetimeString) => { return datetimeString?.substring(11, 16) || '';
return datetimeString.substring(11, 16);
}; };
const opcoesDeHorario = horariosDisponiveis?.slots?.map(item => ({ const opcoesDeHorario = horariosDisponiveis?.slots?.map(item => ({
value: formatarHora(item.datetime), value: formatarHora(item.datetime),
label: formatarHora(item.datetime), label: formatarHora(item.datetime),
disabled: !item.available disabled: !item.available
})); })) || [];
const calcularHorarioTermino = (inicio, sessoes, tempoBase) => { const calcularHorarioTermino = useCallback((inicio, sessoes, tempoBase) => {
if (!inicio || inicio.length !== 5 || !inicio.includes(':')) return ''; if (!inicio || inicio.length !== 5 || !inicio.includes(':')) return '';
const [horas, minutos] = inicio.split(':').map(Number); const [horas, minutos] = inicio.split(':').map(Number);
@ -184,206 +148,210 @@ const calcularHorarioTermino = (inicio, sessoes, tempoBase) => {
const minutoTermino = minutosTermino % 60; 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(() => { useEffect(() => {
// Recalcula o horário de término sempre que houver alteração nas variáveis dependentes
const novoTermino = calcularHorarioTermino(horarioInicio, sessoes, tempoBaseConsulta); const novoTermino = calcularHorarioTermino(horarioInicio, sessoes, tempoBaseConsulta);
setHorarioTermino(novoTermino); setHorarioTermino(novoTermino);
setAgendamento(prev => ({ setAgendamento(prev => ({
...prev, ...prev,
horarioTermino: novoTermino horarioTermino: novoTermino
})); }));
}, [horarioInicio, sessoes, tempoBaseConsulta, setAgendamento]); }, [horarioInicio, sessoes, tempoBaseConsulta, setAgendamento, calcularHorarioTermino]);
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
alert("Agendamento salvo!"); setShowSuccessModal(true);
onSave({...agendamento, horarioInicio:horarioInicio}) };
const handleCloseModal = () => {
setShowSuccessModal(false);
onSave({ ...agendamento, horarioInicio: horarioInicio });
}; };
return ( return (
<div className="form-container"> <div className="form-container">
{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}> <form className="form-agendamento" onSubmit={handleSubmit}>
<h2 className="section-title">Informações do paciente</h2> <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"> <div className="campo-de-input">
<label>CPF do paciente</label> <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>
<div className="campo-de-input"> <div className="campo-de-input">
<label>Nome *</label> <label>Nome *</label>
<input type="text" name="paciente_nome" value={agendamento.paciente_nome} placeholder="Insira o nome do paciente" required onChange={handleChange} /> <input type="text" name="paciente_nome" value={agendamento.paciente_nome} placeholder="Insira o nome do paciente" required onChange={handleChange} />
</div> </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> <div className="campos-informacoes-paciente" id="informacoes-paciente-linha-tres">
<div>
<label>Convênio</label>
<select name="convenio" onChange={handleChange} value={agendamento.insurance_provider}>
<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> <h2 className="section-title">Informações do atendimento</h2>
<div className="campo-informacoes-atendimento"> <div className="campo-informacoes-atendimento">
<div className="campo-de-input-container">
<div className="campo-de-input-container"> {/* NOVO CONTAINER PAI */} <div className="campo-de-input">
<div className="campo-de-input"> <label>Nome do profissional *</label>
<label>Nome do profissional *</label> <input
<input type="text"
type="text" name="nome_medico"
name="nome_medico" // Use o nome correto da propriedade no estado `agendamento` onChange={handleSearchProfissional}
onChange={handleSearchProfissional} value={agendamento?.nome_medico || ''}
value={agendamento?.nome_medico} autoComplete="off"
autoComplete="off" // Ajuda a evitar o autocomplete nativo do navegador required
required />
/> </div>
</div>
{/* DROPDOWN - RENDERIZAÇÃO CONDICIONAL */} {isDropdownOpen && profissionaisFiltrados.length > 0 && (
{isDropdownOpen && profissionaisFiltrados.length > 0 && ( <div className='dropdown-profissionais'>
<div className='dropdown-profissionais'> {profissionaisFiltrados.map((profissional) => (
{profissionaisFiltrados.map((profissional) => ( <div
<div key={profissional.id}
key={profissional.id} // Use o ID do profissional
className='dropdown-item' className='dropdown-item'
onClick={() => handleSelectProfissional(profissional)} onClick={() => handleSelectProfissional(profissional)}
> >
{profissional.full_name} {profissional.full_name}
</div> </div>
))} ))}
</div> </div>
)} )}
</div> </div>
<div className="tipo_atendimento"> <div className="tipo_atendimento">
<label>Tipo de atendimento *</label> <label>Tipo de atendimento *</label>
<select onChange={handleChange} name="tipo_atendimento" > <select onChange={handleChange} name="tipo_atendimento" value={agendamento.tipo_atendimento}>
<option value="presencial" selected>Presencial</option> <option value="presencial">Presencial</option>
<option value="teleconsulta">Teleconsulta</option> <option value="teleconsulta">Teleconsulta</option>
</select> </select>
</div> </div>
</div> </div>
<section id="informacoes-atendimento-segunda-linha"> <section id="informacoes-atendimento-segunda-linha">
<section id="informacoes-atendimento-segunda-linha-esquerda"> <section id="informacoes-atendimento-segunda-linha-esquerda">
<div className="campo-informacoes-atendimento">
<div className="campo-informacoes-atendimento"> <div className="campo-de-input">
<label>Data *</label>
<input type="date" name="dataAtendimento" onChange={handleChange} value={agendamento.dataAtendimento} required />
<div className="campo-de-input"> </div>
<label>Data *</label>
<input type="date" name="dataAtendimento" onChange={handleChange} value={agendamento.dataAtendimento} required /> <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)}
>
<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>
<div className='seletor-wrapper'>
<label>Número de Sessões *</label>
<div className='sessao-contador'>
<button
type="button"
onClick={() => setSessoes(prev => Math.max(0, prev - 1))}
disabled={sessoes === 0}
>
<i className="bi bi-chevron-compact-left"></i>
</button>
<p className='sessao-valor'>{sessoes}</p>
<button
type="button"
onClick={() => setSessoes(prev => Math.min(3, prev + 1))}
disabled={sessoes === 3}
>
<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>
</div> </div>
</div> </section>
<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}
>
{opcao.label}
{opcao.disabled && " (Indisponível)"}
</option>
))}
</select>
</div>
{/* SELETOR DE SESSÕES MODIFICADO */} <section className="informacoes-atendimento-segunda-linha-direita">
{/* Removemos o 'label' para evitar o desalinhamento e colocamos o texto acima */} <div className="campo-de-input">
<div className='seletor-wrapper'> <label>Observações</label>
<label>Número de Sessões *</label> {/* Novo label para o seletor */} <textarea name="observacoes" rows="4" cols="1" onChange={handleChange} value={agendamento.observacoes || ''}></textarea>
<div className='sessao-contador'> </div>
<button </section>
type="button" /* Adicionado para evitar submissão de formulário */ </section>
onClick={() => {if(sessoes === 0)return; else(setSessoes(sessoes - 1))}}
disabled={sessoes === 0} /* Desabilita o botão no limite */ <div className="campo-de-input-check">
> <input
<i className="bi bi-chevron-compact-left"></i> className="form-check-input form-custom-check"
</button> type="checkbox"
name="status"
<p className='sessao-valor'>{sessoes}</p> {/* Adicionada classe para estilização */} onChange={handleChange}
checked={agendamento.status === 'requested'}
<button />
type="button" /* Adicionado para evitar submissão de formulário */ <label className="form-check-label checkbox-label" htmlFor="status">
onClick={() => {if(sessoes === 3 )return; else(setSessoes(sessoes + 1))}} Adicionar a fila de espera
disabled={sessoes === 3} /* Desabilita o botão no limite */ </label>
>
<i className="bi bi-chevron-compact-right"></i>
</button>
</div> </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>
<section className="informacoes-atendimento-segunda-linha-direita"> <div className="form-actions">
<button type="submit" className="btn-primary">Salvar agendamento</button>
<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>
<button type="button" className="btn-cancel" onClick={onCancel}>Cancelar</button> <button type="button" className="btn-cancel" onClick={onCancel}>Cancelar</button>
</div> </div>
</form> </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> </div>
); );
}; };

View File

@ -43,8 +43,6 @@ svg{
font-family: 'Material Symbols Outlined'; font-family: 'Material Symbols Outlined';
font-size: 20px; font-size: 20px;
color:black color:black
} }
.form-container { .form-container {
@ -152,7 +150,6 @@ svg{
background: #e5e7eb; background: #e5e7eb;
} }
.cardconsulta-infosecundaria{ .cardconsulta-infosecundaria{
font-size: small; font-size: small;
} }
@ -166,10 +163,8 @@ svg{
.campo-de-input{ .campo-de-input{
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#informacoes-atendimento-segunda-linha{ #informacoes-atendimento-segunda-linha{
margin-top: 10px; margin-top: 10px;
display: flex; display: flex;
@ -185,13 +180,13 @@ textarea{
.campos-informacoes-paciente, .campos-informacoes-paciente,
.campo-informacoes-atendimento { .campo-informacoes-atendimento {
display: flex; display: flex;
gap: 16px; /* espaço entre campos */ gap: 16px;
} }
.campo-de-input { .campo-de-input {
flex: 1; /* todos os filhos ocupam mesmo espaço */ flex: 1;
display: flex; display: flex;
flex-direction: column; /* mantém label em cima do input */ flex-direction: column;
} }
#informacoes-atendimento-segunda-linha-esquerda select[name="unidade"]{ #informacoes-atendimento-segunda-linha-esquerda select[name="unidade"]{
@ -213,7 +208,7 @@ select[name=solicitante]{
.form-container { .form-container {
width: 100%; width: 100%;
max-width: none; max-width: none;
margin: 0; /* >>> sem espaço para encostar no topo <<< */ margin: 0;
background: #ffffff; background: #ffffff;
border-radius: 12px; border-radius: 12px;
padding: 24px; padding: 24px;
@ -306,169 +301,219 @@ html[data-bs-theme="dark"] svg {
color: #e0e0e0 !important; color: #e0e0e0 !important;
} }
/* CONTAINER PAI - ESSENCIAL PARA POSICIONAMENTO */
.campo-de-input-container { .campo-de-input-container {
position: relative; /* Define o contexto para o dropdown */ position: relative;
/* ... outros estilos de layout (display, margin, etc.) ... */
} }
/* ESTILO DA LISTA DROPDOWN */
.dropdown-profissionais { .dropdown-profissionais {
position: absolute; /* Flutua em relação ao pai (.campo-de-input-container) */ position: absolute;
top: 100%; /* Começa logo abaixo do input */ top: 100%;
left: 0; left: 0;
width: 100%; /* Ocupa toda a largura do container pai */ width: 100%;
/* Estilos visuais */
background-color: white; background-color: white;
border: 1px solid #ccc; border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); 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; max-height: 200px;
overflow-y: auto; overflow-y: auto;
} }
/* ESTILO DE CADA ITEM DO DROPDOWN */
.dropdown-item { .dropdown-item {
padding: 10px; padding: 10px;
cursor: pointer; cursor: pointer;
} }
.dropdown-item:hover { .dropdown-item:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
} }
.tipo_atendimento{ .tipo_atendimento{
margin-left: 3rem; margin-left: 3rem;
} }
/* 1. Estilização Básica e Tamanho (Estado Padrão - Antes de Clicar) */
.checkbox-customs { .checkbox-customs {
/* Remove a aparência padrão do navegador/Bootstrap */
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: none; -moz-appearance: none;
appearance: none; appearance: none;
width: 1.2rem;
/* Define o tamanho desejado */
width: 1.2rem; /* Ajuste conforme o seu gosto (ex: 1.2rem = 19.2px) */
height: 1.2rem; height: 1.2rem;
background-color: #fff;
/* Define o visual "branco com borda preta" */ border: 1px solid #000;
background-color: #fff; /* Fundo branco */ border-radius: 0.25rem;
border: 1px solid #000; /* Borda preta de 1px */
border-radius: 0.25rem; /* Borda levemente arredondada (opcional, imita Bootstrap) */
/* Centraliza o 'check' (quando aparecer) */
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
cursor: pointer; /* Indica que é clicável */ cursor: pointer;
transition: all 0.5s ease;
/* Adiciona a transição suave */
transition: all 0.5s ease; /* Transição em 0.5 segundos para todas as propriedades */
} }
/* 2. Estilização no Estado Clicado (:checked) */
.checkbox-customs:checked { .checkbox-customs:checked {
/* Quando clicado, mantém o fundo branco (se quiser mudar, altere aqui) */
background-color: #fff; 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 */ .checkbox-customs:checked {
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");
background-size: 100% 100%; background-size: 100% 100%;
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
/* Container dos três elementos na linha */
.linha { .linha {
display: flex; display: flex;
align-items: flex-end; /* Garante que os campos de input e o seletor fiquem alinhados pela base */ align-items: flex-end;
gap: 20px; /* Espaçamento entre os campos */ gap: 20px;
} }
/* ------------------------------------------- */
/* ESTILIZAÇÃO DO SELETOR DE SESSÕES */
/* ------------------------------------------- */
.seletor-wrapper { .seletor-wrapper {
/* Garante que o label e o contador fiquem alinhados verticalmente com os selects */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.sessao-contador { .sessao-contador {
/* Estilo de "campo de input" */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background-color: #e9ecef;
/* Cores e Bordas */ border: 1px solid #ced4da;
background-color: #e9ecef; /* Cor cinza claro dos inputs do Bootstrap */ border-radius: 0.25rem;
border: 1px solid #ced4da; /* Borda sutil */ height: 40px;
border-radius: 0.25rem; /* Bordas arredondadas (Padrão Bootstrap) */ width: 100px;
padding: 0 5px;
/* 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 */
font-size: 1rem; font-size: 1rem;
font-weight: 500; font-weight: 500;
} }
.sessao-valor { .sessao-valor {
/* Estilo do número de sessões */
margin: 0; margin: 0;
padding: 0 5px; padding: 0 5px;
font-size: 1.1rem; /* Um pouco maior que o texto dos selects */ font-size: 1.1rem;
color: #007bff; /* Cor azul destacada (como na sua imagem) */ color: #007bff;
} }
.sessao-contador button { .sessao-contador button {
/* Estilo dos botões de chevron */
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
padding: 0 2px; padding: 0 2px;
color: #495057; /* Cor do ícone */ color: #495057;
font-size: 1.5rem; /* Aumenta o tamanho dos ícones do chevron */ font-size: 1.5rem;
line-height: 1; /* Alinha o ícone verticalmente */ line-height: 1;
transition: color 0.2s; transition: color 0.2s;
} }
.sessao-contador button:hover:not(:disabled) { .sessao-contador button:hover:not(:disabled) {
color: #007bff; /* Cor azul ao passar o mouse */ color: #007bff;
} }
.sessao-contador button:disabled { .sessao-contador button:disabled {
cursor: not-allowed; cursor: not-allowed;
color: #adb5bd; /* Cor mais clara quando desabilitado */ color: #adb5bd;
}
/* ------------------------------------------- */
/* GARANTINDO COERÊNCIA NOS SELECTS */
/* ------------------------------------------- */
.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;
}
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;
}
.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;
}

View File

@ -20,6 +20,11 @@
font-size: 24px; font-size: 24px;
cursor: pointer; cursor: pointer;
padding: 5px; padding: 5px;
transition: transform 0.2s ease;
}
.phone-icon-container:hover {
transform: scale(1.1);
} }
.phone-icon { .phone-icon {
@ -33,75 +38,173 @@
} }
.profile-picture-container { .profile-picture-container {
width: 40px; width: 45px;
height: 40px; height: 45px;
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
border: 2px solid #ccc; border: 2px solid #007bff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); 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 { .profile-placeholder {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: #A9A9A9; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%; border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative; position: relative;
} }
.profile-placeholder::after { .placeholder-icon {
content: ''; font-size: 20px;
position: absolute; color: white;
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;
} }
.profile-dropdown { .profile-dropdown {
position: absolute; position: absolute;
top: 50px; top: 60px;
right: 0; right: 0;
background-color: white; background-color: white;
border: 1px solid #ddd; border: 1px solid #e0e0e0;
border-radius: 5px; border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 1000; z-index: 1000;
min-width: 150px; min-width: 180px;
overflow: hidden; overflow: hidden;
animation: dropdownFadeIn 0.2s ease-out;
}
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.dropdown-button { .dropdown-button {
background: none; background: none;
border: none; border: none;
padding: 10px 15px; padding: 12px 16px;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
color: #333; color: #333;
transition: background-color 0.2s; transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 8px;
} }
.dropdown-button:hover { .dropdown-button:hover {
background-color: #f0f0f0; background-color: #f8f9fa;
} }
.logout-button { .logout-button {
color: #cc0000; color: #dc3545;
border-top: 1px solid #f0f0f0;
} }
.logout-button:hover { .logout-button:hover {
background-color: #ffe0e0; 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 { .suporte-card-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@ -187,6 +290,7 @@
margin-bottom: 0; margin-bottom: 0;
} }
/* Chat Online */
.chat-overlay { .chat-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@ -246,6 +350,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background-color 0.2s;
} }
.fechar-chat:hover { .fechar-chat:hover {
@ -260,6 +365,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
background-color: #fafafa;
} }
.mensagem { .mensagem {
@ -267,33 +373,53 @@
padding: 0.75rem; padding: 0.75rem;
border-radius: 12px; border-radius: 12px;
position: relative; position: relative;
animation: messageSlideIn 0.3s ease-out;
}
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.mensagem.usuario { .mensagem.usuario {
align-self: flex-end; align-self: flex-end;
background-color: #e3f2fd; background-color: #007bff;
color: white;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
.mensagem.suporte { .mensagem.suporte {
align-self: flex-start; align-self: flex-start;
background-color: #f5f5f5; background-color: white;
border: 1px solid #e0e0e0;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
} }
.mensagem-texto { .mensagem-texto {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
word-wrap: break-word; word-wrap: break-word;
line-height: 1.4;
} }
.mensagem-hora { .mensagem-hora {
font-size: 0.7rem; font-size: 0.7rem;
color: #666; opacity: 0.8;
text-align: right; text-align: right;
} }
.mensagem.usuario .mensagem-hora {
color: rgba(255, 255, 255, 0.8);
}
.mensagem.suporte .mensagem-hora { .mensagem.suporte .mensagem-hora {
text-align: left; text-align: left;
color: #666;
} }
.chat-input { .chat-input {
@ -313,93 +439,52 @@
outline: none; outline: none;
font-size: 0.9rem; font-size: 0.9rem;
background-color: white; background-color: white;
transition: border-color 0.2s;
} }
.chat-campo:focus { .chat-campo:focus {
border-color: #1e3a8a; border-color: #007bff;
} }
.chat-enviar { .chat-enviar {
background-color: #1e3a8a; background-color: #007bff;
color: white; color: white;
border: none; border: none;
padding: 0.75rem 1rem; padding: 0.75rem 1.5rem;
border-radius: 20px; border-radius: 20px;
cursor: pointer; cursor: pointer;
font-size: 0.9rem; font-size: 0.9rem;
transition: background-color 0.2s;
} }
.chat-enviar:hover { .chat-enviar:hover {
background-color: #1e40af; background-color: #0056b3;
}
/* 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 { /* Responsividade */
background-color: white; @media (max-width: 768px) {
padding: 2rem; .header-container {
border-radius: 12px; padding: 10px 15px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); }
max-width: 400px;
width: 90%; .right-corner-elements {
text-align: center; gap: 15px;
} }
.logout-modal-content h3 { .profile-picture-container {
margin-bottom: 1rem; width: 40px;
color: #333; height: 40px;
font-size: 1.25rem; }
}
.suporte-card-container,
.logout-modal-content p { .chat-container {
margin-bottom: 2rem; margin-right: 10px;
color: #666; margin-left: 10px;
line-height: 1.4; }
}
.suporte-card,
.logout-modal-buttons { .chat-online {
display: flex; width: calc(100vw - 20px);
gap: 1rem; max-width: none;
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;
} }

View File

@ -9,10 +9,29 @@ const Header = () => {
const [mensagem, setMensagem] = useState(''); const [mensagem, setMensagem] = useState('');
const [mensagens, setMensagens] = useState([]); const [mensagens, setMensagens] = useState([]);
const [showLogoutModal, setShowLogoutModal] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false);
const [avatarUrl, setAvatarUrl] = useState(null);
const navigate = useNavigate(); const navigate = useNavigate();
const chatInputRef = useRef(null); const chatInputRef = useRef(null);
const mensagensContainerRef = 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(() => { useEffect(() => {
if (isChatOpen && chatInputRef.current) { if (isChatOpen && chatInputRef.current) {
chatInputRef.current.focus(); chatInputRef.current.focus();
@ -31,6 +50,10 @@ const Header = () => {
setIsDropdownOpen(false); setIsDropdownOpen(false);
}; };
const handleLogoutCancel = () => {
setShowLogoutModal(false);
};
const handleLogoutConfirm = async () => { const handleLogoutConfirm = async () => {
try { try {
const token = const token =
@ -91,8 +114,6 @@ const Header = () => {
} }
}; };
const handleLogoutCancel = () => setShowLogoutModal(false);
const handleProfileClick = () => { const handleProfileClick = () => {
setIsDropdownOpen(!isDropdownOpen); setIsDropdownOpen(!isDropdownOpen);
if (isSuporteCardOpen) setIsSuporteCardOpen(false); if (isSuporteCardOpen) setIsSuporteCardOpen(false);
@ -120,7 +141,7 @@ const Header = () => {
setMensagens([ setMensagens([
{ {
id: 1, id: 1,
texto: 'Olá! 👋 Bem-vindo ao suporte Mediconnect. Como posso te ajudar hoje?', texto: 'Olá! Me chamo Ágatha e sou sua assistente virtual. 👋 Bem-vindo ao suporte Mediconnect. Como posso te ajudar hoje?',
remetente: 'suporte', remetente: 'suporte',
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
} }
@ -132,7 +153,6 @@ const Header = () => {
setMensagem(''); setMensagem('');
}; };
// 🚀 Envio de mensagem com IA
const handleEnviarMensagem = async (e) => { const handleEnviarMensagem = async (e) => {
e.preventDefault(); e.preventDefault();
if (mensagem.trim() === '') return; if (mensagem.trim() === '') return;
@ -170,7 +190,7 @@ const Header = () => {
console.error("Erro ao conectar com o servidor:", error); console.error("Erro ao conectar com o servidor:", error);
const erroMsg = { const erroMsg = {
id: Date.now() + 1, id: Date.now() + 1,
texto: "Ops! 😥 Ocorreu um erro ao tentar falar com o suporte.", texto: "Ops! Ocorreu um erro ao tentar falar com o suporte.",
remetente: 'suporte', remetente: 'suporte',
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
}; };
@ -251,8 +271,12 @@ const Header = () => {
{isDropdownOpen && ( {isDropdownOpen && (
<div className="profile-dropdown"> <div className="profile-dropdown">
<button type="button" onClick={handleViewProfile} className="dropdown-button">Ver Perfil</button> <button type="button" onClick={handleViewProfile} className="dropdown-button">
<button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">Sair (Logout)</button> Ver Perfil
</button>
<button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">
Sair (Logout)
</button>
</div> </div>
)} )}
</div> </div>
@ -295,4 +319,4 @@ const Header = () => {
); );
}; };
export default Header; export default Header;

View File

@ -260,16 +260,6 @@ function Sidebar({ menuItems }) {
})} })}
{/* Logout */} {/* 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 /> <TrocardePerfis />
</ul> </ul>

View 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;
}

View File

@ -1,31 +1,58 @@
import React, { useState, useEffect } from "react"; 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 { UserInfos } from "./utils/Functions-Endpoints/General";
import { useAuth } from "./utils/AuthProvider"; 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 TrocardePerfis = () => {
const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth(); const { getAuthorizationHeader } = useAuth();
const [selectedProfile, setSelectedProfile] = useState("");
const [showProfiles, setShowProfiles] = useState([]); const [showProfiles, setShowProfiles] = useState([]);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const authHeader = getAuthorizationHeader(); const authHeader = getAuthorizationHeader();
setSelectedProfile(location.pathname || ""); try {
const userInfo = await UserInfos(authHeader); const userInfo = await UserInfos(authHeader);
setShowProfiles(userInfo?.roles || []); setShowProfiles(userInfo?.roles || []);
} catch (error) {
console.error("Erro ao buscar informações do usuário:", error);
setShowProfiles([]);
}
}; };
fetchData(); fetchData();
}, [location.pathname, getAuthorizationHeader]); }, [getAuthorizationHeader]);
const handleSelectChange = (e) => { const handleProfileClick = (route) => {
const route = e.target.value; if (route) {
setSelectedProfile(route); navigate(route);
if (route) navigate(route); setIsOpen(false);
}
};
const handleToggle = () => {
setIsOpen(prev => !prev);
}; };
const options = [ const options = [
@ -40,20 +67,47 @@ const TrocardePerfis = () => {
); );
return ( return (
<div className="container-perfis"> <div className="container-perfis-toggle">
<p className="acesso-text">Acesso aos módulos:</p>
<select <div
className="perfil-select" className="toggle-button"
value={selectedProfile} onClick={handleToggle}
onChange={handleSelectChange} role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleToggle();
}
}}
> >
<option value="">Selecionar perfil</option> <span className="acesso-text">Acesso aos módulos</span>
{options.map((opt) => ( <ToggleIcon isOpen={isOpen} />
<option key={opt.key} value={opt.route}> </div>
{opt.label}
</option> {isOpen && (
))} <div className="perfil-list">
</select> {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> </div>
); );
}; };

View File

@ -709,7 +709,7 @@ const handleAvailabilityUpdate = useCallback((newAvailability) => {
</div> </div>
{/* BOTÕES DE AÇÃO */} {/* BOTÕES DE AÇÃO */}
<div className="actions-container"> <div className="btns-container">
<button <button
className="btn btn-success btn-submit" className="btn btn-success btn-submit"
onClick={handleSubmit} onClick={handleSubmit}

View File

@ -632,7 +632,7 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
)} )}
{/* BOTÕES DE AÇÃO */} {/* BOTÕES DE AÇÃO */}
<div className="actions-container"> <div className="btns-container">
<button className="btn btn-success btn-submit" onClick={handleSubmit} disabled={isLoading}> <button className="btn btn-success btn-submit" onClick={handleSubmit} disabled={isLoading}>
{isLoading ? 'Salvando...' : 'Salvar Paciente'} {isLoading ? 'Salvando...' : 'Salvar Paciente'}
</button> </button>
@ -642,6 +642,7 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
</button> </button>
</Link> </Link>
</div> </div>
</div> </div>
); );
} }

View File

@ -1,48 +1,56 @@
import API_KEY from '../apiKeys'; import API_KEY from '../apiKeys';
const GetDoctorByID = async (ID, authHeader) => { const GetDoctorByID = async (ID, authHeader) => {
var myHeaders = new Headers(); const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY); myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader); if (authHeader) myHeaders.append('Authorization', authHeader);
const requestOptions = { method: 'GET', redirect: 'follow', headers: myHeaders }; const requestOptions = {
const res = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${ID}`, 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(); const DictMedico = await res.json();
return DictMedico; return DictMedico;
}; };
const GetAllDoctors = async (authHeader) => { const GetAllDoctors = async (authHeader) => {
var myHeaders = new Headers(); const myHeaders = new Headers();
myHeaders.append("apikey", API_KEY); myHeaders.append('apikey', API_KEY);
myHeaders.append("Authorization", authHeader); if (authHeader) myHeaders.append('Authorization', authHeader);
var requestOptions = { const requestOptions = {
method: 'GET', method: 'GET',
headers: myHeaders, headers: myHeaders,
redirect: 'follow' redirect: 'follow',
}; };
const result = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions)
const DictMedicos = await result.json() const result = await fetch(
return DictMedicos '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 GetDoctorByName = async (nome, authHeader) => {
const Medicos = await GetAllDoctors(authHeader) const Medicos = await GetAllDoctors(authHeader);
for (let i = 0; i < Medicos.length; i++) {
if (Medicos[i].full_name === nome) { for (let i = 0; i < Medicos.length; i++) {
console.log('Medico encontrado:', Medicos[i]); if (Medicos[i].full_name === nome) {
return Medicos[i]; console.log('Médico encontrado:', Medicos[i]);
} return Medicos[i];
else{console.log("nada encontrado")} }
} }
} console.log('Nenhum médico encontrado com o nome:', nome);
return null;
};
export {GetDoctorByID, GetDoctorByName, GetAllDoctors} export { GetDoctorByID, GetDoctorByName, GetAllDoctors };

View File

@ -12,7 +12,6 @@ function TableDoctor() {
const [filtroEspecialidade, setFiltroEspecialidade] = useState("Todos"); const [filtroEspecialidade, setFiltroEspecialidade] = useState("Todos");
const [filtroAniversariante, setFiltroAniversariante] = useState(false); const [filtroAniversariante, setFiltroAniversariante] = useState(false);
const [showFiltrosAvancados, setShowFiltrosAvancados] = useState(false); const [showFiltrosAvancados, setShowFiltrosAvancados] = useState(false);
const [filtroCidade, setFiltroCidade] = useState(""); const [filtroCidade, setFiltroCidade] = useState("");
const [filtroEstado, setFiltroEstado] = useState(""); const [filtroEstado, setFiltroEstado] = useState("");
@ -22,6 +21,9 @@ function TableDoctor() {
const [dataFinal, setDataFinal] = useState(""); const [dataFinal, setDataFinal] = useState("");
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedDoctorId, setSelectedDoctorId] = useState(null); const [selectedDoctorId, setSelectedDoctorId] = useState(null);
@ -36,9 +38,9 @@ function TableDoctor() {
setIdadeMaxima(""); setIdadeMaxima("");
setDataInicial(""); setDataInicial("");
setDataFinal(""); setDataFinal("");
setPaginaAtual(1);
}; };
const deleteDoctor = async (id) => { const deleteDoctor = async (id) => {
const authHeader = getAuthorizationHeader() const authHeader = getAuthorizationHeader()
console.log(id, 'teu id') console.log(id, 'teu id')
@ -63,7 +65,6 @@ function TableDoctor() {
} }
}; };
const ehAniversariante = (dataNascimento) => { const ehAniversariante = (dataNascimento) => {
if (!dataNascimento) return false; if (!dataNascimento) return false;
const hoje = new Date(); const hoje = new Date();
@ -75,7 +76,6 @@ function TableDoctor() {
); );
}; };
const calcularIdade = (dataNascimento) => { const calcularIdade = (dataNascimento) => {
if (!dataNascimento) return 0; if (!dataNascimento) return 0;
const hoje = new Date(); const hoje = new Date();
@ -104,18 +104,16 @@ function TableDoctor() {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions) fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions)
.then(response => response.json()) .then(response => response.json())
.then(result => {setMedicos(result); console.log(result)}) .then(result => setMedicos(result))
.catch(error => console.log('error', error)); .catch(error => console.log('error', error));
}, [isAuthenticated, getAuthorizationHeader]); }, [isAuthenticated, getAuthorizationHeader]);
const medicosFiltrados = Array.isArray(medicos) ? medicos.filter((medico) => { const medicosFiltrados = Array.isArray(medicos) ? medicos.filter((medico) => {
const buscaNome = medico.full_name?.toLowerCase().includes(search.toLowerCase()); const buscaNome = medico.full_name?.toLowerCase().includes(search.toLowerCase());
const buscaCPF = medico.cpf?.toLowerCase().includes(search.toLowerCase()); const buscaCPF = medico.cpf?.toLowerCase().includes(search.toLowerCase());
const buscaEmail = medico.email?.toLowerCase().includes(search.toLowerCase()); const buscaEmail = medico.email?.toLowerCase().includes(search.toLowerCase());
const passaBusca = search === "" || buscaNome || buscaCPF || buscaEmail; const passaBusca = search === "" || buscaNome || buscaCPF || buscaEmail;
const passaEspecialidade = filtroEspecialidade === "Todos" || medico.specialty === filtroEspecialidade; const passaEspecialidade = filtroEspecialidade === "Todos" || medico.specialty === filtroEspecialidade;
const passaAniversario = filtroAniversariante const passaAniversario = filtroAniversariante
@ -132,23 +130,62 @@ function TableDoctor() {
const passaIdadeMinima = idadeMinima ? idade >= parseInt(idadeMinima) : true; const passaIdadeMinima = idadeMinima ? idade >= parseInt(idadeMinima) : true;
const passaIdadeMaxima = idadeMaxima ? idade <= parseInt(idadeMaxima) : true; const passaIdadeMaxima = idadeMaxima ? idade <= parseInt(idadeMaxima) : true;
const passaDataInicial = dataInicial ? const passaDataInicial = dataInicial ?
medico.created_at && new Date(medico.created_at) >= new Date(dataInicial) : true; medico.created_at && new Date(medico.created_at) >= new Date(dataInicial) : true;
const passaDataFinal = dataFinal ? const passaDataFinal = dataFinal ?
medico.created_at && new Date(medico.created_at) <= new Date(dataFinal) : true; medico.created_at && new Date(medico.created_at) <= new Date(dataFinal) : true;
const resultado = passaBusca && passaEspecialidade && passaAniversario && const resultado = passaBusca && passaEspecialidade && passaAniversario &&
passaCidade && passaEstado && passaIdadeMinima && passaIdadeMaxima && passaCidade && passaEstado && passaIdadeMinima && passaIdadeMaxima &&
passaDataInicial && passaDataFinal; passaDataInicial && passaDataFinal;
return resultado; 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(() => { useEffect(() => {
console.log(` Médicos totais: ${medicos.length}, Filtrados: ${medicosFiltrados.length}`); setPaginaAtual(1);
}, [medicos, medicosFiltrados, search]); }, [search, filtroEspecialidade, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal]);
return ( return (
<> <>
@ -169,7 +206,6 @@ function TableDoctor() {
</div> </div>
<div className="card-body"> <div className="card-body">
<div className="card p-3 mb-3 table-doctor-filters"> <div className="card p-3 mb-3 table-doctor-filters">
<h5 className="mb-3"> <h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i>{" "} <i className="bi bi-funnel-fill me-2 text-primary"></i>{" "}
@ -180,16 +216,15 @@ function TableDoctor() {
<input <input
type="text" type="text"
className="form-control" className="form-control"
placeholder="Buscar por nome ou CPF..." placeholder="Buscar por nome, CPF ou email..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
<small className="text-muted"> <small className="text-muted">
Digite o nome completo ou número do CPF Digite o nome completo, CPF ou email
</small> </small>
</div> </div>
<div className="filtros-basicos"> <div className="filtros-basicos">
<select <select
className="form-select filter-especialidade" className="form-select filter-especialidade"
@ -213,7 +248,6 @@ function TableDoctor() {
</select> </select>
<div className="filter-buttons-container"> <div className="filter-buttons-container">
<button <button
className={`btn filter-btn ${filtroAniversariante className={`btn filter-btn ${filtroAniversariante
? "btn-primary" ? "btn-primary"
@ -243,13 +277,11 @@ function TableDoctor() {
</button> </button>
</div> </div>
{showFiltrosAvancados && ( {showFiltrosAvancados && (
<div className="mt-3 p-3 border rounded advanced-filters"> <div className="mt-3 p-3 border rounded advanced-filters">
<h6 className="mb-3">Filtros Avançados</h6> <h6 className="mb-3">Filtros Avançados</h6>
<div className="row g-3"> <div className="row g-3">
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label fw-bold">Cidade</label> <label className="form-label fw-bold">Cidade</label>
<input <input
@ -296,7 +328,6 @@ function TableDoctor() {
/> />
</div> </div>
{/* Data de Cadastro */}
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label fw-bold">Data inicial</label> <label className="form-label fw-bold">Data inicial</label>
<input <input
@ -318,17 +349,21 @@ function TableDoctor() {
</div> </div>
</div> </div>
)} )}
<div className="mt-3">
<div className="contador-medicos">
{medicosFiltrados.length} DE {medicos.length} MÉDICOS ENCONTRADOS
</div>
</div>
</div> </div>
{(search || filtroEspecialidade !== "Todos" || filtroAniversariante ||
{(search || filtroEspecialidade !== "Todos" || filtroAniversariante || // filtroVIP removido
filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && ( filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
<div className="alert alert-info mb-3 filters-active"> <div className="alert alert-info mb-3 filters-active">
<strong>Filtros ativos:</strong> <strong>Filtros ativos:</strong>
<div className="mt-1"> <div className="mt-1">
{search && <span className="badge bg-primary me-2">Busca: "{search}"</span>} {search && <span className="badge bg-primary me-2">Busca: "{search}"</span>}
{filtroEspecialidade !== "Todos" && <span className="badge bg-primary me-2">Especialidade: {filtroEspecialidade}</span>} {filtroEspecialidade !== "Todos" && <span className="badge bg-primary me-2">Especialidade: {filtroEspecialidade}</span>}
{filtroAniversariante && <span className="badge bg-primary me-2">Aniversariantes</span>} {filtroAniversariante && <span className="badge bg-primary me-2">Aniversariantes</span>}
{filtroCidade && <span className="badge bg-primary me-2">Cidade: {filtroCidade}</span>} {filtroCidade && <span className="badge bg-primary me-2">Cidade: {filtroCidade}</span>}
{filtroEstado && <span className="badge bg-primary me-2">Estado: {filtroEstado}</span>} {filtroEstado && <span className="badge bg-primary me-2">Estado: {filtroEstado}</span>}
@ -340,14 +375,6 @@ function TableDoctor() {
</div> </div>
)} )}
<div className="mb-3">
<span className="badge results-badge">
{medicosFiltrados.length} de {medicos.length} médicos encontrados
</span>
</div>
<div className="table-responsive"> <div className="table-responsive">
<table className="table table-striped table-hover table-doctor-table"> <table className="table table-striped table-hover table-doctor-table">
<thead> <thead>
@ -360,8 +387,8 @@ function TableDoctor() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{medicosFiltrados.length > 0 ? ( {medicosPaginados.length > 0 ? (
medicosFiltrados.map((medico) => ( medicosPaginados.map((medico) => (
<tr key={medico.id}> <tr key={medico.id}>
<td> <td>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
@ -371,7 +398,6 @@ function TableDoctor() {
<i className="bi bi-gift"></i> <i className="bi bi-gift"></i>
</span> </span>
)} )}
</div> </div>
</td> </td>
<td>{medico.cpf}</td> <td>{medico.cpf}</td>
@ -410,13 +436,75 @@ function TableDoctor() {
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan="5" className="empty-state"> <td colSpan="5" className="text-center py-4">
Nenhum médico encontrado. <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> </td>
</tr> </tr>
)} )}
</tbody> </tbody>
</table> </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> </div>
</div> </div>

View File

@ -1,38 +1,186 @@
// src/pages/ProfilePage.jsx import React, { useState, useEffect, useCallback } from "react";
import React, { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation, useNavigate } from "react-router-dom";
import "./style/ProfilePage.css"; import "./style/ProfilePage.css";
const simulatedUserData = {
email: "admin@squad23.com", const MOCK_API_BASE_URL = "https://mock.apidog.com/m1/1053378-0-default";
role: "Administrador",
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 ProfilePage = () => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const getRoleFromPath = () => { const getRoleFromPath = useCallback(() => {
const path = location.pathname; const path = location.pathname;
if (path.includes("/admin")) return "Administrador"; if (path.includes("/admin")) return ROLES.ADMIN;
if (path.includes("/secretaria")) return "Secretária"; if (path.includes("/secretaria")) return ROLES.SECRETARY;
if (path.includes("/medico")) return "Médico"; if (path.includes("/medico")) return ROLES.DOCTOR;
if (path.includes("/financeiro")) return "Financeiro"; if (path.includes("/financeiro")) return ROLES.FINANCIAL;
return "Usuário Padrão"; return "Usuário";
}; }, [location.pathname]);
const userRole = simulatedUserData.role || getRoleFromPath(); const userRole = getRoleFromPath();
const userEmail = simulatedUserData.email || "email.nao.encontrado@example.com";
const [userName, setUserName] = useState("Admin Padrão"); 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 [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 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 ( return (
<div className="profile-overlay" role="dialog" aria-modal="true"> <div className="profile-overlay" role="dialog" aria-modal="true">
<div className="profile-modal"> <div className="profile-modal">
@ -47,54 +195,108 @@ const ProfilePage = () => {
<div className="profile-content"> <div className="profile-content">
<div className="profile-left"> <div className="profile-left">
<div className="avatar-wrapper"> <div className="avatar-wrapper">
<div className="avatar-square" /> <div className="avatar-square">
<button {avatarUrl ? (
className="avatar-edit-btn" <img
title="Editar foto" src={avatarUrl}
aria-label="Editar foto" alt="Avatar do usuário"
type="button" 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"
> >
{isUploading ? 'Enviando...' : 'Alterar Foto'}
</button> <input
type="file"
accept="image/*"
onChange={handleAvatarUpload}
disabled={isUploading}
style={{ display: "none" }}
/>
</label>
{isUploading && (
<p className="upload-status">
Processando imagem...
</p>
)}
</div> </div>
</div> </div>
<div className="profile-right"> <div className="profile-right">
<div className="profile-name-row"> <div className="profile-name-row">
{isEditingName ? ( {isEditingName ? (
<input <div className="name-edit-wrapper">
className="profile-name-input" <input
value={userName} className="profile-name-input"
onChange={(e) => setUserName(e.target.value)} value={userName}
onBlur={() => setIsEditingName(false)} onChange={(e) => setUserName(e.target.value)}
onKeyDown={handleNameKeyDown} onBlur={handleNameSave}
autoFocus 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 <button
className="profile-edit-inline" className="profile-edit-inline"
onClick={() => setIsEditingName(!isEditingName)} onClick={() => setIsEditingName(!isEditingName)}
aria-label="Editar nome"
type="button" type="button"
aria-label={isEditingName ? 'Cancelar edição' : 'Editar nome'}
> >
{isEditingName ? 'Cancelar' : 'Editar'}
</button> </button>
</div> </div>
<p className="profile-email"> {error && (
Email: <strong>{userEmail}</strong> <div className="error-message">
</p> {error}
</div>
)}
<p className="profile-role"> <div className="profile-info">
Cargo: <strong>{userRole}</strong> <p className="profile-email">
</p> <span>Email:</span>
<strong>{userEmail}</strong>
</p>
<div className="profile-actions-row"> <p className="profile-role">
<button className="btn btn-close" onClick={handleClose}> <span>Cargo:</span>
Fechar <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> </button>
</div> </div>
</div> </div>
@ -104,4 +306,4 @@ const ProfilePage = () => {
); );
}; };
export default ProfilePage; export default ProfilePage;

View File

@ -8,7 +8,7 @@ import manager from "../components/utils/fetchErros/ManagerFunction";
function TablePaciente({ setCurrentPage, setPatientID }) { function TablePaciente({ setCurrentPage, setPatientID }) {
const { getAuthorizationHeader, isAuthenticated, RefreshingToken } = useAuth(); const { getAuthorizationHeader, isAuthenticated } = useAuth();
const [pacientes, setPacientes] = useState([]); const [pacientes, setPacientes] = useState([]);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@ -23,6 +23,10 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
const [dataInicial, setDataInicial] = useState(""); const [dataInicial, setDataInicial] = useState("");
const [dataFinal, setDataFinal] = useState(""); const [dataFinal, setDataFinal] = useState("");
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedPatientId, setSelectedPatientId] = useState(null); const [selectedPatientId, setSelectedPatientId] = useState(null);
@ -105,7 +109,15 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
} }
}; };
// Função para refresh token (adicionada)
const RefreshingToken = () => {
console.log("Refreshing token...");
// Aqui você pode adicionar a lógica de refresh do token se necessário
// Por enquanto é apenas um placeholder para evitar o erro
};
useEffect(() => { useEffect(() => {
const authHeader = getAuthorizationHeader() const authHeader = getAuthorizationHeader()
console.log(authHeader, 'aqui autorização') console.log(authHeader, 'aqui autorização')
@ -122,7 +134,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions) fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions)
.then(response => { .then(response => {
// 1. VERIFICAÇÃO DO STATUS HTTP (Se não for 2xx)
if (!response.ok) { if (!response.ok) {
return response.json().then(errorData => { return response.json().then(errorData => {
@ -141,7 +153,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
} }
// 3. Se a resposta for OK (2xx), processamos o JSON normalmente
return response.json(); return response.json();
}) })
.then(result => { .then(result => {
@ -193,6 +205,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
setIdadeMaxima(""); setIdadeMaxima("");
setDataInicial(""); setDataInicial("");
setDataFinal(""); setDataFinal("");
setPaginaAtual(1);
}; };
const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => { const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => {
@ -236,13 +249,53 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
return resultado; 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(() => { useEffect(() => {
console.log(` Pacientes totais: ${pacientes?.length}, Filtrados: ${pacientesFiltrados?.length}`); setPaginaAtual(1);
}, [pacientes, pacientesFiltrados, search]); }, [search, filtroConvenio, filtroVIP, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal]);
return ( return (
<> <>
<ModalErro showModal={showModalError} setShowModal={setShowModalError} ErrorData={ErrorInfo}/>
<div className="page-heading"> <div className="page-heading">
<h3>Lista de Pacientes</h3> <h3>Lista de Pacientes</h3>
</div> </div>
@ -293,7 +346,8 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</select> </select>
<button <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" }} style={{ padding: "0.25rem 0.5rem" }}
> >
<i className="bi bi-award me-1"></i> VIP <i className="bi bi-award me-1"></i> VIP
@ -398,6 +452,12 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</div> </div>
</div> </div>
)} )}
<div className="mt-3">
<div className="contador-pacientes">
{pacientesFiltrados.length} DE {pacientes.length} PACIENTES ENCONTRADOS
</div>
</div>
</div> </div>
{(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante || {(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante ||
@ -419,12 +479,6 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</div> </div>
)} )}
<div className="mb-3">
<span className="badge results-badge">
{pacientesFiltrados?.length} de {pacientes?.length} pacientes encontrados
</span>
</div>
<div className="table-responsive"> <div className="table-responsive">
<table className="table table-striped table-hover table-paciente-table"> <table className="table table-striped table-hover table-paciente-table">
<thead> <thead>
@ -437,8 +491,8 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{pacientesFiltrados.length > 0 ? ( {pacientesPaginados.length > 0 ? (
pacientesFiltrados.map((paciente) => ( pacientesPaginados.map((paciente) => (
<tr key={paciente.id}> <tr key={paciente.id}>
<td> <td>
<div className="d-flex align-items-center patient-name-container"> <div className="d-flex align-items-center patient-name-container">
@ -493,13 +547,75 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan="5" className="empty-state"> <td colSpan="5" className="text-center py-4">
Nenhum paciente encontrado. <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> </td>
</tr> </tr>
)} )}
</tbody> </tbody>
</table> </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> </div>
</div> </div>

View File

@ -41,7 +41,7 @@
} }
.modal-header-success { .modal-header-success {
background-color: #28a745 !important; background-color: #1e3a8a !important;
} }
.modal-header-error { .modal-header-error {
@ -104,12 +104,12 @@
} }
.modal-button-success { .modal-button-success {
background-color: #28a745; background-color: #1e3a8a;
color: #fff; color: #fff;
} }
.modal-button-success:hover { .modal-button-success:hover {
background-color: #218838; background-color: #1e3a8a;
} }
.modal-button-error { .modal-button-error {
@ -186,10 +186,8 @@
outline: 2px solid #0056b3; outline: 2px solid #0056b3;
outline-offset: 2px; outline-offset: 2px;
} }
/* Garantir que as cores dos cabeçalhos sejam aplicadas */
.modal-overlay .modal-container .modal-header.modal-header-success { .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 { .modal-overlay .modal-container .modal-header.modal-header-error {
@ -258,7 +256,7 @@
} }
.modal-header-success { .modal-header-success {
background-color: #006400 !important; background-color: #1e3a8a !important;
} }
.modal-header-error { .modal-header-error {

View File

@ -91,7 +91,7 @@
} }
.modal-header.success { .modal-header.success {
background-color: #28a745 !important; background-color: #1e3a8a !important;
} }
.modal-header.error { .modal-header.error {
@ -168,11 +168,11 @@
} }
.modal-confirm-button.success { .modal-confirm-button.success {
background-color: #28a745 !important; background-color: #1e3a8a !important;
} }
.modal-confirm-button.success:hover { .modal-confirm-button.success:hover {
background-color: #218838 !important; background-color: #1e3a8a !important;
} }
.modal-confirm-button.error { .modal-confirm-button.error {

View File

@ -1,6 +1,6 @@
/* src/pages/ProfilePage.css */ /* src/pages/ProfilePage.css */
/* Overlay que cobre toda a tela */ /* Overlay */
.profile-overlay { .profile-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
@ -8,171 +8,318 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 20000; /* acima de header, vlibras, botões de acessibilidade */ z-index: 20000;
padding: 20px; padding: 20px;
box-sizing: border-box;
} }
/* Card central (estilo modal amplo parecido com a 4ª foto) */ /* Modal */
.profile-modal { .profile-modal {
background: #ffffff; background: #ffffff;
border-radius: 10px; border-radius: 12px;
padding: 18px; padding: 20px;
width: min(1100px, 96%); width: min(600px, 96%);
max-width: 1100px; max-width: 600px;
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.5); box-shadow: 0 18px 60px rgba(0, 0, 0, 0.5);
position: relative; position: relative;
box-sizing: border-box;
overflow: visible;
} }
/* Botão fechar (X) no canto do card */ /* Botão fechar */
.profile-close { .profile-close {
position: absolute; position: absolute;
top: 14px; top: 15px;
right: 14px; right: 15px;
background: none; background: none;
border: none; border: none;
font-size: 26px; font-size: 24px;
color: #666; color: #666;
cursor: pointer; 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 { .profile-content {
display: flex; display: flex;
gap: 28px; gap: 30px;
align-items: flex-start; align-items: flex-start;
padding: 22px 18px; padding: 20px 10px;
} }
/* Coluna esquerda - avatar */ /* Avatar */
.profile-left { .profile-left {
width: 220px; width: 160px;
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
/* Avatar quadrado com sombra (estilo da foto 4) */
.avatar-wrapper { .avatar-wrapper {
position: relative; position: relative;
width: 180px; width: 140px;
height: 180px; height: 140px;
} }
.avatar-square { .avatar-square {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 8px; border-radius: 8px;
background-color: #d0d0d0; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
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>"); overflow: hidden;
background-position: center; display: flex;
background-repeat: no-repeat; align-items: center;
background-size: 55%; justify-content: center;
box-shadow: 0 8px 24px rgba(0,0,0,0.25); 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 { .avatar-edit-btn {
position: absolute; position: absolute;
right: -8px; right: -8px;
bottom: -8px; bottom: -8px;
transform: translate(0, 0);
border: none; border: none;
background: #ffffff; background: #ffffff;
padding: 8px 9px; padding: 8px;
border-radius: 50%; 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; cursor: pointer;
font-size: 0.95rem; font-size: 0.9rem;
line-height: 1; 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 { .profile-right {
flex: 1; flex: 1;
min-width: 280px; min-width: 250px;
display: flex;
flex-direction: column;
justify-content: center;
} }
/* Nome e botão de editar inline */
.profile-name-row { .profile-name-row {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
margin-bottom: 10px; margin-bottom: 15px;
} }
.profile-username { .profile-username {
margin: 0; margin: 0;
font-size: 1.9rem; font-size: 1.8rem;
color: #222; color: #222;
font-weight: 600;
} }
.profile-edit-inline { .profile-edit-inline {
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
font-size: 1.05rem; font-size: 1rem;
color: #444; 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 { .profile-name-input {
font-size: 1.6rem; font-size: 1.6rem;
padding: 6px 8px; padding: 5px 8px;
border: 1px solid #e0e0e0; border: 2px solid #007bff;
border-radius: 6px; 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-email,
.profile-role { .profile-role {
margin: 6px 0; margin: 8px 0;
color: #555; color: #555;
font-size: 1rem; 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; display: flex;
gap: 12px; gap: 8px;
margin-top: 18px; }
.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 { .btn {
padding: 8px 14px; padding: 10px 20px;
border-radius: 8px; border-radius: 6px;
border: 1px solid transparent; border: none;
cursor: pointer; cursor: pointer;
font-size: 0.95rem; font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
} }
.btn-close { .btn-close {
background: #f0f0f0; background: #f0f0f0;
color: #222; color: #222;
border: 1px solid #e6e6e6;
} }
/* responsividade */ .btn-close:hover {
@media (max-width: 880px) { background: #e0e0e0;
}
.btn-clear {
background: #dc3545;
color: white;
}
.btn-clear:hover {
background: #c82333;
}
/* Responsividade */
@media (max-width: 680px) {
.profile-content { .profile-content {
flex-direction: column; flex-direction: column;
gap: 14px; gap: 20px;
align-items: center; align-items: center;
text-align: center;
}
.profile-left {
width: 100%;
}
.avatar-wrapper {
margin: 0 auto;
}
.profile-email,
.profile-role {
justify-content: center;
}
.profile-actions {
justify-content: center;
} }
.profile-left { width: 100%; }
.avatar-wrapper { width: 140px; height: 140px; }
.profile-right { width: 100%; text-align: center; }
} }
@media (max-width: 480px) {
.profile-modal {
padding: 15px;
}
.profile-content {
padding: 10px 5px;
}
.profile-username {
font-size: 1.5rem;
}
.avatar-wrapper {
width: 120px;
height: 120px;
}
}.avatar-edit-btn {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background-color 0.2s;
}
.avatar-edit-btn:hover {
background: #0056b3;
}
.avatar-edit-btn.uploading {
background: #6c757d;
cursor: not-allowed;
}
.profile-edit-inline {
background: #f8f9fa;
border: 1px solid #dee2e6;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
margin-left: 10px;
}
.profile-edit-inline:hover {
background: #e9ecef;
}

View File

@ -1,4 +1,3 @@
.table-doctor-container { .table-doctor-container {
line-height: 2.5; line-height: 2.5;
} }
@ -49,7 +48,7 @@
background-color: rgba(0, 0, 0, 0.025); background-color: rgba(0, 0, 0, 0.025);
} }
/* Badges */
.specialty-badge { .specialty-badge {
background-color: #1e3a8a !important; background-color: #1e3a8a !important;
color: white !important; color: white !important;
@ -58,8 +57,6 @@
font-weight: 500; font-weight: 500;
} }
.results-badge { .results-badge {
background-color: #1e3a8a; background-color: #1e3a8a;
color: white; color: white;
@ -75,7 +72,6 @@
font-size: 0.75em; font-size: 0.75em;
} }
.btn-view { .btn-view {
background-color: #E6F2FF !important; background-color: #E6F2FF !important;
color: #004085 !important; color: #004085 !important;
@ -115,7 +111,6 @@
border-color: #ED969E; border-color: #ED969E;
} }
.advanced-filters { .advanced-filters {
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
border-radius: 0.375rem; border-radius: 0.375rem;
@ -132,7 +127,6 @@
font-size: 0.875rem; font-size: 0.875rem;
} }
.delete-modal .modal-header { .delete-modal .modal-header {
background-color: rgba(220, 53, 69, 0.1); background-color: rgba(220, 53, 69, 0.1);
border-bottom: 1px solid rgba(220, 53, 69, 0.2); border-bottom: 1px solid rgba(220, 53, 69, 0.2);
@ -143,7 +137,6 @@
font-weight: 600; font-weight: 600;
} }
.filter-especialidade { .filter-especialidade {
min-width: 180px !important; min-width: 180px !important;
max-width: 200px; max-width: 200px;
@ -160,7 +153,6 @@
padding: 0.375rem 0.75rem; padding: 0.375rem 0.75rem;
} }
.filtros-basicos { .filtros-basicos {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -168,7 +160,6 @@
gap: 0.75rem; gap: 0.75rem;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.table-doctor-table { .table-doctor-table {
font-size: 0.875rem; font-size: 0.875rem;
@ -207,7 +198,6 @@
} }
} }
.empty-state { .empty-state {
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
@ -224,7 +214,6 @@
padding: 0.4em 0.65em; padding: 0.4em 0.65em;
} }
.table-doctor-table tbody tr { .table-doctor-table tbody tr {
transition: background-color 0.15s ease-in-out; transition: background-color 0.15s ease-in-out;
} }
@ -233,4 +222,116 @@
.btn-edit, .btn-edit,
.btn-delete { .btn-delete {
transition: all 0.15s ease-in-out; 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;
} }

View File

@ -1,4 +1,3 @@
.table-paciente-container { .table-paciente-container {
line-height: 2.5; line-height: 2.5;
} }
@ -49,7 +48,6 @@
background-color: rgba(0, 0, 0, 0.025); background-color: rgba(0, 0, 0, 0.025);
} }
.insurance-badge { .insurance-badge {
background-color: #6c757d !important; background-color: #6c757d !important;
color: white !important; color: white !important;
@ -81,7 +79,6 @@
font-size: 0.75em; font-size: 0.75em;
} }
.btn-view { .btn-view {
background-color: #E6F2FF !important; background-color: #E6F2FF !important;
color: #004085 !important; color: #004085 !important;
@ -121,7 +118,6 @@
border-color: #ED969E; border-color: #ED969E;
} }
.advanced-filters { .advanced-filters {
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
border-radius: 0.375rem; border-radius: 0.375rem;
@ -148,7 +144,6 @@
font-weight: 600; font-weight: 600;
} }
.empty-state { .empty-state {
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
@ -165,7 +160,6 @@
padding: 0.4em 0.65em; padding: 0.4em 0.65em;
} }
.table-paciente-table tbody tr { .table-paciente-table tbody tr {
transition: background-color 0.15s ease-in-out; transition: background-color 0.15s ease-in-out;
} }
@ -176,7 +170,6 @@
transition: all 0.15s ease-in-out; transition: all 0.15s ease-in-out;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.table-paciente-table { .table-paciente-table {
font-size: 0.875rem; font-size: 0.875rem;
@ -213,6 +206,7 @@
margin-left: 0 !important; margin-left: 0 !important;
} }
} }
.compact-select { .compact-select {
font-size: 1.0rem; font-size: 1.0rem;
padding: 0.45rem 0.5rem; padding: 0.45rem 0.5rem;
@ -227,8 +221,120 @@
white-space: nowrap; white-space: nowrap;
} }
.table-paciente-filters .d-flex { .table-paciente-filters .d-flex {
align-items: center; align-items: center;
gap: 8px; 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;
} }