Compare commits

..

No commits in common. "main" and "Disponibilidade4" have entirely different histories.

105 changed files with 7272 additions and 18479 deletions

2
.env
View File

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

View File

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

56
et --hard 63659b6 Normal file
View File

@ -0,0 +1,56 @@
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

6943
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,16 +5,6 @@
"dependencies": {
"@ckeditor/ckeditor5-build-classic": "^41.4.2",
"@ckeditor/ckeditor5-react": "^11.0.0",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.1.0",
"@fullcalendar/core": "^6.1.19",
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/react": "^6.1.19",
"@fullcalendar/timegrid": "^6.1.19",
"@jitsi/react-sdk": "^1.4.0",
"@supabase/supabase-js": "^2.86.0",
"@sweetalert2/theme-dark": "^5.0.27",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
@ -28,17 +18,10 @@
"apexcharts": "^5.3.4",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"cors": "^2.8.5",
"dayjs": "^1.11.19",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"firebase": "^12.5.0",
"dayjs": "^1.11.18",
"flatpickr": "^4.6.13",
"helmet": "^8.1.0",
"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",
@ -50,11 +33,9 @@
"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",
"react-toastify": "^11.0.5",
"recharts": "^3.1.2",
"sweetalert2": "^11.22.4",
"tiptap": "^1.32.2",
@ -91,8 +72,5 @@
"sass": "^1.91.0",
"sass-loader": "^16.0.5",
"tailwindcss": "^4.1.13"
},
"overrides": {
"react": "$react"
}
}

View File

@ -1,38 +0,0 @@
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,3 +1,4 @@
.dashboard-container {
padding: 2rem;
font-family: 'Arial', sans-serif;
@ -5,6 +6,7 @@
min-height: 100vh;
}
.dashboard-header {
display: flex;
justify-content: space-between;
@ -32,133 +34,143 @@
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(30, 58, 138, 0.3);
transition: background-color 0.3s, transform 0.25s ease, box-shadow 0.25s ease;
}
.new-user-btn:hover {
background-color: #162d6b;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(30, 58, 138, 0.4);
box-shadow: 0px 4px 12px rgba(30, 58, 138, 0.3);
}
.filters-container {
background: #fff;
border-radius: 12px;
padding: 1.2rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.filters-container:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.filters-title {
font-size: 16px;
font-size: 18px;
font-weight: bold;
margin-bottom: 0.3rem;
color: #333;
}
.filters-subtitle {
font-size: 0.85rem;
font-size: 0.9rem;
color: #666;
margin-bottom: 1rem;
}
.filters-content {
display: flex;
gap: 0.8rem;
gap: 1rem;
align-items: center;
}
.filters-input {
flex: 1;
padding: 0.5rem 0.8rem;
padding: 0.6rem 1rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.9rem;
border-radius: 8px;
font-size: 0.95rem;
color: #333;
min-width: 200px;
transition: all 0.2s ease;
transition: border-color 0.2s, box-shadow 0.2s;
}
.filters-input:focus {
border-color: #1e3a8a;
box-shadow: 0 0 0 2px rgba(30, 58, 138, 0.1);
box-shadow: 0px 0px 0px 3px rgba(30, 58, 138, 0.2);
outline: none;
}
.filters-select {
padding: 0.5rem 0.8rem;
padding: 0.6rem 1rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 0.9rem;
border-radius: 8px;
font-size: 0.95rem;
background: #fff;
color: #333;
cursor: pointer;
min-width: 140px;
transition: all 0.2s ease;
transition: border-color 0.2s, box-shadow 0.2s;
}
.filters-select:focus {
border-color: #1e3a8a;
box-shadow: 0 0 0 2px rgba(30, 58, 138, 0.1);
box-shadow: 0px 0px 0px 3px rgba(30, 58, 138, 0.2);
outline: none;
}
.cards-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.2rem;
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background-color: white;
padding: 1.2rem;
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border: 1px solid transparent;
transition: all 0.25s ease;
transition: transform 0.25s ease, box-shadow 0.25s ease, border 0.25s ease, background 0.25s ease;
cursor: pointer;
}
.highlight:hover {
transform: translateY(-4px);
box-shadow: 0 6px 16px rgba(30, 58, 138, 0.2);
transform: translateY(-6px);
box-shadow: 0 8px 20px rgba(30, 58, 138, 0.2);
background: #f8faff;
border: 1px solid #1e3a8a33;
}
.card-label {
font-size: 0.85rem;
font-size: 0.9rem;
color: #999;
margin-bottom: 0.5rem;
}
.card-value {
font-size: 1.6rem;
font-size: 1.8rem;
font-weight: bold;
margin: 0;
color: #333;
}
.card-extra {
font-size: 0.8rem;
font-size: 0.85rem;
color: #666;
}
.card-extra.positive {
color: #1e3a8a;
font-weight: 600;
}
.user-table-container {
background: #fff;
border-radius: 12px;
padding: 1.2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 1.5rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-top: 2rem;
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.user-table-container:hover {
transform: translateY(-4px);
box-shadow: 0 6px 14px rgba(0,0,0,0.15);
}
.user-table-container h2 {
@ -181,7 +193,7 @@
.user-table th,
.user-table td {
padding: 10px 12px;
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
@ -190,11 +202,10 @@
background-color: #f3f4f6;
color: #333;
font-weight: 600;
font-size: 0.9rem;
}
.user-table tr {
transition: background-color 0.2s ease;
transition: background-color 0.25s ease;
}
.user-table tr:hover {
@ -203,115 +214,44 @@
.profile-badge {
background-color: #1e3a8a;
color: white;
padding: 4px 10px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 500;
color: #f7f7f7;
padding: 3px 8px;
border-radius: 8px;
font-size: 0.85rem;
display: inline-block;
}
.status-badge {
padding: 4px 10px;
border-radius: 6px;
font-size: 0.8rem;
padding: 3px 8px;
border-radius: 8px;
font-size: 0.85rem;
color: #fff;
font-weight: 500;
display: inline-block;
text-transform: capitalize;
}
.status-badge.ativo {
background-color: #1e3a8a;
background-color: #28a745;
}
.status-badge.inativo {
background-color: #6c757d;
background-color: #dc3545;
}
.actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
gap: 10px;
}
.action-btn {
border: none;
padding: 6px 12px;
font-size: 0.8rem;
font-weight: 500;
.action-icon {
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4px;
display: inline-flex;
align-items: center;
gap: 4px;
color: #555;
transition: color 0.2s, transform 0.2s;
}
.action-btn.detalhes {
background-color: #e6f2ff;
color: #004085;
border: 1px solid #b8d4ff;
}
.action-btn.detalhes:hover {
background-color: #cce4ff;
transform: translateY(-1px);
}
.action-btn.editar {
background-color: #fff3cd;
color: #856405;
border: 1px solid #ffeaa7;
}
.action-btn.editar:hover {
background-color: #ffeaa7;
transform: translateY(-1px);
}
.action-btn.excluir {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f1b0b7;
}
.action-btn.excluir:hover {
background-color: #f1b0b7;
transform: translateY(-1px);
}
.save-btn {
background-color: #1e3a8a;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.save-btn:hover {
background-color: #162d6b;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(30, 58, 138, 0.3);
}
.edit-btn {
background-color: #fff3cd;
color: #856405;
border: 1px solid #ffeaa7;
padding: 8px 16px;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.edit-btn:hover {
background-color: #ffeaa7;
transform: translateY(-1px);
.action-icon:hover {
color: #1e3a8a;
transform: scale(1.2);
}
html[data-bs-theme="dark"] .dashboard-container {
@ -326,17 +266,18 @@ html[data-bs-theme="dark"] .dashboard-subtitle {
}
html[data-bs-theme="dark"] .new-user-btn {
background-color: #1e3a8a;
background-color: #2563eb;
color: #fff;
}
html[data-bs-theme="dark"] .new-user-btn:hover {
background-color: #162d6b;
background-color: #1e40af;
}
html[data-bs-theme="dark"] .filters-container,
html[data-bs-theme="dark"] .user-table-container {
background: #1a1a1a;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
box-shadow: 0 4px 6px rgba(0,0,0,0.4);
}
html[data-bs-theme="dark"] .filters-title,
@ -358,19 +299,19 @@ html[data-bs-theme="dark"] .filters-select {
html[data-bs-theme="dark"] .filters-input:focus,
html[data-bs-theme="dark"] .filters-select:focus {
border-color: #1e3a8a;
box-shadow: 0 0 0 2px rgba(30, 58, 138, 0.2);
border-color: #2563eb;
box-shadow: 0px 0px 0px 3px rgba(37, 99, 235, 0.2);
}
html[data-bs-theme="dark"] .cards-container .card {
background-color: #181818;
color: #e0e0e0;
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
box-shadow: 0 4px 6px rgba(0,0,0,0.4);
}
html[data-bs-theme="dark"] .highlight:hover {
background: #1a1f2e;
border: 1px solid #1e3a8a33;
background: #232a3a;
border: 1px solid #2563eb33;
}
html[data-bs-theme="dark"] .card-label {
@ -386,7 +327,7 @@ html[data-bs-theme="dark"] .card-extra {
}
html[data-bs-theme="dark"] .card-extra.positive {
color: #1e3a8a;
color: #2563eb;
}
html[data-bs-theme="dark"] .user-table th {
@ -400,39 +341,26 @@ html[data-bs-theme="dark"] .user-table td {
}
html[data-bs-theme="dark"] .user-table tr:hover {
background-color: #1a1f2e;
background-color: #232a3a;
}
html[data-bs-theme="dark"] .profile-badge {
background-color: #1e3a8a;
background-color: #2563eb;
color: #fff;
}
html[data-bs-theme="dark"] .action-btn.detalhes {
background-color: #e6f2ff;
color: #004085;
border: 1px solid #b8d4ff;
html[data-bs-theme="dark"] .status-badge.ativo {
background-color: #28a745;
}
html[data-bs-theme="dark"] .action-btn.detalhes:hover {
background-color: #cce4ff;
html[data-bs-theme="dark"] .status-badge.inativo {
background-color: #dc3545;
}
html[data-bs-theme="dark"] .action-btn.editar {
background-color: #fff3cd;
color: #856405;
border: 1px solid #ffeaa7;
html[data-bs-theme="dark"] .action-icon {
color: #bdbdbd;
}
html[data-bs-theme="dark"] .action-btn.editar:hover {
background-color: #ffeaa7;
}
html[data-bs-theme="dark"] .action-btn.excluir {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f1b0b7;
}
html[data-bs-theme="dark"] .action-btn.excluir:hover {
background-color: #f1b0b7;
html[data-bs-theme="dark"] .action-icon:hover {
color: #2563eb;
}

View File

@ -1,9 +1,14 @@
import React from "react";
import "./gestao.css";
import { FaEdit, FaTrash } from "react-icons/fa";
function UserDashboard() {
return (
<div className="dashboard-container">
<div className="dashboard-header">
<div>
<h1 className="dashboard-title">Gestão de Usuários</h1>
@ -86,9 +91,8 @@ function UserDashboard() {
<td><span className="status-badge ativo">Ativo</span></td>
<td>20/12/2024, 08:30</td>
<td className="actions">
<button className="action-btn detalhes">Ver Detalhes</button>
<button className="action-btn editar">Editar</button>
<button className="action-btn excluir">Excluir</button>
<span className="action-icon"></span>
<span className="action-icon"></span>
</td>
</tr>
<tr>
@ -99,9 +103,8 @@ function UserDashboard() {
<td><span className="status-badge ativo">Ativo</span></td>
<td>19/12/2024, 14:20</td>
<td className="actions">
<button className="action-btn detalhes">Ver Detalhes</button>
<button className="action-btn editar">Editar</button>
<button className="action-btn excluir">Excluir</button>
<span className="action-icon"></span>
<span className="action-icon"></span>
</td>
</tr>
<tr>
@ -112,9 +115,8 @@ function UserDashboard() {
<td><span className="status-badge ativo">Ativo</span></td>
<td>20/12/2024, 07:45</td>
<td className="actions">
<button className="action-btn detalhes">Ver Detalhes</button>
<button className="action-btn editar">Editar</button>
<button className="action-btn excluir">Excluir</button>
<span className="action-icon"></span>
<span className="action-icon"></span>
</td>
</tr>
<tr>
@ -125,9 +127,8 @@ function UserDashboard() {
<td><span className="status-badge inativo">Inativo</span></td>
<td>15/12/2024, 16:30</td>
<td className="actions">
<button className="action-btn detalhes">Ver Detalhes</button>
<button className="action-btn editar">Editar</button>
<button className="action-btn excluir">Excluir</button>
<span className="action-icon"></span>
<span className="person-badge-fill"></span>
</td>
</tr>
</tbody>
@ -137,4 +138,5 @@ function UserDashboard() {
);
}
export default UserDashboard;

View File

@ -1,90 +0,0 @@
import React, { useState, useEffect } from 'react'
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta'
import API_KEY from '../components/utils/apiKeys'
import { useAuth } from '../components/utils/AuthProvider'
import { UserInfos } from '../components/utils/Functions-Endpoints/General'
const DoctorAgendamentoEditPage = ({DictInfo, setDictInfo}) => {
const {getAuthorizationHeader} = useAuth();
const [consultaToPut, setConsultaToPUT] = useState({})
const [idUsuario, setIdUsuario] = useState("")
const authHeader = getAuthorizationHeader()
useEffect(() => {
//console.log(DictInfo.scheduled_at.split("T")[0])
setDictInfo({...DictInfo, dataAtendimento:DictInfo?.scheduled_at?.split("T")[0]})
const fetchUserInfo = async () => {
const InfosUser = await UserInfos(authHeader)
console.log("Informações", InfosUser)
setIdUsuario(InfosUser.id)
}
fetchUserInfo()
}, [])
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
});
// console.log(DictParaPatch)
//console.log(id)
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));
}
return (
<div>
<FormNovaConsulta agendamento={DictInfo} setAgendamento={setDictInfo} onSave={handleSave}/>
</div>
)
}
export default DoctorAgendamentoEditPage

File diff suppressed because it is too large Load Diff

View File

@ -5,170 +5,63 @@ import { useState, useEffect } from 'react';
import { useAuth } from '../components/utils/AuthProvider';
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
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 [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [RelatoriosFiltrados, setRelatorios] = useState([]);
const [PacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [MedicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [showModal, setShowModal] = useState(false);
const [relatorioModal, setRelatorioModal] = useState(null);
const [termoPesquisa, setTermoPesquisa] = useState('');
const [filtroExame, setFiltroExame] = useState('');
const [examesDisponiveis, setExamesDisponiveis] = useState([]);
const [modalIndex, setModalIndex] = useState(0);
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const totalPaginas = Math.max(1, Math.ceil(relatoriosFinais.length / itensPorPagina));
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal);
const [index, setIndex] = useState();
// busca lista de relatórios
useEffect(() => {
let mounted = true;
const fetchReports = async () => {
try {
const myHeaders = new Headers();
var myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader);
const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
myHeaders.append('Authorization', authHeader);
var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
let userId = null;
let userFullName = null;
try {
const token = authHeader ? authHeader.replace(/^Bearer\s+/i, '') : '';
if (token) {
const userInfo = await UserInfos(token);
userId = userInfo?.id || userInfo?.user?.id || userInfo?.sub || null;
userFullName = userInfo?.full_name || (userInfo?.user && userInfo.user.full_name) || null;
}
const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions);
const data = await res.json();
setRelatorios(data || []);
} catch (err) {
console.warn('Não foi possível obter UserInfos (pode não estar logado):', err);
}
const baseUrl = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*";
let data = [];
if (userId) {
try {
const res = await fetch(`${baseUrl}&doctor_id=eq.${userId}`, requestOptions);
data = await res.json();
} catch (e) {
console.warn('Erro ao buscar por doctor_id:', e);
data = [];
}
if ((!Array.isArray(data) || data.length === 0) && userId) {
try {
const res2 = await fetch(`${baseUrl}&created_by=eq.${userId}`, requestOptions);
data = await res2.json();
} catch (e) {
console.warn('Erro ao buscar por created_by:', e);
data = [];
}
}
if ((!Array.isArray(data) || data.length === 0) && userFullName) {
try {
const encodedName = encodeURIComponent(userFullName);
const res3 = await fetch(`${baseUrl}&requested_by=eq.${encodedName}`, requestOptions);
data = await res3.json();
} catch (e) {
console.warn('Erro ao buscar por requested_by:', e);
data = [];
}
}
}
if (!userId || (!Array.isArray(data) || data.length === 0)) {
try {
const resAll = await fetch(baseUrl, requestOptions);
data = await resAll.json();
} catch (e) {
console.error('Erro listar relatórios (busca completa):', e);
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 (catch):', err);
if (mounted) {
setRelatoriosOriginais([]);
setRelatoriosFiltrados([]);
setRelatoriosFinais([]);
}
console.error('Erro listar relatórios', err);
setRelatorios([]);
}
};
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 < relatoriosFinais.length; i++) {
const rel = relatoriosFinais[i];
for (let i = 0; i < RelatoriosFiltrados.length; i++) {
const rel = RelatoriosFiltrados[i];
// paciente
try {
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
} catch (err) {
pacientes.push(null);
}
// médico: tenta created_by ou requested_by id se existir
try {
if (rel.doctor_id) {
const docRes = await GetDoctorByID(rel.doctor_id, authHeader);
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 if (rel.created_by) {
const docRes = await GetDoctorByID(rel.created_by, authHeader);
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
} else if (rel.requested_by) {
medicos.push({ full_name: rel.requested_by });
} else {
medicos.push({ full_name: '' });
medicos.push({ full_name: rel.requested_by || '' });
}
} catch (err) {
medicos.push({ full_name: rel.requested_by || '' });
@ -177,120 +70,55 @@ const DoctorRelatorioManager = () => {
setPacientesComRelatorios(pacientes);
setMedicosComRelatorios(medicos);
};
if (relatoriosFinais.length > 0) fetchRelData();
if (RelatoriosFiltrados.length > 0) fetchRelData();
else {
setPacientesComRelatorios([]);
setMedicosComRelatorios([]);
}
}, [relatoriosFinais, authHeader]);
}, [RelatoriosFiltrados, authHeader]);
const abrirModal = (relatorio, pageIndex) => {
const globalIndex = relatoriosFinais.findIndex(r => r.id === relatorio.id);
const indexToUse = globalIndex >= 0 ? globalIndex : (indiceInicial + pageIndex);
setRelatorioModal(relatorio);
setModalIndex(indexToUse);
setShowModal(true);
};
const limparFiltros = () => {
setTermoPesquisa('');
setFiltroExame('');
setRelatoriosFinais(relatoriosOriginais);
setPaginaAtual(1);
};
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" }
};
const BaixarPDFdoRelatorio = (nome_paciente) => {
const elemento = document.getElementById("folhaA4");
const opt = { margin: 0, filename: `relatorio_${nome_paciente || "paciente"}.pdf`, html2canvas: { scale: 2 }, jsPDF: { unit: "mm", format: "a4", orientation: "portrait" } };
html2pdf().set(opt).from(elemento).save();
};
const irParaPagina = (pagina) => {
setPaginaAtual(pagina);
};
const avancarPagina = () => {
if (paginaAtual < totalPaginas) {
setPaginaAtual(paginaAtual + 1);
}
};
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 fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal">
<div className="modal-dialog modal-tabela-relatorio">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
<h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name}</h5>
<div className="modal-header text-white">
<h5 className="modal-title ">Relatório de {PacientesComRelatorios[index]?.full_name}</h5>
<button type="button" className="btn-close" onClick={() => setShowModal(false)}></button>
</div>
<div className="modal-body">
<div id={`folhaA4-${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 id="folhaA4">
<div id='header-relatorio'>
<p>Clinica Rise up</p>
<p>Dr - CRM/SP 123456</p>
<p>Avenida - (79) 9 4444-4444</p>
</div>
<div id='infoPaciente' 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> {relatoriosFinais[modalIndex]?.due_at || '—'}</p>
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
<div className="tiptap-viewer-wrapper">
<TiptapViewer htmlContent={relatoriosFinais[modalIndex]?.content_html || relatoriosFinais[modalIndex]?.content || 'Relatório não preenchido.'} />
</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>
<div style={{ marginTop: 20, padding: '0 6px' }}>
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFinais[modalIndex]?.requested_by}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatoriosFinais[modalIndex]?.created_at || '—'}</p>
<div>
<p>Dr {MedicosComRelatorios[index]?.full_name || RelatoriosFiltrados[index]?.requested_by}</p>
<p>Emitido em: {RelatoriosFiltrados[index]?.created_at || '—'}</p>
</div>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name, modalIndex)}>
<i className='bi bi-file-pdf-fill me-1'></i> Baixar em PDF
</button>
<button type="button" className="btn btn-secondary" onClick={() => { setShowModal(false) }}>
Fechar
</button>
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name)}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button>
<button type="button" className="btn btn-primary" onClick={() => { setShowModal(false) }}>Fechar</button>
</div>
</div>
</div>
@ -305,51 +133,14 @@ 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="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>
<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" }} />
</div>
</div>
@ -358,92 +149,36 @@ const DoctorRelatorioManager = () => {
<thead>
<tr>
<th>Paciente</th>
<th>CPF</th>
<th>Exame</th>
<th>Doutor</th>
<th></th>
</tr>
</thead>
<tbody>
{relatoriosPaginados.length > 0 ? (
relatoriosPaginados.map((relatorio, index) => {
const globalIndex = relatoriosFinais.findIndex(r => r.id === relatorio.id);
const paciente = pacientesComRelatorios[globalIndex];
return (
{RelatoriosFiltrados.length > 0 ? (
RelatoriosFiltrados.map((relatorio, idx) => (
<tr key={relatorio.id}>
<td>{paciente?.full_name || 'Carregando...'}</td>
<td>{paciente?.cpf || 'Carregando...'}</td>
<td>{relatorio.exam}</td>
<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 btn-ver-detalhes" onClick={() => abrirModal(relatorio, index)}>
<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 btn-editar" onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<button className="btn btn-sm" style={{ backgroundColor: "#FFF3CD", color: "#856404" }} onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<i className="bi bi-pencil me-1"></i> Editar
</button>
</div>
</td>
</tr>
);
})
))
) : (
<tr><td colSpan="4" className="text-center">Nenhum relatório encontrado.</td></tr>
<tr><td colSpan="8" className="text-center">Nenhum paciente encontrado.</td></tr>
)}
</tbody>
</table>
{relatoriosFinais.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, relatoriosFinais.length)} de {relatoriosFinais.length} itens
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
// src/PagesMedico/EditPageRelatorio.jsx
// 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);
if (authHeader) myHeaders.append("Authorization", 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,14 +101,12 @@ const EditPageRelatorio = () => {
try {
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', 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,
@ -116,29 +114,13 @@ const EditPageRelatorio = () => {
});
if (!res.ok) {
let txt;
try { txt = await res.text(); } catch (e) { txt = 'erro lendo resposta'; }
const txt = await res.text();
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.');
@ -168,3 +150,4 @@ const EditPageRelatorio = () => {
};
export default EditPageRelatorio;

View File

@ -1,25 +1,23 @@
// src/PagesMedico/FormNovoRelatorio.jsx
import React, { useEffect, useState, useRef } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys';
import { useAuth } from '../components/utils/AuthProvider';
import TiptapEditor from './TiptapEditor';
import { GetAllPatients } from '../components/utils/Functions-Endpoints/Patient';
import { GetAllDoctors } from '../components/utils/Functions-Endpoints/Doctor';
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
import { GetAllPatients, GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { GetAllDoctors, GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
import './styleMedico/FormNovoRelatorio.css';
const FormNovoRelatorio = () => {
const { getAuthorizationHeader } = useAuth();
const authHeader = getAuthorizationHeader();
const navigate = useNavigate();
const location = useLocation();
const [patients, setPatients] = useState([]);
const [doctors, setDoctors] = useState([]);
const [loadingPatients, setLoadingPatients] = useState(true);
const [loadingDoctors, setLoadingDoctors] = useState(true);
// formulário
const [form, setForm] = useState({
patient_id: '',
patient_name: '',
@ -29,15 +27,19 @@ const FormNovoRelatorio = () => {
contentHtml: '',
});
// campos de busca (texto)
const [patientQuery, setPatientQuery] = useState('');
const [doctorQuery, setDoctorQuery] = useState('');
// dropdown control
const [showPatientDropdown, setShowPatientDropdown] = useState(false);
const [showDoctorDropdown, setShowDoctorDropdown] = useState(false);
const patientRef = useRef();
const doctorRef = useRef();
const [lockedFromAppointment, setLockedFromAppointment] = useState(false);
useEffect(() => {
// carregar pacientes
let mounted = true;
const loadPatients = async () => {
setLoadingPatients(true);
@ -66,6 +68,7 @@ const FormNovoRelatorio = () => {
return () => { mounted = false; };
}, [authHeader]);
// fechar dropdowns quando clicar fora
useEffect(() => {
const handleClick = (e) => {
if (patientRef.current && !patientRef.current.contains(e.target)) setShowPatientDropdown(false);
@ -99,13 +102,14 @@ const FormNovoRelatorio = () => {
`;
};
// escolher paciente (clicando na lista)
const choosePatient = async (patient) => {
setForm(prev => ({
...prev,
patient_id: patient.id,
patient_name: patient.full_name || '',
patient_birth: patient.birth_date || '',
contentHtml: generateTemplate(patient.full_name || '', patient.birth_date || '', prev.doctor_name)
contentHtml: generateTemplate(patient.full_name || '', patient.birth_date || '', form.doctor_name)
}));
setPatientQuery('');
setShowPatientDropdown(false);
@ -116,12 +120,13 @@ const FormNovoRelatorio = () => {
...prev,
doctor_id: doctor.id,
doctor_name: doctor.full_name || '',
contentHtml: generateTemplate(prev.patient_name, prev.patient_birth, doctor.full_name || '')
contentHtml: generateTemplate(form.patient_name, form.patient_birth, doctor.full_name || '')
}));
setDoctorQuery('');
setShowDoctorDropdown(false);
};
// filtrar pela query (startsWith)
const filteredPatients = patientQuery
? patients.filter(p => (p.full_name || '').toLowerCase().startsWith(patientQuery.toLowerCase())).slice(0, 40)
: [];
@ -132,24 +137,7 @@ const FormNovoRelatorio = () => {
const handleEditorChange = (html) => setForm(prev => ({ ...prev, contentHtml: html }));
useEffect(() => {
if (location && location.state && location.state.appointment) {
const appt = location.state.appointment;
const paciente_nome = location.state.paciente_nome || appt.paciente_nome || '';
const medico_nome = location.state.medico_nome || appt.medico_nome || '';
setForm(prev => ({
...prev,
patient_id: appt.patient_id || prev.patient_id,
patient_name: paciente_nome || prev.patient_name,
patient_birth: prev.patient_birth || '',
doctor_id: appt.doctor_id || prev.doctor_id,
doctor_name: medico_nome || prev.doctor_name,
contentHtml: generateTemplate(paciente_nome, prev.patient_birth || '', medico_nome)
}));
setLockedFromAppointment(true);
}
}, [location]);
// salvar novo relatório
const handleSubmit = async (e) => {
e.preventDefault();
if (!form.patient_id) return alert('Selecione o paciente (clicando no item) antes de salvar.');
@ -158,61 +146,35 @@ const FormNovoRelatorio = () => {
try {
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader);
myHeaders.append('Authorization', authHeader);
myHeaders.append('Content-Type', 'application/json');
myHeaders.append('Accept', 'application/json');
myHeaders.append('Prefer', 'return=representation');
const payload = {
const body = JSON.stringify({
patient_id: form.patient_id,
content: form.contentHtml,
content_html: form.contentHtml,
requested_by: form.doctor_name || ''
};
let userId = null;
try {
const token = authHeader?.replace(/^Bearer\s+/i, '') || '';
const userInfo = await UserInfos(token);
userId =
userInfo?.id ||
userInfo?.user?.id ||
userInfo?.sub ||
userInfo?.data?.id;
} catch (err) {
console.warn('Não foi possível obter o ID do usuário logado:', err);
}
if (userId) {
payload.created_by = userId;
} else {
console.warn('ID do usuário não encontrado, created_by não será incluído.');
}
payload.status = 'draft';
requested_by: form.doctor_name || '',
created_by: form.doctor_id || null,
status: 'draft'
});
const res = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports', {
method: 'POST',
headers: myHeaders,
body: JSON.stringify(payload),
body,
});
if (!res.ok) {
let txt;
try {
txt = await res.json();
} catch {
txt = await res.text();
}
const txt = await res.text();
console.error('Erro POST criar relatório:', res.status, txt);
return alert(`Erro ao criar relatório (ver console). Status ${res.status}\nMensagem: ${JSON.stringify(txt)}`);
// mostra mensagem mais útil
return alert(`Erro ao criar relatório (ver console). Status ${res.status}`);
}
const created = await res.json();
window.dispatchEvent(new Event('reports:refresh'));
alert('Relatório criado com sucesso!');
navigate('/medico/relatorios');
} catch (err) {
console.error('Erro salvar relatório (catch):', err);
console.error('Erro salvar relatório:', err);
alert('Erro ao salvar relatório. Veja console.');
}
};
@ -223,18 +185,17 @@ 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} style={{ position: 'relative' }}>
<div className="col-md-6" ref={patientRef}>
<label className="form-label">Buscar paciente (digite para filtrar)</label>
<input
className="form-control"
placeholder="Comece a digitar (ex.: m para pacientes que começam com m)"
value={lockedFromAppointment ? form.patient_name : patientQuery}
onChange={(e) => { if (!lockedFromAppointment) { setPatientQuery(e.target.value); setShowPatientDropdown(true); } }}
onFocus={() => { if (!lockedFromAppointment) setShowPatientDropdown(true); }}
disabled={lockedFromAppointment}
value={patientQuery}
onChange={(e) => { setPatientQuery(e.target.value); setShowPatientDropdown(true); }}
onFocus={() => setShowPatientDropdown(true)}
/>
{!lockedFromAppointment && showPatientDropdown && patientQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}>
{showPatientDropdown && patientQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '45%' }}>
{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}` : ''}
@ -245,18 +206,17 @@ 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} style={{ position: 'relative' }}>
<div className="col-md-6" ref={doctorRef}>
<label className="form-label">Buscar médico (digite para filtrar)</label>
<input
className="form-control"
placeholder="Comece a digitar o nome do médico"
value={lockedFromAppointment ? form.doctor_name : doctorQuery}
onChange={(e) => { if (!lockedFromAppointment) { setDoctorQuery(e.target.value); setShowDoctorDropdown(true); } }}
onFocus={() => { if (!lockedFromAppointment) setShowDoctorDropdown(true); }}
disabled={lockedFromAppointment}
value={doctorQuery}
onChange={(e) => { setDoctorQuery(e.target.value); setShowDoctorDropdown(true); }}
onFocus={() => setShowDoctorDropdown(true)}
/>
{!lockedFromAppointment && showDoctorDropdown && doctorQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}>
{showDoctorDropdown && doctorQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '45%', right: 0 }}>
{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

@ -1,215 +0,0 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js';
import './styleMedico/NovoRelatorioAudio.css';
const NovoRelatorioAudio = () => {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
paciente_nome: '',
data_exame: new Date().toISOString().split('T')[0],
exam: '',
diagnostico: '',
conclusao: '',
medico_nome: 'Dr.______________________'
});
const handleAudioUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
setLoading(true);
const data = new FormData();
data.append('audio', file);
try {
const response = await fetch('http://localhost:3001/api/transcrever-relatorio', {
method: 'POST',
body: data,
});
if (!response.ok) throw new Error('Erro na comunicação com a API');
const result = await response.json();
setFormData(prev => ({
...prev,
exam: result.exam || prev.exam,
diagnostico: result.diagnostico || prev.diagnostico,
conclusao: result.conclusao || prev.conclusao
}));
} catch (error) {
console.error(error);
alert("Erro: Verifique se o backend (porta 3001) está rodando.");
} finally {
setLoading(false);
e.target.value = null;
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handlePrintPDF = () => {
const element = document.getElementById('documento-final');
const opt = {
margin: 0,
filename: `Laudo_${formData.paciente_nome || 'SemNome'}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2 },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save();
};
return (
<div className="ai-editor-container">
<div className="editor-sidebar">
<div>
<h4><i className="bi bi-robot me-2"></i>AI Report</h4>
<p className="text-muted small">Gerador de laudos automatizado</p>
</div>
<div className="ai-upload-box">
{loading ? (
<div className="spinner-border text-info" role="status"></div>
) : (
<>
<label htmlFor="audioFile" style={{cursor:'pointer', width:'100%', display:'block'}}>
<i className="bi bi-mic fs-2 text-info"></i>
<div style={{color:'#fff', fontWeight:'bold', marginTop:'10px'}}>Gravar / Enviar Áudio</div>
<div className="small text-muted mt-1">Clique para selecionar</div>
</label>
<input
id="audioFile"
type="file"
accept="audio/*"
style={{display:'none'}}
onChange={handleAudioUpload}
/>
</>
)}
</div>
<hr className="border-secondary" />
<div className="form-group">
<label>NOME DO PACIENTE</label>
<input
type="text"
name="paciente_nome"
className="form-control dark-input"
value={formData.paciente_nome}
onChange={handleChange}
placeholder="Ex: Maria Silva"
/>
</div>
<div className="form-group">
<label>EXAME REALIZADO</label>
<input
type="text"
name="exam"
className="form-control dark-input"
value={formData.exam}
onChange={handleChange}
/>
</div>
<div className="form-group">
<label>DIAGNÓSTICO (TEXTO)</label>
<textarea
name="diagnostico"
rows="5"
className="form-control dark-input"
value={formData.diagnostico}
onChange={handleChange}
></textarea>
</div>
<div className="form-group">
<label>CONCLUSÃO</label>
<textarea
name="conclusao"
rows="3"
className="form-control dark-input"
value={formData.conclusao}
onChange={handleChange}
></textarea>
</div>
<div className="mt-auto d-flex gap-2">
<button className="btn btn-outline-light flex-grow-1" onClick={() => navigate(-1)}>Voltar</button>
<button className="btn btn-info flex-grow-1 fw-bold" onClick={handlePrintPDF}>
<i className="bi bi-printer me-2"></i>PDF
</button>
</div>
</div>
<div className="preview-area">
<div id="documento-final" className="paper-a4">
<div style={{borderBottom: '2px solid #000', paddingBottom: '20px', marginBottom: '30px', display:'flex', justifyContent:'space-between'}}>
<div>
<h2 style={{margin:0, fontSize:'18pt', fontWeight:'bold'}}>CLÍNICA RISE UP</h2>
<p style={{margin:0, fontSize:'10pt'}}>Excelência em Diagnóstico por Imagem</p>
</div>
<div style={{textAlign:'right', fontSize:'10pt'}}>
<p style={{margin:0}}><strong>{formData.medico_nome}</strong></p>
<p style={{margin:0}}>CRM/SP 123456</p>
<p style={{margin:0}}>{new Date().toLocaleDateString()}</p>
</div>
</div>
<div style={{backgroundColor:'#f8f9fa', padding:'15px', borderRadius:'4px', marginBottom:'30px'}}>
<table style={{width:'100%', fontSize:'11pt'}}>
<tbody>
<tr>
<td style={{width:'15%', fontWeight:'bold'}}>Paciente:</td>
<td>{formData.paciente_nome || '__________________________'}</td>
</tr>
<tr>
<td style={{fontWeight:'bold'}}>Exame:</td>
<td>{formData.exam || '__________________________'}</td>
</tr>
<tr>
<td style={{fontWeight:'bold'}}>Data:</td>
<td>{new Date(formData.data_exame).toLocaleDateString('pt-BR')}</td>
</tr>
</tbody>
</table>
</div>
<div className="laudo-body">
<h4 style={{textTransform:'uppercase', borderBottom:'1px solid #ccc', paddingBottom:'5px', marginBottom:'15px'}}>Relatório Médico</h4>
<p style={{fontWeight:'bold', marginBottom:'5px'}}>Análise:</p>
<p style={{marginBottom:'25px', textAlign:'justify', whiteSpace:'pre-wrap'}}>
{formData.diagnostico || <span style={{color:'#ccc'}}>O diagnóstico aparecerá aqui após o processamento do áudio...</span>}
</p>
<p style={{fontWeight:'bold', marginBottom:'5px'}}>Conclusão:</p>
<p style={{marginBottom:'40px', textAlign:'justify', fontWeight:'500'}}>
{formData.conclusao}
</p>
</div>
<div style={{marginTop:'50px', textAlign:'center', pageBreakInside:'avoid'}}>
<p style={{marginBottom:0}}>________________________________________</p>
<p style={{fontWeight:'bold', margin:0}}>{formData.medico_nome}</p>
<p style={{fontSize:'10pt', color:'#666'}}>Assinatura Eletrônica</p>
</div>
</div>
</div>
</div>
);
};
export default NovoRelatorioAudio;

View File

@ -6,58 +6,18 @@
/* --- Posiciona a barra de busca corretamente --- */
.busca-atendimento {
display: flex;
align-items: center;
margin-top: 20px;
padding: 0 10px;
align-items: center; /* Alinha os itens verticalmente */
margin-top: 20px; /* Espaço acima da barra de busca */
padding: 0 10px; /* Adiciona um padding lateral para alinhar com o resto */
gap: 15px;
}
.busca-atendimento > div:first-child {
width: 400px;
width: 400px; /* Define um tamanho para a barra de pesquisa */
display: flex;
align-items: center;
}
@media (max-width: 768px) {
.busca-atendimento {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.busca-atendimento > div:first-child {
width: 100%;
}
.btns-e-legenda-container {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.legenda-tabela {
flex-wrap: wrap;
gap: 8px;
}
}
@media (max-width: 576px) {
.busca-atendimento {
padding: 0 5px;
}
.btns-e-legenda-container {
padding: 0 5px;
}
.btn-selecionar-tabeladia,
.btn-selecionar-tabelasemana,
.btn-selecionar-tabelames {
padding: 6px 8px;
font-size: medium;
}
}
.busca-atendimento input {
margin-left: 8px;
border-radius: 8px;
@ -166,20 +126,13 @@
}
.container-btns-agenda-fila_esepera {
margin-top: 30px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: flex-end;
gap: 20px;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
padding: 0 20px 0;
border-bottom: 1px solid #edf1f7;
margin-left: 20px;
}
.btn-fila-espera,
.btn-agenda {
background-color: transparent;
@ -187,7 +140,6 @@
border-bottom: 3px solid transparent;
padding: 8px;
border-radius: 10px 10px 0px 0px;
color: #fff;
font-weight: bold;
cursor: pointer;
}
@ -277,9 +229,3 @@ html[data-bs-theme="dark"] .legenda-item-agendado {
border: 3px solid #4d4d2e;
color: #f7f7c4;
}
.calendar-legend {
margin-top: 8px;
display: flex;
gap: 8px;
justify-content: center;
}

View File

@ -1,31 +0,0 @@
.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,152 +1,55 @@
/* src/PagesMedico/styleMedico/FormNovoRelatorio.css */
#folhaA4 {
width: 210mm;
min-height: 207mm;
padding: 20mm;
margin: 10mm auto;
border: 1px solid #ccc;
background: white;
/* --- Modal centralizada e quadrada (ajustada para ser mais larga e sem quadrado branco no botão fechar) --- */
/* 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;
}
/* 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;
#primeiraLinha{
display: flex;
align-items: center;
justify-content: center;
padding: 0;
flex-direction: row;
gap: 20px;
margin-bottom: 20px;
}
/* caixa branca que contém o conteúdo - ocupa 100% da dialog */
.modal-dialog.modal-dialog-square .modal-content {
input,textarea,label{
font-size: 1.1rem;
}
textarea{
width: 100%;
height: auto;
border-radius: 12px;
box-shadow: 0 12px 30px rgba(11,22,35,0.18);
overflow: hidden;
height: 100px;
}
.submitButton{
display: flex;
flex-direction: column;
background: #fff;
margin-left: auto;
height:50% ;
padding: 8px 20px;
font-size: medium;
}
/* 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;
.bi-download{
font-size: 1.2rem;
margin-right: 5px;
font-weight: bold;
}
.custom-modal-header .modal-title {
margin: 0;
font-size: 1.05rem;
font-weight: 700;
#infoPaciente{
margin-top: 50px;
margin-bottom: 40px;
}
/* botão fechar no header — sem quadrado branco por trás */
.modal-close-btn {
background: transparent !important;
border: none;
width: 40px;
height: 40px;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: none;
outline: none;
position: relative;
z-index: 5;
}
.modal-close-btn::after {
content: '✕';
color: #fff;
font-weight: 700;
font-size: 16px;
}
/* body - faz scroll interno se for longo */
.modal-body {
padding: 18px;
overflow: auto;
flex: 1 1 auto;
}
/* footer */
.custom-modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 14px 18px;
border-top: 1px solid #eee;
background: #fafafa;
}
/* folhaA4 dentro da modal — adapta para caber */
.folhaA4 {
width: 100%;
box-sizing: border-box;
background: transparent;
padding: 0;
}
/* melhor espaçamento e leitura do conteúdo */
#header-relatorio p {
color: #374151;
margin: 6px 0;
#header-relatorio{
text-align: center;
margin-bottom: 30px;
}
#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; }
.info-paciente{
font-weight: bold;
}

View File

@ -1,119 +0,0 @@
/* Container que ocupa toda a altura da tela (menos o header do site) */
.ai-editor-container {
display: flex;
min-height: calc(100vh - 80px); /* Ajuste conforme seu header */
background-color: #e9ecef; /* Cor de "Mesa" */
overflow: hidden;
}
/* --- LADO ESQUERDO: PAINEL DE CONTROLE --- */
.editor-sidebar {
width: 400px;
background-color: #212529; /* Dark Mode para o painel */
color: #fff;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
box-shadow: 4px 0 10px rgba(0,0,0,0.1);
}
.editor-sidebar h4 {
color: #0dcaf0; /* Ciano para destaque */
margin-bottom: 20px;
font-weight: 700;
}
.editor-sidebar label {
font-size: 0.85rem;
color: #adb5bd;
margin-bottom: 5px;
}
/* Estilo customizado para inputs no fundo escuro */
.dark-input {
background-color: #343a40;
border: 1px solid #495057;
color: #fff;
border-radius: 6px;
}
.dark-input:focus {
background-color: #3b4248;
color: #fff;
border-color: #0dcaf0;
box-shadow: none;
}
/* Área de Upload Destacada */
.ai-upload-box {
border: 2px dashed #0dcaf0;
border-radius: 10px;
padding: 20px;
text-align: center;
background: rgba(13, 202, 240, 0.1);
transition: 0.3s;
cursor: pointer;
}
.ai-upload-box:hover {
background: rgba(13, 202, 240, 0.2);
}
/* --- LADO DIREITO: PREVIEW A4 --- */
.preview-area {
flex: 1;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 40px;
overflow-y: auto;
}
.paper-a4 {
width: 210mm;
min-height: 297mm;
background: white;
padding: 25mm;
box-shadow: 0 0 20px rgba(0,0,0,0.15);
color: #000;
font-family: 'Georgia', serif; /* Fonte mais séria para o documento */
font-size: 12pt;
line-height: 1.6;
}
/* Responsividade: Em celular vira coluna única */
@media (max-width: 900px) {
.ai-editor-container {
flex-direction: column;
min-height: auto;
}
.editor-sidebar {
width: 100%;
height: auto;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.preview-area {
padding: 20px;
}
.paper-a4 {
width: 100%;
min-height: auto;
padding: 15mm;
box-shadow: none;
}
}
@media (max-width: 576px) {
.ai-editor-container {
padding: 0;
}
.editor-sidebar {
padding: 15px;
}
.preview-area {
padding: 10px;
}
.paper-a4 {
padding: 10mm;
}
}

View File

@ -14,6 +14,7 @@ const CardConsultaPaciente = ({consulta, setConsulta, setSelectedId, setShowDel
const ids = useMemo(() => {
return {
doctor_id: consulta?.doctor_id,
@ -44,6 +45,9 @@ const CardConsultaPaciente = ({consulta, setConsulta, setSelectedId, setShowDel
}, [ids, authHeader]);
console.log(consulta, "dento do card")
let horario = consulta.scheduled_at.split("T")[1]
let Data = consulta.scheduled_at.split("T")[0]

View File

@ -1,110 +1,81 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import FormConsultaPaciente from './FormConsultaPaciente';
import { useAuth } from '../components/utils/AuthProvider';
import API_KEY from '../components/utils/apiKeys';
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
import React from 'react'
import FormConsultaPaciente from './FormConsultaPaciente'
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../components/utils/AuthProvider'
import API_KEY from '../components/utils/apiKeys'
import dayjs from 'dayjs'
import { UserInfos } from '../components/utils/Functions-Endpoints/General'
const ConsultaCadastroManager = () => {
const { getAuthorizationHeader, user } = useAuth();
const navigate = useNavigate();
const [Dict, setDict] = useState({});
// patient_id fixo do Pedro Abravanel
const [patientId, setPatientId] = useState('bf7d8323-05e1-437a-817c-f08eb5f174ef');
const [idUsuario, setIDusuario] = useState('');
const {getAuthorizationHeader} = useAuth()
const [Dict, setDict] = useState({})
const navigate = useNavigate()
const [idUsuario, setIDusuario] = useState("")
let authHeader = getAuthorizationHeader()
const authHeader = getAuthorizationHeader();
// Opcional: ainda tenta buscar infos do usuário, mas NÃO mostra mais alerta
useEffect(() => {
const ColherInfoUsuario =async () => {
try {
if (!authHeader) return;
const result = await UserInfos(authHeader)
const result = await UserInfos(authHeader);
setIDusuario(result?.profile?.id)
const pid =
result?.patient_id ||
result?.profile?.id ||
user?.patient_id ||
user?.profile?.id ||
user?.user?.id;
if (pid) {
setPatientId(pid);
}
ColherInfoUsuario()
setIDusuario(result?.profile?.id || pid || '');
} catch (e) {
console.error('Erro ao colher infos do usuário:', e);
}
};
ColherInfoUsuario();
}, [authHeader, user]);
}, [])
const handleSave = (Dict) => {
// se por algum motivo não tiver, usa o fixo do Pedro
const finalPatientId = patientId || 'bf7d8323-05e1-437a-817c-f08eb5f174ef';
let DataAtual = dayjs()
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
myHeaders.append('Authorization', authHeader);
myHeaders.append('Content-Type', 'application/json');
var raw = JSON.stringify({
"patient_id": Dict.patient_id,
"doctor_id": Dict.doctor_id,
"scheduled_at": `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`,
"duration_minutes": 30,
"appointment_type": Dict.tipo_consulta,
const raw = JSON.stringify({
patient_id: finalPatientId, // paciente Pedro
doctor_id: Dict.doctor_id,
scheduled_at: `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`,
duration_minutes: 30,
appointment_type: Dict.tipo_consulta,
patient_notes: 'Prefiro horário pela manhã',
insurance_provider: Dict.convenio,
status: 'confirmed',
created_by: idUsuario || finalPatientId,
"patient_notes": "Prefiro horário pela manhã",
"insurance_provider": Dict.convenio,
"status": Dict.status,
"created_by": idUsuario
});
const requestOptions = {
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow',
redirect: 'follow'
};
fetch(
'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments',
requestOptions
)
.then(async (response) => {
if (!response.ok) {
const text = await response.text();
throw new Error(`Erro ao salvar consulta: ${response.status} - ${text}`);
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
}
return response.text();
})
.then(() => {
alert('Consulta solicitada com sucesso!');
navigate('/paciente/agendamento/'); // volta para o calendário
})
.catch((error) => {
console.error('error', error);
alert('Erro ao salvar a consulta. Tente novamente.');
});
};
return (
<div>
<FormConsultaPaciente
agendamento={Dict}
setAgendamento={setDict}
onSave={handleSave}
onCancel={() => navigate('/paciente/agendamento/')}
/>
</div>
);
};
export default ConsultaCadastroManager;
<div>
<FormConsultaPaciente agendamento={Dict} setAgendamento={setDict} onSave={handleSave} onCancel={() => navigate("/paciente/agendamento/")}/>
</div>
)
}
export default ConsultaCadastroManager

View File

@ -4,124 +4,109 @@ 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'
// 1. Importe o useNavigate
import { useNavigate } from 'react-router-dom'
const ConsultaEditPage = ({dadosConsulta}) => {
const ConsultaEditPage = ({ DictInfo }) => {
// 2. Crie a instância do navigate
const navigate = useNavigate();
console.log(dadosConsulta, "editar")
const {getAuthorizationHeader} = useAuth()
const authHeader = getAuthorizationHeader();
const [idUsuario, setIDusuario] = useState(null);
const [Dict, setDict] = useState({});
const [Medico, setMedico] = useState(null);
const [Paciente, setPaciente] = useState(null);
const [idUsuario, setIDusuario] = useState("6e7f8829-0574-42df-9290-8dbb70f75ada")
console.log("dentro do edit", DictInfo)
const [DictInfo, setDict] = useState({})
const [Medico, setMedico] = useState({})
const [Paciente, setPaciente] = useState([])
useEffect(() => {
setDict({ ...DictInfo });
setDict({...dadosConsulta})
const fetchMedicoePaciente = async () => {
console.log(dadosConsulta.doctor_id)
let Medico = await GetDoctorByID(dadosConsulta.doctor_id,authHeader )
let Paciente = await GetPatientByID(dadosConsulta.patient_id,authHeader )
console.log(Paciente, 'Paciente')
setMedico(Medico[0])
setPaciente(Paciente[0])
const fetchInitialData = async () => {
if (DictInfo.doctor_id) {
const medicoData = await GetDoctorByID(DictInfo.doctor_id, authHeader);
setMedico(medicoData[0]);
}
if (DictInfo.patient_id) {
const pacienteData = await GetPatientByID(DictInfo.patient_id, authHeader);
setPaciente(pacienteData[0]);
const ColherInfoUsuario =async () => {
const result = await UserInfos(authHeader)
setIDusuario(result?.profile?.id)
}
};
ColherInfoUsuario()
fetchMedicoePaciente()
const fetchUserInfo = async () => {
const result = await UserInfos(authHeader);
setIDusuario(result?.profile?.id);
};
fetchUserInfo();
fetchInitialData();
}, [DictInfo, authHeader]);
}, [])
useEffect(() => {
if (Medico) {
setDict(prevDict => ({
...prevDict,
medico_nome: Medico?.full_name,
dataAtendimento: DictInfo.scheduled_at?.split("T")[0]
}));
}
}, [Medico, DictInfo.scheduled_at]);
setDict({...DictInfo, medico_nome:Medico?.full_name, dataAtendimento:dadosConsulta.scheduled_at?.split("T")[0]})
}, [Medico])
const handleSave = async (DictParaPatch) => {
try {
const myHeaders = new Headers();
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);
myHeaders.append('Prefer', 'return=representation');
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
const raw = JSON.stringify({
patient_id: DictParaPatch.patient_id,
doctor_id: DictParaPatch.doctor_id,
duration_minutes: 30,
chief_complaint: "Dor de cabeça há 3 ",
created_by: idUsuario,
scheduled_at: `${DictParaPatch.dataAtendimento}T${DictParaPatch.horarioInicio}:00.000Z`,
appointment_type: DictParaPatch.tipo_consulta,
patient_notes: "Prefiro horário pela manhã",
insurance_provider: DictParaPatch.convenio,
status: DictParaPatch.status,
});
const requestOptions = {
var requestOptions = {
method: 'PATCH',
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');
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 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={Dict}
setAgendamento={setDict}
onSave={handleSave}
onCancel={handleCancel}
/>
<FormConsultaPaciente agendamento={DictInfo} setAgendamento={setDict} onSave={handleSave}/>
</div>
)
}
export default ConsultaEditPage;
export default ConsultaEditPage

View File

@ -1,630 +1,155 @@
import React, { useState, useMemo, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys.js';
import AgendamentoCadastroManager from '../pages/AgendamentoCadastroManager.jsx';
import { useAuth } from '../components/utils/AuthProvider.js';
import dayjs from 'dayjs';
import 'dayjs/locale/pt-br';
import isBetween from 'dayjs/plugin/isBetween';
import localeData from 'dayjs/plugin/localeData';
import { ChevronLeft, ChevronRight, Trash2 } from 'lucide-react';
import '../pages/style/Agendamento.css';
import '../pages/style/FilaEspera.css';
import Spinner from '../components/Spinner.jsx';
import React from 'react'
import "./style.css"
import CardConsultaPaciente from './CardConsultaPaciente'
import { useNavigate } from 'react-router-dom'
import { useEffect, useState } from 'react'
import API_KEY from '../components/utils/apiKeys'
import { useAuth } from '../components/utils/AuthProvider'
dayjs.locale('pt-br');
dayjs.extend(isBetween);
dayjs.extend(localeData);
const ConsultasPaciente = ({setConsulta}) => {
const {getAuthorizationHeader} = useAuth()
const Agendamento = ({ setDictInfo }) => {
const navigate = useNavigate();
const { getAuthorizationHeader, user } = useAuth();
console.log('USER NO AGENDAMENTO:', user);
const [showDeleteModal, setShowDeleteModal] = useState(false)
const [selectedID, setSelectedId] = useState("")
let authHeader = getAuthorizationHeader()
const [patientId, setPatientId] = useState('bf7d8323-05e1-437a-817c-f08eb5f174ef');
const [isLoading, setIsLoading] = useState(false); // começa false
const [DictAgendamentosOrganizados, setDictAgendamentosOrganizados] =
useState({});
const [filaEsperaData, setFilaDeEsperaData] = useState([]);
const [FiladeEspera, setFiladeEspera] = useState(false);
const [PageNovaConsulta, setPageConsulta] = useState(false);
const [consultas, setConsultas] = useState([])
const [currentDate, setCurrentDate] = useState(dayjs());
const [selectedDay, setSelectedDay] = useState(dayjs());
const [quickJump, setQuickJump] = useState({
month: currentDate.month(),
year: currentDate.year(),
});
const [isCancelModalOpen, setIsCancelModalOpen] = useState(false);
const [appointmentToCancel, setAppointmentToCancel] = useState(null);
const [cancellationReason, setCancellationReason] = useState('');
const authHeader = useMemo(
() => getAuthorizationHeader(),
[getAuthorizationHeader]
);
// Buscar consultas desse paciente
const carregarDados = async () => {
// só tenta buscar quando tiver header e patientId
if (!authHeader) {
console.warn('Header de autorização não disponível.');
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;
}
if (!patientId) {
console.warn('patientId ainda não carregado, aguardando contexto.');
return;
}
setIsLoading(true);
try {
const myHeaders = new Headers({
Authorization: authHeader,
apikey: API_KEY,
// 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();
});
const requestOptions = { method: 'GET', headers: myHeaders };
const response = await fetch(
`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select=*,doctors(full_name)&patient_id=eq.${patientId}`,
requestOptions
);
if (!response.ok)
throw new Error(`Erro na requisição: ${response.statusText}`);
const consultasBrutas = (await response.json()) || [];
console.log('CONSULTAS BRUTAS PACIENTE:', consultasBrutas);
const newDict = {};
const newFila = [];
for (const agendamento of consultasBrutas) {
const agendamentoMelhorado = {
...agendamento,
medico_nome: agendamento.doctors?.full_name || 'Médico não informado',
};
if (agendamento.status === 'requested') {
newFila.push({
agendamento: agendamentoMelhorado,
Infos: agendamentoMelhorado,
});
} else {
const diaAgendamento = dayjs(
agendamento.scheduled_at
).format('YYYY-MM-DD');
if (newDict[diaAgendamento]) {
newDict[diaAgendamento].push(agendamentoMelhorado);
} else {
newDict[diaAgendamento] = [agendamentoMelhorado];
}
}
// 2. Adicionar a lista no setConsultas
console.log(consultasFiltradas)
setConsultas(consultasFiltradas);
}
for (const key in newDict) {
newDict[key].sort((a, b) =>
a.scheduled_at.localeCompare(b.scheduled_at)
);
}
// Exemplo de como você chamaria (assumindo que DadosAgendamento é sua lista original):
// FiltrarAgendamentos(DadosAgendamento, Paciente.id);
setDictAgendamentosOrganizados(newDict);
setFilaDeEsperaData(newFila);
} catch (err) {
console.error('Falha ao buscar ou processar agendamentos:', err);
setDictAgendamentosOrganizados({});
setFilaDeEsperaData([]);
} finally {
setIsLoading(false);
}
};
// roda quando authHeader ou patientId mudarem
useEffect(() => {
carregarDados();
}, [authHeader, patientId]); // padrão recomendado para fetch com useEffect [web:46][web:82]
var myHeaders = new Headers();
myHeaders.append("Authorization", authHeader);
myHeaders.append("apikey", API_KEY)
const updateAppointmentStatus = async (id, updates) => {
const myHeaders = new Headers({
Authorization: authHeader,
apikey: API_KEY,
'Content-Type': 'application/json',
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();
myHeaders.append("Content-Type", "application/json");
myHeaders.append('apikey', API_KEY)
myHeaders.append("authorization", authHeader)
var raw = JSON.stringify({ "status":"cancelled"
});
const requestOptions = {
var requestOptions = {
method: 'PATCH',
headers: myHeaders,
body: JSON.stringify(updates),
body: raw,
redirect: 'follow'
};
try {
const response = await fetch(
`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${id}`,
requestOptions
);
if (!response.ok) throw new Error('Falha ao atualizar o status.');
return true;
} catch (error) {
console.error('Erro de rede/servidor:', error);
return false;
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 handleCancelClick = (appointmentId) => {
setAppointmentToCancel(appointmentId);
setCancellationReason('');
setIsCancelModalOpen(true);
};
const executeCancellation = async () => {
if (!appointmentToCancel) return;
setIsLoading(true);
const motivo =
cancellationReason.trim() ||
'Cancelado pelo paciente (motivo não especificado)';
const success = await updateAppointmentStatus(appointmentToCancel, {
status: 'cancelled',
cancellation_reason: motivo,
updated_at: new Date().toISOString(),
});
setIsCancelModalOpen(false);
setAppointmentToCancel(null);
setCancellationReason('');
if (success) {
alert('Solicitação cancelada com sucesso!');
setDictAgendamentosOrganizados((prev) => {
const newDict = { ...prev };
for (const date in newDict) {
newDict[date] = newDict[date].filter(
(app) => app.id !== appointmentToCancel
);
}
return newDict;
});
setFilaDeEsperaData((prev) =>
prev.filter((item) => item.agendamento.id !== appointmentToCancel)
);
} else {
alert('Falha ao cancelar a solicitação.');
}
setIsLoading(false);
};
const handleQuickJumpChange = (type, value) =>
setQuickJump((prev) => ({ ...prev, [type]: Number(value) }));
const applyQuickJump = () => {
const newDate = dayjs()
.year(quickJump.year)
.month(quickJump.month)
.date(1);
setCurrentDate(newDate);
setSelectedDay(newDate);
};
const dateGrid = useMemo(() => {
const grid = [];
const startOfMonth = currentDate.startOf('month');
let currentDay = startOfMonth.subtract(startOfMonth.day(), 'day');
for (let i = 0; i < 42; i++) {
grid.push(currentDay);
currentDay = currentDay.add(1, 'day');
}
return grid;
}, [currentDate]);
const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
const handleDateClick = (day) => setSelectedDay(day);
if (isLoading) {
return (
<div
className="form-container"
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '50vh',
}}
>
<Spinner />
</div>
);
}
return (
<div>
<h1>Minhas consultas</h1>
<div
className="btns-gerenciamento-e-consulta"
style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}
>
<button
className="btn btn-primary btn-consulta-paciente"
onClick={() => {
setPageConsulta(true);
setFiladeEspera(false);
}}
>
<i className="bi bi-plus-circle"></i> Solicitar Agendamento
<h1> Gerencie suas consultas</h1>
<div className='form-container'>
<button className="btn btn-primary" onClick={() => {navigate("criar")}}>
<i className="bi bi-plus-circle"></i> Adicionar Consulta
</button>
<button
className="btn btn-primary btn-consulta-paciente"
onClick={() => {
setFiladeEspera(!FiladeEspera);
setPageConsulta(false);
}}
>
<i className="bi bi-list-task me-1"></i> Fila de Espera ({filaEsperaData.length})
</button>
</div>
<h2>Seus proximos atendimentos</h2>
{!PageNovaConsulta ? (
<div className="atendimento-eprocura">
<section className="calendario-ou-filaespera">
{!FiladeEspera ? (
<div className="calendar-wrapper">
<div className="calendar-info-panel">
<div className="info-date-display">
<span>{selectedDay.format('MMM')}</span>
<strong>{selectedDay.format('DD')}</strong>
</div>
<div className="info-details">
<h3>{selectedDay.format('dddd')}</h3>
<p>{selectedDay.format('D [de] MMMM [de] YYYY')}</p>
</div>
<div className="appointments-list">
<div
className="calendar-legend compact-legend"
style={{
marginTop: 4,
gap: 3,
fontSize: 8,
lineHeight: 1.1,
}}
>
<div
className="legend-item"
data-status="completed"
style={{ padding: '0px 5px' }}
>
Realizado
</div>
<div
className="legend-item"
data-status="confirmed"
style={{ padding: '0px 5px' }}
>
Confirmado
</div>
<div
className="legend-item"
data-status="agendado"
style={{ padding: '0px 5px' }}
>
Agendado
</div>
<div
className="legend-item"
data-status="cancelled"
style={{ padding: '0px 5px' }}
>
Cancelado
</div>
</div>
{consultas.map((consulta) => (
<CardConsultaPaciente consulta={consulta} setConsulta={setConsulta} setShowDeleteModal={setShowDeleteModal} setSelectedId={ setSelectedId}/>
<h4>Consultas para {selectedDay.format('DD/MM')}</h4>
{DictAgendamentosOrganizados[
selectedDay.format('YYYY-MM-DD')
]?.length > 0 ? (
DictAgendamentosOrganizados[
selectedDay.format('YYYY-MM-DD')
].map((app) => (
<div
key={app.id}
className="appointment-item"
data-status={app.status}
>
<div className="item-time">
{dayjs(app.scheduled_at).add(3, 'hour').format('HH:mm')}
</div>
<div className="item-details">
<span>Consulta com Dr(a). {app.medico_nome}</span>
</div>
<div className="item-actions">
{app.status !== 'cancelled' &&
dayjs(app.scheduled_at).isAfter(dayjs()) && (
<button
className="btn btn-sm btn-outline-danger"
onClick={() => handleCancelClick(app.id)}
title="Cancelar Consulta"
>
<Trash2 size={16} />
</button>
)}
</div>
</div>
))
) : (
<div className="no-appointments-info">
<p>Nenhuma consulta agendada para esta data.</p>
</div>
)}
</div>
</div>
<div className="calendar-main">
<div className="calendar-legend"></div>
<div className="calendar-controls">
<div className="date-indicator">
<h2>{currentDate.format('MMMM [de] YYYY')}</h2>
<div
className="quick-jump-controls"
style={{
display: 'flex',
gap: '5px',
marginTop: '10px',
}}
>
<select
value={quickJump.month}
onChange={(e) =>
handleQuickJumpChange('month', e.target.value)
}
className="form-select form-select-sm w-auto"
>
{dayjs.months().map((month, index) => (
<option key={index} value={index}>
{month.charAt(0).toUpperCase() + month.slice(1)}
</option>
))}
</select>
<select
value={quickJump.year}
onChange={(e) =>
handleQuickJumpChange('year', e.target.value)
}
className="form-select form-select-sm w-auto"
>
{Array.from({ length: 11 }, (_, i) =>
dayjs().year() - 5 + i
).map((year) => (
<option key={year} value={year}>
{year}
</option>
))}
</select>
<button
className="btn btn-sm btn-outline-primary"
onClick={applyQuickJump}
disabled={
quickJump.month === currentDate.month() &&
quickJump.year === currentDate.year()
}
>
Ir
</button>
</div>
</div>
<div className="nav-buttons">
<button
onClick={() => setCurrentDate((c) => c.subtract(1, 'month'))}
>
<ChevronLeft size={20} />
</button>
<button onClick={() => setCurrentDate(dayjs())}>
Hoje
</button>
<button
onClick={() => setCurrentDate((c) => c.add(1, 'month'))}
>
<ChevronRight size={20} />
</button>
</div>
</div>
<div className="calendar-grid">
{weekDays.map((day) => (
<div key={day} className="day-header">
{day}
</div>
))}
{dateGrid.map((day, index) => {
const appointmentsOnDay =
DictAgendamentosOrganizados[
day.format('YYYY-MM-DD')
] || [];
const cellClasses = `day-cell ${
day.isSame(currentDate, 'month')
? 'current-month'
: 'other-month'
} ${day.isSame(dayjs(), 'day') ? 'today' : ''} ${
day.isSame(selectedDay, 'day') ? 'selected' : ''
}`;
return (
<div
key={index}
className={cellClasses}
onClick={() => handleDateClick(day)}
>
<span>{day.format('D')}</span>
{appointmentsOnDay.length > 0 && (
<div className="appointments-indicator">
{appointmentsOnDay.length}
</div>
)}
</div>
);
})}
</div>
</div>
</div>
) : (
<div className="page-content table-paciente-container">
<section className="row">
<div className="col-12">
<div className="card table-paciente-card">
<div className="card-header">
<h4 className="card-title mb-0">
Minhas Solicitações em Fila de Espera
</h4>
</div>
<div className="card-body">
<div className="table-responsive">
<table className="table table-striped table-hover">
<thead>
<tr>
<th>Médico Solicitado</th>
<th>Data da Solicitação</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{filaEsperaData.length > 0 ? (
filaEsperaData.map((item) => (
<tr key={item.agendamento.id}>
<td>Dr(a). {item.Infos?.medico_nome}</td>
<td>
{dayjs(
item.agendamento.created_at
).format('DD/MM/YYYY HH:mm')}
</td>
<td>
<button
className="btn btn-sm btn-danger"
onClick={() =>
handleCancelClick(item.agendamento.id)
}
>
<i className="bi bi-trash me-1"></i>{' '}
Cancelar
</button>
</td>
</tr>
))
) : (
<tr>
<td
colSpan="3"
className="text-center py-4"
>
<div className="text-muted">
Nenhuma solicitação na fila de espera.
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
</div>
)}
</section>
</div>
) : (
<AgendamentoCadastroManager
setPageConsulta={setPageConsulta}
onSaved={() => {
carregarDados(); // recarrega consultas do paciente
setPageConsulta(false);
}}
/>
)}
{showDeleteModal &&
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
{isCancelModalOpen && (
<div className="modal-overlay">
<div className="modal-content" style={{ maxWidth: '400px' }}>
<div
className="modal-header"
style={{
backgroundColor: '#fee2e2',
borderBottom: '1px solid #fca5a5',
padding: '15px',
borderRadius: '8px 8px 0 0',
}}
>
<h4 style={{ margin: 0, color: '#dc2626' }}>
Confirmação de Cancelamento
</h4>
<div className="modal-header bg-danger bg-opacity-25">
<h5 className="modal-title text-danger">
Confirmação de Exclusão
</h5>
<button
className="close-button"
onClick={() => setIsCancelModalOpen(false)}
style={{
background: 'none',
border: 'none',
fontSize: '1.5rem',
cursor: 'pointer',
}}
>
&times;
</button>
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
></button>
</div>
<div className="modal-body" style={{ padding: '20px' }}>
<p>Qual o motivo do cancelamento?</p>
<textarea
value={cancellationReason}
onChange={(e) => setCancellationReason(e.target.value)}
placeholder="Ex: Precisei viajar, motivo pessoal, etc."
rows="4"
style={{
width: '100%',
padding: '10px',
resize: 'none',
border: '1px solid #ccc',
borderRadius: '4px',
}}
></textarea>
<div className="modal-body">
<p className="mb-0 fs-5">
Tem certeza que deseja excluir este agendamento?
</p>
</div>
<div
className="modal-footer"
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '10px',
padding: '15px',
borderTop: '1px solid #eee',
}}
>
<div className="modal-footer">
<button
className="btn btn-secondary"
onClick={() => setIsCancelModalOpen(false)}
style={{
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
padding: '8px 15px',
borderRadius: '4px',
}}
type="button"
className="btn btn-primary"
onClick={() => setShowDeleteModal(false)}
>
Cancelar
</button>
<button
type="button"
className="btn btn-danger"
onClick={executeCancellation}
style={{
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
padding: '8px 15px',
borderRadius: '4px',
}}
onClick={() => {deleteConsulta(selectedID);setShowDeleteModal(false)}}
>
<Trash2 size={16} style={{ marginRight: '5px' }} /> Excluir
<i className="bi bi-trash me-1"></i> Excluir
</button>
</div>
</div>
</div>
)}
</div>
);
};
</div>}
export default Agendamento;
</div>
</div>
)
}
export default ConsultasPaciente

View File

@ -169,7 +169,7 @@ const formatarHora = (datetimeString) => {
const handleSubmit = (e) => {
e.preventDefault();
alert("Agendamento salvo!");
navigate("/paciente/agendamento")
onSave({...agendamento, horarioInicio:horarioInicio})
};
@ -178,7 +178,11 @@ const handleSubmit = (e) => {
<form className="form-agendamento" onSubmit={handleSubmit}>
1
<h2 className="section-title">Informações do atendimento</h2>
<div className="campo-informacoes-atendimento">
<div className="campo-de-input-container"> {/* NOVO CONTAINER PAI */}

View File

@ -1,71 +1,14 @@
/* Estilo geral do card para agrupar e dar um formato */
.card-consulta {
background-color: #007bff;
display: flex;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
background-color: #007bff; /* Um tom de azul padrão */
display: flex; /* Para colocar horário e info lado a lado */
border-radius: 10px; /* Cantos arredondados */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Sombra suave */
overflow: hidden; /* Garante que o fundo azul não 'vaze' */
/* width: 280px; /* Largura de exemplo */
margin: 20px;
font-family: Arial, sans-serif;
}
@media (max-width: 768px) {
.card-consulta {
flex-direction: column;
margin: 10px;
}
.horario-container {
border-right: none;
border-bottom: 1px solid #0056b3;
padding: 10px 15px;
}
.horario {
font-size: 1.8em;
}
.info-container {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
padding: 15px;
}
.actions-container {
opacity: 1;
visibility: visible;
background: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
box-shadow: none;
margin-left: 0;
padding: 0;
justify-content: flex-start;
margin-top: 10px;
}
.card-consulta:hover .actions-container {
transform: none;
}
}
@media (max-width: 576px) {
.horario {
font-size: 1.5em;
}
.informacao {
font-size: 1em;
}
.btn-edit-custom-style,
.btn-delete-custom-style {
padding: 6px 10px;
font-size: 0.8rem;
}
font-family: Arial, sans-serif; /* Fonte legível */
}
/* 1. Estilo para o Horário (Fundo Azul e Texto Branco/Grande) */
@ -161,12 +104,3 @@
background-color: #c82333; /* Um vermelho um pouco mais escuro para o hover */
filter: brightness(90%); /* Alternativa: escurecer um pouco mais */
}
.btns-container{
display: flex;
gap: 10px;
}
.h2-proximos-agendamentos{
margin-top: 20px;
}

View File

@ -1,165 +0,0 @@
import React, { useMemo } from 'react';
import dayjs from 'dayjs';
import { ChevronLeft, ChevronRight, Edit, Trash2 } from 'lucide-react';
import Spinner from '../Spinner.jsx'; // Certifique-se de que o caminho está correto
const CalendarComponent = ({
currentDate,
setCurrentDate,
selectedDay,
setSelectedDay,
DictAgendamentosOrganizados,
showSpinner,
setSelectedId,
setShowDeleteModal,
setShowConfirmModal,
quickJump,
handleQuickJumpChange,
applyQuickJump
}) => {
// Gera os 42 dias para o grid do calendário
const generateDateGrid = () => {
const grid = [];
const startOfMonth = currentDate.startOf('month');
// Começa no domingo da semana em que o mês começa (day() retorna 0 para domingo)
let currentDay = startOfMonth.subtract(startOfMonth.day(), 'day');
// Gera 6 semanas (6*7 = 42 dias)
for (let i = 0; i < 42; i++) {
grid.push(currentDay);
currentDay = currentDay.add(1, 'day');
}
return grid;
};
const dateGrid = useMemo(() => generateDateGrid(), [currentDate]);
const weekDays = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
const handleDateClick = (day) => {
setSelectedDay(day);
// Opcional: Adicionar lógica para abrir o agendamento do dia/semana/mês (sua primeira imagem)
// Se você quiser que o clique abra a tela de agendamento de slots:
// Exemplo: onSelectDate(day);
};
// Função para obter o status de agendamentos para o indicador (ponto azul)
const getAppointmentCount = (day) => {
return DictAgendamentosOrganizados[day.format('YYYY-MM-DD')]?.length || 0;
};
return (
<div className="calendar-wrapper">
{/* Painel lateral de informações */}
<div className="calendar-info-panel">
<div className="info-date-display">
<span>{selectedDay.format('MMM')}</span>
<strong>{selectedDay.format('DD')}</strong>
</div>
<div className="info-details">
<h3>{selectedDay.format('dddd')}</h3>
<p>{selectedDay.format('D [de] MMMM [de] YYYY')}</p>
</div>
<div className="appointments-list">
<h4>Consultas para {selectedDay.format('DD/MM')}</h4>
{showSpinner ? <Spinner/> : (DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')]?.length > 0) ? (
DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')].map(app => (
<div key={app.id} className="appointment-item" data-status={app.status}>
<div className="item-time">{dayjs(app.scheduled_at).format('HH:mm')}</div>
<div className="item-details">
<span>{app.paciente_nome}</span>
<small>Dr(a). {app.medico_nome}</small>
</div>
<div className="appointment-actions">
{app.status === 'cancelled' ? (
<button className="btn-action btn-edit" onClick={() => { setSelectedId(app.id); setShowConfirmModal(true); }}>
<Edit size={16} />
</button>
) : (
<button className="btn-action btn-delete" onClick={() => { setSelectedId(app.id); setShowDeleteModal(true); }}>
<Trash2 size={16} />
</button>
)}
</div>
</div>
))
) : (
<div className="no-appointments-info"><p>Nenhuma consulta agendada.</p></div>
)}
</div>
</div>
{/* Calendário Principal */}
<div className="calendar-main">
{/* Legenda */}
<div className="calendar-legend">
<div className="legend-item" data-status="completed">Realizado</div>
<div className="legend-item" data-status="confirmed">Confirmado</div>
<div className="legend-item" data-status="agendado">Agendado</div>
<div className="legend-item" data-status="cancelled">Cancelado</div>
</div>
{/* Controles de navegação e Jump */}
<div className="calendar-controls">
<div className="date-indicator">
<h2>{currentDate.format('MMMM [de] YYYY')}</h2>
<div className="quick-jump-controls" style={{ display: 'flex', gap: '5px', marginTop: '10px' }}>
<select
value={quickJump.month}
onChange={(e) => handleQuickJumpChange('month', e.target.value)}
className="form-select form-select-sm w-auto"
>
{dayjs.months().map((month, index) => (
<option key={index} value={index}>{month.charAt(0).toUpperCase() + month.slice(1)}</option>
))}
</select>
<select
value={quickJump.year}
onChange={(e) => handleQuickJumpChange('year', e.target.value)}
className="form-select form-select-sm w-auto"
>
{Array.from({ length: 11 }, (_, i) => dayjs().year() - 5 + i).map(year => (
<option key={year} value={year}>{year}</option>
))}
</select>
<button
className="btn btn-sm btn-outline-primary"
onClick={applyQuickJump}
disabled={quickJump.month === currentDate.month() && quickJump.year === currentDate.year()}
>
Ir
</button>
</div>
</div>
<div className="nav-buttons">
<button onClick={() => { setCurrentDate(currentDate.subtract(1, 'month')); setSelectedDay(currentDate.subtract(1, 'month')); }}><ChevronLeft size={20} /></button>
<button onClick={() => { setCurrentDate(dayjs()); setSelectedDay(dayjs()); }}>Hoje</button>
<button onClick={() => { setCurrentDate(currentDate.add(1, 'month')); setSelectedDay(currentDate.add(1, 'month')); }}><ChevronRight size={20} /></button>
</div>
</div>
{/* Grid dos dias */}
<div className="calendar-grid">
{weekDays.map(day => <div key={day} className="day-header">{day}</div>)}
{dateGrid.map((day, index) => {
const count = getAppointmentCount(day);
const cellClasses = `day-cell ${day.isSame(currentDate, 'month') ? 'current-month' : 'other-month'} ${day.isSame(dayjs(), 'day') ? 'today' : ''} ${day.isSame(selectedDay, 'day') ? 'selected' : ''}`;
return (
<div
key={index}
className={cellClasses}
onClick={() => handleDateClick(day)}
>
<span>{day.format('D')}</span>
{count > 0 && <div className="appointments-indicator">{count}</div>}
</div>
);
})}
</div>
</div>
</div>
);
};
export default CalendarComponent;

View File

@ -4,122 +4,89 @@ 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, setShowConfirmModal, corModal, selectedID, coresConsultas, setListaConsultaID, listaConsultasID} ) => {
const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, setDictInfo, setSelectedId} ) => {
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,
patient_id: DadosConsulta?.patient_id,
status: DadosConsulta?.status
};
}, [DadosConsulta]);
let nameArrayPaciente = DadosConsulta?.paciente_nome?.split(' ')
let nameArrayMedico = DadosConsulta?.medico_nome?.split(' ')
let indice_cor = listaConsultasID.indexOf(DadosConsulta.id)
useEffect(() => {
const BuscarMedicoEPaciente = async () => {
if (!ids.doctor_id || !ids.patient_id || ids.status === 'nada') return;
try {
const [Doctor, Patient] = await Promise.all([
GetDoctorByID(ids.doctor_id, authHeader),
GetPatientByID(ids.patient_id, authHeader)
]);
setMedico(Doctor?.[0] || null);
setPaciente(Patient?.[0] || null);
} catch (error) {
console.error('Erro ao buscar médico/paciente:', error);
}
};
BuscarMedicoEPaciente();
}, [ids, authHeader]);
let nameArrayPaciente = Paciente?.full_name.split(' ')
let nameArrayMedico = Medico?.full_name.split(' ')
console.log(DadosConsulta.status)
return (
<div className={`container-cardconsulta container-cardconsulta-${TabelaAgendamento}`}>
{DadosConsulta.id?
<div className={`cardconsulta`} id={indice_cor !== -1 ? `status-card-consulta-${coresConsultas[indice_cor]}` : `status-card-consulta-${DadosConsulta.status}`}>
<div className='cardconsulta' id={`status-card-consulta-${DadosConsulta.status}`}>
<div>
<section className='cardconsulta-infosecundaria'>
<p>Medico:{DadosConsulta.horario} {nameArrayMedico && nameArrayMedico.length > 0 ? nameArrayMedico[0] : ''} {nameArrayMedico && nameArrayMedico.length > 1 ? ` ${nameArrayMedico[1]}` : ''} </p>
<p>{DadosConsulta.horario} {nameArrayMedico && nameArrayMedico.length > 0 ? nameArrayMedico[0] : ''} {nameArrayMedico && nameArrayMedico.length > 1 ? ` ${nameArrayMedico[1]}` : ''} </p>
</section>
<section className='cardconsulta-infoprimaria'>
<p>Paciente: {nameArrayPaciente && nameArrayPaciente.length > 0 ? nameArrayPaciente[0] : ''} {nameArrayPaciente && nameArrayPaciente.length > 1 ? ` ${nameArrayPaciente[1]}` : ''}- {}
</p>
<p>{nameArrayPaciente && nameArrayPaciente.length > 0 ? nameArrayPaciente[0] : ''} {nameArrayPaciente && nameArrayPaciente.length > 1 ? ` ${nameArrayPaciente[1]}` : ''}- {}</p>
</section>
</div>
<div className='actions-container'>
<button className="btn btn-sm btn-edit-custom"
onClick={() => {
navigate(`edit`);
console.log(DadosConsulta);
setDictInfo({
...DadosConsulta,
paciente_cpf: DadosConsulta?.paciente_cpf,
paciente_nome: DadosConsulta?.paciente_nome,
nome_medico: DadosConsulta?.medico_nome
});
onClick={() => {navigate(`2/edit`)
setDictInfo({...DadosConsulta,paciente_cpf:Paciente.cpf, paciente_nome:Paciente.full_name, nome_medico:Medico.full_name})
}}
>
<i className="bi bi-pencil me-1"></i>
</button>
{indice_cor !== -1 ? (
// Caso o ID esteja na lista
<>
{coresConsultas[indice_cor] === "cancelled" ?
<button
className="btn btn-sm btn-confirm-style"
onClick={() => {
console.log(DadosConsulta.id);
setShowConfirmModal(true);
setSelectedId(DadosConsulta.id);
}}
>
<i className="bi bi-check-lg"></i>
</button>
:
<button
className="btn btn-sm btn-delete-custom-style "
onClick={() => {
console.log(DadosConsulta.id);
console.log(DadosConsulta.id)
setSelectedId(DadosConsulta.id);
setShowDeleteModal(true);
}}
>
<i className="bi bi-trash me-1"></i>
</button>
}
</>
) : (
// 🧩 Caso normal segue a lógica do status
<>
{DadosConsulta.status === "cancelled" ? (
<button
className="btn btn-sm btn-confirm-style"
onClick={() => {
console.log(DadosConsulta.id);
setShowConfirmModal(true);
setSelectedId(DadosConsulta.id);
}}
>
<i className="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>
:
null
@ -127,8 +94,6 @@ const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, se
}
</div>
)
}

View File

@ -1,366 +1,265 @@
import InputMask from "react-input-mask";
import "./style/formagendamentos.css";
import { useState, useEffect, useCallback } from "react";
import { GetPatientByCPF, GetAllPatients } from "../utils/Functions-Endpoints/Patient";
import { GetAllDoctors } from "../utils/Functions-Endpoints/Doctor";
import { useState, useEffect } from "react";
import { GetPatientByCPF } from "../utils/Functions-Endpoints/Patient";
import { GetDoctorByName, 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();
const {getAuthorizationHeader} = useAuth()
const [sessoes, setSessoes] = useState(1);
const [tempoBaseConsulta] = useState(30);
const [horarioInicio, setHorarioInicio] = useState("");
const [horarioTermino, setHorarioTermino] = useState("");
const [horariosDisponiveis, sethorariosDisponiveis] = useState([]);
const [status, setStatus] = useState(agendamento?.status || "confirmed");
const [isSubmitting, setIsSubmitting] = useState(false);
console.log(agendamento, 'aqui2')
const [todosProfissionais, setTodosProfissionais] = useState([]);
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 [todosPacientes, setTodosPacientes] = useState([]);
const [pacientesFiltrados, setPacientesFiltrados] = useState([]);
const [isDropdownPacienteOpen, setIsDropdownPacienteOpen] = useState(false);
const authHeader = getAuthorizationHeader();
const [horarioInicio, setHorarioInicio] = useState('');
const [horarioTermino, setHorarioTermino] = useState('');
const [horariosDisponiveis, sethorariosDisponiveis] = useState([])
let authHeader = getAuthorizationHeader()
const FormatCPF = (valor) => {
const digits = String(valor).replace(/\D/g, "").slice(0, 11);
const digits = String(valor).replace(/\D/g, '').slice(0, 11);
return digits
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d)/, "$1.$2")
.replace(/(\d{3})(\d{1,2})$/, "$1-$2");
};
.replace(/(\d{3})(\d)/, '$1.$2')
.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)
if (name === "email") {
if(name === 'email'){
setAgendamento({...agendamento, contato:{
...agendamento.contato,
email:value
}})}
else if(name === 'status'){
if(agendamento.status==='requested'){
setAgendamento((prev) => ({
...prev,
contato: {
...(prev.contato || {}),
email: value,
},
status:'confirmed',
}));
} else if (name === "status") {
}else if(agendamento.status === 'confirmed'){
console.log(value)
setAgendamento((prev) => ({
...prev,
status: prev.status === "requested" ? "confirmed" : "requested",
status:'requested',
}));
setStatus((prev) => (prev === "confirmed" ? "requested" : "confirmed"));
} else if (name === "paciente_cpf") {
const cpfFormatted = FormatCPF(value);
}}
else if(name === 'paciente_cpf'){
let cpfFormatted = FormatCPF(value)
const fetchPatient = async () => {
const patientData = await GetPatientByCPF(cpfFormatted, authHeader);
let patientData = await GetPatientByCPF(cpfFormatted, authHeader);
if (patientData) {
setAgendamento((prev) => ({
...prev,
paciente_nome: patientData.full_name,
patient_id: patientData.id,
patient_id: patientData.id
}));
}}
setAgendamento(prev => ({ ...prev, cpf: cpfFormatted }))
fetchPatient()
}else if(name==='convenio'){
setAgendamento({...agendamento,insurance_provider:value})
}
};
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]);
const ChamarPacientes = useCallback(async () => {
const Pacientes = await GetAllPatients(authHeader);
setTodosPacientes(Pacientes || []);
}, [authHeader]);
// AUTOCOMPLETE PACIENTE
const handleSearchPaciente = (e) => {
const term = e.target.value;
setAgendamento((prev) => ({ ...prev, paciente_nome: term }));
if (term.trim() === "") {
setPacientesFiltrados([]);
setIsDropdownPacienteOpen(false);
return;
}
const filtered = todosPacientes.filter((p) =>
p.full_name.toLowerCase().includes(term.toLowerCase())
);
setPacientesFiltrados(filtered);
setIsDropdownPacienteOpen(filtered.length > 0);
useEffect(() => {
const ChamarMedicos = async () => {
const Medicos = await GetAllDoctors(authHeader)
setTodosProfissionais(Medicos)
}
ChamarMedicos();
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("apikey", API_KEY)
myHeaders.append("Authorization", `Bearer ${authHeader.split(' ')[1]}`);
var raw = JSON.stringify({
"doctor_id": agendamento.doctor_id,
"start_date": agendamento.dataAtendimento,
"end_date": `${agendamento.dataAtendimento}T23:59:59.999Z`,
});
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
const handleSelectPaciente = (paciente) => {
setAgendamento((prev) => ({
...prev,
patient_id: paciente.id,
paciente_nome: paciente.full_name,
paciente_cpf: paciente.cpf,
}));
setPacientesFiltrados([]);
setIsDropdownPacienteOpen(false);
};
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));
// AUTOCOMPLETE PROFISSIONAL
}, [agendamento.dataAtendimento, agendamento.doctor_id])
// FUNÇÃO DE BUSCA E FILTRAGEM
const handleSearchProfissional = (e) => {
const term = e.target.value;
handleChange(e);
if (term.trim() === "") {
// 2. Lógica de filtragem:
if (term.trim() === '') {
setProfissionaisFiltrados([]);
setIsDropdownOpen(false);
return;
}
const filtered = todosProfissionais.filter((p) =>
// Adapte o nome da propriedade (ex: 'nome', 'full_name')
const filtered = todosProfissionais.filter(p =>
p.full_name.toLowerCase().includes(term.toLowerCase())
);
setProfissionaisFiltrados(filtered);
setIsDropdownOpen(filtered.length > 0);
setIsDropdownOpen(filtered.length > 0); // Abre se houver resultados
};
const handleSelectProfissional = (profissional) => {
setAgendamento((prev) => ({
// FUNÇÃO PARA SELECIONAR UM ITEM DO DROPDOWN
const handleSelectProfissional = async (profissional) => {
setAgendamento(prev => ({
...prev,
doctor_id: profissional.id,
nome_medico: profissional.full_name,
nome_medico: profissional.full_name
}));
// 2. Fecha o dropdown
setProfissionaisFiltrados([]);
setIsDropdownOpen(false);
};
const formatarHora = (datetimeString) => {
return datetimeString?.substring(11, 16) || "";
return datetimeString.substring(11, 16);
};
useEffect(() => {
if (agendamento?.scheduled_at) {
setHorarioInicio(formatarHora(agendamento.scheduled_at));
}
}, []);
const opcoesDeHorario = horariosDisponiveis?.slots?.map(item => ({
useEffect(() => {
ChamarMedicos();
ChamarPacientes();
}, [ChamarMedicos, ChamarPacientes]);
useEffect(() => {
if (!agendamento.dataAtendimento || !agendamento.doctor_id) return;
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", `Bearer ${authHeader.split(" ")[1]}`);
const raw = JSON.stringify({
doctor_id: agendamento.doctor_id,
start_date: agendamento.dataAtendimento,
end_date: `${agendamento.dataAtendimento}T23:59:59.999Z`,
});
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) => {
sethorariosDisponiveis(result);
})
.catch((error) => console.log("error", error));
}, [agendamento.dataAtendimento, agendamento.doctor_id, authHeader]);
const slotsArray = Array.isArray(horariosDisponiveis)
? horariosDisponiveis
: horariosDisponiveis?.slots || [];
const opcoesDeHorario = slotsArray.map((item) => ({
value: formatarHora(item.datetime),
label: formatarHora(item.datetime),
disabled: !item.available,
disabled: !item.available
}));
const calcularHorarioTermino = useCallback((inicio, sessoesParam, tempoBase) => {
if (!inicio || inicio.length !== 5 || !inicio.includes(":")) return "";
const calcularHorarioTermino = (inicio, sessoes, tempoBase) => {
if (!inicio || inicio.length !== 5 || !inicio.includes(':')) return '';
const [horas, minutos] = inicio.split(":").map(Number);
const minutosInicio = horas * 60 + minutos;
const duracaoTotalMinutos = sessoesParam * tempoBase;
const [horas, minutos] = inicio.split(':').map(Number);
const minutosInicio = (horas * 60) + minutos;
const duracaoTotalMinutos = sessoes * tempoBase;
const minutosTermino = minutosInicio + duracaoTotalMinutos;
const horaTermino = Math.floor(minutosTermino / 60) % 24;
const minutoTermino = minutosTermino % 60;
const formatar = (num) => String(num).padStart(2, "0");
return `${formatar(horaTermino)}:${formatar(minutoTermino)}`;
const formatar = (num) => String(num).padStart(2, '0');
}, []);
return `${formatar(horaTermino)}:${formatar(minutoTermino)}`;
};
useEffect(() => {
const novoTermino = calcularHorarioTermino(
horarioInicio,
sessoes,
tempoBaseConsulta
);
// 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) => ({
setAgendamento(prev => ({
...prev,
horarioTermino: novoTermino,
horarioTermino: novoTermino
}));
}, [horarioInicio, sessoes, tempoBaseConsulta, calcularHorarioTermino, setAgendamento]);
}, [horarioInicio, sessoes, tempoBaseConsulta, setAgendamento]);
const handleSubmit = (e) => {
e.preventDefault();
if (isSubmitting) return;
if (
!agendamento.doctor_id ||
!agendamento.patient_id ||
!agendamento.dataAtendimento ||
!horarioInicio
) {
alert(
"Por favor, preencha o profissional, paciente, data e horário de início."
);
return;
}
setIsSubmitting(true);
const payload = {
...agendamento,
horarioInicio,
status,
duration_minutes: sessoes * tempoBaseConsulta,
};
onSave?.(payload);
setIsSubmitting(false);
};
const handleCheckbox = () => {
if (status === "confirmed") {
setStatus("requested");
setAgendamento((prev) => ({ ...prev, status: "requested" }));
} else {
setStatus("confirmed");
setAgendamento((prev) => ({ ...prev, status: "confirmed" }));
}
alert("Agendamento salvo!");
onSave({...agendamento, horarioInicio:horarioInicio})
};
return (
<div className="form-container">
<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="campo-de-input-container">
<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={handleSearchPaciente}
autoComplete="off"
/>
</div>
<div className="campos-informacoes-paciente" id="informacoes-paciente-linha-um">
<div className="campo-de-input">
<label>CPF do paciente</label>
<InputMask
mask="999.999.999-99"
maskChar={null}
type="text"
name="paciente_cpf"
placeholder="000.000.000-00"
value={agendamento.paciente_cpf || ""}
onChange={handleChange}
autoComplete="off"
/>
<input type="text" name="paciente_cpf" placeholder="000.000.000-00" onChange={handleChange} value={agendamento.paciente_cpf}/>
</div>
{isDropdownPacienteOpen && pacientesFiltrados.length > 0 && (
<div className="dropdown-pacientes">
{pacientesFiltrados.map((paciente) => (
<div
key={paciente.id}
className="dropdown-item"
onClick={() => handleSelectPaciente(paciente)}
>
{`${paciente.full_name} - ${paciente.cpf}`}
</div>
))}
</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
className="campos-informacoes-paciente"
id="informacoes-paciente-linha-tres"
>
<div >
<label>Convênio</label>
<select
name="convenio"
onChange={handleChange}
value={agendamento.insurance_provider || "publico"}
>
<select name="convenio" onChange={handleChange}>
<option value="publico">Público</option>
<option value="unimed">Unimed</option>
<option value="bradesco_saude">Bradesco Saúde</option>
<option value="hapvida">Hapvida</option>
</select>
</div>
</div>
<h2 className="section-title">Informações do atendimento</h2>
<div className="campo-informacoes-atendimento">
<div className="campo-de-input-container">
<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"
name="nome_medico" // Use o nome correto da propriedade no estado `agendamento`
onChange={handleSearchProfissional}
value={agendamento?.nome_medico || ""}
autoComplete="off"
value={agendamento?.nome_medico}
autoComplete="off" // Ajuda a evitar o autocomplete nativo do navegador
required
/>
</div>
{/* DROPDOWN - RENDERIZAÇÃO CONDICIONAL */}
{isDropdownOpen && profissionaisFiltrados.length > 0 && (
<div className="dropdown-profissionais">
<div className='dropdown-profissionais'>
{profissionaisFiltrados.map((profissional) => (
<div
key={profissional.id}
className="dropdown-item"
key={profissional.id} // Use o ID do profissional
className='dropdown-item'
onClick={() => handleSelectProfissional(profissional)}
>
{profissional.full_name}
@ -372,45 +271,38 @@ const FormNovaConsulta = ({ onCancel, onSave, setAgendamento, agendamento }) =>
<div className="tipo_atendimento">
<label>Tipo de atendimento *</label>
<select
name="tipo_atendimento"
onChange={handleChange}
value={agendamento.tipo_atendimento || "presencial"}
>
<option value="presencial">Presencial</option>
<select onChange={handleChange} name="tipo_atendimento" >
<option value="presencial" selected>Presencial</option>
<option value="teleconsulta">Teleconsulta</option>
</select>
</div>
</div>
<section id="informacoes-atendimento-segunda-linha">
<section id="informacoes-atendimento-segunda-linha-esquerda">
<div className="campo-informacoes-atendimento">
<div className="campo-de-input">
<label>Data *</label>
<input
type="date"
name="dataAtendimento"
onChange={handleChange}
value={agendamento.dataAtendimento || ""}
required
/>
<input type="date" name="dataAtendimento" onChange={handleChange} required />
</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)}
required
>
<option value="" disabled>
Selecione a hora de início
</option>
{opcoesDeHorario.map((opcao, index) => (
<option value="" disabled>Selecione a hora de início</option>
{opcoesDeHorario?.map((opcao, index) => (
<option
key={index}
value={opcao.value}
@ -423,52 +315,71 @@ const FormNovaConsulta = ({ onCancel, onSave, setAgendamento, agendamento }) =>
</select>
</div>
{/* SELETOR DE SESSÕES MODIFICADO */}
{/* Removemos o 'label' para evitar o desalinhamento e colocamos o texto acima */}
<div className='seletor-wrapper'>
<label>Número de Sessões *</label> {/* Novo label para o seletor */}
<div className='sessao-contador'>
<button
type="button" /* Adicionado para evitar submissão de formulário */
onClick={() => {if(sessoes === 0)return; else(setSessoes(sessoes - 1))}}
disabled={sessoes === 0} /* Desabilita o botão no limite */
>
<i className="bi bi-chevron-compact-left"></i>
</button>
<p className='sessao-valor'>{sessoes}</p> {/* Adicionada classe para estilização */}
<button
type="button" /* Adicionado para evitar submissão de formulário */
onClick={() => {if(sessoes === 3 )return; else(setSessoes(sessoes + 1))}}
disabled={sessoes === 3} /* Desabilita o botão no limite */
>
<i className="bi bi-chevron-compact-right"></i>
</button>
</div>
</div>
<div className="campo-de-input">
<label htmlFor="termino">Término *</label>
<input
type="text"
id="termino"
name="termino"
value={horarioTermino || "— —"}
value={horarioTermino || '— —'}
readOnly
className="horario-termino-readonly"
/>
</div>
</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"
onChange={handleChange}
value={agendamento.observacoes || ""}
></textarea>
<textarea name="observacoes" rows="4" cols="1"></textarea>
</div>
</section>
</section>
</section>
<div className="form-actions">
<button
type="submit"
className="btn-primary"
disabled={isSubmitting}
>
{isSubmitting ? "Salvando..." : "Salvar agendamento"}
</button>
<button
type="button"
className="btn-cancel"
onClick={onCancel}
>
Cancelar
</button>
<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,30 +2,15 @@ import React, { useState, useEffect } from 'react';
import CardConsulta from './CardConsulta';
import "./style/styleTabelas/tabeladia.css";
import Spinner from '../Spinner';
const TabelaAgendamentoDia = ({ agendamentos, setShowDeleteModal, setDictInfo,selectedID, setSelectedId, setShowConfirmModal, coresConsultas, setListaConsultaID, listaConsultasID }) => {
const [indiceAcesso, setIndiceAcesso] = useState(null)
const TabelaAgendamentoDia = ({ handleClickAgendamento, agendamentos, setShowDeleteModal, setDictInfo, setSelectedId }) => {
const [indiceAcesso, setIndiceAcesso] = useState(0)
const [Dia, setDia] = useState()
const agendamentosDoDia = agendamentos?.semana1?.segunda || [];
const nomeMedico = agendamentosDoDia.find(item => item.medico)?.medico || 'Profissional';
let ListaDiasComAgendamentos = Object.keys(agendamentos)
const [showSpinner, setShowSpinner] = useState(true);
useEffect(() => {
if (!agendamentos) return;
const dias = Object.keys(agendamentos);
if (dias.length > 0) {
setIndiceAcesso(0); // começa no primeiro dia disponível
setDia(dias[0]); // seta o Dia inicial
setShowSpinner(false)
}
}, [agendamentos]);
console.log(agendamentos)
//console.log(Dia, "hshdhshhsdhs")
@ -34,28 +19,6 @@ useEffect(() => {
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>
@ -64,7 +27,7 @@ useEffect(() => {
<button onClick={() => {if(indiceAcesso === 0)return; else(setIndiceAcesso(indiceAcesso - 1))}}> <i className="bi bi-chevron-compact-left"></i></button>
<p>{Dia ? formatarDataComDia(Dia) : ''}</p>
<p>{Dia ? `${Dia?.split('-')[2]}/${Dia?.split('-')[1]}/${Dia?.split('-')[0]}`: ''}</p>
<button onClick={() => {if(ListaDiasComAgendamentos.length - 1 === indiceAcesso)return; else(setIndiceAcesso(indiceAcesso + 1))}}> <i className="bi bi-chevron-compact-right"></i></button>
</div>
@ -87,20 +50,12 @@ useEffect(() => {
<td className='coluna-horario'><p className='horario-texto'>{`${horario[0]}:${horario[1]}`}</p></td>
<td className='mostrar-horario'>
<div>
<CardConsulta DadosConsulta={agendamento} TabelaAgendamento={'dia'} setShowDeleteModal={setShowDeleteModal} setDictInfo={setDictInfo} setSelectedId={setSelectedId} selectedID={selectedID} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
<div onClick={() => handleClickAgendamento(agendamento)}>
<CardConsulta DadosConsulta={agendamento} TabelaAgendamento={'dia'} setShowDeleteModal={setShowDeleteModal} setDictInfo={setDictInfo} setSelectedId={setSelectedId}/>
</div>
</td>
</tr>
)})}
{showSpinner &&
<tr>
<td colspan='2'>
<Spinner/>
</td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -6,23 +6,15 @@ import "./style/styleTabelas/tabelames.css";
import { useEffect, useState } from 'react';
import { useMemo } from 'react';
import Spinner from '../Spinner';
const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModal, setSelectedId ,setDictInfo, setShowConfirmModal, coresConsultas ,setListaConsultaID, listaConsultasID }) => {
const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModal, setSelectedId ,setDictInfo }) => {
const dataHoje = dayjs();
const AnoAtual = dataHoje.year();
const mes = dataHoje.month() + 1;
const [showSpinner, setShowSpinner] = useState(true)
console.log(agendamentos)
let ListaDiasDatas = ListarDiasdoMes(AnoAtual, mes);
const [AgendamentosSemanaisOrganizados, setAgendamentosSemanaisOrganizados] = useState({})
const [indice, setIndice] = useState(mes.toString())
const [indice, setIndice] = useState("10")
const [AgendamentosMensaisOrganizados, setAgendamentosMensaisOrganizados] = useState({
"01": { "nomeDoMes": "janeiro" },
@ -41,17 +33,6 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModa
useEffect(() => {
if (!agendamentos) return;
const meses = Object.keys(agendamentos);
if (meses.length > 0) {
// começa no primeiro dia disponível
setIndice(mes.toString()); // seta o Dia inicial
setShowSpinner(false)
}
}, [agendamentos]);
const OrganizarAgendamentosSemanais = useMemo(() => {
if (!agendamentos || Object.keys(agendamentos).length === 0) return {};
@ -59,6 +40,7 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModa
const DiasComAtendimentos = Object.keys(agendamentos)
const semanas = {}
for (let i = 0; i < DiasComAtendimentos.length; i++) {
const DiaComAtendimento = DiasComAtendimentos[i]
const [_, MesDoAgendamento, DiaDoAgendamento] = DiaComAtendimento.split("-")
@ -74,19 +56,19 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModa
}
switch (diaSemana) {
case 'segunda-feira':
case 'Monday':
semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento])
break
case 'terça-feira':
case 'Tuesday':
semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento])
break
case 'quarta-feira':
case 'Wednesday':
semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento])
break
case 'quinta-feira':
case 'Thursday':
semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento])
break
case 'sexta-feira':
case 'Friday':
semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento])
break
default:
@ -220,12 +202,9 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModa
{
semana && typeof semana === "object" && Object.keys(semana).map((dia) => (
<td key={dia} >
<div className='dia-tabelamensal'>
<p> {(semana[dia]|| [])[0]?.scheduled_at.split("-")[2].split("T")[0]}</p>
</div>
<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}/>
<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}/>
{semana[dia].length > 3 ? (
<div>
<p>{` +${semana[dia].length - 2}`}</p>
@ -236,16 +215,8 @@ const TabelaAgendamentoMes = ({ ListarDiasdoMes, agendamentos, setShowDeleteModa
))
}
</tr>
)})}
{showSpinner &&
<tr>
<td colspan='5'>
<Spinner/>
</td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -4,42 +4,20 @@ import "./style/styleTabelas/tabelasemana.css";
import dayjs from 'dayjs';
import { useEffect, useState, useMemo } from 'react';
import weekOfYear from 'dayjs/plugin/weekOfYear'
import Spinner from '../Spinner';
dayjs.extend(weekOfYear)
const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteModal ,setSelectedId ,setDictInfo, setShowConfirmModal, coresConsultas ,setListaConsultaID, listaConsultasID}) => {
const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteModal ,setSelectedId ,setDictInfo}) => {
// 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);
const [showSpinner, setShowSpinner] = useState(true)
useEffect(() => {
if (!agendamentos) return;
const semanas = Object.keys(agendamentos);
if (semanas.length > 0) {
setIndice(0)
setShowSpinner(false)
}
}, [agendamentos]);
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);
@ -68,34 +46,30 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
segunda: [], terça: [], quarta: [], quinta: [], sexta: []
}
}
console.log(diaSemana)
switch (diaSemana) {
case 'segunda-feira':
console.log("segunda")
case 'Monday':
semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento])
break
case 'terça-feira':
case 'Tuesday':
semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento])
break
case 'quarta-feira':
case 'Wednesday':
semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento])
break
case 'quinta-feira':
case 'Thursday':
semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento])
break
case 'sexta-feira':
case 'Friday':
semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento])
break
default:
break
}
}
console.log(semanas, "agendamentos semanais")
return semanas
}, [agendamentos, AnoAtual])
}, [agendamentos, AnoAtual]) // Adicionei AnoAtual como dependência por segurança
// --- EFEITO PARA POPULAR O ESTADO ---
@ -149,10 +123,10 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
? `Semana ${chaveDaSemanaAtual.replace('semana', '')} / ${AnoAtual}`
: 'Nenhuma semana encontrada';
// --- RENDERIZAÇÃO ---
return (
<div>
{/* Container de Navegação */}
<div id='tabela-seletor-container'>
<button
@ -185,65 +159,54 @@ 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(semanaParaRenderizar, "aqui")
console.log(horario)
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 TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar?.segunda[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
{semanaParaRenderizar.segunda[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.segunda[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />
: null
}
</td>
<td>
{semanaParaRenderizar.terça[indiceLinha]
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.terça[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
? <CardConsulta DadosConsulta={semanaParaRenderizar.terça[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/>
: null
}
</td>
<td>
{semanaParaRenderizar.quarta[indiceLinha]
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.quarta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
? <CardConsulta DadosConsulta={semanaParaRenderizar.quarta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/>
: null
}
</td>
<td>
{semanaParaRenderizar.quinta[indiceLinha]
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.quinta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
? <CardConsulta DadosConsulta={semanaParaRenderizar.quinta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />
: null
}
</td>
<td>
{semanaParaRenderizar.sexta[indiceLinha]
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.sexta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID} />
? <CardConsulta DadosConsulta={semanaParaRenderizar.sexta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />
: null
}
</td>
</tr>
)})}
{showSpinner &&
<tr>
<td colspan='6'>
<Spinner/>
</td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -1,10 +1,3 @@
@media (max-width: 768px) {
.container-cardconsulta {
padding-right: 80px; /* Espaço para os botões */
position: relative;
}
}
.actions-container {
display: flex;
gap: 8px;
@ -57,84 +50,12 @@
/* 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) */
/* .fs-4 do Bootstrap já cuida do tamanho do ícone. Se precisar de mais controle, adicione aqui. */
.btn-confirm-style{
background-color: #5ce687;
}
.card-verde{
background-color: #b7ffbd;
border: #91d392;
}
/* Aplique isso às classes que contêm os nomes do Médico e do Paciente */
.cardconsulta-infosecundaria p,
.cardconsulta-infoprimaria p {
/* 1. Força o texto a não quebrar para a próxima linha */
white-space: nowrap;
/* 2. Oculta qualquer texto que ultrapasse a largura do contêiner */
overflow: hidden;
/* 3. Adiciona reticências (...) ao final do texto truncado */
text-overflow: ellipsis;
}
.tabelamensal .container-cardconsulta{
width: 24rem;
}
@media (max-width: 768px) {
.actions-container {
opacity: 1;
visibility: visible;
background: none;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: none;
box-shadow: none;
margin-left: 0;
padding: 0;
justify-content: flex-end;
position: absolute;
top: 10px;
right: 10px;
}
.container-cardconsulta:hover .actions-container {
transform: none;
}
.tabelamensal .container-cardconsulta {
width: 100%;
max-width: 100%;
}
}
@media (max-width: 576px) {
.container-cardconsulta {
padding-right: 10px; /* Remove o padding extra para telas muito pequenas */
}
.actions-container {
position: static;
margin-top: 10px;
justify-content: flex-start;
}
.btn-edit-custom-style,
.btn-delete-custom-style,
.btn-confirm-style {
padding: 6px 10px;
font-size: 0.8rem;
}
.action-button .bi {
/* Exemplo: se precisar de um ajuste fino além do fs-4 */
/* font-size: 1.5rem; */
}

View File

@ -43,6 +43,8 @@ svg{
font-family: 'Material Symbols Outlined';
font-size: 20px;
color:black
}
.form-container {
@ -150,6 +152,7 @@ svg{
background: #e5e7eb;
}
.cardconsulta-infosecundaria{
font-size: small;
}
@ -163,8 +166,10 @@ svg{
.campo-de-input{
display: flex;
flex-direction: column;
}
#informacoes-atendimento-segunda-linha{
margin-top: 10px;
display: flex;
@ -180,74 +185,13 @@ textarea{
.campos-informacoes-paciente,
.campo-informacoes-atendimento {
display: flex;
gap: 16px;
}
@media (max-width: 768px) {
.campos-informacoes-paciente,
.campo-informacoes-atendimento {
flex-direction: column;
gap: 10px;
}
#informacoes-atendimento-segunda-linha {
flex-direction: column;
gap: 10px;
}
#informacoes-atendimento-segunda-linha-esquerda select[name="unidade"],
input[type="time"],
select[name=solicitante],
.campo-de-input {
width: 100% !important;
max-width: 100%;
}
.tipo_atendimento {
margin-left: 0;
}
.linha {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.sessao-contador {
width: 100px;
}
}
@media (max-width: 576px) {
.form-container {
padding: 15px;
}
.form-title {
font-size: 22px;
}
.form-agendamento input,
.form-agendamento select,
.form-agendamento textarea {
font-size: 13px;
}
.form-actions {
flex-direction: column;
gap: 10px;
}
.btn-primary,
.btn-cancel {
width: 100%;
}
gap: 16px; /* espaço entre campos */
}
.campo-de-input {
flex: 1;
flex: 1; /* todos os filhos ocupam mesmo espaço */
display: flex;
flex-direction: column;
flex-direction: column; /* mantém label em cima do input */
}
#informacoes-atendimento-segunda-linha-esquerda select[name="unidade"]{
@ -269,7 +213,7 @@ select[name=solicitante]{
.form-container {
width: 100%;
max-width: none;
margin: 0;
margin: 0; /* >>> sem espaço para encostar no topo <<< */
background: #ffffff;
border-radius: 12px;
padding: 24px;
@ -362,24 +306,29 @@ html[data-bs-theme="dark"] svg {
color: #e0e0e0 !important;
}
/* CONTAINER PAI - ESSENCIAL PARA POSICIONAMENTO */
.campo-de-input-container {
position: relative;
position: relative; /* Define o contexto para o dropdown */
/* ... outros estilos de layout (display, margin, etc.) ... */
}
/* ESTILO DA LISTA DROPDOWN */
.dropdown-profissionais {
position: absolute;
top: 100%;
position: absolute; /* Flutua em relação ao pai (.campo-de-input-container) */
top: 100%; /* Começa logo abaixo do input */
left: 0;
width: 100%;
width: 100%; /* Ocupa toda a largura do container pai */
/* Estilos visuais */
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 100;
z-index: 100; /* Alto z-index para garantir que fique acima de outros elementos */
max-height: 200px;
overflow-y: auto;
}
/* ESTILO DE CADA ITEM DO DROPDOWN */
.dropdown-item {
padding: 10px;
cursor: pointer;
@ -391,453 +340,135 @@ html[data-bs-theme="dark"] svg {
.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;
width: 1.2rem;
/* Define o tamanho desejado */
width: 1.2rem; /* Ajuste conforme o seu gosto (ex: 1.2rem = 19.2px) */
height: 1.2rem;
background-color: #fff;
border: 1px solid #000;
border-radius: 0.25rem;
/* 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) */
display: inline-block;
vertical-align: middle;
cursor: pointer;
transition: all 0.5s ease;
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 */
}
/* 2. Estilização no Estado Clicado (:checked) */
.checkbox-customs:checked {
/* Quando clicado, mantém o fundo branco (se quiser mudar, altere aqui) */
background-color: #fff;
/* Se você quiser que a borda mude de cor ao clicar, altere aqui. */
/* border-color: #007bff; */ /* Exemplo: borda azul */
}
/* 3. Ocultar o 'Check' Padrão e Criar um Check Customizado */
/* O Bootstrap/Navegador insere um ícone de 'check'. Vamos controlá-lo com background-image. */
.checkbox-customs:checked {
/* Este código do Bootstrap usa um SVG para o ícone de 'check' */
/* Aqui, estamos forçando o ícone de 'check' a ser preto para combinar com a borda preta. */
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23000' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
/* Garante que o ícone fique centralizado e preencha o espaço */
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
}
/* Container dos três elementos na linha */
.linha {
display: flex;
align-items: flex-end;
gap: 20px;
align-items: flex-end; /* Garante que os campos de input e o seletor fiquem alinhados pela base */
gap: 20px; /* Espaçamento entre os campos */
}
/* ------------------------------------------- */
/* 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;
background-color: #e9ecef;
border: 1px solid #ced4da;
border-radius: 0.25rem;
height: 40px;
width: 100px;
padding: 0 5px;
/* 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 */
font-size: 1rem;
font-weight: 500;
}
.sessao-valor {
/* Estilo do número de sessões */
margin: 0;
padding: 0 5px;
font-size: 1.1rem;
color: #007bff;
font-size: 1.1rem; /* Um pouco maior que o texto dos selects */
color: #007bff; /* Cor azul destacada (como na sua imagem) */
}
.sessao-contador button {
/* Estilo dos botões de chevron */
background: none;
border: none;
cursor: pointer;
padding: 0 2px;
color: #495057;
font-size: 1.5rem;
line-height: 1;
color: #495057; /* Cor do ícone */
font-size: 1.5rem; /* Aumenta o tamanho dos ícones do chevron */
line-height: 1; /* Alinha o ícone verticalmente */
transition: color 0.2s;
}
.sessao-contador button:hover:not(:disabled) {
color: #007bff;
color: #007bff; /* Cor azul ao passar o mouse */
}
.sessao-contador button:disabled {
cursor: not-allowed;
color: #adb5bd;
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) */
}
/* ========== Modal Overlay ========== */
.modal-overlay {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 9999;
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* ========== Modal Content ========== */
.modal-content {
background-color: #fff;
border-radius: 10px;
width: 400px;
max-width: 90%;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
overflow: hidden;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* ========== Modal Header ========== */
.modal-header {
background-color: #1e3a8a;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header.success {
background-color: #1e3a8a !important;
}
.modal-header.error {
background-color: #dc3545 !important;
}
.modal-header .modal-title {
color: #fff;
margin: 0;
font-size: 1.2rem;
font-weight: bold;
}
.modal-close-btn {
background: none;
border: none;
font-size: 20px;
color: #fff;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.modal-close-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* ========== Modal Body ========== */
.modal-body {
padding: 25px 20px;
background: #fff;
}
.modal-body .modal-message {
color: #111;
font-size: 1.1rem;
margin: 0;
font-weight: 600;
line-height: 1.4;
text-align: center;
}
.modal-submessage {
color: #666;
font-size: 0.9rem;
margin: 10px 0 0 0;
line-height: 1.4;
text-align: center;
}
/* ========== Modal Footer ========== */
.modal-footer {
display: flex;
justify-content: flex-end;
padding: 15px 20px;
border-top: 1px solid #ddd;
background: #fff;
}
.modal-confirm-btn {
background-color: #1e3a8a;
color: #fff;
border: none;
padding: 8px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: all 0.2s ease;
}
.modal-confirm-btn:hover {
background-color: #1e40af;
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.modal-confirm-btn.success {
background-color: #1e3a8a !important;
}
.modal-confirm-btn.success:hover {
background-color: #1e40af !important;
}
.modal-confirm-btn.error {
background-color: #dc3545 !important;
}
.modal-confirm-btn.error:hover {
background-color: #c82333 !important;
}
/* ========== Dark Mode ========== */
html[data-bs-theme="dark"] .modal-content {
background: #232323 !important;
border: 1px solid #404053;
}
html[data-bs-theme="dark"] .modal-header {
background: #1e3a8a !important;
}
html[data-bs-theme="dark"] .modal-header.success {
background-color: #1e3a8a !important;
}
html[data-bs-theme="dark"] .modal-header.error {
background-color: #dc3545 !important;
}
html[data-bs-theme="dark"] .modal-header .modal-title,
html[data-bs-theme="dark"] .modal-close-btn {
color: white !important;
}
html[data-bs-theme="dark"] .modal-body {
background: #232323 !important;
}
html[data-bs-theme="dark"] .modal-body .modal-message {
color: #e0e0e0 !important;
}
html[data-bs-theme="dark"] .modal-submessage {
color: #b0b7c3 !important;
}
html[data-bs-theme="dark"] .modal-footer {
background: #232323 !important;
border-top: 1px solid #404053;
}
html[data-bs-theme="dark"] .modal-confirm-btn {
background: #2563eb !important;
}
html[data-bs-theme="dark"] .modal-confirm-btn:hover {
background: #1e40af !important;
}
/* ========== Responsive ========== */
@media (max-width: 768px) {
.modal-content {
width: 95%;
margin: 1rem;
}
.modal-body {
padding: 20px 15px;
}
.modal-message {
font-size: 1rem;
}
}
@media (max-width: 480px) {
.modal-header {
padding: 12px 15px;
}
.modal-header .modal-title {
font-size: 1.1rem;
}
.modal-body {
padding: 15px;
}
.modal-footer {
padding: 12px 15px;
}
.modal-confirm-btn {
padding: 6px 16px;
font-size: 0.9rem;
}
}
.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;
}
.campo-cpf{
margin-left: 40px;
}
input[name="paciente_cpf"]{
width: 12rem;
}
.dropdown-pacientes{
position: absolute;
top: 100%;
left: 0;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
z-index: 100;
max-height: 200px;
overflow-y: auto;
}
#informacoes-atendimento-segunda-linha .linha-horarios {
display: flex;
gap: 16px;
align-items: flex-end; /* alinha pela base dos inputs */
}
#informacoes-atendimento-segunda-linha .linha-horarios .campo-de-input {
flex: 1;
}
.campo-de-input-container {
display: flex;
gap: 16px; /* nome e cpf na mesma linha */
flex-wrap: wrap;
}
.campo-de-input {
display: flex;
flex-direction: column;
margin-bottom: 12px;
}
.campo-de-input label {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
}
.campo-de-input input,
.campo-de-input select,
.campo-de-input textarea {
width: 220px; /* ajuste pro layout que você quer */
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 14px;
}
/* placeholder visível e suave */
.campo-de-input input::placeholder {
color: #999;
opacity: 1; /* garante no Firefox */
}
/* bloco da coluna esquerda (Data, Início, Término) */
#informacoes-atendimento-segunda-linha-esquerda {
display: flex;
flex-direction: column;
gap: 12px;
}
/* linha com Início e Término */
#informacoes-atendimento-segunda-linha-esquerda .linha {
display: flex;
gap: 16px;
align-items: flex-end;
}
/* mesma largura pros três campos */
#informacoes-atendimento-segunda-linha-esquerda .campo-de-input input,
#informacoes-atendimento-segunda-linha-esquerda .campo-de-input select {
width: 230px;
box-sizing: border-box;
}
.informacoes-atendimento-segunda-linha-direita {
width: 100%;
}
.informacoes-atendimento-segunda-linha-direita .campo-de-input textarea {
width: 100%; /* ocupa toda a coluna da direita */
min-height: 150px; /* aumenta a altura (muda pra 200, 250 se quiser maior) */
resize: vertical;
box-sizing: border-box;
}
#informacoes-atendimento-segunda-linha {
display: grid;
grid-template-columns: auto 1.8fr; /* coluna da direita grande, mas não infinita */
gap: 24px;
}
/* garante que o container da direita não estoure */
.informacoes-atendimento-segunda-linha-direita {
max-width: 800px; /* ajusta se quiser menor/maior */
width: 100%;
}
.informacoes-atendimento-segunda-linha-direita .campo-de-input textarea {
width: 100%;
min-height: 150px;
resize: vertical;
box-sizing: border-box;
}

View File

@ -6,8 +6,6 @@
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

@ -231,9 +231,3 @@ html[data-bs-theme="dark"] .cards-que-faltam {
}
.dia-tabelamensal p {
font-weight: bold; /* Deixa o número em negrito */
color: #0078d7; /* Garante que seja preto */
font-size: 16px; /* Ajuste o tamanho para harmonizar com o restante */
/* Adicione a mesma família de fonte usada para o restante do app, se necessário */
}

View File

@ -6,7 +6,6 @@
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 */
@ -68,10 +67,10 @@
.tabelasemanal tr:hover {
background-color: #f1f1f1 !important;
}
/*
tr{
width: 1000px;
}*/
}
html[data-bs-theme="dark"] .tabelasemanal {
border: 4px solid #333;
@ -112,5 +111,3 @@ html[data-bs-theme="dark"] .tabelasemanal .cardconsulta {
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
border-left: 5px solid #333;
}

View File

@ -1,242 +0,0 @@
@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

@ -1,158 +0,0 @@
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

@ -1,467 +0,0 @@
/* 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

@ -1,171 +0,0 @@
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

@ -1,339 +0,0 @@
/* ========== Variáveis CSS ========== */
:root {
--toggle-bg: #ffffff;
--toggle-border: #e5e7eb;
--toggle-hover: #f3f4f6;
--toggle-active: #dbeafe;
--toggle-text: #1f2937;
--toggle-text-secondary: #374151;
--toggle-icon: #6b7280;
--toggle-accent: #2563eb;
--toggle-accent-hover: #1d4ed8;
--toggle-shadow: rgba(0, 0, 0, 0.05);
--toggle-shadow-hover: rgba(0, 0, 0, 0.1);
}
/* ========== Container Principal ========== */
.toggle-sidebar-wrapper {
background: var(--toggle-bg);
border-radius: 12px;
border: 1px solid var(--toggle-border);
margin-bottom: 16px;
overflow-y: auto;
overflow-x: hidden;
box-shadow: 0 1px 3px var(--toggle-shadow);
transition: box-shadow 0.2s ease;
scrollbar-width: none;
-ms-overflow-style: none;
}
.toggle-sidebar-wrapper::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
.toggle-sidebar-wrapper:hover {
box-shadow: 0 4px 6px var(--toggle-shadow-hover);
}
.container-title {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
cursor: pointer;
background-color: var(--toggle-bg);
transition: background-color 0.2s ease;
border-bottom: 1px solid var(--toggle-border);
}
.container-title:hover {
background-color: var(--toggle-hover);
}
.toggle-title {
color: var(--toggle-text);
font-weight: 600;
font-size: 16px;
margin: 0;
user-select: none;
letter-spacing: -0.01em;
}
.toggle-arrow {
color: var(--toggle-icon);
transition: transform 0.2s ease;
font-size: 14px;
}
.container-title:hover .toggle-arrow {
color: var(--toggle-accent);
}
/* ========== Menu Lista ========== */
.sidebar-menu-list {
list-style: none;
padding: 8px;
margin: 0;
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
min-height: 100%;
/* Scroll invisível mas funcional */
scrollbar-width: none;
-ms-overflow-style: none;
}
.sidebar-menu-list::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
.sidebar-item {
list-style: none;
margin: 2px 0;
}
/* ========== Links da Sidebar ========== */
.sidebar-link {
display: flex;
align-items: center;
text-decoration: none;
color: var(--toggle-text-secondary);
padding: 11px 14px;
border-radius: 8px;
font-size: 15px;
width: 100%;
text-align: left;
border: none;
background: none;
cursor: pointer;
font-weight: 500;
transition: none;
gap: 12px;
}
.sidebar-link i {
font-size: 19px;
color: var(--toggle-icon);
transition: none;
}
.sidebar-link:hover {
background-color: var(--toggle-hover);
color: var(--toggle-text);
}
.sidebar-link:hover i {
color: var(--toggle-accent);
}
.sidebar-link.active {
background-color: var(--toggle-active);
color: var(--toggle-accent);
font-weight: 600;
}
.sidebar-link.active i {
color: var(--toggle-accent);
}
/* ========== Título da Sidebar ========== */
.sidebar-title {
padding: 18px 14px 10px;
font-size: 13px;
color: var(--toggle-text-secondary);
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
}
/* ========== Itens com Submenu ========== */
.sidebar-item.has-sub {
margin: 4px 0;
}
.sidebar-item.has-sub .sidebar-link {
position: relative;
}
.sidebar-item.has-sub .sidebar-link::after {
content: "\F285";
font-family: "bootstrap-icons";
margin-left: auto;
transition: transform 0.2s ease;
font-size: 12px;
color: var(--toggle-icon);
}
.sidebar-item.has-sub.active .sidebar-link::after {
transform: rotate(180deg);
}
.sidebar-item.has-sub .sidebar-link.submenu-active {
background-color: var(--toggle-hover);
color: var(--toggle-text);
}
.sidebar-item.has-sub .sidebar-link.submenu-active i {
color: var(--toggle-accent);
}
/* ========== Submenu ========== */
.submenu {
list-style: none;
padding: 4px 0 4px 8px;
margin: 0;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
/* Scroll invisível */
scrollbar-width: none;
-ms-overflow-style: none;
}
.submenu::-webkit-scrollbar {
display: none;
}
.sidebar-item.has-sub.active .submenu {
max-height: 1000px;
overflow: visible;
}
.submenu-item {
margin: 2px 0;
}
.submenu-item .sidebar-link {
padding: 9px 14px 9px 40px;
font-size: 14px;
position: relative;
}
.submenu-item .sidebar-link::before {
content: '';
position: absolute;
left: 22px;
top: 50%;
transform: translateY(-50%);
width: 5px;
height: 5px;
border-radius: 50%;
background-color: var(--toggle-icon);
}
.submenu-item .sidebar-link:hover::before {
background-color: var(--toggle-accent);
}
.submenu-item .sidebar-link.active::before {
background-color: var(--toggle-accent);
}
/* ========== Ícones e Indicadores ========== */
.external-icon {
font-size: 11px;
margin-left: auto;
opacity: 0.6;
color: var(--toggle-icon);
}
.active-indicator {
display: none;
}
/* ========== Botão de Logout ========== */
.logout-item {
margin-top: auto;
padding-top: 16px;
border-top: 1px solid var(--toggle-border);
position: sticky;
bottom: 0;
background: var(--toggle-bg);
z-index: 10;
}
.logout-button {
display: flex;
align-items: center;
text-decoration: none;
color: #dc3545;
padding: 11px 14px;
border-radius: 8px;
font-size: 15px;
width: 100%;
text-align: left;
border: none;
background: none;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s ease;
gap: 12px;
}
.logout-button i {
font-size: 19px;
color: #dc3545;
}
.logout-button:hover {
background-color: #fee;
color: #c82333;
}
.logout-button:hover i {
color: #c82333;
}
/* ========== Scroll Invisível mas Funcional ========== */
/* Containers do Mazer template que envolvem o ToggleSidebar */
#sidebar,
#sidebar .sidebar-wrapper,
#sidebar .sidebar-wrapper.active,
.sidebar-wrapper,
.sidebar-wrapper.active,
.sidebar-menu,
.sidebar-menu ul,
.sidebar-menu ul.menu,
ul.menu {
overflow-y: auto !important;
overflow-x: hidden !important;
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
#sidebar::-webkit-scrollbar,
#sidebar .sidebar-wrapper::-webkit-scrollbar,
#sidebar .sidebar-wrapper.active::-webkit-scrollbar,
.sidebar-wrapper::-webkit-scrollbar,
.sidebar-wrapper.active::-webkit-scrollbar,
.sidebar-menu::-webkit-scrollbar,
.sidebar-menu ul::-webkit-scrollbar,
.sidebar-menu ul.menu::-webkit-scrollbar,
ul.menu::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
background: transparent !important;
}
/* ========== Dark Mode Support ========== */
html[data-bs-theme="dark"] {
--toggle-bg: #1f2937;
--toggle-border: #374151;
--toggle-hover: #374151;
--toggle-active: #1e3a8a;
--toggle-text: #f9fafb;
--toggle-text-secondary: #d1d5db;
--toggle-icon: #9ca3af;
--toggle-accent: #60a5fa;
--toggle-accent-hover: #3b82f6;
--toggle-shadow: rgba(0, 0, 0, 0.3);
--toggle-shadow-hover: rgba(0, 0, 0, 0.5);
}

View File

@ -1,13 +1,13 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { useAuth } from "./utils/AuthProvider";
import API_KEY from "./utils/apiKeys";
import "./AgendarConsulta/style/formagendamentos.css";
import { GetAllDoctors } from './utils/Functions-Endpoints/Doctor';
const ENDPOINT_CRIAR_EXCECAO = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_exceptions";
const FormCriarExcecao = ({ onCancel, doctorID }) => {
const { getAuthorizationHeader, user, getUserInfo } = useAuth();
const [dadosAtendimento, setDadosAtendimento] = useState({
profissional: doctorID || '',
@ -18,13 +18,6 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
motivo: ''
});
const [todosProfissionais, setTodosProfissionais] = useState([]);
const [profissionaisFiltrados, setProfissionaisFiltrados] = useState([]);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [doctorSearchName, setDoctorSearchName] = useState('');
const [searchingDoctor, setSearchingDoctor] = useState(false);
const handleAtendimentoChange = (e) => {
const { value, name } = e.target;
setDadosAtendimento(prev => ({
@ -33,52 +26,6 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
}));
};
useEffect(() => {
const loadDoctors = async () => {
setSearchingDoctor(true);
let authHeader = '';
try { authHeader = getAuthorizationHeader ? getAuthorizationHeader() : ''; } catch {}
try {
const Medicos = await GetAllDoctors(authHeader);
setTodosProfissionais(Array.isArray(Medicos) ? Medicos : []);
} catch (err) {
console.error('Erro ao carregar médicos:', err);
setTodosProfissionais([]);
} finally {
setSearchingDoctor(false);
}
};
loadDoctors();
}, [getAuthorizationHeader]);
const handleSearchProfissional = (e) => {
const term = e.target.value;
setDoctorSearchName(term);
if (term.trim() === '') {
setProfissionaisFiltrados([]);
setIsDropdownOpen(false);
return;
}
const filtered = todosProfissionais.filter(p =>
(p.full_name || '').toLowerCase().includes(term.toLowerCase())
);
setProfissionaisFiltrados(filtered);
setIsDropdownOpen(filtered.length > 0);
};
const handleSelectProfissional = (profissional) => {
setDadosAtendimento(prev => ({
...prev,
profissional: profissional.id
}));
setDoctorSearchName(profissional.full_name || '');
setProfissionaisFiltrados([]);
setIsDropdownOpen(false);
};
// lista simples de valores permitidos
const ALLOWED_KINDS = ['disponibilidade_extra', 'bloqueio'];
const handleSubmitExcecao = async (e) => {
e.preventDefault();
console.log("Tentando criar Exceção.");
@ -90,13 +37,6 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
return;
}
// usa diretamente o value selecionado (já definido no <select>) e valida
const mappedKind = tipoAtendimento;
if (!ALLOWED_KINDS.includes(mappedKind)) {
alert(`Tipo inválido: "${tipoAtendimento}". Tipos aceitos: ${ALLOWED_KINDS.join(', ')}`);
return;
}
const startTime = inicio ? inicio + ":00" : null;
const endTime = termino ? termino + ":00" : null;
@ -130,7 +70,7 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
const raw = JSON.stringify({
doctor_id: profissional,
date: dataAtendimento,
kind: mappedKind,
kind: tipoAtendimento,
start_time: startTime,
end_time: endTime,
reason: motivo,
@ -179,30 +119,7 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
<h2 className="section-title">Informações da Nova Exceção</h2>
<div className="campo-informacoes-atendimento">
{/* Busca por nome usando filtragem local */}
<div className="campo-de-input campo-de-input-container">
<label>Nome do médico</label>
<input
type="text"
name="doctorSearchName"
placeholder="Digite o nome do médico"
value={doctorSearchName}
onChange={handleSearchProfissional}
autoComplete="off"
/>
{isDropdownOpen && profissionaisFiltrados.length > 0 && (
<div className="dropdown-profissionais">
{profissionaisFiltrados.map(p => (
<div key={p.id} className="dropdown-item" onClick={() => handleSelectProfissional(p)}>
{p.full_name}
</div>
))}
</div>
)}
{searchingDoctor && <small>Carregando médicos...</small>}
</div>
{/* ID do profissional (preenchido ao selecionar) */}
<div className="campo-de-input">
<label>ID do profissional *</label>
<input
@ -217,11 +134,12 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
<div className="campo-de-input">
<label>Tipo de exceção *</label>
<select name="tipoAtendimento" onChange={handleAtendimentoChange} value={dadosAtendimento.tipoAtendimento} required>
<option value="" disabled>Selecione o tipo de exceção</option>
<option value="disponibilidade_extra" >Liberação</option>
<option value="bloqueio" >Bloqueio</option>
<option value="">Selecione o tipo de exceção</option>
<option value="liberacao" >Liberação (Criar Slot)</option>
<option value="bloqueio" >Bloqueio (Remover Slot)</option>
</select>
</div>
</div>
<section id="informacoes-atendimento-segunda-linha">

View File

@ -1,5 +1,6 @@
import React, { useState } from 'react'
import React from 'react'
import '../PagesMedico/styleMedico/FormNovoRelatorio.css'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../components/utils/AuthProvider'
import { GetPatientByCPF } from '../components/utils/Functions-Endpoints/Patient'
@ -13,49 +14,6 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
const [showModal, setShowModal] = useState(false)
// --- NOVO: Estado para controlar o loading da transcrição ---
const [isTranscribing, setIsTranscribing] = useState(false);
// --- NOVA FUNÇÃO: Envia o áudio e preenche o formulário ---
const handleAudioUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
setIsTranscribing(true); // Ativa o spinner
const formData = new FormData();
formData.append('audio', file); // 'audio' deve ser o nome esperado no backend
try {
// ATENÇÃO: Substitua essa URL pela rota do seu backend que criamos
const response = await fetch('http://localhost:3001/api/transcrever-relatorio', {
method: 'POST',
body: formData,
// headers: { 'Authorization': authHeader } // Descomente se seu backend precisar de token
});
if (!response.ok) throw new Error("Falha na transcrição");
const data = await response.json();
// Atualiza o DictInfo com os dados vindos da IA
setDictInfo((prev) => ({
...prev,
exam: data.exam || prev.exam, // Preenche se a IA achou, senão mantém o antigo
diagnostico: data.diagnostico || prev.diagnostico,
conclusao: data.conclusao || prev.conclusao
}));
} catch (error) {
console.error("Erro no upload de áudio:", error);
alert("Não foi possível gerar o relatório por áudio. Verifique o backend.");
} finally {
setIsTranscribing(false); // Desativa o spinner
e.target.value = null; // Limpa o input para permitir enviar o mesmo arquivo novamente se quiser
}
};
// -----------------------------------------------------------
const BaixarPDFdoRelatorio = () => {
const elemento = document.getElementById("folhaA4"); // tua div do relatório
const opt = {
@ -100,16 +58,20 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
console.log(DictInfo)
setShowModal(true)
onSave({
"patient_id": DictInfo.paciente_id,
"exam": DictInfo.exam,
"diagnosis": DictInfo.diagnostico, // Garanta que o backend espera 'diagnosis' mas seu state usa 'diagnostico'
"diagnosis": DictInfo.diagnosis,
"conclusion": DictInfo.conclusao,
"status": "draft",
"requested_by": DictInfo.requested_by,
"hide_date": false,
"hide_signature": false,
});
}
return (
@ -148,28 +110,6 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
<div className='card'>
{/* --- ÁREA DE UPLOAD DE ÁUDIO (INSERIDA AQUI) --- */}
<div className="p-3 mb-3 border-bottom bg-light">
<label className="form-label fw-bold">🎙 Preenchimento Automático via Áudio</label>
<div className="d-flex align-items-center gap-3">
{isTranscribing ? (
<div className="d-flex align-items-center text-primary">
<div className="spinner-border spinner-border-sm me-2" role="status"></div>
<span>A IA está gerando o relatório... aguarde.</span>
</div>
) : (
<input
type="file"
className="form-control"
accept="audio/*"
onChange={handleAudioUpload}
/>
)}
</div>
<small className="text-muted">Envie um áudio ditando o exame, diagnóstico e conclusão.</small>
</div>
{/* ----------------------------------------------- */}
<form action="" onSubmit={handleSubmit}>
<div id='primeiraLinha'>
@ -191,7 +131,7 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
<div className="col-md-2 mb-3">
<label >Exame:</label>
<input type="text" className="form-control" name="exam" onChange={handleChange} value={DictInfo.exam || ''} />
<input type="text" className="form-control" name="exam" onChange={handleChange} />
</div>
@ -237,8 +177,7 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
<p>Paciente: {DictInfo?.paciente_nome}</p>
<p>Data de nascimento: </p>
{/* Corrigi de data_exam para data_exame para bater com o state */}
<p>Data do exame: {DictInfo.data_exame}</p>
<p>Data do exame: {DictInfo.data_exam}</p>
<p>Exame: {DictInfo.exam}</p>
@ -250,7 +189,7 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
<div>
<p>Dr {DictInfo.requested_by}</p>
<p>Emitido em: {new Date().toLocaleDateString()}</p>
<p>Emitido em: 0</p>
</div>
</div>

View File

@ -20,11 +20,6 @@
font-size: 24px;
cursor: pointer;
padding: 5px;
transition: transform 0.2s ease;
}
.phone-icon-container:hover {
transform: scale(1.1);
}
.phone-icon {
@ -38,173 +33,75 @@
}
.profile-picture-container {
width: 45px;
height: 45px;
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
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;
border: 2px solid #ccc;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
.profile-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-color: #A9A9A9;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.placeholder-icon {
font-size: 20px;
color: white;
.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;
}
.profile-dropdown {
position: absolute;
top: 60px;
top: 50px;
right: 0;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
z-index: 1000;
min-width: 180px;
min-width: 150px;
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: 12px 16px;
padding: 10px 15px;
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: #f8f9fa;
background-color: #f0f0f0;
}
.logout-button {
color: #dc3545;
border-top: 1px solid #f0f0f0;
color: #cc0000;
}
.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;
@ -223,8 +120,6 @@
z-index: 2001;
margin-top: 80px;
margin-right: 20px;
/* Adicionado para responsividade */
max-width: 90vw;
}
.suporte-card {
@ -292,7 +187,6 @@
margin-bottom: 0;
}
/* Chat Online */
.chat-overlay {
position: fixed;
top: 0;
@ -311,8 +205,6 @@
z-index: 3001;
margin-top: 80px;
margin-right: 20px;
/* Adicionado para responsividade */
max-width: 90vw;
}
.chat-online {
@ -354,7 +246,6 @@
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.fechar-chat:hover {
@ -369,7 +260,6 @@
display: flex;
flex-direction: column;
gap: 1rem;
background-color: #fafafa;
}
.mensagem {
@ -377,53 +267,33 @@
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: #007bff;
color: white;
background-color: #e3f2fd;
border-bottom-right-radius: 4px;
}
.mensagem.suporte {
align-self: flex-start;
background-color: white;
border: 1px solid #e0e0e0;
background-color: #f5f5f5;
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;
opacity: 0.8;
color: #666;
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 {
@ -443,164 +313,93 @@
outline: none;
font-size: 0.9rem;
background-color: white;
transition: border-color 0.2s;
}
.chat-campo:focus {
border-color: #007bff;
border-color: #1e3a8a;
}
.chat-enviar {
background-color: #007bff;
background-color: #1e3a8a;
color: white;
border: none;
padding: 0.75rem 1.5rem;
padding: 0.75rem 1rem;
border-radius: 20px;
cursor: pointer;
font-size: 0.9rem;
}
.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;
}
.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;
}
.chat-enviar:hover {
background-color: #0056b3;
.logout-cancel-button:hover {
background-color: #f0f0f0;
}
/* 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-top: 60px;
margin-right: 10px;
margin-left: 10px;
}
.suporte-card,
.chat-online {
width: calc(100vw - 20px);
max-width: none;
}
}
@media (max-width: 576px) {
.header-container {
padding: 8px 10px;
}
.right-corner-elements {
gap: 10px;
}
.profile-picture-container {
width: 35px;
height: 35px;
}
.phone-icon-container {
font-size: 20px;
}
.suporte-card-container,
.chat-container {
margin-top: 50px;
margin-right: 5px;
margin-left: 5px;
}
.suporte-card {
padding: 1rem;
}
.chat-online {
width: calc(100vw - 10px);
height: 80vh; /* Limita a altura para telas pequenas */
}
.chat-input {
padding: 0.75rem;
}
.chat-campo {
padding: 0.5rem;
}
.chat-enviar {
padding: 0.5rem 1rem;
}
}
@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;
}
}
.header-container {
pointer-events: none;
}
.phone-icon-container,
.profile-section {
pointer-events: auto;
}
.header-container { pointer-events: auto; }
.phone-icon-container, .profile-section { pointer-events: auto; }
.logout-modal-overlay, .suporte-card-overlay, .chat-overlay {
z-index: 110000 !important;
pointer-events: auto !important;
}
.logout-cancel-button {
padding: 10px 18px;
border-radius: 8px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
}
.logout-confirm-button {
padding: 10px 18px;
border-radius: 8px;
padding: 0.75rem 1.5rem;
border: none;
background: #dc3545;
color: #fff;
border-radius: 8px;
background-color: #dc3545;
color: white;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.logout-confirm-button:hover {
background-color: #c82333;
}

View File

@ -1,84 +1,36 @@
// src/components/Header/Header.jsx
import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import './Header.css';
const Header = () => {
// --- hooks (sempre na mesma ordem) ---
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isSuporteCardOpen, setIsSuporteCardOpen] = useState(false);
const [isChatOpen, setIsChatOpen] = useState(false);
const [mensagem, setMensagem] = useState('');
const [mensagens, setMensagens] = useState([]);
const [showLogoutModal, setShowLogoutModal] = useState(false);
const [avatarUrl, setAvatarUrl] = useState(null);
const navigate = useNavigate();
const location = useLocation();
const chatInputRef = useRef(null);
const mensagensContainerRef = useRef(null);
// foco quando abre chat
useEffect(() => {
if (isChatOpen && chatInputRef.current) {
chatInputRef.current.focus();
}
}, [isChatOpen]);
// scroll automático quando nova mensagem
useEffect(() => {
if (mensagensContainerRef.current) {
mensagensContainerRef.current.scrollTop = mensagensContainerRef.current.scrollHeight;
}
}, [mensagens]);
// carrega avatar se existir
useEffect(() => {
const loadAvatar = () => {
const localAvatar = localStorage.getItem('user_avatar');
if (localAvatar) setAvatarUrl(localAvatar);
};
loadAvatar();
const onStorage = () => loadAvatar();
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, []);
// ESC fecha qualquer overlay/portal aberto (logout / suporte / chat)
useEffect(() => {
const onKey = (e) => {
if (e.key === 'Escape') {
if (showLogoutModal) setShowLogoutModal(false);
if (isSuporteCardOpen) setIsSuporteCardOpen(false);
if (isChatOpen) setIsChatOpen(false);
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [showLogoutModal, isSuporteCardOpen, isChatOpen]);
// --- handlers logout (mantive comportamento) ---
// Funções de Logout (do seu código)
const handleLogoutClick = () => {
setShowLogoutModal(true);
setIsDropdownOpen(false);
};
const clearAuthData = () => {
["token","authToken","userToken","access_token","user","auth","userData"].forEach(key => {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
});
if (window.caches) {
caches.keys().then(names => {
names.forEach(name => {
if (name.includes("auth") || name.includes("api")) caches.delete(name);
});
}).catch(()=>{});
}
};
const handleLogoutConfirm = async () => {
try {
const token =
@ -90,34 +42,57 @@ const Header = () => {
sessionStorage.getItem("authToken");
if (token) {
try {
await fetch("https://mock.apidog.com/m1/1053378-0-default/auth/v1/logout", {
const response = await fetch(
"https://mock.apidog.com/m1/1053378-0-default/auth/v1/logout",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
} catch (err) {
// ignora erro de rede / endpoint prossegue para limpar local
console.warn('logout endpoint error (ignored):', err);
}
);
if (response.status === 204) console.log("Logout realizado com sucesso");
else if (response.status === 401) console.log("Token inválido ou expirado");
else {
try {
const errorData = await response.json();
console.error("Erro no logout:", errorData);
} catch {
console.error("Erro no logout - status:", response.status);
}
}
}
clearAuthData();
navigate('/login');
} catch (err) {
console.error('Erro no logout:', err);
navigate("/login");
} catch (error) {
console.error("Erro durante logout:", error);
clearAuthData();
navigate('/login');
navigate("/login");
} finally {
setShowLogoutModal(false);
}
};
const clearAuthData = () => {
["token","authToken","userToken","access_token","user","auth","userData"].forEach(key => {
localStorage.removeItem(key);
sessionStorage.removeItem(key);
});
if (window.caches) {
caches.keys().then(names => {
names.forEach(name => {
if (name.includes("auth") || name.includes("api")) caches.delete(name);
});
});
}
};
const handleLogoutCancel = () => setShowLogoutModal(false);
// --- profile / suporte / chat handlers ---
const handleProfileClick = () => {
setIsDropdownOpen(!isDropdownOpen);
if (isSuporteCardOpen) setIsSuporteCardOpen(false);
@ -130,12 +105,14 @@ const Header = () => {
};
const handleSuporteClick = () => {
setIsSuporteCardOpen((s) => !s);
setIsDropdownOpen(false);
setIsSuporteCardOpen(!isSuporteCardOpen);
if (isDropdownOpen) setIsDropdownOpen(false);
if (isChatOpen) setIsChatOpen(false);
};
const handleCloseSuporteCard = () => setIsSuporteCardOpen(false);
const handleCloseSuporteCard = () => {
setIsSuporteCardOpen(false);
};
const handleChatClick = () => {
setIsChatOpen(true);
@ -170,7 +147,9 @@ const Header = () => {
setMensagem('');
setTimeout(() => {
if (chatInputRef.current) chatInputRef.current.focus();
if (chatInputRef.current) {
chatInputRef.current.focus();
}
}, 0);
setTimeout(() => {
@ -181,18 +160,19 @@ const Header = () => {
'Já encaminhei sua solicitação para nossa equipe técnica.',
'Vou ajudar você a resolver isso!'
];
const respostaSuporte = {
id: Date.now() + 1,
texto: respostas[Math.floor(Math.random() * respostas.length)],
remetente: 'suporte',
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
};
setMensagens(prev => [...prev, respostaSuporte]);
}, 900);
}, 1000);
};
// --- subcomponentes (UI) ---
const SuporteCardContent = ({ onOpenChat }) => (
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>
@ -211,7 +191,7 @@ const Header = () => {
</div>
</div>
<div className="contato-item clickable" onClick={onOpenChat} role="button" tabIndex={0}>
<div className="contato-item clickable" onClick={handleChatClick}>
<div className="contato-info">
<div className="contato-nome">Chat Online</div>
<div className="contato-descricao">Disponível 24/7</div>
@ -220,11 +200,11 @@ const Header = () => {
</div>
);
const ChatOnlineContent = ({ mensagens, onSend, onClose }) => (
<div className="chat-online" role="dialog" aria-modal="true">
const ChatOnline = () => (
<div className="chat-online">
<div className="chat-header">
<h3 className="chat-titulo">Chat de Suporte</h3>
<button type="button" className="fechar-chat" onClick={onClose} aria-label="Fechar chat">×</button>
<button type="button" className="fechar-chat" onClick={handleCloseChat}>×</button>
</div>
<div className="chat-mensagens" ref={mensagensContainerRef}>
@ -236,7 +216,7 @@ const Header = () => {
))}
</div>
<form className="chat-input" onSubmit={onSend}>
<form className="chat-input" onSubmit={handleEnviarMensagem}>
<input
ref={chatInputRef}
type="text"
@ -251,140 +231,20 @@ const Header = () => {
</div>
);
// --- portals: Logout / Suporte / Chat (garante top-most e clickable) ---
const PortalWrapper = ({ children, z = 99999 }) => {
if (typeof document === 'undefined') return null;
return createPortal(
<div style={{ position: 'fixed', inset: 0, zIndex: z }}>
{children}
</div>,
document.body
);
};
const LogoutModalPortal = ({ onCancel, onConfirm }) => {
if (typeof document === 'undefined') return null;
return createPortal(
<div
className="logout-modal-overlay"
style={{
position: 'fixed',
top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 110000
}}
onClick={onCancel}
>
<div
className="logout-modal-content"
style={{
backgroundColor: 'white',
padding: '1.6rem',
borderRadius: '12px',
boxShadow: '0 8px 24px rgba(0,0,0,0.2)',
maxWidth: '480px',
width: '100%',
textAlign: 'center'
}}
onClick={(e) => e.stopPropagation()}
>
<h3 style={{ marginTop: 0 }}>Confirmar Logout</h3>
<p>Tem certeza que deseja encerrar a sessão?</p>
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center', marginTop: '1rem' }}>
<button onClick={onCancel} className="logout-cancel-button">Cancelar</button>
<button onClick={onConfirm} className="logout-confirm-button">Sair</button>
</div>
</div>
</div>,
document.body
);
};
const SuportePortal = ({ onClose, children }) => {
if (typeof document === 'undefined') return null;
return createPortal(
<div
className="suporte-card-overlay"
style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'transparent',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'flex-start',
zIndex: 105000,
pointerEvents: 'auto'
}}
onClick={onClose}
>
<div
className="suporte-card-container"
style={{ marginTop: '80px', marginRight: '20px' }}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
document.body
);
};
const ChatPortal = ({ onClose, children }) => {
if (typeof document === 'undefined') return null;
return createPortal(
<div
className="chat-overlay"
style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'flex-start',
zIndex: 115000,
pointerEvents: 'auto'
}}
onClick={onClose}
>
<div
className="chat-container"
style={{ marginTop: '80px', marginRight: '20px' }}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>,
document.body
);
};
// --- evita render na rota de login (mantendo hooks invocados) ---
if (location.pathname === '/login') {
return null;
}
// --- JSX principal (header visual) ---
return (
<div className="header-container" style={{ pointerEvents: 'auto' }}>
<div className="header-container">
<div className="right-corner-elements">
<div
className="phone-icon-container"
onClick={handleSuporteClick}
role="button"
tabIndex={0}
style={{ pointerEvents: 'auto' }}
>
<div className="phone-icon-container" onClick={handleSuporteClick}>
<span className="phone-icon" role="img" aria-label="telefone">📞</span>
</div>
<div className="profile-section" style={{ pointerEvents: 'auto' }}>
<div className="profile-picture-container" onClick={handleProfileClick} role="button" tabIndex={0}>
<div className="profile-section">
<div className="profile-picture-container" onClick={handleProfileClick}>
<div className="profile-placeholder"></div>
</div>
{isDropdownOpen && (
<div className="profile-dropdown" onClick={(e) => e.stopPropagation()}>
<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>
</div>
@ -392,21 +252,38 @@ const Header = () => {
</div>
</div>
{/* logout modal via portal */}
{showLogoutModal && <LogoutModalPortal onCancel={handleLogoutCancel} onConfirm={handleLogoutConfirm} />}
{/* suporte portal */}
{isSuporteCardOpen && (
<SuportePortal onClose={handleCloseSuporteCard}>
<SuporteCardContent onOpenChat={handleChatClick} />
</SuportePortal>
{/* Modal de Logout */}
{showLogoutModal && (
<div className="logout-modal-overlay">
<div className="logout-modal-content">
<h3>Confirmar Logout</h3>
<p>Tem certeza que deseja encerrar a sessão?</p>
<div className="logout-modal-buttons">
<button onClick={handleLogoutCancel} className="logout-cancel-button">
Cancelar
</button>
<button onClick={handleLogoutConfirm} className="logout-confirm-button">
Sair
</button>
</div>
</div>
</div>
)}
{isSuporteCardOpen && (
<div className="suporte-card-overlay" onClick={handleCloseSuporteCard}>
<div className="suporte-card-container" onClick={(e) => e.stopPropagation()}>
<SuporteCard />
</div>
</div>
)}
{/* chat portal */}
{isChatOpen && (
<ChatPortal onClose={handleCloseChat}>
<ChatOnlineContent mensagens={mensagens} onSend={handleEnviarMensagem} onClose={handleCloseChat} />
</ChatPortal>
<div className="chat-overlay">
<div className="chat-container">
<ChatOnline />
</div>
</div>
)}
</div>
);

View File

@ -1,16 +1,7 @@
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import TrocardePerfis from "./TrocardePerfis";
import MobileMenuToggle from "./MobileMenuToggle";
import ToggleSidebar from "./ToggleSidebar";
import { useAuth } from "./utils/AuthProvider";
import PacienteItems from "../data/sidebar-items-paciente.json"
import DoctorItems from "../data/sidebar-items-medico.json"
import admItems from "../data/sidebar-items-adm.json"
import SecretariaItems from "../data/sidebar-items-secretaria.json"
import FinanceiroItems from "../data/sidebar-items-financeiro.json"
import { UserInfos } from "./utils/Functions-Endpoints/General";
function Sidebar({ menuItems }) {
const [isActive, setIsActive] = useState(true);
@ -19,24 +10,6 @@ function Sidebar({ menuItems }) {
const [showLogoutModal, setShowLogoutModal] = useState(false);
const navigate = useNavigate();
const [roleUser, setRoleUser] = useState([])
const {getAuthorizationHeader} = useAuth();
const authHeader = getAuthorizationHeader();
let pathname = window.location.pathname.split("/")[1]
// useEffect para definir quais toggle da sidebar devem aparecer
useEffect(() => {
let teste = localStorage.getItem("roleUser")
setRoleUser(teste)
}, [authHeader])
// Detecta se é mobile/tablet
useEffect(() => {
const checkScreenSize = () => {
@ -45,7 +18,6 @@ function Sidebar({ menuItems }) {
setIsActive(!mobile);
};
checkScreenSize();
window.addEventListener("resize", checkScreenSize);
return () => window.removeEventListener("resize", checkScreenSize);
@ -119,7 +91,6 @@ function Sidebar({ menuItems }) {
const handleLogoutCancel = () => setShowLogoutModal(false);
const renderLink = (item) => {
if (item.url && item.url.startsWith("/")) {
return (
@ -242,41 +213,65 @@ function Sidebar({ menuItems }) {
<div className="sidebar-menu">
<ul className="menu">
{menuItems &&
menuItems.map((item, index) => {
if (item.isTitle)
return (
<li key={index} className="sidebar-title">
{item.name}
</li>
);
{roleUser.includes("admin") &&
<ToggleSidebar perfil={"administrador"} items={admItems} defaultOpen={pathname.includes("admin") } />
}
{roleUser.includes("admin") || roleUser.includes("secretaria") ?
<ToggleSidebar perfil={"secretaria"} items={SecretariaItems} defaultOpen={pathname.includes("secretaria")} />
:
null
}
{roleUser.includes("admin") || roleUser.includes("medico") ?
<ToggleSidebar perfil={"medico"} items={DoctorItems} defaultOpen={pathname.includes("medico") } />
:null
}
{roleUser.includes("admin") || roleUser.includes("financeiro") ?
<ToggleSidebar perfil={"financeiro"} items={FinanceiroItems} defaultOpen={pathname.includes("financeiro") } />
:null
}
{roleUser.includes("admin") || roleUser.includes("paciente") ?
<ToggleSidebar perfil={"paciente"} items={PacienteItems} defaultOpen={pathname.includes("paciente") } />
: null
}
{/* Botão de Logout */}
<li className="sidebar-item logout-item">
if (item.submenu)
return (
<li
key={index}
className={`sidebar-item has-sub ${
openSubmenu === item.key ? "active" : ""
}`}
>
<button
className="sidebar-link logout-button"
type="button"
className="sidebar-link btn"
onClick={() => handleSubmenuClick(item.key)}
>
<i className={`bi bi-${item.icon}`}></i>
<span>{item.name}</span>
</button>
<ul
className={`submenu ${
openSubmenu === item.key ? "active" : ""
}`}
>
{item.submenu.map((subItem, subIndex) => (
<li key={subIndex} className="submenu-item">
{renderLink(subItem)}
</li>
))}
</ul>
</li>
);
return (
<li key={index} className="sidebar-item">
{renderLink(item)}
</li>
);
})}
{/* Logout */}
<li className="sidebar-item">
<button
type="button"
className="sidebar-link btn"
onClick={handleLogoutClick}
>
<i className="bi bi-box-arrow-right"></i>
<span>Sair</span>
<span>Sair (Logout)</span>
</button>
</li>
<TrocardePerfis />
</ul>
</div>

View File

@ -1,15 +0,0 @@
import React from 'react'
const Spinner = () => {
return (
<div>
<div className="d-flex justify-content-center align-items-center" style={{ height: "100%" }}>
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Carregando...</span>
</div>
</div>
</div>
)
}
export default Spinner

View File

@ -1,139 +0,0 @@
import React from 'react'
import { useState, useEffect } from 'react'
import { Link, useLocation } from 'react-router-dom'
import "./Estilo/Toggle.css"
const ToggleSidebar = ({ perfil, items, defaultOpen = false }) => {
const [isOpen, setOpen] = useState(defaultOpen)
const [openSubmenu, setOpenSubmenu] = useState(null)
const [activeLink, setActiveLink] = useState('')
const location = useLocation()
useEffect(() => {
const currentPath = location.pathname
setActiveLink(currentPath)
const findActiveSubmenu = () => {
for (let item of items) {
if (item.submenu) {
const activeSubItem = item.submenu.find(subItem =>
subItem.url === currentPath
)
if (activeSubItem) {
setOpenSubmenu(item.key)
return
}
} else if (item.url === currentPath) {
setActiveLink(currentPath)
}
}
}
findActiveSubmenu()
}, [location.pathname, items])
const OpenClose = () => {
setOpen(!isOpen)
}
const handleSubmenuClick = (key) => {
setOpenSubmenu(openSubmenu === key ? null : key)
}
const isLinkActive = (url) => {
return activeLink === url
}
const renderLink = (item, isSubmenu = false) => {
const isActive = isLinkActive(item.url)
const linkClass = `sidebar-link ${isActive ? 'active' : ''} ${isSubmenu ? 'submenu-link' : ''}`
if (item.url && item.url.startsWith("/")) {
return (
<Link
to={item.url}
className={linkClass}
onClick={() => !isSubmenu && setActiveLink(item.url)}
>
{item.icon && <i className={`bi bi-${item.icon}`}></i>}
<span>{item.name}</span>
{isActive && <div className="active-indicator"></div>}
</Link>
)
}
return (
<a
href={item.url}
className={linkClass}
target="_blank"
rel="noopener noreferrer"
>
{item.icon && <i className={`bi bi-${item.icon}`}></i>}
<span>{item.name}</span>
<i className="bi bi-box-arrow-up-right external-icon"></i>
</a>
)
}
return (
<div className="toggle-sidebar-wrapper">
<div className='container-title' onClick={OpenClose}>
<p className='toggle-title'>{perfil}</p>
{isOpen
? <i className="bi bi-caret-down-fill toggle-arrow"></i>
: <i className="bi bi-caret-right-fill toggle-arrow"></i>
}
</div>
{isOpen && (
<ul className='sidebar-menu-list'>
{items.map((item, index) => {
if (item.isTitle) {
return (
<li key={`title-${index}`} className="sidebar-title">
<span>{item.name}</span>
</li>
)
}
if (item.submenu) {
const isSubmenuActive = openSubmenu === item.key
return (
<li
key={item.key || index}
className={`sidebar-item has-sub ${isSubmenuActive ? "active" : ""}`}
>
<button
type="button"
className={`sidebar-link btn ${isSubmenuActive ? "submenu-active" : ""}`}
onClick={() => handleSubmenuClick(item.key)}
>
<i className={`bi bi-${item.icon}`}></i>
<span>{item.name}</span>
</button>
<ul className={`submenu ${isSubmenuActive ? "active" : ""}`}>
{item.submenu.map((subItem, subIndex) => (
<li key={subIndex} className="submenu-item">
{renderLink(subItem, true)}
</li>
))}
</ul>
</li>
)
}
return (
<li key={item.key || index} className="sidebar-item">
{renderLink(item)}
</li>
)
})}
</ul>
)}
</div>
)
}
export default ToggleSidebar

View File

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

View File

@ -199,14 +199,3 @@
padding: 0.6rem 1.2rem;
}
}
@media (max-width: 576px) {
.doctor-form-container { padding: 0.75rem; }
.doctor-form-title { font-size: 1.75rem; }
.form-section { padding: 0.75rem; }
.section-header { font-size: 1.25rem; }
.form-label { font-size: 1rem; }
.form-control-custom { font-size: 1rem; }
.btns-container { display: flex; flex-direction: column; gap: 8px; }
.btn-submit, .btn-cancel { width: 100%; margin-right: 0; }
}

View File

@ -2,15 +2,14 @@ import React, { useState, useRef, useCallback } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom";
import "./DoctorForm.css";
import HorariosDisponibilidade from "../doctors/HorariosDisponibilidade";
import { useAuth } from '../utils/AuthProvider';
import API_KEY from '../utils/apiKeys';
const ENDPOINT_AVAILABILITY = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
const ENDPOINT_AVAILABILITY =
"https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability";
function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
const navigate = useNavigate();
const location = useLocation();
const { getAuthorizationHeader } = useAuth();
const FormatTelefones = (valor) => {
const digits = String(valor).replace(/\D/g, "").slice(0, 11);
@ -55,6 +54,7 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
);
};
const [avatarUrl, setAvatarUrl] = useState(null);
const [showRequiredModal, setShowRequiredModal] = useState(false);
const [emptyFields, setEmptyFields] = useState([]);
@ -74,15 +74,6 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
horarios: false,
});
const resolveAuthHeader = () => {
try {
const h = getAuthorizationHeader();
return h || '';
} catch {
return '';
}
}
const handleToggleCollapse = (section) => {
setCollapsedSections((prevState) => ({
...prevState,
@ -137,9 +128,12 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
const handleAvailabilityUpdate = useCallback((newAvailability) => {
setFormData((prev) => {
if (JSON.stringify(prev.availability) !== JSON.stringify(newAvailability)) {
return { ...prev, availability: newAvailability };
}
return prev;
});
}, [setFormData]);
}, []);
const handleCepBlur = async () => {
const cep = formData.cep?.replace(/\D/g, "");
@ -219,68 +213,21 @@ const handleAvailabilityUpdate = useCallback((newAvailability) => {
}
}, 300);
};
const handleCreateAvailability = async (doctorId, availabilityData) => {
const handleCreateAvailability = async (newAvailability) => {
try {
const myHeaders = new Headers();
const authHeader = resolveAuthHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
if (API_KEY) myHeaders.append("apikey", API_KEY);
const response = await fetch(ENDPOINT_AVAILABILITY, {
method: "POST",
headers: myHeaders,
body: JSON.stringify({
doctor_id: doctorId,
availability: availabilityData,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
}),
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newAvailability),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Erro ${response.status}: ${errorText}`);
}
const data = await response.json();
console.log("Disponibilidade criada :", data);
return data;
alert("Disponibilidade criada com sucesso!");
} catch (error) {
console.error("Erro ao criar disponibilidade:", error);
throw error;
}
};
const handlePatchAvailability = async (id, updatedAvailability) => {
try {
const myHeaders = new Headers();
const authHeader = resolveAuthHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
if (API_KEY) myHeaders.append("apikey", API_KEY);
const response = await fetch(`${ENDPOINT_AVAILABILITY}?id=eq.${id}`, {
method: "PATCH",
headers: myHeaders,
body: JSON.stringify({
availability: updatedAvailability,
updated_at: new Date().toISOString()
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Erro ${response.status}: ${errorText}`);
}
const data = await response.json();
console.log("Disponibilidade atualizada:", data);
return data;
} catch (error) {
console.error("Erro ao atualizar disponibilidade:", error);
throw error;
alert("Erro ao criar disponibilidade.");
}
};
@ -323,20 +270,23 @@ const handleAvailabilityUpdate = useCallback((newAvailability) => {
}
try {
// Chama a função onSave (handleSave no DoctorEditPage) com o formData completo.
// A lógica de salvamento do médico e da disponibilidade é responsabilidade do componente pai.
await onSave({ ...formData });
} catch (error) {
console.error("Erro ao salvar médico ou disponibilidade:", error);
alert("Erro ao salvar médico ou disponibilidade.");
if (formData.availability && formData.availability.length > 0) {
}
alert("Médico salvo com sucesso!");
} catch (error) {
console.error("Erro ao salvar médico:", error);
alert("Erro ao salvar médico.");
};
};
const handleModalClose = () => {
setShowRequiredModal(false);
};
return (
<>
{/* Modal de Alerta */}
@ -759,7 +709,7 @@ const handleAvailabilityUpdate = useCallback((newAvailability) => {
</div>
{/* BOTÕES DE AÇÃO */}
<div className="btns-container">
<div className="actions-container">
<button
className="btn btn-success btn-submit"
onClick={handleSubmit}

View File

@ -1,168 +0,0 @@
.horarios-container {
max-width: 1100px;
margin: 0 auto;
font-family: "Inter", sans-serif;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
/* Cards mais compactos */
.day-card {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px;
height: 220px;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: all 0.2s ease;
}
.day-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.06);
}
.day-card.checked {
background-color: #1f2937;
color: white;
border-color: #4b5563;
}
/* Cabeçalho compacto */
.day-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-radius: 6px;
border-bottom: 1px solid #e5e7eb;
cursor: pointer;
}
.day-header.checked {
background-color: #1f2937;
color: white;
border-color: #4b5563;
}
.day-header label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
font-size: 14px;
}
/* Seção de blocos mais compacta */
.blocks-section {
margin-top: 6px;
display: flex;
flex-direction: column;
gap: 6px;
flex-grow: 1;
overflow-y: auto;
padding-right: 2px;
}
/* Blocos de tempo menores */
.time-block {
background-color: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
transition: transform 0.15s, box-shadow 0.15s;
}
.time-block:hover {
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08);
}
.time-block.new {
background-color: #eef2ff;
border-color: #6366f1;
}
/* Inputs compactos */
.time-inputs {
display: flex;
flex-direction: column;
gap: 6px;
}
.input-wrapper {
position: relative;
}
.input-wrapper input {
padding: 4px 24px 4px 4px;
border: 1px solid #d1d5db;
border-radius: 4px;
width: 100%;
font-size: 13px;
outline: none;
}
.clock-icon {
position: absolute;
right: 6px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
font-size: 12px;
}
/* Botões compactos */
.btn-remove {
margin-top: 6px;
width: 100%;
background-color: #ef4444;
border: none;
color: white;
font-weight: bold;
padding: 4px 0;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.15s;
}
.btn-remove:hover {
background-color: #dc2626;
}
.btn-add {
background-color: #10b981;
color: white;
font-weight: bold;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
align-self: center;
transition: background-color 0.15s;
width: 100%;
font-size: 13px;
}
.btn-add:hover {
background-color: #059669;
}
@media (max-width: 768px) {
.horarios-container { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 576px) {
.horarios-container { grid-template-columns: 1fr; gap: 10px; }
.day-card { height: auto; }
.day-header label { font-size: 13px; }
.time-inputs { flex-direction: column; }
.input-wrapper input { font-size: 12px; }
.btn-add { font-size: 12px; }
.btn-remove { font-size: 12px; }
}

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Clock } from "lucide-react";
import "./HorariosDisponibilidade.css";
const initialBlockTemplate = {
id: null,
@ -10,13 +9,13 @@ const initialBlockTemplate = {
};
const emptyAvailabilityTemplate = [
{ dia: "Domingo", weekday: 0, isChecked: false, blocos: [] },
{ dia: "Segunda-feira", weekday: 1, isChecked: false, blocos: [] },
{ dia: "Terça-feira", weekday: 2, isChecked: false, blocos: [] },
{ dia: "Quarta-feira", weekday: 3, isChecked: false, blocos: [] },
{ dia: "Quinta-feira", weekday: 4, isChecked: false, blocos: [] },
{ dia: "Sexta-feira", weekday: 5, isChecked: false, blocos: [] },
{ dia: "Sábado", weekday: 6, isChecked: false, blocos: [] },
{ dia: "Segunda-feira", isChecked: false, blocos: [] },
{ dia: "Terça-feira", isChecked: false, blocos: [] },
{ dia: "Quarta-feira", isChecked: false, blocos: [] },
{ dia: "Quinta-feira", isChecked: false, blocos: [] },
{ dia: "Sexta-feira", isChecked: false, blocos: [] },
{ dia: "Sábado", isChecked: false, blocos: [] },
{ dia: "Domingo", isChecked: false, blocos: [] },
];
const HorariosDisponibilidade = ({
@ -35,18 +34,11 @@ const HorariosDisponibilidade = ({
}
}, [initialAvailability]);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
if (onUpdate) onUpdate(availability);
}, [availability, onUpdate]);
const handleDayCheck = useCallback((dayIndex, currentIsChecked) => {
const isChecked = !currentIsChecked;
setAvailability((prev) => {
const updated = prev.map((day, i) =>
setAvailability((prev) =>
prev.map((day, i) =>
i === dayIndex
? {
...day,
@ -64,15 +56,14 @@ const HorariosDisponibilidade = ({
: [],
}
: day
)
);
console.log('handleDayCheck - updated availability:', updated);
return updated;
});
}, []);
const handleAddBlock = useCallback((dayIndex) => {
const tempId = Date.now() + Math.random();
const newBlock = { ...initialBlockTemplate, id: tempId, isNew: true };
setAvailability((prev) =>
prev.map((day, i) =>
i === dayIndex
@ -117,90 +108,299 @@ const HorariosDisponibilidade = ({
);
}, []);
const handleSave = useCallback(() => {
if (onUpdate) onUpdate(availability);
}, [availability, onUpdate]);
return (
<div className="horarios-container">
{availability.map((day, dayIndex) => (
<div key={day.dia} className="day-card">
<div
className={`day-header ${day.isChecked ? "checked" : ""}`}
onClick={() => handleDayCheck(dayIndex, day.isChecked)}
>
<label>
<span>{day.dia}</span>
<input type="checkbox" checked={day.isChecked} readOnly />
</label>
</div>
{day.isChecked && (
<div className="blocks-section">
<div className="blocks-grid">
{day.blocos.map((bloco) => (
const renderTimeBlock = (dayIndex, bloco) => (
<div
key={bloco.id}
className={`time-block ${bloco.isNew ? "new" : ""}`}
style={{
display: "flex",
flexDirection: window.innerWidth < 640 ? "column" : "row",
alignItems: window.innerWidth < 640 ? "flex-start" : "center",
justifyContent: "space-between",
padding: "8px",
marginBottom: "8px",
borderRadius: "8px",
boxShadow:
"0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
transition: "all 0.3s",
backgroundColor: bloco.isNew ? "#eef2ff" : "#ffffff",
border: bloco.isNew ? "2px solid #6366f1" : "1px solid #e5e7eb",
}}
>
<div
style={{
display: "flex",
flexDirection: window.innerWidth < 640 ? "column" : "row",
gap: window.innerWidth < 640 ? "0" : "12px",
width: window.innerWidth < 640 ? "100%" : "auto",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: "4px",
marginBottom: window.innerWidth < 640 ? "8px" : "0",
}}
>
<label
htmlFor={`inicio-${dayIndex}-${bloco.id}`}
style={{ fontWeight: 500, color: "#4b5563", width: "64px" }}
>
<div className="time-inputs">
<label>
Início:
<div className="input-wrapper">
</label>
<div style={{ position: "relative" }}>
<input
id={`inicio-${dayIndex}-${bloco.id}`}
type="time"
value={bloco.inicio}
onChange={(e) =>
handleTimeChange(
dayIndex,
bloco.id,
"inicio",
e.target.value
)
handleTimeChange(dayIndex, bloco.id, "inicio", e.target.value)
}
style={{
padding: "4px 6px",
border: "1px solid #d1d5db",
borderRadius: "6px",
width: "100%",
boxSizing: "border-box",
outline: "none",
fontSize: "13px",
}}
step="300"
/>
<Clock
size={12}
style={{
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
color: "#9ca3af",
pointerEvents: "none",
}}
/>
<Clock className="clock-icon" size={14} />
</div>
</label>
</div>
<label>
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
<label
htmlFor={`termino-${dayIndex}-${bloco.id}`}
style={{
fontWeight: 500,
color: "#4b5563",
width: "56px",
fontSize: "13px",
}}
>
Término:
<div className="input-wrapper">
</label>
<div style={{ position: "relative" }}>
<input
id={`termino-${dayIndex}-${bloco.id}`}
type="time"
value={bloco.termino}
onChange={(e) =>
handleTimeChange(
dayIndex,
bloco.id,
"termino",
e.target.value
)
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",
}}
step="300"
/>
<Clock
size={12}
style={{
position: "absolute",
right: "8px",
top: "50%",
transform: "translateY(-50%)",
color: "#9ca3af",
pointerEvents: "none",
}}
/>
<Clock className="clock-icon" size={14} />
</div>
</div>
</div>
<button
onClick={() => handleRemoveBlock(dayIndex, bloco.id)}
style={{
marginTop: window.innerWidth < 640 ? "8px" : "0",
padding: "4px 10px",
backgroundColor: "#ef4444",
color: "white",
fontWeight: 600,
borderRadius: "13px",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
transition: "all 0.2s",
width: window.innerWidth < 640 ? "100%" : "auto",
cursor: "pointer",
border: "none",
opacity: 1,
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#dc2626")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "#ef4444")
}
>
Remover Bloco
</button>
{bloco.isNew && (
<span
style={{
fontSize: "12px",
color: "#6366f1",
marginTop: "8px",
marginLeft: window.innerWidth < 640 ? "0" : "16px",
fontWeight: 500,
}}
></span>
)}
</div>
);
return (
<div
style={{
maxWidth: "960px",
margin: "0 auto",
fontFamily: "Inter, sans-serif",
}}
>
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
{availability.map((day, dayIndex) => {
const isChecked = day.isChecked;
const dayHeaderStyle = {
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 0",
borderBottom: "1px solid #e5e7eb",
marginBottom: "16px",
backgroundColor: isChecked ? "#1f2937" : "#f9fafb",
borderRadius: "8px",
paddingLeft: "16px",
paddingRight: "16px",
cursor: "pointer",
transition: "background-color 0.2s",
};
return (
<div
key={day.dia}
style={{
backgroundColor: "#f9fafb",
padding: "8px",
borderRadius: "10px",
border: "1px solid #e5e7eb",
}}
>
<div
style={{
...dayHeaderStyle,
backgroundColor: isChecked ? "#1f2937" : "#f9fafb",
borderBottom: isChecked
? "1px solid #4b5563"
: "1px solid #e5e7eb",
}}
onClick={() => handleDayCheck(dayIndex, isChecked)}
>
<label
style={{
fontSize: "18px",
fontWeight: "bold",
color: isChecked ? "white" : "#1f2937",
display: "flex",
alignItems: "center",
gap: "12px",
cursor: "pointer",
}}
>
<span>{day.dia}</span>
<input
type="checkbox"
checked={isChecked}
onChange={() => {}}
style={{
width: "20px",
height: "20px",
accentColor: isChecked ? "#3b82f6" : "#9ca3af",
marginLeft: "8px",
}}
/>
</label>
</div>
<button
className="btn-remove"
onClick={() => handleRemoveBlock(dayIndex, bloco.id)}
{isChecked && (
<div style={{ marginTop: "16px" }}>
{day.blocos.length === 0 && (
<p
style={{
color: "#6b7280",
fontStyle: "italic",
marginBottom: "16px",
}}
>
Remover
</button>
</div>
))}
Nenhum bloco de horário definido.
</p>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "16px",
}}
>
{day.blocos.map((bloco) =>
renderTimeBlock(dayIndex, bloco)
)}
</div>
<button
className="btn-add"
onClick={() => handleAddBlock(dayIndex)}
style={{
marginTop: "15px",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "10px 22px",
backgroundColor: "#10b981",
color: "white",
fontWeight: "bold",
borderRadius: "12px",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
transition: "all 0.3s",
cursor: "pointer",
border: "none",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#059669")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "#10b981")
}
>
+ Adicionar novo bloco
</button>
</div>
)}
</div>
))}
);
})}
</div>
</div>
);
};

View File

@ -52,8 +52,6 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
}));
};
useEffect(() => {
const peso = parseFloat(formData.weight_kg);
const altura = parseFloat(formData.height_m);
@ -65,20 +63,9 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
}
}, [formData.weight_kg, formData.height_m, setFormData]);
const handleCep = (value) => {
if(value.length === 8){
fetch(`https://viacep.com.br/ws/${value}/json/`)
.then(response => response.json())
.then(result => {setFormData(prev => ({...prev, street:result.logradouro,neighborhood:result.bairro,city:result.localidade,
state:result.estado
}))})
}
}
const handleChange = (e) => {
const { name, value, type, checked, files } = e.target;
console.log(name, value)
if (value && emptyFields.includes(name)) {
setEmptyFields(prev => prev.filter(field => field !== name));
}
@ -115,9 +102,6 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
setFormData(prev => ({ ...prev, [name]: FormatPeso(value) }));
} else if (name === 'rn_in_insurance' || name === 'vip' || name === 'validadeIndeterminada') {
setFormData(prev => ({ ...prev, [name]: checked }));
} else if(name === 'cep'){
handleCep(value)
setFormData(prev => ({...prev, [name]: value}))
} else {
setFormData(prev => ({ ...prev, [name]: value }));
}
@ -205,6 +189,9 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
throw new Error('Email inválido. Por favor, verifique o email digitado.');
}
await onSave({ ...formData, bmi: parseFloat(formData.bmi) || null });
};
@ -645,7 +632,7 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
)}
{/* BOTÕES DE AÇÃO */}
<div className="btns-container">
<div className="actions-container">
<button className="btn btn-success btn-submit" onClick={handleSubmit} disabled={isLoading}>
{isLoading ? 'Salvando...' : 'Salvar Paciente'}
</button>
@ -655,7 +642,6 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
</button>
</Link>
</div>
</div>
);
}

View File

@ -1,56 +1,48 @@
import API_KEY from '../apiKeys';
const GetDoctorByID = async (ID, authHeader) => {
const myHeaders = new Headers();
var 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) => {
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader);
const requestOptions = {
const GetAllDoctors = async (authHeader) => {
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
var 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);
const Medicos = await GetAllDoctors(authHeader)
for (let i = 0; i < Medicos.length; i++) {
if (Medicos[i].full_name === nome) {
console.log('Médico encontrado:', Medicos[i]);
console.log('Medico encontrado:', Medicos[i]);
return Medicos[i];
}
else{console.log("nada encontrado")}
}
console.log('Nenhum médico encontrado com o nome:', nome);
return null;
};
export { GetDoctorByID, GetDoctorByName, GetAllDoctors };
}
export {GetDoctorByID, GetDoctorByName, GetAllDoctors}

View File

@ -1,47 +1,32 @@
import API_KEY from "../apiKeys";
// Função para pegar as informações do usuário logado
const UserInfos = async (access_token) => {
if (!access_token) throw new Error("access_token é obrigatório em UserInfos");
// Normaliza o formato do token
const Token = access_token.replace(/^bearer/i, "Bearer");
let Token = access_token.replace('bearer', 'Bearer')
const myHeaders = new Headers();
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", Token);
const requestOptions = {
method: "GET",
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: "follow",
redirect: 'follow'
};
try {
const userInfo = await fetch(
`https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/user-info`,
requestOptions
);
if (!userInfo.ok) {
const text = await userInfo.text();
console.error("Erro em UserInfos:", userInfo.status, text);
throw new Error(`Erro ${userInfo.status} ao buscar informações do usuário`);
const userInfo = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/user-info`, requestOptions)
const userInfoData = await userInfo.json()
console.log(userInfoData, "Dados do usuário")
return userInfoData
}
const userInfoData = await userInfo.json();
console.log("Dados do usuário:", userInfoData);
return userInfoData;
} catch (error) {
console.error("Erro na função UserInfos:", error);
throw error;
}
};
const UploadFotoAvatar = ( userID,access_token,file) => {
const SearchCep = async (cep) => {
fetch(`https://brasilapi.com.br/api/cep/v1/${cep}`)
.then(response => console.log(response))
}
export { UserInfos,SearchCep };
export {UserInfos}

View File

@ -1,17 +0,0 @@
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

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

View File

@ -1,26 +0,0 @@
.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;
}

View File

@ -1,4 +1,8 @@
[
{
"name": "Menu",
"isTitle": true
},
{
"name": "Lista de Pacientes",
"icon": "clipboard-heart-fill",
@ -21,14 +25,16 @@
"icon": "table",
"url": "/admin/laudo"
},
{
"name": "Gestão de Usuários",
"icon": "person-badge-fill",
"url": "/admin/gestao"
},
{
"name": "Painel Administrativo",
"icon": "file-bar-graph-fill",
"url": "/admin/painel"
},
{
"name": "Gestão de Usuários",
"icon": "people-fill",
"url": "/admin/gestao"
}
]

View File

@ -1,4 +1,8 @@
[
{
"name": "Menu-Financeiro",
"isTitle": true
},
{
"name": "Controle Financeiro",
"icon": "cash-coin",

View File

@ -1,16 +1,18 @@
[
{
"name": "Menu",
"isTitle": true
},
{
"name": "Prontuário",
"icon": "calendar-plus-fill",
"url": "/medico/prontuario"
},
{
"name": "Seus Agendamentos",
"icon": "calendar",
"url": "/medico/agendamento"
},
{
"name": "Relatório por Áudio",
"icon": "file-earmark-plus-fill",
"url": "/medico/novo-relatorio-audio"
"url": "/medico/agendamentoMedico"
},
{
@ -19,7 +21,6 @@
"url": "/medico/relatorios"
},
{
"name": "Chat com pacientes",
"icon": "chat-dots-fill",

View File

@ -1,14 +1,10 @@
[
{
"name": "Início",
"icon": "house-fill",
"url": "/paciente"
},
{
"name": "Minhas consulta",
"icon": "calendar-plus-fill",
"url": "/paciente/agendamento"
},
{
"name": "Meus laudos",
"icon": "table",

View File

@ -1,4 +1,9 @@
[
{
"name": "Menu",
"isTitle": true
},
{
"name":"Início",
"url": "/secretaria/",
@ -20,5 +25,11 @@
"name": "Agendar consulta",
"icon": "calendar-plus-fill",
"url": "/secretaria/agendamento"
},
{
"name": "Laudo do Paciente",
"icon": "table",
"url": "/secretaria/laudo"
}
]

View File

@ -1,20 +0,0 @@
// 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);

View File

@ -1,9 +1,3 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',

View File

@ -1,13 +0,0 @@
// 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;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +1,75 @@
import React, { useEffect, useState } from 'react';
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta';
import API_KEY from '../components/utils/apiKeys';
import { useAuth } from '../components/utils/AuthProvider';
import dayjs from 'dayjs';
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
import React from 'react'
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta'
import API_KEY from '../components/utils/apiKeys'
import { useAuth } from '../components/utils/AuthProvider'
import { useEffect,useState } from 'react'
import dayjs from 'dayjs'
import { UserInfos } from '../components/utils/Functions-Endpoints/General'
const AgendamentoCadastroManager = ({setPageConsulta}) => {
const AgendamentoCadastroManager = ({ setPageConsulta, Dict, onSaved }) => {
const { getAuthorizationHeader, user } = useAuth();
const [agendamento, setAgendamento] = useState({ status: 'confirmed' });
const [idUsuario, setIDusuario] = useState('0');
const {getAuthorizationHeader} = useAuth()
const [agendamento, setAgendamento] = useState({status:'confirmed'})
const [idUsuario, setIDusuario] = useState('0')
// patient_id do paciente logado (ou fallback para o Pedro)
let authHeader = getAuthorizationHeader()
const patientId = 'bf7d8323-05e1-437a-817c-f08eb5f174ef';
const authHeader = getAuthorizationHeader();
useEffect(() => {
if (!Dict) {
setAgendamento({ status: 'confirmed' });
} else {
setAgendamento(Dict);
}
const ColherInfoUsuario =async () => {
try {
const result = await UserInfos(authHeader);
setIDusuario(result?.profile?.id);
} catch (e) {
console.error('Erro ao buscar infos do usuário:', e);
const result = await UserInfos(authHeader)
setIDusuario(result?.profile?.id)
}
};
ColherInfoUsuario()
if (authHeader) {
ColherInfoUsuario();
}
}, [Dict, authHeader]);
const handleSave = async (DictForm) => {
if (!authHeader) {
alert('Sem autorização. Faça login novamente.');
return;
}
}, [])
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
myHeaders.append('Authorization', authHeader);
myHeaders.append('Content-Type', 'application/json');
const raw = JSON.stringify({
patient_id: patientId, // paciente logado
doctor_id: DictForm.doctor_id,
scheduled_at: `${DictForm.dataAtendimento}T${DictForm.horarioInicio}:00`,
duration_minutes: 30,
appointment_type: DictForm.tipo_consulta,
patient_notes: '',
insurance_provider: DictForm.convenio,
status: 'confirmed', // ou 'confirmed'
created_by: idUsuario,
created_at: dayjs().toISOString(),
const handleSave = (Dict) => {
let DataAtual = dayjs()
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify({
"patient_id": Dict.patient_id,
"doctor_id": Dict.doctor_id,
"scheduled_at": `${Dict.dataAtendimento}T${Dict.horarioInicio}:00.000Z`,
"duration_minutes": 30,
"appointment_type": Dict.tipo_consulta,
"patient_notes": "Prefiro horário pela manhã",
"insurance_provider": Dict.convenio,
"status": Dict.status,
"created_by": idUsuario
});
const requestOptions = {
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow',
redirect: 'follow'
};
try {
const response = await fetch(
'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments',
requestOptions
);
if (response.ok) {
if (onSaved) onSaved(); // pai recarrega e fecha
else setPageConsulta(false);
} else {
console.error('Erro ao criar agendamento:', await response.text());
alert('Falha ao criar agendamento.');
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));
}
} catch (error) {
console.error('Erro de rede:', error);
alert('Erro de rede ao salvar agendamento.');
}
};
return (
<div>
<FormNovaConsulta
onSave={handleSave}
agendamento={agendamento}
setAgendamento={setAgendamento}
onCancel={() => setPageConsulta(false)}
/>
</div>
);
};
export default AgendamentoCadastroManager;
<FormNovaConsulta onSave={handleSave} agendamento={agendamento} setAgendamento={setAgendamento} onCancel={() => setPageConsulta(false)}/>
</div>
)
}
export default AgendamentoCadastroManager

View File

@ -13,15 +13,17 @@ 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")
//console.log(DictInfo, 'aqui')
useEffect(() => {
setDictInfo({...DictInfo, dataAtendimento:DictInfo.scheduled_at.split("T")[0]})
setDictInfo({...DictInfo?.Infos,...DictInfo?.agendamento})
const ColherInfoUsuario =async () => {
const result = await UserInfos(authHeader)
@ -51,7 +53,9 @@ const AgendamentoEditPage = ({setDictInfo, DictInfo}) => {
"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`,

View File

@ -7,7 +7,7 @@ import { Link } from "react-router-dom";
import { useAuth } from "../components/utils/AuthProvider";
const Details = (DictInfo) => {
const Details = () => {
const parametros = useParams();
const {getAuthorizationHeader, isAuthenticated} = useAuth();
const [paciente, setPaciente] = useState({});
@ -22,29 +22,19 @@ const Details = (DictInfo) => {
navigate(`/${prefixo}/pacientes`);
}
const navigateEdit = () => {
const prefixo = location.pathname.split("/")[1];
navigate(`/${prefixo}/medicos/edit`);
}
useEffect(() => {
if (!DictInfo) return;
if (!patientID) return;
console.log(patientID, 'teu id')
const authHeader = getAuthorizationHeader()
GetPatientByID(DictInfo.DictInfo.id, authHeader)
GetPatientByID(patientID, authHeader)
.then((data) => {
console.log(data, "paciente vindo da API");
setPaciente(data[0]); // supabase retorna array
})
.catch((err) => console.error("Erro ao buscar paciente:", err));
}, [DictInfo]);
}, [patientID]);
const handleDelete = async (anexoId) => {
@ -92,9 +82,11 @@ const navigateEdit = () => {
</div>
</div>
<button className="btn btn-light" onClick={() => navigateEdit()} >
<Link to={`edit`}>
<button className="btn btn-light" >
<i className="bi bi-pencil-square"></i> Editar
</button>
</Link>
</div>
</div>

View File

@ -1,513 +1,311 @@
import React, { useState, useEffect, useMemo } from "react";
import React, { useState, useEffect, useCallback } from "react";
import HorariosDisponibilidade from "../components/doctors/HorariosDisponibilidade";
import { useAuth } from "../components/utils/AuthProvider";
import API_KEY from "../components/utils/apiKeys";
import "./style/DisponibilidadesDoctorPage.css";
const ENDPOINT =
"https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability";
const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
const DOCTORS_ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors";
const diasDaSemana = [
"Domingo",
"Segunda",
"Terça",
"Quarta",
"Quinta",
"Sexta",
"Sábado"
const MEDICOS_MOCKADOS = [
{ id: 53, nome: "João Silva" },
{ id: 19, nome: "Ana Costa" },
{ id: 11, nome: "Pedro Santos" },
];
const weekdayNumToStr = {
0: "sunday",
1: "monday",
2: "tuesday",
3: "wednesday",
4: "thursday",
5: "friday",
6: "saturday",
const diasDaSemana = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];
const formatarDataHora = (isoString) => {
if (!isoString) return "N/A";
try {
const data = new Date(isoString);
// Usa o toLocaleTimeString para extrair hora e minuto
return data.toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
timeZone: "UTC",
});
} catch {
return "Data Inválida";
}
};
const weekdayStrToNum = Object.fromEntries(
Object.entries(weekdayNumToStr).map(([num, str]) => [str, Number(num)])
);
const DisponibilidadesDoctorPage = () => {
const { getAuthorizationHeader } = useAuth();
const [disponibilidades, setDisponibilidades] = useState([]);
const [doctors, setDoctors] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
const [editando, setEditando] = useState(null);
const [expandedDoctors, setExpandedDoctors] = useState({});
const [showSuggestions, setShowSuggestions] = useState(false);
const [availabilityEdit, setAvailabilityEdit] = useState([]);
// Add the missing state variables
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedDisponibilidadeId, setSelectedDisponibilidadeId] = useState(null);
const [loading, setLoading] = useState(false);
const [filtroMedicoNome, setFiltroMedicoNome] = useState("");
const [gerenciarModo, setGerenciarModo] = useState(false);
const [editando, setEditando] = useState(null); // ID da disponibilidade sendo editada
const getHeaders = () => {
const myHeaders = new Headers();
const authHeader = getAuthorizationHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
if (API_KEY) myHeaders.append("apikey", API_KEY);
myHeaders.append("Prefer", "return=representation");
return myHeaders;
const encontrarMedicoIdPorNome = (nome) => {
if (!nome) return null;
const termo = nome.toLowerCase();
const medico = MEDICOS_MOCKADOS.find((m) =>
m.nome.toLowerCase().includes(termo)
);
return medico ? medico.id : null;
};
useEffect(() => {
const fetchDoctors = async () => {
try {
const requestOptions = {
method: "GET",
headers: getHeaders(),
};
const response = await fetch(DOCTORS_ENDPOINT, requestOptions);
const result = await response.json();
setDoctors(Array.isArray(result) ? result : []);
} catch (error) {
setDoctors([]);
const fetchDisponibilidades = useCallback(async (nome) => {
setLoading(true);
const doctorId = encontrarMedicoIdPorNome(nome);
if (!doctorId) {
setLoading(false);
return;
}
};
fetchDoctors();
}, [getAuthorizationHeader]);
useEffect(() => {
const fetchDisponibilidades = async () => {
try {
const res = await fetch(ENDPOINT, { method: "GET", headers: getHeaders() });
if (res.ok) {
const res = await fetch(`${ENDPOINT}?doctor_id=eq.${doctorId}`);
const data = await res.json();
setDisponibilidades(Array.isArray(data) ? data : []);
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);
}
} catch (error) {
}, []);
useEffect(() => {
if (!gerenciarModo && editando) {
setEditando(null);
}
}, [gerenciarModo]);
useEffect(() => {
if (editando) return;
if (filtroMedicoNome) {
const timer = setTimeout(() => {
fetchDisponibilidades(filtroMedicoNome);
}, 300);
return () => clearTimeout(timer);
} else {
setDisponibilidades([]);
}
};
fetchDisponibilidades();
}, [getAuthorizationHeader]);
}, [filtroMedicoNome, fetchDisponibilidades, editando]);
const toggleExpandDoctor = (doctorId) => {
setExpandedDoctors((prev) => ({ ...prev, [doctorId]: !prev[doctorId] }));
};
const salvarTodasDisponibilidades = async (doctorId, horariosAtualizados) => {
const atualizarDisponibilidade = async (id, novoIntervalo) => {
try {
const headers = getHeaders();
const promises = [];
const currentIds = new Set();
for (const dia of horariosAtualizados) {
if (dia.isChecked && dia.blocos.length > 0) {
for (const bloco of dia.blocos) {
const inicio = bloco.inicio.includes(":") ? bloco.inicio : bloco.inicio + ":00";
const termino = bloco.termino.includes(":") ? bloco.termino : bloco.termino + ":00";
const payload = {
doctor_id: doctorId,
weekday: weekdayNumToStr[dia.weekday],
start_time: inicio,
end_time: termino,
slot_minutes: bloco.slot_minutes || 30,
appointment_type: bloco.appointment_type || "presencial",
active: true,
};
if (bloco.id && !bloco.isNew) {
currentIds.add(bloco.id);
promises.push(
fetch(`${ENDPOINT}?id=eq.${bloco.id}`, {
method: "PATCH",
headers,
body: JSON.stringify(payload),
}).then(() => ({ type: 'PATCH', id: bloco.id }))
);
} else {
promises.push(
fetch(ENDPOINT, {
method: "POST",
headers,
body: JSON.stringify(payload),
})
.then(res => res.json())
.then(data => {
const createdItem = Array.isArray(data) ? data[0] : data;
return { type: 'POST', id: createdItem?.id };
})
);
}
}
}
}
const results = await Promise.all(promises);
results.forEach(res => {
if (res.type === 'POST' && res.id) currentIds.add(res.id);
const res = await fetch(`${ENDPOINT}?id=eq.${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slot_minutes: novoIntervalo }),
});
const existingRes = await fetch(`${ENDPOINT}?doctor_id=eq.${String(doctorId)}`, {
method: "GET", headers
});
if (existingRes.ok) {
const existingData = await existingRes.json();
const deletePromises = existingData
.filter(dbItem => !currentIds.has(dbItem.id))
.map(dbItem =>
fetch(`${ENDPOINT}?id=eq.${dbItem.id}`, { method: "DELETE", headers })
);
await Promise.all(deletePromises);
}
setEditando(null);
setAvailabilityEdit([]);
const res = await fetch(ENDPOINT, { method: "GET", headers: getHeaders() });
if (res.ok) {
const data = await res.json();
setDisponibilidades(Array.isArray(data) ? data : []);
alert("Disponibilidade atualizada com sucesso!");
setEditando(null);
fetchDisponibilidades(filtroMedicoNome);
} else {
alert("Erro ao atualizar disponibilidade");
}
} catch (error) {
console.error(error);
} catch {
alert("Falha ao conectar com o servidor");
}
};
const deletarDisponibilidade = async (id) => {
if (!window.confirm("Deseja realmente excluir esta disponibilidade?")) return;
if (!window.confirm("Deseja realmente excluir esta disponibilidade?"))
return;
try {
const res = await fetch(`${ENDPOINT}?id=eq.${id}`, { method: "DELETE", headers: getHeaders() });
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));
setShowDeleteModal(false);
setSelectedDisponibilidadeId(null);
} else {
alert("Erro ao excluir disponibilidade");
}
} catch (error) {
console.error("Erro ao excluir disponibilidade:", error);
} catch {
alert("Erro ao conectar com o servidor");
}
};
const handleOpenDeleteModal = (id) => {
setSelectedDisponibilidadeId(id);
setShowDeleteModal(true);
};
const disponibilidadeParaEdicao = editando
? disponibilidades.find((d) => d.id === editando)
: null;
const handleCloseDeleteModal = () => {
setShowDeleteModal(false);
setSelectedDisponibilidadeId(null);
};
const disponibilidadesAgrupadas = useMemo(() => {
const agrupadas = {};
doctors.forEach((doctor) => {
agrupadas[doctor.id] = {
doctor_id: doctor.id,
doctor_name: doctor.full_name || doctor.name,
disponibilidades: [],
};
});
disponibilidades.forEach((disp) => {
if (agrupadas[disp.doctor_id]) agrupadas[disp.doctor_id].disponibilidades.push(disp);
});
Object.values(agrupadas).forEach((grupo) => {
if (grupo.disponibilidades.length === 0) {
grupo.disponibilidades.push({
id: `empty-${grupo.doctor_id}`,
doctor_id: grupo.doctor_id,
doctor_name: grupo.doctor_name,
is_empty: true,
});
}
});
let resultado = Object.values(agrupadas);
if (searchTerm) resultado = resultado.filter((grupo) => grupo.doctor_name.toLowerCase().includes(searchTerm.toLowerCase()));
return resultado;
}, [disponibilidades, doctors, searchTerm]);
const formatTime = (timeString) => {
if (!timeString) return "";
return timeString.includes(":") ? timeString.substring(0, 5) : timeString;
};
const getDiaSemana = (weekday) => {
const dias = {
0: "Domingo",
1: "Segunda",
2: "Terça",
3: "Quarta",
4: "Quinta",
5: "Sexta",
6: "Sábado",
sunday: "Domingo",
monday: "Segunda",
tuesday: "Terça",
wednesday: "Quarta",
thursday: "Quinta",
friday: "Sexta",
saturday: "Sábado",
};
const key = typeof weekday === "string" ? weekday.toLowerCase() : weekday;
return dias[key] || "Desconhecido";
};
const initialAvailabilityParaEdicao = useMemo(() => {
if (!editando) return [];
const disponibilidadesMedico = disponibilidades.filter((d) => String(d.doctor_id) === String(editando));
const blocosPorDia = {};
disponibilidadesMedico.forEach((d) => {
const num = typeof d.weekday === "string" ? weekdayStrToNum[d.weekday.toLowerCase()] : d.weekday;
if (num === undefined) return;
if (!blocosPorDia[num]) blocosPorDia[num] = [];
if (d.active !== false) {
blocosPorDia[num].push({
const initialAvailabilityParaEdicao = diasDaSemana.map((dia, weekdayIndex) => {
const blocosDoDia = disponibilidades
.filter(d => d.weekday === weekdayIndex)
.map(d => ({
id: d.id,
inicio: formatTime(d.start_time) || "07:00",
termino: formatTime(d.end_time) || "17:00",
slot_minutes: d.slot_minutes || 30,
appointment_type: d.appointment_type || "presencial",
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,
});
}
});
const resultado = [1, 2, 3, 4, 5, 6, 0].map((weekday) => {
const blocosDoDia = blocosPorDia[weekday] || [];
slot_minutes: d.slot_minutes,
}));
return {
dia: diasDaSemana[weekday],
weekday: weekday,
dia,
isChecked: blocosDoDia.length > 0,
blocos:
blocosDoDia.length > 0
? blocosDoDia
: [
{
id: null,
inicio: "07:00",
termino: "17:00",
slot_minutes: 30,
appointment_type: "presencial",
isNew: true,
},
],
blocos: blocosDoDia,
};
});
return resultado;
}, [disponibilidades, editando]);
const handleUpdateHorarios = (horariosAtualizados) => {
if (!editando) return;
setAvailabilityEdit(horariosAtualizados || []);
};
console.log("Horários editados:", horariosAtualizados);
const filteredDoctors = useMemo(() => {
if (!searchTerm) return doctors;
return doctors.filter((doc) => (doc.full_name || doc.name).toLowerCase().includes(searchTerm.toLowerCase()));
}, [doctors, searchTerm]);
const handleCancelarEdicao = () => {
setEditando(null);
setAvailabilityEdit([]);
};
const handleDoctorSelect = (doctor) => {
setSearchTerm(doctor.full_name || doctor.name);
setShowSuggestions(false);
};
const handleClearSearch = () => {
setSearchTerm("");
setShowSuggestions(false);
};
const getStatusBadgeClass = (disp) => {
if (disp.is_empty) return "status-badge status-not-configured";
if (disp.active === false) return "status-badge status-inactive";
return "status-badge status-active";
};
const getStatusText = (disp) => {
if (disp.is_empty) return "Não configurado";
if (disp.active === false) return "Inativa";
return "Ativa";
fetchDisponibilidades(filtroMedicoNome);
};
return (
<div className="disponibilidades-container">
<h1 className="disponibilidades-title">Disponibilidades dos Médicos</h1>
<div id="main-content">
{/* 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>
<div className="search-container">
<div className="search-input-container">
{/* 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",
}}
>
{editando
? "← Voltar para Tabela"
: gerenciarModo
? "← Voltar"
: "+ Gerenciar Disponibilidades"}
</button>
</div>
{/* Campo de busca - ESCONDIDO NO MODO DE EDIÇÃO */}
{!editando && (
<div className="atendimento-eprocura">
<div className="busca-atendimento">
<input
type="text"
placeholder="Buscar médico por nome..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setShowSuggestions(true);
placeholder="Filtrar por Nome do Médico..."
value={filtroMedicoNome}
onChange={(e) => setFiltroMedicoNome(e.target.value)}
style={{
border: "1px solid #ccc",
borderRadius: "4px",
padding: "5px",
marginTop: "10px",
marginBottom: "10px",
}}
onFocus={() => setShowSuggestions(true)}
className="search-input"
/>
{searchTerm && (
<button onClick={handleClearSearch} className="clear-search-btn">
×
</button>
)}
</div>
{showSuggestions && searchTerm && filteredDoctors.length > 0 && (
<div className="suggestions-dropdown">
{filteredDoctors.map((doc) => (
<div key={doc.id} onClick={() => handleDoctorSelect(doc)} className="suggestion-item">
{doc.full_name || doc.name}
</div>
))}
</div>
)}
</div>
<section className="calendario-ou-filaespera">
<div className="fila-container">
<h2 className="section-title">{editando ? `Editar Horários` : "Lista de Disponibilidades"}</h2>
<h2 className="fila-titulo">
{editando
? "Editar Disponibilidade"
: gerenciarModo
? "Gerenciar Disponibilidades"
: "Disponibilidades Encontradas"}{" "}
({disponibilidades.length})
</h2>
{doctors.length === 0 ? (
<p className="loading-text">Carregando médicos...</p>
{loading ? (
<p>Carregando...</p>
) : disponibilidades.length === 0 ? (
<p>Nenhuma disponibilidade encontrada.</p>
) : editando ? (
<>
<div className="edit-container">
{initialAvailabilityParaEdicao.length > 0 ? (
<HorariosDisponibilidade initialAvailability={initialAvailabilityParaEdicao} onUpdate={handleUpdateHorarios} onCancel={handleCancelarEdicao} />
) : (
<p className="loading-text">Carregando horários para edição...</p>
)}
</div>
<div className="disp-buttons-container">
<HorariosDisponibilidade
initialAvailability={initialAvailabilityParaEdicao}
onUpdate={handleUpdateHorarios}
/>
<button
onClick={() =>
salvarTodasDisponibilidades(editando, availabilityEdit.length > 0 ? availabilityEdit : initialAvailabilityParaEdicao)
handleUpdateHorarios(initialAvailabilityParaEdicao)
}
className="disp-btn-primary"
style={{
marginTop: "20px",
padding: "10px 20px",
fontSize: "16px",
fontWeight: "bold",
borderRadius: "8px",
backgroundColor: "#3b82f6",
color: "white",
border: "none",
cursor: "pointer",
}}
>
Salvar Alterações
</button>
<button onClick={handleCancelarEdicao} className="disp-btn-danger">
Cancelar
</button>
</div>
</>
) : (
<div className="doctor-group-container">
{disponibilidadesAgrupadas.length === 0 ? (
<p className="no-results">Nenhum médico encontrado</p>
) : (
disponibilidadesAgrupadas.map((grupo) => (
<div key={grupo.doctor_id} className={`doctor-group ${expandedDoctors[grupo.doctor_id] ? "expanded" : ""}`}>
<div className="doctor-header" onClick={() => toggleExpandDoctor(grupo.doctor_id)}>
<h3 className="doctor-name">
{grupo.doctor_name}
<span className="doctor-hours">({grupo.disponibilidades.filter((d) => !d.is_empty).length} horários)</span>
</h3>
<span className={`expand-icon ${expandedDoctors[grupo.doctor_id] ? "expanded" : ""}`}></span>
</div>
{expandedDoctors[grupo.doctor_id] && (
<div className="doctor-content">
<div className="edit-btn-container">
<button onClick={() => setEditando(grupo.doctor_id)} className="disp-btn-edit">
{grupo.disponibilidades.some((d) => !d.is_empty) ? "Editar" : "Cadastrar Horários"}
</button>
</div>
<div className="table-container">
<table className="disponibilidades-table">
<table style={{ width: "100%", borderCollapse: "collapse" }}>
<thead>
<tr>
<th>Dia da Semana</th>
<th>Início</th>
<th>Término</th>
<th>Intervalo (min)</th>
<th>Tipo</th>
<th>Status</th>
<th>Ações</th>
<th>Intervalo</th>
<th>Tipo Consulta</th>
{gerenciarModo && <th>Ações</th>}
</tr>
</thead>
<tbody>
{grupo.disponibilidades.map((disp) => (
{disponibilidades.map((disp) => (
<tr key={disp.id}>
<td>{disp.is_empty ? "Nenhum horário cadastrado" : getDiaSemana(disp.weekday)}</td>
<td>{disp.is_empty ? "-" : formatTime(disp.start_time)}</td>
<td>{disp.is_empty ? "-" : formatTime(disp.end_time)}</td>
<td>{disp.is_empty ? "-" : disp.slot_minutes || 30}</td>
<td>{disp.is_empty ? "-" : disp.appointment_type || "presencial"}</td>
<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>
<span className={getStatusBadgeClass(disp)}>{getStatusText(disp)}</span>
</td>
<td>
{!disp.is_empty && (
<button
onClick={() => handleOpenDeleteModal(disp.id)}
className="disp-btn-delete"
onClick={() => setEditando(disp.id)}
style={{
backgroundColor: "#10b981",
color: "white",
borderRadius: "6px",
}}
>
Editar
</button>{" "}
<button
onClick={() => deletarDisponibilidade(disp.id)}
style={{
backgroundColor: "#c72f2f",
color: "white",
borderRadius: "6px",
}}
>
Excluir
</button>
)}
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
))
)}
</div>
)}
</div>
</section>
{showDeleteModal && (
<div
className="modal fade show delete-modal"
style={{
display: "block",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
tabIndex="-1"
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#dc3545', color: 'white' }}>
<h5 className="modal-title">
Confirmação de Exclusão
</h5>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
Tem certeza que deseja excluir esta disponibilidade?
</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={handleCloseDeleteModal}
>
Cancelar
</button>
<button
type="button"
className="btn btn-danger"
onClick={() => deletarDisponibilidade(selectedDisponibilidadeId)}
>
Excluir
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};

View File

@ -4,24 +4,37 @@ import { useParams,Link, useNavigate, useLocation } from "react-router-dom";
import { GetDoctorByID } from "../components/utils/Functions-Endpoints/Doctor";
import { useAuth } from "../components/utils/AuthProvider";
const DoctorDetails = ({DictInfo}) => {
const Details = () => {
const {getAuthorizationHeader} = useAuth();
const [doctor, setDoctor] = useState({});
const Parametros = useParams()
const navigate = useNavigate();
const location = useLocation();
const navigateEdit = () => {
const prefixo = location.pathname.split("/")[1];
navigate(`/${prefixo}/medicos/edit`);
}
const Voltar = () => {
const prefixo = location.pathname.split("/")[1];
navigate(`/${prefixo}/medicos`);
}
const doctorID = Parametros.id
useEffect(() => {
if (!doctorID) return;
const authHeader = getAuthorizationHeader()
GetDoctorByID(doctorID, authHeader)
.then((data) => {
console.log(data, "médico vindo da API");
setDoctor(data[0])
; // supabase retorna array
})
.catch((err) => console.error("Erro ao buscar paciente:", err));
}, [doctorID]);
//if (!doctor) return <p style={{ textAlign: "center" }}>Carregando...</p>;
return (
<>
<div className="card p-3 shadow-sm">
@ -37,13 +50,15 @@ const Voltar = () => {
<img src={avatarPlaceholder} alt="" />
</div>
<div className="media-body ms-3 font-extrabold">
<span>{DictInfo.full_name || "Nome Completo"}</span>
<p>{DictInfo.cpf || "CPF"}</p>
<span>{doctor.nome || "Nome Completo"}</span>
<p>{doctor.cpf || "CPF"}</p>
</div>
</div>
<button className="btn btn-light" onClick={() => {navigateEdit()}} >
<Link to={`edit`}>
<button className="btn btn-light" onClick={() => {console.log(doctor.id)}} >
<i className="bi bi-pencil-square"></i> Editar
</button>
</Link>
</div>
</div>
@ -54,29 +69,29 @@ const Voltar = () => {
<div className="row">
<div className="col-md-6 mb-3">
<label className="font-extrabold">Nome:</label>
<p>{DictInfo.full_name || "-"}</p>
<p>{doctor.full_name || "-"}</p>
</div>
<div className="col-md-6 mb-3">
<label className="font-extrabold">Data de nascimento:</label>
<p>{DictInfo.birth_date || "-"}</p>
<p>{doctor.birth_date || "-"}</p>
</div>
<div className="col-md-6 mb-3">
<label className="font-extrabold">CPF:</label>
<p>{DictInfo.cpf || "-"}</p>
<p>{doctor.cpf || "-"}</p>
</div>
<div className="col-md-6 mb-3">
<label className="font-extrabold">CRM:</label>
<p>{DictInfo.crm || "-"}</p>
<p>{doctor.crm || "-"}</p>
</div>
<div className="col-md-6 mb-3">
<label className="font-extrabold">Estado do CRM:</label>
<p>{DictInfo.crm_uf || "-"}</p>
<p>{doctor.crm_uf || "-"}</p>
</div>
<div className="col-md-6 mb-3">
<label className="font-extrabold">Especialização:</label>
<p>{DictInfo.specialty || "-"}</p>
<p>{doctor.specialty || "-"}</p>
</div>
</div>
</div>
@ -88,31 +103,31 @@ const Voltar = () => {
<div className="row">
<div className="col-md-4 mb-3">
<label className="font-extrabold">CEP:</label>
<p>{DictInfo.cep || "-"}</p>
<p>{doctor.cep || "-"}</p>
</div>
<div className="col-md-8 mb-3">
<label className="font-extrabold">Rua:</label>
<p>{DictInfo.street || "-"}</p>
<p>{doctor.street || "-"}</p>
</div>
<div className="col-md-4 mb-3">
<label className="font-extrabold">Bairro:</label>
<p>{DictInfo.neighborhood || "-"}</p>
<p>{doctor.neighborhood || "-"}</p>
</div>
<div className="col-md-4 mb-3">
<label className="font-extrabold">Cidade:</label>
<p>{DictInfo.city || "-"}</p>
<p>{doctor.city || "-"}</p>
</div>
<div className="col-md-2 mb-3">
<label className="font-extrabold">Estado:</label>
<p>{DictInfo.state || "-"}</p>
<p>{doctor.state || "-"}</p>
</div>
<div className="col-md-4 mb-3">
<label className="font-extrabold">Número:</label>
<p>{DictInfo.number || "-"}</p>
<p>{doctor.number || "-"}</p>
</div>
<div className="col-md-8 mb-3">
<label className="font-extrabold">Complemento:</label>
<p>{DictInfo.complement || "-"}</p>
<p>{doctor.complement || "-"}</p>
</div>
</div>
</div>
@ -124,15 +139,15 @@ const Voltar = () => {
<div className="row">
<div className="col-md-6 mb-3">
<label className="font-extrabold">Email:</label>
<p>{DictInfo.email || "-"}</p>
<p>{doctor.email || "-"}</p>
</div>
<div className="col-md-6 mb-3">
<label className="font-extrabold">Telefone:</label>
<p>{DictInfo.phone_mobile || "-"}</p>
<p>{doctor.phone_mobile || "-"}</p>
</div>
<div className="col-md-6 mb-3">
<label className="font-extrabold">Telefone 2:</label>
<p>{DictInfo.phone2 || "-"}</p>
<p>{doctor.phone2 || "-"}</p>
</div>
</div>
@ -141,4 +156,4 @@ const Voltar = () => {
);
};
export default DoctorDetails;
export default Details;

View File

@ -1,333 +1,146 @@
import React, { useState, useEffect, useMemo } from "react";
import { useParams, useNavigate, useLocation } from "react-router-dom";
import React, { useEffect, useState, useCallback } from "react";
import { useParams, useSearchParams } from "react-router-dom";
import { GetDoctorByID } from "../components/utils/Functions-Endpoints/Doctor";
import DoctorForm from "../components/doctors/DoctorForm";
import { useAuth } from "../components/utils/AuthProvider";
import API_KEY from "../components/utils/apiKeys";
const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors";
const ENDPOINT_AVAILABILITY = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
const ENDPOINT_AVAILABILITY =
"https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
const diasDaSemana = ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"];
const weekdayNumToStr = {
0: "sunday",
1: "monday",
2: "tuesday",
3: "wednesday",
4: "thursday",
5: "friday",
6: "saturday",
};
const weekdayStrToNum = Object.fromEntries(
Object.entries(weekdayNumToStr).map(([num, str]) => [str, Number(num)])
);
const EditDoctorPage = () => {
const { id } = useParams();
const navigate = useNavigate();
const location = useLocation();
const DoctorEditPage = () => {
const { getAuthorizationHeader } = useAuth();
const [DoctorToPUT, setDoctorPUT] = useState({});
const [doctor, setDoctor] = useState(null);
const [availability, setAvailability] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const Parametros = useParams();
const [searchParams] = useSearchParams();
const DoctorID = Parametros.id;
const availabilityId = searchParams.get("availabilityId");
const effectiveId = id;
const [availabilityToPATCH, setAvailabilityToPATCH] = useState(null);
const [mode, setMode] = useState("doctor");
const getHeaders = () => {
const myHeaders = new Headers();
useEffect(() => {
const authHeader = getAuthorizationHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
if (API_KEY) myHeaders.append("apikey", API_KEY);
myHeaders.append("Prefer", "return=representation");
return myHeaders;
};
const salvarDisponibilidades = async (doctorId, horariosAtualizados) => {
try {
const headers = getHeaders();
const promises = [];
const currentIds = new Set();
if (availabilityId) {
setMode("availability");
for (const dia of horariosAtualizados) {
if (dia.isChecked && dia.blocos.length > 0) {
for (const bloco of dia.blocos) {
const inicio = bloco.inicio.includes(":") ? bloco.inicio : bloco.inicio + ":00";
const termino = bloco.termino.includes(":") ? bloco.termino : bloco.termino + ":00";
const payload = {
doctor_id: doctorId,
weekday: weekdayNumToStr[dia.weekday],
start_time: inicio,
end_time: termino,
slot_minutes: bloco.slot_minutes || 30,
appointment_type: bloco.appointment_type || "presencial",
active: true,
};
if (bloco.id && !bloco.isNew) {
currentIds.add(bloco.id);
promises.push(
fetch(`${ENDPOINT_AVAILABILITY}?id=eq.${bloco.id}`, {
method: "PATCH",
headers,
body: JSON.stringify(payload),
}).then((res) => {
if (!res.ok) throw new Error(`Erro no PATCH: ${res.status}`);
return { type: "PATCH", id: bloco.id };
})
);
} else {
promises.push(
fetch(ENDPOINT_AVAILABILITY, {
method: "POST",
headers,
body: JSON.stringify(payload),
fetch(`${ENDPOINT_AVAILABILITY}?id=eq.${availabilityId}&select=*`, {
method: "GET",
headers: {
apikey: API_KEY,
Authorization: authHeader,
},
})
.then((res) => res.json())
.then((data) => {
const createdItem = Array.isArray(data) ? data[0] : data;
if (createdItem && createdItem.id) {
return { type: "POST", id: createdItem.id };
if (data && data.length > 0) {
setAvailabilityToPATCH(data[0]);
console.log("Disponibilidade vinda da API:", data[0]);
}
return { type: "POST", id: null };
})
);
}
}
}
}
const results = await Promise.all(promises);
results.forEach((res) => {
if (res.type === "POST" && res.id) currentIds.add(res.id);
});
const existingDisponibilidadesRes = await fetch(
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${String(doctorId)}`,
{ method: "GET", headers }
);
if (existingDisponibilidadesRes.ok) {
const existingDisponibilidades = await existingDisponibilidadesRes.json();
const deletePromises = existingDisponibilidades
.filter((disp) => !currentIds.has(disp.id))
.map((disp) =>
fetch(`${ENDPOINT_AVAILABILITY}?id=eq.${disp.id}`, {
method: "DELETE",
headers,
.catch((err) => console.error("Erro ao buscar disponibilidade:", err));
} else {
setMode("doctor");
GetDoctorByID(DoctorID, authHeader)
.then((data) => {
console.log(data, "médico vindo da API");
setDoctorPUT(data[0]);
})
);
await Promise.all(deletePromises);
.catch((err) => console.error("Erro ao buscar paciente:", err));
}
}, [DoctorID, availabilityId, getAuthorizationHeader]);
const updatedResponse = await fetch(
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${doctorId}&order=weekday.asc,start_time.asc`,
{ method: "GET", headers }
const HandlePutDoctor = async () => {
const authHeader = getAuthorizationHeader();
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify(DoctorToPUT);
console.log("Enviando médico para atualização (PUT):", DoctorToPUT);
var requestOptions = {
method: "PUT",
headers: myHeaders,
body: raw,
redirect: "follow",
};
try {
const response = await fetch(
`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${DoctorID}`,
requestOptions
);
if (updatedResponse.ok) {
const updatedData = await updatedResponse.json();
setAvailability(updatedData);
}
console.log("Resposta PUT Doutor:", response);
alert("Dados do médico atualizados com sucesso!");
} catch (error) {
console.error("Erro ao atualizar médico:", error);
alert("Erro ao atualizar dados do médico.");
throw error;
}
};
const normalizeAvailabilityForForm = (availabilityData) => {
if (!Array.isArray(availabilityData)) return [];
// 2. Função para Atualizar DISPONIBILIDADE (PATCH)
const HandlePatchAvailability = async (data) => {
const authHeader = getAuthorizationHeader();
const disponibilidadesMedico = availabilityData.filter((d) =>
String(d.doctor_id) === String(effectiveId) && d.active !== false
);
const blocosPorDia = {};
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
disponibilidadesMedico.forEach((d) => {
const num = typeof d.weekday === "string" ? weekdayStrToNum[d.weekday.toLowerCase()] : d.weekday;
if (num === undefined || num === null) return;
if (!blocosPorDia[num]) blocosPorDia[num] = [];
blocosPorDia[num].push({
id: d.id,
inicio: d.start_time?.substring(0, 5) || "07:00",
termino: d.end_time?.substring(0, 5) || "17:00",
slot_minutes: d.slot_minutes || 30,
appointment_type: d.appointment_type || "presencial",
isNew: false,
});
});
var raw = JSON.stringify(data);
const resultado = [1, 2, 3, 4, 5, 6, 0].map((weekday) => {
const blocosDoDia = blocosPorDia[weekday] || [];
return {
dia: diasDaSemana[weekday],
weekday: weekday,
isChecked: blocosDoDia.length > 0,
blocos:
blocosDoDia.length > 0
? blocosDoDia
: [
{
id: null,
inicio: "07:00",
termino: "17:00",
slot_minutes: 30,
appointment_type: "presencial",
isNew: true,
},
],
};
});
console.log("Enviando disponibilidade para atualização (PATCH):", data);
return resultado;
};
const availabilityFormatted = useMemo(() => {
return normalizeAvailabilityForForm(availability);
}, [availability, effectiveId]);
useEffect(() => {
const fetchDoctorData = async () => {
if (!effectiveId || effectiveId === "edit") {
alert("ID do médico não encontrado");
navigate("/secretaria/medicos");
return;
}
try {
const doctorResponse = await fetch(`${ENDPOINT}?id=eq.${effectiveId}`, {
method: "GET",
headers: getHeaders(),
});
if (!doctorResponse.ok) {
throw new Error("Erro ao carregar dados do médico");
}
const doctorData = await doctorResponse.json();
if (doctorData.length === 0) {
throw new Error("Médico não encontrado");
}
setDoctor(doctorData[0]);
const availabilityResponse = await fetch(
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${effectiveId}&order=weekday.asc,start_time.asc`,
{
method: "GET",
headers: getHeaders(),
}
);
if (availabilityResponse.ok) {
const availabilityData = await availabilityResponse.json();
setAvailability(availabilityData);
} else {
setAvailability([]);
}
} catch (error) {
alert("Erro ao carregar dados do médico");
navigate("/secretaria/medicos");
} finally {
setIsLoading(false);
}
};
if (effectiveId) {
fetchDoctorData();
}
}, [effectiveId, navigate]);
const handleSave = async (formData) => {
const { availability: updatedAvailability, ...doctorDataToSave } = formData;
try {
setIsSaving(true);
const response = await fetch(`${ENDPOINT}?id=eq.${effectiveId}`, {
var requestOptions = {
method: "PATCH",
headers: getHeaders(),
body: JSON.stringify(doctorDataToSave),
});
if (!response.ok) {
throw new Error("Erro ao salvar dados do médico");
}
if (updatedAvailability && updatedAvailability.length > 0) {
await salvarDisponibilidades(effectiveId, updatedAvailability);
}
alert("Médico e horários atualizados com sucesso!");
navigate("/secretaria/medicos");
headers: myHeaders,
body: raw,
redirect: "follow",
};
try {
const response = await fetch(
`${ENDPOINT_AVAILABILITY}?id=eq.${availabilityId}`,
requestOptions
);
console.log("Resposta PATCH Disponibilidade:", response);
alert("Disponibilidade atualizada com sucesso!");
// Opcional: Redirecionar de volta para a lista de disponibilidades
// navigate('/disponibilidades');
} catch (error) {
alert(`Erro ao salvar dados: ${error.message}`);
} finally {
setIsSaving(false);
console.error("Erro ao atualizar disponibilidade:", error);
alert("Erro ao atualizar disponibilidade.");
throw error;
}
console.log('Horários a serem salvos:', updatedAvailability);
};
const handleCancel = () => {
navigate("/secretaria/medicos");
};
if (isLoading) {
return (
<div className="container mt-4">
<div className="d-flex justify-content-center">
<div className="spinner-border" role="status">
<span className="visually-hidden">Carregando...</span>
</div>
</div>
<p className="text-center mt-2">
Carregando dados do médico ID: {effectiveId || "..."}
</p>
</div>
);
}
if (!doctor) {
if (!isLoading) {
return (
<div className="container mt-4">
<div className="alert alert-danger">
Médico não encontrado
</div>
</div>
);
}
return null;
}
const formData = {
...doctor,
availability: (doctor && doctor.availability) ? doctor.availability : availabilityFormatted,
};
return (
<div className="container mt-4">
<div className="row">
<div className="col-12">
<h1>Editar Médico</h1>
<div>
<h1 className="text-2xl font-bold mb-4">
{mode === "availability"
? `Editar Horário Disponível (ID: ${availabilityId.substring(0, 8)})`
: `Editar Médico (ID: ${DoctorID})`}
</h1>
<DoctorForm
formData={formData}
setFormData={setDoctor}
onSave={handleSave}
onCancel={handleCancel}
isLoading={isSaving}
isEditing={true}
onSave={
mode === "availability" ? HandlePatchAvailability : HandlePutDoctor
}
formData={mode === "availability" ? availabilityToPATCH : DoctorToPUT}
setFormData={
mode === "availability" ? setAvailabilityToPATCH : setDoctorPUT
}
isEditingAvailability={mode === "availability"}
/>
</div>
</div>
</div>
);
};
export default EditDoctorPage;
export default DoctorEditPage;

View File

@ -4,7 +4,7 @@ import { useAuth } from "../components/utils/AuthProvider";
import { Link } from "react-router-dom";
import "./style/TableDoctor.css";
function TableDoctor({setDictInfo}) {
function TableDoctor() {
const { getAuthorizationHeader, isAuthenticated } = useAuth();
const [medicos, setMedicos] = useState([]);
@ -12,6 +12,7 @@ function TableDoctor({setDictInfo}) {
const [filtroEspecialidade, setFiltroEspecialidade] = useState("Todos");
const [filtroAniversariante, setFiltroAniversariante] = useState(false);
const [showFiltrosAvancados, setShowFiltrosAvancados] = useState(false);
const [filtroCidade, setFiltroCidade] = useState("");
const [filtroEstado, setFiltroEstado] = useState("");
@ -21,15 +22,9 @@ function TableDoctor({setDictInfo}) {
const [dataFinal, setDataFinal] = useState("");
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedDoctorId, setSelectedDoctorId] = useState(null);
const [sortKey, setSortKey] = useState(null);
const [sortDir, setSortDir] = useState('asc');
const limparFiltros = () => {
setSearch("");
setFiltroEspecialidade("Todos");
@ -41,9 +36,9 @@ function TableDoctor({setDictInfo}) {
setIdadeMaxima("");
setDataInicial("");
setDataFinal("");
setPaginaAtual(1);
};
const deleteDoctor = async (id) => {
const authHeader = getAuthorizationHeader()
console.log(id, 'teu id')
@ -68,6 +63,7 @@ function TableDoctor({setDictInfo}) {
}
};
const ehAniversariante = (dataNascimento) => {
if (!dataNascimento) return false;
const hoje = new Date();
@ -79,6 +75,7 @@ function TableDoctor({setDictInfo}) {
);
};
const calcularIdade = (dataNascimento) => {
if (!dataNascimento) return 0;
const hoje = new Date();
@ -107,16 +104,18 @@ function TableDoctor({setDictInfo}) {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions)
.then(response => response.json())
.then(result => setMedicos(result))
.then(result => {setMedicos(result); console.log(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
@ -133,75 +132,23 @@ function TableDoctor({setDictInfo}) {
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 applySorting = (arr) => {
if (!Array.isArray(arr) || !sortKey) return arr;
const copy = [...arr];
if (sortKey === 'nome') {
copy.sort((a, b) => (a.full_name || '').localeCompare((b.full_name || ''), undefined, { sensitivity: 'base' }));
} else if (sortKey === 'idade') {
copy.sort((a, b) => calcularIdade(a.birth_date) - calcularIdade(b.birth_date));
}
if (sortDir === 'desc') copy.reverse();
return copy;
};
const medicosOrdenados = applySorting(medicosFiltrados);
const totalPaginas = Math.ceil(medicosFiltrados.length / itensPorPagina);
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const medicosPaginados = medicosOrdenados.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(() => {
setPaginaAtual(1);
}, [search, filtroEspecialidade, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal, sortKey, sortDir]);
console.log(` Médicos totais: ${medicos.length}, Filtrados: ${medicosFiltrados.length}`);
}, [medicos, medicosFiltrados, search]);
return (
<>
@ -222,6 +169,7 @@ function TableDoctor({setDictInfo}) {
</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>{" "}
@ -232,15 +180,16 @@ function TableDoctor({setDictInfo}) {
<input
type="text"
className="form-control"
placeholder="Buscar por nome, CPF ou email..."
placeholder="Buscar por nome ou CPF..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<small className="text-muted">
Digite o nome completo, CPF ou email
Digite o nome completo ou número do CPF
</small>
</div>
<div className="filtros-basicos">
<select
className="form-select filter-especialidade"
@ -264,6 +213,7 @@ function TableDoctor({setDictInfo}) {
</select>
<div className="filter-buttons-container">
<button
className={`btn filter-btn ${filtroAniversariante
? "btn-primary"
@ -274,34 +224,6 @@ function TableDoctor({setDictInfo}) {
<i className="bi bi-calendar me-1"></i> Aniversariantes
</button>
</div>
<div className="vr mx-2 d-none d-md-block" />
<div className="d-flex align-items-center gap-2">
<span className="text-muted small">Ordenar por:</span>
{(() => {
const sortValue = sortKey ? `${sortKey}-${sortDir}` : '';
return (
<select
className="form-select compact-select sort-select w-auto"
value={sortValue}
onChange={(e) => {
const v = e.target.value;
if (!v) { setSortKey(null); setSortDir('asc'); return; }
const [k, d] = v.split('-');
setSortKey(k);
setSortDir(d);
}}
>
<option value="">Sem ordenação</option>
<option value="nome-asc">Nome (A-Z)</option>
<option value="nome-desc">Nome (Z-A)</option>
<option value="idade-asc">Idade (crescente)</option>
<option value="idade-desc">Idade (decrescente)</option>
</select>
);
})()}
</div>
</div>
<div className="d-flex justify-content-between align-items-center mt-3">
@ -321,11 +243,13 @@ function TableDoctor({setDictInfo}) {
</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
@ -372,6 +296,7 @@ function TableDoctor({setDictInfo}) {
/>
</div>
{/* Data de Cadastro */}
<div className="col-md-6">
<label className="form-label fw-bold">Data inicial</label>
<input
@ -393,14 +318,36 @@ function TableDoctor({setDictInfo}) {
</div>
</div>
)}
</div>
<div className="mt-3">
<div className="contador-medicos">
{medicosFiltrados.length} DE {medicos.length} MÉDICOS ENCONTRADOS
{(search || filtroEspecialidade !== "Todos" || filtroAniversariante || // filtroVIP removido
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>}
{idadeMinima && <span className="badge bg-primary me-2">Idade mín: {idadeMinima}</span>}
{idadeMaxima && <span className="badge bg-primary me-2">Idade máx: {idadeMaxima}</span>}
{dataInicial && <span className="badge bg-primary me-2">Data inicial: {dataInicial}</span>}
{dataFinal && <span className="badge bg-primary me-2">Data final: {dataFinal}</span>}
</div>
</div>
)}
<div className="mb-3">
<span className="badge results-badge">
{medicosFiltrados.length} de {medicos.length} médicos encontrados
</span>
</div>
<div className="table-responsive">
<table className="table table-striped table-hover table-doctor-table">
<thead>
@ -413,8 +360,8 @@ function TableDoctor({setDictInfo}) {
</tr>
</thead>
<tbody>
{medicosPaginados.length > 0 ? (
medicosPaginados.map((medico) => (
{medicosFiltrados.length > 0 ? (
medicosFiltrados.map((medico) => (
<tr key={medico.id}>
<td>
<div className="d-flex align-items-center">
@ -424,6 +371,7 @@ function TableDoctor({setDictInfo}) {
<i className="bi bi-gift"></i>
</span>
)}
</div>
</td>
<td>{medico.cpf}</td>
@ -435,14 +383,14 @@ function TableDoctor({setDictInfo}) {
<td>{medico.email || 'Não informado'}</td>
<td>
<div className="d-flex gap-2">
<Link to={`details/${medico.id}`}>
<button className="btn btn-sm btn-view" onClick={() => setDictInfo({...medico})}>
<Link to={`${medico.id}`}>
<button className="btn btn-sm btn-view">
<i className="bi bi-eye me-1"></i> Ver Detalhes
</button>
</Link>
<Link to={`edit/${medico.id}`}>
<button className="btn btn-sm btn-edit" onClick={() => setDictInfo({...medico})}>
<Link to={`${medico.id}/edit`}>
<button className="btn btn-sm btn-edit">
<i className="bi bi-pencil me-1"></i> Editar
</button>
</Link>
@ -462,74 +410,13 @@ function TableDoctor({setDictInfo}) {
))
) : (
<tr>
<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 colSpan="5" className="empty-state">
Nenhum médico encontrado.
</td>
</tr>
)}
</tbody>
</table>
{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>
@ -551,10 +438,15 @@ function TableDoctor({setDictInfo}) {
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#dc3545', color: 'white' }}>
<div className="modal-header">
<h5 className="modal-title">
Confirmação de Exclusão
</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
></button>
</div>
<div className="modal-body">

View File

@ -1,21 +1,34 @@
import React from 'react'
import PatientForm from '../components/patients/PatientForm'
import {useEffect, useState} from 'react'
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient'
import API_KEY from '../components/utils/apiKeys'
import {useNavigate, useParams } from 'react-router-dom'
import { useAuth } from '../components/utils/AuthProvider'
const EditPage = ({DictInfo}) => {
const EditPage = () => {
const navigate = useNavigate()
const Parametros = useParams()
const [PatientToPUT, setPatientPUT] = useState({})
const [showSuccessModal, setShowSuccessModal] = useState(false)
const { getAuthorizationHeader, isAuthenticated } = useAuth();
const PatientID = Parametros.id
useEffect(() => {
setPatientPUT(DictInfo)
}, [DictInfo])
const authHeader = getAuthorizationHeader()
GetPatientByID(PatientID, authHeader)
.then((data) => {
console.log(data[0], "paciente vindo da API");
setPatientPUT(data[0]); // supabase retorna array
})
.catch((err) => console.error("Erro ao buscar paciente:", err));
}, [PatientID])
const HandlePutPatient = async () => {
const authHeader = getAuthorizationHeader()
@ -26,9 +39,9 @@ const HandlePutPatient = async () => {
myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify({...PatientToPUT, bmi:Number(PatientToPUT) || null});
var raw = JSON.stringify(PatientToPUT);
console.log("Enviando atualização:", PatientToPUT);
console.log("Enviando paciente para atualização:", PatientToPUT);
var requestOptions = {
method: 'PATCH',
@ -37,16 +50,25 @@ const HandlePutPatient = async () => {
redirect: 'follow'
};
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients?id=eq.${PatientToPUT.id}`,requestOptions)
.then(response => {
try {
const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients?id=eq.${PatientID}`,requestOptions);
console.log(response)
if (response.ok) {
setShowSuccessModal(true)
if(response.ok === false){
console.error("Erro ao atualizar paciente:");
}
return response
})
.then(result => console.log(result))
.catch(error => console.log("erro", error))
else{
console.log("ATUALIZADO COM SUCESSO");
navigate('/secretaria/pacientes')
}
return response;
} catch (error) {
console.error("Erro ao atualizar paciente:", error);
throw error;
}
};
@ -60,46 +82,6 @@ const HandlePutPatient = async () => {
setFormData={setPatientPUT}
/>
{showSuccessModal && (
<div
className="modal fade show delete-modal"
style={{
display: "block",
backgroundColor: "rgba(0, 0, 0, 0,5)",
}}
tabIndex="-1"
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
<h5 className="modal-title">
Paciente Editado
</h5>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
Paciente editado com sucesso!
</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() => {
setShowSuccessModal(false)
navigate(-1)
}}
>
OK
</button>
</div>
</div>
</div>
</div>
)}
</div>
)
}

View File

@ -1,5 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import 'dayjs/locale/pt-br';
import weekday from 'dayjs/plugin/weekday';
@ -24,10 +23,12 @@ const getDateRange = (date, view) => {
toDate = startDayjs.format('YYYY-MM-DD');
titleRange = startDayjs.format('DD/MM/YYYY');
} else if (view === 'semanal') {
// Padrão Dayjs: Sunday=0, Monday=1.
// startOf('week') pode ser Domingo ou Segunda, dependendo do locale.
// Se precisar forçar a Segunda-feira:
let weekStart = startDayjs.startOf('week');
if (weekStart.day() !== 1) {
weekStart = startDayjs.weekday(1);
if (weekStart.day() !== 1) { // Se não for segunda-feira (1), ajusta
weekStart = startDayjs.weekday(1); // Vai para a segunda-feira desta semana
}
const weekEnd = weekStart.add(6, 'day');
@ -51,21 +52,12 @@ const getDateRange = (date, view) => {
const ExcecoesDisponibilidade = () => {
const { getAuthorizationHeader } = useAuth();
const navigate = useNavigate();
const [pageNovaExcecao, setPageNovaExcecao] = useState(false);
const [excecoes, setExcecoes] = useState([]);
const [loading, setLoading] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedExceptionId, setSelectedExceptionId] = useState(null);
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
const [filtroMedicoId, setFiltroMedicoId] = useState('');
const [filtroData, setFiltroData] = useState(dayjs().format('YYYY-MM-DD'));
const [listaDeMedicos, setListaDeMedicos] = useState([]);
const [searchTermDoctor, setSearchTermDoctor] = useState('');
const [filteredDoctors, setFilteredDoctors] = useState([]);
const [selectedDoctor, setSelectedDoctor] = useState(null);
const [visualizacao, setVisualizacao] = useState('diario');
@ -131,51 +123,8 @@ const ExcecoesDisponibilidade = () => {
fetchExcecoes(fromDate, toDate, filtroMedicoId);
}, [fetchExcecoes, filtroMedicoId, fromDate, toDate]);
useEffect(() => {
const fetchDoctors = async () => {
const myHeaders = new Headers();
const authHeader = resolveAuthHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
if (API_KEY) myHeaders.append("apikey", API_KEY);
try {
const response = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?select=id,full_name', {
method: 'GET',
headers: myHeaders
});
if (response.ok) {
const doctors = await response.json();
setListaDeMedicos(doctors);
}
} catch (error) {
console.error('Erro ao buscar médicos:', error);
}
};
fetchDoctors();
}, []);
const handleSearchDoctors = (term) => {
setSearchTermDoctor(term);
if (term.trim() === '') {
setFilteredDoctors([]);
return;
}
const filtered = listaDeMedicos.filter(doc =>
doc.full_name.toLowerCase().includes(term.toLowerCase())
);
setFilteredDoctors(filtered);
};
const limparFiltros = () => {
setSearchTermDoctor('');
setFilteredDoctors([]);
setSelectedDoctor(null);
setFiltroMedicoId('');
setFiltroData(dayjs().format('YYYY-MM-DD'));
setVisualizacao('diario');
};
const deleteExcecao = async (id) => {
if (!window.confirm("Confirma exclusão desta exceção?")) return;
const myHeaders = new Headers();
const authHeader = resolveAuthHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
@ -190,9 +139,6 @@ const ExcecoesDisponibilidade = () => {
});
if (res.ok) {
setExcecoes(prev => prev.filter(x => x.id !== id));
setShowDeleteModal(false);
setSuccessMessage('Exceção excluída com sucesso!');
setShowSuccessModal(true);
} else {
const text = await res.text();
console.error('Erro ao deletar exceção', res.status, text);
@ -217,7 +163,7 @@ const ExcecoesDisponibilidade = () => {
return (
<div>
{/* Título e Botão de Criação */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<h1>Gerenciar Exceções de Disponibilidade</h1>
<button
@ -229,80 +175,30 @@ const ExcecoesDisponibilidade = () => {
</button>
</div>
<div className="card p-3 mb-3" style={{ marginTop: '20px' }}>
<h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i>
Filtros
</h5>
<div className='atendimento-eprocura'>
<div className="row g-3 mb-3">
<div className="col-md-6">
<label className="form-label fw-bold">Buscar Médico</label>
{/* Filtros de Médico e Data */}
<div className='busca-atendimento'>
<div>
<i className="fa-solid fa-user-doctor"></i>
<input
type="text"
className="form-control"
placeholder="Digite o nome do médico..."
value={searchTermDoctor}
onChange={(e) => handleSearchDoctors(e.target.value)}
placeholder="Filtrar por ID do Médico..."
value={filtroMedicoId}
onChange={(e) => setFiltroMedicoId(e.target.value)}
/>
<small className="text-muted">Filtre as exceções por médico</small>
{searchTermDoctor && filteredDoctors.length > 0 && (
<div className="list-group mt-2" style={{ maxHeight: '200px', overflowY: 'auto' }}>
{filteredDoctors.map((doc) => (
<button
key={doc.id}
className="list-group-item list-group-item-action"
onClick={() => {
setSearchTermDoctor(doc.full_name);
setFilteredDoctors([]);
setSelectedDoctor(doc);
setFiltroMedicoId(doc.id);
}}
>
{doc.full_name}
</button>
))}
</div>
)}
</div>
<div className="col-md-6">
<label className="form-label fw-bold">Data de Referência</label>
<div>
<i className="fa-solid fa-calendar"></i>
<input
type="date"
className="form-control"
value={filtroData}
onChange={(e) => setFiltroData(e.target.value)}
/>
<small className="text-muted">Selecione a data base para visualização</small>
</div>
</div>
<div className="d-flex justify-content-between align-items-center">
<div>
{selectedDoctor && (
<span className="badge bg-primary me-2">
<i className="bi bi-person-fill me-1"></i>
{selectedDoctor.full_name}
</span>
)}
<div className="contador-pacientes" style={{ display: 'inline-block' }}>
{excecoes.length} DE {excecoes.length} EXCEÇÕES ENCONTRADAS
</div>
</div>
<button
className="btn btn-outline-secondary btn-sm"
onClick={limparFiltros}
>
<i className="bi bi-arrow-clockwise me-1"></i> Limpar Filtros
</button>
</div>
</div>
<div className='atendimento-eprocura'>
{/* Botões de Visualização (Dia/Semana/Mês) */}
<div className='container-btns-agenda-fila_esepera'>
<button
className={`btn-agenda ${visualizacao === "diario" ? "opc-agenda-ativo" : ""}`}
@ -324,7 +220,7 @@ const ExcecoesDisponibilidade = () => {
</button>
</div>
{/* Tabela de Exceções (Título usa o titleRange calculado) */}
<section className='calendario-ou-filaespera'>
<div className="fila-container">
<h2 className="fila-titulo">Exceções em {titleRange} ({excecoes.length})</h2>
@ -368,10 +264,7 @@ const ExcecoesDisponibilidade = () => {
<button
className="btn btn-sm btn-delete"
onClick={() => {
setSelectedExceptionId(exc.id);
setShowDeleteModal(true);
}}
onClick={() => deleteExcecao(exc.id)}
>
<i className="bi bi-trash me-1"></i> Excluir
</button>
@ -385,89 +278,6 @@ const ExcecoesDisponibilidade = () => {
</div>
</section>
</div>
{showDeleteModal && (
<div
className="modal fade show delete-modal"
style={{
display: "block",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
tabIndex="-1"
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#dc3545', color: 'white' }}>
<h5 className="modal-title">
Confirmação de Exclusão
</h5>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
Tem certeza que deseja excluir esta exceção?
</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={() => deleteExcecao(selectedExceptionId)}
>
Excluir
</button>
</div>
</div>
</div>
</div>
)}
{showSuccessModal && (
<div
className="modal fade show delete-modal"
style={{
display: "block",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
tabIndex="-1"
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
<h5 className="modal-title">
Sucesso
</h5>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
{successMessage}
</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() => setShowSuccessModal(false)}
>
OK
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect, useMemo, useCallback } from "react";
import "./style/FinanceiroDashboard.css";
import './style/FinanceiroDashboard.css';
const CONVENIOS_LIST = [
"Particular",
@ -8,7 +8,7 @@ const CONVENIOS_LIST = [
"SulAmérica",
"Unimed",
"Cassio",
"Outro",
"Outro"
];
function CurrencyInput({ value, onChange, label, id }) {
@ -17,25 +17,22 @@ function CurrencyInput({ value, onChange, label, id }) {
let stringValue = String(numericValue);
while (stringValue.length < 3) {
stringValue = "0" + stringValue;
stringValue = '0' + stringValue;
}
const integerPart = stringValue.slice(0, -2);
const decimalPart = stringValue.slice(-2);
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ".");
const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
return `R$ ${formattedInteger},${decimalPart}`;
}, [value]);
const handleKeyDown = useCallback(
(e) => {
const handleKeyDown = useCallback((e) => {
const key = e.key;
if (
["Backspace", "Delete", "ArrowLeft", "ArrowRight", "Tab"].includes(key)
) {
if (key === "Backspace" || key === "Delete") {
if (['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(key)) {
if (key === 'Backspace' || key === 'Delete') {
e.preventDefault();
const numericValue = value || 0;
let newValueString = String(numericValue);
@ -66,9 +63,7 @@ function CurrencyInput({ value, onChange, label, id }) {
const newNumericValue = parseInt(newValueString);
onChange(newNumericValue);
},
[value, onChange]
);
}, [value, onChange]);
return (
<div className="form-group">
@ -96,7 +91,7 @@ function mockFetchPagamentos() {
data_vencimento: "2025-09-30",
status: "pendente",
desconto: 0,
observacoes: "Pagamento parcelado em 2x",
observacoes: "Pagamento parcelado em 2x"
},
{
id: "PAY-002",
@ -106,7 +101,7 @@ function mockFetchPagamentos() {
data_vencimento: "2025-09-15",
status: "pago",
desconto: 1000,
observacoes: "",
observacoes: ""
},
{
id: "PAY-003",
@ -116,7 +111,7 @@ function mockFetchPagamentos() {
data_vencimento: "2025-09-20",
status: "vencido",
desconto: 0,
observacoes: "Não respondeu ao contato",
observacoes: "Não respondeu ao contato"
},
{
id: "PAY-004",
@ -126,8 +121,8 @@ function mockFetchPagamentos() {
data_vencimento: "2025-09-29",
status: "pago",
desconto: 500,
observacoes: "Desconto por pagamento adiantado",
},
observacoes: "Desconto por pagamento adiantado"
}
];
}
@ -137,11 +132,7 @@ export default function FinanceiroDashboard() {
const [query, setQuery] = useState("");
const [filtroStatus, setFiltroStatus] = useState("Todos");
const [novoPagamento, setNovoPagamento] = useState(false);
const [summary, setSummary] = useState({
totalRecebido: 0,
totalAReceber: 0,
totalDescontos: 0,
});
const [summary, setSummary] = useState({ totalRecebido: 0, totalAReceber: 0, totalDescontos: 0 });
useEffect(() => {
const data = mockFetchPagamentos();
@ -150,13 +141,7 @@ export default function FinanceiroDashboard() {
function formatCurrency(centavos) {
const valorEmReais = centavos / 100;
return (
"R$ " +
valorEmReais
.toFixed(2)
.replace(".", ",")
.replace(/\B(?=(\d{3})+(?!\d))/g, ".")
);
return "R$ " + valorEmReais.toFixed(2).replace(".", ",").replace(/\B(?=(\d{3})+(?!\d))/g, '.');
}
function getValorLiquido(valor, desconto) {
@ -164,11 +149,10 @@ export default function FinanceiroDashboard() {
}
const filteredPagamentos = useMemo(() => {
return pagamentos.filter((p) => {
return pagamentos.filter(p => {
const q = query.toLowerCase();
const statusOk = filtroStatus === "Todos" || p.status === filtroStatus;
const buscaOk =
p.paciente.nome.toLowerCase().includes(q) ||
const buscaOk = p.paciente.nome.toLowerCase().includes(q) ||
p.id.toLowerCase().includes(q);
return statusOk && buscaOk;
});
@ -179,50 +163,42 @@ export default function FinanceiroDashboard() {
let aReceber = 0;
let descontos = 0;
filteredPagamentos.forEach((p) => {
filteredPagamentos.forEach(p => {
const valorLiquido = getValorLiquido(p.valor, p.desconto);
if (p.status === "pago") {
if (p.status === 'pago') {
recebido += valorLiquido;
descontos += p.desconto;
} else {
aReceber += valorLiquido;
aReceber += p.valor;
}
});
setSummary({
totalRecebido: recebido,
totalAReceber: aReceber,
totalDescontos: descontos,
totalDescontos: descontos
});
}, [filteredPagamentos]);
function handleDelete(id) {
if (window.confirm("Tem certeza que deseja excluir este pagamento?")) {
setPagamentos((prev) => prev.filter((p) => p.id !== id));
setPagamentos(prev => prev.filter(p => p.id !== id));
setModalPagamento(null);
}
}
function handleSave(pagamento) {
if (
!pagamento.paciente.nome ||
!pagamento.valor ||
!pagamento.data_vencimento ||
!pagamento.paciente.convenio
) {
if (!pagamento.paciente.nome || !pagamento.valor || !pagamento.data_vencimento || !pagamento.paciente.convenio) {
alert("Preencha Paciente, Convênio, Valor e Data de Vencimento.");
return;
}
if (novoPagamento) {
const newId =
"PAY-" + (pagamentos.length + 1).toString().padStart(3, "0");
const newId = "PAY-" + (pagamentos.length + 1).toString().padStart(3, "0");
pagamento.id = newId;
setPagamentos((prev) => [...prev, pagamento]);
setPagamentos(prev => [...prev, pagamento]);
} else {
setPagamentos((prev) =>
prev.map((p) => (p.id === pagamento.id ? pagamento : p))
);
setPagamentos(prev => prev.map(p => p.id === pagamento.id ? pagamento : p));
}
setModalPagamento(null);
setNovoPagamento(false);
@ -258,30 +234,27 @@ export default function FinanceiroDashboard() {
className="input-field"
placeholder="Buscar paciente"
value={query}
onChange={(e) => setQuery(e.target.value)}
onChange={e => setQuery(e.target.value)}
style={{ flexGrow: 1 }}
/>
<select
className="select-field"
value={filtroStatus}
onChange={(e) => setFiltroStatus(e.target.value)}
>
<select className="select-field" value={filtroStatus} onChange={e => setFiltroStatus(e.target.value)}>
<option value="Todos">Todos</option>
<option value="pago">Pago</option>
<option value="pendente">Pendente</option>
<option value="vencido">Vencido</option>
</select>
<button
className="btn btn-primary"
className="action-btn"
style={{ background: "#3b82f6", color: "#fff", borderColor: "#3b82f6" }}
onClick={() => {
setModalPagamento({
paciente: { nome:"", convenio: CONVENIOS_LIST[0] },
valor:0,
forma_pagamento:"Dinheiro",
data_vencimento: new Date().toISOString().split("T")[0],
data_vencimento: new Date().toISOString().split('T')[0],
status:"pendente",
desconto:0,
observacoes: "",
observacoes:""
});
setNovoPagamento(true);
}}
@ -309,38 +282,29 @@ export default function FinanceiroDashboard() {
</tr>
</thead>
<tbody>
{filteredPagamentos.map((p) => (
{filteredPagamentos.map(p => (
<tr key={p.id}>
<td style={{ fontWeight: 600 }}>{p.paciente.nome}</td>
<td>{p.paciente.nome}</td>
<td>{p.paciente.convenio}</td>
<td>{formatCurrency(p.valor)}</td>
<td>{formatCurrency(p.desconto)}</td>
<td style={{ fontWeight: 600 }}>
{formatCurrency(getValorLiquido(p.valor, p.desconto))}
</td>
<td style={{ fontWeight: 600 }}>{formatCurrency(getValorLiquido(p.valor, p.desconto))}</td>
<td>{p.forma_pagamento}</td>
<td>{p.data_vencimento.split("-").reverse().join("/")}</td>
<td>
<span className={`badge ${p.status}`}>
{p.status.toUpperCase()}
</span>
</td>
<td>{p.data_vencimento.split('-').reverse().join('/')}</td>
<td><span className={`badge ${p.status}`}>{p.status.toUpperCase()}</span></td>
<td>
<div className="action-group">
<button
className="btn-view"
onClick={() => {
setModalPagamento({ ...p });
setNovoPagamento(false);
}}
className="action-btn"
onClick={() => { setModalPagamento({...p}); setNovoPagamento(false); }}
>
<i className="bi bi-eye me-1"></i> Ver Detalhes
Ver / Editar
</button>
<button
className="btn-delete"
className="action-btn delete"
onClick={() => handleDelete(p.id)}
>
<i className="bi bi-trash me-1"></i> Excluir
Excluir
</button>
</div>
</td>
@ -353,20 +317,11 @@ export default function FinanceiroDashboard() {
</div>
{modalPagamento && (
<div
className="modal"
onClick={(e) => e.target.classList.contains("modal") && closeModal()}
>
<div className="modal" onClick={(e) => e.target.classList.contains('modal') && closeModal()}>
<div className="modal-card">
<div className="modal-header">
<h2>
{novoPagamento
? "Adicionar Pagamento"
: `Editar Pagamento - ${modalPagamento.paciente.nome}`}
</h2>
<button className="close-btn" onClick={closeModal}>
×
</button>
<h2>{novoPagamento ? "Adicionar Pagamento" : `Editar Pagamento - ${modalPagamento.paciente.nome}`}</h2>
<button className="close-btn" onClick={closeModal}>×</button>
</div>
<div className="modal-body">
@ -376,15 +331,7 @@ export default function FinanceiroDashboard() {
id="paciente_nome"
className="input-field"
value={modalPagamento.paciente.nome}
onChange={(e) =>
setModalPagamento({
...modalPagamento,
paciente: {
...modalPagamento.paciente,
nome: e.target.value,
},
})
}
onChange={e => setModalPagamento({...modalPagamento, paciente:{...modalPagamento.paciente, nome:e.target.value}})}
/>
</div>
@ -394,21 +341,11 @@ export default function FinanceiroDashboard() {
id="convenio"
className="select-field"
value={modalPagamento.paciente.convenio}
onChange={(e) =>
setModalPagamento({
...modalPagamento,
paciente: {
...modalPagamento.paciente,
convenio: e.target.value,
},
})
}
onChange={e => setModalPagamento({...modalPagamento, paciente:{...modalPagamento.paciente, convenio:e.target.value}})}
>
<option value="">Selecione</option>
{CONVENIOS_LIST.map((conv) => (
<option key={conv} value={conv}>
{conv}
</option>
{CONVENIOS_LIST.map(conv => (
<option key={conv} value={conv}>{conv}</option>
))}
</select>
</div>
@ -416,18 +353,14 @@ export default function FinanceiroDashboard() {
id="valor"
label="Valor da consulta (R$)"
value={modalPagamento.valor}
onChange={(newValue) =>
setModalPagamento({ ...modalPagamento, valor: newValue })
}
onChange={newValue => setModalPagamento({...modalPagamento, valor: newValue})}
/>
<CurrencyInput
id="desconto"
label="Desconto aplicado (R$)"
value={modalPagamento.desconto}
onChange={(newValue) =>
setModalPagamento({ ...modalPagamento, desconto: newValue })
}
onChange={newValue => setModalPagamento({...modalPagamento, desconto: newValue})}
/>
<div className="form-group">
@ -436,12 +369,7 @@ export default function FinanceiroDashboard() {
id="forma"
className="select-field"
value={modalPagamento.forma_pagamento}
onChange={(e) =>
setModalPagamento({
...modalPagamento,
forma_pagamento: e.target.value,
})
}
onChange={e => setModalPagamento({...modalPagamento, forma_pagamento:e.target.value})}
>
<option>Dinheiro</option>
<option>Cartão</option>
@ -457,12 +385,7 @@ export default function FinanceiroDashboard() {
className="input-field"
type="date"
value={modalPagamento.data_vencimento}
onChange={(e) =>
setModalPagamento({
...modalPagamento,
data_vencimento: e.target.value,
})
}
onChange={e => setModalPagamento({...modalPagamento, data_vencimento:e.target.value})}
/>
</div>
@ -472,12 +395,7 @@ export default function FinanceiroDashboard() {
id="status"
className="select-field"
value={modalPagamento.status}
onChange={(e) =>
setModalPagamento({
...modalPagamento,
status: e.target.value,
})
}
onChange={e => setModalPagamento({...modalPagamento, status:e.target.value})}
>
<option value="pago">Pago</option>
<option value="pendente">Pendente</option>
@ -492,25 +410,22 @@ export default function FinanceiroDashboard() {
className="input-field"
rows={3}
value={modalPagamento.observacoes}
onChange={(e) =>
setModalPagamento({
...modalPagamento,
observacoes: e.target.value,
})
}
onChange={e => setModalPagamento({...modalPagamento, observacoes:e.target.value})}
/>
</div>
<div className="modal-footer">
<button
className="btn-view"
onClick={() => handleSave(modalPagamento)}
>
<i className="bi bi-check-circle me-1"></i> Salvar
</button>
<button className="btn-delete" onClick={closeModal}>
<i className="bi bi-x-circle me-1"></i> Cancelar
</button>
</div>
<div className="modal-footer">
<button className="action-btn" onClick={() => handleSave(modalPagamento)}>
Salvar
</button>
<button
className="action-btn"
onClick={closeModal}
style={{ borderColor: '#d1d5db', color: '#4b5563' }}
>
Cancelar
</button>
</div>
</div>
</div>

View File

@ -1,8 +1,6 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { FaUser, FaUserPlus, FaCalendarAlt, FaCalendarCheck } from 'react-icons/fa';
import { useAuth } from '../components/utils/AuthProvider';
import API_KEY from '../components/utils/apiKeys';
import './style/Inicio.css';
import { Link } from 'react-router-dom';
@ -10,142 +8,19 @@ import { Link } from 'react-router-dom';
function Inicio() {
const navigate = useNavigate();
const { getAuthorizationHeader, isAuthenticated } = useAuth();
const [pacientes, setPacientes] = useState([]);
const [medicos, setMedicos] = useState([]);
const [agendamentos, setAgendamentos] = useState([]);
const [agendamentosComPacientes, setAgendamentosComPacientes] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchPacientes = async () => {
try {
const authHeader = getAuthorizationHeader();
const myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions);
if (response.ok) {
const data = await response.json();
setPacientes(data);
console.log('Pacientes carregados:', data.length);
} else {
console.error(' Erro ao buscar pacientes:', response.status);
}
} catch (error) {
console.error(' Erro ao buscar pacientes:', error);
}
};
const fetchMedicos = async () => {
try {
const authHeader = getAuthorizationHeader();
const myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions);
if (response.ok) {
const data = await response.json();
setMedicos(data);
console.log(' Médicos carregados:', data.length);
} else {
console.error('Erro ao buscar médicos:', response.status);
}
} catch (error) {
console.error(' Erro ao buscar médicos:', error);
}
};
const fetchAgendamentos = async () => {
try {
const authHeader = getAuthorizationHeader();
const myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions);
if (response.ok) {
const data = await response.json();
setAgendamentos(data);
console.log(' Agendamentos carregados:', data.length);
} else {
console.error(' Erro ao buscar agendamentos:', response.status);
}
} catch (error) {
console.error(' Erro ao buscar agendamentos:', error);
} finally {
setLoading(false);
}
};
if (isAuthenticated) {
fetchPacientes();
fetchMedicos();
fetchAgendamentos();
}
}, [isAuthenticated, getAuthorizationHeader]);
useEffect(() => {
if (agendamentos.length > 0 && pacientes.length > 0 && medicos.length > 0) {
const agendamentosComNomes = agendamentos.map(agendamento => {
const paciente = pacientes.find(p => p.id === agendamento.patient_id);
const medico = medicos.find(m => m.id === agendamento.doctor_id);
return {
...agendamento,
nomePaciente: paciente?.full_name || 'Paciente não encontrado',
nomeMedico: medico?.full_name || 'Médico não encontrado',
especialidadeMedico: medico?.specialty || ''
};
});
setAgendamentosComPacientes(agendamentosComNomes);
}
}, [agendamentos, pacientes, medicos]);
const totalPacientes = pacientes.length;
const novosEsseMes = pacientes.filter(p => p.created_at && new Date(p.created_at).getMonth() === new Date().getMonth()).length;
const novosEsseMes = pacientes.filter(p => p.createdAt && new Date(p.createdAt).getMonth() === new Date().getMonth()).length;
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
const agendamentosDoDia = agendamentosComPacientes.filter(a => {
if (!a.scheduled_at) return false;
const dataAgendamento = new Date(a.scheduled_at);
dataAgendamento.setHours(0, 0, 0, 0);
return dataAgendamento.getTime() === hoje.getTime();
});
const agendamentosDoDia = agendamentos.filter(
a => a.data && new Date(a.data).getDate() === hoje.getDate()
);
const agendamentosHoje = agendamentosDoDia.length;
const pendencias = agendamentos.filter(a => a.status === 'pending' || a.status === 'scheduled').length;
return (
<div className="dashboard-container">
<div className="dashboard-header">
@ -182,7 +57,7 @@ function Inicio() {
<div className="stat-card">
<div className="stat-info">
<span className="stat-label">PENDÊNCIAS</span>
<span className="stat-value">{loading ? '...' : pendencias}</span>
<span className="stat-value">0</span>
</div>
<div className="stat-icon-wrapper orange"><FaCalendarAlt className="stat-icon" /></div>
</div>
@ -217,54 +92,14 @@ function Inicio() {
<div className="appointments-section">
<h2>Próximos Agendamentos</h2>
{loading ? (
<div className="no-appointments-content">
<p>Carregando agendamentos...</p>
</div>
) : agendamentosHoje > 0 ? (
<div className="agendamentos-list">
{agendamentosDoDia.slice(0, 5).map(agendamento => (
{agendamentosHoje > 0 ? (
<div>
{agendamentosDoDia.map(agendamento => (
<div key={agendamento.id} className="agendamento-item">
<div className="agendamento-info">
<div className="agendamento-time-date">
<p className="agendamento-hora">
{new Date(agendamento.scheduled_at).toLocaleTimeString('pt-BR', {
hour: '2-digit',
minute: '2-digit'
})}
</p>
<p className="agendamento-data">
{new Date(agendamento.scheduled_at).toLocaleDateString('pt-BR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
})}
</p>
</div>
<div className="agendamento-detalhes">
<p className="agendamento-paciente">
<strong>Paciente:</strong> {agendamento.nomePaciente}
</p>
<p className="agendamento-medico">
<strong>Dr(a):</strong> {agendamento.nomeMedico}
{agendamento.especialidadeMedico && ` - ${agendamento.especialidadeMedico}`}
</p>
</div>
<span className={`agendamento-status status-${agendamento.status}`}>
{agendamento.status === 'scheduled' ? 'Agendado' :
agendamento.status === 'completed' ? 'Concluído' :
agendamento.status === 'pending' ? 'Pendente' :
agendamento.status === 'cancelled' ? 'Cancelado' :
agendamento.status === 'requested' ? '' : agendamento.status}
</span>
</div>
<p>{agendamento.nomePaciente}</p>
<p>{new Date(agendamento.data).toLocaleTimeString()}</p>
</div>
))}
{agendamentosHoje > 5 && (
<button className="view-all-button" onClick={() => navigate('/secretaria/agendamento')}>
Ver todos os {agendamentosHoje} agendamentos
</button>
)}
</div>
) : (
<div className="no-appointments-content">

View File

@ -1,511 +1,337 @@
// src/pages/LaudoManager.jsx
import API_KEY from '../components/utils/apiKeys';
import { Link } from 'react-router-dom';
import React, { useState, useEffect } from 'react';
import { useAuth } from '../components/utils/AuthProvider';
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js';
import TiptapViewer from '../PagesMedico/TiptapViewer'
import '../PagesMedico/styleMedico/DoctorRelatorioManager.css';
import React, { useState, useEffect } from "react";
import "./LaudoStyle.css"; // Importa o CSS externo
const LaudoManager = () => {
const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth();
const authHeader = getAuthorizationHeader();
/* ===== Mock data (simula APIDOG) ===== */
function mockFetchLaudos() {
return [
{
id: "LAU-300551296",
pedido: 300551296,
data: "29/07/2025",
paciente: { nome: "Sarah Mariana Oliveira", cpf: "616.869.070-**", nascimento: "1990-03-25", convenio: "Unimed" },
solicitante: "Sandro Rangel Santos",
exame: "US - Abdome Total",
conteudo: "RELATÓRIO MÉDICO\n\nAchados: Imagens compatíveis com ...\nConclusão: Órgãos sem alterações significativas.",
status: "rascunho"
},
{
id: "LAU-300659170",
pedido: 300659170,
data: "29/07/2025",
paciente: { nome: "Laissa Helena Marquetti", cpf: "950.684.57-**", nascimento: "1986-09-12", convenio: "Bradesco" },
solicitante: "Sandro Rangel Santos",
exame: "US - Mamária Bilateral",
conteudo: "RELATÓRIO MÉDICO\n\nAchados: text...",
status: "liberado"
},
{
id: "LAU-300658301",
pedido: 300658301,
data: "28/07/2025",
paciente: { nome: "Vera Lúcia Oliveira Santos", cpf: "928.005.**", nascimento: "1979-02-02", convenio: "Particular" },
solicitante: "Dr. Fulano",
exame: "US - Transvaginal",
conteudo: "RELATÓRIO MÉDICO\n\nAchados: ...",
status: "entregue"
}
];
}
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [showModal, setShowModal] = useState(false);
const [relatorioModal, setRelatorioModal] = useState(null);
const [termoPesquisa, setTermoPesquisa] = useState('');
const [filtroExame, setFiltroExame] = useState('');
const [modalIndex, setModalIndex] = useState(0);
function mockDeleteLaudo(id) {
return new Promise((res) => setTimeout(() => res({ ok: true }), 500));
}
const [showProtocolModal, setShowProtocolModal] = useState(false);
const [protocolForIndex, setProtocolForIndex] = useState(null);
/* ===== Componente ===== */
export default function LaudoManager() {
const [laudos, setLaudos] = useState([]);
const [openDropdownId, setOpenDropdownId] = useState(null);
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
/* viewerLaudo é usado para mostrar o editor/leitura;
previewLaudo é usado para a pré-visualização (sem bloquear) */
const [viewerLaudo, setViewerLaudo] = useState(null);
const [previewLaudo, setPreviewLaudo] = useState(null);
const [showPreview, setShowPreview] = useState(false);
const [noPermissionText, setNoPermissionText] = useState(null);
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
const [toDelete, setToDelete] = useState(null);
const [loadingDelete, setLoadingDelete] = useState(false);
const isSecretary = true;
/* notificação simples (sem backdrop) para 'sem permissão' */
const [showNoPermission, setShowNoPermission] = useState(false);
const totalPaginas = Math.max(1, Math.ceil(relatoriosFinais.length / itensPorPagina));
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal);
/* pesquisa */
const [query, setQuery] = useState("");
/* Para simplificar: eu assumo aqui que estamos na visão da secretaria */
const isSecretary = true; // permanece true (somente leitura)
useEffect(() => {
let mounted = true;
const fetchReports = async () => {
try {
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader);
const 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();
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);
if (mounted) {
setRelatoriosOriginais([]);
setRelatoriosFiltrados([]);
setRelatoriosFinais([]);
}
}
};
fetchReports();
const refreshHandler = () => fetchReports();
window.addEventListener('reports:refresh', refreshHandler);
return () => {
mounted = false;
window.removeEventListener('reports:refresh', refreshHandler);
};
}, [authHeader]);
// Importa os dados mock apenas
const data = mockFetchLaudos();
setLaudos(data);
}, []);
// Fecha dropdown ao clicar fora
useEffect(() => {
const fetchRelData = async () => {
const pacientes = [];
const medicos = [];
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);
function onDocClick(e) {
if (e.target.closest && e.target.closest('.action-btn')) return;
if (e.target.closest && e.target.closest('.dropdown')) return;
setOpenDropdownId(null);
}
document.addEventListener('click', onDocClick);
return () => document.removeEventListener('click', onDocClick);
}, []);
function toggleDropdown(id, e) {
e.stopPropagation();
setOpenDropdownId(prev => (prev === id ? null : id));
}
/* (botao editar) */
function handleOpenViewer(laudo) {
setOpenDropdownId(null);
if (isSecretary) {
// (notificação sem bloquear)
setShowNoPermission(true);
return;
}
setViewerLaudo(laudo);
}
/* (botao imprimir) */
function handlePrint(laudo) {
// evitar bug: fechar viewer antes de abrir preview
setViewerLaudo(null);
setPreviewLaudo(laudo);
setShowPreview(true);
setOpenDropdownId(null);
}
/* (botao excluir) */
function handleRequestDelete(laudo) {
setToDelete(laudo);
setOpenDropdownId(null);
setShowConfirmDelete(true);
}
/* (funcionalidade do botao de excluir) */
async function doConfirmDelete(confirm) {
if (!toDelete) return;
if (!confirm) {
setShowConfirmDelete(false);
setToDelete(null);
return;
}
setLoadingDelete(true);
try {
const doctorId = rel.created_by || rel.requested_by || null;
if (doctorId) {
const docRes = await GetDoctorByID(doctorId, authHeader);
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
const resp = await mockDeleteLaudo(toDelete.id);
if (resp.ok || resp === true) {
// removo o laudo da lista local
setLaudos(curr => curr.filter(l => l.id !== toDelete.id));
setShowConfirmDelete(false);
setToDelete(null);
alert("Laudo excluído com sucesso.");
} else {
medicos.push({ full_name: rel.requested_by || '' });
alert("Erro ao excluir. Tente novamente.");
}
} catch (err) {
medicos.push({ full_name: rel.requested_by || '' });
alert("Erro de rede ao excluir.");
} finally {
setLoadingDelete(false);
}
}
setPacientesComRelatorios(pacientes);
setMedicosComRelatorios(medicos);
};
if (relatoriosFiltrados.length > 0) fetchRelData();
else {
setPacientesComRelatorios([]);
setMedicosComRelatorios([]);
}
}, [relatoriosFiltrados, authHeader]);
const abrirModal = (relatorio, index) => {
setRelatorioModal(relatorio);
setModalIndex(index);
setShowModal(true);
};
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 handleEditClick = (relatorio) => {
if (isSecretary) {
setNoPermissionText('Sem permissão para editar/criar laudo.');
return;
}
navigate(`/medico/relatorios/${relatorio.id}/edit`);
};
const handleOpenProtocol = (relatorio, index) => {
setProtocolForIndex({ relatorio, index });
setShowProtocolModal(true);
};
const handleLiberarLaudo = async (relatorio) => {
if (isSecretary) {
setNoPermissionText('Ainda não implementado');
return;
}
try {
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader);
myHeaders.append('Content-Type', 'application/json');
myHeaders.append('Prefer', 'return=representation');
const body = JSON.stringify({ status: 'liberado' });
const res = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${relatorio.id}`, {
method: 'PATCH',
headers: myHeaders,
body
/* filtro de pesquisa (por pedido ou nome do paciente) */
const normalized = (s = "") => String(s).toLowerCase();
const filteredLaudos = laudos.filter(l => {
const q = normalized(query).trim();
if (!q) return true;
if (normalized(l.pedido).includes(q)) return true;
if (normalized(l.paciente?.nome).includes(q)) return true;
return false;
});
if (!res.ok) {
const txt = await res.text().catch(()=> '');
throw new Error('Erro ao liberar laudo: ' + res.status + ' ' + txt);
}
const refreshed = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", {
method: 'GET',
headers: (() => { const h=new Headers(); h.append('apikey', API_KEY); if(authHeader) h.append('Authorization', authHeader); return h; })(),
});
const data = await refreshed.json();
setRelatoriosOriginais(Array.isArray(data)? data : []);
setRelatoriosFiltrados(Array.isArray(data)? data : []);
setRelatoriosFinais(Array.isArray(data)? data : []);
alert('Laudo liberado com sucesso.');
} catch (err) {
console.error(err);
alert('Erro ao liberar laudo. Veja console.');
}
};
useEffect(() => {
const q = (termoPesquisa || '').toLowerCase().trim();
const ex = (filtroExame || '').toLowerCase().trim();
let items = relatoriosOriginais || [];
if (q) {
items = items.filter(r => {
const patientName = (r.patient_name || r.patient_fullname || '').toString().toLowerCase();
const pedido = (r.id || r.request_id || r.request || '').toString().toLowerCase();
return patientName.includes(q) || pedido.includes(q) || (r.patient_id && r.patient_id.toString().includes(q));
});
}
if (ex) items = items.filter(r => (r.exam || r.exame || '').toLowerCase().includes(ex));
setRelatoriosFiltrados(items);
setRelatoriosFinais(items);
setPaginaAtual(1);
}, [termoPesquisa, filtroExame, relatoriosOriginais]);
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 className="laudo-wrap">
<div className="left-col">
<div className="title-row">
<div>
<div className="page-heading"><h3>Lista de Relatórios</h3></div>
<div className="page-content">
<section className="row">
<div className="col-12">
<div className="card">
<div className="card-header d-flex justify-content-between align-items-center">
<h4 className="card-title mb-0">Relatórios Cadastrados</h4>
<div>
<button
className="btn btn-primary"
onClick={() => setNoPermissionText('Sem permissão para editar/criar laudo.')}
title="Secretaria não pode criar relatórios"
>
<i className="bi bi-plus-circle"></i> Adicionar Relatório
</button>
<div className="page-title">Gerenciamento de Laudo</div>
{/* removi a linha "Visualização: Secretaria" conforme pedido */}
</div>
</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="row">
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Buscar por nome ou CPF do paciente</label>
<div style={{ marginBottom:12 }}>
<input
type="text"
className="form-control"
placeholder="Digite nome ou CPF do paciente..."
value={termoPesquisa}
onChange={(e) => setTermoPesquisa(e.target.value)}
placeholder="Pesquisar paciente ou pedido..."
value={query}
onChange={e => setQuery(e.target.value)}
style={{ width:"100%", padding:12, borderRadius:8, border:"1px solid #e6eef8" }}
/>
</div>
</div>
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Filtrar por tipo de exame</label>
<input
type="text"
className="form-control"
placeholder="Digite o tipo de exame..."
value={filtroExame}
onChange={(e) => setFiltroExame(e.target.value)}
/>
</div>
</div>
<div className="col-md-2 d-flex align-items-end">
<button className="btn btn-outline-secondary w-100" onClick={limparFiltros}>
<i className="bi bi-arrow-clockwise"></i> Limpar
</button>
</div>
</div>
<div className="mt-2">
<div className="contador-relatorios">
{relatoriosFinais.length} DE {relatoriosOriginais.length} RELATÓRIOS ENCONTRADOS
</div>
</div>
</div>
<div className="table-responsive">
<table className="table table-striped table-hover">
<thead>
<tr>
<th>Paciente</th>
<th>CPF</th>
<th>Exame</th>
<th></th>
</tr>
</thead>
<tbody>
{relatoriosPaginados.length > 0 ? (
relatoriosPaginados.map((relatorio, index) => {
const paciente = pacientesComRelatorios[index] || {};
return (
<tr key={relatorio.id || index}>
<td>{paciente?.full_name || relatorio.patient_name || 'Carregando...'}</td>
<td>{paciente?.cpf || 'Carregando...'}</td>
<td>{relatorio.exam || relatorio.exame || '—'}</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={() => handleEditClick(relatorio)}>
<i className="bi bi-pencil me-1"></i> Editar
</button>
<button className="btn btn-sm btn-protocolo" onClick={() => handleOpenProtocol(relatorio, index)}>
<i className="bi bi-send me-1"></i> Protocolo
</button>
<button className="btn btn-sm btn-liberar" onClick={() => handleLiberarLaudo(relatorio)}>
<i className="bi bi-unlock me-1"></i> Liberar laudo
</button>
</div>
</td>
</tr>
);
})
{filteredLaudos.length === 0 ? (
<div className="empty">Nenhum laudo encontrado.</div>
) : (
<tr><td colSpan="4" className="text-center">Nenhum relatório encontrado.</td></tr>
)}
</tbody>
</table>
<div style={{ borderRadius:8, overflow:"visible", boxShadow:"0 0 0 1px #eef6ff" }}>
{filteredLaudos.map((l) => (
<div className="laudo-row" key={l.id}>
<div className="col" style={{ flex: "0 0 160px" }}>
<div style={{ fontWeight:700 }}>{l.pedido}</div>
<div className="small-muted">{l.data}</div>
</div>
<div className="col" style={{ flex:2 }}>
<div style={{ fontWeight:600 }}>{l.paciente.nome}</div>
<div className="small-muted">{l.paciente.cpf} {l.paciente.convenio}</div>
</div>
<div className="col" style={{ flex:1 }}>{l.exame}</div>
<div className="col small">{l.solicitante}</div>
<div className="col small" style={{ flex: "0 0 80px", textAlign:"left" }}>{l.status}</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 className="row-actions">
<div className="action-btn" onClick={(e)=> toggleDropdown(l.id, e)} title="Ações">
<i class="bi bi-three-dots-vertical"></i>
</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>
{openDropdownId === l.id && (
<div className="dropdown" data-laudo-dropdown={l.id}>
<div className="item" onClick={() => handleOpenViewer(l)}>Editar</div>
<div className="item" onClick={() => handlePrint(l)}>Imprimir</div>
<div className="item" onClick={() => { alert("Protocolo de entrega: formulário (não implementado)."); setOpenDropdownId(null); }}>Protocolo de entrega</div>
<div className="item" onClick={() => { alert("Liberar laudo: requer permissão de médico. (não implementado)"); setOpenDropdownId(null); }}>Liberar laudo</div>
<div className="item" onClick={() => handleRequestDelete(l)} style={{ color:"#c23b3b" }}>Excluir laudo</div>
</div>
)}
</div>
</div>
))}
<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>
</section>
</div>
{showModal && relatorioModal && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
<h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name || relatorioModal.patient_name || 'Paciente'}</h5>
</div>
<div className="modal-body">
<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' style={{ padding: '0 6px' }}>
<p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name || relatorioModal.patient_name || '—'}</p>
<p><strong>Data de nascimento:</strong> {pacientesComRelatorios[modalIndex]?.birth_date || '—'}</p>
<p><strong>Data do exame:</strong> {relatorioModal?.due_at || relatorioModal?.date || '—'}</p>
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
<div className="tiptap-viewer-wrapper">
<TiptapViewer htmlContent={relatorioModal?.content_html || relatorioModal?.content || relatorioModal?.diagnosis || 'Relatório não preenchido.'} />
{/* Viewer modal (modo leitura) — só abre para quem tem permissão */}
{viewerLaudo && !showPreview && !isSecretary && (
<div className="viewer-modal" style={{ pointerEvents:"auto" }}>
<div className="modal-backdrop" onClick={() => setViewerLaudo(null)} />
<div className="modal-card" role="dialog" aria-modal="true">
<div className="viewer-header">
<div>
<div style={{ fontSize:18, fontWeight:700 }}>{viewerLaudo.paciente.nome}</div>
<div className="patient-info">
Nasc.: {viewerLaudo.paciente.nascimento} {computeAge(viewerLaudo.paciente.nascimento)} anos {viewerLaudo.paciente.cpf} {viewerLaudo.paciente.convenio}
</div>
</div>
<div style={{ marginTop: 20, padding: '0 6px' }}>
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatorioModal?.requested_by || '—'}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatorioModal?.created_at || '—'}</p>
</div>
<div style={{ display:"flex", gap:8 }}>
<button className="tool-btn" onClick={() => { setPreviewLaudo(viewerLaudo); setShowPreview(true); setViewerLaudo(null); }}>Pré-visualizar / Imprimir</button>
<button className="tool-btn" onClick={() => setViewerLaudo(null)}>Fechar</button>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name || 'paciente', modalIndex)}>
<i className='bi bi-file-pdf-fill me-1'></i> Baixar em PDF
</button>
<button type="button" className="btn btn-secondary" onClick={() => { setShowModal(false) }}>
Fechar
</button>
<div className="toolbar">
<div className="tool-btn">B</div>
<div className="tool-btn"><i>I</i></div>
<div className="tool-btn"><u>U</u></div>
<div className="tool-btn">Fonte</div>
<div className="tool-btn">Tamanho</div>
<div className="tool-btn">Lista</div>
<div className="tool-btn">Campos</div>
<div className="tool-btn">Modelos</div>
<div className="tool-btn">Imagens</div>
</div>
<div className="editor-area" aria-readonly>
{viewerLaudo.conteudo.split("\n").map((line, i) => (
<p key={i} style={{ margin: line.trim()==="" ? "8px 0" : "6px 0" }}>{line}</p>
))}
</div>
<div className="footer-controls">
<div className="toggle small-muted">
<label><input type="checkbox" disabled /> Pré-visualização</label>
<label style={{ marginLeft:12 }}><input type="checkbox" disabled /> Ocultar data</label>
<label style={{ marginLeft:12 }}><input type="checkbox" disabled /> Ocultar assinatura</label>
</div>
<div style={{ display:"flex", gap:8 }}>
<button className="btn secondary" onClick={() => { if(window.confirm("Cancelar e voltar à lista? Todas alterações não salvas serão perdidas.")) setViewerLaudo(null); }}>Cancelar</button>
<button className="btn primary" onClick={() => alert("Salvar (não implementado para secretaria).")}>Salvar laudo</button>
</div>
</div>
</div>
</div>
)}
{showProtocolModal && protocolForIndex && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
<h5 className="modal-title">Protocolo de Entrega - {protocolForIndex.relatorio?.patient_name || 'Paciente'}</h5>
</div>
<div className="modal-body">
<div style={{ padding: '0 6px' }}>
<p><strong>Pedido:</strong> {protocolForIndex.relatorio?.id || protocolForIndex.relatorio?.pedido}</p>
<p><strong>Paciente:</strong> {protocolForIndex.relatorio?.patient_name || '—'}</p>
<p><strong>Data:</strong> {protocolForIndex.relatorio?.due_at || protocolForIndex.relatorio?.date || '—'}</p>
<hr />
<p>Protocolo de entrega gerado automaticamente. (Substitua pelo endpoint real se houver)</p>
{/* Preview modal — agora não bloqueia a tela (sem backdrop escuro), botão imprimir é interativo */}
{showPreview && previewLaudo && (
<div className="preview-modal" style={{ pointerEvents:"none" /* container não bloqueia */ }}>
<div /* sem backdrop, assim não deixa a tela escura/blocked */ />
<div className="modal-card" style={{ maxWidth:900, pointerEvents:"auto" }}>
<div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12 }}>
<div style={{ fontWeight:700 }}>Pré-visualização - {previewLaudo.paciente.nome}</div>
<div style={{ display:"flex", gap:8 }}>
<button className="tool-btn" onClick={() => alert("Imprimir (simulado).")}>Imprimir / Download</button>
<button className="tool-btn" onClick={() => { setShowPreview(false); setPreviewLaudo(null); }}>Fechar</button>
</div>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => {
const idx = protocolForIndex.index ?? 0;
BaixarPDFdoRelatorio(protocolForIndex.relatorio?.patient_name || 'paciente', idx);
}}>
<i className='bi bi-file-earmark-pdf-fill me-1'></i> Baixar Protocolo (PDF)
</button>
<button type="button" className="btn btn-secondary" onClick={() => setShowProtocolModal(false)}>
Fechar
</button>
<div style={{ border: "1px solid #e6eef8", borderRadius:6, padding:18, background:"#fff" }}>
<div style={{ marginBottom:8, fontSize:14, color:"#33475b" }}>
<strong>RELATÓRIO MÉDICO</strong>
</div>
<div style={{ marginBottom:14, fontSize:13, color:"#546b7f" }}>
{previewLaudo.paciente.nome} Nasc.: {previewLaudo.paciente.nascimento} CPF: {previewLaudo.paciente.cpf}
</div>
<div style={{ whiteSpace:"pre-wrap", fontSize:15, color:"#1f2d3d", lineHeight:1.5 }}>
{previewLaudo.conteudo}
</div>
</div>
</div>
</div>
)}
{noPermissionText && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
<h5 className="modal-title">Aviso</h5>
</div>
<div className="modal-body">
<p>{noPermissionText}</p>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => setNoPermissionText(null)}>Fechar</button>
</div>
</div>
{/* Notificação simples: Sem permissão (exibe sem backdrop escuro) - centralizada */}
{showNoPermission && (
<div className="notice-card" role="alert" aria-live="polite">
<div style={{ fontWeight:700, marginBottom:6 }}>Sem permissão para editar</div>
<div style={{ marginBottom:10, color:"#5a6f80" }}>Você está na visualização da secretaria. Edição disponível somente para médicos autorizados.</div>
<div style={{ textAlign:"right" }}>
<button className="tool-btn" onClick={() => setShowNoPermission(false)}>Fechar</button>
</div>
</div>
)}
{/* Confirm delete modal (simples: Sim / Não) */}
{showConfirmDelete && toDelete && (
<div className="confirm-modal" style={{ pointerEvents:"auto" }}>
<div className="modal-card" style={{ maxWidth:480 }}>
<div style={{ fontWeight:700, marginBottom:8 }}>Confirmar exclusão</div>
<div style={{ marginBottom:12 }}>Você tem certeza que quer excluir o laudo <strong>{toDelete.pedido} - {toDelete.paciente.nome}</strong> ? Esta ação é irreversível.</div>
<div style={{ display:"flex", justifyContent:"flex-end", gap:8 }}>
<button className="tool-btn" onClick={() => doConfirmDelete(false)} disabled={loadingDelete}>Não</button>
<button className="tool-btn" onClick={() => doConfirmDelete(true)} disabled={loadingDelete} style={{ background: loadingDelete ? "#d7e8ff" : "#ffecec", border: "1px solid #ffd7d7" }}>
{loadingDelete ? "Excluindo..." : "Sim, excluir"}
</button>
</div>
</div>
</div>
)}
</div>
);
};
}
export default LaudoManager;
/* ===== Helpers ===== */
function computeAge(birth) {
if (!birth) return "-";
const [y,m,d] = birth.split("-").map(x => parseInt(x,10));
if (!y) return "-";
const today = new Date();
let age = today.getFullYear() - y;
const mm = today.getMonth() + 1;
const dd = today.getDate();
if (mm < m || (mm === m && dd < d)) age--;
return age;
}

View File

@ -310,38 +310,3 @@ html[data-bs-theme="dark"] .notice-card {
color: #e0e0e0 !important;
box-shadow: 0 8px 30px rgba(10,20,40,0.32) !important;
}
/* Botões coloridos para Protocolo e Liberar (combina com estilo dos outros botões) */
.btn-protocolo {
background-color: #E6F2FF;
color: #004085;
border: 1px solid #d6e9ff;
padding: 8px 12px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
.btn-protocolo:hover {
background-color: #cce5ff;
}
/* Liberar laudo - estilo parecido com o botão editar (amarelo claro) */
.btn-liberar {
background-color: #FFF3CD;
color: #856404;
border: 1px solid #ffeaa7;
padding: 8px 12px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
}
.btn-liberar:hover {
background-color: #ffeaa7;
}
/* Ajuste visual (pequeno) para espaçamento horizontal dos botões da linha */
.table-responsive .d-flex.gap-2 .btn {
display: inline-flex;
align-items: center;
gap: 6px;
}

View File

@ -1,13 +1,11 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, use } 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: "",
@ -118,7 +116,6 @@ function Login({ onEnterSystem }) {
if (data.access_token) {
const UserData = await UserInfos(`bearer ${data.access_token}`);
console.log(UserData, "Dados do usuário");
localStorage.setItem("roleUser", UserData.roles)
if (UserData?.roles?.includes("admin")) {
navigate(`/admin/`);
@ -128,12 +125,7 @@ function Login({ onEnterSystem }) {
navigate(`/medico/`);
} else if (UserData?.roles?.includes("financeiro")) {
navigate(`/financeiro/`);
} else if (UserData?.roles?.includes("paciente")) {
navigate(`/paciente/`);
}
}else{
console.log("Erro na tentativa de login")
setShowCabecalho(true)
}
} else {
setAlert("Preencha todos os campos!");
@ -156,7 +148,11 @@ function Login({ onEnterSystem }) {
<p className="auth-subtitle mb-5">
Entre com os dados que você inseriu durante o registro.
</p>
<CabecalhoError showCabecalho={showCabecalho} message={"E-mail ou senha incorretos."}/>
{alert && (
<div className="alert alert-info" role="alert">
{alert}
</div>
)}
<form onSubmit={handleLogin}>
<div className="form-group position-relative has-icon-left mb-4">
<input

View File

@ -1,139 +0,0 @@
import React, { useState, useMemo, useEffect } from 'react';
import dayjs from 'dayjs';
import CalendarComponent from '../components/AgendarConsulta/CalendarComponent.jsx';
import { useAuth } from '../components/utils/AuthProvider.js';
import TabelaAgendamentoDia from '../components/AgendarConsulta/TabelaAgendamentoDia';
dayjs.locale('pt-br');
const MedicoAgendamento = () => {
const { getAuthorizationHeader, user } = useAuth();
const [currentDate, setCurrentDate] = useState(dayjs());
const [selectedDay, setSelectedDay] = useState(dayjs());
const [DictAgendamentosOrganizados, setAgendamentosOrganizados] = useState({});
const [showSpinner, setShowSpinner] = useState(true);
const [modoVisualizacao, setModoVisualizacao] = useState('Dia');
const [quickJump, setQuickJump] = useState({
month: currentDate.month(),
year: currentDate.year()
});
const handleQuickJumpChange = (type, value) => {
setQuickJump(prev => ({ ...prev, [type]: Number(value) }));
};
const applyQuickJump = () => {
let newDate = dayjs().year(quickJump.year).month(quickJump.month).date(1);
setCurrentDate(newDate);
setSelectedDay(newDate);
};
const [selectedID, setSelectedId] = useState('0');
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false); t
useEffect(() => {
const mockAgendamentos = {
[dayjs().format('YYYY-MM-DD')]: [
{ id: 1, scheduled_at: dayjs().set('hour', 10).set('minute', 0).toISOString(), paciente_nome: "Paciente Teste 1", medico_nome: "Dr. Mock", status: "agendado" },
{ id: 2, scheduled_at: dayjs().set('hour', 11).set('minute', 30).toISOString(), paciente_nome: "Paciente Teste 2", medico_nome: "Dr. Mock", status: "confirmed" },
],
'2025-10-27': [
{ id: 3, scheduled_at: '2025-10-27T19:30:00Z', paciente_nome: 'Davi Andrade', medico_nome: 'Dr. João', status: 'agendado' },
{ id: 4, scheduled_at: '2025-10-27T20:00:00Z', paciente_nome: 'Davi Andrade', medico_nome: 'Dr. João', status: 'agendado' },
{ id: 5, scheduled_at: '2025-10-27T21:30:00Z', paciente_nome: 'Davi Andrade', medico_nome: 'Dr. João', status: 'agendado' },
]
};
const today = dayjs();
const startOfMonth = today.startOf('month');
const nov11 = startOfMonth.add(10, 'day').format('YYYY-MM-DD');
mockAgendamentos[nov11] = [
{ id: 6, scheduled_at: `${nov11}T10:30:00Z`, paciente_nome: 'Paciente C', medico_nome: 'Isaac Kauã', status: 'agendado' },
{ id: 7, scheduled_at: `${nov11}T11:00:00Z`, paciente_nome: 'João Gustavo', medico_nome: 'João Gustavo', status: 'agendado' },
{ id: 8, scheduled_at: `${nov11}T12:30:00Z`, paciente_nome: 'João Gustavo', medico_nome: 'João Gustavo', status: 'agendado' },
{ id: 9, scheduled_at: `${nov11}T15:00:00Z`, paciente_nome: 'Pedro Abravanel', medico_nome: 'Fernando Prichowski', status: 'agendado' },
];
setAgendamentosOrganizados(mockAgendamentos);
setShowSpinner(false);
}, []);
const handleSelectSlot = (timeSlot, doctorId) => {
alert(`Abrir tela de Nova Consulta para o dia ${selectedDay.format('DD/MM/YYYY')} às ${timeSlot} com o Médico ID: ${doctorId}`);
};
const isMedico = true;
const medicoLogadoID = user?.doctor_id || "ID_MEDICO_DEFAULT";
return (
<div className='agendamento-medico-container'>
<h1>Agenda do Médico: {user?.full_name || "Nome do Médico"}</h1>
<div className="btns-gerenciamento-e-consulta" style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
<button className='manage-button btn' onClick={() => {}}><i className="bi bi-gear-fill me-1"></i> Mudar Disponibilidade</button>
<button className="btn btn-primary" onClick={() => {}}><i className="bi bi-person-plus-fill"></i> Adicionar Paciente</button>
</div>
<div className="tab-buttons" style={{ marginBottom: '20px' }}>
<button className={`btn ${modoVisualizacao === 'Dia' ? 'btn-primary' : 'btn-outline-primary'}`} onClick={() => setModoVisualizacao('Dia')}>Dia</button>
<button className={`btn ${modoVisualizacao === 'Semana' ? 'btn-primary' : 'btn-outline-primary'}`} onClick={() => setModoVisualizacao('Semana')}>Semana</button>
<button className={`btn ${modoVisualizacao === 'Mês' ? 'btn-primary' : 'btn-outline-primary'}`} onClick={() => setModoVisualizacao('Mês')}>Mês</button>
</div>
<div className='agenda-e-calendario-wrapper'>
{}
<div className='medico-calendar-column' style={{ flex: 1 }}>
<CalendarComponent
currentDate={currentDate}
setCurrentDate={setCurrentDate}
selectedDay={selectedDay}
setSelectedDay={setSelectedDay}
DictAgendamentosOrganizados={DictAgendamentosOrganizados}
showSpinner={showSpinner}
T
setSelectedId={setSelectedId}
setShowDeleteModal={setShowDeleteModal}
setShowConfirmModal={setShowConfirmModal}
quickJump={quickJump}
handleQuickJumpChange={handleQuickJumpChange}
applyQuickJump={applyQuickJump}
/>
</div>
{}
<div className='medico-schedule-column' style={{ flex: 2 }}>
{modoVisualizacao === 'Dia' && (
<TabelaAgendamentoDia
selectedDay={selectedDay}
agendamentosDoDia={DictAgendamentosOrganizados[selectedDay.format('YYYY-MM-DD')] || []}
onSelectSlot={handleSelectSlot}
isMedicoView={isMedico}
medicoID={medicoLogadoID}
horariosDeTrabalho={[
{ hora: '19:30', medicoId: '123' },
{ hora: '20:00', medicoId: '123' },
{ hora: '21:30', medicoId: '123' }
]}
/>
)}
{}
{}
{}
</div>
</div>
</div>
);
};
export default MedicoAgendamento;

View File

@ -1,82 +1,34 @@
import React, { useState, useEffect, useCallback } from "react";
// src/pages/ProfilePage.jsx
import React, { useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import "./style/ProfilePage.css";
const ROLES = {
ADMIN: "Administrador",
SECRETARY: "Secretária",
DOCTOR: "Médico",
FINANCIAL: "Financeiro"
const simulatedUserData = {
email: "admin@squad23.com",
role: "Administrador",
};
const ProfilePage = () => {
const location = useLocation();
const navigate = useNavigate();
const getRoleFromPath = useCallback(() => {
const getRoleFromPath = () => {
const path = location.pathname;
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]);
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";
};
const userRole = getRoleFromPath();
const userRole = simulatedUserData.role || getRoleFromPath();
const userEmail = simulatedUserData.email || "email.nao.encontrado@example.com";
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 [error, setError] = useState(null);
useEffect(() => {
const handleEscKey = (event) => {
if (event.keyCode === 27) handleClose();
};
document.addEventListener('keydown', handleEscKey);
return () => document.removeEventListener('keydown', handleEscKey);
}, []);
useEffect(() => {
const loadProfileData = () => {
const localAvatar = localStorage.getItem('user_avatar');
if (localAvatar) {
setAvatarUrl(localAvatar);
}
};
loadProfileData();
const handleStorageChange = () => {
loadProfileData();
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, []);
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 handleNameKeyDown = (e) => {
if (e.key === "Enter") setIsEditingName(false);
};
const handleClose = () => navigate(-1);
@ -95,92 +47,54 @@ const ProfilePage = () => {
<div className="profile-content">
<div className="profile-left">
<div className="avatar-wrapper">
<div className="avatar-square">
{avatarUrl ? (
<img
src={avatarUrl}
alt="Avatar do usuário"
className="avatar-img"
onError={() => {
setAvatarUrl(null);
localStorage.removeItem('user_avatar');
}}
/>
) : (
<div className="avatar-placeholder">
{userName.split(' ').map(n => n[0]).join('').toUpperCase()}
</div>
)}
</div>
<p style={{
textAlign: 'center',
marginTop: '10px',
fontSize: '0.85rem',
color: '#666'
}}>
Gerencie seu avatar no menu do perfil acima
</p>
<div className="avatar-square" />
<button
className="avatar-edit-btn"
title="Editar foto"
aria-label="Editar foto"
type="button"
>
</button>
</div>
</div>
<div className="profile-right">
<div className="profile-name-row">
{isEditingName ? (
<div className="name-edit-wrapper">
<input
className="profile-name-input"
value={userName}
onChange={(e) => setUserName(e.target.value)}
onBlur={handleNameSave}
onBlur={() => setIsEditingName(false)}
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>
{error && (
<div className="error-message">
{error}
</div>
)}
<div className="profile-info">
<p className="profile-email">
<span>Email:</span>
<strong>{userEmail}</strong>
Email: <strong>{userEmail}</strong>
</p>
<p className="profile-role">
<span>Cargo:</span>
<strong>{userRole}</strong>
Cargo: <strong>{userRole}</strong>
</p>
</div>
<div className="profile-actions">
<button
className="btn btn-close"
onClick={handleClose}
>
Fechar Perfil
<div className="profile-actions-row">
<button className="btn btn-close" onClick={handleClose}>
Fechar
</button>
</div>
</div>

View File

@ -1,132 +0,0 @@
import React, { useState, useEffect } from 'react';
import dayjs from 'dayjs';
import 'dayjs/locale/pt-br';
import { ChevronLeft, ChevronRight, Edit, Trash2, User, Stethoscope } from 'lucide-react';
// Configura o Day.js para usar o idioma português do Brasil
dayjs.locale('pt-br');
const TabelaAgendamentoDia = ({
agendamentos,
setDictInfo,
setShowDeleteModal,
setSelectedId,
setShowConfirmModal,
listaConsultasID,
coresConsultas
}) => {
const [currentDate, setCurrentDate] = useState(dayjs());
const [appointmentsForDay, setAppointmentsForDay] = useState([]);
useEffect(() => {
const formattedDate = currentDate.format('YYYY-MM-DD');
const dailyAppointments = agendamentos[formattedDate] || [];
const appointmentsComStatusAtualizado = dailyAppointments.map(app => {
const index = listaConsultasID.indexOf(app.id);
if (index > -1) {
return { ...app, status: coresConsultas[index] };
}
return app;
});
setAppointmentsForDay(appointmentsComStatusAtualizado);
}, [currentDate, agendamentos, listaConsultasID, coresConsultas]);
const handlePrevDay = () => {
setCurrentDate(currentDate.subtract(1, 'day'));
};
const handleNextDay = () => {
setCurrentDate(currentDate.add(1, 'day'));
};
const handleEdit = (agendamento) => {
// Adapte para a sua lógica de edição, talvez abrindo um modal
console.log("Editar:", agendamento);
setDictInfo(agendamento);
};
const handleDelete = (id) => {
setSelectedId(id);
setShowDeleteModal(true);
};
// Gera os horários do dia (ex: 08:00 às 18:00)
const renderTimeSlots = () => {
const slots = [];
for (let i = 8; i <= 18; i++) {
const time = `${i.toString().padStart(2, '0')}:00`;
const hourlyAppointments = appointmentsForDay.filter(app =>
dayjs(app.scheduled_at).format('HH:mm') === time
);
slots.push(
<div className="time-slot" key={time}>
<div className="time-marker">{time}</div>
<div className="appointments-container">
{hourlyAppointments.length > 0 ? (
hourlyAppointments.map(app => (
<div key={app.id} className="appointment-card" data-status={app.status}>
<div className="card-content">
<div className="card-line">
<Stethoscope size={16} className="card-icon" />
<strong>Dr(a):</strong> {app.medico_nome}
</div>
<div className="card-line">
<User size={16} className="card-icon" />
<strong>Paciente:</strong> {app.paciente_nome}
</div>
</div>
<div className="card-actions">
<button className="btn-card-action" onClick={() => handleEdit(app)}>
<Edit size={16} />
</button>
<button className="btn-card-action btn-delete" onClick={() => handleDelete(app.id)}>
<Trash2 size={16} />
</button>
</div>
</div>
))
) : (
<div className="no-appointment-placeholder"></div>
)}
</div>
</div>
);
}
return slots;
};
return (
<div className="modern-daily-view">
<div className="calendar-header">
<button onClick={handlePrevDay} className="btn-nav">
<ChevronLeft size={24} />
</button>
<h2>{currentDate.format('dddd, D [de] MMMM [de] YYYY')}</h2>
<button onClick={handleNextDay} className="btn-nav">
<ChevronRight size={24} />
</button>
</div>
<div className="timeline">
{renderTimeSlots()}
</div>
</div>
);
};
export default TabelaAgendamentoDia;

View File

@ -1,15 +1,12 @@
import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import API_KEY from "../components/utils/apiKeys";
import { useAuth } from "../components/utils/AuthProvider";
import "./style/TablePaciente.css";
import ModalErro from "../components/utils/fetchErros/ModalErro";
import manager from "../components/utils/fetchErros/ManagerFunction";
function TablePaciente({ setCurrentPage, setPatientID }) {
function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
const { getAuthorizationHeader, isAuthenticated } = useAuth();
const navigate = useNavigate();
const { getAuthorizationHeader, isAuthenticated, RefreshingToken } = useAuth();
const [pacientes, setPacientes] = useState([]);
const [search, setSearch] = useState("");
@ -24,18 +21,10 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
const [dataInicial, setDataInicial] = useState("");
const [dataFinal, setDataFinal] = useState("");
const [sortKey, setSortKey] = useState(null);
const [sortDir, setSortDir] = useState('asc');
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedPatientId, setSelectedPatientId] = useState(null);
const [showModalError, setShowModalError] = useState("");
const [showModalError, setShowModalError] = useState(false);
const [ ErrorInfo, setErrorInfo] = useState({})
@ -114,12 +103,7 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
}
};
const RefreshingToken = () => {
console.log("Refreshing token...");
};
useEffect(() => {
const authHeader = getAuthorizationHeader()
console.log(authHeader, 'aqui autorização')
@ -136,7 +120,7 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
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 => {
@ -152,23 +136,27 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
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 => {
console.error(error, "deu erro")
manager(setShowModalError, RefreshingToken, setErrorInfo, 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);
});
}, [isAuthenticated, getAuthorizationHeader]);
@ -207,7 +195,6 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
setIdadeMaxima("");
setDataInicial("");
setDataFinal("");
setPaginaAtual(1);
};
const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => {
@ -251,67 +238,13 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
return resultado;
}) : [];
const applySorting = (arr) => {
if (!Array.isArray(arr) || !sortKey) return arr;
const copy = [...arr];
if (sortKey === 'nome') {
copy.sort((a, b) => (a.full_name || '').localeCompare((b.full_name || ''), undefined, { sensitivity: 'base' }));
} else if (sortKey === 'idade') {
copy.sort((a, b) => calcularIdade(a.birth_date) - calcularIdade(b.birth_date));
}
if (sortDir === 'desc') copy.reverse();
return copy;
};
const pacientesOrdenados = applySorting(pacientesFiltrados);
const totalPaginas = Math.ceil(pacientesFiltrados.length / itensPorPagina);
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const pacientesPaginados = pacientesOrdenados.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(() => {
setPaginaAtual(1);
}, [search, filtroConvenio, filtroVIP, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal, sortKey, sortDir]);
console.log(` Pacientes totais: ${pacientes?.length}, Filtrados: ${pacientesFiltrados?.length}`);
}, [pacientes, pacientesFiltrados, search]);
return (
<>
<ModalErro showModal={showModalError} setShowModal={setShowModalError} ErrorData={ErrorInfo}/>
<div className="page-heading">
<h3>Lista de Pacientes</h3>
</div>
@ -363,7 +296,6 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
<button
className={`btn btn-sm ${filtroVIP ? "btn-primary" : "btn-outline-primary"}`}
onClick={() => setFiltroVIP(!filtroVIP)}
style={{ padding: "0.25rem 0.5rem" }}
>
<i className="bi bi-award me-1"></i> VIP
@ -377,33 +309,6 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
>
<i className="bi bi-calendar me-1"></i> Aniversariantes
</button>
<div className="vr mx-2 d-none d-md-block" />
<div className="d-flex align-items-center gap-2">
<span className="text-muted small">Ordenar por:</span>
{(() => {
const sortValue = sortKey ? `${sortKey}-${sortDir}` : '';
return (
<select
className="form-select compact-select sort-select w-auto"
value={sortValue}
onChange={(e) => {
const v = e.target.value;
if (!v) { setSortKey(null); setSortDir('asc'); return; }
const [k, d] = v.split('-');
setSortKey(k);
setSortDir(d);
}}
>
<option value="">Sem ordenação</option>
<option value="nome-asc">Nome (A-Z)</option>
<option value="nome-desc">Nome (Z-A)</option>
<option value="idade-asc">Idade (crescente)</option>
<option value="idade-desc">Idade (decrescente)</option>
</select>
);
})()}
</div>
</div>
<div className="d-flex justify-content-between align-items-center">
@ -495,12 +400,31 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
</div>
</div>
)}
</div>
<div className="mt-3">
<div className="contador-pacientes">
{pacientesFiltrados.length} DE {pacientes.length} PACIENTES ENCONTRADOS
{(search || filtroConvenio !== "Todos" || filtroVIP || 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>}
{filtroConvenio !== "Todos" && <span className="badge bg-primary me-2">Convênio: {filtroConvenio}</span>}
{filtroVIP && <span className="badge bg-primary me-2">VIP</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>}
{idadeMinima && <span className="badge bg-primary me-2">Idade mín: {idadeMinima}</span>}
{idadeMaxima && <span className="badge bg-primary me-2">Idade máx: {idadeMaxima}</span>}
{dataInicial && <span className="badge bg-primary me-2">Data inicial: {dataInicial}</span>}
{dataFinal && <span className="badge bg-primary me-2">Data final: {dataFinal}</span>}
</div>
</div>
)}
<div className="mb-3">
<span className="badge results-badge">
{pacientesFiltrados?.length} de {pacientes?.length} pacientes encontrados
</span>
</div>
<div className="table-responsive">
@ -515,8 +439,8 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
</tr>
</thead>
<tbody>
{pacientesPaginados.length > 0 ? (
pacientesPaginados.map((paciente) => (
{pacientesFiltrados.length > 0 ? (
pacientesFiltrados.map((paciente) => (
<tr key={paciente.id}>
<td>
<div className="d-flex align-items-center patient-name-container">
@ -544,21 +468,17 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
<td>{paciente.email || 'Não informado'}</td>
<td>
<div className="d-flex gap-2">
<Link to={"details"}>
<button className="btn btn-sm btn-view" onClick={() => setDictInfo(paciente)}>
<Link to={`${paciente.id}`}>
<button className="btn btn-sm btn-view">
<i className="bi bi-eye me-1"></i> Ver Detalhes
</button>
</Link>
<button
className="btn btn-sm btn-edit"
onClick={() => {
setDictInfo(paciente);
navigate('edit');
}}
>
<Link to={`${paciente.id}/edit`}>
<button className="btn btn-sm btn-edit">
<i className="bi bi-pencil me-1"></i> Editar
</button>
</Link>
<button
className="btn btn-sm btn-delete"
@ -575,75 +495,13 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
))
) : (
<tr>
<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 colSpan="5" className="empty-state">
Nenhum paciente encontrado.
</td>
</tr>
)}
</tbody>
</table>
{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>
@ -665,10 +523,15 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#dc3545', color: 'white' }}>
<div className="modal-header">
<h5 className="modal-title">
Confirmação de Exclusão
</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
></button>
</div>
<div className="modal-body">

View File

@ -1,327 +0,0 @@
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import {
FaCalendarAlt,
FaCalendarCheck,
FaFileAlt,
FaUserMd,
FaClock,
} from "react-icons/fa";
import { useAuth } from "../components/utils/AuthProvider";
import API_KEY from "../components/utils/apiKeys";
import "./style/inicioPaciente.css";
function InicioPaciente() {
const navigate = useNavigate();
const { getAuthorizationHeader, isAuthenticated } = useAuth();
const [agendamentos, setAgendamentos] = useState([]);
const [medicos, setMedicos] = useState([]);
const [agendamentosComMedicos, setAgendamentosComMedicos] = useState([]);
const [loading, setLoading] = useState(true);
const [pacienteId, setPacienteId] = useState(null);
useEffect(() => {
const userId =
localStorage.getItem("user_id") || localStorage.getItem("patient_id");
setPacienteId(userId);
}, []);
useEffect(() => {
const fetchMedicos = async () => {
try {
const authHeader = getAuthorizationHeader();
const myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
const requestOptions = {
method: "GET",
headers: myHeaders,
redirect: "follow",
};
const response = await fetch(
"https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors",
requestOptions
);
if (response.ok) {
const data = await response.json();
setMedicos(data);
console.log(" Médicos carregados:", data.length);
} else {
console.error(" Erro ao buscar médicos:", response.status);
}
} catch (error) {
console.error(" Erro ao buscar médicos:", error);
}
};
const fetchAgendamentos = async () => {
try {
const authHeader = getAuthorizationHeader();
const myHeaders = new Headers();
myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", authHeader);
const requestOptions = {
method: "GET",
headers: myHeaders,
redirect: "follow",
};
// Buscar todos os agendamentos (depois filtraremos pelo paciente)
const response = await fetch(
"https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments",
requestOptions
);
if (response.ok) {
const data = await response.json();
setAgendamentos(data);
console.log(" Agendamentos carregados:", data.length);
} else {
console.error(" Erro ao buscar agendamentos:", response.status);
}
} catch (error) {
console.error(" Erro ao buscar agendamentos:", error);
} finally {
setLoading(false);
}
};
if (isAuthenticated) {
fetchMedicos();
fetchAgendamentos();
}
}, [isAuthenticated, getAuthorizationHeader]);
useEffect(() => {
if (agendamentos.length > 0 && medicos.length > 0) {
const agendamentosComNomes = agendamentos.map((agendamento) => {
const medico = medicos.find((m) => m.id === agendamento.doctor_id);
return {
...agendamento,
nomeMedico: medico?.full_name || "Médico não encontrado",
especialidadeMedico: medico?.specialty || "",
};
});
setAgendamentosComMedicos(agendamentosComNomes);
}
}, [agendamentos, medicos]);
const meusAgendamentos = agendamentosComMedicos.filter((a) =>
pacienteId ? a.patient_id === pacienteId : true
);
const hoje = new Date();
hoje.setHours(0, 0, 0, 0);
const agendamentosFuturos = meusAgendamentos
.filter((a) => {
if (!a.scheduled_at) return false;
const dataAgendamento = new Date(a.scheduled_at);
return (
dataAgendamento >= hoje &&
a.status !== "cancelled" &&
a.status !== "completed"
);
})
.sort((a, b) => new Date(a.scheduled_at) - new Date(b.scheduled_at));
const proximasConsultas = agendamentosFuturos.length;
const consultasHoje = agendamentosFuturos.filter((a) => {
const dataAgendamento = new Date(a.scheduled_at);
dataAgendamento.setHours(0, 0, 0, 0);
return dataAgendamento.getTime() === hoje.getTime();
}).length;
const consultasPendentes = meusAgendamentos.filter(
(a) => a.status === "pending" || a.status === "requested"
).length;
const historicoConsultas = meusAgendamentos.filter(
(a) => a.status === "completed"
).length;
return (
<div className="dashboard-paciente-container">
<div className="dashboard-paciente-header">
<h1>Bem-vindo ao MediConnect</h1>
<p>Gerencie suas consultas e acompanhe seu histórico médico</p>
</div>
<div className="stats-paciente-grid">
<div className="stat-paciente-card">
<div className="stat-paciente-info">
<span className="stat-paciente-label">Próximas Consultas</span>
<span className="stat-paciente-value">{proximasConsultas}</span>
</div>
<div className="stat-paciente-icon-wrapper blue">
<FaCalendarAlt className="stat-paciente-icon" />
</div>
</div>
<div className="stat-paciente-card">
<div className="stat-paciente-info">
<span className="stat-paciente-label">Consultas Hoje</span>
<span className="stat-paciente-value">{consultasHoje}</span>
</div>
<div className="stat-paciente-icon-wrapper green">
<FaCalendarCheck className="stat-paciente-icon" />
</div>
</div>
<div className="stat-paciente-card">
<div className="stat-paciente-info">
<span className="stat-paciente-label">Aguardando</span>
<span className="stat-paciente-value">
{loading ? "..." : consultasPendentes}
</span>
</div>
<div className="stat-paciente-icon-wrapper purple">
<FaClock className="stat-paciente-icon" />
</div>
</div>
<div className="stat-paciente-card">
<div className="stat-paciente-info">
<span className="stat-paciente-label">Realizadas</span>
<span className="stat-paciente-value">{historicoConsultas}</span>
</div>
<div className="stat-paciente-icon-wrapper orange">
<FaFileAlt className="stat-paciente-icon" />
</div>
</div>
</div>
<div className="quick-actions-paciente">
<h2>Acesso Rápido</h2>
<div className="actions-paciente-grid">
<div
className="action-paciente-button"
onClick={() => navigate("/paciente/agendamento")}
>
<FaCalendarCheck className="action-paciente-icon" />
<div className="action-paciente-info">
<span className="action-paciente-title">Minhas Consultas</span>
<span className="action-paciente-desc">
Ver todos os agendamentos
</span>
</div>
</div>
<div
className="action-paciente-button"
onClick={() => navigate("/paciente/laudo")}
>
<FaFileAlt className="action-paciente-icon" />
<div className="action-paciente-info">
<span className="action-paciente-title">Meus Laudos</span>
<span className="action-paciente-desc">
Acessar documentos médicos
</span>
</div>
</div>
<div
className="action-paciente-button"
onClick={() => navigate("/paciente/agendamento")}
>
<FaUserMd className="action-paciente-icon" />
<div className="action-paciente-info">
<span className="action-paciente-title">Meus Médicos</span>
<span className="action-paciente-desc">
Ver histórico de atendimentos
</span>
</div>
</div>
</div>
</div>
<div className="proximas-consultas-section">
<h2>Próximas Consultas</h2>
{loading ? (
<div className="no-consultas-content">
<p>Carregando suas consultas...</p>
</div>
) : agendamentosFuturos.length > 0 ? (
<div className="consultas-paciente-list">
{agendamentosFuturos.slice(0, 3).map((agendamento) => (
<div key={agendamento.id} className="consulta-paciente-item">
<div className="consulta-paciente-info">
<div className="consulta-paciente-time-date">
<p className="consulta-paciente-hora">
{new Date(agendamento.scheduled_at).toLocaleTimeString(
"pt-BR",
{
hour: "2-digit",
minute: "2-digit",
}
)}
</p>
<p className="consulta-paciente-data">
{new Date(agendamento.scheduled_at).toLocaleDateString(
"pt-BR",
{
day: "2-digit",
month: "short",
year: "numeric",
}
)}
</p>
</div>
<div className="consulta-paciente-detalhes">
<p className="consulta-paciente-medico">
<FaUserMd className="consulta-icon" />
<strong>Dr(a):</strong> {agendamento.nomeMedico}
</p>
{agendamento.especialidadeMedico && (
<p className="consulta-paciente-especialidade">
{agendamento.especialidadeMedico}
</p>
)}
</div>
<span
className={`consulta-paciente-status status-${agendamento.status}`}
>
{agendamento.status === "scheduled"
? "Confirmado"
: agendamento.status === "pending"
? "Aguardando"
: agendamento.status === "requested"
? "Solicitado"
: agendamento.status}
</span>
</div>
</div>
))}
{agendamentosFuturos.length > 3 && (
<button
className="view-all-paciente-button"
onClick={() => navigate("/paciente/agendamento")}
>
Ver todas as consultas
</button>
)}
</div>
) : (
<div className="no-consultas-content">
<FaCalendarCheck className="no-consultas-icon" />
<p>Você não tem consultas agendadas</p>
<button
className="agendar-paciente-button"
onClick={() => navigate("/paciente/agendamento/criar")}
>
Agendar Consulta
</button>
</div>
)}
</div>
</div>
);
}
export default InicioPaciente;

View File

@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
.filtros-container select,
.filtros-container input {
padding: 0.5rem;
@ -17,403 +15,415 @@
}
.unidade-selecionarprofissional{
background-color: #ffffff;
padding: 20px 20px;
background-color: #fdfdfdde;
padding: 20px 10px;
display: flex;
justify-content: flex-start;
align-items: center;
border-radius:10px ;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.unidade-selecionarprofissional input, .unidade-selecionarprofissional select {
margin-left: 8px;
border-radius: 8px;
padding: 5px;
width: 20%;
}
.unidade-selecionarprofissional select{
width: 7%;
}
.busca-atendimento{
display: flex;
flex-direction: row;
margin: 0;
margin:10px;
justify-content: flex-start;
}
.busca-atendimento input {
border: 2px solid #000000;
.busca-atendimento select{
padding:5px;
border-radius:8px ;
padding: 10px 15px;
margin-left: 15px;
background-color: #0078d7;
color: white;
font-weight: bold;
}
.busca-atendimento input{
margin-left: 8px;
border-radius: 8px;
padding: 5px;
width: 100%;
font-size: 1rem;
margin-left: 0;
}
.btn-selecionar-tabeladia, .btn-selecionar-tabelasemana, .btn-selecionar-tabelames {
background-color: rgba(231, 231, 231, 0.808);
padding:8px 10px;
font-size: larger;
font-weight: bold;
border-style: hidden;
}
.btn-selecionar-tabeladia{
border-radius: 10px 0px 0px 10px;
}
.btn-selecionar-tabelames{
border-radius: 0px 10px 10px 0px;
}
.btn-selecionar-tabeladia.ativo, .btn-selecionar-tabelasemana.ativo, .btn-selecionar-tabelames.ativo{
background-color: lightcyan;
border-color: darkcyan;
font-weight: bolder;
}
.legenda-tabela{
display: flex;
margin-top: 30px;
margin-bottom: 10px;
gap: 15px;
justify-content: flex-end;
}
.legenda-item-realizado{
background-color: #2c5e37;
}
.legenda-item-confirmed{
background-color: #1e90ff;
}
.legenda-item-cancelado{
background-color: #d9534f;
}
.legenda-item-agendado{
background-color: #f0ad4e;
}
#status-card-consulta-completed, .legenda-item-realizado {
background-color: #b7ffbd;
border:3px solid #91d392;
padding: 5px;
font-weight: bold;
border-radius: 10px;
}
#status-card-consulta-cancelled, .legenda-item-cancelado {
background-color: #ffb7cc;
border:3px solid #ff6c84;
padding: 5px;
font-weight: bold;
border-radius: 10px;
}
#status-card-consulta-confirmed, .legenda-item-confirmed {
background-color: #eef8fb;
border:3px solid #d8dfe7;
padding: 5px;
font-weight: bold;
border-radius: 10px;
}
#status-card-consulta-agendado, .legenda-item-agendado {
background-color: #f7f7c4;
border:3px solid #f3ce67;
padding: 5px;
font-weight: bold;
border-radius: 10px;
}
.btns-e-legenda-container{
display: flex;
justify-content: space-between;
flex-direction: row;
margin-top: 10px;
}
.calendario {
border-collapse: collapse;
width: 100%;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 12px rgb(255, 255, 255);
border: 10px solid #ffffffc5;
background-color: rgb(253, 253, 253);
}
.calendario-ou-filaespera{
margin-top: 0;
}
.container-btns-agenda-fila_esepera{
margin-top: 20px;
margin-left: 0;
margin-top: 30px;
display: flex;
flex-direction: row;
gap: 0;
border-bottom: 2px solid #E2E8F0;
margin-bottom: 20px;
gap: 20px;
margin-left:20px ;
}
.btn-fila-espera, .btn-agenda{
background-color: transparent;
border: 0;
border-bottom: 2px solid transparent;
padding: 10px 12px;
border-radius: 0;
font-weight: 600;
color: #718096;
cursor: pointer;
margin-bottom: -2px;
transition: color 0.2s, border-color 0.2s;
}
.btn-fila-espera:hover, .btn-agenda:hover {
color: #2B6CB0;
border: 0px ;
border-bottom: 3px solid rgb(253, 253, 253);
padding: 8px;
border-radius: 10px 10px 0px 0px;
font-weight: bold;
}
.opc-filaespera-ativo, .opc-agenda-ativo{
color: #4299E1;
background-color: transparent;
border-bottom: 2px solid #4299E1;
color: white;
background-color: #5980fd;
}
.input-e-dropdown-wrapper {
position: relative;
width: 100%;
margin: 0;
html[data-bs-theme="dark"] {
body {
background-color: #121212;
color: #e0e0e0;
}
.dropdown-medicos {
position: absolute;
top: 100%;
left: 0;
width: 100%;
z-index: 1000;
background-color: #ffffff;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-top: none;
max-height: 250px;
overflow-y: auto;
}
.dropdown-item { padding: 10px 15px; cursor: pointer; }
.dropdown-item:hover { background-color: #f0f0f0; }
.calendar-wrapper {
display: flex;
gap: 24px;
background-color: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
font-family: 'Inter', sans-serif;
margin-top: 20px;
}
.calendar-info-panel { flex: 0 0 300px; border-right: 1px solid #E2E8F0; padding-right: 24px; display: flex; flex-direction: column; }
.info-date-display { background-color: #EDF2F7; border-radius: 8px; padding: 12px; text-align: center; margin-bottom: 16px; }
.info-date-display span { font-weight: 600; color: #718096; text-transform: uppercase; font-size: 0.9rem; }
.info-date-display strong { display: block; font-size: 2.5rem; font-weight: 700; color: #2D3748; }
.info-details { text-align: center; margin-bottom: 24px; }
.info-details h3 { font-size: 1.25rem; font-weight: 600; color: #2D3748; margin: 0; text-transform: capitalize; }
.info-details p { color: #718096; margin: 0; }
.appointments-list { flex-grow: 1; overflow-y: auto; }
.appointments-list h4 { font-size: 1rem; font-weight: 600; color: #4A5568; margin-bottom: 12px; position: sticky; top: 0; background-color: #fff; padding-bottom: 8px; }
.appointment-item { display: flex; gap: 12px; padding: 10px; border-radius: 6px; border-left: 4px solid; margin-bottom: 8px; background-color: #F7FAFC; }
.item-time { font-weight: 600; color: #2B6CB0; }
.item-details { display: flex; flex-direction: column; }
.item-details span { font-weight: 500; color: #2D3748; }
.item-details small { color: #718096; }
.no-appointments-info { text-align: center; padding: 20px; color: #A0AEC0; }
.appointment-item[data-status="confirmed"] { border-color: #4299E1; }
.appointment-item[data-status="completed"] { border-color: #48BB78; }
.appointment-item[data-status="cancelled"] { border-color: #F56565; }
.appointment-item[data-status="agendado"],
.appointment-item[data-status="requested"] { border-color: #ED8936; }
.calendar-main { flex-grow: 1; }
.calendar-controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.date-indicator h2 { font-size: 1.5rem; font-weight: 600; color: #2D3748; margin: 0; text-transform: capitalize; }
.nav-buttons { display: flex; gap: 8px; }
.nav-buttons button { padding: 8px 12px; border-radius: 6px; border: 1px solid #CBD5E0; background-color: #fff; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.nav-buttons button:hover { background-color: #EDF2F7; }
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; }
.day-header { font-weight: 600; color: #718096; text-align: center; padding: 8px 0; font-size: 0.875rem; }
.day-cell { min-height: 110px; border-radius: 8px; border: 1px solid #E2E8F0; padding: 8px; transition: background-color 0.2s, border-color 0.2s; cursor: pointer; position: relative; }
.day-cell span { font-weight: 600; color: #4A5568; }
.day-cell:hover { background-color: #EDF2F7; border-color: #BEE3F8; }
.day-cell.other-month { background-color: #F7FAFC; }
.day-cell.other-month span { color: #A0AEC0; }
.day-cell.today span { background-color: #4299E1; color: #fff; border-radius: 50%; padding: 2px 6px; display: inline-block; }
.day-cell.selected { background-color: #BEE3F8; border-color: #4299E1; }
.appointments-indicator { background-color: #4299E1; color: white; font-size: 0.7rem; font-weight: bold; border-radius: 50%; width: 18px; height: 18px; display: flex; justify-content: center; align-items: center; position: absolute; bottom: 8px; right: 8px; }
.calendar-legend {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-bottom: 16px;
}
.legend-item {
padding: 6px 12px;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
color: #333;
}
.legend-item[data-status="completed"] {
background-color: #C6F6D5;
border: 1px solid #9AE6B4;
color: #2F855A;
}
.legend-item[data-status="confirmed"] {
background-color: #EBF8FF;
border: 1px solid #BEE3F8;
color: #3182CE;
}
.legend-item[data-status="agendado"] {
background-color: #FEFCBF;
border: 1px solid #F6E05E;
color: #B7791F;
}
.legend-item[data-status="cancelled"] {
background-color: #FED7D7;
border: 1px solid #FEB2B2;
color: #C53030;
}
.appointment-item {
display: flex;
align-items: center;
justify-content: space-between;
}
.item-details {
flex-grow: 1;
}
.appointment-actions {
display: flex;
gap: 8px;
}
.btn-action {
padding: 8px;
border-radius: 8px;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.btn-action.btn-edit {
background-color: #FEFCBF;
color: #B7791F;
}
.btn-action.btn-edit:hover {
background-color: #F6E05E;
}
.btn-action.btn-delete {
background-color: #E53E3E;
color: #fff;
}
.btn-action.btn-delete:hover {
background-color: #C53030;
}
.table-wrapper {
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 600px;
}
@media (max-width: 768px) {
.busca-atendimento {
flex-direction: column;
gap: 10px;
}
.container-btns-agenda-fila_esepera {
flex-direction: column;
align-items: flex-start;
gap: 10px;
flex-wrap: wrap;
}
.btns-gerenciamento-e-consulta {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
}
.btn-adicionar-consulta {
padding: 8px 12px;
font-size: 0.8rem;
white-space: normal;
text-align: center;
.calendario {
background-color: #1e1e1e;
border: 10px solid #333;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.unidade-selecionarprofissional {
background-color: #1e1e1e;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.unidade-selecionarprofissional input,
.unidade-selecionarprofissional select,
.busca-atendimento select,
.busca-atendimento input {
background-color: #2c2c2c;
color: #e0e0e0;
border: 1px solid #444;
}
.btn-buscar,
.btn-selecionar-tabeladia,
.btn-selecionar-tabelasemana,
.btn-selecionar-tabelames {
background-color: #2c2c2c;
color: #e0e0e0;
border: none;
}
.btn-selecionar-tabeladia.ativo,
.btn-selecionar-tabelasemana.ativo,
.btn-selecionar-tabelames.ativo {
background-color: #005a9e;
border-color: #004578;
color: #fff;
}
.legenda-item-realizado {
background-color: #14532d;
border-color: #166534;
}
.legenda-item-confirmado {
background-color: #1e3a8a;
border-color: #2563eb;
}
.legenda-item-cancelado {
background-color: #7f1d1d;
border-color: #dc2626;
}
.legenda-item-agendado {
background-color: #78350f;
border-color: #f59e0b;
}
#status-card-consulta-realizado,
.legenda-item-realizado {
background-color: #14532d;
border: 3px solid #166534;
color: #e0e0e0;
}
#status-card-consulta-cancelado,
.legenda-item-cancelado {
background-color: #7f1d1d;
border: 3px solid #dc2626;
color: #e0e0e0;
}
#status-card-consulta-confirmado,
.legenda-item-confirmado {
background-color: #1e3a8a;
border: 3px solid #2563eb;
color: #e0e0e0;
}
#status-card-consulta-agendado,
.legenda-item-agendado {
background-color: #78350f;
border: 3px solid #f59e0b;
color: #e0e0e0;
}
.btns-e-legenda-container {
background-color: #181818;
}
.container-btns-agenda-fila_esepera {
background-color: #181818;
}
.btn-fila-espera,
.btn-agenda {
background-color: #2c2c2c;
color: #e0e0e0;
border-bottom: 3px solid #333;
}
.opc-filaespera-ativo,
.opc-agenda-ativo {
color: #fff;
background-color: #005a9e;
}
}
/* Estilo para o botão de Editar */
.btn-edit-custom {
background-color: #FFF3CD;
color: #856404;
}
/* Estilo para o botão de Excluir (Deletar) */
.btn-delete-custom {
background-color: #F8D7DA;
color: #721C24;
padding: 10px;
}
.cardconsulta{
display:flex;
align-items: center;
flex-direction: row;
}
.container-botons{
display: flex;
flex-direction: column;
align-items: stretch;
}
#tabela-seletor-container {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.calendar-wrapper {
flex-direction: column;
padding: 16px;
}
.calendar-info-panel {
border-right: none;
border-bottom: 1px solid #E2E8F0;
padding-right: 0;
padding-bottom: 16px;
}
.calendar-grid { grid-template-columns: repeat(4, 1fr); }
.calendar-controls { flex-direction: column; align-items: flex-start; gap: 8px; }
}
@media (max-width: 576px) {
.calendar-grid { grid-template-columns: 1fr; }
.date-indicator h2 { font-size: 1.25rem; }
.legend-item { font-size: 0.75rem; padding: 4px 8px; }
.appointment-item { flex-direction: column; align-items: stretch; gap: 8px; }
.appointment-actions { width: 100%; }
.btn-action { width: 100%; }
}
@media (max-width: 425px) {
.calendar-main {
overflow-x: auto;
}
.calendar-grid {
min-width: 400px;
grid-template-columns: repeat(7, 1fr);
}
.day-cell {
min-height: 80px;
}
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
border: 1px solid #E2E8F0;
background-color: #fff;
border-radius: 8px;
margin-bottom: 16px;
padding: 6px 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
width: fit-content;
margin: 0 auto;
}
table {
min-width: 600px;
font-size: 0.875rem;
}
table th,
table td {
padding: 8px;
#tabela-seletor-container p {
margin: 0;
font-size: 23px;
font-weight: 500;
color: #4085f6;
text-align: center;
white-space: nowrap;
}
}
.container-btns-agenda-fila_esepera {
display: flex;
justify-content: space-between; /* abas à esquerda, botões à direita */
align-items: center;
gap: 16px; /* opcional: espaço entre os blocos */
width: 100%;
}
/* garante que os botões fiquem em linha */
.btns-gerenciamento-e-consulta {
display: flex;
gap: 8px;
}
/* em telas muito pequenas, pode empilhar verticalmente */
@media (max-width: 768px) {
.container-btns-agenda-fila_esepera {
flex-direction: column;
align-items: stretch;
}
.btns-gerenciamento-e-consulta {
justify-content: flex-start; /* ou center se preferir */
flex-wrap: wrap;
}
}
/* barra de abas + botões */
.container-btns-agenda-fila_esepera {
margin-bottom: 0; /* cola com a linha de baixo */
}
/* um respiro pequeno entre as abas e o conteúdo,
mas igual para Agenda e Fila de espera */
.calendario-ou-filaespera {
margin-top: 4px; /* aumenta um pouquinho, não 20px */
padding-top: 0;
}
/* se o título "Fila de Espera" estiver mais distante,
aproxima o conteúdo dessa área também */
.page-content.table-paciente-container {
margin-top: 4px;
}
/* 1) container das abas + botões: encostado no topo */
.container-btns-agenda-fila_esepera {
margin-bottom: 0;
}
/* 2) sempre cria um espaçamento logo DEPOIS das abas,
antes de qualquer conteúdo (Agenda ou Fila) */
.container-btns-agenda-fila_esepera + .calendario-ou-filaespera {
margin-top: 8px; /* aumenta ou diminui aqui */
padding-top: 0;
}
/* 3) garante que o primeiro filho da section não roube/colapse margens */
.calendario-ou-filaespera > *:first-child {
margin-top: 0 !important;
}
/* mesmos blocos azuis e quadrados */
.btn-consulta-paciente {
display: inline-flex;
align-items: center; /* alinha ícone + texto verticalmente */
justify-content: center;
gap: 6px; /* espaço entre ícone e texto */
padding: 10px 22px; /* altura/largura parecidas com os azuis */
border-radius: 6px; /* se os outros forem 6px, mantém igual */
font-weight: 500;
#tabela-seletor-container button {
background: transparent;
border: none;
}
/* garante mesma cor dos blocos da secretaria */
.btn-consulta-paciente.btn-primary {
background-color: #1d4ed8; /* azul escuro do primeiro bloco */
}
/* se quiser hover igual */
.btn-consulta-paciente.btn-primary:hover {
background-color: #1437a3;
}
.btn-consulta-paciente {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 22px;
color: #555;
font-size: 20px;
cursor: pointer;
padding: 4px 6px;
border-radius: 6px;
font-weight: 500;
border: none;
transition: all 0.2s ease-in-out;
}
#tabela-seletor-container button:hover {
background-color: rgba(0, 0, 0, 0.05);
color: #000;
}
#tabela-seletor-container i {
pointer-events: none;
}
.input-e-dropdown-wrapper {
position: relative;
width: 350px;
margin-left: auto;
}
.busca-atendimento {
}
.busca-atendimento > div {
/* Garante que a div interna do input ocupe toda a largura do wrapper */
width: 100%;
/* Estilos para o contêiner do ícone e input, se necessário */
}
.busca-atendimento input {
/* Garante que o input preencha a largura disponível dentro do seu contêiner */
width: calc(100% - 40px); /* Exemplo: 100% menos a largura do ícone (aprox. 40px) */
/* ... outros estilos de borda, padding, etc. do seu input ... */
}
/* 3. O Dropdown: Posicionamento e Estilização */
.dropdown-medicos {
/* POSICIONAMENTO: Faz o dropdown flutuar */
position: absolute;
/* POSICIONAMENTO: Coloca o topo do dropdown logo abaixo do input */
top: 100%;
left: 0;
/* LARGURA: Essencial. Ocupa 100% do .input-e-dropdown-wrapper, limitando-se a ele. */
width: 100%;
/* SOBREPOSIÇÃO: Garante que fique acima de outros elementos (como a Fila de Espera) */
z-index: 1000;
/* ESTILIZAÇÃO: Aparência */
background-color: #ffffff; /* Fundo branco para não vazar */
border: 1px solid #ccc; /* Borda leve */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Sombra para profundidade */
border-top: none; /* Deixa o visual mais integrado ao input */
/* COMPORTAMENTO: Limite de altura e scroll */
max-height: 250px;
overflow-y: auto;
}
/* 4. Estilização de cada item do dropdown */
.dropdown-item {
padding: 10px 15px;
cursor: pointer;
font-size: 14px;
color: #333;
/* Evita que nomes muito longos quebrem ou saiam da caixa */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dropdown-item:hover {
background-color: #f0f0f0; /* Cor ao passar o mouse */
color: #007bff;
}

View File

@ -1,394 +0,0 @@
.disponibilidades-container {
padding: 20px;
background: #f5f7fa;
min-height: 100vh;
}
.disponibilidades-title {
font-size: 1.5rem;
font-weight: bold;
color: #2c3e50;
margin-bottom: 20px;
}
.search-container {
margin: 10px 0 25px 0;
position: relative;
background-color: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border: 1px solid #e1e8ed;
}
.search-input-container {
position: relative;
}
.search-input {
border: 1px solid #dce1e6;
border-radius: 6px;
padding: 10px 40px 10px 12px;
width: 100%;
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s;
}
.search-input:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.1);
}
.clear-search-btn {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
font-size: 18px;
cursor: pointer;
color: #7f8c8d;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.2s;
}
.clear-search-btn:hover {
background-color: #f8f9fa;
color: #e74c3c;
}
.suggestions-dropdown {
border: 1px solid #e1e8ed;
border-radius: 6px;
background-color: white;
position: absolute;
z-index: 1000;
width: calc(100% - 30px);
max-height: 200px;
overflow-y: auto;
margin-top: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
left: 15px;
right: 15px;
}
.suggestion-item {
padding: 10px 12px;
cursor: pointer;
border-bottom: 1px solid #f8f9fa;
transition: background-color 0.2s;
font-size: 14px;
color: #2c3e50;
}
.suggestion-item:hover {
background-color: #f8f9fa;
}
.suggestion-item:last-child {
border-bottom: none;
}
/* Collapsible Styles */
.doctor-group {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
margin-bottom: 8px;
overflow: hidden;
border: 1px solid #e1e8ed;
}
.doctor-header {
padding: 16px 20px;
background-color: #f1f3f5;
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s;
border-bottom: 1px solid #e1e8ed;
}
.doctor-header:hover {
background-color: #e9ecef;
}
.doctor-name {
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
margin: 0;
}
.doctor-hours {
font-size: 0.9rem;
font-weight: normal;
color: #7f8c8d;
margin-left: 8px;
}
.expand-icon {
font-size: 1rem;
color: #7f8c8d;
transform: rotate(0deg);
transition: transform 0.3s ease;
}
.expand-icon.expanded {
transform: rotate(180deg);
}
.doctor-content {
padding: 0;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
}
.doctor-group.expanded .doctor-content {
max-height: 5000px;
}
/* Table Styles */
.table-container {
overflow-x: auto;
padding: 0 10px 10px 10px;
}
.disponibilidades-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
background-color: white;
}
.disponibilidades-table thead {
background-color: #f8f9fa;
}
.disponibilidades-table thead th {
padding: 6px 16px;
text-align: left;
font-weight: 600;
color: #2c3e50;
border-bottom: 1px solid #e1e8ed;
font-size: 0.875rem;
}
.disponibilidades-table tbody tr {
transition: background-color 0.2s;
border-bottom: 1px solid #f1f3f5;
}
.disponibilidades-table tbody tr:last-child {
border-bottom: none;
}
.disponibilidades-table tbody tr:hover {
background-color: #f8f9fa;
}
.disponibilidades-table td {
padding: 12px 16px;
color: #495057;
border-bottom: 1px solid #f1f3f5;
}
/* Status Badges */
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
color: white;
display: inline-block;
text-align: center;
min-width: 80px;
}
.status-active {
background-color: #27ae60;
}
@media (max-width: 576px) {
.disponibilidades-container { padding: 12px; }
.disponibilidades-title { font-size: 1.25rem; }
.search-container { padding: 12px; }
.doctor-header { padding: 12px; }
.doctor-name { font-size: 1rem; }
.doctor-hours { display: none; }
.expand-icon { font-size: 0.9rem; }
.table-container { padding: 0 6px 6px 6px; }
.disponibilidades-table thead th { padding: 6px 8px; }
.disponibilidades-table td { padding: 10px 8px; font-size: 0.8125rem; }
.disponibilidades-table thead th:nth-child(4),
.disponibilidades-table thead th:nth-child(5),
.disponibilidades-table thead th:nth-child(7),
.disponibilidades-table tbody td:nth-child(4),
.disponibilidades-table tbody td:nth-child(5),
.disponibilidades-table tbody td:nth-child(7) {
display: none;
}
.disp-buttons-container { flex-direction: column; gap: 10px; }
.disp-btn-primary, .disp-btn-danger { width: 100%; }
.suggestions-dropdown { width: calc(100% - 30px); left: 15px; right: 15px; }
}
.status-inactive {
background-color: #e74c3c;
}
.status-not-configured {
background-color: #7f8c8d;
}
/* Buttons */
.edit-btn-container {
padding: 8px 10px 0px 10px;
background-color: #f8f9fa;
}
.disp-btn-edit {
background-color: #ffe8a1;
color: #2c3e50;
border: 1px solid #f0d860;
border-radius: 6px;
padding: 8px 16px;
cursor: pointer;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.2s;
}
.disp-btn-edit:hover {
background-color: #ffcc00;
border-color: #e6b800;
}
.disp-btn-delete {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
font-weight: 500;
font-size: 0.75rem;
transition: all 0.2s;
}
.disp-btn-delete:hover {
background-color: #f1b0b7;
border-color: #e89ca6;
}
/* Edit Mode Styles */
.edit-container {
background-color: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 1px solid #e1e8ed;
}
.disp-buttons-container {
display: flex;
gap: 12px;
margin-top: 20px;
justify-content: flex-start;
}
.disp-btn-primary {
padding: 10px 20px;
font-size: 0.875rem;
font-weight: 600;
border-radius: 6px;
background-color: #3498db;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.2s;
}
.disp-btn-primary:hover {
background-color: #2980b9;
}
.disp-btn-danger {
padding: 10px 20px;
font-size: 0.875rem;
font-weight: 600;
border-radius: 6px;
background-color: #fa273c;
color: white;
border: none;
cursor: pointer;
transition: background-color 0.2s;
}
.disp-btn-danger:hover {
background-color: #f41936;
}
/* Section Titles */
.section-title {
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid #e1e8ed;
}
/* Loading and Empty States */
.loading-text, .no-results {
text-align: center;
padding: 40px 20px;
color: #7f8c8d;
font-size: 1rem;
}
.no-results {
background-color: white;
border-radius: 8px;
margin: 20px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* Responsive Design */
@media (max-width: 768px) {
.disponibilidades-container {
padding: 16px;
}
.disponibilidades-table {
font-size: 0.75rem;
}
.disponibilidades-table th,
.disponibilidades-table td {
padding: 8px 12px;
}
.doctor-header {
padding: 12px 16px;
}
.disp-buttons-container {
flex-direction: column;
}
.disp-btn-primary,
.disp-btn-danger {
width: 100%;
}
}

View File

@ -41,7 +41,7 @@
}
.modal-header-success {
background-color: #1e3a8a !important;
background-color: #28a745 !important;
}
.modal-header-error {
@ -104,12 +104,12 @@
}
.modal-button-success {
background-color: #1e3a8a;
background-color: #28a745;
color: #fff;
}
.modal-button-success:hover {
background-color: #1e3a8a;
background-color: #218838;
}
.modal-button-error {
@ -186,8 +186,10 @@
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: #1e3a8a !important;
background-color: #28a745 !important;
}
.modal-overlay .modal-container .modal-header.modal-header-error {
@ -256,7 +258,7 @@
}
.modal-header-success {
background-color: #1e3a8a !important;
background-color: #006400 !important;
}
.modal-header-error {

View File

@ -190,12 +190,6 @@ html, body {
}
/* ===== Fila de Espera ===== */
@media (max-width: 992px) {
.fila-container {
overflow-x: auto;
}
}
.fila-container {
width: 100%;
max-width: none;
@ -262,15 +256,6 @@ html, body {
font-size: 1.5rem;
}
}
@media (max-width: 576px) {
.unidade-selecionarprofissional { flex-direction: column; gap: 10px; }
.unidade-selecionarprofissional input,
.unidade-selecionarprofissional select { width: 100%; margin-left: 0; }
.busca-fila-espera { position: static; width: 100%; margin-bottom: 8px; }
.fila-header { height: auto; flex-direction: column; gap: 8px; }
.btns-e-legenda-container { flex-direction: column; gap: 10px; }
.legenda-tabela { justify-content: center; flex-wrap: wrap; }
}
.fila-header {
position: relative;
display: flex;
@ -292,13 +277,6 @@ html, body {
transition: border-color 0.2s;
}
@media (max-width: 768px) {
.busca-fila-espera {
width: 100%;
position: static;
}
}
.busca-fila-espera:focus {
border-color: #888;
}

View File

@ -21,12 +21,6 @@
margin-bottom: 10px;
}
@media (max-width: 1200px) {
.summary-card {
min-width: 180px;
}
}
.summary-card {
flex: 1;
min-width: 200px;
@ -45,7 +39,6 @@
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 500;
color: #fff;
opacity: 0.9;
}
@ -114,87 +107,43 @@
}
/* Botões de ação */
.action-group {
display: flex;
gap: 8px;
align-items: center;
}
.btn-view {
background-color: #E6F2FF !important;
color: #004085 !important;
border: 1px solid #B8D4F0 !important;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
border-radius: 6px;
.action-btn {
cursor: pointer;
transition: all 0.15s ease-in-out;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-view:hover {
background-color: #D1E7FF !important;
border-color: #9EC5FE !important;
}
.btn-edit {
background-color: #FFF3CD !important;
color: #856404 !important;
border: 1px solid #FFEAA7 !important;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease-in-out;
text-decoration: none;
display: inline-block;
text-align: center;
border: 1px solid #d7e6fb;
background: #fff;
transition: all 0.2s ease;
font-size: 13px;
}
.btn-edit:hover {
background-color: #FFEEBA !important;
border-color: #FFE087 !important;
.action-btn:hover {
background: #f6f9fc;
border-color: #93c5fd;
}
.btn-delete:hover {
background-color: #F1B0B7 !important;
border-color: #ED969E !important;
}
.btn-delete {
background-color: #F8D7DA !important;
color: #721C24 !important;
border: 1px solid #F5C6CB !important;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s ease-in-out;
text-decoration: none;
display: inline-block;
text-align: center;
.action-btn.delete {
border-color: #fca5a5;
color: #b91c1c;
}
html[data-bs-theme="dark"] .btn-view {
background-color: #1e3a8a !important;
color: #e0e0e0 !important;
border-color: #374151 !important;
}
html[data-bs-theme="dark"] .btn-edit {
background-color: #78350f !important;
color: #fef3c7 !important;
border-color: #374151 !important;
}
html[data-bs-theme="dark"] .btn-delete {
background-color: #7f1d1d !important;
color: #fee2e2 !important;
border-color: #374151 !important;
.action-btn.delete:hover {
background: #fee2e2;
border-color: #ef4444;
}
/* Badges de status */
.badge {
display: inline-block;
padding: 8px 18px !important;
padding: 4px 10px;
border-radius: 9999px;
font-size: 14px !important;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
@ -233,18 +182,12 @@ html[data-bs-theme="dark"] .btn-delete {
padding: 24px;
width: 100%;
max-width: 550px;
max-height: 85vh;
max-height: 90vh;
overflow-y: auto;
box-sizing: border-box;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
}
@media (max-width: 576px) {
.modal-card {
padding: 16px;
max-height: 95vh;
}
}
.modal-header {
display: flex;
justify-content: space-between;
@ -255,7 +198,7 @@ html[data-bs-theme="dark"] .btn-delete {
.modal-header h2 {
font-size: 20px;
font-weight: 700;
color: #fff;
color: #1f2937;
margin: 0;
}
@ -265,12 +208,6 @@ html[data-bs-theme="dark"] .btn-delete {
gap: 16px;
}
.modal-card .input-field,
.modal-card .select-field,
.modal-card textarea {
width: 100%;
}
.form-group {
display: flex;
flex-direction: column;
@ -304,23 +241,12 @@ html[data-bs-theme="dark"] .btn-delete {
gap: 10px;
margin-top: 24px;
}
.input-field,
.select-field,
textarea {
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
box-sizing: border-box;
font-size: 14px;
transition: border-color 0.2s, box-shadow 0.2s;
background-color: #fff;
}
/* Inputs e selects */
.input-field,
.select-field,
textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
@ -343,26 +269,6 @@ textarea {
min-height: 80px;
}
.financeiro-wrap .input-field:not(.modal-card *),
.financeiro-wrap .select-field:not(.modal-card *),
.financeiro-wrap textarea:not(.modal-card *) {
width: 30%;
}
@media (max-width: 768px) {
.financeiro-wrap .input-field:not(.modal-card *),
.financeiro-wrap .select-field:not(.modal-card *),
.financeiro-wrap textarea:not(.modal-card *) {
width: 100%;
}
}
.modal-card .input-field,
.modal-card .select-field,
.modal-card textarea {
width: 100%;
}
/* Mensagem quando não há pagamentos */
.empty {
text-align: center;

View File

@ -1,134 +1,3 @@
/* Container Principal */
/* Responsividade */
@media (max-width: 1200px) {
.dashboard-container {
padding: 1.5rem;
}
}
@media (max-width: 768px) {
.dashboard-container {
padding: 1rem;
}
.dashboard-header h1 {
font-size: 1.5rem;
}
.dashboard-header p {
margin-bottom: 1.5rem;
}
.stats-grid {
grid-template-columns: 1fr 1fr; /* 2 colunas em tablets */
gap: 1rem;
}
.stat-value {
font-size: 1.5rem;
}
.stat-icon-wrapper {
width: 40px;
height: 40px;
}
.stat-icon {
font-size: 1rem;
}
.actions-grid {
grid-template-columns: 1fr; /* 1 coluna em tablets */
gap: 1rem;
}
.action-icon {
font-size: 1.8rem;
}
.action-title {
font-size: 0.9rem;
}
.appointments-section {
padding: 1.5rem;
}
.agendamento-info {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.agendamento-time-date {
flex-direction: row;
gap: 1rem;
min-width: auto;
}
.agendamento-detalhes {
min-width: auto;
}
}
@media (max-width: 576px) {
.dashboard-container {
padding: 0.5rem;
}
.dashboard-header h1 {
font-size: 1.3rem;
}
.dashboard-header p {
margin-bottom: 1rem;
}
.stats-grid {
grid-template-columns: 1fr; /* 1 coluna em celulares */
}
.stat-card {
padding: 1rem;
}
.stat-value {
font-size: 1.3rem;
}
.action-button {
padding: 1rem;
}
.appointments-section {
padding: 1rem;
}
.agendamento-item {
padding: 0.75rem 1rem;
}
.agendamento-hora {
font-size: 1.1rem;
}
.agendamento-data {
font-size: 0.7rem;
}
.agendamento-paciente,
.agendamento-medico {
font-size: 0.85rem;
}
.manage-button,
.view-all-button {
padding: 0.6rem 1.2rem;
font-size: 0.8rem;
}
}
/* Container Principal */
.dashboard-container {
padding: 2rem;
@ -201,10 +70,10 @@
}
/* Cores dos ícones */
.stat-icon-wrapper.blue { background-color: #1D3B88; }
.stat-icon-wrapper.green { background-color: #399CE5; }
.stat-icon-wrapper.purple { background-color: #5F5DF2; }
.stat-icon-wrapper.orange { background-color: #051AFF; }
.stat-icon-wrapper.blue { background-color: #5d5dff; }
.stat-icon-wrapper.green { background-color: #30d158; }
.stat-icon-wrapper.purple { background-color: #a272ff; }
.stat-icon-wrapper.orange { background-color: #f1952e; }
/* Seção de Ações Rápidas */
.quick-actions h2 {
@ -360,191 +229,3 @@ html[data-bs-theme="dark"] .manage-button {
html[data-bs-theme="dark"] .manage-button:hover {
background-color: #2323b0;
}
/* Lista de Agendamentos */
.agendamentos-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.agendamento-item {
background-color: #f9fafb;
border-left: 4px solid #5d5dff;
border-radius: 8px;
padding: 1rem 1.25rem;
transition: all 0.2s ease;
}
.agendamento-item:hover {
background-color: #f0f2f5;
transform: translateX(5px);
}
.agendamento-info {
display: flex;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.agendamento-time-date {
display: flex;
flex-direction: column;
align-items: center;
min-width: 90px;
}
.agendamento-hora {
font-size: 1.3rem;
font-weight: 700;
color: #5d5dff;
margin: 0;
line-height: 1.2;
}
.agendamento-data {
font-size: 0.75rem;
font-weight: 500;
color: #888;
margin: 0;
margin-top: 0.25rem;
}
.agendamento-detalhes {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
min-width: 300px;
}
.agendamento-paciente,
.agendamento-medico {
font-size: 0.95rem;
color: #444;
margin: 0;
line-height: 1.4;
}
.agendamento-paciente strong,
.agendamento-medico strong {
font-weight: 600;
color: #333;
}
.agendamento-status {
font-size: 0.75rem;
font-weight: 600;
padding: 0.4rem 0.8rem;
border-radius: 20px;
text-transform: uppercase;
}
.agendamento-status.status-scheduled {
background-color: #e3f2fd;
color: #1976d2;
}
.agendamento-status.status-completed {
background-color: #e8f5e9;
color: #388e3c;
}
.agendamento-status.status-pending {
background-color: #fff3e0;
color: #f57c00;
}
.agendamento-status.status-cancelled {
background-color: #ffebee;
color: #d32f2f;
}
.agendamento-status.status-requested {
background-color: #f3e5f5;
color: #7b1fa2;
}
.view-all-button {
width: 100%;
margin-top: 1rem;
background-color: #f0f2f5;
color: #5d5dff;
border: 2px solid #5d5dff;
border-radius: 8px;
padding: 0.75rem 1.5rem;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.view-all-button:hover {
background-color: #5d5dff;
color: #fff;
}
/* Dark Mode - Agendamentos */
html[data-bs-theme="dark"] .agendamento-item {
background-color: #2a2a2a;
border-left-color: #6c6cff;
}
html[data-bs-theme="dark"] .agendamento-item:hover {
background-color: #333;
}
html[data-bs-theme="dark"] .agendamento-hora {
color: #8888ff;
}
html[data-bs-theme="dark"] .agendamento-data {
color: #999;
}
html[data-bs-theme="dark"] .agendamento-paciente,
html[data-bs-theme="dark"] .agendamento-medico {
color: #d0d0d0;
}
html[data-bs-theme="dark"] .agendamento-paciente strong,
html[data-bs-theme="dark"] .agendamento-medico strong {
color: #e0e0e0;
}
html[data-bs-theme="dark"] .agendamento-status.status-scheduled {
background-color: #1a3a52;
color: #64b5f6;
}
html[data-bs-theme="dark"] .agendamento-status.status-completed {
background-color: #1b3a1f;
color: #81c784;
}
html[data-bs-theme="dark"] .agendamento-status.status-pending {
background-color: #3d2817;
color: #ffb74d;
}
html[data-bs-theme="dark"] .agendamento-status.status-cancelled {
background-color: #3d1f1f;
color: #e57373;
}
html[data-bs-theme="dark"] .agendamento-status.status-requested {
background-color: #2d1f3d;
color: #ba68c8;
}
html[data-bs-theme="dark"] .view-all-button {
background-color: #2a2a2a;
color: #8888ff;
border-color: #6c6cff;
}
html[data-bs-theme="dark"] .view-all-button:hover {
background-color: #6c6cff;
color: #fff;
}

View File

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

View File

@ -1,6 +1,6 @@
/* src/pages/ProfilePage.css */
/* Overlay */
/* Overlay que cobre toda a tela */
.profile-overlay {
position: fixed;
inset: 0;
@ -8,318 +8,171 @@
display: flex;
align-items: center;
justify-content: center;
z-index: 20000;
z-index: 20000; /* acima de header, vlibras, botões de acessibilidade */
padding: 20px;
box-sizing: border-box;
}
/* Modal */
/* Card central (estilo modal amplo parecido com a 4ª foto) */
.profile-modal {
background: #ffffff;
border-radius: 12px;
padding: 20px;
width: min(600px, 96%);
max-width: 600px;
border-radius: 10px;
padding: 18px;
width: min(1100px, 96%);
max-width: 1100px;
box-shadow: 0 18px 60px rgba(0, 0, 0, 0.5);
position: relative;
box-sizing: border-box;
overflow: visible;
}
/* Botão fechar */
/* Botão fechar (X) no canto do card */
.profile-close {
position: absolute;
top: 15px;
right: 15px;
top: 14px;
right: 14px;
background: none;
border: none;
font-size: 24px;
font-size: 26px;
color: #666;
cursor: pointer;
padding: 5px;
line-height: 1;
}
.profile-close:hover {
color: #333;
}
/* Layout */
/* Conteúdo dividido em 2 colunas: esquerda avatar / direita infos */
.profile-content {
display: flex;
gap: 30px;
gap: 28px;
align-items: flex-start;
padding: 20px 10px;
padding: 22px 18px;
}
/* Avatar */
/* Coluna esquerda - avatar */
.profile-left {
width: 160px;
width: 220px;
display: flex;
justify-content: center;
}
/* Avatar quadrado com sombra (estilo da foto 4) */
.avatar-wrapper {
position: relative;
width: 140px;
height: 140px;
width: 180px;
height: 180px;
}
.avatar-square {
width: 100%;
height: 100%;
border-radius: 8px;
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;
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);
}
/* 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;
padding: 8px 9px;
border-radius: 50%;
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
box-shadow: 0 6px 14px rgba(0,0,0,0.18);
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s ease;
font-size: 0.95rem;
line-height: 1;
}
.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 */
/* Coluna direita - informações */
.profile-right {
flex: 1;
min-width: 250px;
min-width: 280px;
display: flex;
flex-direction: column;
justify-content: center;
}
/* Nome e botão de editar inline */
.profile-name-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
margin-bottom: 10px;
}
.profile-username {
margin: 0;
font-size: 1.8rem;
font-size: 1.9rem;
color: #222;
font-weight: 600;
}
.profile-edit-inline {
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
font-size: 1.05rem;
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: 5px 8px;
border: 2px solid #007bff;
padding: 6px 8px;
border: 1px solid #e0e0e0;
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: 8px 0;
margin: 6px 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: 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;
gap: 12px;
margin-top: 18px;
}
/* botões */
.btn {
padding: 10px 20px;
border-radius: 6px;
border: none;
padding: 8px 14px;
border-radius: 8px;
border: 1px solid transparent;
cursor: pointer;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
font-size: 0.95rem;
}
.btn-close {
background: #f0f0f0;
color: #222;
border: 1px solid #e6e6e6;
}
.btn-close:hover {
background: #e0e0e0;
}
.btn-clear {
background: #dc3545;
color: white;
}
.btn-clear:hover {
background: #c82333;
}
/* Responsividade */
@media (max-width: 680px) {
/* responsividade */
@media (max-width: 880px) {
.profile-content {
flex-direction: column;
gap: 20px;
gap: 14px;
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;
}
}
@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;
.profile-left { width: 100%; }
.avatar-wrapper { width: 140px; height: 140px; }
.profile-right { width: 100%; text-align: center; }
}

View File

@ -1,13 +1,3 @@
.table-doctor-container {
line-height: 2.5;
}
/* Adiciona responsividade para a tabela */
@media (max-width: 992px) {
.table-doctor-card {
overflow-x: auto;
}
}
.table-doctor-container {
line-height: 2.5;
@ -59,7 +49,7 @@
background-color: rgba(0, 0, 0, 0.025);
}
/* Badges */
.specialty-badge {
background-color: #1e3a8a !important;
color: white !important;
@ -68,6 +58,8 @@
font-weight: 500;
}
.results-badge {
background-color: #1e3a8a;
color: white;
@ -83,6 +75,7 @@
font-size: 0.75em;
}
.btn-view {
background-color: #E6F2FF !important;
color: #004085 !important;
@ -122,6 +115,7 @@
border-color: #ED969E;
}
.advanced-filters {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
@ -138,6 +132,7 @@
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);
@ -148,6 +143,7 @@
font-weight: 600;
}
.filter-especialidade {
min-width: 180px !important;
max-width: 200px;
@ -164,6 +160,7 @@
padding: 0.375rem 0.75rem;
}
.filtros-basicos {
display: flex;
flex-wrap: wrap;
@ -171,16 +168,6 @@
gap: 0.75rem;
}
@media (max-width: 576px) {
.table-doctor-card .card-header { padding: 0.75rem 1rem; }
.table-doctor-table th, .table-doctor-table td { padding: 8px 6px; }
.table-doctor-table thead th:nth-child(2),
.table-doctor-table thead th:nth-child(4),
.table-doctor-table tbody td:nth-child(2),
.table-doctor-table tbody td:nth-child(4) { display: none; }
.filter-buttons-container { width: 100%; }
.filter-btn { width: 100%; }
}
@media (max-width: 768px) {
.table-doctor-table {
@ -220,6 +207,7 @@
}
}
.empty-state {
padding: 2rem;
text-align: center;
@ -236,6 +224,7 @@
padding: 0.4em 0.65em;
}
.table-doctor-table tbody tr {
transition: background-color 0.15s ease-in-out;
}
@ -245,115 +234,3 @@
.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,13 +1,3 @@
.table-paciente-container {
line-height: 2.5;
}
/* Adiciona responsividade para a tabela */
@media (max-width: 992px) {
.table-paciente-card {
overflow-x: auto;
}
}
.table-paciente-container {
line-height: 2.5;
@ -59,6 +49,7 @@
background-color: rgba(0, 0, 0, 0.025);
}
.insurance-badge {
background-color: #6c757d !important;
color: white !important;
@ -90,6 +81,7 @@
font-size: 0.75em;
}
.btn-view {
background-color: #E6F2FF !important;
color: #004085 !important;
@ -129,6 +121,7 @@
border-color: #ED969E;
}
.advanced-filters {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
@ -155,6 +148,7 @@
font-weight: 600;
}
.empty-state {
padding: 2rem;
text-align: center;
@ -171,6 +165,7 @@
padding: 0.4em 0.65em;
}
.table-paciente-table tbody tr {
transition: background-color 0.15s ease-in-out;
}
@ -181,6 +176,7 @@
transition: all 0.15s ease-in-out;
}
@media (max-width: 768px) {
.table-paciente-table {
font-size: 0.875rem;
@ -217,7 +213,6 @@
margin-left: 0 !important;
}
}
.compact-select {
font-size: 1.0rem;
padding: 0.45rem 0.5rem;
@ -232,130 +227,8 @@
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;
}
@media (max-width: 576px) {
.table-paciente-card .card-header { padding: 0.75rem 1rem; }
.table-paciente-table th, .table-paciente-table td { padding: 8px 6px; }
.table-paciente-table thead th:nth-child(2),
.table-paciente-table thead th:nth-child(4),
.table-paciente-table tbody td:nth-child(2),
.table-paciente-table tbody td:nth-child(4) { display: none; }
.table-paciente-filters .btn-sm { width: 100%; }
}

View File

@ -1,446 +0,0 @@
.dashboard-paciente-container {
padding: 2rem;
background-color: #f7f9fc;
flex-grow: 1;
min-height: 100vh;
}
/* Header - Paciente */
.dashboard-paciente-header {
margin-bottom: 2rem;
}
.dashboard-paciente-header h1 {
font-size: 2rem;
font-weight: 600;
color: #333;
margin-bottom: 0.5rem;
}
.dashboard-paciente-header p {
font-size: 1rem;
color: #666;
}
/* Estatísticas - Paciente */
.stats-paciente-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.stat-paciente-card {
background-color: #fff;
border-radius: 12px;
padding: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.stat-paciente-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
}
.stat-paciente-info {
display: flex;
flex-direction: column;
}
.stat-paciente-label {
font-size: 0.75rem;
font-weight: 600;
color: #888;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-paciente-value {
font-size: 2.2rem;
font-weight: 700;
color: #444;
}
.stat-paciente-icon-wrapper {
width: 55px;
height: 55px;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
}
.stat-paciente-icon {
font-size: 1.4rem;
color: #fff;
}
/* Cores dos ícones - Paciente */
.stat-paciente-icon-wrapper.blue { background-color: #051AFF; }
.stat-paciente-icon-wrapper.green { background-color: #5F5DF2; }
.stat-paciente-icon-wrapper.purple { background-color: #a272ff; }
.stat-paciente-icon-wrapper.orange { background-color: #0065FF; }
/* Ações Rápidas - Paciente */
.quick-actions-paciente h2 {
font-size: 1.3rem;
font-weight: 600;
color: #333;
margin-bottom: 1.5rem;
}
.actions-paciente-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.action-paciente-button {
background-color: #fff;
border-radius: 12px;
padding: 1.5rem;
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.2s ease-in-out;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.action-paciente-button:hover {
transform: translateY(-5px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.12);
}
.action-paciente-icon {
font-size: 2.5rem;
margin-right: 1.2rem;
color: #5d5dff;
}
.action-paciente-info {
display: flex;
flex-direction: column;
}
.action-paciente-title {
font-size: 1.05rem;
font-weight: 600;
color: #444;
margin-bottom: 0.25rem;
}
.action-paciente-desc {
font-size: 0.85rem;
color: #888;
}
/* Próximas Consultas - Paciente */
.proximas-consultas-section {
background-color: #fff;
border-radius: 12px;
padding: 2rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05);
}
.proximas-consultas-section h2 {
font-size: 1.3rem;
font-weight: 600;
color: #333;
margin-bottom: 1.5rem;
}
/* Lista de Consultas - Paciente */
.consultas-paciente-list {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.consulta-paciente-item {
background: linear-gradient(135deg, #f9fafb 0%, #ffffff 100%);
border-left: 5px solid #5d5dff;
border-radius: 10px;
padding: 1.25rem 1.5rem;
transition: all 0.3s ease;
}
.consulta-paciente-item:hover {
background: linear-gradient(135deg, #f0f2f5 0%, #fafbfc 100%);
transform: translateX(8px);
box-shadow: 0 4px 12px rgba(93, 93, 255, 0.15);
}
.consulta-paciente-info {
display: flex;
align-items: center;
gap: 2rem;
flex-wrap: wrap;
}
.consulta-paciente-time-date {
display: flex;
flex-direction: column;
align-items: center;
min-width: 90px;
padding: 0.5rem;
background-color: #f0f2ff;
border-radius: 8px;
}
.consulta-paciente-hora {
font-size: 1.5rem;
font-weight: 700;
color: #5d5dff;
margin: 0;
line-height: 1.2;
}
.consulta-paciente-data {
font-size: 0.8rem;
font-weight: 500;
color: #7777aa;
margin: 0;
margin-top: 0.25rem;
text-transform: capitalize;
}
.consulta-paciente-detalhes {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
min-width: 250px;
}
.consulta-paciente-medico {
font-size: 1rem;
color: #444;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.consulta-icon {
color: #5d5dff;
font-size: 1.1rem;
}
.consulta-paciente-medico strong {
font-weight: 600;
color: #333;
}
.consulta-paciente-especialidade {
font-size: 0.85rem;
color: #666;
margin: 0;
margin-left: 1.6rem;
font-style: italic;
}
.consulta-paciente-status {
font-size: 0.75rem;
font-weight: 600;
padding: 0.5rem 1rem;
border-radius: 20px;
text-transform: uppercase;
white-space: nowrap;
}
.consulta-paciente-status.status-scheduled {
background-color: #e3f2fd;
color: #1976d2;
}
.consulta-paciente-status.status-pending {
background-color: #fff3e0;
color: #f57c00;
}
.consulta-paciente-status.status-requested {
background-color: #f3e5f5;
color: #7b1fa2;
}
/* Sem Consultas */
.no-consultas-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 3rem 1rem;
}
.no-consultas-icon {
font-size: 4rem;
color: #bbb;
margin-bottom: 1.5rem;
}
.no-consultas-content p {
font-size: 1.1rem;
color: #666;
margin-bottom: 2rem;
}
.agendar-paciente-button,
.view-all-paciente-button {
background-color: #5d5dff;
color: #fff;
border: none;
border-radius: 8px;
padding: 0.875rem 2rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.agendar-paciente-button:hover,
.view-all-paciente-button:hover {
background-color: #4444ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(93, 93, 255, 0.3);
}
.view-all-paciente-button {
width: 100%;
margin-top: 1rem;
background-color: #f0f2f5;
color: #5d5dff;
border: 2px solid #5d5dff;
}
.view-all-paciente-button:hover {
background-color: #5d5dff;
color: #fff;
}
/* Dark Mode - Paciente */
html[data-bs-theme="dark"] .dashboard-paciente-container {
background-color: #121212;
color: #e0e0e0;
}
html[data-bs-theme="dark"] .dashboard-paciente-header h1,
html[data-bs-theme="dark"] .dashboard-paciente-header p,
html[data-bs-theme="dark"] .quick-actions-paciente h2,
html[data-bs-theme="dark"] .proximas-consultas-section h2,
html[data-bs-theme="dark"] .action-paciente-title,
html[data-bs-theme="dark"] .stat-paciente-value {
color: #e0e0e0;
}
html[data-bs-theme="dark"] .stat-paciente-card,
html[data-bs-theme="dark"] .action-paciente-button,
html[data-bs-theme="dark"] .proximas-consultas-section {
background-color: #1e1e1e;
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
html[data-bs-theme="dark"] .stat-paciente-label,
html[data-bs-theme="dark"] .action-paciente-desc,
html[data-bs-theme="dark"] .no-consultas-content p {
color: #b0b0b0;
}
html[data-bs-theme="dark"] .consulta-paciente-item {
background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%);
border-left-color: #6c6cff;
}
html[data-bs-theme="dark"] .consulta-paciente-item:hover {
background: linear-gradient(135deg, #333 0%, #252525 100%);
box-shadow: 0 4px 12px rgba(108, 108, 255, 0.2);
}
html[data-bs-theme="dark"] .consulta-paciente-time-date {
background-color: #2a2a3a;
}
html[data-bs-theme="dark"] .consulta-paciente-hora {
color: #8888ff;
}
html[data-bs-theme="dark"] .consulta-paciente-data {
color: #9999cc;
}
html[data-bs-theme="dark"] .consulta-paciente-medico,
html[data-bs-theme="dark"] .consulta-paciente-especialidade {
color: #d0d0d0;
}
html[data-bs-theme="dark"] .consulta-paciente-medico strong {
color: #e0e0e0;
}
html[data-bs-theme="dark"] .consulta-icon,
html[data-bs-theme="dark"] .action-paciente-icon {
color: #8888ff;
}
html[data-bs-theme="dark"] .consulta-paciente-status.status-scheduled {
background-color: #1a3a52;
color: #64b5f6;
}
html[data-bs-theme="dark"] .consulta-paciente-status.status-pending {
background-color: #3d2817;
color: #ffb74d;
}
html[data-bs-theme="dark"] .consulta-paciente-status.status-requested {
background-color: #2d1f3d;
color: #ba68c8;
}
html[data-bs-theme="dark"] .no-consultas-icon {
color: #666;
}
html[data-bs-theme="dark"] .agendar-paciente-button {
background-color: #6c6cff;
}
html[data-bs-theme="dark"] .agendar-paciente-button:hover {
background-color: #5555dd;
}
html[data-bs-theme="dark"] .view-all-paciente-button {
background-color: #2a2a2a;
color: #8888ff;
border-color: #6c6cff;
}
html[data-bs-theme="dark"] .view-all-paciente-button:hover {
background-color: #6c6cff;
color: #fff;
}
/* Responsivo */
@media (max-width: 768px) {
.dashboard-paciente-container {
padding: 1rem;
}
.consulta-paciente-info {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.consulta-paciente-time-date {
width: 100%;
flex-direction: row;
justify-content: space-around;
}
}

View File

@ -14,13 +14,10 @@ import DoctorEditPage from "../../pages/DoctorEditPage";
import UserDashboard from '../../PagesAdm/gestao.jsx';
import PainelAdministrativo from '../../PagesAdm/painel.jsx';
import admItems from "../../data/sidebar-items-adm.json";
import {useState} from "react"
// ...restante do código...
function Perfiladm() {
const [DictInfo, setDictInfo] = useState({})
return (
<div id="app" className="active">
@ -30,12 +27,12 @@ function Perfiladm() {
<Route path="/" element={<UserDashboard />} />
<Route path="/pacientes/cadastro" element={<PatientCadastroManager />} />
<Route path="/medicos/cadastro" element={<DoctorCadastroManager />} />
<Route path="/pacientes" element={<TablePaciente setDictInfo={setDictInfo}/>} />
<Route path="/medicos" element={<DoctorTable setDictInfo={setDictInfo} />} />
<Route path="/pacientes/details" element={<Details DictInfo={DictInfo} />} />
<Route path="/pacientes/edit" element={<EditPage DictInfo={DictInfo} />} />
<Route path="/medicos/details" element={<DoctorDetails DictInfo={DictInfo}/>} />
<Route path="/medicos/edit" element={<DoctorEditPage DictInfo={DictInfo}/>} />
<Route path="/pacientes" element={<TablePaciente />} />
<Route path="/medicos" element={<DoctorTable />} />
<Route path="/pacientes/:id" element={<Details />} />
<Route path="/pacientes/:id/edit" element={<EditPage />} />
<Route path="/medicos/:id" element={<DoctorDetails />} />
<Route path="/medicos/:id/edit" element={<DoctorEditPage />} />
<Route path="/agendamento" element={<Agendamento />} />
<Route path="/laudo" element={<LaudoManager />} />

Some files were not shown because too many files have changed in this diff Show More