Exceções: Buscar médico pelo nome

This commit is contained in:
Eduarda-SS 2025-11-04 16:26:51 -03:00
parent 626fcc8124
commit 0fd21399e1
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
# Cole o token de acesso aqui
WHATSAPP_TOKEN=EAAVZA9C5Lx9IBPjITD8IZCZCeGRBIACX9PInHcNHxuhmp5vK7t40Yn0kc9ZC4YeKx1ZC69tnc1MtcQFWCptQimDvQIIvugiw7BNdi0ak1COfBmIZAMAkzskVkk5qhG9WnMsVmZBEoy9AXcbI53vbqSQooZCCN7LkOhbigZCaZC3VqfLnrmIzKZBC0QhzdSzTpvfQYHocDAzCS8ejf2o6WVSXYlqJEOuLzFEkvtGR6eLvNQi6QZDZD
WHATSAPP_TOKEN=EAAVZA9C5Lx9IBP0kF76Yy5GJquZCOkQZCtnsLDYJZCLRfZA7BrOsZBPBk7BODsDuU1r5qYNu5vsRFlI1tNZBlnQpWXsZCZBrkqTygGphqQLZCvikGDyZBEFEyknkWM9oadz1xVtAA65JKXFbGFIJWhmFMOgauWXZC072CSkApe5UZCVGZCZAqc5we1TqCcFBvLqWnUexosBRIEb8kSThWlEDheHNoP7MrjwNcYaNBczmFmhq9aPqKm6jCgjwqjZBI0jVLjdooKkZCanaz9ZA3ZBIfNbyq8FOYUI
# Cole o ID do número de telefone aqui
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": {
"@ckeditor/ckeditor5-build-classic": "^41.4.2",
"@ckeditor/ckeditor5-react": "^11.0.0",
"@jitsi/react-sdk": "^1.4.0",
"@sweetalert2/theme-dark": "^5.0.27",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
@ -18,10 +19,15 @@
"apexcharts": "^5.3.4",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"cors": "^2.8.5",
"dayjs": "^1.11.18",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"flatpickr": "^4.6.13",
"html2pdf.js": "^0.12.1",
"lucide-react": "^0.543.0",
"node-fetch": "^3.3.2",
"openai": "^6.7.0",
"perfect-scrollbar": "^1.5.6",
"powershell": "^2.3.3",
"quill": "^2.0.3",
@ -33,6 +39,7 @@
"react-flatpickr": "^4.0.11",
"react-icons": "^5.5.0",
"react-input-mask": "^2.0.4",
"react-is": "^19.2.0",
"react-quill": "^2.0.0",
"react-router-dom": "^7.9.2",
"react-scripts": "5.0.1",
@ -72,5 +79,8 @@
"sass": "^1.91.0",
"sass-loader": "^16.0.5",
"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 { Link } from 'react-router-dom';
import { useState, useEffect } from 'react';
@ -8,56 +7,96 @@ import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js';
import TiptapViewer from './TiptapViewer';
import './styleMedico/DoctorRelatorioManager.css';
const DoctorRelatorioManager = () => {
const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth();
const authHeader = getAuthorizationHeader();
const [RelatoriosFiltrados, setRelatorios] = useState([]);
const [PacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [MedicosComRelatorios, setMedicosComRelatorios] = useState([]);
let authHeader = getAuthorizationHeader();
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
const [pacientesData, setPacientesData] = useState({});
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [showModal, setShowModal] = useState(false);
const [index, setIndex] = useState();
const [relatorioModal, setRelatorioModal] = useState(null);
const [termoPesquisa, setTermoPesquisa] = useState('');
const [filtroExame, setFiltroExame] = useState('');
const [examesDisponiveis, setExamesDisponiveis] = useState([]);
const [modalIndex, setModalIndex] = useState(0);
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const totalPaginas = Math.ceil(relatoriosFinais.length / itensPorPagina);
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal);
// busca lista de relatórios
useEffect(() => {
let mounted = true;
const fetchReports = async () => {
try {
var myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
myHeaders.append('Authorization', authHeader);
if (authHeader) myHeaders.append('Authorization', authHeader);
var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions);
const data = await res.json();
setRelatorios(data || []);
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) {
console.error('Erro listar relatórios', err);
setRelatorios([]);
if (mounted) {
setRelatoriosOriginais([]);
setRelatoriosFiltrados([]);
setRelatoriosFinais([]);
}
}
};
fetchReports();
const refreshHandler = () => fetchReports();
window.addEventListener('reports:refresh', refreshHandler);
return () => {
mounted = false;
window.removeEventListener('reports:refresh', refreshHandler);
};
}, [authHeader]);
// depois que RelatoriosFiltrados mudar, busca pacientes e médicos correspondentes
useEffect(() => {
const fetchRelData = async () => {
const pacientes = [];
const medicos = [];
for (let i = 0; i < RelatoriosFiltrados.length; i++) {
const rel = RelatoriosFiltrados[i];
// paciente
for (let i = 0; i < relatoriosFiltrados.length; i++) {
const rel = relatoriosFiltrados[i];
try {
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
} catch (err) {
pacientes.push(null);
}
// médico: tenta created_by ou requested_by id se existir
try {
const doctorId = rel.created_by || rel.requested_by || null;
if (doctorId) {
// se created_by é id (uuid) usamos GetDoctorByID, senão se requested_by for nome, guardamos nome
const docRes = await GetDoctorByID(doctorId, authHeader);
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
} else {
@ -70,55 +109,117 @@ const DoctorRelatorioManager = () => {
setPacientesComRelatorios(pacientes);
setMedicosComRelatorios(medicos);
};
if (RelatoriosFiltrados.length > 0) fetchRelData();
if (relatoriosFiltrados.length > 0) fetchRelData();
else {
setPacientesComRelatorios([]);
setMedicosComRelatorios([]);
}
}, [RelatoriosFiltrados, authHeader]);
}, [relatoriosFiltrados, authHeader]);
const BaixarPDFdoRelatorio = (nome_paciente) => {
const elemento = document.getElementById("folhaA4");
const opt = { margin: 0, filename: `relatorio_${nome_paciente || "paciente"}.pdf`, html2canvas: { scale: 2 }, jsPDF: { unit: "mm", format: "a4", orientation: "portrait" } };
const abrirModal = (relatorio, index) => {
setRelatorioModal(relatorio);
setModalIndex(index);
setShowModal(true);
};
// Função para limpar filtros
const limparFiltros = () => {
setTermoPesquisa('');
setFiltroExame('');
setRelatoriosFinais(relatoriosOriginais);
};
const BaixarPDFdoRelatorio = (nome_paciente, idx) => {
const 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();
};
const irParaPagina = (pagina) => {
setPaginaAtual(pagina);
};
const avancarPagina = () => {
if (paginaAtual < totalPaginas) {
setPaginaAtual(paginaAtual + 1);
}
};
const voltarPagina = () => {
if (paginaAtual > 1) {
setPaginaAtual(paginaAtual - 1);
}
};
const gerarNumerosPaginas = () => {
const paginas = [];
const paginasParaMostrar = 5;
let inicio = Math.max(1, paginaAtual - Math.floor(paginasParaMostrar / 2));
let fim = Math.min(totalPaginas, inicio + paginasParaMostrar - 1);
inicio = Math.max(1, fim - paginasParaMostrar + 1);
for (let i = inicio; i <= fim; i++) {
paginas.push(i);
}
return paginas;
};
return (
<div>
{showModal && (
<div className="modal">
<div className="modal-dialog modal-tabela-relatorio">
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowModal(false)}>
<div className="modal-dialog modal-dialog-square" role="document" onClick={(e) => e.stopPropagation()}>
<div className="modal-content">
<div className="modal-header text-white">
<h5 className="modal-title ">Relatório de {PacientesComRelatorios[index]?.full_name}</h5>
<button type="button" className="btn-close" onClick={() => setShowModal(false)}></button>
<div className="modal-header custom-modal-header">
<h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name}</h5>
<button type="button" className="btn-close modal-close-btn" aria-label="Close" onClick={() => setShowModal(false)}></button>
</div>
<div className="modal-body">
<div id="folhaA4">
<div id='header-relatorio'>
<p>Clinica Rise up</p>
<p>Dr - CRM/SP 123456</p>
<p>Avenida - (79) 9 4444-4444</p>
<div id={`folhaA4-${modalIndex}`} className="folhaA4">
<div id='header-relatorio' style={{ textAlign: 'center', marginBottom: 24 }}>
<p style={{ margin: 0 }}>Clinica Rise up</p>
<p style={{ margin: 0 }}>Dr - CRM/SP 123456</p>
<p style={{ margin: 0 }}>Avenida - (79) 9 4444-4444</p>
</div>
<div id='infoPaciente'>
<p>Paciente: {PacientesComRelatorios[index]?.full_name}</p>
<p>Data de nascimento: {PacientesComRelatorios[index]?.birth_date}</p>
<p>Data do exame: {RelatoriosFiltrados[index]?.due_at || ''}</p>
{/* Exibe conteúdo salvo (content_html) */}
<p style={{ marginTop: '15px', fontWeight: 'bold' }}>Conteúdo do Relatório:</p>
<TiptapViewer htmlContent={RelatoriosFiltrados[index]?.content_html || RelatoriosFiltrados[index]?.content || 'Relatório não preenchido.'} />
<div id='infoPaciente' style={{ padding: '0 6px' }}>
<p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name}</p>
<p><strong>Data de nascimento:</strong> {pacientesComRelatorios[modalIndex]?.birth_date || '—'}</p>
<p><strong>Data do exame:</strong> {relatoriosFiltrados[modalIndex]?.due_at || '—'}</p>
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
<div className="tiptap-viewer-wrapper">
<TiptapViewer htmlContent={relatoriosFiltrados[modalIndex]?.content_html || relatoriosFiltrados[modalIndex]?.content || 'Relatório não preenchido.'} />
</div>
</div>
<div>
<p>Dr {MedicosComRelatorios[index]?.full_name || RelatoriosFiltrados[index]?.requested_by}</p>
<p>Emitido em: {RelatoriosFiltrados[index]?.created_at || '—'}</p>
<div style={{ marginTop: 20, padding: '0 6px' }}>
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFiltrados[modalIndex]?.requested_by}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatoriosFiltrados[modalIndex]?.created_at || '—'}</p>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name)}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button>
<button type="button" className="btn btn-primary" onClick={() => { setShowModal(false) }}>Fechar</button>
<div className="modal-footer custom-modal-footer">
<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>
@ -133,14 +234,51 @@ const DoctorRelatorioManager = () => {
<div className="card-header d-flex justify-content-between align-items-center">
<h4 className="card-title mb-0">Relatórios Cadastrados</h4>
<Link to={'criar'}>
<button className="btn btn-primary"><i className="bi bi-plus-circle"></i> Adicionar Relatório</button>
<button className="btn btn-primary">
<i className="bi bi-plus-circle"></i> Adicionar Relatório
</button>
</Link>
</div>
<div className="card-body">
<div className="card p-3 mb-3">
<h5 className="mb-3"><i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros</h5>
<div className="d-flex flex-nowrap align-items-center gap-2" style={{ overflowX: "auto", paddingBottom: "6px" }}>
<input type="text" className="form-control" placeholder="Buscar por nome..." style={{ minWidth: 250, maxWidth: 300, width: 260, flex: "0 0 auto" }} />
<h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros
</h5>
<div className="row">
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Buscar por nome ou CPF do paciente</label>
<input
type="text"
className="form-control"
placeholder="Digite nome ou CPF do paciente..."
value={termoPesquisa}
onChange={(e) => setTermoPesquisa(e.target.value)}
/>
</div>
</div>
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Filtrar por tipo de exame</label>
<input
type="text"
className="form-control"
placeholder="Digite o tipo de exame..."
value={filtroExame}
onChange={(e) => setFiltroExame(e.target.value)}
/>
</div>
</div>
<div className="col-md-2 d-flex align-items-end">
<button className="btn btn-outline-secondary w-100" onClick={limparFiltros}>
<i className="bi bi-arrow-clockwise"></i> Limpar
</button>
</div>
</div>
<div className="mt-2">
<div className="contador-relatorios">
{relatoriosFinais.length} DE {relatoriosOriginais.length} RELATÓRIOS ENCONTRADOS
</div>
</div>
</div>
@ -149,36 +287,91 @@ const DoctorRelatorioManager = () => {
<thead>
<tr>
<th>Paciente</th>
<th>Doutor</th>
<th>CPF</th>
<th>Exame</th>
<th></th>
</tr>
</thead>
<tbody>
{RelatoriosFiltrados.length > 0 ? (
RelatoriosFiltrados.map((relatorio, idx) => (
<tr key={relatorio.id}>
<td className='infos-paciente'>{PacientesComRelatorios[idx]?.full_name}</td>
<td className='infos-paciente'>{MedicosComRelatorios[idx]?.full_name || relatorio.requested_by || '-'}</td>
<td>
<div className="d-flex gap-2">
<button className="btn btn-sm" style={{ backgroundColor: "#E6F2FF", color: "#004085" }} onClick={() => { setShowModal(true); setIndex(idx); }}>
<i className="bi bi-eye me-1"></i> Ver Detalhes
</button>
<button className="btn btn-sm" style={{ backgroundColor: "#FFF3CD", color: "#856404" }} onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<i className="bi bi-pencil me-1"></i> Editar
</button>
</div>
</td>
</tr>
))
{relatoriosPaginados.length > 0 ? (
relatoriosPaginados.map((relatorio, index) => {
const paciente = pacientesData[relatorio.patient_id];
return (
<tr key={relatorio.id}>
<td>{paciente?.full_name || 'Carregando...'}</td>
<td>{paciente?.cpf || 'Carregando...'}</td>
<td>{relatorio.exam}</td>
<td>
<div className="d-flex gap-2">
<button className="btn btn-sm btn-ver-detalhes" onClick={() => abrirModal(relatorio, index)}>
<i className="bi bi-eye me-1"></i> Ver Detalhes
</button>
<button className="btn btn-sm btn-editar" onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<i className="bi bi-pencil me-1"></i> Editar
</button>
</div>
</td>
</tr>
);
})
) : (
<tr><td colSpan="8" className="text-center">Nenhum paciente encontrado.</td></tr>
<tr><td colSpan="4" className="text-center">Nenhum relatório encontrado.</td></tr>
)}
</tbody>
</table>
</div>
{relatoriosFinais.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, relatoriosFinais.length)} de {relatoriosFinais.length} itens
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div>
</div>
</div>
</div>
@ -188,4 +381,4 @@ const DoctorRelatorioManager = () => {
);
};
export default DoctorRelatorioManager;
export default DoctorRelatorioManager;

View File

@ -1,4 +1,4 @@
// EditPageRelatorio.jsx
// src/PagesMedico/EditPageRelatorio.jsx
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys';
@ -52,7 +52,7 @@ const EditPageRelatorio = () => {
try {
const myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
if (authHeader) myHeaders.append("Authorization", authHeader);
const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
// Pega relatório por id (supabase geralmente retorna array para ?id=eq.X)
@ -101,12 +101,14 @@ const EditPageRelatorio = () => {
try {
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
myHeaders.append('Authorization', authHeader);
if (authHeader) myHeaders.append('Authorization', authHeader);
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 });
// supabase: PATCH via query id=eq.<id>
const res = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${params.id}`, {
method: 'PATCH',
headers: myHeaders,
@ -114,13 +116,29 @@ const EditPageRelatorio = () => {
});
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);
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!');
navigate('/medico/relatorios');
} catch (err) {
console.error(err);
alert('Erro ao salvar. Veja console.');
@ -149,5 +167,4 @@ 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 { useNavigate } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys';
@ -39,7 +40,7 @@ const FormNovoRelatorio = () => {
const doctorRef = useRef();
useEffect(() => {
// carregar pacientes
// carregar pacientes e médicos
let mounted = true;
const loadPatients = async () => {
setLoadingPatients(true);
@ -109,7 +110,7 @@ const FormNovoRelatorio = () => {
patient_id: patient.id,
patient_name: patient.full_name || '',
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('');
setShowPatientDropdown(false);
@ -120,7 +121,7 @@ const FormNovoRelatorio = () => {
...prev,
doctor_id: doctor.id,
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('');
setShowDoctorDropdown(false);
@ -137,7 +138,7 @@ const FormNovoRelatorio = () => {
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) => {
e.preventDefault();
if (!form.patient_id) return alert('Selecione o paciente (clicando no item) antes de salvar.');
@ -146,35 +147,51 @@ const FormNovoRelatorio = () => {
try {
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
myHeaders.append('Authorization', authHeader);
if (authHeader) myHeaders.append('Authorization', authHeader);
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,
content: form.contentHtml,
content_html: form.contentHtml,
requested_by: form.doctor_name || '',
created_by: form.doctor_id || null,
status: 'draft'
});
requested_by: form.doctor_name || ''
};
// 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', {
method: 'POST',
headers: myHeaders,
body,
body: JSON.stringify(payload),
});
if (!res.ok) {
const txt = await res.text();
// tenta ler JSON, se não for JSON lê o texto
let txt;
try {
txt = await res.json();
} catch (err) {
txt = await res.text();
}
console.error('Erro POST criar relatório:', res.status, txt);
// mostra mensagem mais útil
return alert(`Erro ao criar relatório (ver console). Status ${res.status}`);
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!');
navigate('/medico/relatorios');
} 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.');
}
};
@ -185,7 +202,7 @@ const FormNovoRelatorio = () => {
<form onSubmit={handleSubmit} className="card p-4 mb-4">
<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>
<input
className="form-control"
@ -195,7 +212,7 @@ const FormNovoRelatorio = () => {
onFocus={() => setShowPatientDropdown(true)}
/>
{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 => (
<li key={p.id} className="list-group-item list-group-item-action" onClick={() => choosePatient(p)}>
{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>
<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>
<input
className="form-control"
@ -216,7 +233,7 @@ const FormNovoRelatorio = () => {
onFocus={() => setShowDoctorDropdown(true)}
/>
{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 => (
<li key={d.id} className="list-group-item list-group-item-action" onClick={() => chooseDoctor(d)}>
{d.full_name} {d.crm ? `- CRM ${d.crm}` : ''}

View File

@ -12,4 +12,4 @@ const TiptapViewer = ({ htmlContent }) => {
);
};
export default TiptapViewer;
export default TiptapViewer;

View File

@ -112,4 +112,4 @@ function Relatorio() {
);
}
export default Relatorio;
export default Relatorio;

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 {
width: 210mm;
min-height: 207mm;
padding: 20mm;
margin: 10mm auto;
border: 1px solid #ccc;
background: white;
/* src/PagesMedico/styleMedico/FormNovoRelatorio.css */
/* --- Modal centralizada e quadrada (ajustada para ser mais larga e sem quadrado branco no botão fechar) --- */
/* backdrop + center */
.modal.modal-centered {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(10, 20, 30, 0.45);
z-index: 2000;
padding: 20px;
}
#primeiraLinha{
display: flex;
flex-direction: row;
gap: 20px;
margin-bottom: 20px;
/* dialog box — maior horizontalmente e altura automática para scroll interno */
.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;
}
input,textarea,label{
font-size: 1.1rem;
/* caixa branca que contém o conteúdo - ocupa 100% da dialog */
.modal-dialog.modal-dialog-square .modal-content {
width: 100%;
height: auto;
border-radius: 12px;
box-shadow: 0 12px 30px rgba(11,22,35,0.18);
overflow: hidden;
display: flex;
flex-direction: column;
background: #fff;
}
textarea{
width: 100%;
height: 100px;
/* header */
.custom-modal-header {
position: relative;
background: linear-gradient(90deg, #203B75 0%, #274A8A 100%);
color: #fff;
padding: 14px 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.submitButton{
display: flex;
margin-left: auto;
height:50% ;
padding: 8px 20px;
font-size: medium;
.custom-modal-header .modal-title {
margin: 0;
font-size: 1.05rem;
font-weight: 700;
}
.bi-download{
font-size: 1.2rem;
margin-right: 5px;
font-weight: bold;
/* 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;
}
#infoPaciente{
margin-top: 50px;
margin-bottom: 40px;
.modal-close-btn::after {
content: '✕';
color: #fff;
font-weight: 700;
font-size: 16px;
}
#header-relatorio{
text-align: center;
margin-bottom: 30px;
/* body - faz scroll interno se for longo */
.modal-body {
padding: 18px;
overflow: auto;
flex: 1 1 auto;
}
.info-paciente{
font-weight: bold;
/* 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;
}
#infoPaciente p {
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 { UserInfos } from '../components/utils/Functions-Endpoints/General'
import FormConsultaPaciente from './FormConsultaPaciente'
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor'
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 [idUsuario, setIDusuario] = useState("6e7f8829-0574-42df-9290-8dbb70f75ada")
const { getAuthorizationHeader } = useAuth()
const authHeader = getAuthorizationHeader();
const [DictInfo, setDict] = useState({})
const [Medico, setMedico] = useState({})
const [Paciente, setPaciente] = useState([])
useEffect(() => {
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 [idUsuario, setIDusuario] = useState(null);
const [DictInfo, setDict] = useState({});
const [Medico, setMedico] = useState(null);
const [Paciente, setPaciente] = useState(null);
useEffect(() => {
setDict({ ...dadosConsulta });
const fetchInitialData = async () => {
if (dadosConsulta.doctor_id) {
const medicoData = await GetDoctorByID(dadosConsulta.doctor_id, authHeader);
setMedico(medicoData[0]);
}
const ColherInfoUsuario =async () => {
const result = await UserInfos(authHeader)
setIDusuario(result?.profile?.id)
if (dadosConsulta.patient_id) {
const pacienteData = await GetPatientByID(dadosConsulta.patient_id, authHeader);
setPaciente(pacienteData[0]);
}
ColherInfoUsuario()
fetchMedicoePaciente()
}, [])
};
useEffect(() => {
setDict({...DictInfo, medico_nome:Medico?.full_name, dataAtendimento:dadosConsulta.scheduled_at?.split("T")[0]})
}, [Medico])
let authHeader = getAuthorizationHeader()
const handleSave = (DictParaPatch) => {
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append('apikey', API_KEY)
myHeaders.append("authorization", authHeader)
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
});
var requestOptions = {
method: 'PATCH',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${DictInfo.id}`, requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
const fetchUserInfo = async () => {
const result = await UserInfos(authHeader);
setIDusuario(result?.profile?.id);
};
fetchUserInfo();
fetchInitialData();
}, [dadosConsulta, authHeader]);
useEffect(() => {
if (Medico) {
setDict(prevDict => ({
...prevDict,
medico_nome: Medico?.full_name,
dataAtendimento: dadosConsulta.scheduled_at?.split("T")[0]
}));
}
return (
<div>
<FormConsultaPaciente agendamento={DictInfo} setAgendamento={setDict} onSave={handleSave}/>
</div>
)
}, [Medico, dadosConsulta.scheduled_at]);
const handleSave = async (DictParaPatch) => {
try {
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append('apikey', API_KEY);
myHeaders.append("authorization", authHeader);
myHeaders.append('Prefer', 'return=representation');
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 = {
method: 'PATCH',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${DictInfo.id}`, requestOptions);
if (!response.ok) {
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 (
<div>
{}
<FormConsultaPaciente
agendamento={DictInfo}
setAgendamento={setDict}
onSave={handleSave}
onCancel={handleCancel}
/>
</div>
)
}
export default ConsultaEditPage
export default ConsultaEditPage;

View File

@ -6,150 +6,142 @@ import { useEffect, useState } from 'react'
import API_KEY from '../components/utils/apiKeys'
import { useAuth } from '../components/utils/AuthProvider'
const ConsultasPaciente = ({setConsulta}) => {
const {getAuthorizationHeader} = useAuth()
const ConsultasPaciente = ({ setConsulta }) => {
const { getAuthorizationHeader } = useAuth()
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [selectedID, setSelectedId] = useState("")
let authHeader = getAuthorizationHeader()
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [selectedID, setSelectedId] = useState("")
let authHeader = getAuthorizationHeader()
const [consultas, setConsultas] = useState([])
const [consultas, setConsultas] = useState([])
const FiltrarAgendamentos = (agendamentos, id) => {
if (!agendamentos || !Array.isArray(agendamentos)) {
console.error("A lista de agendamentos é inválida.");
setConsultas([]);
return;
}
const FiltrarAgendamentos = (agendamentos, id) => {
// Verifica se a lista de agendamentos é válida antes de tentar filtrar
if (!agendamentos || !Array.isArray(agendamentos)) {
console.error("A lista de agendamentos é inválida.");
setConsultas([]); // Garante que setConsultas receba uma lista vazia
return;
const consultasFiltradas = agendamentos.filter(agendamento => {
return agendamento.patient_id && agendamento.patient_id.toString() === id.toString();
});
console.log(consultasFiltradas)
setConsultas(consultasFiltradas);
}
// 1. Filtragem
// O método .filter() cria uma nova lista contendo apenas os itens que retornarem 'true'
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();
});
useEffect(() => {
const fetchConsultas = async () => {
try {
const myHeaders = new Headers();
myHeaders.append("Authorization", authHeader);
myHeaders.append("apikey", API_KEY)
// 2. Adicionar a lista no setConsultas
console.log(consultasFiltradas)
setConsultas(consultasFiltradas);
}
const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
// Exemplo de como você chamaria (assumindo que DadosAgendamento é sua lista original):
// FiltrarAgendamentos(DadosAgendamento, Paciente.id);
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select=*", requestOptions);
const result = await response.json();
FiltrarAgendamentos(result, "6e7f8829-0574-42df-9290-8dbb70f75ada");
} catch (error) {
console.log('error', error);
}
};
useEffect(() => {
var myHeaders = new Headers();
myHeaders.append("Authorization", authHeader);
myHeaders.append("apikey", API_KEY)
fetchConsultas();
}, [authHeader]);
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
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(result => {FiltrarAgendamentos(result, "6e7f8829-0574-42df-9290-8dbb70f75ada" )})
.catch(error => console.log('error', error));
}, [])
const navigate = useNavigate()
const deleteConsulta= (ID) => {
var myHeaders = new Headers();
const navigate = useNavigate()
const deleteConsulta = async (ID) => {
try {
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append('apikey', API_KEY)
myHeaders.append("authorization", authHeader)
var raw = JSON.stringify({ "status":"cancelled"
});
var requestOptions = {
method: 'PATCH',
headers: myHeaders,
body: raw,
redirect: 'follow'
myHeaders.append('apikey', API_KEY);
myHeaders.append("authorization", authHeader);
const raw = JSON.stringify({ "status": "cancelled" });
const requestOptions = {
method: 'PATCH',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${selectedID}`, 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)
}
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${ID}`, requestOptions);
return (
<div>
<h1> Gerencie suas consultas</h1>
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));
<div className='form-container'>
<button className="btn btn-primary" onClick={() => {navigate("criar")}}>
<i className="bi bi-plus-circle"></i> Adicionar Consulta
console.log("Consulta cancelada com sucesso!");
alert("Consulta cancelada com sucesso!");
</button>
} catch (error) {
console.error('Erro ao cancelar a consulta:', error);
alert('Erro ao cancelar a consulta. Veja o console.');
}
}
<h2>Seus proximos atendimentos</h2>
return (
<div>
<h1> Gerencie suas consultas</h1>
{consultas.map((consulta) => (
<CardConsultaPaciente consulta={consulta} setConsulta={setConsulta} setShowDeleteModal={setShowDeleteModal} setSelectedId={ setSelectedId}/>
))}
{showDeleteModal &&
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header bg-danger bg-opacity-25">
<h5 className="modal-title text-danger">
Confirmação de Exclusão
</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
></button>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
Tem certeza que deseja excluir este agendamento?
</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() => setShowDeleteModal(false)}
>
Cancelar
<div className='form-container'>
<button className="btn btn-primary" onClick={() => { navigate("criar") }}>
<i className="bi bi-plus-circle"></i> Adicionar Consulta
</button>
<button
type="button"
className="btn btn-danger"
onClick={() => {deleteConsulta(selectedID);setShowDeleteModal(false)}}
>
<i className="bi bi-trash me-1"></i> Excluir
</button>
</div>
<h2>Seus próximos atendimentos</h2>
{consultas.map((consulta) => (
<CardConsultaPaciente key={consulta.id} consulta={consulta} setConsulta={setConsulta} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} />
))}
{showDeleteModal &&
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header bg-danger bg-opacity-25">
<h5 className="modal-title text-danger">
Confirmação de Exclusão
</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
></button>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
Tem certeza que deseja excluir este agendamento?
</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() => setShowDeleteModal(false)}
>
Cancelar
</button>
<button
type="button"
className="btn btn-danger"
onClick={() => { deleteConsulta(selectedID); setShowDeleteModal(false) }}
>
<i className="bi bi-trash me-1"></i> Excluir
</button>
</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 { useMemo } from 'react';
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 {getAuthorizationHeader} = useAuth()
const authHeader = getAuthorizationHeader()
const [Paciente, setPaciente] = useState()
const [Medico, setMedico] = useState()
const [decidirBotton, setDecidirBotton] = useState("")
const ids = useMemo(() => {
return {
doctor_id: DadosConsulta?.doctor_id,
@ -45,23 +51,35 @@ const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, se
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 (
<div className={`container-cardconsulta container-cardconsulta-${TabelaAgendamento}`}>
{DadosConsulta.id?
<div className={`container-cardconsulta container-cardconsulta-${TabelaAgendamento} ` }>
<div className='cardconsulta' id={`status-card-consulta-${DadosConsulta.status}`}>
{DadosConsulta.id?
<div className={`cardconsulta`} id={indice_cor !== -1 ? `status-card-consulta-${coresConsultas[indice_cor]}` : `status-card-consulta-${DadosConsulta.status}`}>
<div>
<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 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>
</div>
@ -75,16 +93,35 @@ const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, se
>
<i className="bi bi-pencil me-1"></i>
</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
className="btn btn-sm btn-delete-custom-style "
onClick={() => {
console.log(DadosConsulta.id)
setSelectedId(DadosConsulta.id);
setShowDeleteModal(true);
}}
>
<i className="bi bi-trash me-1"></i>
</button>
}
</div>
</div>
@ -93,6 +130,8 @@ const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, se
}
</div>
)

View File

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

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import CardConsulta from './CardConsulta';
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 [Dia, setDia] = useState()
const agendamentosDoDia = agendamentos?.semana1?.segunda || [];
@ -10,8 +10,7 @@ const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDel
let ListaDiasComAgendamentos = Object.keys(agendamentos)
console.log(agendamentos)
//console.log(Dia, "hshdhshhsdhs")
@ -19,6 +18,28 @@ const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDel
setDia(ListaDiasComAgendamentos[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 (
<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>
<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>
</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='mostrar-horario'>
<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>
</td>
</tr>

View File

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

View File

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

View File

@ -50,7 +50,7 @@
/* 6. Estilo de hover para o botão de exclusão */
.btn-delete-custom-style: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) */
@ -58,4 +58,15 @@
.action-button .bi {
/* Exemplo: se precisar de um ajuste fino além do fs-4 */
/* 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-size: 20px;
color:black
}
.form-container {
@ -152,7 +150,6 @@ svg{
background: #e5e7eb;
}
.cardconsulta-infosecundaria{
font-size: small;
}
@ -166,10 +163,8 @@ svg{
.campo-de-input{
display: flex;
flex-direction: column;
}
#informacoes-atendimento-segunda-linha{
margin-top: 10px;
display: flex;
@ -185,13 +180,13 @@ textarea{
.campos-informacoes-paciente,
.campo-informacoes-atendimento {
display: flex;
gap: 16px; /* espaço entre campos */
gap: 16px;
}
.campo-de-input {
flex: 1; /* todos os filhos ocupam mesmo espaço */
flex: 1;
display: flex;
flex-direction: column; /* mantém label em cima do input */
flex-direction: column;
}
#informacoes-atendimento-segunda-linha-esquerda select[name="unidade"]{
@ -213,7 +208,7 @@ select[name=solicitante]{
.form-container {
width: 100%;
max-width: none;
margin: 0; /* >>> sem espaço para encostar no topo <<< */
margin: 0;
background: #ffffff;
border-radius: 12px;
padding: 24px;
@ -306,169 +301,219 @@ html[data-bs-theme="dark"] svg {
color: #e0e0e0 !important;
}
/* CONTAINER PAI - ESSENCIAL PARA POSICIONAMENTO */
.campo-de-input-container {
position: relative; /* Define o contexto para o dropdown */
/* ... outros estilos de layout (display, margin, etc.) ... */
position: relative;
}
/* ESTILO DA LISTA DROPDOWN */
.dropdown-profissionais {
position: absolute; /* Flutua em relação ao pai (.campo-de-input-container) */
top: 100%; /* Começa logo abaixo do input */
position: absolute;
top: 100%;
left: 0;
width: 100%; /* Ocupa toda a largura do container pai */
/* Estilos visuais */
width: 100%;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 100; /* Alto z-index para garantir que fique acima de outros elementos */
z-index: 100;
max-height: 200px;
overflow-y: auto;
overflow-y: auto;
}
/* ESTILO DE CADA ITEM DO DROPDOWN */
.dropdown-item {
padding: 10px;
cursor: pointer;
}
.dropdown-item:hover {
background-color: #f0f0f0;
background-color: #f0f0f0;
}
.tipo_atendimento{
margin-left: 3rem;
}
/* 1. Estilização Básica e Tamanho (Estado Padrão - Antes de Clicar) */
.checkbox-customs {
/* Remove a aparência padrão do navegador/Bootstrap */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/* Define o tamanho desejado */
width: 1.2rem; /* Ajuste conforme o seu gosto (ex: 1.2rem = 19.2px) */
width: 1.2rem;
height: 1.2rem;
/* Define o visual "branco com borda preta" */
background-color: #fff; /* Fundo branco */
border: 1px solid #000; /* Borda preta de 1px */
border-radius: 0.25rem; /* Borda levemente arredondada (opcional, imita Bootstrap) */
/* Centraliza o 'check' (quando aparecer) */
background-color: #fff;
border: 1px solid #000;
border-radius: 0.25rem;
display: inline-block;
vertical-align: middle;
cursor: pointer; /* Indica que é clicável */
/* Adiciona a transição suave */
transition: all 0.5s ease; /* Transição em 0.5 segundos para todas as propriedades */
cursor: pointer;
transition: all 0.5s ease;
}
/* 2. Estilização no Estado Clicado (:checked) */
.checkbox-customs:checked {
/* Quando clicado, mantém o fundo branco (se quiser mudar, altere aqui) */
background-color: #fff;
/* Se você quiser que a borda mude de cor ao clicar, altere aqui. */
/* border-color: #007bff; */ /* Exemplo: borda azul */
}
/* 3. Ocultar o 'Check' Padrão e Criar um Check Customizado */
/* O Bootstrap/Navegador insere um ícone de 'check'. Vamos controlá-lo com background-image. */
.checkbox-customs:checked {
/* Este código do Bootstrap usa um SVG para o ícone de 'check' */
/* Aqui, estamos forçando o ícone de 'check' a ser preto para combinar com a borda preta. */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
/* Garante que o ícone fique centralizado e preencha o espaço */
.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-position: center;
background-repeat: no-repeat;
}
/* Container dos três elementos na linha */
.linha {
display: flex;
align-items: flex-end; /* Garante que os campos de input e o seletor fiquem alinhados pela base */
gap: 20px; /* Espaçamento entre os campos */
align-items: flex-end;
gap: 20px;
}
/* ------------------------------------------- */
/* ESTILIZAÇÃO DO SELETOR DE SESSÕES */
/* ------------------------------------------- */
.seletor-wrapper {
/* Garante que o label e o contador fiquem alinhados verticalmente com os selects */
display: flex;
flex-direction: column;
}
.sessao-contador {
/* Estilo de "campo de input" */
display: flex;
align-items: center;
justify-content: space-between;
/* Cores e Bordas */
background-color: #e9ecef; /* Cor cinza claro dos inputs do Bootstrap */
border: 1px solid #ced4da; /* Borda sutil */
border-radius: 0.25rem; /* Bordas arredondadas (Padrão Bootstrap) */
/* Garante a mesma altura dos selects */
height: 40px; /* Ajuste este valor para corresponder à altura exata do seu select */
width: 100px; /* Largura ajustável */
padding: 0 5px; /* Padding interno */
background-color: #e9ecef;
border: 1px solid #ced4da;
border-radius: 0.25rem;
height: 40px;
width: 100px;
padding: 0 5px;
font-size: 1rem;
font-weight: 500;
}
.sessao-valor {
/* Estilo do número de sessões */
margin: 0;
padding: 0 5px;
font-size: 1.1rem; /* Um pouco maior que o texto dos selects */
color: #007bff; /* Cor azul destacada (como na sua imagem) */
font-size: 1.1rem;
color: #007bff;
}
.sessao-contador button {
/* Estilo dos botões de chevron */
background: none;
border: none;
cursor: pointer;
padding: 0 2px;
color: #495057; /* Cor do ícone */
font-size: 1.5rem; /* Aumenta o tamanho dos ícones do chevron */
line-height: 1; /* Alinha o ícone verticalmente */
color: #495057;
font-size: 1.5rem;
line-height: 1;
transition: color 0.2s;
}
.sessao-contador button:hover:not(:disabled) {
color: #007bff; /* Cor azul ao passar o mouse */
color: #007bff;
}
.sessao-contador button:disabled {
cursor: not-allowed;
color: #adb5bd; /* Cor mais clara quando desabilitado */
}
/* ------------------------------------------- */
/* 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) */
color: #adb5bd;
}
.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 */
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 4px solid #4a90e2; /* borda azul, altere para a cor desejada */
}
/* 1. Estilização do TD (Container) */
.coluna-horario {

View File

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

View File

@ -9,10 +9,29 @@ const Header = () => {
const [mensagem, setMensagem] = useState('');
const [mensagens, setMensagens] = useState([]);
const [showLogoutModal, setShowLogoutModal] = useState(false);
const [avatarUrl, setAvatarUrl] = useState(null);
const navigate = useNavigate();
const chatInputRef = useRef(null);
const mensagensContainerRef = useRef(null);
useEffect(() => {
const loadAvatar = () => {
const localAvatar = localStorage.getItem('user_avatar');
if (localAvatar) {
setAvatarUrl(localAvatar);
}
};
loadAvatar();
const handleStorageChange = () => {
loadAvatar();
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, []);
useEffect(() => {
if (isChatOpen && chatInputRef.current) {
chatInputRef.current.focus();
@ -25,12 +44,16 @@ const Header = () => {
}
}, [mensagens]);
// Funções de Logout (do seu código)
// --- Logout ---
const handleLogoutClick = () => {
setShowLogoutModal(true);
setIsDropdownOpen(false);
};
const handleLogoutCancel = () => {
setShowLogoutModal(false);
};
const handleLogoutConfirm = async () => {
try {
const token =
@ -77,7 +100,7 @@ const Header = () => {
};
const clearAuthData = () => {
["token","authToken","userToken","access_token","user","auth","userData"].forEach(key => {
["token", "authToken", "userToken", "access_token", "user", "auth", "userData"].forEach(key => {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
});
@ -91,8 +114,6 @@ const Header = () => {
}
};
const handleLogoutCancel = () => setShowLogoutModal(false);
const handleProfileClick = () => {
setIsDropdownOpen(!isDropdownOpen);
if (isSuporteCardOpen) setIsSuporteCardOpen(false);
@ -100,7 +121,7 @@ const Header = () => {
};
const handleViewProfile = () => {
navigate('/perfil');
navigate('/perfil');
setIsDropdownOpen(false);
};
@ -120,7 +141,7 @@ const Header = () => {
setMensagens([
{
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',
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
}
@ -132,10 +153,11 @@ const Header = () => {
setMensagem('');
};
const handleEnviarMensagem = (e) => {
const handleEnviarMensagem = async (e) => {
e.preventDefault();
if (mensagem.trim() === '') return;
// Mensagem do usuário
const novaMensagemUsuario = {
id: Date.now(),
texto: mensagem,
@ -146,51 +168,55 @@ const Header = () => {
setMensagens(prev => [...prev, novaMensagemUsuario]);
setMensagem('');
setTimeout(() => {
if (chatInputRef.current) {
chatInputRef.current.focus();
}
}, 0);
try {
const response = await fetch("http://localhost:5000/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: mensagem }),
});
setTimeout(() => {
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!'
];
const data = await response.json();
// Resposta da IA
const respostaSuporte = {
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',
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
};
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 = () => (
<div className="suporte-card">
<h2 className="suporte-titulo">Suporte</h2>
<p className="suporte-subtitulo">Entre em contato conosco através dos canais abaixo</p>
<div className="contato-item">
<div className="contato-info">
<div className="contato-nome">Email</div>
<div className="contato-descricao">suporte@mediconnect.com</div>
</div>
</div>
<div className="contato-item">
<div className="contato-info">
<div className="contato-nome">Telefone</div>
<div className="contato-descricao">(11) 3333-4444</div>
</div>
</div>
<div className="contato-item clickable" onClick={handleChatClick}>
<div className="contato-info">
<div className="contato-nome">Chat Online</div>
@ -206,7 +232,7 @@ const Header = () => {
<h3 className="chat-titulo">Chat de Suporte</h3>
<button type="button" className="fechar-chat" onClick={handleCloseChat}>×</button>
</div>
<div className="chat-mensagens" ref={mensagensContainerRef}>
{mensagens.map((msg) => (
<div key={msg.id} className={`mensagem ${msg.remetente}`}>
@ -215,7 +241,7 @@ const Header = () => {
</div>
))}
</div>
<form className="chat-input" onSubmit={handleEnviarMensagem}>
<input
ref={chatInputRef}
@ -240,13 +266,17 @@ const Header = () => {
<div className="profile-section">
<div className="profile-picture-container" onClick={handleProfileClick}>
<div className="profile-placeholder"></div>
<div className="profile-placeholder"></div>
</div>
{isDropdownOpen && (
<div className="profile-dropdown">
<button type="button" onClick={handleViewProfile} className="dropdown-button">Ver Perfil</button>
<button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">Sair (Logout)</button>
<button type="button" onClick={handleViewProfile} className="dropdown-button">
Ver Perfil
</button>
<button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">
Sair (Logout)
</button>
</div>
)}
</div>

View File

@ -260,16 +260,6 @@ function Sidebar({ menuItems }) {
})}
{/* Logout */}
<li className="sidebar-item">
<button
type="button"
className="sidebar-link btn"
onClick={handleLogoutClick}
>
<i className="bi bi-box-arrow-right"></i>
<span>Sair (Logout)</span>
</button>
</li>
<TrocardePerfis />
</ul>

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 { useNavigate, useLocation } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { UserInfos } from "./utils/Functions-Endpoints/General";
import { useAuth } from "./utils/AuthProvider";
import "../pages/style/TrocardePerfis.css";
import "./TrocardePerfis.css";
const ToggleIcon = ({ isOpen }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={{ transition: 'transform 0.3s', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
);
const TrocardePerfis = () => {
const location = useLocation();
const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth();
const [selectedProfile, setSelectedProfile] = useState("");
const [showProfiles, setShowProfiles] = useState([]);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
const fetchData = async () => {
const authHeader = getAuthorizationHeader();
setSelectedProfile(location.pathname || "");
const userInfo = await UserInfos(authHeader);
setShowProfiles(userInfo?.roles || []);
try {
const userInfo = await UserInfos(authHeader);
setShowProfiles(userInfo?.roles || []);
} catch (error) {
console.error("Erro ao buscar informações do usuário:", error);
setShowProfiles([]);
}
};
fetchData();
}, [location.pathname, getAuthorizationHeader]);
}, [getAuthorizationHeader]);
const handleSelectChange = (e) => {
const route = e.target.value;
setSelectedProfile(route);
if (route) navigate(route);
const handleProfileClick = (route) => {
if (route) {
navigate(route);
setIsOpen(false);
}
};
const handleToggle = () => {
setIsOpen(prev => !prev);
};
const options = [
@ -40,20 +67,47 @@ const TrocardePerfis = () => {
);
return (
<div className="container-perfis">
<p className="acesso-text">Acesso aos módulos:</p>
<select
className="perfil-select"
value={selectedProfile}
onChange={handleSelectChange}
<div className="container-perfis-toggle">
<div
className="toggle-button"
onClick={handleToggle}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleToggle();
}
}}
>
<option value="">Selecionar perfil</option>
{options.map((opt) => (
<option key={opt.key} value={opt.route}>
{opt.label}
</option>
))}
</select>
<span className="acesso-text">Acesso aos módulos</span>
<ToggleIcon isOpen={isOpen} />
</div>
{isOpen && (
<div className="perfil-list">
{options.length > 0 ? (
options.map((opt) => (
<div
key={opt.key}
className="perfil-item"
onClick={() => handleProfileClick(opt.route)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleProfileClick(opt.route);
}
}}
>
{opt.label}
</div>
))
) : (
<p className="no-profiles">Nenhum perfil disponível.</p>
)}
</div>
)}
</div>
);
};

View File

@ -126,12 +126,14 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
}
};
const handleAvailabilityUpdate = useCallback(
(newAvailability) => {
setFormData((prev) => ({ ...prev, availability: newAvailability }));
},
[setFormData]
);
const handleAvailabilityUpdate = useCallback((newAvailability) => {
setFormData((prev) => {
if (JSON.stringify(prev.availability) !== JSON.stringify(newAvailability)) {
return { ...prev, availability: newAvailability };
}
return prev;
});
}, []);
const handleCepBlur = async () => {
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 missingFields = [];
if (!formData.full_name) missingFields.push("full_name");
@ -290,20 +273,12 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
await onSave({ ...formData });
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) {
console.error("Erro ao salvar médico ou disponibilidade:", error);
alert("Erro ao salvar médico ou disponibilidade.");
console.error("Erro ao salvar médico:", error);
alert("Erro ao salvar médico.");
};
};
@ -734,7 +709,7 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
</div>
{/* BOTÕES DE AÇÃO */}
<div className="actions-container">
<div className="btns-container">
<button
className="btn btn-success btn-submit"
onClick={handleSubmit}

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";
const initialBlockTemplate = {
@ -21,21 +21,19 @@ const emptyAvailabilityTemplate = [
const HorariosDisponibilidade = ({
initialAvailability = emptyAvailabilityTemplate,
onUpdate,
onCancel,
}) => {
const [availability, setAvailability] = useState(initialAvailability);
const isFirstRun = useRef(true);
useEffect(() => {
if (initialAvailability !== emptyAvailabilityTemplate) {
if (initialAvailability && initialAvailability.length > 0) {
setAvailability(initialAvailability);
} else {
setAvailability(emptyAvailabilityTemplate);
}
}, [initialAvailability]);
useEffect(() => {
if (onUpdate) {
onUpdate(availability);
}
}, [availability, onUpdate]);
const handleDayCheck = useCallback((dayIndex, currentIsChecked) => {
const isChecked = !currentIsChecked;
@ -110,6 +108,10 @@ const HorariosDisponibilidade = ({
);
}, []);
const handleSave = useCallback(() => {
if (onUpdate) onUpdate(availability);
}, [availability, onUpdate]);
const renderTimeBlock = (dayIndex, bloco) => (
<div
key={bloco.id}
@ -165,7 +167,7 @@ const HorariosDisponibilidade = ({
width: "100%",
boxSizing: "border-box",
outline: "none",
fontSize: "13px"
fontSize: "13px",
}}
step="300"
/>
@ -186,7 +188,12 @@ const HorariosDisponibilidade = ({
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<label
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:
</label>
@ -199,13 +206,13 @@ const HorariosDisponibilidade = ({
handleTimeChange(dayIndex, bloco.id, "termino", e.target.value)
}
style={{
padding: "4px 6px",
border: "1px solid #d1d5db",
borderRadius: "6px",
width: "100%",
boxSizing: "border-box",
outline: "none",
fontSize: "13px",
padding: "4px 6px",
border: "1px solid #d1d5db",
borderRadius: "6px",
width: "100%",
boxSizing: "border-box",
outline: "none",
fontSize: "13px",
}}
step="300"
/>
@ -258,8 +265,7 @@ const HorariosDisponibilidade = ({
marginLeft: window.innerWidth < 640 ? "0" : "16px",
fontWeight: 500,
}}
>
</span>
></span>
)}
</div>
);

View File

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

View File

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

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')
}else if(ErrorData.httpStatus === 404){
setModalMensagem("Erro interno do sistema")
}else{setModalMensagem(ErrorData.mensagem)}
}else if(ErrorData.httpStatus === undefined){
setModalMensagem("Erro operacional no sistema")
}
else{setModalMensagem(ErrorData.mensagem)}
}, [ErrorData])
@ -24,7 +27,7 @@ return(
<div>
{showModal ?
{showModal === "modal"?
<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 navigate = useNavigate();
const [listaTodosAgendamentos, setListaTodosAgendamentos] = useState([])
const [selectedID, setSelectedId] = useState('0')
const [filaEsperaData, setfilaEsperaData] = useState([])
const [filaEsperaData, setFilaEsperaData] = useState([])
const [FiladeEspera, setFiladeEspera] = useState(false);
const [tabela, setTabela] = useState('diario');
const [PageNovaConsulta, setPageConsulta] = useState(false);
@ -42,72 +45,92 @@ const Agendamento = ({setDictInfo}) => {
const [FiltredTodosMedicos, setFiltredTodosMedicos] = 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()
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)
let dicionario = {
agendamento: agendamento,
Infos: {
nome_medico: medico[0]?.full_name,
doctor_id: medico[0]?.id,
patient_id: paciente[0].id,
paciente_nome: paciente[0].full_name,
paciente_cpf: paciente[0].cpf
}
};
return dicionario;
};
let DictAgendamentosOrganizados = {};
let ListaFilaDeEspera = [];
useMemo(() => {
if (!listaTodosAgendamentos.length) return { agendamentosOrganizados: {}, filaEsperaData: [] };
console.log("recarregando")
const DictAgendamentosOrganizados = {};
const ListaFilaDeEspera = [];
const fetchDados = async () => {
for (const agendamento of listaTodosAgendamentos) {
if (agendamento.status === 'requested') {
let v = await ConfigurarFiladeEspera(agendamento.patient_id, agendamento.doctor_id, agendamento);
ListaFilaDeEspera.push(v);
} else {
const DiaAgendamento = agendamento.scheduled_at.split("T")[0];
if (DiaAgendamento in DictAgendamentosOrganizados) {
DictAgendamentosOrganizados[DiaAgendamento].push(agendamento);
} else {
DictAgendamentosOrganizados[DiaAgendamento] = [agendamento];
}
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);
}
}
for (const DiaAgendamento in DictAgendamentosOrganizados) {
DictAgendamentosOrganizados[DiaAgendamento].sort((a, b) => {
if (a.scheduled_at < b.scheduled_at) return -1;
if (a.scheduled_at > b.scheduled_at) return 1;
return 0;
const medico = cacheMedicos[agendamento.doctor_id];
const paciente = cachePacientes[agendamento.patient_id];
ListaFilaDeEspera.push({
agendamento,
Infos: {
nome_medico: medico[0]?.full_name,
doctor_id: medico[0]?.id,
patient_id: paciente[0]?.id,
paciente_nome: paciente[0]?.full_name,
paciente_cpf: paciente[0]?.cpf,
},
});
} else {
const DiaAgendamento = agendamento.scheduled_at.split("T")[0];
if (DiaAgendamento in DictAgendamentosOrganizados) {
DictAgendamentosOrganizados[DiaAgendamento].push(agendamento);
} else {
DictAgendamentosOrganizados[DiaAgendamento] = [agendamento];
}
}
}
// Ordenar por data
for (const DiaAgendamento in DictAgendamentosOrganizados) {
DictAgendamentosOrganizados[DiaAgendamento].sort((a, b) => a.scheduled_at.localeCompare(b.scheduled_at));
}
const chavesOrdenadas = Object.keys(DictAgendamentosOrganizados).sort((a, b) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
const chavesOrdenadas = Object.keys(DictAgendamentosOrganizados).sort();
let DictAgendamentosFinal = {};
const DictAgendamentosFinal = {};
for (const data of chavesOrdenadas) {
DictAgendamentosFinal[data] = DictAgendamentosOrganizados[data];
DictAgendamentosFinal[data] = DictAgendamentosOrganizados[data];
}
setAgendamentosOrganizados(DictAgendamentosFinal);
setfilaEsperaData(ListaFilaDeEspera);
};
setAgendamentosOrganizados(DictAgendamentosFinal);
setFilaEsperaData(ListaFilaDeEspera);
};
fetchDados();
return { agendamentosOrganizados: DictAgendamentosOrganizados, filaEsperaData: ListaFilaDeEspera };
}, [listaTodosAgendamentos]); // 👉 só recalcula quando a lista muda
useEffect(() => {
var myHeaders = new Headers();
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)
.then(response => response.json())
.then(result => {FiltrarAgendamentos(result);console.log(result)})
.then(result => {setListaTodosAgendamentos(result);console.log(result)})
.catch(error => console.log('error', error));
const PegarTodosOsMedicos = async () => {
@ -135,24 +158,7 @@ const Agendamento = ({setDictInfo}) => {
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) => {
var myHeaders = new Headers();
@ -161,7 +167,32 @@ const deleteConsulta = (selectedPatientId) => {
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(() => {
@ -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) => {
setSearchTermDoctor(term);
if (term.trim() === '') {
setFiltredTodosMedicos([]);
if(MedicoFiltrado.id !== "vazio"){
console.log("Medico escolhido, mas vai ser apagado")
console.log(cacheAgendamentos, "cache ")
}
setFiltredTodosMedicos([]);
setMedicoFiltrado({id:"vazio"})
//2 FiltrarAgendamentos()
return;
}
if (FiltredTodosMedicos.length === 1){
setMedicoFiltrado({...FiltredTodosMedicos[0]})
}
const filtered = ListaDeMedicos.filter(medico =>
medico.nomeMedico.toLowerCase().includes(term.toLowerCase())
@ -252,10 +320,9 @@ const handleSearchMedicos = (term) => {
};
const handleClickCancel = () => setPageConsulta(false)
return (
<div>
<div className='spinner'></div>
<h1>Agendar nova consulta</h1>
@ -306,6 +373,9 @@ const handleSearchMedicos = (term) => {
className='dropdown-item'
onClick={() => {
setSearchTermDoctor(medico.nomeMedico);
setFiltredTodosMedicos([]);
setMedicoFiltrado(medico)
}}
>
<p>{medico.nomeMedico} </p>
@ -336,6 +406,7 @@ const handleSearchMedicos = (term) => {
}}
>
Fila de espera
</button>
</div>
@ -364,10 +435,10 @@ const handleSearchMedicos = (term) => {
</div>
</section>
{/* Componentes de Tabela - Adicionado props de delete da main */}
{tabela === "diario" && <TabelaAgendamentoDia handleClickAgendamento={handleClickAgendamento} agendamentos={DictAgendamentosOrganizados} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />}
{tabela === 'semanal' && <TabelaAgendamentoSemana agendamentos={DictAgendamentosOrganizados} ListarDiasdoMes={ListarDiasdoMes} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/>}
{tabela === 'mensal' && <TabelaAgendamentoMes ListarDiasdoMes={ListarDiasdoMes} aplicarCores={true} agendamentos={DictAgendamentosOrganizados} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />}
{/* Componentes de Tabela - Adicionado props de delete da main */}
{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} 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} selectedID={selectedID} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>}
</div>
</div>
)
@ -455,7 +526,7 @@ const handleSearchMedicos = (term) => {
<div className="modal-header bg-danger bg-opacity-25">
<h5 className="modal-title text-danger">
Confirmação de Exclusão
Confirmação de Cancelamento
</h5>
<button
type="button"
@ -466,8 +537,12 @@ const handleSearchMedicos = (term) => {
<div className="modal-body">
<p className="mb-0 fs-5">
Tem certeza que deseja excluir este agendamento?
Qual o motivo do cancelamento?
</p>
<div className='campo-de-input'>
<textarea className='input-modal' value={motivoCancelamento} onChange={(e) => setMotivoCancelamento(e.target.value)} />
</div>
</div>
<div className="modal-footer">
@ -475,7 +550,9 @@ const handleSearchMedicos = (term) => {
<button
type="button"
className="btn btn-primary"
onClick={() => setShowDeleteModal(false)}
onClick={() => {setShowDeleteModal(false);
}}
>
Cancelar
</button>
@ -484,7 +561,22 @@ const handleSearchMedicos = (term) => {
<button
type="button"
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
@ -494,6 +586,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>
)

View File

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

View File

@ -1,75 +1,68 @@
import React, { useState, useEffect, useCallback } from "react";
import { Link } from "react-router-dom";
const ENDPOINT_LISTAR = "https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability";
import HorariosDisponibilidade from "../components/doctors/HorariosDisponibilidade";
const ENDPOINT =
"https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability";
const MEDICOS_MOCKADOS = [
{ id: 53, nome: " João Silva" },
{ id: 19, nome: " Ana Costa" },
{ id: 11, nome: " Pedro Santos" },
{ id: 53, nome: "João Silva" },
{ id: 19, nome: "Ana Costa" },
{ id: 11, nome: "Pedro Santos" },
];
const diasDaSemana = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];
const formatarDataHora = (isoString) => {
if (!isoString) return "N/A";
try {
const data = new Date(isoString);
return data.toLocaleTimeString("pt-BR", { hour: '2-digit', minute: '2-digit', timeZone: 'UTC' });
} catch (error) {
// Usa o toLocaleTimeString para extrair hora e minuto
return data.toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
timeZone: "UTC",
});
} catch {
return "Data Inválida";
}
};
const DisponibilidadesDoctorPage = () => {
const [disponibilidades, setDisponibilidades] = useState([]);
const [loading, setLoading] = useState(false);
const [filtroMedicoNome, setFiltroMedicoNome] = useState("");
const [medicoEncontradoId, setMedicoEncontradoId] = useState(null);
const [gerenciarModo, setGerenciarModo] = useState(false);
const [editando, setEditando] = useState(null); // ID da disponibilidade sendo editada
const encontrarMedicoIdPorNome = (nome) => {
if (!nome) return null;
const termoBusca = nome.toLowerCase();
const medico = MEDICOS_MOCKADOS.find(m =>
m.nome.toLowerCase().includes(termoBusca)
);
return medico ? medico.id : null;
if (!nome) return null;
const termo = nome.toLowerCase();
const medico = MEDICOS_MOCKADOS.find((m) =>
m.nome.toLowerCase().includes(termo)
);
return medico ? medico.id : null;
};
const fetchDisponibilidades = useCallback(async (nome) => {
setLoading(true);
setDisponibilidades([]);
setMedicoEncontradoId(null);
const doctorId = encontrarMedicoIdPorNome(nome);
if (!doctorId) {
setLoading(false);
return;
setLoading(false);
return;
}
const url = `${ENDPOINT_LISTAR}?select=*&doctor_id=eq.${doctorId}`;
try {
const response = await fetch(url);
const result = await response.json();
let dados = Array.isArray(result) ? result : [];
const res = await fetch(`${ENDPOINT}?doctor_id=eq.${doctorId}`);
const data = await res.json();
setDisponibilidades(dados);
setMedicoEncontradoId(doctorId);
} catch (error) {
setDisponibilidades(
Array.isArray(data)
? data
: data && Array.isArray(data.items)
? data.items
: []
);
} catch (e) {
console.error("Erro ao buscar disponibilidades:", e);
setDisponibilidades([]);
} finally {
setLoading(false);
@ -77,111 +70,244 @@ const DisponibilidadesDoctorPage = () => {
}, []);
useEffect(() => {
if (!gerenciarModo && editando) {
setEditando(null);
}
}, [gerenciarModo]);
useEffect(() => {
if (editando) return;
if (filtroMedicoNome) {
const timer = setTimeout(() => {
fetchDisponibilidades(filtroMedicoNome);
}, 300);
}, 300);
return () => clearTimeout(timer);
} else {
setDisponibilidades([]);
setMedicoEncontradoId(null);
setDisponibilidades([]);
}
}, [filtroMedicoNome, fetchDisponibilidades]);
}, [filtroMedicoNome, fetchDisponibilidades, editando]);
const rotaGerenciar = medicoEncontradoId
? `../medicos/${medicoEncontradoId}/edit`
: `../medicos/novo/edit`;
const atualizarDisponibilidade = async (id, novoIntervalo) => {
try {
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 (
<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" }}>
Disponibilidades por Médico
</h1>
<Link
to={rotaGerenciar}
{/* Botão Voltar/Gerenciar */}
<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"
style={{
padding: "10px 20px",
fontSize: "14px",
whiteSpace: "nowrap",
textDecoration: "none",
display: "inline-block",
}}
>
+ Gerenciar Disponibilidades
</Link>
{editando
? "← Voltar para Tabela"
: gerenciarModo
? "← Voltar"
: "+ Gerenciar Disponibilidades"}
</button>
</div>
<div className="atendimento-eprocura">
<div className="busca-atendimento">
<div style={{ marginRight: '10px' }}>
<i className="fa-solid fa-user-doctor"></i>
{/* Campo de busca - ESCONDIDO NO MODO DE EDIÇÃO */}
{!editando && (
<div className="atendimento-eprocura">
<div className="busca-atendimento">
<input
type="text"
placeholder="Filtrar por Nome do Médico..."
value={filtroMedicoNome}
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>
)}
<section className="calendario-ou-filaespera">
<div className="fila-container">
<h2 className="fila-titulo">
Disponibilidades Encontradas ({disponibilidades.length})
</h2>
<section className="calendario-ou-filaespera">
<div className="fila-container">
<h2 className="fila-titulo">
{editando
? "Editar Disponibilidade"
: gerenciarModo
? "Gerenciar Disponibilidades"
: "Disponibilidades Encontradas"}{" "}
({disponibilidades.length})
</h2>
{loading ? (
<p className="text-center py-10">Carregando disponibilidades...</p>
) : (filtroMedicoNome && disponibilidades.length === 0) ? (
<p className="text-center py-10">
Nenhuma disponibilidade encontrada para o nome buscado.
</p>
) : (
<table className="fila-tabela" style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
{[ "Dia da Semana", "Início", "Término", "Intervalo", "Tipo Consulta"].map(
(header) => (
<th
key={header}
style={{ padding: "10px", borderBottom: "2px solid #ddd", textAlign: "left" }}
{loading ? (
<p>Carregando...</p>
) : disponibilidades.length === 0 ? (
<p>Nenhuma disponibilidade encontrada.</p>
) : editando ? (
<>
<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 style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th>Dia da Semana</th>
<th>Início</th>
<th>Término</th>
<th>Intervalo</th>
<th>Tipo Consulta</th>
{gerenciarModo && <th>Ações</th>}
</tr>
</thead>
<tbody>
{disponibilidades.map((disp) => (
<tr key={disp.id}>
<td>{diasDaSemana[disp.weekday]}</td>
<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",
}}
>
{header}
</th>
)
Editar
</button>{" "}
<button
onClick={() => deletarDisponibilidade(disp.id)}
style={{
backgroundColor: "#c72f2f",
color: "white",
borderRadius: "6px",
}}
>
Excluir
</button>
</td>
)}
</tr>
</thead>
<tbody>
{disponibilidades.map((disp, index) => (
<tr key={disp.id || index} style={{ borderBottom: "1px solid #eee" }}>
<td style={{ padding: "10px", fontSize: "0.9em" }}>
{diasDaSemana[disp.weekday] || disp.weekday}
</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>
))}
</tbody>
</table>
)}
</div>
</section>
</div>
))}
</tbody>
</table>
)}
</div>
</section>
</div>
);
};
export default DisponibilidadesDoctorPage;
export default DisponibilidadesDoctorPage;

View File

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

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

View File

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

View File

@ -4,9 +4,11 @@ import API_KEY from "../components/utils/apiKeys";
import { useAuth } from "../components/utils/AuthProvider";
import "./style/TablePaciente.css";
import ModalErro from "../components/utils/fetchErros/ModalErro";
import manager from "../components/utils/fetchErros/ManagerFunction";
function TablePaciente({ setCurrentPage, setPatientID }) {
const { getAuthorizationHeader, isAuthenticated, RefreshingToken } = useAuth();
const { getAuthorizationHeader, isAuthenticated } = useAuth();
const [pacientes, setPacientes] = useState([]);
const [search, setSearch] = useState("");
@ -21,12 +23,16 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
const [dataInicial, setDataInicial] = useState("");
const [dataFinal, setDataFinal] = useState("");
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedPatientId, setSelectedPatientId] = useState(null);
const [showModalError, setShowModalError] = useState(false);
const [showModalError, setShowModalError] = useState("");
const [ ErrorInfo, setErrorInfo] = useState({})
const [ErrorInfo, setErrorInfo] = useState({})
const GetAnexos = async (id) => {
var myHeaders = new Headers();
@ -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(() => {
const authHeader = getAuthorizationHeader()
console.log(authHeader, 'aqui autorização')
@ -120,7 +134,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions)
.then(response => {
// 1. VERIFICAÇÃO DO STATUS HTTP (Se não for 2xx)
if (!response.ok) {
return response.json().then(errorData => {
@ -136,27 +150,23 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
console.error("ERRO DETALHADO:", errorObject);
throw errorObject;
});
}
// 3. Se a resposta for OK (2xx), processamos o JSON normalmente
return response.json();
})
.then(result => {
// 4. Bloco de SUCESSO
setPacientes(result);
console.log("Sucesso:", result);
// IMPORTANTE: Se o modal estava aberto, feche-o no sucesso
setShowModalError(false);
})
.catch(error => {
// 5. Bloco de ERRO (Captura erros de rede ou o erro lançado pelo 'throw')
//console.error('Falha na requisição:', error.message);
if(error.httpStatus === 401){
RefreshingToken()
}
setErrorInfo(error)
setShowModalError(true);
console.error(error, "deu erro")
manager(setShowModalError, RefreshingToken, setErrorInfo, error)
});
}, [isAuthenticated, getAuthorizationHeader]);
@ -195,6 +205,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
setIdadeMaxima("");
setDataInicial("");
setDataFinal("");
setPaginaAtual(1);
};
const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => {
@ -238,13 +249,53 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
return resultado;
}) : [];
const totalPaginas = Math.ceil(pacientesFiltrados.length / itensPorPagina);
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const pacientesPaginados = pacientesFiltrados.slice(indiceInicial, indiceFinal);
const irParaPagina = (pagina) => {
setPaginaAtual(pagina);
};
const avancarPagina = () => {
if (paginaAtual < totalPaginas) {
setPaginaAtual(paginaAtual + 1);
}
};
const voltarPagina = () => {
if (paginaAtual > 1) {
setPaginaAtual(paginaAtual - 1);
}
};
const gerarNumerosPaginas = () => {
const paginas = [];
const paginasParaMostrar = 5;
let inicio = Math.max(1, paginaAtual - Math.floor(paginasParaMostrar / 2));
let fim = Math.min(totalPaginas, inicio + paginasParaMostrar - 1);
inicio = Math.max(1, fim - paginasParaMostrar + 1);
for (let i = inicio; i <= fim; i++) {
paginas.push(i);
}
return paginas;
};
useEffect(() => {
console.log(` Pacientes totais: ${pacientes?.length}, Filtrados: ${pacientesFiltrados?.length}`);
}, [pacientes, pacientesFiltrados, search]);
setPaginaAtual(1);
}, [search, filtroConvenio, filtroVIP, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal]);
return (
<>
<ModalErro showModal={showModalError} setShowModal={setShowModalError} ErrorData={ErrorInfo}/>
<div className="page-heading">
<h3>Lista de Pacientes</h3>
</div>
@ -295,7 +346,8 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</select>
<button
className={`btn btn-sm ${filtroVIP ? "btn-primary" : "btn-outline-primary"}`}
className={`btn btn-sm ${filtroVIP ? "btn-primary" : "btn-outline-primary"}`}
onClick={() => setFiltroVIP(!filtroVIP)}
style={{ padding: "0.25rem 0.5rem" }}
>
<i className="bi bi-award me-1"></i> VIP
@ -400,6 +452,12 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</div>
</div>
)}
<div className="mt-3">
<div className="contador-pacientes">
{pacientesFiltrados.length} DE {pacientes.length} PACIENTES ENCONTRADOS
</div>
</div>
</div>
{(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante ||
@ -421,12 +479,6 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</div>
)}
<div className="mb-3">
<span className="badge results-badge">
{pacientesFiltrados?.length} de {pacientes?.length} pacientes encontrados
</span>
</div>
<div className="table-responsive">
<table className="table table-striped table-hover table-paciente-table">
<thead>
@ -439,8 +491,8 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</tr>
</thead>
<tbody>
{pacientesFiltrados.length > 0 ? (
pacientesFiltrados.map((paciente) => (
{pacientesPaginados.length > 0 ? (
pacientesPaginados.map((paciente) => (
<tr key={paciente.id}>
<td>
<div className="d-flex align-items-center patient-name-container">
@ -495,13 +547,75 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
))
) : (
<tr>
<td colSpan="5" className="empty-state">
Nenhum paciente encontrado.
<td colSpan="5" className="text-center py-4">
<div className="text-muted">
<i className="bi bi-search display-4"></i>
<p className="mt-2">Nenhum paciente encontrado com os filtros aplicados.</p>
{(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante ||
filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
<button className="btn btn-outline-primary btn-sm mt-2" onClick={limparFiltros}>
Limpar filtros
</button>
)}
</div>
</td>
</tr>
)}
</tbody>
</table>
{/* Paginação */}
{pacientesFiltrados.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, pacientesFiltrados.length)} de {pacientesFiltrados.length} pacientes
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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