This commit is contained in:
Eduarda-SS 2025-11-04 16:40:42 -03:00
commit a709dffde8
58 changed files with 7489 additions and 3983 deletions

2
.env Normal file
View File

@ -0,0 +1,2 @@
OPENAI_API_KEY=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
PORT=5000

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

View File

@ -1,56 +0,0 @@
3993097 (HEAD -> main) Merge branch 'main' of https://git.popcode.com.br/RiseUP/riseup-squad23
63659b6 Verificação do cpf e colocar o erro 404
ecae83c (riseup/main, riseup/HEAD, origin/main, origin/HEAD) Merge pull request 'Conectando-o-resto-das-API' (#2) from Conectando-o-resto-das-API into main
908d545 (riseup/Conectando-o-resto-das-API, origin/Conectando-o-resto-das-API) feat: adicionar upload e delete de anexos do paciente
4b404c0 Merge branch 'main' of https://git.popcode.com.br/RiseUP/riseup-squad23
bd20c2d Merge remote-tracking branch 'origin/main'
8aeabd1 (riseup/Fix-dos-erros-do-projeto, origin/Fix-dos-erros-do-projeto) FIx: todos os erros que aparecia no console foram resolvidos
0e29e7d melhorias na organização de pastas
98f076a Mergin com TableMelhorias
589d590 Mergin com novas alterações de laudo
7b28e2a Details melhorias
9480edc (riseup/PaginaDetalhes, origin/PaginaDetalhes) Pàgina detalhes
e4515cf Adição das cores nos cards de consulta
d3dd2fd (riseup/TableMelhorias, origin/TableMelhorias) Detalhe nas tabelas
a54b119 Delete Anexos apos pacientes forem excluidos
6e93cb5 atualizar paciente
b9a35be começo do concerto do editar
82469bc Details funcional
cdfe4ea Validação de CPF
57c8f67 (riseup/DetalhesMedico, origin/DetalhesMedico) Detalhes do medico
b021444 Mudanças formularios e detalhes
d5d03b0 (riseup/mudanças-de-laudo, origin/mudanças-de-laudo) atualização do laudo
a502bbd agendamentos no incio
8e1fcd9 Merge branch 'feature/novo-cadastro-paciente'
bea9076 Merge remote-tracking branch 'origin/PaginaDetalhes'
e35f217 mergin branch inicio com main
1af8268 Atualizacão do laudo
725d60d feat: ajeitei o nome
bab85ff (riseup/AgendamentoSidebar, origin/AgendamentoSidebar) Concertar Agendamento
b2707e3 Refatora o estilo do formulário do paciente para uma aparência de cartão com tipografia maior
37e8959 Refatora o estilo do formulário do paciente para uma aparência de cartão com tipografia maior
0930385 feat: uma piquena mudança
f6a19c4 feat: Adiciona formulário de cadastro de paciente
d91b5cf form de agendar consulta melhorado
0a60dd7 Tabela semana e mes
7f07950 (riseup/feature-Melhoria-no-Dashboard, origin/feature-Melhoria-no-Dashboard) feat: Criação da página início e melhoria na navegação
39e25ad Pagina de detalhes atualizada
4f84791 pequenas mudanaças na tabela de semana e mes
6737955 form para nova consulta e tabelas de horario
26ded17 Nova pagina de detalhes
874de84 Inicio do agendamento
f3e7470 (riseup/gerenciamento-de-laudo, origin/gerenciamento-de-laudo) Laudo do Paciente
709cd4e Merge finalizado
d6b3e86 Merge detalhes-do-pacientes para main
08ffa55 Merge remote-tracking branch 'origin/CrudMedico'
70c4d5f Termino da organização
edd567d Inicio da organização
9c09113 Mudanças pos feedback de davi
aa3a5fa Criação da página dos detalhes dos pacientes
5534568 Inicio de detalhes e atualização do paciente
06ff7d5 Funcionalidade de delete e botão de opções
5b63fa2 Mascara telefones
fb9d783 adição da mascara do CPF
a489d84 metodo GET e POST
4eaabbd first commit
a244691 Initial commit

5592
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
"dependencies": { "dependencies": {
"@ckeditor/ckeditor5-build-classic": "^41.4.2", "@ckeditor/ckeditor5-build-classic": "^41.4.2",
"@ckeditor/ckeditor5-react": "^11.0.0", "@ckeditor/ckeditor5-react": "^11.0.0",
"@jitsi/react-sdk": "^1.4.0",
"@sweetalert2/theme-dark": "^5.0.27", "@sweetalert2/theme-dark": "^5.0.27",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0", "@testing-library/jest-dom": "^6.8.0",
@ -18,10 +19,15 @@
"apexcharts": "^5.3.4", "apexcharts": "^5.3.4",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"cors": "^2.8.5",
"dayjs": "^1.11.18", "dayjs": "^1.11.18",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"flatpickr": "^4.6.13", "flatpickr": "^4.6.13",
"html2pdf.js": "^0.12.1", "html2pdf.js": "^0.12.1",
"lucide-react": "^0.543.0", "lucide-react": "^0.543.0",
"node-fetch": "^3.3.2",
"openai": "^6.7.0",
"perfect-scrollbar": "^1.5.6", "perfect-scrollbar": "^1.5.6",
"powershell": "^2.3.3", "powershell": "^2.3.3",
"quill": "^2.0.3", "quill": "^2.0.3",
@ -33,6 +39,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",
@ -72,5 +79,8 @@
"sass": "^1.91.0", "sass": "^1.91.0",
"sass-loader": "^16.0.5", "sass-loader": "^16.0.5",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13"
},
"overrides": {
"react": "$react"
} }
} }

38
server.js Normal file
View File

@ -0,0 +1,38 @@
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import OpenAI from "openai";
dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // Coloque sua chave no .env
});
app.post("/api/chat", async (req, res) => {
try {
const { message } = req.body;
const completion = await client.chat.completions.create({
model: "gpt-4o-mini", // modelo rápido e leve
messages: [
{
role: "system",
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 },
],
});
const resposta = completion.choices[0].message.content;
res.json({ resposta });
} catch (error) {
console.error("Erro no servidor:", error);
res.status(500).json({ erro: "Erro ao conectar com a IA" });
}
});
app.listen(5000, () => console.log("Servidor rodando na porta 5000"));

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,56 +7,96 @@ 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 [showModal, setShowModal] = useState(false); const [relatoriosFinais, setRelatoriosFinais] = useState([]);
const [index, setIndex] = useState(); const [pacientesData, setPacientesData] = useState({});
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [showModal, setShowModal] = useState(false);
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);
// busca lista de relatórios
useEffect(() => { useEffect(() => {
let mounted = true;
const fetchReports = async () => { const fetchReports = async () => {
try { try {
var myHeaders = new Headers(); var 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 = { method: 'GET', headers: myHeaders, redirect: 'follow' }; var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions); const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions);
const data = await res.json(); const data = await res.json();
setRelatorios(data || []);
const uniqueMap = new Map();
(Array.isArray(data) ? data : []).forEach(r => {
if (r && r.id) uniqueMap.set(r.id, r);
});
const unique = Array.from(uniqueMap.values())
.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
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);
setRelatorios([]); if (mounted) {
setRelatoriosOriginais([]);
setRelatoriosFiltrados([]);
setRelatoriosFinais([]);
}
} }
}; };
fetchReports(); fetchReports();
const refreshHandler = () => fetchReports();
window.addEventListener('reports:refresh', refreshHandler);
return () => {
mounted = false;
window.removeEventListener('reports:refresh', refreshHandler);
};
}, [authHeader]); }, [authHeader]);
// depois que RelatoriosFiltrados mudar, busca pacientes e médicos correspondentes
useEffect(() => { useEffect(() => {
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];
// paciente
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);
} catch (err) { } catch (err) {
pacientes.push(null); pacientes.push(null);
} }
// médico: tenta created_by ou requested_by id se existir
try { try {
const doctorId = rel.created_by || rel.requested_by || null; const doctorId = rel.created_by || rel.requested_by || null;
if (doctorId) { if (doctorId) {
// se created_by é id (uuid) usamos GetDoctorByID, senão se requested_by for nome, guardamos nome
const docRes = await GetDoctorByID(doctorId, authHeader); const docRes = await GetDoctorByID(doctorId, authHeader);
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes); medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
} else { } else {
@ -70,55 +109,117 @@ 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 BaixarPDFdoRelatorio = (nome_paciente) => { const abrirModal = (relatorio, index) => {
const elemento = document.getElementById("folhaA4"); setRelatorioModal(relatorio);
const opt = { margin: 0, filename: `relatorio_${nome_paciente || "paciente"}.pdf`, html2canvas: { scale: 2 }, jsPDF: { unit: "mm", format: "a4", orientation: "portrait" } }; setModalIndex(index);
setShowModal(true);
};
// Função para limpar filtros
const limparFiltros = () => {
setTermoPesquisa('');
setFiltroExame('');
setRelatoriosFinais(relatoriosOriginais);
};
const BaixarPDFdoRelatorio = (nome_paciente, idx) => {
const elemento = document.getElementById(`folhaA4-${idx}`);
if (!elemento) {
console.error('Elemento para gerar PDF não encontrado:', `folhaA4-${idx}`);
return;
}
const opt = {
margin: 0,
filename: `relatorio_${nome_paciente || "paciente"}.pdf`,
html2canvas: { scale: 2 },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" }
};
html2pdf().set(opt).from(elemento).save(); 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"> <div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowModal(false)}>
<div className="modal-dialog modal-tabela-relatorio"> <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 text-white"> <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" 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"> <div id={`folhaA4-${modalIndex}`} className="folhaA4">
<div id='header-relatorio'> <div id='header-relatorio' style={{ textAlign: 'center', marginBottom: 24 }}>
<p>Clinica Rise up</p> <p style={{ margin: 0 }}>Clinica Rise up</p>
<p>Dr - CRM/SP 123456</p> <p style={{ margin: 0 }}>Dr - CRM/SP 123456</p>
<p>Avenida - (79) 9 4444-4444</p> <p style={{ margin: 0 }}>Avenida - (79) 9 4444-4444</p>
</div> </div>
<div id='infoPaciente'> <div id='infoPaciente' style={{ padding: '0 6px' }}>
<p>Paciente: {PacientesComRelatorios[index]?.full_name}</p> <p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name}</p>
<p>Data de nascimento: {PacientesComRelatorios[index]?.birth_date}</p> <p><strong>Data de nascimento:</strong> {pacientesComRelatorios[modalIndex]?.birth_date || '—'}</p>
<p>Data do exame: {RelatoriosFiltrados[index]?.due_at || ''}</p> <p><strong>Data do exame:</strong> {relatoriosFiltrados[modalIndex]?.due_at || '—'}</p>
{/* Exibe conteúdo salvo (content_html) */}
<p style={{ marginTop: '15px', fontWeight: 'bold' }}>Conteúdo do Relatório:</p> <p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
<TiptapViewer htmlContent={RelatoriosFiltrados[index]?.content_html || RelatoriosFiltrados[index]?.content || 'Relatório não preenchido.'} /> <div className="tiptap-viewer-wrapper">
<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' }}>
<p>Dr {MedicosComRelatorios[index]?.full_name || RelatoriosFiltrados[index]?.requested_by}</p> <p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFiltrados[modalIndex]?.requested_by}</p>
<p>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">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name)}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button> <div className="modal-footer custom-modal-footer">
<button type="button" className="btn btn-primary" onClick={() => { setShowModal(false) }}>Fechar</button> <button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name, modalIndex)}>
<i className='bi bi-file-pdf-fill'></i> baixar em pdf
</button>
<button type="button" className="btn btn-outline-secondary" onClick={() => { setShowModal(false) }}>
Fechar
</button>
</div> </div>
</div> </div>
</div> </div>
@ -133,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>
@ -149,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) => {
const paciente = pacientesData[relatorio.patient_id];
return (
<tr key={relatorio.id}> <tr key={relatorio.id}>
<td className='infos-paciente'>{PacientesComRelatorios[idx]?.full_name}</td> <td>{paciente?.full_name || 'Carregando...'}</td>
<td className='infos-paciente'>{MedicosComRelatorios[idx]?.full_name || relatorio.requested_by || '-'}</td> <td>{paciente?.cpf || 'Carregando...'}</td>
<td>{relatorio.exam}</td>
<td> <td>
<div className="d-flex gap-2"> <div className="d-flex gap-2">
<button className="btn btn-sm" style={{ backgroundColor: "#E6F2FF", color: "#004085" }} onClick={() => { setShowModal(true); setIndex(idx); }}> <button className="btn btn-sm btn-ver-detalhes" onClick={() => abrirModal(relatorio, index)}>
<i className="bi bi-eye me-1"></i> Ver Detalhes <i className="bi bi-eye me-1"></i> Ver Detalhes
</button> </button>
<button className="btn btn-sm btn-editar" onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<button className="btn btn-sm" style={{ backgroundColor: "#FFF3CD", color: "#856404" }} onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<i className="bi bi-pencil me-1"></i> Editar <i className="bi bi-pencil me-1"></i> Editar
</button> </button>
</div> </div>
</td> </td>
</tr> </tr>
)) );
})
) : ( ) : (
<tr><td colSpan="8" 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>
{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>
<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>

View File

@ -1,4 +1,4 @@
// EditPageRelatorio.jsx // src/PagesMedico/EditPageRelatorio.jsx
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys'; import API_KEY from '../components/utils/apiKeys';
@ -52,7 +52,7 @@ const EditPageRelatorio = () => {
try { try {
const 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);
const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' }; const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
// Pega relatório por id (supabase geralmente retorna array para ?id=eq.X) // Pega relatório por id (supabase geralmente retorna array para ?id=eq.X)
@ -101,12 +101,14 @@ const EditPageRelatorio = () => {
try { try {
const 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);
myHeaders.append('Content-Type', 'application/json'); myHeaders.append('Content-Type', 'application/json');
myHeaders.append('Accept', 'application/json');
// pedir que o Supabase retorne a representação do registro atualizado (opcional)
myHeaders.append('Prefer', 'return=representation');
const body = JSON.stringify({ content_html: html }); const body = JSON.stringify({ content_html: html });
// supabase: PATCH via query id=eq.<id>
const res = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${params.id}`, { const res = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${params.id}`, {
method: 'PATCH', method: 'PATCH',
headers: myHeaders, headers: myHeaders,
@ -114,13 +116,29 @@ const EditPageRelatorio = () => {
}); });
if (!res.ok) { if (!res.ok) {
const txt = await res.text(); let txt;
try { txt = await res.text(); } catch (e) { txt = 'erro lendo resposta'; }
console.error('Erro PATCH', res.status, txt); console.error('Erro PATCH', res.status, txt);
throw new Error('Erro na API'); throw new Error('Erro na API');
} }
// Recebe o dado atualizado e atualiza o estado do componente
let updatedData;
try {
updatedData = await res.json();
} catch (e) {
updatedData = null;
}
const updatedReport = Array.isArray(updatedData) ? updatedData[0] : updatedData;
if (updatedReport) {
setReport(updatedReport);
setHtml(updatedReport.content_html || '');
}
alert('Relatório atualizado com sucesso!'); alert('Relatório atualizado com sucesso!');
navigate('/medico/relatorios'); navigate('/medico/relatorios');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
alert('Erro ao salvar. Veja console.'); alert('Erro ao salvar. Veja console.');
@ -150,4 +168,3 @@ const EditPageRelatorio = () => {
}; };
export default EditPageRelatorio; export default EditPageRelatorio;

View File

@ -1,3 +1,4 @@
// src/PagesMedico/FormNovoRelatorio.jsx
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys'; import API_KEY from '../components/utils/apiKeys';
@ -39,7 +40,7 @@ const FormNovoRelatorio = () => {
const doctorRef = useRef(); const doctorRef = useRef();
useEffect(() => { useEffect(() => {
// carregar pacientes // carregar pacientes e médicos
let mounted = true; let mounted = true;
const loadPatients = async () => { const loadPatients = async () => {
setLoadingPatients(true); setLoadingPatients(true);
@ -109,7 +110,7 @@ const FormNovoRelatorio = () => {
patient_id: patient.id, patient_id: patient.id,
patient_name: patient.full_name || '', patient_name: patient.full_name || '',
patient_birth: patient.birth_date || '', patient_birth: patient.birth_date || '',
contentHtml: generateTemplate(patient.full_name || '', patient.birth_date || '', form.doctor_name) contentHtml: generateTemplate(patient.full_name || '', patient.birth_date || '', prev.doctor_name)
})); }));
setPatientQuery(''); setPatientQuery('');
setShowPatientDropdown(false); setShowPatientDropdown(false);
@ -120,7 +121,7 @@ const FormNovoRelatorio = () => {
...prev, ...prev,
doctor_id: doctor.id, doctor_id: doctor.id,
doctor_name: doctor.full_name || '', doctor_name: doctor.full_name || '',
contentHtml: generateTemplate(form.patient_name, form.patient_birth, doctor.full_name || '') contentHtml: generateTemplate(prev.patient_name, prev.patient_birth, doctor.full_name || '')
})); }));
setDoctorQuery(''); setDoctorQuery('');
setShowDoctorDropdown(false); setShowDoctorDropdown(false);
@ -137,7 +138,7 @@ const FormNovoRelatorio = () => {
const handleEditorChange = (html) => setForm(prev => ({ ...prev, contentHtml: html })); const handleEditorChange = (html) => setForm(prev => ({ ...prev, contentHtml: html }));
// salvar novo relatório // salvar novo relatório (agora com Prefer: return=representation e dispatch para refresh)
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (!form.patient_id) return alert('Selecione o paciente (clicando no item) antes de salvar.'); if (!form.patient_id) return alert('Selecione o paciente (clicando no item) antes de salvar.');
@ -146,35 +147,51 @@ const FormNovoRelatorio = () => {
try { try {
const 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);
myHeaders.append('Content-Type', 'application/json'); myHeaders.append('Content-Type', 'application/json');
myHeaders.append('Accept', 'application/json');
// pedir que o Supabase retorne a representação do registro criado
myHeaders.append('Prefer', 'return=representation');
const body = JSON.stringify({ // monta o payload apenas com campos válidos
const payload = {
patient_id: form.patient_id, patient_id: form.patient_id,
content: form.contentHtml, content: form.contentHtml,
content_html: form.contentHtml, content_html: form.contentHtml,
requested_by: form.doctor_name || '', requested_by: form.doctor_name || ''
created_by: form.doctor_id || null, };
status: 'draft' // só inclui created_by se tiver um id válido
}); if (form.doctor_id) payload.created_by = form.doctor_id;
payload.status = 'draft';
const res = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports', { const res = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports', {
method: 'POST', method: 'POST',
headers: myHeaders, headers: myHeaders,
body, body: JSON.stringify(payload),
}); });
if (!res.ok) { if (!res.ok) {
const txt = await res.text(); // tenta ler JSON, se não for JSON lê o texto
console.error('Erro POST criar relatório:', res.status, txt); let txt;
// mostra mensagem mais útil try {
return alert(`Erro ao criar relatório (ver console). Status ${res.status}`); txt = await res.json();
} catch (err) {
txt = await res.text();
} }
console.error('Erro POST criar relatório:', res.status, txt);
return alert(`Erro ao criar relatório (ver console). Status ${res.status}\nMensagem: ${JSON.stringify(txt)}`);
}
const created = await res.json();
console.log('Relatório criado:', created);
// dispara refresh global para a lista (DoctorRelatorioManager está escutando)
window.dispatchEvent(new Event('reports:refresh'));
alert('Relatório criado com sucesso!'); alert('Relatório criado com sucesso!');
navigate('/medico/relatorios'); navigate('/medico/relatorios');
} catch (err) { } catch (err) {
console.error('Erro salvar relatório:', err); console.error('Erro salvar relatório (catch):', err);
alert('Erro ao salvar relatório. Veja console.'); alert('Erro ao salvar relatório. Veja console.');
} }
}; };
@ -185,7 +202,7 @@ const FormNovoRelatorio = () => {
<form onSubmit={handleSubmit} className="card p-4 mb-4"> <form onSubmit={handleSubmit} className="card p-4 mb-4">
<div className="row g-3 align-items-end"> <div className="row g-3 align-items-end">
<div className="col-md-6" ref={patientRef}> <div className="col-md-6" ref={patientRef} style={{ position: 'relative' }}>
<label className="form-label">Buscar paciente (digite para filtrar)</label> <label className="form-label">Buscar paciente (digite para filtrar)</label>
<input <input
className="form-control" className="form-control"
@ -195,7 +212,7 @@ const FormNovoRelatorio = () => {
onFocus={() => setShowPatientDropdown(true)} onFocus={() => setShowPatientDropdown(true)}
/> />
{showPatientDropdown && patientQuery && ( {showPatientDropdown && patientQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '45%' }}> <ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}>
{filteredPatients.length > 0 ? filteredPatients.map(p => ( {filteredPatients.length > 0 ? filteredPatients.map(p => (
<li key={p.id} className="list-group-item list-group-item-action" onClick={() => choosePatient(p)}> <li key={p.id} className="list-group-item list-group-item-action" onClick={() => choosePatient(p)}>
{p.full_name} {p.cpf ? `- ${p.cpf}` : ''} {p.full_name} {p.cpf ? `- ${p.cpf}` : ''}
@ -206,7 +223,7 @@ const FormNovoRelatorio = () => {
<div className="form-text">Clique no paciente desejado para selecioná-lo e preencher o template.</div> <div className="form-text">Clique no paciente desejado para selecioná-lo e preencher o template.</div>
</div> </div>
<div className="col-md-6" ref={doctorRef}> <div className="col-md-6" ref={doctorRef} style={{ position: 'relative' }}>
<label className="form-label">Buscar médico (digite para filtrar)</label> <label className="form-label">Buscar médico (digite para filtrar)</label>
<input <input
className="form-control" className="form-control"
@ -216,7 +233,7 @@ const FormNovoRelatorio = () => {
onFocus={() => setShowDoctorDropdown(true)} onFocus={() => setShowDoctorDropdown(true)}
/> />
{showDoctorDropdown && doctorQuery && ( {showDoctorDropdown && doctorQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '45%', right: 0 }}> <ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}>
{filteredDoctors.length > 0 ? filteredDoctors.map(d => ( {filteredDoctors.length > 0 ? filteredDoctors.map(d => (
<li key={d.id} className="list-group-item list-group-item-action" onClick={() => chooseDoctor(d)}> <li key={d.id} className="list-group-item list-group-item-action" onClick={() => chooseDoctor(d)}>
{d.full_name} {d.crm ? `- CRM ${d.crm}` : ''} {d.full_name} {d.crm ? `- CRM ${d.crm}` : ''}

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,55 +1,152 @@
#folhaA4 { /* src/PagesMedico/styleMedico/FormNovoRelatorio.css */
width: 210mm;
min-height: 207mm;
padding: 20mm;
margin: 10mm auto;
border: 1px solid #ccc;
background: white;
} /* --- Modal centralizada e quadrada (ajustada para ser mais larga e sem quadrado branco no botão fechar) --- */
#primeiraLinha{ /* backdrop + center */
.modal.modal-centered {
position: fixed;
inset: 0;
display: flex; display: flex;
flex-direction: row; align-items: center;
gap: 20px; justify-content: center;
margin-bottom: 20px; background: rgba(10, 20, 30, 0.45);
z-index: 2000;
padding: 20px;
} }
input,textarea,label{ /* dialog box — maior horizontalmente e altura automática para scroll interno */
font-size: 1.1rem; .modal-dialog.modal-dialog-square {
width: 880px; /* largura aumentada */
max-width: 96vw;
height: auto; /* deixa altura automática (melhor para conteúdo longo) */
max-height: 92vh;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
} }
textarea{ /* caixa branca que contém o conteúdo - ocupa 100% da dialog */
.modal-dialog.modal-dialog-square .modal-content {
width: 100%; width: 100%;
height: 100px; height: auto;
border-radius: 12px;
} box-shadow: 0 12px 30px rgba(11,22,35,0.18);
overflow: hidden;
.submitButton{
display: flex; display: flex;
margin-left: auto; flex-direction: column;
height:50% ; background: #fff;
padding: 8px 20px;
font-size: medium;
} }
.bi-download{ /* header */
font-size: 1.2rem; .custom-modal-header {
margin-right: 5px; position: relative;
font-weight: bold; background: linear-gradient(90deg, #203B75 0%, #274A8A 100%);
color: #fff;
padding: 14px 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
} }
#infoPaciente{ .custom-modal-header .modal-title {
margin-top: 50px; margin: 0;
margin-bottom: 40px; font-size: 1.05rem;
font-weight: 700;
} }
#header-relatorio{ /* botão fechar no header — sem quadrado branco por trás */
.modal-close-btn {
background: transparent !important;
border: none;
width: 40px;
height: 40px;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: none;
outline: none;
position: relative;
z-index: 5;
}
.modal-close-btn::after {
content: '✕';
color: #fff;
font-weight: 700;
font-size: 16px;
}
/* body - faz scroll interno se for longo */
.modal-body {
padding: 18px;
overflow: auto;
flex: 1 1 auto;
}
/* footer */
.custom-modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 14px 18px;
border-top: 1px solid #eee;
background: #fafafa;
}
/* folhaA4 dentro da modal — adapta para caber */
.folhaA4 {
width: 100%;
box-sizing: border-box;
background: transparent;
padding: 0;
}
/* melhor espaçamento e leitura do conteúdo */
#header-relatorio p {
color: #374151;
margin: 6px 0;
text-align: center; text-align: center;
margin-bottom: 30px;
} }
.info-paciente{ #infoPaciente p {
font-weight: bold; margin: 10px 0;
color: #3d4650;
}
/* tornar o viewer responsivo */
.tiptap-viewer-wrapper {
border: 1px dashed #e7e7e7;
padding: 12px;
margin-top: 10px;
background: #fff;
}
/* barra de scroll customizada (opcional) */
.modal-body::-webkit-scrollbar {
width: 10px;
}
.modal-body::-webkit-scrollbar-thumb {
background: rgba(100,100,100,0.18);
border-radius: 8px;
}
.modal-body::-webkit-scrollbar-track {
background: rgba(0,0,0,0.02);
}
/* responsividade para telas pequenas: mantém centralizado, ajusta proporção */
@media (max-width: 680px) {
.modal-dialog.modal-dialog-square {
width: 92vw;
height: 86vh;
}
.modal-close-btn { width: 36px; height: 36px; }
.custom-modal-footer { padding: 10px; }
.modal-body { padding: 12px; }
} }

View File

@ -4,109 +4,121 @@ import { useState, useEffect } from 'react'
import API_KEY from '../components/utils/apiKeys' import API_KEY from '../components/utils/apiKeys'
import { UserInfos } from '../components/utils/Functions-Endpoints/General' import { UserInfos } from '../components/utils/Functions-Endpoints/General'
import FormConsultaPaciente from './FormConsultaPaciente' import FormConsultaPaciente from './FormConsultaPaciente'
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor' import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor'
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient' import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient'
const ConsultaEditPage = ({dadosConsulta}) => { // 1. Importe o useNavigate
import { useNavigate } from 'react-router-dom'
console.log(dadosConsulta, "editar") const ConsultaEditPage = ({ dadosConsulta }) => {
// 2. Crie a instância do navigate
const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth() const { getAuthorizationHeader } = useAuth()
const authHeader = getAuthorizationHeader();
const [idUsuario, setIDusuario] = useState("6e7f8829-0574-42df-9290-8dbb70f75ada") const [idUsuario, setIDusuario] = useState(null);
const [DictInfo, setDict] = useState({});
const [DictInfo, setDict] = useState({}) const [Medico, setMedico] = useState(null);
const [Paciente, setPaciente] = useState(null);
const [Medico, setMedico] = useState({})
const [Paciente, setPaciente] = useState([])
useEffect(() => { useEffect(() => {
setDict({...dadosConsulta}) setDict({ ...dadosConsulta });
const fetchMedicoePaciente = async () => {
console.log(dadosConsulta.doctor_id)
let Medico = await GetDoctorByID(dadosConsulta.doctor_id,authHeader )
let Paciente = await GetPatientByID(dadosConsulta.patient_id,authHeader )
console.log(Paciente, 'Paciente')
setMedico(Medico[0])
setPaciente(Paciente[0])
const fetchInitialData = async () => {
if (dadosConsulta.doctor_id) {
const medicoData = await GetDoctorByID(dadosConsulta.doctor_id, authHeader);
setMedico(medicoData[0]);
} }
const ColherInfoUsuario =async () => { if (dadosConsulta.patient_id) {
const result = await UserInfos(authHeader) const pacienteData = await GetPatientByID(dadosConsulta.patient_id, authHeader);
setPaciente(pacienteData[0]);
setIDusuario(result?.profile?.id)
} }
ColherInfoUsuario() };
fetchMedicoePaciente()
const fetchUserInfo = async () => {
const result = await UserInfos(authHeader);
setIDusuario(result?.profile?.id);
};
}, []) fetchUserInfo();
fetchInitialData();
}, [dadosConsulta, authHeader]);
useEffect(() => { useEffect(() => {
setDict({...DictInfo, medico_nome:Medico?.full_name, dataAtendimento:dadosConsulta.scheduled_at?.split("T")[0]}) if (Medico) {
}, [Medico]) setDict(prevDict => ({
...prevDict,
medico_nome: Medico?.full_name,
dataAtendimento: dadosConsulta.scheduled_at?.split("T")[0]
}));
}
}, [Medico, dadosConsulta.scheduled_at]);
const handleSave = async (DictParaPatch) => {
try {
let authHeader = getAuthorizationHeader() const myHeaders = new Headers();
const handleSave = (DictParaPatch) => {
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json"); myHeaders.append("Content-Type", "application/json");
myHeaders.append('apikey', API_KEY) myHeaders.append('apikey', API_KEY);
myHeaders.append("authorization", authHeader) myHeaders.append("authorization", authHeader);
myHeaders.append('Prefer', 'return=representation');
console.log(DictParaPatch)
var raw = JSON.stringify({"patient_id": DictParaPatch.patient_id,
"doctor_id": DictParaPatch.doctor_id,
"duration_minutes": 30,
"chief_complaint": "Dor de cabeça há 3 ",
"created_by": idUsuario,
"scheduled_at": `${DictParaPatch.dataAtendimento}T${DictParaPatch.horarioInicio}:00.000Z`,
"appointment_type": DictParaPatch.tipo_consulta,
"patient_notes": "Prefiro horário pela manhã",
"insurance_provider": DictParaPatch.convenio,
"status": DictParaPatch.status,
"created_by": idUsuario
const raw = JSON.stringify({
patient_id: DictParaPatch.patient_id,
doctor_id: DictParaPatch.doctor_id,
duration_minutes: 30,
chief_complaint: "Dor de cabeça há 3 ",
created_by: idUsuario,
scheduled_at: `${DictParaPatch.dataAtendimento}T${DictParaPatch.horarioInicio}:00.000Z`,
appointment_type: DictParaPatch.tipo_consulta,
patient_notes: "Prefiro horário pela manhã",
insurance_provider: DictParaPatch.convenio,
status: DictParaPatch.status,
}); });
const requestOptions = {
var requestOptions = {
method: 'PATCH', method: 'PATCH',
headers: myHeaders, headers: myHeaders,
body: raw, body: raw,
redirect: 'follow' redirect: 'follow'
}; };
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${DictInfo.id}`, requestOptions) const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${DictInfo.id}`, requestOptions);
.then(response => response.text())
.then(result => console.log(result)) if (!response.ok) {
.catch(error => console.log('error', error)); const text = await response.text();
console.error('Erro no PATCH:', response.status, text);
throw new Error('Erro na API');
} }
const updatedData = await response.json();
if (updatedData && updatedData.length > 0) {
setDict(updatedData[0]);
}
console.log('Consulta atualizada com sucesso!', updatedData);
alert('Consulta atualizada com sucesso!');
} catch (error) {
console.error('Erro ao salvar consulta:', error);
alert('Erro ao salvar consulta. Veja o console.');
}
};
const handleCancel = () => {
navigate(-1);
};
return ( return (
<div> <div>
<FormConsultaPaciente agendamento={DictInfo} setAgendamento={setDict} onSave={handleSave}/> {}
<FormConsultaPaciente
agendamento={DictInfo}
setAgendamento={setDict}
onSave={handleSave}
onCancel={handleCancel}
/>
</div> </div>
) )
} }
export default ConsultaEditPage export default ConsultaEditPage;

View File

@ -9,7 +9,6 @@ import { useAuth } from '../components/utils/AuthProvider'
const ConsultasPaciente = ({ setConsulta }) => { const ConsultasPaciente = ({ setConsulta }) => {
const { getAuthorizationHeader } = useAuth() const { getAuthorizationHeader } = useAuth()
const [showDeleteModal, setShowDeleteModal] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false)
const [selectedID, setSelectedId] = useState("") const [selectedID, setSelectedId] = useState("")
let authHeader = getAuthorizationHeader() let authHeader = getAuthorizationHeader()
@ -17,73 +16,76 @@ const ConsultasPaciente = ({setConsulta}) => {
const [consultas, setConsultas] = useState([]) const [consultas, setConsultas] = useState([])
const FiltrarAgendamentos = (agendamentos, id) => { const FiltrarAgendamentos = (agendamentos, id) => {
// Verifica se a lista de agendamentos é válida antes de tentar filtrar
if (!agendamentos || !Array.isArray(agendamentos)) { if (!agendamentos || !Array.isArray(agendamentos)) {
console.error("A lista de agendamentos é inválida."); console.error("A lista de agendamentos é inválida.");
setConsultas([]); // Garante que setConsultas receba uma lista vazia setConsultas([]);
return; return;
} }
// 1. Filtragem
// O método .filter() cria uma nova lista contendo apenas os itens que retornarem 'true'
const consultasFiltradas = agendamentos.filter(agendamento => { const consultasFiltradas = agendamentos.filter(agendamento => {
// A condição: o patient_id do agendamento deve ser estritamente igual ao id fornecido
// Usamos toString() para garantir a comparação, pois um pode ser number e o outro string
return agendamento.patient_id && agendamento.patient_id.toString() === id.toString(); return agendamento.patient_id && agendamento.patient_id.toString() === id.toString();
}); });
// 2. Adicionar a lista no setConsultas
console.log(consultasFiltradas) console.log(consultasFiltradas)
setConsultas(consultasFiltradas); setConsultas(consultasFiltradas);
} }
// Exemplo de como você chamaria (assumindo que DadosAgendamento é sua lista original):
// FiltrarAgendamentos(DadosAgendamento, Paciente.id);
useEffect(() => { useEffect(() => {
var myHeaders = new Headers(); const fetchConsultas = async () => {
try {
const myHeaders = new Headers();
myHeaders.append("Authorization", authHeader); myHeaders.append("Authorization", authHeader);
myHeaders.append("apikey", API_KEY) myHeaders.append("apikey", API_KEY)
var requestOptions = { const requestOptions = {
method: 'GET', method: 'GET',
headers: myHeaders, headers: myHeaders,
redirect: 'follow' redirect: 'follow'
}; };
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select&doctor_id&patient_id&status&scheduled_at&order&limit&offset", requestOptions) const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select=*", requestOptions);
.then(response => response.json()) const result = await response.json();
.then(result => {FiltrarAgendamentos(result, "6e7f8829-0574-42df-9290-8dbb70f75ada" )}) FiltrarAgendamentos(result, "6e7f8829-0574-42df-9290-8dbb70f75ada");
.catch(error => console.log('error', error)); } catch (error) {
console.log('error', error);
}
};
}, []) fetchConsultas();
}, [authHeader]);
const navigate = useNavigate() const navigate = useNavigate()
const deleteConsulta = async (ID) => {
const deleteConsulta= (ID) => { try {
var myHeaders = new Headers(); const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json"); myHeaders.append("Content-Type", "application/json");
myHeaders.append('apikey', API_KEY) myHeaders.append('apikey', API_KEY);
myHeaders.append("authorization", authHeader) myHeaders.append("authorization", authHeader);
const raw = JSON.stringify({ "status": "cancelled" });
var raw = JSON.stringify({ "status":"cancelled" const requestOptions = {
});
var requestOptions = {
method: 'PATCH', method: 'PATCH',
headers: myHeaders, headers: myHeaders,
body: raw, body: raw,
redirect: 'follow' redirect: 'follow'
}; };
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${selectedID}`, requestOptions) const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${ID}`, requestOptions);
.then(response => {if(response.status !== 200)(console.log(response))})
.then(result => console.log(result))
.catch(error => console.log('error', error));
console.log("deletar", ID) if (!response.ok) {
const errorText = await response.text();
throw new Error(`Falha ao cancelar consulta: ${response.status} - ${errorText}`);
}
setConsultas(prevConsultas => prevConsultas.filter(consulta => consulta.id !== ID));
console.log("Consulta cancelada com sucesso!");
alert("Consulta cancelada com sucesso!");
} catch (error) {
console.error('Erro ao cancelar a consulta:', error);
alert('Erro ao cancelar a consulta. Veja o console.');
}
} }
return ( return (
@ -91,22 +93,19 @@ const FiltrarAgendamentos = (agendamentos, id) => {
<h1> Gerencie suas consultas</h1> <h1> Gerencie suas consultas</h1>
<div className='form-container'> <div className='form-container'>
<button className="btn btn-primary" onClick={() => { navigate("criar") }}> <button className="btn btn-primary" onClick={() => { navigate("criar") }}>
<i className="bi bi-plus-circle"></i> Adicionar Consulta <i className="bi bi-plus-circle"></i> Adicionar Consulta
</button> </button>
<h2>Seus proximos atendimentos</h2> <h2>Seus próximos atendimentos</h2>
{consultas.map((consulta) => ( {consultas.map((consulta) => (
<CardConsultaPaciente consulta={consulta} setConsulta={setConsulta} setShowDeleteModal={setShowDeleteModal} setSelectedId={ setSelectedId}/> <CardConsultaPaciente key={consulta.id} consulta={consulta} setConsulta={setConsulta} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} />
))} ))}
{showDeleteModal && {showDeleteModal &&
<div className="modal-dialog modal-dialog-centered"> <div className="modal-dialog modal-dialog-centered">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header bg-danger bg-opacity-25"> <div className="modal-header bg-danger bg-opacity-25">
<h5 className="modal-title text-danger"> <h5 className="modal-title text-danger">
Confirmação de Exclusão Confirmação de Exclusão
@ -117,15 +116,12 @@ const FiltrarAgendamentos = (agendamentos, id) => {
onClick={() => setShowDeleteModal(false)} onClick={() => setShowDeleteModal(false)}
></button> ></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<p className="mb-0 fs-5"> <p className="mb-0 fs-5">
Tem certeza que deseja excluir este agendamento? Tem certeza que deseja excluir este agendamento?
</p> </p>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button <button
type="button" type="button"
className="btn btn-primary" className="btn btn-primary"
@ -133,23 +129,19 @@ const FiltrarAgendamentos = (agendamentos, id) => {
> >
Cancelar Cancelar
</button> </button>
<button <button
type="button" type="button"
className="btn btn-danger" className="btn btn-danger"
onClick={() => { deleteConsulta(selectedID); setShowDeleteModal(false) }} onClick={() => { deleteConsulta(selectedID); setShowDeleteModal(false) }}
> >
<i className="bi bi-trash me-1"></i> Excluir <i className="bi bi-trash me-1"></i> Excluir
</button> </button>
</div> </div>
</div> </div>
</div>} </div>}
</div> </div>
</div> </div>
) )
} }
export default ConsultasPaciente export default ConsultasPaciente;

View File

@ -4,14 +4,20 @@ import { useAuth } from '../utils/AuthProvider';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useMemo } from 'react'; import { useMemo } from 'react';
import "./style/card-consulta.css" import "./style/card-consulta.css"
const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, setDictInfo, setSelectedId} ) => { const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, setDictInfo, setSelectedId, setShowConfirmModal, corModal, selectedID, coresConsultas, setListaConsultaID, listaConsultasID} ) => {
const navigate = useNavigate(); const navigate = useNavigate();
const {getAuthorizationHeader} = useAuth() const {getAuthorizationHeader} = useAuth()
const authHeader = getAuthorizationHeader() const authHeader = getAuthorizationHeader()
const [Paciente, setPaciente] = useState() const [Paciente, setPaciente] = useState()
const [Medico, setMedico] = useState() const [Medico, setMedico] = useState()
const [decidirBotton, setDecidirBotton] = useState("")
const ids = useMemo(() => { const ids = useMemo(() => {
return { return {
doctor_id: DadosConsulta?.doctor_id, doctor_id: DadosConsulta?.doctor_id,
@ -45,23 +51,35 @@ const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, se
let nameArrayMedico = Medico?.full_name.split(' ') let nameArrayMedico = Medico?.full_name.split(' ')
console.log(DadosConsulta.status)
let indice_cor = listaConsultasID.indexOf(DadosConsulta.id)
// console.log(coresConsultas)
//console.log(indice_cor,"indice no cores")
return ( return (
<div className={`container-cardconsulta container-cardconsulta-${TabelaAgendamento} ` }> <div className={`container-cardconsulta container-cardconsulta-${TabelaAgendamento} ` }>
{DadosConsulta.id? {DadosConsulta.id?
<div className='cardconsulta' id={`status-card-consulta-${DadosConsulta.status}`}> <div className={`cardconsulta`} id={indice_cor !== -1 ? `status-card-consulta-${coresConsultas[indice_cor]}` : `status-card-consulta-${DadosConsulta.status}`}>
<div> <div>
<section className='cardconsulta-infosecundaria'> <section className='cardconsulta-infosecundaria'>
<p>{DadosConsulta.horario} {nameArrayMedico && nameArrayMedico.length > 0 ? nameArrayMedico[0] : ''} {nameArrayMedico && nameArrayMedico.length > 1 ? ` ${nameArrayMedico[1]}` : ''} </p> <p>Medico:{DadosConsulta.horario} {nameArrayMedico && nameArrayMedico.length > 0 ? nameArrayMedico[0] : ''} {nameArrayMedico && nameArrayMedico.length > 1 ? ` ${nameArrayMedico[1]}` : ''} </p>
</section> </section>
<section className='cardconsulta-infoprimaria'> <section className='cardconsulta-infoprimaria'>
<p>{nameArrayPaciente && nameArrayPaciente.length > 0 ? nameArrayPaciente[0] : ''} {nameArrayPaciente && nameArrayPaciente.length > 1 ? ` ${nameArrayPaciente[1]}` : ''}- {}</p> <p>Paciente: {nameArrayPaciente && nameArrayPaciente.length > 0 ? nameArrayPaciente[0] : ''} {nameArrayPaciente && nameArrayPaciente.length > 1 ? ` ${nameArrayPaciente[1]}` : ''}- {}
</p>
</section> </section>
</div> </div>
@ -75,16 +93,35 @@ const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, se
> >
<i className="bi bi-pencil me-1"></i> <i className="bi bi-pencil me-1"></i>
</button> </button>
{DadosConsulta.status === "cancelled" ?
<button
className="btn btn-sm btn-confirm-style"
onClick={() => {
console.log(DadosConsulta.id)
setShowConfirmModal(true)
setSelectedId(DadosConsulta.id);
}}
>
<i class="bi bi-check-lg"></i>
</button>
:
<button <button
className="btn btn-sm btn-delete-custom-style " className="btn btn-sm btn-delete-custom-style "
onClick={() => { onClick={() => {
console.log(DadosConsulta.id) console.log(DadosConsulta.id)
setSelectedId(DadosConsulta.id); setSelectedId(DadosConsulta.id);
setShowDeleteModal(true); setShowDeleteModal(true);
}} }}
> >
<i className="bi bi-trash me-1"></i> <i className="bi bi-trash me-1"></i>
</button> </button>
}
</div> </div>
</div> </div>
@ -94,6 +131,8 @@ const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, se
} }
</div> </div>
) )
} }

View File

@ -1,36 +1,25 @@
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] = useState(30);
const [sessoes,setSessoes] = useState(1) const [showSuccessModal, setShowSuccessModal] = useState(false);
const [tempoBaseConsulta, setTempoBaseConsulta] = useState(30); // NOVO: Tempo base da consulta em minutos const [todosProfissionais, setTodosProfissionais] = useState([]);
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 [profissionaisFiltrados, setProfissionaisFiltrados] = useState([]);
const [isDropdownOpen, setIsDropdownOpen] = useState(false); 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);
@ -38,137 +27,116 @@ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
.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,
email:value
}})}
else if(name === 'status'){
if(agendamento.status==='requested'){
setAgendamento((prev) => ({
...prev, ...prev,
status:'confirmed', contato: { ...prev.contato, email: value }
})); }));
}else if(agendamento.status === 'confirmed'){ } else if (name === 'status') {
console.log(value) setAgendamento(prev => ({
setAgendamento((prev) => ({
...prev, ...prev,
status:'requested', status: prev.status === 'requested' ? 'confirmed' : 'requested'
})); }));
}} } else if (name === 'paciente_cpf') {
const cpfFormatted = FormatCPF(value);
else if(name === 'paciente_cpf'){
let cpfFormatted = FormatCPF(value)
const fetchPatient = async () => { const fetchPatient = async () => {
let patientData = await GetPatientByCPF(cpfFormatted, authHeader); const patientData = await GetPatientByCPF(cpfFormatted, authHeader);
if (patientData) { if (patientData) {
setAgendamento((prev) => ({ setAgendamento(prev => ({
...prev, ...prev,
paciente_nome: patientData.full_name, paciente_nome: patientData.full_name,
patient_id: patientData.id patient_id: patientData.id
})); }));
}} }
setAgendamento(prev => ({ ...prev, cpf: cpfFormatted })) };
fetchPatient() setAgendamento(prev => ({ ...prev, paciente_cpf: cpfFormatted }));
fetchPatient();
} else if (name === 'convenio') { } else if (name === 'convenio') {
setAgendamento({...agendamento,insurance_provider:value}) setAgendamento(prev => ({ ...prev, insurance_provider: value }));
} } else {
else{ setAgendamento(prev => ({ ...prev, [name]: value }));
setAgendamento({...agendamento,[name]:value})
}
} }
};
const ChamarMedicos = useCallback(async () => {
const Medicos = await GetAllDoctors(authHeader);
setTodosProfissionais(Medicos);
}, [authHeader]);
useEffect(() => { useEffect(() => {
const ChamarMedicos = async () => {
const Medicos = await GetAllDoctors(authHeader)
setTodosProfissionais(Medicos)
}
ChamarMedicos(); ChamarMedicos();
}, [ChamarMedicos]);
var myHeaders = new Headers(); useEffect(() => {
if (!agendamento.dataAtendimento || !agendamento.doctor_id) return;
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json"); myHeaders.append("Content-Type", "application/json");
myHeaders.append("apikey", API_KEY) myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", `Bearer ${authHeader.split(' ')[1]}`); myHeaders.append("Authorization", `Bearer ${authHeader.split(' ')[1]}`);
var raw = JSON.stringify({ const raw = JSON.stringify({
"doctor_id": agendamento.doctor_id, doctor_id: agendamento.doctor_id,
"start_date": agendamento.dataAtendimento, start_date: agendamento.dataAtendimento,
"end_date": `${agendamento.dataAtendimento}T23:59:59.999Z`, end_date: `${agendamento.dataAtendimento}T23:59:59.999Z`,
}); });
var requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
headers: myHeaders, headers: myHeaders,
body: raw, body: raw,
redirect: 'follow'
}; };
fetch("https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/get-available-slots", requestOptions) fetch("https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/get-available-slots", requestOptions)
.then(response => response.json()) .then(response => response.json())
.then(result => {console.log(result); sethorariosDisponiveis(result)}) .then(result => sethorariosDisponiveis(result))
.catch(error => console.log('error', error)); .catch(error => console.log('error', error));
}, [agendamento.dataAtendimento, agendamento.doctor_id, authHeader]);
}, [agendamento.dataAtendimento, agendamento.doctor_id])
// FUNÇÃO DE BUSCA E FILTRAGEM
const handleSearchProfissional = (e) => { 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);
@ -180,40 +148,54 @@ 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">
@ -221,44 +203,40 @@ const handleSubmit = (e) => {
<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>
<div className="campos-informacoes-paciente" id="informacoes-paciente-linha-tres">
<div className="campos-informacoes-paciente" id="informacoes-paciente-linha-tres">
<div> <div>
<label>Convênio</label> <label>Convênio</label>
<select name="convenio" onChange={handleChange}> <select name="convenio" onChange={handleChange} value={agendamento.insurance_provider}>
<option value="publico">Público</option> <option value="publico">Público</option>
<option value="unimed">Unimed</option> <option value="unimed">Unimed</option>
<option value="bradesco_saude">Bradesco Saúde</option> <option value="bradesco_saude">Bradesco Saúde</option>
<option value="hapvida">Hapvida</option> <option value="hapvida">Hapvida</option>
</select> </select>
</div> </div>
</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" // Use o nome correto da propriedade no estado `agendamento` name="nome_medico"
onChange={handleSearchProfissional} onChange={handleSearchProfissional}
value={agendamento?.nome_medico} value={agendamento?.nome_medico || ''}
autoComplete="off" // Ajuda a evitar o autocomplete nativo do navegador autoComplete="off"
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} // Use o ID do profissional key={profissional.id}
className='dropdown-item' className='dropdown-item'
onClick={() => handleSelectProfissional(profissional)} onClick={() => handleSelectProfissional(profissional)}
> >
@ -271,27 +249,22 @@ const handleSubmit = (e) => {
<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"> <div className="campo-de-input">
<label>Data *</label> <label>Data *</label>
<input type="date" name="dataAtendimento" onChange={handleChange} required /> <input type="date" name="dataAtendimento" onChange={handleChange} value={agendamento.dataAtendimento} required />
</div>
</div> </div>
<div className="linha"> <div className="linha">
{/* Dropdown de Início (Não modificado) */}
<div className="campo-de-input"> <div className="campo-de-input">
<label htmlFor="inicio">Início *</label> <label htmlFor="inicio">Início *</label>
<select <select
@ -302,7 +275,7 @@ const handleSubmit = (e) => {
onChange={(e) => setHorarioInicio(e.target.value)} onChange={(e) => setHorarioInicio(e.target.value)}
> >
<option value="" disabled>Selecione a hora de início</option> <option value="" disabled>Selecione a hora de início</option>
{opcoesDeHorario?.map((opcao, index) => ( {opcoesDeHorario.map((opcao, index) => (
<option <option
key={index} key={index}
value={opcao.value} value={opcao.value}
@ -315,30 +288,29 @@ const handleSubmit = (e) => {
</select> </select>
</div> </div>
{/* SELETOR DE SESSÕES MODIFICADO */}
{/* Removemos o 'label' para evitar o desalinhamento e colocamos o texto acima */}
<div className='seletor-wrapper'> <div className='seletor-wrapper'>
<label>Número de Sessões *</label> {/* Novo label para o seletor */} <label>Número de Sessões *</label>
<div className='sessao-contador'> <div className='sessao-contador'>
<button <button
type="button" /* Adicionado para evitar submissão de formulário */ type="button"
onClick={() => {if(sessoes === 0)return; else(setSessoes(sessoes - 1))}} onClick={() => setSessoes(prev => Math.max(0, prev - 1))}
disabled={sessoes === 0} /* Desabilita o botão no limite */ disabled={sessoes === 0}
> >
<i className="bi bi-chevron-compact-left"></i> <i className="bi bi-chevron-compact-left"></i>
</button> </button>
<p className='sessao-valor'>{sessoes}</p> {/* Adicionada classe para estilização */} <p className='sessao-valor'>{sessoes}</p>
<button <button
type="button" /* Adicionado para evitar submissão de formulário */ type="button"
onClick={() => {if(sessoes === 3 )return; else(setSessoes(sessoes + 1))}} onClick={() => setSessoes(prev => Math.min(3, prev + 1))}
disabled={sessoes === 3} /* Desabilita o botão no limite */ disabled={sessoes === 3}
> >
<i className="bi bi-chevron-compact-right"></i> <i className="bi bi-chevron-compact-right"></i>
</button> </button>
</div> </div>
</div> </div>
<div className="campo-de-input"> <div className="campo-de-input">
<label htmlFor="termino">Término *</label> <label htmlFor="termino">Término *</label>
<input <input
@ -349,37 +321,37 @@ const handleSubmit = (e) => {
readOnly readOnly
className="horario-termino-readonly" className="horario-termino-readonly"
/> />
</div> </div>
</div> </div>
</div>
</section> </section>
<section className="informacoes-atendimento-segunda-linha-direita"> <section className="informacoes-atendimento-segunda-linha-direita">
<div className="campo-de-input"> <div className="campo-de-input">
<label>Observações</label> <label>Observações</label>
<textarea name="observacoes" rows="4" cols="1"></textarea> <textarea name="observacoes" rows="4" cols="1" onChange={handleChange} value={agendamento.observacoes || ''}></textarea>
</div> </div>
</section> </section>
</section> </section>
<div className="campo-de-input-check">
<input
className="form-check-input form-custom-check"
type="checkbox"
name="status"
onChange={handleChange}
checked={agendamento.status === 'requested'}
/>
<label className="form-check-label checkbox-label" htmlFor="status">
Adicionar a fila de espera
</label>
</div>
<div className="form-actions"> <div className="form-actions">
<button type="submit" className="btn-primary">Salvar agendamento</button> <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

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import CardConsulta from './CardConsulta'; import CardConsulta from './CardConsulta';
import "./style/styleTabelas/tabeladia.css"; import "./style/styleTabelas/tabeladia.css";
const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDeleteModal, setDictInfo, setSelectedId }) => { const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDeleteModal, setDictInfo,selectedID, setSelectedId, setShowConfirmModal, coresConsultas, setListaConsultaID, listaConsultasID }) => {
const [indiceAcesso, setIndiceAcesso] = useState(0) const [indiceAcesso, setIndiceAcesso] = useState(0)
const [Dia, setDia] = useState() const [Dia, setDia] = useState()
const agendamentosDoDia = agendamentos?.semana1?.segunda || []; const agendamentosDoDia = agendamentos?.semana1?.segunda || [];
@ -10,7 +10,6 @@ const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDel
let ListaDiasComAgendamentos = Object.keys(agendamentos) let ListaDiasComAgendamentos = Object.keys(agendamentos)
console.log(agendamentos)
//console.log(Dia, "hshdhshhsdhs") //console.log(Dia, "hshdhshhsdhs")
@ -19,6 +18,28 @@ const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDel
setDia(ListaDiasComAgendamentos[indiceAcesso]) setDia(ListaDiasComAgendamentos[indiceAcesso])
}, [indiceAcesso]) }, [indiceAcesso])
const formatarDataComDia = (dataISO) => {
if (!dataISO) return '';
const data = new Date(dataISO); // converte para objeto Date
// nomes dos dias da semana
const dias = [
'Segunda-feira',
'Terça-feira',
'Quarta-feira',
'Quinta-feira',
'Sexta-feira',
'Sábado'
];
const diaSemana = dias[data.getDay()]; // 0 = Domingo, 1 = Segunda, etc.
const dia = dataISO.split('-')[2];
const mes = dataISO.split('-')[1];
const ano = dataISO.split('-')[0];
return `${diaSemana}, ${dia}/${mes}/${ano}`;
};
return ( return (
<div> <div>
@ -27,7 +48,7 @@ const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDel
<button onClick={() => {if(indiceAcesso === 0)return; else(setIndiceAcesso(indiceAcesso - 1))}}> <i className="bi bi-chevron-compact-left"></i></button> <button onClick={() => {if(indiceAcesso === 0)return; else(setIndiceAcesso(indiceAcesso - 1))}}> <i className="bi bi-chevron-compact-left"></i></button>
<p>{Dia ? `${Dia?.split('-')[2]}/${Dia?.split('-')[1]}/${Dia?.split('-')[0]}`: ''}</p> <p>{Dia ? formatarDataComDia(Dia) : ''}</p>
<button onClick={() => {if(ListaDiasComAgendamentos.length - 1 === indiceAcesso)return; else(setIndiceAcesso(indiceAcesso + 1))}}> <i className="bi bi-chevron-compact-right"></i></button> <button onClick={() => {if(ListaDiasComAgendamentos.length - 1 === indiceAcesso)return; else(setIndiceAcesso(indiceAcesso + 1))}}> <i className="bi bi-chevron-compact-right"></i></button>
</div> </div>
@ -51,7 +72,7 @@ const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDel
<td className='coluna-horario'><p className='horario-texto'>{`${horario[0]}:${horario[1]}`}</p></td> <td className='coluna-horario'><p className='horario-texto'>{`${horario[0]}:${horario[1]}`}</p></td>
<td className='mostrar-horario'> <td className='mostrar-horario'>
<div onClick={() => handleClickAgendamento(agendamento)}> <div onClick={() => handleClickAgendamento(agendamento)}>
<CardConsulta DadosConsulta={agendamento} TabelaAgendamento={'dia'} setShowDeleteModal={setShowDeleteModal} setDictInfo={setDictInfo} setSelectedId={setSelectedId}/> <CardConsulta DadosConsulta={agendamento} TabelaAgendamento={'dia'} setShowDeleteModal={setShowDeleteModal} setDictInfo={setDictInfo} setSelectedId={setSelectedId} selectedID={selectedID} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -6,7 +6,7 @@ import "./style/styleTabelas/tabelames.css";
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModal, setSelectedId ,setDictInfo }) => { const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModal, setSelectedId ,setDictInfo, setShowConfirmModal, coresConsultas ,setListaConsultaID, listaConsultasID }) => {
const dataHoje = dayjs(); const dataHoje = dayjs();
const AnoAtual = dataHoje.year(); const AnoAtual = dataHoje.year();
@ -56,19 +56,19 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModa
} }
switch (diaSemana) { switch (diaSemana) {
case 'Monday': case 'segunda-feira':
semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento])
break break
case 'Tuesday': case 'terça-feira':
semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento])
break break
case 'Wednesday': case 'quarta-feira':
semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento])
break break
case 'Thursday': case 'quinta-feira':
semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento])
break break
case 'Friday': case 'sexta-feira':
semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento])
break break
default: default:
@ -202,9 +202,9 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModa
{ {
semana && typeof semana === "object" && Object.keys(semana).map((dia) => ( semana && typeof semana === "object" && Object.keys(semana).map((dia) => (
<td key={dia} > <td key={dia} >
<CardConsulta DadosConsulta={((semana[dia]|| [])[0]) || {status:'vazio'}} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/> <CardConsulta TabelaAgendamento={'mes'} DadosConsulta={((semana[dia]|| [])[0]) || {status:'vazio'}} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
<CardConsulta DadosConsulta={((semana[dia]|| [])[1]) || {status:'vazio'}} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/> <CardConsulta TabelaAgendamento={'mes'} DadosConsulta={((semana[dia]|| [])[1]) || {status:'vazio'}} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
<CardConsulta DadosConsulta={((semana[dia]|| [])[2]) || {status:'vazio'}} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/> <CardConsulta TabelaAgendamento={'mes'} DadosConsulta={((semana[dia]|| [])[2]) || {status:'vazio'}} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
{semana[dia].length > 3 ? ( {semana[dia].length > 3 ? (
<div> <div>
<p>{` +${semana[dia].length - 2}`}</p> <p>{` +${semana[dia].length - 2}`}</p>

View File

@ -6,18 +6,20 @@ import { useEffect, useState, useMemo } from 'react';
import weekOfYear from 'dayjs/plugin/weekOfYear' import weekOfYear from 'dayjs/plugin/weekOfYear'
dayjs.extend(weekOfYear) dayjs.extend(weekOfYear)
const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteModal ,setSelectedId ,setDictInfo}) => { const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteModal ,setSelectedId ,setDictInfo, setShowConfirmModal, coresConsultas ,setListaConsultaID, listaConsultasID}) => {
// Armazena o objeto COMPLETO das semanas organizadas // Armazena o objeto COMPLETO das semanas organizadas
const [semanasOrganizadas, setSemanasOrganizadas] = useState({}); const [semanasOrganizadas, setSemanasOrganizadas] = useState({});
// Controla qual semana está sendo exibida (o índice da chave no objeto) // Controla qual semana está sendo exibida (o índice da chave no objeto)
const [Indice, setIndice] = useState(0); const [Indice, setIndice] = useState(0);
console.log(agendamentos, "agendamentos diarios")
const dataHoje = dayjs(); const dataHoje = dayjs();
const AnoAtual = dataHoje.year(); const AnoAtual = dataHoje.year();
const mes = dataHoje.month() + 1; const mes = dataHoje.month() + 1;
let DiasdoMes = ListarDiasdoMes(AnoAtual, mes)
// Array de chaves (ex: ['semana40', 'semana41', ...]) // Array de chaves (ex: ['semana40', 'semana41', ...])
const chavesDasSemanas = Object.keys(semanasOrganizadas); const chavesDasSemanas = Object.keys(semanasOrganizadas);
@ -46,30 +48,34 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
segunda: [], terça: [], quarta: [], quinta: [], sexta: [] segunda: [], terça: [], quarta: [], quinta: [], sexta: []
} }
} }
console.log(diaSemana)
switch (diaSemana) { switch (diaSemana) {
case 'Monday':
case 'segunda-feira':
console.log("segunda")
semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento])
break break
case 'Tuesday': case 'terça-feira':
semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento])
break break
case 'Wednesday': case 'quarta-feira':
semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento])
break break
case 'Thursday': case 'quinta-feira':
semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento])
break break
case 'Friday': case 'sexta-feira':
semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento])
break break
default: default:
break break
} }
} }
console.log(semanas, "agendamentos semanais")
return semanas return semanas
}, [agendamentos, AnoAtual]) // Adicionei AnoAtual como dependência por segurança }, [agendamentos, AnoAtual])
// --- EFEITO PARA POPULAR O ESTADO --- // --- EFEITO PARA POPULAR O ESTADO ---
@ -159,49 +165,50 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
<tbody> <tbody>
{indicesDeLinha.map((indiceLinha) => { {indicesDeLinha.map((indiceLinha) => {
let schedulet_at = semanaParaRenderizar.segunda[indiceLinha].scheduled_at.split("T") //let schedulet_at = semanaParaRenderizar.segunda[indiceLinha].scheduled_at.split("T")
let horario = schedulet_at[1].split(":") // let horario = schedulet_at[1].split(":")
console.log(horario)
console.log(semanaParaRenderizar, "aqui")
return( return(
<tr key={indiceLinha}> <tr key={indiceLinha}>
{/* Célula para Horário (Pode ser ajustado para mostrar o horário real) */}
<td> <td>
<p className='horario-texto'> {`${horario[0]}:${horario[1]}`} </p> {/* <p className='horario-texto'> {`${horario[0]}:${horario[1]}`} </p>*/}
</td> </td>
{/* Mapeamento de COLUNAS (dias) */} {/* Mapeamento de COLUNAS (dias) */}
<td> <td>
{semanaParaRenderizar.segunda[indiceLinha] {semanaParaRenderizar?.segunda[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.segunda[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} /> ? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar?.segunda[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
: null : null
} }
</td> </td>
<td> <td>
{semanaParaRenderizar.terça[indiceLinha] {semanaParaRenderizar.terça[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.terça[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/> ? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.terça[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
: null : null
} }
</td> </td>
<td> <td>
{semanaParaRenderizar.quarta[indiceLinha] {semanaParaRenderizar.quarta[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.quarta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/> ? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.quarta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
: null : null
} }
</td> </td>
<td> <td>
{semanaParaRenderizar.quinta[indiceLinha] {semanaParaRenderizar.quinta[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.quinta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} /> ? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.quinta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
: null : null
} }
</td> </td>
<td> <td>
{semanaParaRenderizar.sexta[indiceLinha] {semanaParaRenderizar.sexta[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.sexta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} /> ? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.sexta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID} />
: null : null
} }
</td> </td>

View File

@ -50,7 +50,7 @@
/* 6. Estilo de hover para o botão de exclusão */ /* 6. Estilo de hover para o botão de exclusão */
.btn-delete-custom-style:hover { .btn-delete-custom-style:hover {
background-color: #c82333; /* Um vermelho um pouco mais escuro para o hover */ background-color: #c82333; /* Um vermelho um pouco mais escuro para o hover */
filter: brightness(90%); /* Alternativa: escurecer um pouco mais */
} }
/* 7. Estilos para os ícones dentro dos botões (já está no JSX com fs-4) */ /* 7. Estilos para os ícones dentro dos botões (já está no JSX com fs-4) */
@ -59,3 +59,14 @@
/* Exemplo: se precisar de um ajuste fino além do fs-4 */ /* Exemplo: se precisar de um ajuste fino além do fs-4 */
/* font-size: 1.5rem; */ /* font-size: 1.5rem; */
} }
.btn-confirm-style{
background-color: #5ce687;
}
.card-verde{
background-color: #b7ffbd;
border: #91d392;
}

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,29 +301,24 @@ 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 {
position: absolute; /* Flutua em relação ao pai (.campo-de-input-container) */
top: 100%; /* Começa logo abaixo do input */
left: 0;
width: 100%; /* Ocupa toda a largura do container pai */
/* Estilos visuais */ .dropdown-profissionais {
position: absolute;
top: 100%;
left: 0;
width: 100%;
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;
@ -340,135 +330,190 @@ html[data-bs-theme="dark"] svg {
.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

@ -6,6 +6,8 @@
overflow: hidden; /* mantém o arredondado */ overflow: hidden; /* mantém o arredondado */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 4px solid #4a90e2; /* borda azul, altere para a cor desejada */ border: 4px solid #4a90e2; /* borda azul, altere para a cor desejada */
} }
/* 1. Estilização do TD (Container) */ /* 1. Estilização do TD (Container) */
.coluna-horario { .coluna-horario {

View File

@ -6,6 +6,7 @@
overflow: hidden; /* mantém o arredondado */ overflow: hidden; /* mantém o arredondado */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 4px solid #4a90e2; /* borda azul, altere para a cor desejada */ border: 4px solid #4a90e2; /* borda azul, altere para a cor desejada */
} }
/* Células da tabela */ /* Células da tabela */
@ -67,10 +68,10 @@
.tabelasemanal tr:hover { .tabelasemanal tr:hover {
background-color: #f1f1f1 !important; background-color: #f1f1f1 !important;
} }
/*
tr{ tr{
width: 1000px; width: 1000px;
} }*/
html[data-bs-theme="dark"] .tabelasemanal { html[data-bs-theme="dark"] .tabelasemanal {
border: 4px solid #333; border: 4px solid #333;
@ -111,3 +112,5 @@ html[data-bs-theme="dark"] .tabelasemanal .cardconsulta {
box-shadow: 0 1px 3px rgba(0,0,0,0.3); box-shadow: 0 1px 3px rgba(0,0,0,0.3);
border-left: 5px solid #333; border-left: 5px solid #333;
} }

View File

@ -0,0 +1,242 @@
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.video-chat-container {
font-family: Arial, sans-serif;
}
/* --- O BOTÃO FLUTUANTE (COM CORREÇÃO) --- */
.video-chat-button {
position: fixed;
bottom: 20px;
right: 95px;
z-index: 9999;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #007bff;
color: white;
border: none;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
cursor: pointer;
display: flex;
align-items: center; /* <-- Correção do alinhamento */
justify-content: center;
transition: all 0.3s ease;
}
.video-chat-button:hover {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
/* --- A JANELA DE CHAT --- */
.video-chat-window {
position: fixed;
bottom: 90px;
right: 95px;
width: 500px;
height: 380px;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.15);
z-index: 10000;
display: flex;
flex-direction: column;
overflow: hidden; /* Importante para o border-radius */
/* Animação de "surgir" */
animation: slide-up 0.3s ease-out;
/* Animação "premium" para tela cheia */
transition: all 0.4s ease-in-out;
}
/* --- MODO TELA CHEIA (SIMULADO) --- */
.video-chat-window.pseudo-fullscreen {
width: 100vw;
height: 100vh;
bottom: 0;
right: 0;
border-radius: 0;
border: none;
z-index: 99999;
}
.video-chat-window.pseudo-fullscreen .video-chat-header {
display: none;
}
/* --- HEADER DA JANELA --- */
.video-chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #f7f7f7;
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0; /* Impede o header de encolher */
}
.video-chat-header h3 {
margin: 0;
font-size: 16px;
}
.video-chat-controls {
display: flex;
align-items: center;
gap: 8px;
}
.control-btn {
background: none;
border: none;
cursor: pointer;
color: #888;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.control-btn:hover {
background-color: #e0e0e0;
}
.close-btn {
font-size: 24px;
line-height: 1;
}
.fullscreen-btn {
font-size: 14px;
}
/* --- CORPO DA JANELA (CONSOLIDADO) --- */
.video-chat-body {
flex-grow: 1; /* Ocupa todo o espaço vertical */
overflow-y: hidden; /* Os filhos (lista, call-screen) cuidam do scroll */
display: flex;
flex-direction: column;
padding: 0; /* Os filhos cuidam do padding */
transition: padding 0.4s ease-in-out;
}
.video-chat-window.pseudo-fullscreen .video-chat-body {
padding: 0;
}
/* --- 1. LISTA DE PACIENTES --- */
.patient-list-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.patient-list-container > p {
padding: 15px 15px 10px 15px;
margin: 0;
font-size: 15px;
color: #555;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0; /* Impede de encolher */
}
.patient-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto; /* Adiciona scroll SÓ AQUI */
flex-grow: 1; /* Ocupa o espaço restante */
}
.patient-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.patient-item:hover {
background-color: #f9f9f9;
}
.patient-item span {
font-weight: 600;
color: #333;
}
.call-btn {
display: flex;
align-items: center;
gap: 6px;
background-color: #28a745;
color: white;
border: none;
border-radius: 5px;
padding: 8px 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.call-btn:hover {
background-color: #218838;
}
/* --- 2. TELA DE CHAMADA --- */
.call-screen {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: #2c2c2c;
color: white;
}
.call-screen h4 {
margin: 0;
padding: 12px;
text-align: center;
background-color: rgba(0,0,0,0.2);
font-size: 16px;
flex-shrink: 0; /* Impede de encolher */
}
.video-placeholder {
flex-grow: 1; /* Ocupa todo o espaço */
display: flex;
align-items: center;
justify-content: center;
width: 100%;
background-color: #1a1a1a;
color: #888;
font-style: italic;
overflow: hidden; /* Caso o <iframe>/video tente vazar */
}
.call-actions {
padding: 15px;
display: flex;
justify-content: center;
background-color: rgba(0,0,0,0.2);
flex-shrink: 0; /* Impede de encolher */
}
.hang-up-btn {
display: flex;
align-items: center;
gap: 10px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 50px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.hang-up-btn:hover {
background-color: #c82333;
transform: scale(1.05);
}

View File

@ -0,0 +1,158 @@
import React, { useState, useEffect } from 'react';
import './BotaoVideoChamada.css';
import { FaVideo, FaExpand, FaCompress, FaPhoneSlash } from 'react-icons/fa';
import { JitsiMeeting } from '@jitsi/react-sdk';
import { db } from '../firebaseConfig';
import { ref, set, remove } from "firebase/database";
// MOCK PACIENTE
const mockPacientes = [
{ id: 1, name: 'Paciente' }
];
// DADOS DO MÉDICO
const MEU_ID_MEDICO = 'medico-99';
const MEU_NOME_MEDICO = 'Dr. Rafael';
const BotaoVideoChamada = () => {
const [isOpen, setIsOpen] = useState(false);
const [isFullScreen, setIsFullScreen] = useState(false);
const [callActive, setCallActive] = useState(false);
const [callingPatient, setCallingPatient] = useState(null);
const [roomName, setRoomName] = useState('');
// UseEffect da tecla "Esc"
useEffect(() => {
const handleEscKey = (event) => {
if (event.key === 'Escape' && isFullScreen) {
setIsFullScreen(false);
}
};
document.addEventListener('keydown', handleEscKey);
return () => document.removeEventListener('keydown', handleEscKey);
}, [isFullScreen]);
// Função para INICIAR a chamada
const handleStartCall = (paciente) => {
// Adiciona o #config para pular o lobby
const newRoomName = `mediconnect-call-${MEU_ID_MEDICO}-${paciente.id}-${Date.now()}#config.prejoinPageEnabled=false`;
const callRef = ref(db, `calls/paciente-${paciente.id}`);
set(callRef, {
incomingCall: {
fromId: MEU_ID_MEDICO,
fromName: MEU_NOME_MEDICO,
roomName: newRoomName
}
});
setRoomName(newRoomName);
setCallingPatient(paciente);
setCallActive(true);
};
// Função para ENCERRAR a chamada
const handleHangUp = () => {
if (callingPatient) {
const callRef = ref(db, `calls/paciente-${callingPatient.id}`);
remove(callRef);
}
setCallActive(false);
setCallingPatient(null);
setRoomName('');
console.log("Chamada encerrada.");
};
// Função para fechar a janela
const toggleVideoChat = () => {
setIsOpen(!isOpen);
if (isOpen) {
handleHangUp();
setIsFullScreen(false);
}
};
// Função de Tela Cheia
const handleFullScreen = () => {
setIsFullScreen(!isFullScreen);
};
return (
<div className="video-chat-container">
{isOpen && (
<div className={`video-chat-window ${isFullScreen ? 'pseudo-fullscreen' : ''}`}>
<div className="video-chat-header">
<h3>{callActive ? `Em chamada com...` : 'Iniciar Chamada'}</h3>
{/* ================================== */}
{/* BOTÕES DE VOLTA - CORREÇÃO AQUI */}
{/* ================================== */}
<div className="video-chat-controls">
<button onClick={handleFullScreen} className="control-btn fullscreen-btn">
{isFullScreen ? <FaCompress size={14} /> : <FaExpand size={14} />}
</button>
<button onClick={toggleVideoChat} className="control-btn close-btn">
&times;
</button>
</div>
</div>
<div className="video-chat-body">
{callActive ? (
// TELA DE CHAMADA ATIVA (JITSI)
<div className="call-screen">
<JitsiMeeting
roomName={roomName}
domain="meet.jit.si"
userInfo={{
displayName: MEU_NOME_MEDICO
}}
configOverwrite={{
prejoinPageEnabled: false,
enableWelcomePage: false,
enableClosePage: false,
toolbarButtons: [
'microphone', 'camera', 'desktop', 'hangup', 'chat', 'settings'
],
}}
interfaceConfigOverwrite={{
SHOW_SUBJECT: false,
DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
}}
getIFrameRef={(iframe) => { iframe.style.height = '100%'; }}
onApiReady={(api) => {
api.on('videoConferenceLeft', handleHangUp);
}}
/>
</div>
) : (
// TELA DE LISTA DE PACIENTES
<div className="patient-list-container">
<p>Selecione um paciente para iniciar a chamada:</p>
<ul className="patient-list">
{mockPacientes.map((paciente) => (
<li key={paciente.id} className="patient-item">
<span>{paciente.name}</span>
<button onClick={() => handleStartCall(paciente)} className="call-btn">
<FaVideo size={14} /> Chamar
</button>
</li>
))}
</ul>
</div>
)}
</div>
</div>
)}
{/* Botão flutuante */}
<button className="video-chat-button" onClick={toggleVideoChat}>
<FaVideo size={22} color="white" />
</button>
</div>
);
};
export default BotaoVideoChamada;

View File

@ -0,0 +1,467 @@
/* ARQUIVO CSS COMPLETAMENTE NOVO E SEPARADO */
@keyframes slide-up-paciente { /* Nome do keyframe mudado */
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.paciente-video-container { /* Classe mudada */
font-family: Arial, sans-serif;
}
.paciente-video-button { /* Classe mudada */
position: fixed;
bottom: 20px;
right: 95px; /* Posição igual ao outro, ao lado da acessibilidade */
z-index: 9999;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #007bff; /* Cor pode ser diferente, se quiser */
color: white;
border: none;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.paciente-video-button:hover { /* Classe mudada */
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.paciente-video-window { /* Classe mudada */
position: fixed;
bottom: 90px;
right: 95px;
width: 500px;
height: 380px;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.15);
z-index: 10000;
display: flex;
flex-direction: column;
overflow: hidden;
animation: slide-up-paciente 0.3s ease-out; /* Keyframe mudado */
transition: all 0.4s ease-in-out;
}
.paciente-video-window.pseudo-fullscreen { /* Classe mudada */
width: 100vw;
height: 100vh;
bottom: 0;
right: 0;
border-radius: 0;
border: none;
z-index: 99999;
}
.paciente-video-window.pseudo-fullscreen .paciente-video-header { /* Classe mudada */
display: none;
}
.paciente-video-header { /* Classe mudada */
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #f7f7f7;
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0;
}
.paciente-video-header h3 { /* Classe mudada */
margin: 0;
font-size: 16px;
}
.paciente-video-controls { /* Classe mudada */
display: flex;
align-items: center;
gap: 8px;
}
/* Os estilos internos (como .control-btn, .call-screen, .patient-list)
podem ser mantidos, pois estão "dentro" das classes que mudamos.
Mas para garantir 100% de separação, renomeei todos.
*/
.paciente-video-body { /* Classe mudada */
flex-grow: 1;
overflow-y: hidden;
display: flex;
flex-direction: column;
padding: 0;
transition: padding 0.4s ease-in-out;
}
.paciente-video-window.pseudo-fullscreen .paciente-video-body { /* Classe mudada */
padding: 0;
}
/* Estilos da Lista e Chamada (copiados e prefixados)
Não problema em reutilizar .patient-list, .call-screen, etc,
mas vamos renomear para segurança.
*/
.patient-list-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.patient-list-container > p {
padding: 15px 15px 10px 15px;
margin: 0;
font-size: 15px;
color: #555;
border-bottom: 1px solid #f0f0f0;
flex-shrink: 0;
}
.patient-list {
list-style: none;
margin: 0;
padding: 0;
overflow-y: auto;
flex-grow: 1;
}
.patient-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s;
}
.patient-item:hover {
background-color: #f9f9f9;
}
.patient-item span {
font-weight: 600;
color: #333;
}
.call-btn {
display: flex;
align-items: center;
gap: 6px;
background-color: #28a745;
color: white;
border: none;
border-radius: 5px;
padding: 8px 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.call-btn:hover {
background-color: #218838;
}
/* Tela de Chamada */
.call-screen {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: #2c2c2c;
color: white;
}
.call-screen h4 {
margin: 0;
padding: 12px;
text-align: center;
background-color: rgba(0,0,0,0.2);
font-size: 16px;
flex-shrink: 0;
}
.video-placeholder {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
background-color: #1a1a1a;
color: #888;
font-style: italic;
overflow: hidden;
}
.call-actions {
padding: 15px;
display: flex;
justify-content: center;
background-color: rgba(0,0,0,0.2);
flex-shrink: 0;
}
.hang-up-btn {
display: flex;
align-items: center;
gap: 10px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 50px;
padding: 12px 24px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.hang-up-btn:hover {
background-color: #c82333;
transform: scale(1.05);
}
/* Controles (reutilizados, mas dentro de .paciente-video-header) */
.control-btn {
background: none;
border: none;
cursor: pointer;
color: #888;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.control-btn:hover {
background-color: #e0e0e0;
}
.close-btn {
font-size: 24px;
line-height: 1;
}
.fullscreen-btn {
font-size: 14px;
}
/* Animação de surgir */
@keyframes slide-up-paciente {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Animação "Pulsar" (Ringing) */
@keyframes ringing {
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 123, 255, 0.7); }
70% { transform: scale(1.1); box-shadow: 0 0 0 20px rgba(0, 123, 255, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(0, 123, 255, 0); }
}
.paciente-video-container {
font-family: Arial, sans-serif;
}
/* Botão flutuante */
.paciente-video-button {
position: fixed;
bottom: 20px;
right: 95px;
z-index: 9999;
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #007bff;
color: white;
border: none;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.paciente-video-button:hover {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
/* Aplica a animação "pulsar" */
.paciente-video-button.ringing {
animation: ringing 1.5s infinite;
}
/* Janela de Vídeo */
.paciente-video-window {
position: fixed;
bottom: 90px;
right: 95px;
width: 500px;
height: 380px;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.15);
z-index: 10000;
display: flex;
flex-direction: column;
overflow: hidden;
animation: slide-up-paciente 0.3s ease-out;
transition: all 0.4s ease-in-out;
}
/* Modo Tela Cheia (Simulado) */
.paciente-video-window.pseudo-fullscreen {
width: 100vw;
height: 100vh;
bottom: 0;
right: 0;
border-radius: 0;
border: none;
z-index: 99999;
}
.paciente-video-window.pseudo-fullscreen .paciente-video-header {
display: none;
}
/* Header da Janela */
.paciente-video-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background-color: #f7f7f7;
border-bottom: 1px solid #e0e0e0;
flex-shrink: 0;
}
.paciente-video-header h3 {
margin: 0;
font-size: 16px;
}
.paciente-video-controls {
display: flex;
align-items: center;
gap: 8px;
}
/* Corpo da Janela */
.paciente-video-body {
flex-grow: 1;
overflow-y: hidden;
display: flex;
flex-direction: column;
padding: 0;
transition: padding 0.4s ease-in-out;
}
.paciente-video-window.pseudo-fullscreen .paciente-video-body {
padding: 0;
}
/* --- ESTILOS DOS 3 ESTADOS --- */
/* 1. Tela de Chamada Ativa (Jitsi) */
.call-screen {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background-color: #2c2c2c;
color: white;
}
.video-placeholder { /* (Caso o Jitsi não carregue) */
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
background-color: #1a1a1a;
color: #888;
font-style: italic;
overflow: hidden;
}
/* 2. Tela de Chamada Recebida */
.incoming-call-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
background-color: #f7f9fc;
padding: 20px;
text-align: center;
}
.incoming-call-screen p {
font-size: 16px;
color: #555;
margin: 0;
}
.incoming-call-screen h3 {
font-size: 24px;
color: #333;
margin: 10px 0 30px 0;
}
.incoming-call-actions {
display: flex;
justify-content: space-around;
width: 100%;
max-width: 300px;
}
.incoming-call-actions button {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
border: none;
border-radius: 50%;
width: 70px;
height: 70px;
justify-content: center;
font-size: 14px;
font-weight: 600;
color: white;
cursor: pointer;
transition: all 0.2s;
}
.incoming-call-actions button:hover {
transform: scale(1.1);
}
.decline-btn { /* Botão Recusar */
background-color: #dc3545;
}
.decline-btn:hover {
background-color: #c82333;
}
.accept-btn { /* Botão Atender */
background-color: #28a745;
}
.accept-btn:hover {
background-color: #218838;
}
/* 3. Tela de Espera (Ocioso) */
.patient-idle-screen {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #888;
font-style: italic;
padding: 20px;
text-align: center;
}
/* Estilos dos controles (reutilizados) */
.control-btn {
background: none;
border: none;
cursor: pointer;
color: #888;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.control-btn:hover {
background-color: #e0e0e0;
}
.close-btn {
font-size: 24px;
line-height: 1;
}
.fullscreen-btn {
font-size: 14px;
}

View File

@ -0,0 +1,171 @@
import React, { useState, useEffect } from 'react';
import './BotaoVideoPaciente.css';
import { FaVideo, FaExpand, FaCompress, FaPhoneSlash, FaPhone } from 'react-icons/fa';
import { JitsiMeeting } from '@jitsi/react-sdk';
import { db } from '../firebaseConfig';
import { ref, onValue, remove } from "firebase/database";
// ID DO PACIENTE
const MEU_ID_PACIENTE = '1'; // Deve ser '1' para bater com o do médico
const BotaoVideoPaciente = () => {
const [isOpen, setIsOpen] = useState(false);
const [isFullScreen, setIsFullScreen] = useState(false);
const [callActive, setCallActive] = useState(false);
const [roomName, setRoomName] = useState('');
const [incomingCallData, setIncomingCallData] = useState(null);
const [callerName, setCallerName] = useState('');
// "Ouvinte" do Firebase
useEffect(() => {
const callRef = ref(db, `calls/paciente-${MEU_ID_PACIENTE}`);
const unsubscribe = onValue(callRef, (snapshot) => {
const data = snapshot.val();
if (data && data.incomingCall) {
setIncomingCallData(data.incomingCall);
setCallerName(data.incomingCall.fromName);
setIsOpen(true);
} else {
setIncomingCallData(null);
setCallActive(false);
}
});
return () => unsubscribe();
}, []);
// UseEffect da tecla "Esc"
useEffect(() => {
const handleEscKey = (event) => {
if (event.key === 'Escape' && isFullScreen) {
setIsFullScreen(false);
}
};
document.addEventListener('keydown', handleEscKey);
return () => document.removeEventListener('keydown', handleEscKey);
}, [isFullScreen]);
// Função para ATENDER
const handleAcceptCall = () => {
if (!incomingCallData) return;
setRoomName(incomingCallData.roomName);
setCallActive(true);
setIncomingCallData(null);
};
// Função para RECUSAR / DESLIGAR
const handleHangUp = () => {
const callRef = ref(db, `calls/paciente-${MEU_ID_PACIENTE}`);
remove(callRef);
setCallActive(false);
setRoomName('');
setCallerName('');
setIncomingCallData(null);
};
// Função para fechar a janela
const toggleVideoChat = () => {
setIsOpen(!isOpen);
if (isOpen) {
handleHangUp();
setIsFullScreen(false);
}
};
const handleFullScreen = () => {
setIsFullScreen(!isFullScreen);
};
// Renderiza o conteúdo (Ocioso, Recebendo, Em Chamada)
const renderContent = () => {
// 1ª Prioridade: Em chamada ativa
if (callActive) {
return (
<div className="call-screen">
<JitsiMeeting
roomName={roomName}
domain="meet.jit.si"
// Informações do Usuário (Paciente)
userInfo={{
displayName: 'Paciente' // Você pode mudar isso
}}
// Configurações para pular todas as telas
configOverwrite={{
prejoinPageEnabled: false,
enableWelcomePage: false,
enableClosePage: false,
toolbarButtons: [
'microphone', 'camera', 'hangup', 'chat', 'settings'
],
}}
// Configurações da Interface
interfaceConfigOverwrite={{
SHOW_SUBJECT: false,
DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
}}
getIFrameRef={(iframe) => { iframe.style.height = '100%'; }}
onApiReady={(api) => {
api.on('videoConferenceLeft', handleHangUp);
}}
/>
</div>
);
}
// 2ª Prioridade: Recebendo uma chamada
if (incomingCallData) {
return (
<div className="incoming-call-screen">
<p>Chamada recebida de:</p>
<h3>{callerName || 'Médico'}</h3>
<div className="incoming-call-actions">
<button className="decline-btn" onClick={handleHangUp}>
<FaPhoneSlash size={20} /> Recusar
</button>
<button className="accept-btn" onClick={handleAcceptCall}>
<FaPhone size={20} /> Atender
</button>
</div>
</div>
);
}
// 3ª Prioridade: Nenhuma chamada, tela de espera
return (
<div className="patient-idle-screen">
<p>Aguardando chamadas do seu médico...</p>
</div>
);
};
return (
<div className="paciente-video-container">
{isOpen && (
<div className={`paciente-video-window ${isFullScreen ? 'pseudo-fullscreen' : ''}`}>
<div className="paciente-video-header">
<h3>{callActive ? `Em chamada...` : (incomingCallData ? 'Chamada Recebida' : 'Videochamada')}</h3>
<div className="paciente-video-controls">
<button onClick={handleFullScreen} className="control-btn fullscreen-btn">
{isFullScreen ? <FaCompress size={14} /> : <FaExpand size={14} />}
</button>
<button onClick={toggleVideoChat} className="control-btn close-btn">
&times;
</button>
</div>
</div>
<div className="paciente-video-body">
{renderContent()}
</div>
</div>
)}
<button
className={`paciente-video-button ${incomingCallData ? 'ringing' : ''}`}
onClick={toggleVideoChat}
>
<FaVideo size={22} color="white" />
</button>
</div>
);
};
export default BotaoVideoPaciente;

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%;
text-align: center;
} }
.logout-modal-content h3 { .right-corner-elements {
margin-bottom: 1rem; gap: 15px;
color: #333;
font-size: 1.25rem;
} }
.logout-modal-content p { .profile-picture-container {
margin-bottom: 2rem; width: 40px;
color: #666; height: 40px;
line-height: 1.4;
} }
.logout-modal-buttons { .suporte-card-container,
display: flex; .chat-container {
gap: 1rem; margin-right: 10px;
justify-content: center; margin-left: 10px;
} }
.logout-cancel-button { .suporte-card,
padding: 0.75rem 1.5rem; .chat-online {
border: 1px solid #ccc; width: calc(100vw - 20px);
border-radius: 8px; max-width: none;
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();
@ -25,12 +44,16 @@ const Header = () => {
} }
}, [mensagens]); }, [mensagens]);
// Funções de Logout (do seu código) // --- Logout ---
const handleLogoutClick = () => { const handleLogoutClick = () => {
setShowLogoutModal(true); setShowLogoutModal(true);
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 podemos ajudar você 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,10 +153,11 @@ const Header = () => {
setMensagem(''); setMensagem('');
}; };
const handleEnviarMensagem = (e) => { const handleEnviarMensagem = async (e) => {
e.preventDefault(); e.preventDefault();
if (mensagem.trim() === '') return; if (mensagem.trim() === '') return;
// Mensagem do usuário
const novaMensagemUsuario = { const novaMensagemUsuario = {
id: Date.now(), id: Date.now(),
texto: mensagem, texto: mensagem,
@ -146,30 +168,34 @@ const Header = () => {
setMensagens(prev => [...prev, novaMensagemUsuario]); setMensagens(prev => [...prev, novaMensagemUsuario]);
setMensagem(''); setMensagem('');
setTimeout(() => { try {
if (chatInputRef.current) { const response = await fetch("http://localhost:5000/api/chat", {
chatInputRef.current.focus(); method: "POST",
} headers: { "Content-Type": "application/json" },
}, 0); body: JSON.stringify({ message: mensagem }),
});
setTimeout(() => { const data = await response.json();
const respostas = [
'Entendi sua dúvida. Vou verificar isso para você.',
'Obrigado pela informação. Estou analisando seu caso.',
'Pode me dar mais detalhes sobre o problema?',
'Já encaminhei sua solicitação para nossa equipe técnica.',
'Vou ajudar você a resolver isso!'
];
// Resposta da IA
const respostaSuporte = { const respostaSuporte = {
id: Date.now() + 1, id: Date.now() + 1,
texto: respostas[Math.floor(Math.random() * respostas.length)], texto: data.resposta || data.reply || "Desculpe, não consegui processar sua pergunta no momento 😅",
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' })
}; };
setMensagens(prev => [...prev, respostaSuporte]); setMensagens(prev => [...prev, respostaSuporte]);
}, 1000); } catch (error) {
console.error("Erro ao conectar com o servidor:", error);
const erroMsg = {
id: Date.now() + 1,
texto: "Ops! Ocorreu um erro ao tentar falar com o suporte.",
remetente: 'suporte',
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
};
setMensagens(prev => [...prev, erroMsg]);
}
}; };
const SuporteCard = () => ( const SuporteCard = () => (
@ -245,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>

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();
}
}}
>
<span className="acesso-text">Acesso aos módulos</span>
<ToggleIcon isOpen={isOpen} />
</div>
{isOpen && (
<div className="perfil-list">
{options.length > 0 ? (
options.map((opt) => (
<div
key={opt.key}
className="perfil-item"
onClick={() => handleProfileClick(opt.route)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleProfileClick(opt.route);
}
}}
> >
<option value="">Selecionar perfil</option>
{options.map((opt) => (
<option key={opt.key} value={opt.route}>
{opt.label} {opt.label}
</option> </div>
))} ))
</select> ) : (
<p className="no-profiles">Nenhum perfil disponível.</p>
)}
</div>
)}
</div> </div>
); );
}; };

View File

@ -126,12 +126,14 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
} }
}; };
const handleAvailabilityUpdate = useCallback( const handleAvailabilityUpdate = useCallback((newAvailability) => {
(newAvailability) => { setFormData((prev) => {
setFormData((prev) => ({ ...prev, availability: newAvailability })); if (JSON.stringify(prev.availability) !== JSON.stringify(newAvailability)) {
}, return { ...prev, availability: newAvailability };
[setFormData] }
); return prev;
});
}, []);
const handleCepBlur = async () => { const handleCepBlur = async () => {
const cep = formData.cep?.replace(/\D/g, ""); const cep = formData.cep?.replace(/\D/g, "");
@ -229,25 +231,6 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
} }
}; };
const handlePatchAvailability = async (id, updatedAvailability) => {
try {
const response = await fetch(`${ENDPOINT_AVAILABILITY}?id=${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(updatedAvailability),
});
const data = await response.json();
console.log("Disponibilidade atualizada:", data);
alert("Disponibilidade atualizada com sucesso!");
} catch (error) {
console.error("Erro ao atualizar disponibilidade:", error);
alert("Erro ao atualizar disponibilidade.");
}
};
const handleSubmit = async () => { const handleSubmit = async () => {
const missingFields = []; const missingFields = [];
if (!formData.full_name) missingFields.push("full_name"); if (!formData.full_name) missingFields.push("full_name");
@ -290,20 +273,12 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
await onSave({ ...formData }); await onSave({ ...formData });
if (formData.availability && formData.availability.length > 0) { if (formData.availability && formData.availability.length > 0) {
if (formData.availabilityId) {
await handlePatchAvailability(
formData.availabilityId,
formData.availability
);
} else {
await handleCreateAvailability(formData.availability);
}
} }
alert("Médico salvo e disponibilidade enviada ao mock com sucesso!"); alert("Médico salvo com sucesso!");
} catch (error) { } catch (error) {
console.error("Erro ao salvar médico ou disponibilidade:", error); console.error("Erro ao salvar médico:", error);
alert("Erro ao salvar médico ou disponibilidade."); alert("Erro ao salvar médico.");
}; };
}; };
@ -734,7 +709,7 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
</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

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback, useRef } from "react";
import { Clock } from "lucide-react"; import { Clock } from "lucide-react";
const initialBlockTemplate = { const initialBlockTemplate = {
@ -21,21 +21,19 @@ const emptyAvailabilityTemplate = [
const HorariosDisponibilidade = ({ const HorariosDisponibilidade = ({
initialAvailability = emptyAvailabilityTemplate, initialAvailability = emptyAvailabilityTemplate,
onUpdate, onUpdate,
onCancel,
}) => { }) => {
const [availability, setAvailability] = useState(initialAvailability); const [availability, setAvailability] = useState(initialAvailability);
const isFirstRun = useRef(true);
useEffect(() => { useEffect(() => {
if (initialAvailability !== emptyAvailabilityTemplate) { if (initialAvailability && initialAvailability.length > 0) {
setAvailability(initialAvailability); setAvailability(initialAvailability);
} else {
setAvailability(emptyAvailabilityTemplate);
} }
}, [initialAvailability]); }, [initialAvailability]);
useEffect(() => {
if (onUpdate) {
onUpdate(availability);
}
}, [availability, onUpdate]);
const handleDayCheck = useCallback((dayIndex, currentIsChecked) => { const handleDayCheck = useCallback((dayIndex, currentIsChecked) => {
const isChecked = !currentIsChecked; const isChecked = !currentIsChecked;
@ -110,6 +108,10 @@ const HorariosDisponibilidade = ({
); );
}, []); }, []);
const handleSave = useCallback(() => {
if (onUpdate) onUpdate(availability);
}, [availability, onUpdate]);
const renderTimeBlock = (dayIndex, bloco) => ( const renderTimeBlock = (dayIndex, bloco) => (
<div <div
key={bloco.id} key={bloco.id}
@ -165,7 +167,7 @@ const HorariosDisponibilidade = ({
width: "100%", width: "100%",
boxSizing: "border-box", boxSizing: "border-box",
outline: "none", outline: "none",
fontSize: "13px" fontSize: "13px",
}} }}
step="300" step="300"
/> />
@ -186,7 +188,12 @@ const HorariosDisponibilidade = ({
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}> <div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<label <label
htmlFor={`termino-${dayIndex}-${bloco.id}`} htmlFor={`termino-${dayIndex}-${bloco.id}`}
style={{ fontWeight: 500, color: "#4b5563", width: "56px", fontSize: "13px" }} style={{
fontWeight: 500,
color: "#4b5563",
width: "56px",
fontSize: "13px",
}}
> >
Término: Término:
</label> </label>
@ -258,8 +265,7 @@ const HorariosDisponibilidade = ({
marginLeft: window.innerWidth < 640 ? "0" : "16px", marginLeft: window.innerWidth < 640 ? "0" : "16px",
fontWeight: 500, fontWeight: 500,
}} }}
> ></span>
</span>
)} )}
</div> </div>
); );

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()
return DictMedicos
}
const result = await fetch(
'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?select=id,full_name,crm&limit=500',
requestOptions
);
const DictMedicos = await result.json();
return DictMedicos;
};
const GetDoctorByName = async (nome, authHeader) => { const GetDoctorByName = async (nome, authHeader) => {
const Medicos = await GetAllDoctors(authHeader) const Medicos = await GetAllDoctors(authHeader);
for (let i = 0; i < Medicos.length; i++) { for (let i = 0; i < Medicos.length; i++) {
if (Medicos[i].full_name === nome) { if (Medicos[i].full_name === nome) {
console.log('Medico encontrado:', Medicos[i]); console.log('Médico encontrado:', Medicos[i]);
return 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

@ -0,0 +1,17 @@
import React from 'react';
import './style.css';
const CabecalhoError = ({ showCabecalho, message, errorCode }) => {
if (!showCabecalho) return null;
return (
<div className="cabecalho-error-wrapper">
<div className="cabecalho-error">
<p className='cabecalho-error-text'>{message}</p>
</div>
</div>
);
};
export default CabecalhoError;

View File

@ -0,0 +1,15 @@
function manager (setShowModal, RefreshingToken, setErrorInfo,errorData) {
console.log((errorData, "MANAGER"))
if(errorData.httpStatus === 401){
RefreshingToken()
}else{
setErrorInfo(errorData)
setShowModal("modal");
}
}
export default manager

View File

@ -16,7 +16,10 @@ if( ErrorData.httpStatus === 401){
console.log('uaua') console.log('uaua')
}else if(ErrorData.httpStatus === 404){ }else if(ErrorData.httpStatus === 404){
setModalMensagem("Erro interno do sistema") setModalMensagem("Erro interno do sistema")
}else{setModalMensagem(ErrorData.mensagem)} }else if(ErrorData.httpStatus === undefined){
setModalMensagem("Erro operacional no sistema")
}
else{setModalMensagem(ErrorData.mensagem)}
}, [ErrorData]) }, [ErrorData])
@ -24,7 +27,7 @@ return(
<div> <div>
{showModal ? {showModal === "modal"?
<div className="modal-overlay"> <div className="modal-overlay">

View File

@ -0,0 +1,26 @@
.cabecalho-error {
background-color: #f3616d; /* vermelho forte */
color: white;
display: flex;
align-items: center; /* centraliza verticalmente */
justify-content: center; /* centraliza horizontalmente */
padding: 12px 20px;
margin: 10px;
border-radius: 8px;
font-weight: bold;
font-size: 16px;
width: calc(100% - 20px); /* ocupa quase toda a largura da div pai */
box-sizing: border-box;
}
.cabecalho-error-text {
margin: 0;
padding-left: 30px; /* espaço para "Erro 404" */
position: relative;
}
.cabecalho-error-code {
position: absolute;
left: 0;
font-weight: 700;
}

20
src/firebaseConfig.js Normal file
View File

@ -0,0 +1,20 @@
// 1. ADICIONE ESTAS DUAS LINHAS NO TOPO
import { initializeApp } from "firebase/app";
import { getDatabase } from "firebase/database";
// 2. COLE AQUI O OBJETO QUE VOCÊ COPIOU DO SITE DO FIREBASE
const firebaseConfig = {
apiKey: "SUA_API_KEY...",
authDomain: "medimeconnect.firebaseapp.com",
databaseURL: "https://medimeconnect-default-rtdb.firebaseio.com",
projectId: "medimeconnect",
storageBucket: "medimeconnect.appspot.com",
messagingSenderId: "SEU_ID_MESSAGING",
appId: "SEU_APP_ID"
};
// 3. TENHA CERTEZA QUE ESSAS LINHAS ESTÃO NO FINAL
const app = initializeApp(firebaseConfig);
// A LINHA MAIS IMPORTANTE QUE ESTÁ FALTANDO É ESTA:
export const db = getDatabase(app);

13
src/openaiService.js Normal file
View File

@ -0,0 +1,13 @@
// src/services/openaiService.js
export async function perguntarOpenAI(mensagem) {
const resposta = await fetch("http://localhost:5000/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message: mensagem }),
});
const data = await resposta.json();
return data.reply;
}

View File

@ -21,12 +21,15 @@ import { Search } from 'lucide-react';
const Agendamento = ({setDictInfo}) => { const Agendamento = ({setDictInfo}) => {
const navigate = useNavigate(); const navigate = useNavigate();
const [listaTodosAgendamentos, setListaTodosAgendamentos] = useState([])
const [selectedID, setSelectedId] = useState('0') const [selectedID, setSelectedId] = useState('0')
const [filaEsperaData, setfilaEsperaData] = useState([]) const [filaEsperaData, setFilaEsperaData] = useState([])
const [FiladeEspera, setFiladeEspera] = useState(false); const [FiladeEspera, setFiladeEspera] = useState(false);
const [tabela, setTabela] = useState('diario'); const [tabela, setTabela] = useState('diario');
const [PageNovaConsulta, setPageConsulta] = useState(false); const [PageNovaConsulta, setPageConsulta] = useState(false);
@ -42,39 +45,59 @@ const Agendamento = ({setDictInfo}) => {
const [FiltredTodosMedicos, setFiltredTodosMedicos] = useState([]) const [FiltredTodosMedicos, setFiltredTodosMedicos] = useState([])
const [searchTermDoctor, setSearchTermDoctor] = useState(''); const [searchTermDoctor, setSearchTermDoctor] = useState('');
const [MedicoFiltrado, setMedicoFiltrado] = useState({id:"vazio"})
const [cacheAgendamentos, setCacheAgendamentos] = useState([])
const [showConfirmModal, setShowConfirmModal] = useState(false)
const [motivoCancelamento, setMotivoCancelamento] = useState("")
const [corModal, setCorModal] = useState("")
const [listaConsultasID, setListaConsultaID] = useState([])
const [coresConsultas,setCoresConsultas] = useState([])
let authHeader = getAuthorizationHeader() let authHeader = getAuthorizationHeader()
const cacheMedicos = {};
const cachePacientes = {};
const FiltrarAgendamentos = async (listaTodosAgendamentos) => {
const ConfigurarFiladeEspera = async (patient_id, doctor_id, agendamento) => {
let medico = await GetDoctorByID(doctor_id, authHeader);
let paciente = await GetPatientByID(patient_id, authHeader);
console.log(medico) useMemo(() => {
if (!listaTodosAgendamentos.length) return { agendamentosOrganizados: {}, filaEsperaData: [] };
console.log("recarregando")
const DictAgendamentosOrganizados = {};
const ListaFilaDeEspera = [];
let dicionario = { const fetchDados = async () => {
agendamento: agendamento, for (const agendamento of listaTodosAgendamentos) {
if (agendamento.status === "requested") {
// Cache de médico e paciente
if (!cacheMedicos[agendamento.doctor_id]) {
cacheMedicos[agendamento.doctor_id] = await GetDoctorByID(agendamento.doctor_id, authHeader);
}
if (!cachePacientes[agendamento.patient_id]) {
cachePacientes[agendamento.patient_id] = await GetPatientByID(agendamento.patient_id, authHeader);
}
const medico = cacheMedicos[agendamento.doctor_id];
const paciente = cachePacientes[agendamento.patient_id];
ListaFilaDeEspera.push({
agendamento,
Infos: { Infos: {
nome_medico: medico[0]?.full_name, nome_medico: medico[0]?.full_name,
doctor_id: medico[0]?.id, doctor_id: medico[0]?.id,
patient_id: paciente[0].id, patient_id: paciente[0]?.id,
paciente_nome: paciente[0].full_name, paciente_nome: paciente[0]?.full_name,
paciente_cpf: paciente[0].cpf paciente_cpf: paciente[0]?.cpf,
} },
}; });
return dicionario;
};
let DictAgendamentosOrganizados = {};
let ListaFilaDeEspera = [];
for (const agendamento of listaTodosAgendamentos) {
if (agendamento.status === 'requested') {
let v = await ConfigurarFiladeEspera(agendamento.patient_id, agendamento.doctor_id, agendamento);
ListaFilaDeEspera.push(v);
} else { } else {
const DiaAgendamento = agendamento.scheduled_at.split("T")[0]; const DiaAgendamento = agendamento.scheduled_at.split("T")[0];
@ -86,28 +109,28 @@ const Agendamento = ({setDictInfo}) => {
} }
} }
// Ordenar por data
for (const DiaAgendamento in DictAgendamentosOrganizados) { for (const DiaAgendamento in DictAgendamentosOrganizados) {
DictAgendamentosOrganizados[DiaAgendamento].sort((a, b) => { DictAgendamentosOrganizados[DiaAgendamento].sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at));
if (a.scheduled_at < b.scheduled_at) return -1;
if (a.scheduled_at > b.scheduled_at) return 1;
return 0;
});
} }
const chavesOrdenadas = Object.keys(DictAgendamentosOrganizados).sort();
const chavesOrdenadas = Object.keys(DictAgendamentosOrganizados).sort((a, b) => { const DictAgendamentosFinal = {};
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
let DictAgendamentosFinal = {};
for (const data of chavesOrdenadas) { for (const data of chavesOrdenadas) {
DictAgendamentosFinal[data] = DictAgendamentosOrganizados[data]; DictAgendamentosFinal[data] = DictAgendamentosOrganizados[data];
} }
setAgendamentosOrganizados(DictAgendamentosFinal); setAgendamentosOrganizados(DictAgendamentosFinal);
setfilaEsperaData(ListaFilaDeEspera); setFilaEsperaData(ListaFilaDeEspera);
}; };
fetchDados();
return { agendamentosOrganizados: DictAgendamentosOrganizados, filaEsperaData: ListaFilaDeEspera };
}, [listaTodosAgendamentos]); // 👉 só recalcula quando a lista muda
useEffect(() => { useEffect(() => {
var myHeaders = new Headers(); var myHeaders = new Headers();
myHeaders.append("Authorization", authHeader); myHeaders.append("Authorization", authHeader);
@ -121,7 +144,7 @@ const Agendamento = ({setDictInfo}) => {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select&doctor_id&patient_id&status&scheduled_at&order&limit&offset", requestOptions) fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select&doctor_id&patient_id&status&scheduled_at&order&limit&offset", requestOptions)
.then(response => response.json()) .then(response => response.json())
.then(result => {FiltrarAgendamentos(result);console.log(result)}) .then(result => {setListaTodosAgendamentos(result);console.log(result)})
.catch(error => console.log('error', error)); .catch(error => console.log('error', error));
const PegarTodosOsMedicos = async () => { const PegarTodosOsMedicos = async () => {
@ -135,24 +158,7 @@ const Agendamento = ({setDictInfo}) => {
PegarTodosOsMedicos() PegarTodosOsMedicos()
}, []) }, [])
useEffect(() => {
console.log("mudou FiltredTodosMedicos:", FiltredTodosMedicos);
if (FiltredTodosMedicos.length === 1) {
const unicoMedico = FiltredTodosMedicos[0];
console.log(unicoMedico)
const idMedicoFiltrado = unicoMedico.idMedico;
console.log(`Médico único encontrado: ${unicoMedico.nomeMedico}. ID: ${idMedicoFiltrado}`);
const agendamentosDoMedico = filtrarAgendamentosPorMedico(
DictAgendamentosOrganizados,
idMedicoFiltrado
);
console.log(`Total de agendamentos filtrados para este médico: ${agendamentosDoMedico.length}`);
console.log("Lista completa de Agendamentos do Médico:", agendamentosDoMedico);
FiltrarAgendamentos(agendamentosDoMedico)
}
}, [FiltredTodosMedicos]);
const deleteConsulta = (selectedPatientId) => { const deleteConsulta = (selectedPatientId) => {
var myHeaders = new Headers(); var myHeaders = new Headers();
@ -161,7 +167,32 @@ const deleteConsulta = (selectedPatientId) => {
myHeaders.append("authorization", authHeader) myHeaders.append("authorization", authHeader)
var raw = JSON.stringify({ "status":"cancelled" var raw = JSON.stringify({ "status":"cancelled",
"cancellation_reason": motivoCancelamento
});
var requestOptions = {
method: 'PATCH',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${selectedPatientId}`, requestOptions)
.then(response => {if(response.status !== 200)(console.log(response))})
.then(result => console.log(result))
.catch(error => console.log('error', error));
}
const confirmConsulta = (selectedPatientId) => {
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append('apikey', API_KEY)
myHeaders.append("authorization", authHeader)
var raw = JSON.stringify({ "status":"confirmed"
}); });
@ -179,17 +210,6 @@ const deleteConsulta = (selectedPatientId) => {
} }
const filtrarAgendamentosPorMedico = (dictAgendamentos, idMedicoFiltrado) => {
const todasAsListasDeAgendamentos = Object.values(dictAgendamentos);
const todosOsAgendamentos = todasAsListasDeAgendamentos.flat();
const agendamentosFiltrados = todosOsAgendamentos.filter(agendamento =>
agendamento.doctor_id === idMedicoFiltrado
);
return agendamentosFiltrados;
};
const filteredAgendamentos = useMemo(() => { const filteredAgendamentos = useMemo(() => {
@ -238,12 +258,60 @@ const filtrarAgendamentosPorMedico = (dictAgendamentos, idMedicoFiltrado) => {
}; };
useEffect(() => {
console.log("mudou FiltredTodosMedicos:", FiltredTodosMedicos);
if (MedicoFiltrado.id != "vazio" ) {
const unicoMedico = MedicoFiltrado;
console.log(unicoMedico)
const idMedicoFiltrado = unicoMedico.idMedico;
console.log(`Médico único encontrado: ${unicoMedico.nomeMedico}. ID: ${idMedicoFiltrado}`);
const agendamentosDoMedico = filtrarAgendamentosPorMedico(
DictAgendamentosOrganizados,
idMedicoFiltrado
);
console.log(`Total de agendamentos filtrados para este médico: ${agendamentosDoMedico.length}`);
console.log("Lista completa de Agendamentos do Médico:", agendamentosDoMedico);
//FiltrarAgendamentos(agendamentosDoMedico)
setListaTodosAgendamentos(agendamentosDoMedico)
}
}, [FiltredTodosMedicos, MedicoFiltrado]);
const filtrarAgendamentosPorMedico = (dictAgendamentos, idMedicoFiltrado) => {
setCacheAgendamentos(DictAgendamentosOrganizados);
const todasAsListasDeAgendamentos = Object.values(dictAgendamentos);
const todosOsAgendamentos = todasAsListasDeAgendamentos.flat();
const agendamentosFiltrados = todosOsAgendamentos.filter(agendamento =>
agendamento.doctor_id === idMedicoFiltrado
);
return agendamentosFiltrados;
};
const handleSearchMedicos = (term) => { const handleSearchMedicos = (term) => {
setSearchTermDoctor(term); setSearchTermDoctor(term);
if (term.trim() === '') { if (term.trim() === '') {
if(MedicoFiltrado.id !== "vazio"){
console.log("Medico escolhido, mas vai ser apagado")
console.log(cacheAgendamentos, "cache ")
}
setFiltredTodosMedicos([]); setFiltredTodosMedicos([]);
setMedicoFiltrado({id:"vazio"})
//2 FiltrarAgendamentos()
return; return;
} }
if (FiltredTodosMedicos.length === 1){
setMedicoFiltrado({...FiltredTodosMedicos[0]})
}
const filtered = ListaDeMedicos.filter(medico => const filtered = ListaDeMedicos.filter(medico =>
medico.nomeMedico.toLowerCase().includes(term.toLowerCase()) medico.nomeMedico.toLowerCase().includes(term.toLowerCase())
@ -252,10 +320,9 @@ const handleSearchMedicos = (term) => {
}; };
const handleClickCancel = () => setPageConsulta(false)
return ( return (
<div> <div>
<div className='spinner'></div>
<h1>Agendar nova consulta</h1> <h1>Agendar nova consulta</h1>
@ -306,6 +373,9 @@ const handleSearchMedicos = (term) => {
className='dropdown-item' className='dropdown-item'
onClick={() => { onClick={() => {
setSearchTermDoctor(medico.nomeMedico); setSearchTermDoctor(medico.nomeMedico);
setFiltredTodosMedicos([]);
setMedicoFiltrado(medico)
}} }}
> >
<p>{medico.nomeMedico} </p> <p>{medico.nomeMedico} </p>
@ -336,6 +406,7 @@ const handleSearchMedicos = (term) => {
}} }}
> >
Fila de espera Fila de espera
</button> </button>
</div> </div>
@ -365,9 +436,9 @@ const handleSearchMedicos = (term) => {
</section> </section>
{/* Componentes de Tabela - Adicionado props de delete da main */} {/* Componentes de Tabela - Adicionado props de delete da main */}
{tabela === "diario" && <TabelaAgendamentoDia handleClickAgendamento={handleClickAgendamento} agendamentos={DictAgendamentosOrganizados} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />} {tabela === "diario" && <TabelaAgendamentoDia handleClickAgendamento={handleClickAgendamento} agendamentos={DictAgendamentosOrganizados} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} selectedID={selectedID} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID} />}
{tabela === 'semanal' && <TabelaAgendamentoSemana agendamentos={DictAgendamentosOrganizados} ListarDiasdoMes={ListarDiasdoMes} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/>} {tabela === 'semanal' && <TabelaAgendamentoSemana agendamentos={DictAgendamentosOrganizados} ListarDiasdoMes={ListarDiasdoMes} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} selectedID={selectedID} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>}
{tabela === 'mensal' && <TabelaAgendamentoMes ListarDiasdoMes={ListarDiasdoMes} aplicarCores={true} agendamentos={DictAgendamentosOrganizados} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />} {tabela === 'mensal' && <TabelaAgendamentoMes ListarDiasdoMes={ListarDiasdoMes} aplicarCores={true} agendamentos={DictAgendamentosOrganizados} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} selectedID={selectedID} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>}
</div> </div>
</div> </div>
) )
@ -455,7 +526,7 @@ const handleSearchMedicos = (term) => {
<div className="modal-header bg-danger bg-opacity-25"> <div className="modal-header bg-danger bg-opacity-25">
<h5 className="modal-title text-danger"> <h5 className="modal-title text-danger">
Confirmação de Exclusão Confirmação de Cancelamento
</h5> </h5>
<button <button
type="button" type="button"
@ -466,8 +537,12 @@ const handleSearchMedicos = (term) => {
<div className="modal-body"> <div className="modal-body">
<p className="mb-0 fs-5"> <p className="mb-0 fs-5">
Tem certeza que deseja excluir este agendamento? Qual o motivo do cancelamento?
</p> </p>
<div className='campo-de-input'>
<textarea className='input-modal' value={motivoCancelamento} onChange={(e) => setMotivoCancelamento(e.target.value)} />
</div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
@ -475,7 +550,9 @@ const handleSearchMedicos = (term) => {
<button <button
type="button" type="button"
className="btn btn-primary" className="btn btn-primary"
onClick={() => setShowDeleteModal(false)} onClick={() => {setShowDeleteModal(false);
}}
> >
Cancelar Cancelar
</button> </button>
@ -484,7 +561,22 @@ const handleSearchMedicos = (term) => {
<button <button
type="button" type="button"
className="btn btn-danger" className="btn btn-danger"
onClick={() => {deleteConsulta(selectedID);setShowDeleteModal(false)}} onClick={() => {deleteConsulta(selectedID);
setShowDeleteModal(false)
let lista_cores = coresConsultas
let lista = listaConsultasID
lista.push(selectedID)
lista_cores.push("cancelled")
setCoresConsultas(lista_cores)
setListaConsultaID(lista)
console.log("lista", lista)
}}
> >
<i className="bi bi-trash me-1"></i> Excluir <i className="bi bi-trash me-1"></i> Excluir
@ -495,6 +587,79 @@ const handleSearchMedicos = (term) => {
</div>)} </div>)}
{showConfirmModal &&(
<div
className="modal fade show"
style={{
display: "block",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
tabIndex="-1"
onClick={(e) =>
e.target.classList.contains("modal") && setShowDeleteModal(false)
}
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header bg-success">
<h5 className="modal-title">
Confirmação de edição
</h5>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
Tem certeza que deseja retirar o cancelamento ?
</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() => {setShowConfirmModal(false); setSelectedId("")}}
>
Cancelar
</button>
<button
type="button"
className="btn btn-success"
onClick={() => {confirmConsulta(selectedID);setShowConfirmModal(false)
let lista_cores = coresConsultas
let lista = listaConsultasID
lista.push(selectedID)
lista_cores.push("confirmed")
setCoresConsultas(lista_cores)
setListaConsultaID(lista)
}}
>
<i className="bi bi-trash me-1"></i> Confirmar
</button>
</div>
</div>
</div>
</div>)
}
</div> </div>
) )
} }

View File

@ -13,7 +13,6 @@ const AgendamentoEditPage = ({setDictInfo, DictInfo}) => {
//let DataAtual = dayjs() //let DataAtual = dayjs()
const {getAuthorizationHeader} = useAuth() const {getAuthorizationHeader} = useAuth()
const params = useParams() const params = useParams()
const [PatientToPatch, setPatientToPatch] = useState({})
let id = params.id let id = params.id
@ -22,8 +21,7 @@ const AgendamentoEditPage = ({setDictInfo, DictInfo}) => {
//console.log(DictInfo, 'aqui') //console.log(DictInfo, 'aqui')
useEffect(() => { useEffect(() => {
setDictInfo({...DictInfo?.Infos,...DictInfo?.agendamento}) setDictInfo({...DictInfo, dataAtendimento:DictInfo.scheduled_at.split("T")[0]})
const ColherInfoUsuario =async () => { const ColherInfoUsuario =async () => {
const result = await UserInfos(authHeader) const result = await UserInfos(authHeader)

View File

@ -1,75 +1,68 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { Link } from "react-router-dom"; import HorariosDisponibilidade from "../components/doctors/HorariosDisponibilidade";
const ENDPOINT =
const ENDPOINT_LISTAR = "https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability"; "https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability";
const MEDICOS_MOCKADOS = [ const MEDICOS_MOCKADOS = [
{ id: 53, nome: "João Silva" }, { id: 53, nome: "João Silva" },
{ id: 19, nome: "Ana Costa" }, { id: 19, nome: "Ana Costa" },
{ id: 11, nome: "Pedro Santos" }, { id: 11, nome: "Pedro Santos" },
]; ];
const diasDaSemana = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"]; const diasDaSemana = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];
const formatarDataHora = (isoString) => { const formatarDataHora = (isoString) => {
if (!isoString) return "N/A"; if (!isoString) return "N/A";
try { try {
const data = new Date(isoString); const data = new Date(isoString);
// Usa o toLocaleTimeString para extrair hora e minuto
return data.toLocaleTimeString("pt-BR", { hour: '2-digit', minute: '2-digit', timeZone: 'UTC' }); return data.toLocaleTimeString("pt-BR", {
} catch (error) { hour: "2-digit",
minute: "2-digit",
timeZone: "UTC",
});
} catch {
return "Data Inválida"; return "Data Inválida";
} }
}; };
const DisponibilidadesDoctorPage = () => { const DisponibilidadesDoctorPage = () => {
const [disponibilidades, setDisponibilidades] = useState([]); const [disponibilidades, setDisponibilidades] = useState([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [filtroMedicoNome, setFiltroMedicoNome] = useState(""); const [filtroMedicoNome, setFiltroMedicoNome] = useState("");
const [gerenciarModo, setGerenciarModo] = useState(false);
const [medicoEncontradoId, setMedicoEncontradoId] = useState(null); const [editando, setEditando] = useState(null); // ID da disponibilidade sendo editada
const encontrarMedicoIdPorNome = (nome) => { const encontrarMedicoIdPorNome = (nome) => {
if (!nome) return null; if (!nome) return null;
const termoBusca = nome.toLowerCase(); const termo = nome.toLowerCase();
const medico = MEDICOS_MOCKADOS.find((m) =>
m.nome.toLowerCase().includes(termo)
const medico = MEDICOS_MOCKADOS.find(m =>
m.nome.toLowerCase().includes(termoBusca)
); );
return medico ? medico.id : null; return medico ? medico.id : null;
}; };
const fetchDisponibilidades = useCallback(async (nome) => { const fetchDisponibilidades = useCallback(async (nome) => {
setLoading(true); setLoading(true);
setDisponibilidades([]);
setMedicoEncontradoId(null);
const doctorId = encontrarMedicoIdPorNome(nome); const doctorId = encontrarMedicoIdPorNome(nome);
if (!doctorId) { if (!doctorId) {
setLoading(false); setLoading(false);
return; return;
} }
const url = `${ENDPOINT_LISTAR}?select=*&doctor_id=eq.${doctorId}`;
try { try {
const response = await fetch(url); const res = await fetch(`${ENDPOINT}?doctor_id=eq.${doctorId}`);
const result = await response.json(); const data = await res.json();
let dados = Array.isArray(result) ? result : []; setDisponibilidades(
Array.isArray(data)
setDisponibilidades(dados); ? data
setMedicoEncontradoId(doctorId); : data && Array.isArray(data.items)
} catch (error) { ? data.items
: []
);
} catch (e) {
console.error("Erro ao buscar disponibilidades:", e);
setDisponibilidades([]); setDisponibilidades([]);
} finally { } finally {
setLoading(false); setLoading(false);
@ -77,6 +70,13 @@ const DisponibilidadesDoctorPage = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!gerenciarModo && editando) {
setEditando(null);
}
}, [gerenciarModo]);
useEffect(() => {
if (editando) return;
if (filtroMedicoNome) { if (filtroMedicoNome) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
fetchDisponibilidades(filtroMedicoNome); fetchDisponibilidades(filtroMedicoNome);
@ -85,93 +85,220 @@ const DisponibilidadesDoctorPage = () => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
} else { } else {
setDisponibilidades([]); setDisponibilidades([]);
setMedicoEncontradoId(null);
} }
}, [filtroMedicoNome, fetchDisponibilidades]); }, [filtroMedicoNome, fetchDisponibilidades, editando]);
const rotaGerenciar = medicoEncontradoId const atualizarDisponibilidade = async (id, novoIntervalo) => {
? `../medicos/${medicoEncontradoId}/edit` try {
: `../medicos/novo/edit`; const res = await fetch(`${ENDPOINT}?id=eq.${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slot_minutes: novoIntervalo }),
});
if (res.ok) {
alert("Disponibilidade atualizada com sucesso!");
setEditando(null);
fetchDisponibilidades(filtroMedicoNome);
} else {
alert("Erro ao atualizar disponibilidade");
}
} catch {
alert("Falha ao conectar com o servidor");
}
};
const deletarDisponibilidade = async (id) => {
if (!window.confirm("Deseja realmente excluir esta disponibilidade?"))
return;
try {
const res = await fetch(`${ENDPOINT}?id=eq.${id}`, { method: "DELETE" });
if (res.ok) {
alert("Disponibilidade excluída!");
setDisponibilidades((prev) => prev.filter((d) => d.id !== id));
} else {
alert("Erro ao excluir disponibilidade");
}
} catch {
alert("Erro ao conectar com o servidor");
}
};
const disponibilidadeParaEdicao = editando
? disponibilidades.find((d) => d.id === editando)
: null;
const initialAvailabilityParaEdicao = diasDaSemana.map((dia, weekdayIndex) => {
const blocosDoDia = disponibilidades
.filter(d => d.weekday === weekdayIndex)
.map(d => ({
id: d.id,
inicio: d.start_time
? new Date(d.start_time).toISOString().substring(11, 16)
: "07:00",
termino: d.end_time
? new Date(d.end_time).toISOString().substring(11, 16)
: "17:00",
isNew: false,
slot_minutes: d.slot_minutes,
}));
return {
dia,
isChecked: blocosDoDia.length > 0,
blocos: blocosDoDia,
};
});
const handleUpdateHorarios = (horariosAtualizados) => {
console.log("Horários editados:", horariosAtualizados);
setEditando(null);
fetchDisponibilidades(filtroMedicoNome);
};
return ( return (
<div id="main-content"> <div id="main-content">
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> {/* Cabeçalho */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<h1 style={{ fontSize: "1.5rem", fontWeight: "bold", color: "#333" }}> <h1 style={{ fontSize: "1.5rem", fontWeight: "bold", color: "#333" }}>
Disponibilidades por Médico Disponibilidades por Médico
</h1> </h1>
<Link {/* Botão Voltar/Gerenciar */}
to={rotaGerenciar} <button
onClick={() => {
if (editando) {
setEditando(null); // Se está editando, volta para a tabela
} else {
setGerenciarModo((m) => !m); // Senão, alterna o modo de gerenciamento
}
}}
className="btn-primary" className="btn-primary"
style={{ style={{
padding: "10px 20px", padding: "10px 20px",
fontSize: "14px", fontSize: "14px",
whiteSpace: "nowrap", whiteSpace: "nowrap",
textDecoration: "none",
display: "inline-block",
}} }}
> >
+ Gerenciar Disponibilidades {editando
</Link> ? "← Voltar para Tabela"
: gerenciarModo
? "← Voltar"
: "+ Gerenciar Disponibilidades"}
</button>
</div> </div>
{/* Campo de busca - ESCONDIDO NO MODO DE EDIÇÃO */}
{!editando && (
<div className="atendimento-eprocura"> <div className="atendimento-eprocura">
<div className="busca-atendimento"> <div className="busca-atendimento">
<div style={{ marginRight: '10px' }}>
<i className="fa-solid fa-user-doctor"></i>
<input <input
type="text" type="text"
placeholder="Filtrar por Nome do Médico..." placeholder="Filtrar por Nome do Médico..."
value={filtroMedicoNome} value={filtroMedicoNome}
onChange={(e) => setFiltroMedicoNome(e.target.value)} onChange={(e) => setFiltroMedicoNome(e.target.value)}
style={{ border: "1px solid #ccc", borderRadius: "4px", padding: "5px" }} style={{
border: "1px solid #ccc",
borderRadius: "4px",
padding: "5px",
marginTop: "10px",
marginBottom: "10px",
}}
/> />
</div> </div>
</div> </div>
)}
<section className="calendario-ou-filaespera"> <section className="calendario-ou-filaespera">
<div className="fila-container"> <div className="fila-container">
<h2 className="fila-titulo"> <h2 className="fila-titulo">
Disponibilidades Encontradas ({disponibilidades.length}) {editando
? "Editar Disponibilidade"
: gerenciarModo
? "Gerenciar Disponibilidades"
: "Disponibilidades Encontradas"}{" "}
({disponibilidades.length})
</h2> </h2>
{loading ? ( {loading ? (
<p className="text-center py-10">Carregando disponibilidades...</p> <p>Carregando...</p>
) : (filtroMedicoNome && disponibilidades.length === 0) ? ( ) : disponibilidades.length === 0 ? (
<p className="text-center py-10"> <p>Nenhuma disponibilidade encontrada.</p>
Nenhuma disponibilidade encontrada para o nome buscado. ) : editando ? (
</p> <>
<HorariosDisponibilidade
initialAvailability={initialAvailabilityParaEdicao}
onUpdate={handleUpdateHorarios}
/>
<button
onClick={() =>
handleUpdateHorarios(initialAvailabilityParaEdicao)
}
style={{
marginTop: "20px",
padding: "10px 20px",
fontSize: "16px",
fontWeight: "bold",
borderRadius: "8px",
backgroundColor: "#3b82f6",
color: "white",
border: "none",
cursor: "pointer",
}}
>
Salvar Alterações
</button>
</>
) : ( ) : (
<table className="fila-tabela" style={{ width: "100%", borderCollapse: "collapse" }}> <table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead> <thead>
<tr> <tr>
{[ "Dia da Semana", "Início", "Término", "Intervalo", "Tipo Consulta"].map( <th>Dia da Semana</th>
(header) => ( <th>Início</th>
<th <th>Término</th>
key={header} <th>Intervalo</th>
style={{ padding: "10px", borderBottom: "2px solid #ddd", textAlign: "left" }} <th>Tipo Consulta</th>
> {gerenciarModo && <th>Ações</th>}
{header}
</th>
)
)}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{disponibilidades.map((disp, index) => ( {disponibilidades.map((disp) => (
<tr key={disp.id || index} style={{ borderBottom: "1px solid #eee" }}> <tr key={disp.id}>
<td style={{ padding: "10px", fontSize: "0.9em" }}> <td>{diasDaSemana[disp.weekday]}</td>
{diasDaSemana[disp.weekday] || disp.weekday} <td>{formatarDataHora(disp.start_time)}</td>
<td>{formatarDataHora(disp.end_time)}</td>
<td>{disp.slot_minutes}</td>
<td>{disp.appointment_type}</td>
{gerenciarModo && (
<td>
<button
onClick={() => setEditando(disp.id)}
style={{
backgroundColor: "#10b981",
color: "white",
borderRadius: "6px",
}}
>
Editar
</button>{" "}
<button
onClick={() => deletarDisponibilidade(disp.id)}
style={{
backgroundColor: "#c72f2f",
color: "white",
borderRadius: "6px",
}}
>
Excluir
</button>
</td> </td>
<td style={{ padding: "10px", fontSize: "0.9em" }}> )}
{formatarDataHora(disp.start_time)}
</td>
<td style={{ padding: "10px", fontSize: "0.9em" }}>
{formatarDataHora(disp.end_time)}
</td>
<td style={{ padding: "10px", fontSize: "0.9em" }}>{disp.slot_minutes}</td>
<td style={{ padding: "10px", fontSize: "0.9em" }}>{disp.appointment_type}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -180,7 +307,6 @@ const DisponibilidadesDoctorPage = () => {
</div> </div>
</section> </section>
</div> </div>
</div>
); );
}; };

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,11 +1,13 @@
import React, { useState, useEffect, use } from "react"; import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../components/utils/AuthProvider"; import { useAuth } from "../components/utils/AuthProvider";
import API_KEY from "../components/utils/apiKeys"; import API_KEY from "../components/utils/apiKeys";
import { UserInfos } from "../components/utils/Functions-Endpoints/General"; import { UserInfos } from "../components/utils/Functions-Endpoints/General";
import CabecalhoError from "../components/utils/fetchErros/CabecalhoError";
function Login({ onEnterSystem }) { function Login({ onEnterSystem }) {
const { setAuthTokens } = useAuth(); const { setAuthTokens } = useAuth();
const [showCabecalho, setShowCabecalho ] = useState(false)
const navigate = useNavigate(); const navigate = useNavigate();
const [form, setForm] = useState({ const [form, setForm] = useState({
username: "", username: "",
@ -126,6 +128,9 @@ function Login({ onEnterSystem }) {
} else if (UserData?.roles?.includes("financeiro")) { } else if (UserData?.roles?.includes("financeiro")) {
navigate(`/financeiro/`); navigate(`/financeiro/`);
} }
}else{
console.log("ERROROROROROOR")
setShowCabecalho(true)
} }
} else { } else {
setAlert("Preencha todos os campos!"); setAlert("Preencha todos os campos!");
@ -148,11 +153,7 @@ function Login({ onEnterSystem }) {
<p className="auth-subtitle mb-5"> <p className="auth-subtitle mb-5">
Entre com os dados que você inseriu durante o registro. Entre com os dados que você inseriu durante o registro.
</p> </p>
{alert && ( <CabecalhoError showCabecalho={showCabecalho} message={"E-mail ou senha incorretos."}/>
<div className="alert alert-info" role="alert">
{alert}
</div>
)}
<form onSubmit={handleLogin}> <form onSubmit={handleLogin}>
<div className="form-group position-relative has-icon-left mb-4"> <div className="form-group position-relative has-icon-left mb-4">
<input <input

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 ? (
<div className="name-edit-wrapper">
<input <input
className="profile-name-input" className="profile-name-input"
value={userName} value={userName}
onChange={(e) => setUserName(e.target.value)} onChange={(e) => setUserName(e.target.value)}
onBlur={() => setIsEditingName(false)} onBlur={handleNameSave}
onKeyDown={handleNameKeyDown} onKeyDown={handleNameKeyDown}
autoFocus 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>
{error && (
<div className="error-message">
{error}
</div>
)}
<div className="profile-info">
<p className="profile-email"> <p className="profile-email">
Email: <strong>{userEmail}</strong> <span>Email:</span>
<strong>{userEmail}</strong>
</p> </p>
<p className="profile-role"> <p className="profile-role">
Cargo: <strong>{userRole}</strong> <span>Cargo:</span>
<strong>{userRole}</strong>
</p> </p>
</div>
<div className="profile-actions-row"> <div className="profile-actions">
<button className="btn btn-close" onClick={handleClose}> {avatarUrl && (
Fechar <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>

View File

@ -4,9 +4,11 @@ import API_KEY from "../components/utils/apiKeys";
import { useAuth } from "../components/utils/AuthProvider"; import { useAuth } from "../components/utils/AuthProvider";
import "./style/TablePaciente.css"; import "./style/TablePaciente.css";
import ModalErro from "../components/utils/fetchErros/ModalErro"; import ModalErro from "../components/utils/fetchErros/ModalErro";
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("");
@ -21,10 +23,14 @@ 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);
const [showModalError, setShowModalError] = useState(false); const [showModalError, setShowModalError] = useState("");
const [ErrorInfo, setErrorInfo] = useState({}) const [ErrorInfo, setErrorInfo] = useState({})
@ -103,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')
@ -120,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 => {
@ -136,27 +150,23 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
console.error("ERRO DETALHADO:", errorObject); console.error("ERRO DETALHADO:", errorObject);
throw errorObject; throw errorObject;
}); });
} }
// 3. Se a resposta for OK (2xx), processamos o JSON normalmente
return response.json(); return response.json();
}) })
.then(result => { .then(result => {
// 4. Bloco de SUCESSO
setPacientes(result); setPacientes(result);
console.log("Sucesso:", result); console.log("Sucesso:", result);
// IMPORTANTE: Se o modal estava aberto, feche-o no sucesso
setShowModalError(false); setShowModalError(false);
}) })
.catch(error => { .catch(error => {
// 5. Bloco de ERRO (Captura erros de rede ou o erro lançado pelo 'throw') console.error(error, "deu erro")
//console.error('Falha na requisição:', error.message); manager(setShowModalError, RefreshingToken, setErrorInfo, error)
if(error.httpStatus === 401){
RefreshingToken()
}
setErrorInfo(error)
setShowModalError(true);
}); });
}, [isAuthenticated, getAuthorizationHeader]); }, [isAuthenticated, getAuthorizationHeader]);
@ -195,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) => {
@ -238,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>
@ -296,6 +347,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
<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
@ -400,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 ||
@ -421,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>
@ -439,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">
@ -495,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

@ -427,3 +427,9 @@ html[data-bs-theme="dark"] {
background-color: #f0f0f0; /* Cor ao passar o mouse */ background-color: #f0f0f0; /* Cor ao passar o mouse */
color: #007bff; color: #007bff;
} }
.input-modal{
width: 80%;
}

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 { width: 140px; height: 140px; } .profile-left {
.profile-right { width: 100%; text-align: center; } width: 100%;
}
.avatar-wrapper {
margin: 0 auto;
}
.profile-email,
.profile-role {
justify-content: center;
}
.profile-actions {
justify-content: 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;
} }
@ -234,3 +223,115 @@
.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;
}

View File

@ -9,7 +9,7 @@ import Chat from "../../PagesMedico/Chat";
import DoctorItems from "../../data/sidebar-items-medico.json"; import DoctorItems from "../../data/sidebar-items-medico.json";
import FormNovoRelatorio from "../../PagesMedico/FormNovoRelatorio"; import FormNovoRelatorio from "../../PagesMedico/FormNovoRelatorio";
import EditPageRelatorio from "../../PagesMedico/EditPageRelatorio"; import EditPageRelatorio from "../../PagesMedico/EditPageRelatorio";
// ...existing code... import BotaoVideoChamada from '../../components/BotaoVideoChamada';
function PerfilMedico() { function PerfilMedico() {
return ( return (
@ -27,6 +27,10 @@ function PerfilMedico() {
<Route path="/chat" element={<Chat />} /> <Route path="/chat" element={<Chat />} />
</Routes> </Routes>
</div> </div>
{/* ADICIONADO AQUI */}
<BotaoVideoChamada />
</div> </div>
); );

View File

@ -6,6 +6,10 @@ import LaudoManager from "../../pages/LaudoManager";
import ConsultaCadastroManager from "../../PagesPaciente/ConsultaCadastroManager"; import ConsultaCadastroManager from "../../PagesPaciente/ConsultaCadastroManager";
import ConsultasPaciente from "../../PagesPaciente/ConsultasPaciente"; import ConsultasPaciente from "../../PagesPaciente/ConsultasPaciente";
import ConsultaEditPage from "../../PagesPaciente/ConsultaEditPage"; import ConsultaEditPage from "../../PagesPaciente/ConsultaEditPage";
// 1. IMPORTAÇÃO ADICIONADA
import BotaoVideoPaciente from "../../components/BotaoVideoPaciente";
function PerfilPaciente({ onLogout }) { function PerfilPaciente({ onLogout }) {
const [dadosConsulta, setConsulta] = useState({}) const [dadosConsulta, setConsulta] = useState({})
@ -26,7 +30,12 @@ const [dadosConsulta, setConsulta] = useState({})
<Route path="*" element={<h2>Página não encontrada</h2>} /> <Route path="*" element={<h2>Página não encontrada</h2>} />
</Routes> </Routes>
</div> </div>
{/* 2. COMPONENTE ADICIONADO AQUI */}
<BotaoVideoPaciente />
</div> </div>
); );
} }