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 # .env
# Cole o token de acesso aqui # Cole o token de acesso aqui
WHATSAPP_TOKEN=EAAVZA9C5Lx9IBP0kF76Yy5GJquZCOkQZCtnsLDYJZCLRfZA7BrOsZBPBk7BODsDuU1r5qYNu5vsRFlI1tNZBlnQpWXsZCZBrkqTygGphqQLZCvikGDyZBEFEyknkWM9oadz1xVtAA65JKXFbGFIJWhmFMOgauWXZC072CSkApe5UZCVGZCZAqc5we1TqCcFBvLqWnUexosBRIEb8kSThWlEDheHNoP7MrjwNcYaNBczmFmhq9aPqKm6jCgjwqjZBI0jVLjdooKkZCanaz9ZA3ZBIfNbyq8FOYUI WHATSAPP_TOKEN=EAAVZA9C5Lx9IBPjITD8IZCZCeGRBIACX9PInHcNHxuhmp5vK7t40Yn0kc9ZC4YeKx1ZC69tnc1MtcQFWCptQimDvQIIvugiw7BNdi0ak1COfBmIZAMAkzskVkk5qhG9WnMsVmZBEoy9AXcbI53vbqSQooZCCN7LkOhbigZCaZC3VqfLnrmIzKZBC0QhzdSzTpvfQYHocDAzCS8ejf2o6WVSXYlqJEOuLzFEkvtGR6eLvNQi6QZDZD
# Cole o ID do número de telefone aqui # Cole o ID do número de telefone aqui
WHATSAPP_PHONE_NUMBER_ID=806117442588831 WHATSAPP_PHONE_NUMBER_ID=806117442588831

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": { "dependencies": {
"@ckeditor/ckeditor5-build-classic": "^41.4.2", "@ckeditor/ckeditor5-build-classic": "^41.4.2",
"@ckeditor/ckeditor5-react": "^11.0.0", "@ckeditor/ckeditor5-react": "^11.0.0",
"@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", "@sweetalert2/theme-dark": "^5.0.27",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0", "@testing-library/jest-dom": "^6.8.0",
@ -28,17 +18,10 @@
"apexcharts": "^5.3.4", "apexcharts": "^5.3.4",
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1", "bootstrap-icons": "^1.13.1",
"cors": "^2.8.5", "dayjs": "^1.11.18",
"dayjs": "^1.11.19",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"firebase": "^12.5.0",
"flatpickr": "^4.6.13", "flatpickr": "^4.6.13",
"helmet": "^8.1.0",
"html2pdf.js": "^0.12.1", "html2pdf.js": "^0.12.1",
"lucide-react": "^0.543.0", "lucide-react": "^0.543.0",
"node-fetch": "^3.3.2",
"openai": "^6.7.0",
"perfect-scrollbar": "^1.5.6", "perfect-scrollbar": "^1.5.6",
"powershell": "^2.3.3", "powershell": "^2.3.3",
"quill": "^2.0.3", "quill": "^2.0.3",
@ -50,11 +33,9 @@
"react-flatpickr": "^4.0.11", "react-flatpickr": "^4.0.11",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"react-is": "^19.2.0",
"react-quill": "^2.0.0", "react-quill": "^2.0.0",
"react-router-dom": "^7.9.2", "react-router-dom": "^7.9.2",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-toastify": "^11.0.5",
"recharts": "^3.1.2", "recharts": "^3.1.2",
"sweetalert2": "^11.22.4", "sweetalert2": "^11.22.4",
"tiptap": "^1.32.2", "tiptap": "^1.32.2",
@ -91,8 +72,5 @@
"sass": "^1.91.0", "sass": "^1.91.0",
"sass-loader": "^16.0.5", "sass-loader": "^16.0.5",
"tailwindcss": "^4.1.13" "tailwindcss": "^4.1.13"
},
"overrides": {
"react": "$react"
} }
} }

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

View File

@ -1,9 +1,14 @@
import React from "react"; import React from "react";
import "./gestao.css"; import "./gestao.css";
import { FaEdit, FaTrash } from "react-icons/fa";
function UserDashboard() { function UserDashboard() {
return ( return (
<div className="dashboard-container"> <div className="dashboard-container">
<div className="dashboard-header"> <div className="dashboard-header">
<div> <div>
<h1 className="dashboard-title">Gestão de Usuários</h1> <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><span className="status-badge ativo">Ativo</span></td>
<td>20/12/2024, 08:30</td> <td>20/12/2024, 08:30</td>
<td className="actions"> <td className="actions">
<button className="action-btn detalhes">Ver Detalhes</button> <span className="action-icon"></span>
<button className="action-btn editar">Editar</button> <span className="action-icon"></span>
<button className="action-btn excluir">Excluir</button>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -99,9 +103,8 @@ function UserDashboard() {
<td><span className="status-badge ativo">Ativo</span></td> <td><span className="status-badge ativo">Ativo</span></td>
<td>19/12/2024, 14:20</td> <td>19/12/2024, 14:20</td>
<td className="actions"> <td className="actions">
<button className="action-btn detalhes">Ver Detalhes</button> <span className="action-icon"></span>
<button className="action-btn editar">Editar</button> <span className="action-icon"></span>
<button className="action-btn excluir">Excluir</button>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -112,9 +115,8 @@ function UserDashboard() {
<td><span className="status-badge ativo">Ativo</span></td> <td><span className="status-badge ativo">Ativo</span></td>
<td>20/12/2024, 07:45</td> <td>20/12/2024, 07:45</td>
<td className="actions"> <td className="actions">
<button className="action-btn detalhes">Ver Detalhes</button> <span className="action-icon"></span>
<button className="action-btn editar">Editar</button> <span className="action-icon"></span>
<button className="action-btn excluir">Excluir</button>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -125,9 +127,8 @@ function UserDashboard() {
<td><span className="status-badge inativo">Inativo</span></td> <td><span className="status-badge inativo">Inativo</span></td>
<td>15/12/2024, 16:30</td> <td>15/12/2024, 16:30</td>
<td className="actions"> <td className="actions">
<button className="action-btn detalhes">Ver Detalhes</button> <span className="action-icon"></span>
<button className="action-btn editar">Editar</button> <span className="person-badge-fill"></span>
<button className="action-btn excluir">Excluir</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -137,4 +138,5 @@ function UserDashboard() {
); );
} }
export default 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 { useAuth } from '../components/utils/AuthProvider';
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient'; import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor'; import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js'; import html2pdf from 'html2pdf.js';
import TiptapViewer from './TiptapViewer'; import TiptapViewer from './TiptapViewer';
import './styleMedico/DoctorRelatorioManager.css';
const DoctorRelatorioManager = () => { const DoctorRelatorioManager = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth(); const { getAuthorizationHeader } = useAuth();
const authHeader = getAuthorizationHeader(); const authHeader = getAuthorizationHeader();
const [RelatoriosFiltrados, setRelatorios] = useState([]);
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]); const [PacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]); const [MedicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false);
const [relatorioModal, setRelatorioModal] = useState(null); const [index, setIndex] = useState();
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);
// busca lista de relatórios
useEffect(() => { useEffect(() => {
let mounted = true;
const fetchReports = async () => { const fetchReports = async () => {
try { try {
const myHeaders = new Headers(); var myHeaders = new Headers();
myHeaders.append('apikey', API_KEY); myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader); myHeaders.append('Authorization', authHeader);
const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' }; var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
const res = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*", requestOptions);
let userId = null; const data = await res.json();
let userFullName = null; setRelatorios(data || []);
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;
}
} catch (err) { } catch (err) {
console.warn('Não foi possível obter UserInfos (pode não estar logado):', err); console.error('Erro listar relatórios', err);
} setRelatorios([]);
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([]);
}
} }
}; };
fetchReports(); fetchReports();
const refreshHandler = () => fetchReports();
window.addEventListener('reports:refresh', refreshHandler);
return () => {
mounted = false;
window.removeEventListener('reports:refresh', refreshHandler);
};
}, [authHeader]); }, [authHeader]);
// depois que RelatoriosFiltrados mudar, busca pacientes e médicos correspondentes
useEffect(() => { useEffect(() => {
const fetchRelData = async () => { const fetchRelData = async () => {
const pacientes = []; const pacientes = [];
const medicos = []; const medicos = [];
for (let i = 0; i < relatoriosFinais.length; i++) { for (let i = 0; i < RelatoriosFiltrados.length; i++) {
const rel = relatoriosFinais[i]; const rel = RelatoriosFiltrados[i];
// paciente
try { try {
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader); const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes); pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
} catch (err) { } catch (err) {
pacientes.push(null); pacientes.push(null);
} }
// médico: tenta created_by ou requested_by id se existir
try { try {
if (rel.doctor_id) { const doctorId = rel.created_by || rel.requested_by || null;
const docRes = await GetDoctorByID(rel.doctor_id, authHeader); 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); 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 { } else {
medicos.push({ full_name: '' }); medicos.push({ full_name: rel.requested_by || '' });
} }
} catch (err) { } catch (err) {
medicos.push({ full_name: rel.requested_by || '' }); medicos.push({ full_name: rel.requested_by || '' });
@ -177,120 +70,55 @@ const DoctorRelatorioManager = () => {
setPacientesComRelatorios(pacientes); setPacientesComRelatorios(pacientes);
setMedicosComRelatorios(medicos); setMedicosComRelatorios(medicos);
}; };
if (RelatoriosFiltrados.length > 0) fetchRelData();
if (relatoriosFinais.length > 0) fetchRelData();
else { else {
setPacientesComRelatorios([]); setPacientesComRelatorios([]);
setMedicosComRelatorios([]); setMedicosComRelatorios([]);
} }
}, [relatoriosFinais, authHeader]); }, [RelatoriosFiltrados, authHeader]);
const abrirModal = (relatorio, pageIndex) => { const BaixarPDFdoRelatorio = (nome_paciente) => {
const globalIndex = relatoriosFinais.findIndex(r => r.id === relatorio.id); const elemento = document.getElementById("folhaA4");
const indexToUse = globalIndex >= 0 ? globalIndex : (indiceInicial + pageIndex); const opt = { margin: 0, filename: `relatorio_${nome_paciente || "paciente"}.pdf`, html2canvas: { scale: 2 }, jsPDF: { unit: "mm", format: "a4", orientation: "portrait" } };
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" }
};
html2pdf().set(opt).from(elemento).save(); html2pdf().set(opt).from(elemento).save();
}; };
const irParaPagina = (pagina) => {
setPaginaAtual(pagina);
};
const avancarPagina = () => {
if (paginaAtual < totalPaginas) {
setPaginaAtual(paginaAtual + 1);
}
};
const voltarPagina = () => {
if (paginaAtual > 1) {
setPaginaAtual(paginaAtual - 1);
}
};
const gerarNumerosPaginas = () => {
const paginas = [];
const paginasParaMostrar = 5;
let inicio = Math.max(1, paginaAtual - Math.floor(paginasParaMostrar / 2));
let fim = Math.min(totalPaginas, inicio + paginasParaMostrar - 1);
inicio = Math.max(1, fim - paginasParaMostrar + 1);
for (let i = inicio; i <= fim; i++) {
paginas.push(i);
}
return paginas;
};
return ( return (
<div> <div>
{showModal && ( {showModal && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1"> <div className="modal">
<div className="modal-dialog modal-dialog-centered modal-lg"> <div className="modal-dialog modal-tabela-relatorio">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}> <div className="modal-header text-white">
<h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name}</h5> <h5 className="modal-title ">Relatório de {PacientesComRelatorios[index]?.full_name}</h5>
<button type="button" className="btn-close" onClick={() => setShowModal(false)}></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">
<div id={`folhaA4-${modalIndex}`} className="folhaA4"> <div id="folhaA4">
<div id='header-relatorio' style={{ textAlign: 'center', marginBottom: 24 }}> <div id='header-relatorio'>
<p style={{ margin: 0 }}>Clinica Rise up</p> <p>Clinica Rise up</p>
<p style={{ margin: 0 }}>Dr - CRM/SP 123456</p> <p>Dr - CRM/SP 123456</p>
<p style={{ margin: 0 }}>Avenida - (79) 9 4444-4444</p> <p>Avenida - (79) 9 4444-4444</p>
</div> </div>
<div id='infoPaciente' style={{ padding: '0 6px' }}> <div id='infoPaciente'>
<p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name}</p> <p>Paciente: {PacientesComRelatorios[index]?.full_name}</p>
<p><strong>Data de nascimento:</strong> {pacientesComRelatorios[modalIndex]?.birth_date || '—'}</p> <p>Data de nascimento: {PacientesComRelatorios[index]?.birth_date}</p>
<p><strong>Data do exame:</strong> {relatoriosFinais[modalIndex]?.due_at || '—'}</p> <p>Data do exame: {RelatoriosFiltrados[index]?.due_at || ''}</p>
{/* Exibe conteúdo salvo (content_html) */}
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p> <p style={{ marginTop: '15px', fontWeight: 'bold' }}>Conteúdo do Relatório:</p>
<div className="tiptap-viewer-wrapper"> <TiptapViewer htmlContent={RelatoriosFiltrados[index]?.content_html || RelatoriosFiltrados[index]?.content || 'Relatório não preenchido.'} />
<TiptapViewer htmlContent={relatoriosFinais[modalIndex]?.content_html || relatoriosFinais[modalIndex]?.content || 'Relatório não preenchido.'} />
</div>
</div> </div>
<div style={{ marginTop: 20, padding: '0 6px' }}> <div>
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFinais[modalIndex]?.requested_by}</p> <p>Dr {MedicosComRelatorios[index]?.full_name || RelatoriosFiltrados[index]?.requested_by}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatoriosFinais[modalIndex]?.created_at || '—'}</p> <p>Emitido em: {RelatoriosFiltrados[index]?.created_at || '—'}</p>
</div> </div>
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name, modalIndex)}> <button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name)}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button>
<i className='bi bi-file-pdf-fill me-1'></i> Baixar em PDF <button type="button" className="btn btn-primary" onClick={() => { setShowModal(false) }}>Fechar</button>
</button>
<button type="button" className="btn btn-secondary" onClick={() => { setShowModal(false) }}>
Fechar
</button>
</div> </div>
</div> </div>
</div> </div>
@ -305,51 +133,14 @@ const DoctorRelatorioManager = () => {
<div className="card-header d-flex justify-content-between align-items-center"> <div className="card-header d-flex justify-content-between align-items-center">
<h4 className="card-title mb-0">Relatórios Cadastrados</h4> <h4 className="card-title mb-0">Relatórios Cadastrados</h4>
<Link to={'criar'}> <Link to={'criar'}>
<button className="btn btn-primary"> <button className="btn btn-primary"><i className="bi bi-plus-circle"></i> Adicionar Relatório</button>
<i className="bi bi-plus-circle"></i> Adicionar Relatório
</button>
</Link> </Link>
</div> </div>
<div className="card-body"> <div className="card-body">
<div className="card p-3 mb-3"> <div className="card p-3 mb-3">
<h5 className="mb-3"> <h5 className="mb-3"><i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros</h5>
<i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros <div className="d-flex flex-nowrap align-items-center gap-2" style={{ overflowX: "auto", paddingBottom: "6px" }}>
</h5> <input type="text" className="form-control" placeholder="Buscar por nome..." style={{ minWidth: 250, maxWidth: 300, width: 260, flex: "0 0 auto" }} />
<div className="row">
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Buscar por nome ou CPF do paciente</label>
<input
type="text"
className="form-control"
placeholder="Digite nome ou CPF do paciente..."
value={termoPesquisa}
onChange={(e) => setTermoPesquisa(e.target.value)}
/>
</div>
</div>
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Filtrar por tipo de exame</label>
<input
type="text"
className="form-control"
placeholder="Digite o tipo de exame..."
value={filtroExame}
onChange={(e) => setFiltroExame(e.target.value)}
/>
</div>
</div>
<div className="col-md-2 d-flex align-items-end">
<button className="btn btn-outline-secondary w-100" onClick={limparFiltros}>
<i className="bi bi-arrow-clockwise"></i> Limpar
</button>
</div>
</div>
<div className="mt-2">
<div className="contador-relatorios">
{relatoriosFinais.length} DE {relatoriosOriginais.length} RELATÓRIOS ENCONTRADOS
</div>
</div> </div>
</div> </div>
@ -358,92 +149,36 @@ const DoctorRelatorioManager = () => {
<thead> <thead>
<tr> <tr>
<th>Paciente</th> <th>Paciente</th>
<th>CPF</th> <th>Doutor</th>
<th>Exame</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{relatoriosPaginados.length > 0 ? ( {RelatoriosFiltrados.length > 0 ? (
relatoriosPaginados.map((relatorio, index) => { RelatoriosFiltrados.map((relatorio, idx) => (
const globalIndex = relatoriosFinais.findIndex(r => r.id === relatorio.id);
const paciente = pacientesComRelatorios[globalIndex];
return (
<tr key={relatorio.id}> <tr key={relatorio.id}>
<td>{paciente?.full_name || 'Carregando...'}</td> <td className='infos-paciente'>{PacientesComRelatorios[idx]?.full_name}</td>
<td>{paciente?.cpf || 'Carregando...'}</td> <td className='infos-paciente'>{MedicosComRelatorios[idx]?.full_name || relatorio.requested_by || '-'}</td>
<td>{relatorio.exam}</td>
<td> <td>
<div className="d-flex gap-2"> <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 <i className="bi bi-eye me-1"></i> Ver Detalhes
</button> </button>
<button className="btn btn-sm btn-editar" onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<button className="btn btn-sm" style={{ backgroundColor: "#FFF3CD", color: "#856404" }} onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<i className="bi bi-pencil me-1"></i> Editar <i className="bi bi-pencil me-1"></i> Editar
</button> </button>
</div> </div>
</td> </td>
</tr> </tr>
); ))
})
) : ( ) : (
<tr><td colSpan="4" className="text-center">Nenhum relatório encontrado.</td></tr> <tr><td colSpan="8" className="text-center">Nenhum paciente encontrado.</td></tr>
)} )}
</tbody> </tbody>
</table> </table>
{relatoriosFinais.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div> </div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, relatoriosFinais.length)} de {relatoriosFinais.length} itens
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -1,25 +1,23 @@
// src/PagesMedico/FormNovoRelatorio.jsx
import React, { useEffect, useState, useRef } from 'react'; 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 API_KEY from '../components/utils/apiKeys';
import { useAuth } from '../components/utils/AuthProvider'; import { useAuth } from '../components/utils/AuthProvider';
import TiptapEditor from './TiptapEditor'; import TiptapEditor from './TiptapEditor';
import { GetAllPatients } from '../components/utils/Functions-Endpoints/Patient'; import { GetAllPatients, GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { GetAllDoctors } from '../components/utils/Functions-Endpoints/Doctor'; import { GetAllDoctors, GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
import './styleMedico/FormNovoRelatorio.css'; import './styleMedico/FormNovoRelatorio.css';
const FormNovoRelatorio = () => { const FormNovoRelatorio = () => {
const { getAuthorizationHeader } = useAuth(); const { getAuthorizationHeader } = useAuth();
const authHeader = getAuthorizationHeader(); const authHeader = getAuthorizationHeader();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const [patients, setPatients] = useState([]); const [patients, setPatients] = useState([]);
const [doctors, setDoctors] = useState([]); const [doctors, setDoctors] = useState([]);
const [loadingPatients, setLoadingPatients] = useState(true); const [loadingPatients, setLoadingPatients] = useState(true);
const [loadingDoctors, setLoadingDoctors] = useState(true); const [loadingDoctors, setLoadingDoctors] = useState(true);
// formulário
const [form, setForm] = useState({ const [form, setForm] = useState({
patient_id: '', patient_id: '',
patient_name: '', patient_name: '',
@ -29,15 +27,19 @@ const FormNovoRelatorio = () => {
contentHtml: '', contentHtml: '',
}); });
// campos de busca (texto)
const [patientQuery, setPatientQuery] = useState(''); const [patientQuery, setPatientQuery] = useState('');
const [doctorQuery, setDoctorQuery] = useState(''); const [doctorQuery, setDoctorQuery] = useState('');
// dropdown control
const [showPatientDropdown, setShowPatientDropdown] = useState(false); const [showPatientDropdown, setShowPatientDropdown] = useState(false);
const [showDoctorDropdown, setShowDoctorDropdown] = useState(false); const [showDoctorDropdown, setShowDoctorDropdown] = useState(false);
const patientRef = useRef(); const patientRef = useRef();
const doctorRef = useRef(); const doctorRef = useRef();
const [lockedFromAppointment, setLockedFromAppointment] = useState(false);
useEffect(() => { useEffect(() => {
// carregar pacientes
let mounted = true; let mounted = true;
const loadPatients = async () => { const loadPatients = async () => {
setLoadingPatients(true); setLoadingPatients(true);
@ -66,6 +68,7 @@ const FormNovoRelatorio = () => {
return () => { mounted = false; }; return () => { mounted = false; };
}, [authHeader]); }, [authHeader]);
// fechar dropdowns quando clicar fora
useEffect(() => { useEffect(() => {
const handleClick = (e) => { const handleClick = (e) => {
if (patientRef.current && !patientRef.current.contains(e.target)) setShowPatientDropdown(false); 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) => { const choosePatient = async (patient) => {
setForm(prev => ({ setForm(prev => ({
...prev, ...prev,
patient_id: patient.id, patient_id: patient.id,
patient_name: patient.full_name || '', patient_name: patient.full_name || '',
patient_birth: patient.birth_date || '', patient_birth: patient.birth_date || '',
contentHtml: generateTemplate(patient.full_name || '', patient.birth_date || '', prev.doctor_name) contentHtml: generateTemplate(patient.full_name || '', patient.birth_date || '', form.doctor_name)
})); }));
setPatientQuery(''); setPatientQuery('');
setShowPatientDropdown(false); setShowPatientDropdown(false);
@ -116,12 +120,13 @@ const FormNovoRelatorio = () => {
...prev, ...prev,
doctor_id: doctor.id, doctor_id: doctor.id,
doctor_name: doctor.full_name || '', 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(''); setDoctorQuery('');
setShowDoctorDropdown(false); setShowDoctorDropdown(false);
}; };
// filtrar pela query (startsWith)
const filteredPatients = patientQuery const filteredPatients = patientQuery
? patients.filter(p => (p.full_name || '').toLowerCase().startsWith(patientQuery.toLowerCase())).slice(0, 40) ? 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 })); const handleEditorChange = (html) => setForm(prev => ({ ...prev, contentHtml: html }));
useEffect(() => { // salvar novo relatório
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]);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
if (!form.patient_id) return alert('Selecione o paciente (clicando no item) antes de salvar.'); if (!form.patient_id) return alert('Selecione o paciente (clicando no item) antes de salvar.');
@ -158,61 +146,35 @@ const FormNovoRelatorio = () => {
try { try {
const myHeaders = new Headers(); const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY); myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader); myHeaders.append('Authorization', authHeader);
myHeaders.append('Content-Type', 'application/json'); 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, patient_id: form.patient_id,
content: form.contentHtml,
content_html: form.contentHtml, content_html: form.contentHtml,
requested_by: form.doctor_name || '' requested_by: form.doctor_name || '',
}; created_by: form.doctor_id || null,
status: 'draft'
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';
const res = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports', { const res = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports', {
method: 'POST', method: 'POST',
headers: myHeaders, headers: myHeaders,
body: JSON.stringify(payload), body,
}); });
if (!res.ok) { if (!res.ok) {
let txt; const txt = await res.text();
try {
txt = await res.json();
} catch {
txt = await res.text();
}
console.error('Erro POST criar relatório:', res.status, txt); 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!'); alert('Relatório criado com sucesso!');
navigate('/medico/relatorios'); navigate('/medico/relatorios');
} catch (err) { } 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.'); alert('Erro ao salvar relatório. Veja console.');
} }
}; };
@ -223,18 +185,17 @@ const FormNovoRelatorio = () => {
<form onSubmit={handleSubmit} className="card p-4 mb-4"> <form onSubmit={handleSubmit} className="card p-4 mb-4">
<div className="row g-3 align-items-end"> <div className="row g-3 align-items-end">
<div className="col-md-6" ref={patientRef} style={{ position: 'relative' }}> <div className="col-md-6" ref={patientRef}>
<label className="form-label">Buscar paciente (digite para filtrar)</label> <label className="form-label">Buscar paciente (digite para filtrar)</label>
<input <input
className="form-control" className="form-control"
placeholder="Comece a digitar (ex.: m para pacientes que começam com m)" placeholder="Comece a digitar (ex.: m para pacientes que começam com m)"
value={lockedFromAppointment ? form.patient_name : patientQuery} value={patientQuery}
onChange={(e) => { if (!lockedFromAppointment) { setPatientQuery(e.target.value); setShowPatientDropdown(true); } }} onChange={(e) => { setPatientQuery(e.target.value); setShowPatientDropdown(true); }}
onFocus={() => { if (!lockedFromAppointment) setShowPatientDropdown(true); }} onFocus={() => setShowPatientDropdown(true)}
disabled={lockedFromAppointment}
/> />
{!lockedFromAppointment && showPatientDropdown && patientQuery && ( {showPatientDropdown && patientQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}> <ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '45%' }}>
{filteredPatients.length > 0 ? filteredPatients.map(p => ( {filteredPatients.length > 0 ? filteredPatients.map(p => (
<li key={p.id} className="list-group-item list-group-item-action" onClick={() => choosePatient(p)}> <li key={p.id} className="list-group-item list-group-item-action" onClick={() => choosePatient(p)}>
{p.full_name} {p.cpf ? `- ${p.cpf}` : ''} {p.full_name} {p.cpf ? `- ${p.cpf}` : ''}
@ -245,18 +206,17 @@ const FormNovoRelatorio = () => {
<div className="form-text">Clique no paciente desejado para selecioná-lo e preencher o template.</div> <div className="form-text">Clique no paciente desejado para selecioná-lo e preencher o template.</div>
</div> </div>
<div className="col-md-6" ref={doctorRef} style={{ position: 'relative' }}> <div className="col-md-6" ref={doctorRef}>
<label className="form-label">Buscar médico (digite para filtrar)</label> <label className="form-label">Buscar médico (digite para filtrar)</label>
<input <input
className="form-control" className="form-control"
placeholder="Comece a digitar o nome do médico" placeholder="Comece a digitar o nome do médico"
value={lockedFromAppointment ? form.doctor_name : doctorQuery} value={doctorQuery}
onChange={(e) => { if (!lockedFromAppointment) { setDoctorQuery(e.target.value); setShowDoctorDropdown(true); } }} onChange={(e) => { setDoctorQuery(e.target.value); setShowDoctorDropdown(true); }}
onFocus={() => { if (!lockedFromAppointment) setShowDoctorDropdown(true); }} onFocus={() => setShowDoctorDropdown(true)}
disabled={lockedFromAppointment}
/> />
{!lockedFromAppointment && showDoctorDropdown && doctorQuery && ( {showDoctorDropdown && doctorQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}> <ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '45%', right: 0 }}>
{filteredDoctors.length > 0 ? filteredDoctors.map(d => ( {filteredDoctors.length > 0 ? filteredDoctors.map(d => (
<li key={d.id} className="list-group-item list-group-item-action" onClick={() => chooseDoctor(d)}> <li key={d.id} className="list-group-item list-group-item-action" onClick={() => chooseDoctor(d)}>
{d.full_name} {d.crm ? `- CRM ${d.crm}` : ''} {d.full_name} {d.crm ? `- CRM ${d.crm}` : ''}

View File

@ -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 --- */ /* --- Posiciona a barra de busca corretamente --- */
.busca-atendimento { .busca-atendimento {
display: flex; display: flex;
align-items: center; align-items: center; /* Alinha os itens verticalmente */
margin-top: 20px; margin-top: 20px; /* Espaço acima da barra de busca */
padding: 0 10px; padding: 0 10px; /* Adiciona um padding lateral para alinhar com o resto */
gap: 15px; gap: 15px;
} }
.busca-atendimento > div:first-child { .busca-atendimento > div:first-child {
width: 400px; width: 400px; /* Define um tamanho para a barra de pesquisa */
display: flex; display: flex;
align-items: center; 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 { .busca-atendimento input {
margin-left: 8px; margin-left: 8px;
border-radius: 8px; border-radius: 8px;
@ -166,20 +126,13 @@
} }
.container-btns-agenda-fila_esepera { .container-btns-agenda-fila_esepera {
margin-top: 30px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between;
align-items: flex-end;
gap: 20px; gap: 20px;
margin-left: 20px;
margin-top: 0;
margin-bottom: 0;
margin-left: 0;
padding: 0 20px 0;
border-bottom: 1px solid #edf1f7;
} }
.btn-fila-espera, .btn-fila-espera,
.btn-agenda { .btn-agenda {
background-color: transparent; background-color: transparent;
@ -187,7 +140,6 @@
border-bottom: 3px solid transparent; border-bottom: 3px solid transparent;
padding: 8px; padding: 8px;
border-radius: 10px 10px 0px 0px; border-radius: 10px 10px 0px 0px;
color: #fff;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
} }
@ -277,9 +229,3 @@ html[data-bs-theme="dark"] .legenda-item-agendado {
border: 3px solid #4d4d2e; border: 3px solid #4d4d2e;
color: #f7f7c4; 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 */ #primeiraLinha{
.modal-dialog.modal-dialog-square {
width: 880px; /* largura aumentada */
max-width: 96vw;
height: auto; /* deixa altura automática (melhor para conteúdo longo) */
max-height: 92vh;
margin: 0;
display: flex; display: flex;
align-items: center; flex-direction: row;
justify-content: center; gap: 20px;
padding: 0; margin-bottom: 20px;
} }
/* caixa branca que contém o conteúdo - ocupa 100% da dialog */ input,textarea,label{
.modal-dialog.modal-dialog-square .modal-content { font-size: 1.1rem;
}
textarea{
width: 100%; width: 100%;
height: auto; height: 100px;
border-radius: 12px;
box-shadow: 0 12px 30px rgba(11,22,35,0.18); }
overflow: hidden;
.submitButton{
display: flex; display: flex;
flex-direction: column; margin-left: auto;
background: #fff; height:50% ;
padding: 8px 20px;
font-size: medium;
} }
/* header */ .bi-download{
.custom-modal-header { font-size: 1.2rem;
position: relative; margin-right: 5px;
background: linear-gradient(90deg, #203B75 0%, #274A8A 100%); font-weight: bold;
color: #fff;
padding: 14px 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
} }
.custom-modal-header .modal-title { #infoPaciente{
margin: 0; margin-top: 50px;
font-size: 1.05rem; margin-bottom: 40px;
font-weight: 700;
} }
/* botão fechar no header — sem quadrado branco por trás */ #header-relatorio{
.modal-close-btn {
background: transparent !important;
border: none;
width: 40px;
height: 40px;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: none;
outline: none;
position: relative;
z-index: 5;
}
.modal-close-btn::after {
content: '✕';
color: #fff;
font-weight: 700;
font-size: 16px;
}
/* body - faz scroll interno se for longo */
.modal-body {
padding: 18px;
overflow: auto;
flex: 1 1 auto;
}
/* footer */
.custom-modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 14px 18px;
border-top: 1px solid #eee;
background: #fafafa;
}
/* folhaA4 dentro da modal — adapta para caber */
.folhaA4 {
width: 100%;
box-sizing: border-box;
background: transparent;
padding: 0;
}
/* melhor espaçamento e leitura do conteúdo */
#header-relatorio p {
color: #374151;
margin: 6px 0;
text-align: center; text-align: center;
margin-bottom: 30px;
} }
#infoPaciente p { .info-paciente{
margin: 10px 0; font-weight: bold;
color: #3d4650;
}
/* tornar o viewer responsivo */
.tiptap-viewer-wrapper {
border: 1px dashed #e7e7e7;
padding: 12px;
margin-top: 10px;
background: #fff;
}
/* barra de scroll customizada (opcional) */
.modal-body::-webkit-scrollbar {
width: 10px;
}
.modal-body::-webkit-scrollbar-thumb {
background: rgba(100,100,100,0.18);
border-radius: 8px;
}
.modal-body::-webkit-scrollbar-track {
background: rgba(0,0,0,0.02);
}
/* responsividade para telas pequenas: mantém centralizado, ajusta proporção */
@media (max-width: 680px) {
.modal-dialog.modal-dialog-square {
width: 92vw;
height: 86vh;
}
.modal-close-btn { width: 36px; height: 36px; }
.custom-modal-footer { padding: 10px; }
.modal-body { padding: 12px; }
} }

View File

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

View File

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

View File

@ -1,630 +1,155 @@
import React, { useState, useMemo, useEffect } from 'react'; import React from 'react'
import { useNavigate } from 'react-router-dom'; import "./style.css"
import API_KEY from '../components/utils/apiKeys.js'; import CardConsultaPaciente from './CardConsultaPaciente'
import AgendamentoCadastroManager from '../pages/AgendamentoCadastroManager.jsx'; import { useNavigate } from 'react-router-dom'
import { useAuth } from '../components/utils/AuthProvider.js'; import { useEffect, useState } from 'react'
import dayjs from 'dayjs'; import API_KEY from '../components/utils/apiKeys'
import 'dayjs/locale/pt-br'; import { useAuth } from '../components/utils/AuthProvider'
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';
dayjs.locale('pt-br'); const ConsultasPaciente = ({setConsulta}) => {
dayjs.extend(isBetween); const {getAuthorizationHeader} = useAuth()
dayjs.extend(localeData);
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 [consultas, setConsultas] = useState([])
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 [currentDate, setCurrentDate] = useState(dayjs()); const FiltrarAgendamentos = (agendamentos, id) => {
const [selectedDay, setSelectedDay] = useState(dayjs()); // Verifica se a lista de agendamentos é válida antes de tentar filtrar
const [quickJump, setQuickJump] = useState({ if (!agendamentos || !Array.isArray(agendamentos)) {
month: currentDate.month(), console.error("A lista de agendamentos é inválida.");
year: currentDate.year(), setConsultas([]); // Garante que setConsultas receba uma lista vazia
});
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.');
return; return;
} }
if (!patientId) { // 1. Filtragem
console.warn('patientId ainda não carregado, aguardando contexto.'); // O método .filter() cria uma nova lista contendo apenas os itens que retornarem 'true'
return; 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
setIsLoading(true); return agendamento.patient_id && agendamento.patient_id.toString() === id.toString();
try {
const myHeaders = new Headers({
Authorization: authHeader,
apikey: API_KEY,
}); });
const requestOptions = { method: 'GET', headers: myHeaders };
const response = await fetch( // 2. Adicionar a lista no setConsultas
`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?select=*,doctors(full_name)&patient_id=eq.${patientId}`, console.log(consultasFiltradas)
requestOptions setConsultas(consultasFiltradas);
); }
if (!response.ok) // Exemplo de como você chamaria (assumindo que DadosAgendamento é sua lista original):
throw new Error(`Erro na requisição: ${response.statusText}`); // FiltrarAgendamentos(DadosAgendamento, Paciente.id);
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];
}
}
}
for (const key in newDict) {
newDict[key].sort((a, b) =>
a.scheduled_at.localeCompare(b.scheduled_at)
);
}
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(() => { useEffect(() => {
carregarDados(); var myHeaders = new Headers();
}, [authHeader, patientId]); // padrão recomendado para fetch com useEffect [web:46][web:82] myHeaders.append("Authorization", authHeader);
myHeaders.append("apikey", API_KEY)
const updateAppointmentStatus = async (id, updates) => { var requestOptions = {
const myHeaders = new Headers({ method: 'GET',
Authorization: authHeader, headers: myHeaders,
apikey: API_KEY, redirect: 'follow'
'Content-Type': 'application/json', };
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', method: 'PATCH',
headers: myHeaders, headers: myHeaders,
body: JSON.stringify(updates), body: raw,
redirect: 'follow'
}; };
try { fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${selectedID}`, requestOptions)
const response = await fetch( .then(response => {if(response.status !== 200)(console.log(response))})
`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments?id=eq.${id}`, .then(result => console.log(result))
requestOptions .catch(error => console.log('error', error));
);
if (!response.ok) throw new Error('Falha ao atualizar o status.'); console.log("deletar", ID)
return true;
} catch (error) {
console.error('Erro de rede/servidor:', error);
return false;
} }
};
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 ( return (
<div> <div>
<h1>Minhas consultas</h1> <h1> Gerencie suas consultas</h1>
<div
className="btns-gerenciamento-e-consulta" <div className='form-container'>
style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}
> <button className="btn btn-primary" onClick={() => {navigate("criar")}}>
<button <i className="bi bi-plus-circle"></i> Adicionar Consulta
className="btn btn-primary btn-consulta-paciente"
onClick={() => {
setPageConsulta(true);
setFiladeEspera(false);
}}
>
<i className="bi bi-plus-circle"></i> Solicitar Agendamento
</button> </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 ? ( {consultas.map((consulta) => (
<div className="atendimento-eprocura"> <CardConsultaPaciente consulta={consulta} setConsulta={setConsulta} setShowDeleteModal={setShowDeleteModal} setSelectedId={ setSelectedId}/>
<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>
<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> {showDeleteModal &&
<select <div className="modal-dialog modal-dialog-centered">
value={quickJump.year} <div className="modal-content">
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);
}}
/>
)}
{isCancelModalOpen && ( <div className="modal-header bg-danger bg-opacity-25">
<div className="modal-overlay"> <h5 className="modal-title text-danger">
<div className="modal-content" style={{ maxWidth: '400px' }}> Confirmação de Exclusão
<div </h5>
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>
<button <button
className="close-button" type="button"
onClick={() => setIsCancelModalOpen(false)} className="btn-close"
style={{ onClick={() => setShowDeleteModal(false)}
background: 'none', ></button>
border: 'none',
fontSize: '1.5rem',
cursor: 'pointer',
}}
>
&times;
</button>
</div> </div>
<div className="modal-body" style={{ padding: '20px' }}>
<p>Qual o motivo do cancelamento?</p> <div className="modal-body">
<textarea <p className="mb-0 fs-5">
value={cancellationReason} Tem certeza que deseja excluir este agendamento?
onChange={(e) => setCancellationReason(e.target.value)} </p>
placeholder="Ex: Precisei viajar, motivo pessoal, etc."
rows="4"
style={{
width: '100%',
padding: '10px',
resize: 'none',
border: '1px solid #ccc',
borderRadius: '4px',
}}
></textarea>
</div> </div>
<div
className="modal-footer" <div className="modal-footer">
style={{
display: 'flex',
justifyContent: 'flex-end',
gap: '10px',
padding: '15px',
borderTop: '1px solid #eee',
}}
>
<button <button
className="btn btn-secondary" type="button"
onClick={() => setIsCancelModalOpen(false)} className="btn btn-primary"
style={{ onClick={() => setShowDeleteModal(false)}
backgroundColor: '#6c757d',
color: 'white',
border: 'none',
padding: '8px 15px',
borderRadius: '4px',
}}
> >
Cancelar Cancelar
</button> </button>
<button <button
type="button"
className="btn btn-danger" className="btn btn-danger"
onClick={executeCancellation} onClick={() => {deleteConsulta(selectedID);setShowDeleteModal(false)}}
style={{
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
padding: '8px 15px',
borderRadius: '4px',
}}
> >
<Trash2 size={16} style={{ marginRight: '5px' }} /> Excluir <i className="bi bi-trash me-1"></i> Excluir
</button> </button>
</div> </div>
</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) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
alert("Agendamento salvo!"); alert("Agendamento salvo!");
navigate("/paciente/agendamento")
onSave({...agendamento, horarioInicio:horarioInicio}) onSave({...agendamento, horarioInicio:horarioInicio})
}; };
@ -178,7 +178,11 @@ const handleSubmit = (e) => {
<form className="form-agendamento" onSubmit={handleSubmit}> <form className="form-agendamento" onSubmit={handleSubmit}>
1
<h2 className="section-title">Informações do atendimento</h2> <h2 className="section-title">Informações do atendimento</h2>
<div className="campo-informacoes-atendimento"> <div className="campo-informacoes-atendimento">
<div className="campo-de-input-container"> {/* NOVO CONTAINER PAI */} <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 */ /* Estilo geral do card para agrupar e dar um formato */
.card-consulta { .card-consulta {
background-color: #007bff; background-color: #007bff; /* Um tom de azul padrão */
display: flex; display: flex; /* Para colocar horário e info lado a lado */
border-radius: 10px; border-radius: 10px; /* Cantos arredondados */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Sombra suave */
overflow: hidden; overflow: hidden; /* Garante que o fundo azul não 'vaze' */
/* width: 280px; /* Largura de exemplo */
margin: 20px; margin: 20px;
font-family: Arial, sans-serif; font-family: Arial, sans-serif; /* Fonte legível */
}
@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;
}
} }
/* 1. Estilo para o Horário (Fundo Azul e Texto Branco/Grande) */ /* 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 */ background-color: #c82333; /* Um vermelho um pouco mais escuro para o hover */
filter: brightness(90%); /* Alternativa: escurecer um pouco mais */ 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,121 +4,88 @@ import { useAuth } from '../utils/AuthProvider';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useMemo } from 'react'; import { useMemo } from 'react';
import "./style/card-consulta.css" import "./style/card-consulta.css"
const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, setDictInfo, setSelectedId, setShowConfirmModal, corModal, selectedID, coresConsultas, setListaConsultaID, listaConsultasID} ) => { const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, setDictInfo, setSelectedId} ) => {
const navigate = useNavigate(); const navigate = useNavigate();
const {getAuthorizationHeader} = useAuth() const {getAuthorizationHeader} = useAuth()
const authHeader = getAuthorizationHeader() const authHeader = getAuthorizationHeader()
const [Paciente, setPaciente] = useState() const [Paciente, setPaciente] = useState()
const [Medico, setMedico] = useState() const [Medico, setMedico] = useState()
const [decidirBotton, setDecidirBotton] = useState("") const ids = useMemo(() => {
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 ( return (
<div className={`container-cardconsulta container-cardconsulta-${TabelaAgendamento} ` }> <div className={`container-cardconsulta container-cardconsulta-${TabelaAgendamento}`}>
{DadosConsulta.id? {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> <div>
<section className='cardconsulta-infosecundaria'> <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>
<section className='cardconsulta-infoprimaria'> <section className='cardconsulta-infoprimaria'>
<p>Paciente: {nameArrayPaciente && nameArrayPaciente.length > 0 ? nameArrayPaciente[0] : ''} {nameArrayPaciente && nameArrayPaciente.length > 1 ? ` ${nameArrayPaciente[1]}` : ''}- {} <p>{nameArrayPaciente && nameArrayPaciente.length > 0 ? nameArrayPaciente[0] : ''} {nameArrayPaciente && nameArrayPaciente.length > 1 ? ` ${nameArrayPaciente[1]}` : ''}- {}</p>
</p>
</section> </section>
</div> </div>
<div className='actions-container'> <div className='actions-container'>
<button className="btn btn-sm btn-edit-custom" <button className="btn btn-sm btn-edit-custom"
onClick={() => {
navigate(`edit`); onClick={() => {navigate(`2/edit`)
console.log(DadosConsulta); setDictInfo({...DadosConsulta,paciente_cpf:Paciente.cpf, paciente_nome:Paciente.full_name, nome_medico:Medico.full_name})
setDictInfo({
...DadosConsulta,
paciente_cpf: DadosConsulta?.paciente_cpf,
paciente_nome: DadosConsulta?.paciente_nome,
nome_medico: DadosConsulta?.medico_nome
});
}} }}
> >
<i className="bi bi-pencil me-1"></i> <i className="bi bi-pencil me-1"></i>
</button> </button>
{indice_cor !== -1 ? (
// Caso o ID esteja na lista
<>
{coresConsultas[indice_cor] === "cancelled" ?
<button <button
className="btn btn-sm btn-confirm-style" className="btn btn-sm btn-delete-custom-style "
onClick={() => { onClick={() => {
console.log(DadosConsulta.id); 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); setSelectedId(DadosConsulta.id);
setShowDeleteModal(true); setShowDeleteModal(true);
}} }}
> >
<i className="bi bi-trash me-1"></i> <i className="bi bi-trash me-1"></i>
</button> </button>
</div>
}
</>
) : (
// 🧩 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> </div>
: :
@ -127,8 +94,6 @@ const CardConsulta = ( {DadosConsulta, TabelaAgendamento, setShowDeleteModal, se
} }
</div> </div>
) )
} }

View File

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

View File

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

View File

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

View File

@ -4,42 +4,20 @@ import "./style/styleTabelas/tabelasemana.css";
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useEffect, useState, useMemo } from 'react'; import { useEffect, useState, useMemo } from 'react';
import weekOfYear from 'dayjs/plugin/weekOfYear' import weekOfYear from 'dayjs/plugin/weekOfYear'
import Spinner from '../Spinner';
dayjs.extend(weekOfYear) 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 // Armazena o objeto COMPLETO das semanas organizadas
const [semanasOrganizadas, setSemanasOrganizadas] = useState({}); const [semanasOrganizadas, setSemanasOrganizadas] = useState({});
// Controla qual semana está sendo exibida (o índice da chave no objeto) // Controla qual semana está sendo exibida (o índice da chave no objeto)
const [Indice, setIndice] = useState(0); const [Indice, setIndice] = useState(0);
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 dataHoje = dayjs();
const AnoAtual = dataHoje.year(); const AnoAtual = dataHoje.year();
const mes = dataHoje.month() + 1; const mes = dataHoje.month() + 1;
let DiasdoMes = ListarDiasdoMes(AnoAtual, mes)
// Array de chaves (ex: ['semana40', 'semana41', ...]) // Array de chaves (ex: ['semana40', 'semana41', ...])
const chavesDasSemanas = Object.keys(semanasOrganizadas); const chavesDasSemanas = Object.keys(semanasOrganizadas);
@ -68,34 +46,30 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
segunda: [], terça: [], quarta: [], quinta: [], sexta: [] segunda: [], terça: [], quarta: [], quinta: [], sexta: []
} }
} }
console.log(diaSemana)
switch (diaSemana) { switch (diaSemana) {
case 'Monday':
case 'segunda-feira':
console.log("segunda")
semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento])
break break
case 'terça-feira': case 'Tuesday':
semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento])
break break
case 'quarta-feira': case 'Wednesday':
semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento])
break break
case 'quinta-feira': case 'Thursday':
semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento])
break break
case 'sexta-feira': case 'Friday':
semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento]) semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento])
break break
default: default:
break break
} }
} }
console.log(semanas, "agendamentos semanais")
return semanas return semanas
}, [agendamentos, AnoAtual]) }, [agendamentos, AnoAtual]) // Adicionei AnoAtual como dependência por segurança
// --- EFEITO PARA POPULAR O ESTADO --- // --- EFEITO PARA POPULAR O ESTADO ---
@ -149,10 +123,10 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
? `Semana ${chaveDaSemanaAtual.replace('semana', '')} / ${AnoAtual}` ? `Semana ${chaveDaSemanaAtual.replace('semana', '')} / ${AnoAtual}`
: 'Nenhuma semana encontrada'; : 'Nenhuma semana encontrada';
// --- RENDERIZAÇÃO ---
return ( return (
<div> <div>
{/* Container de Navegação */}
<div id='tabela-seletor-container'> <div id='tabela-seletor-container'>
<button <button
@ -185,65 +159,54 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
<tbody> <tbody>
{indicesDeLinha.map((indiceLinha) => { {indicesDeLinha.map((indiceLinha) => {
//let schedulet_at = semanaParaRenderizar.segunda[indiceLinha].scheduled_at.split("T") let schedulet_at = semanaParaRenderizar.segunda[indiceLinha].scheduled_at.split("T")
// let horario = schedulet_at[1].split(":") let horario = schedulet_at[1].split(":")
console.log(horario)
console.log(semanaParaRenderizar, "aqui")
return( return(
<tr key={indiceLinha}> <tr key={indiceLinha}>
{/* Célula para Horário (Pode ser ajustado para mostrar o horário real) */}
<td> <td>
{/* <p className='horario-texto'> {`${horario[0]}:${horario[1]}`} </p>*/} <p className='horario-texto'> {`${horario[0]}:${horario[1]}`} </p>
</td> </td>
{/* Mapeamento de COLUNAS (dias) */} {/* Mapeamento de COLUNAS (dias) */}
<td> <td>
{semanaParaRenderizar?.segunda[indiceLinha] {semanaParaRenderizar.segunda[indiceLinha]
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar?.segunda[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/> ? <CardConsulta DadosConsulta={semanaParaRenderizar.segunda[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />
: null : null
} }
</td> </td>
<td> <td>
{semanaParaRenderizar.terça[indiceLinha] {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 : null
} }
</td> </td>
<td> <td>
{semanaParaRenderizar.quarta[indiceLinha] {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 : null
} }
</td> </td>
<td> <td>
{semanaParaRenderizar.quinta[indiceLinha] {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 : null
} }
</td> </td>
<td> <td>
{semanaParaRenderizar.sexta[indiceLinha] {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 : null
} }
</td> </td>
</tr> </tr>
)})} )})}
{showSpinner &&
<tr>
<td colspan='6'>
<Spinner/>
</td>
</tr>
}
</tbody> </tbody>
</table> </table>
</div> </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 { .actions-container {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -57,84 +50,12 @@
/* 6. Estilo de hover para o botão de exclusão */ /* 6. Estilo de hover para o botão de exclusão */
.btn-delete-custom-style:hover { .btn-delete-custom-style:hover {
background-color: #c82333; /* Um vermelho um pouco mais escuro para o hover */ background-color: #c82333; /* Um vermelho um pouco mais escuro para o hover */
filter: brightness(90%); /* Alternativa: escurecer um pouco mais */
} }
/* 7. Estilos para os ícones dentro dos botões (já está no JSX com fs-4) */ /* 7. Estilos para os ícones dentro dos botões (já está no JSX com fs-4) */
/* .fs-4 do Bootstrap já cuida do tamanho do ícone. Se precisar de mais controle, adicione aqui. */ /* .fs-4 do Bootstrap já cuida do tamanho do ícone. Se precisar de mais controle, adicione aqui. */
.action-button .bi {
/* Exemplo: se precisar de um ajuste fino além do fs-4 */
.btn-confirm-style{ /* font-size: 1.5rem; */
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;
}
} }

View File

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

View File

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

View File

@ -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 { useAuth } from "./utils/AuthProvider";
import API_KEY from "./utils/apiKeys"; import API_KEY from "./utils/apiKeys";
import "./AgendarConsulta/style/formagendamentos.css"; 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 ENDPOINT_CRIAR_EXCECAO = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_exceptions";
const FormCriarExcecao = ({ onCancel, doctorID }) => { const FormCriarExcecao = ({ onCancel, doctorID }) => {
const { getAuthorizationHeader, user, getUserInfo } = useAuth(); const { getAuthorizationHeader, user, getUserInfo } = useAuth();
const [dadosAtendimento, setDadosAtendimento] = useState({ const [dadosAtendimento, setDadosAtendimento] = useState({
profissional: doctorID || '', profissional: doctorID || '',
@ -18,13 +18,6 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
motivo: '' 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 handleAtendimentoChange = (e) => {
const { value, name } = e.target; const { value, name } = e.target;
setDadosAtendimento(prev => ({ 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) => { const handleSubmitExcecao = async (e) => {
e.preventDefault(); e.preventDefault();
console.log("Tentando criar Exceção."); console.log("Tentando criar Exceção.");
@ -90,13 +37,6 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
return; 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 startTime = inicio ? inicio + ":00" : null;
const endTime = termino ? termino + ":00" : null; const endTime = termino ? termino + ":00" : null;
@ -130,7 +70,7 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
const raw = JSON.stringify({ const raw = JSON.stringify({
doctor_id: profissional, doctor_id: profissional,
date: dataAtendimento, date: dataAtendimento,
kind: mappedKind, kind: tipoAtendimento,
start_time: startTime, start_time: startTime,
end_time: endTime, end_time: endTime,
reason: motivo, reason: motivo,
@ -179,30 +119,7 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
<h2 className="section-title">Informações da Nova Exceção</h2> <h2 className="section-title">Informações da Nova Exceção</h2>
<div className="campo-informacoes-atendimento"> <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"> <div className="campo-de-input">
<label>ID do profissional *</label> <label>ID do profissional *</label>
<input <input
@ -217,11 +134,12 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
<div className="campo-de-input"> <div className="campo-de-input">
<label>Tipo de exceção *</label> <label>Tipo de exceção *</label>
<select name="tipoAtendimento" onChange={handleAtendimentoChange} value={dadosAtendimento.tipoAtendimento} required> <select name="tipoAtendimento" onChange={handleAtendimentoChange} value={dadosAtendimento.tipoAtendimento} required>
<option value="" disabled>Selecione o tipo de exceção</option> <option value="">Selecione o tipo de exceção</option>
<option value="disponibilidade_extra" >Liberação</option> <option value="liberacao" >Liberação (Criar Slot)</option>
<option value="bloqueio" >Bloqueio</option> <option value="bloqueio" >Bloqueio (Remover Slot)</option>
</select> </select>
</div> </div>
</div> </div>
<section id="informacoes-atendimento-segunda-linha"> <section id="informacoes-atendimento-segunda-linha">

View File

@ -1,61 +1,19 @@
import React, { useState } from 'react' import React from 'react'
import '../PagesMedico/styleMedico/FormNovoRelatorio.css' import '../PagesMedico/styleMedico/FormNovoRelatorio.css'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { useAuth } from '../components/utils/AuthProvider' import { useAuth } from '../components/utils/AuthProvider'
import { GetPatientByCPF } from '../components/utils/Functions-Endpoints/Patient' import { GetPatientByCPF } from '../components/utils/Functions-Endpoints/Patient'
import { FormatCPF } from '../components/utils/Formatar/Format' import { FormatCPF } from '../components/utils/Formatar/Format'
import html2pdf from 'html2pdf.js' import html2pdf from 'html2pdf.js'
const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => { const FormRelatorio = ({onSave, DictInfo, setDictInfo }) => {
const { getAuthorizationHeader } = useAuth() const {getAuthorizationHeader} = useAuth()
let authHeader = getAuthorizationHeader() let authHeader = getAuthorizationHeader()
const navigate = useNavigate() const navigate= useNavigate()
const [showModal, setShowModal] = useState(false) 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 BaixarPDFdoRelatorio = () => {
const elemento = document.getElementById("folhaA4"); // tua div do relatório const elemento = document.getElementById("folhaA4"); // tua div do relatório
const opt = { const opt = {
@ -71,7 +29,7 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
console.log(name, value) console.log(name, value)
if (name === 'paciente_cpf') { if(name === 'paciente_cpf') {
const formattedCPF = FormatCPF(value); const formattedCPF = FormatCPF(value);
setDictInfo((prev) => ({ ...prev, [name]: formattedCPF })); setDictInfo((prev) => ({ ...prev, [name]: formattedCPF }));
@ -80,17 +38,17 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
if (patientData) { if (patientData) {
setDictInfo((prev) => ({ setDictInfo((prev) => ({
...prev, ...prev,
paciente_cpf: value, paciente_cpf:value,
paciente_nome: patientData.full_name, paciente_nome: patientData.full_name,
paciente_id: patientData.id paciente_id: patientData.id
})); }));
} }
}; };
if (formattedCPF.length === 14) { if(formattedCPF.length === 14){
fetchPatient(); fetchPatient();
} }
} else { }else{
setDictInfo((prev) => ({ ...prev, [name]: value })); setDictInfo((prev) => ({ ...prev, [name]: value }));
} }
} }
@ -100,21 +58,25 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
console.log(DictInfo) console.log(DictInfo)
setShowModal(true) setShowModal(true)
onSave({
onSave({
"patient_id": DictInfo.paciente_id, "patient_id": DictInfo.paciente_id,
"exam": DictInfo.exam, "exam": DictInfo.exam,
"diagnosis": DictInfo.diagnostico, // Garanta que o backend espera 'diagnosis' mas seu state usa 'diagnostico' "diagnosis": DictInfo.diagnosis,
"conclusion": DictInfo.conclusao, "conclusion": DictInfo.conclusao,
"status": "draft", "status": "draft",
"requested_by": DictInfo.requested_by, "requested_by": DictInfo.requested_by,
"hide_date": false, "hide_date": false,
"hide_signature": false, "hide_signature": false,
}); });
} }
return ( return (
<div> <div>
{showModal && ( {showModal &&(
<div className="modal" style={{ display: 'block', backgroundColor: 'rgba(0,0,0,0.5)' }}> <div className="modal" style={{ display: 'block', backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog"> <div className="modal-dialog">
<div className="modal-content"> <div className="modal-content">
@ -130,12 +92,12 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
<p>Você também pode baixa-lo agora em pdf</p> <p>Você também pode baixa-lo agora em pdf</p>
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
<button className="btn btn-primary" onClick={BaixarPDFdoRelatorio}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button> <button className="btn btn-primary" onClick={ BaixarPDFdoRelatorio}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button>
<button <button
type="button" type="button"
className="btn btn-primary" className="btn btn-primary"
onClick={() => { setShowModal(false); navigate(('/medico/relatorios')) }} onClick={() => {setShowModal(false); navigate(('/medico/relatorios'))}}
> >
Fechar Fechar
</button> </button>
@ -148,28 +110,6 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
<div className='card'> <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}> <form action="" onSubmit={handleSubmit}>
<div id='primeiraLinha'> <div id='primeiraLinha'>
@ -191,7 +131,7 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
<div className="col-md-2 mb-3"> <div className="col-md-2 mb-3">
<label >Exame:</label> <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> </div>
@ -237,8 +177,7 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
<p>Paciente: {DictInfo?.paciente_nome}</p> <p>Paciente: {DictInfo?.paciente_nome}</p>
<p>Data de nascimento: </p> <p>Data de nascimento: </p>
{/* Corrigi de data_exam para data_exame para bater com o state */} <p>Data do exame: {DictInfo.data_exam}</p>
<p>Data do exame: {DictInfo.data_exame}</p>
<p>Exame: {DictInfo.exam}</p> <p>Exame: {DictInfo.exam}</p>
@ -250,7 +189,7 @@ const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
<div> <div>
<p>Dr {DictInfo.requested_by}</p> <p>Dr {DictInfo.requested_by}</p>
<p>Emitido em: {new Date().toLocaleDateString()}</p> <p>Emitido em: 0</p>
</div> </div>
</div> </div>

View File

@ -20,11 +20,6 @@
font-size: 24px; font-size: 24px;
cursor: pointer; cursor: pointer;
padding: 5px; padding: 5px;
transition: transform 0.2s ease;
}
.phone-icon-container:hover {
transform: scale(1.1);
} }
.phone-icon { .phone-icon {
@ -38,173 +33,75 @@
} }
.profile-picture-container { .profile-picture-container {
width: 45px; width: 40px;
height: 45px; height: 40px;
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
border: 2px solid #007bff; border: 2px solid #ccc;
box-shadow: 0 2px 8px rgba(0, 123, 255, 0.3); box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.profile-picture-container:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.4);
}
.profile-photo {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
display: block;
} }
.profile-placeholder { .profile-placeholder {
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background-color: #A9A9A9;
border-radius: 50%; border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: relative; position: relative;
} }
.placeholder-icon { .profile-placeholder::after {
font-size: 20px; content: '';
color: white; 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 { .profile-dropdown {
position: absolute; position: absolute;
top: 60px; top: 50px;
right: 0; right: 0;
background-color: white; background-color: white;
border: 1px solid #e0e0e0; border: 1px solid #ddd;
border-radius: 12px; border-radius: 5px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 1000; z-index: 1000;
min-width: 180px; min-width: 150px;
overflow: hidden; overflow: hidden;
animation: dropdownFadeIn 0.2s ease-out;
}
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.dropdown-button { .dropdown-button {
background: none; background: none;
border: none; border: none;
padding: 12px 16px; padding: 10px 15px;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
color: #333; color: #333;
transition: background-color 0.2s; transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 8px;
} }
.dropdown-button:hover { .dropdown-button:hover {
background-color: #f8f9fa; background-color: #f0f0f0;
} }
.logout-button { .logout-button {
color: #dc3545; color: #cc0000;
border-top: 1px solid #f0f0f0;
} }
.logout-button:hover { .logout-button:hover {
background-color: #ffe0e0; background-color: #ffe0e0;
} }
/* Modal de Logout */
.logout-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.logout-modal-content {
background-color: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 90%;
text-align: center;
}
.logout-modal-content h3 {
margin-bottom: 1rem;
color: #333;
font-size: 1.25rem;
}
.logout-modal-content p {
margin-bottom: 2rem;
color: #666;
line-height: 1.4;
}
.logout-modal-buttons {
display: flex;
gap: 1rem;
justify-content: center;
}
.logout-cancel-button {
padding: 0.75rem 1.5rem;
border: 1px solid #ccc;
border-radius: 8px;
background-color: transparent;
color: #333;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.logout-cancel-button:hover {
background-color: #f0f0f0;
border-color: #999;
}
.logout-confirm-button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background-color: #dc3545;
color: white;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.logout-confirm-button:hover {
background-color: #c82333;
}
/* Suporte Card */
.suporte-card-overlay { .suporte-card-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@ -223,8 +120,6 @@
z-index: 2001; z-index: 2001;
margin-top: 80px; margin-top: 80px;
margin-right: 20px; margin-right: 20px;
/* Adicionado para responsividade */
max-width: 90vw;
} }
.suporte-card { .suporte-card {
@ -292,7 +187,6 @@
margin-bottom: 0; margin-bottom: 0;
} }
/* Chat Online */
.chat-overlay { .chat-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
@ -311,8 +205,6 @@
z-index: 3001; z-index: 3001;
margin-top: 80px; margin-top: 80px;
margin-right: 20px; margin-right: 20px;
/* Adicionado para responsividade */
max-width: 90vw;
} }
.chat-online { .chat-online {
@ -354,7 +246,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
transition: background-color 0.2s;
} }
.fechar-chat:hover { .fechar-chat:hover {
@ -369,7 +260,6 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
background-color: #fafafa;
} }
.mensagem { .mensagem {
@ -377,53 +267,33 @@
padding: 0.75rem; padding: 0.75rem;
border-radius: 12px; border-radius: 12px;
position: relative; position: relative;
animation: messageSlideIn 0.3s ease-out;
}
@keyframes messageSlideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.mensagem.usuario { .mensagem.usuario {
align-self: flex-end; align-self: flex-end;
background-color: #007bff; background-color: #e3f2fd;
color: white;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
.mensagem.suporte { .mensagem.suporte {
align-self: flex-start; align-self: flex-start;
background-color: white; background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
} }
.mensagem-texto { .mensagem-texto {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
word-wrap: break-word; word-wrap: break-word;
line-height: 1.4;
} }
.mensagem-hora { .mensagem-hora {
font-size: 0.7rem; font-size: 0.7rem;
opacity: 0.8; color: #666;
text-align: right; text-align: right;
} }
.mensagem.usuario .mensagem-hora {
color: rgba(255, 255, 255, 0.8);
}
.mensagem.suporte .mensagem-hora { .mensagem.suporte .mensagem-hora {
text-align: left; text-align: left;
color: #666;
} }
.chat-input { .chat-input {
@ -443,164 +313,93 @@
outline: none; outline: none;
font-size: 0.9rem; font-size: 0.9rem;
background-color: white; background-color: white;
transition: border-color 0.2s;
} }
.chat-campo:focus { .chat-campo:focus {
border-color: #007bff; border-color: #1e3a8a;
} }
.chat-enviar { .chat-enviar {
background-color: #007bff; background-color: #1e3a8a;
color: white; color: white;
border: none; border: none;
padding: 0.75rem 1.5rem; padding: 0.75rem 1rem;
border-radius: 20px; border-radius: 20px;
cursor: pointer; cursor: pointer;
font-size: 0.9rem; 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; transition: background-color 0.2s;
} }
.chat-enviar:hover { .logout-cancel-button:hover {
background-color: #0056b3; 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 { .logout-confirm-button {
padding: 10px 18px; padding: 0.75rem 1.5rem;
border-radius: 8px;
border: none; border: none;
background: #dc3545; border-radius: 8px;
color: #fff; background-color: #dc3545;
color: white;
cursor: pointer; 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 React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom'; import { useNavigate } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import './Header.css'; import './Header.css';
const Header = () => { const Header = () => {
// --- hooks (sempre na mesma ordem) ---
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isSuporteCardOpen, setIsSuporteCardOpen] = useState(false); const [isSuporteCardOpen, setIsSuporteCardOpen] = useState(false);
const [isChatOpen, setIsChatOpen] = useState(false); const [isChatOpen, setIsChatOpen] = useState(false);
const [mensagem, setMensagem] = useState(''); const [mensagem, setMensagem] = useState('');
const [mensagens, setMensagens] = useState([]); const [mensagens, setMensagens] = useState([]);
const [showLogoutModal, setShowLogoutModal] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false);
const [avatarUrl, setAvatarUrl] = useState(null);
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const chatInputRef = useRef(null); const chatInputRef = useRef(null);
const mensagensContainerRef = useRef(null); const mensagensContainerRef = useRef(null);
// foco quando abre chat
useEffect(() => { useEffect(() => {
if (isChatOpen && chatInputRef.current) { if (isChatOpen && chatInputRef.current) {
chatInputRef.current.focus(); chatInputRef.current.focus();
} }
}, [isChatOpen]); }, [isChatOpen]);
// scroll automático quando nova mensagem
useEffect(() => { useEffect(() => {
if (mensagensContainerRef.current) { if (mensagensContainerRef.current) {
mensagensContainerRef.current.scrollTop = mensagensContainerRef.current.scrollHeight; mensagensContainerRef.current.scrollTop = mensagensContainerRef.current.scrollHeight;
} }
}, [mensagens]); }, [mensagens]);
// carrega avatar se existir // Funções de Logout (do seu código)
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) ---
const handleLogoutClick = () => { const handleLogoutClick = () => {
setShowLogoutModal(true); setShowLogoutModal(true);
setIsDropdownOpen(false); 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 () => { const handleLogoutConfirm = async () => {
try { try {
const token = const token =
@ -90,34 +42,57 @@ const Header = () => {
sessionStorage.getItem("authToken"); sessionStorage.getItem("authToken");
if (token) { if (token) {
try { const response = await fetch(
await fetch("https://mock.apidog.com/m1/1053378-0-default/auth/v1/logout", { "https://mock.apidog.com/m1/1053378-0-default/auth/v1/logout",
{
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`, 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(); clearAuthData();
navigate('/login'); navigate("/login");
} catch (err) { } catch (error) {
console.error('Erro no logout:', err); console.error("Erro durante logout:", error);
clearAuthData(); clearAuthData();
navigate('/login'); navigate("/login");
} finally { } finally {
setShowLogoutModal(false); 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); const handleLogoutCancel = () => setShowLogoutModal(false);
// --- profile / suporte / chat handlers ---
const handleProfileClick = () => { const handleProfileClick = () => {
setIsDropdownOpen(!isDropdownOpen); setIsDropdownOpen(!isDropdownOpen);
if (isSuporteCardOpen) setIsSuporteCardOpen(false); if (isSuporteCardOpen) setIsSuporteCardOpen(false);
@ -130,12 +105,14 @@ const Header = () => {
}; };
const handleSuporteClick = () => { const handleSuporteClick = () => {
setIsSuporteCardOpen((s) => !s); setIsSuporteCardOpen(!isSuporteCardOpen);
setIsDropdownOpen(false); if (isDropdownOpen) setIsDropdownOpen(false);
if (isChatOpen) setIsChatOpen(false); if (isChatOpen) setIsChatOpen(false);
}; };
const handleCloseSuporteCard = () => setIsSuporteCardOpen(false); const handleCloseSuporteCard = () => {
setIsSuporteCardOpen(false);
};
const handleChatClick = () => { const handleChatClick = () => {
setIsChatOpen(true); setIsChatOpen(true);
@ -170,7 +147,9 @@ const Header = () => {
setMensagem(''); setMensagem('');
setTimeout(() => { setTimeout(() => {
if (chatInputRef.current) chatInputRef.current.focus(); if (chatInputRef.current) {
chatInputRef.current.focus();
}
}, 0); }, 0);
setTimeout(() => { setTimeout(() => {
@ -181,18 +160,19 @@ const Header = () => {
'Já encaminhei sua solicitação para nossa equipe técnica.', 'Já encaminhei sua solicitação para nossa equipe técnica.',
'Vou ajudar você a resolver isso!' 'Vou ajudar você a resolver isso!'
]; ];
const respostaSuporte = { const respostaSuporte = {
id: Date.now() + 1, id: Date.now() + 1,
texto: respostas[Math.floor(Math.random() * respostas.length)], texto: respostas[Math.floor(Math.random() * respostas.length)],
remetente: 'suporte', remetente: 'suporte',
hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) hora: new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
}; };
setMensagens(prev => [...prev, respostaSuporte]); setMensagens(prev => [...prev, respostaSuporte]);
}, 900); }, 1000);
}; };
// --- subcomponentes (UI) --- const SuporteCard = () => (
const SuporteCardContent = ({ onOpenChat }) => (
<div className="suporte-card"> <div className="suporte-card">
<h2 className="suporte-titulo">Suporte</h2> <h2 className="suporte-titulo">Suporte</h2>
<p className="suporte-subtitulo">Entre em contato conosco através dos canais abaixo</p> <p className="suporte-subtitulo">Entre em contato conosco através dos canais abaixo</p>
@ -211,7 +191,7 @@ const Header = () => {
</div> </div>
</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-info">
<div className="contato-nome">Chat Online</div> <div className="contato-nome">Chat Online</div>
<div className="contato-descricao">Disponível 24/7</div> <div className="contato-descricao">Disponível 24/7</div>
@ -220,11 +200,11 @@ const Header = () => {
</div> </div>
); );
const ChatOnlineContent = ({ mensagens, onSend, onClose }) => ( const ChatOnline = () => (
<div className="chat-online" role="dialog" aria-modal="true"> <div className="chat-online">
<div className="chat-header"> <div className="chat-header">
<h3 className="chat-titulo">Chat de Suporte</h3> <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>
<div className="chat-mensagens" ref={mensagensContainerRef}> <div className="chat-mensagens" ref={mensagensContainerRef}>
@ -236,7 +216,7 @@ const Header = () => {
))} ))}
</div> </div>
<form className="chat-input" onSubmit={onSend}> <form className="chat-input" onSubmit={handleEnviarMensagem}>
<input <input
ref={chatInputRef} ref={chatInputRef}
type="text" type="text"
@ -251,140 +231,20 @@ const Header = () => {
</div> </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 ( return (
<div className="header-container" style={{ pointerEvents: 'auto' }}> <div className="header-container">
<div className="right-corner-elements"> <div className="right-corner-elements">
<div <div className="phone-icon-container" onClick={handleSuporteClick}>
className="phone-icon-container"
onClick={handleSuporteClick}
role="button"
tabIndex={0}
style={{ pointerEvents: 'auto' }}
>
<span className="phone-icon" role="img" aria-label="telefone">📞</span> <span className="phone-icon" role="img" aria-label="telefone">📞</span>
</div> </div>
<div className="profile-section" style={{ pointerEvents: 'auto' }}> <div className="profile-section">
<div className="profile-picture-container" onClick={handleProfileClick} role="button" tabIndex={0}> <div className="profile-picture-container" onClick={handleProfileClick}>
<div className="profile-placeholder"></div> <div className="profile-placeholder"></div>
</div> </div>
{isDropdownOpen && ( {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={handleViewProfile} className="dropdown-button">Ver Perfil</button>
<button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">Sair (Logout)</button> <button type="button" onClick={handleLogoutClick} className="dropdown-button logout-button">Sair (Logout)</button>
</div> </div>
@ -392,21 +252,38 @@ const Header = () => {
</div> </div>
</div> </div>
{/* logout modal via portal */} {/* Modal de Logout */}
{showLogoutModal && <LogoutModalPortal onCancel={handleLogoutCancel} onConfirm={handleLogoutConfirm} />} {showLogoutModal && (
<div className="logout-modal-overlay">
{/* suporte portal */} <div className="logout-modal-content">
{isSuporteCardOpen && ( <h3>Confirmar Logout</h3>
<SuportePortal onClose={handleCloseSuporteCard}> <p>Tem certeza que deseja encerrar a sessão?</p>
<SuporteCardContent onOpenChat={handleChatClick} /> <div className="logout-modal-buttons">
</SuportePortal> <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 && ( {isChatOpen && (
<ChatPortal onClose={handleCloseChat}> <div className="chat-overlay">
<ChatOnlineContent mensagens={mensagens} onSend={handleEnviarMensagem} onClose={handleCloseChat} /> <div className="chat-container">
</ChatPortal> <ChatOnline />
</div>
</div>
)} )}
</div> </div>
); );

View File

@ -1,16 +1,7 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import TrocardePerfis from "./TrocardePerfis";
import MobileMenuToggle from "./MobileMenuToggle"; 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 }) { function Sidebar({ menuItems }) {
const [isActive, setIsActive] = useState(true); const [isActive, setIsActive] = useState(true);
@ -19,24 +10,6 @@ function Sidebar({ menuItems }) {
const [showLogoutModal, setShowLogoutModal] = useState(false); const [showLogoutModal, setShowLogoutModal] = useState(false);
const navigate = useNavigate(); 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 // Detecta se é mobile/tablet
useEffect(() => { useEffect(() => {
const checkScreenSize = () => { const checkScreenSize = () => {
@ -45,7 +18,6 @@ function Sidebar({ menuItems }) {
setIsActive(!mobile); setIsActive(!mobile);
}; };
checkScreenSize(); checkScreenSize();
window.addEventListener("resize", checkScreenSize); window.addEventListener("resize", checkScreenSize);
return () => window.removeEventListener("resize", checkScreenSize); return () => window.removeEventListener("resize", checkScreenSize);
@ -119,7 +91,6 @@ function Sidebar({ menuItems }) {
const handleLogoutCancel = () => setShowLogoutModal(false); const handleLogoutCancel = () => setShowLogoutModal(false);
const renderLink = (item) => { const renderLink = (item) => {
if (item.url && item.url.startsWith("/")) { if (item.url && item.url.startsWith("/")) {
return ( return (
@ -242,41 +213,65 @@ function Sidebar({ menuItems }) {
<div className="sidebar-menu"> <div className="sidebar-menu">
<ul className="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") && if (item.submenu)
<ToggleSidebar perfil={"administrador"} items={admItems} defaultOpen={pathname.includes("admin") } /> return (
} <li
{roleUser.includes("admin") || roleUser.includes("secretaria") ? key={index}
<ToggleSidebar perfil={"secretaria"} items={SecretariaItems} defaultOpen={pathname.includes("secretaria")} /> className={`sidebar-item has-sub ${
: openSubmenu === item.key ? "active" : ""
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">
<button <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} onClick={handleLogoutClick}
> >
<i className="bi bi-box-arrow-right"></i> <i className="bi bi-box-arrow-right"></i>
<span>Sair</span> <span>Sair (Logout)</span>
</button> </button>
</li> </li>
<TrocardePerfis />
</ul> </ul>
</div> </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 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 { UserInfos } from "./utils/Functions-Endpoints/General";
import { useAuth } from "./utils/AuthProvider"; import { useAuth } from "./utils/AuthProvider";
import "./TrocardePerfis.css"; import "../pages/style/TrocardePerfis.css";
const ToggleIcon = ({ isOpen }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={{ transition: 'transform 0.3s', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
);
const TrocardePerfis = () => { const TrocardePerfis = () => {
const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth(); const { getAuthorizationHeader } = useAuth();
const [selectedProfile, setSelectedProfile] = useState("");
const [showProfiles, setShowProfiles] = useState([]); const [showProfiles, setShowProfiles] = useState([]);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const authHeader = getAuthorizationHeader(); const authHeader = getAuthorizationHeader();
try { setSelectedProfile(location.pathname || "");
const userInfo = await UserInfos(authHeader); const userInfo = await UserInfos(authHeader);
setShowProfiles(userInfo?.roles || []); setShowProfiles(userInfo?.roles || []);
} catch (error) {
console.error("Erro ao buscar informações do usuário:", error);
setShowProfiles([]);
}
}; };
fetchData(); fetchData();
}, [getAuthorizationHeader]); }, [location.pathname, getAuthorizationHeader]);
const handleProfileClick = (route) => { const handleSelectChange = (e) => {
if (route) { const route = e.target.value;
navigate(route); setSelectedProfile(route);
setIsOpen(false); if (route) navigate(route);
}
};
const handleToggle = () => {
setIsOpen(prev => !prev);
}; };
const options = [ const options = [
@ -67,47 +40,20 @@ const TrocardePerfis = () => {
); );
return ( return (
<div className="container-perfis-toggle"> <div className="container-perfis">
<p className="acesso-text">Acesso aos módulos:</p>
<div <select
className="toggle-button" className="perfil-select"
onClick={handleToggle} value={selectedProfile}
role="button" onChange={handleSelectChange}
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleToggle();
}
}}
>
<span className="acesso-text">Acesso aos módulos</span>
<ToggleIcon isOpen={isOpen} />
</div>
{isOpen && (
<div className="perfil-list">
{options.length > 0 ? (
options.map((opt) => (
<div
key={opt.key}
className="perfil-item"
onClick={() => handleProfileClick(opt.route)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleProfileClick(opt.route);
}
}}
> >
<option value="">Selecionar perfil</option>
{options.map((opt) => (
<option key={opt.key} value={opt.route}>
{opt.label} {opt.label}
</div> </option>
)) ))}
) : ( </select>
<p className="no-profiles">Nenhum perfil disponível.</p>
)}
</div>
)}
</div> </div>
); );
}; };

View File

@ -199,14 +199,3 @@
padding: 0.6rem 1.2rem; 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 { Link, useNavigate, useLocation } from "react-router-dom";
import "./DoctorForm.css"; import "./DoctorForm.css";
import HorariosDisponibilidade from "../doctors/HorariosDisponibilidade"; 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 }) { function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { getAuthorizationHeader } = useAuth();
const FormatTelefones = (valor) => { const FormatTelefones = (valor) => {
const digits = String(valor).replace(/\D/g, "").slice(0, 11); 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 [avatarUrl, setAvatarUrl] = useState(null);
const [showRequiredModal, setShowRequiredModal] = useState(false); const [showRequiredModal, setShowRequiredModal] = useState(false);
const [emptyFields, setEmptyFields] = useState([]); const [emptyFields, setEmptyFields] = useState([]);
@ -74,15 +74,6 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
horarios: false, horarios: false,
}); });
const resolveAuthHeader = () => {
try {
const h = getAuthorizationHeader();
return h || '';
} catch {
return '';
}
}
const handleToggleCollapse = (section) => { const handleToggleCollapse = (section) => {
setCollapsedSections((prevState) => ({ setCollapsedSections((prevState) => ({
...prevState, ...prevState,
@ -137,9 +128,12 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
const handleAvailabilityUpdate = useCallback((newAvailability) => { const handleAvailabilityUpdate = useCallback((newAvailability) => {
setFormData((prev) => { setFormData((prev) => {
if (JSON.stringify(prev.availability) !== JSON.stringify(newAvailability)) {
return { ...prev, availability: newAvailability }; return { ...prev, availability: newAvailability };
}
return prev;
}); });
}, [setFormData]); }, []);
const handleCepBlur = async () => { const handleCepBlur = async () => {
const cep = formData.cep?.replace(/\D/g, ""); const cep = formData.cep?.replace(/\D/g, "");
@ -219,68 +213,21 @@ const handleAvailabilityUpdate = useCallback((newAvailability) => {
} }
}, 300); }, 300);
}; };
const handleCreateAvailability = async (newAvailability) => {
const handleCreateAvailability = async (doctorId, availabilityData) => {
try { 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, { const response = await fetch(ENDPOINT_AVAILABILITY, {
method: "POST", method: "POST",
headers: myHeaders, headers: {
body: JSON.stringify({ "Content-Type": "application/json",
doctor_id: doctorId, },
availability: availabilityData, body: JSON.stringify(newAvailability),
created_at: new Date().toISOString(),
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(); const data = await response.json();
console.log("Disponibilidade criada:", data); console.log("Disponibilidade criada :", data);
return data; alert("Disponibilidade criada com sucesso!");
} catch (error) { } catch (error) {
console.error("Erro ao criar disponibilidade:", error); console.error("Erro ao criar disponibilidade:", error);
throw error; alert("Erro ao criar disponibilidade.");
}
};
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;
} }
}; };
@ -323,20 +270,23 @@ const handleAvailabilityUpdate = useCallback((newAvailability) => {
} }
try { 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 }); await onSave({ ...formData });
} catch (error) { if (formData.availability && formData.availability.length > 0) {
console.error("Erro ao salvar médico ou disponibilidade:", error);
alert("Erro ao salvar médico ou disponibilidade.");
} }
alert("Médico salvo com sucesso!");
} catch (error) {
console.error("Erro ao salvar médico:", error);
alert("Erro ao salvar médico.");
};
}; };
const handleModalClose = () => { const handleModalClose = () => {
setShowRequiredModal(false); setShowRequiredModal(false);
}; };
return ( return (
<> <>
{/* Modal de Alerta */} {/* Modal de Alerta */}
@ -759,7 +709,7 @@ const handleAvailabilityUpdate = useCallback((newAvailability) => {
</div> </div>
{/* BOTÕES DE AÇÃO */} {/* BOTÕES DE AÇÃO */}
<div className="btns-container"> <div className="actions-container">
<button <button
className="btn btn-success btn-submit" className="btn btn-success btn-submit"
onClick={handleSubmit} 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 React, { useState, useEffect, useCallback, useRef } from "react";
import { Clock } from "lucide-react"; import { Clock } from "lucide-react";
import "./HorariosDisponibilidade.css";
const initialBlockTemplate = { const initialBlockTemplate = {
id: null, id: null,
@ -10,13 +9,13 @@ const initialBlockTemplate = {
}; };
const emptyAvailabilityTemplate = [ const emptyAvailabilityTemplate = [
{ dia: "Domingo", weekday: 0, isChecked: false, blocos: [] }, { dia: "Segunda-feira", isChecked: false, blocos: [] },
{ dia: "Segunda-feira", weekday: 1, isChecked: false, blocos: [] }, { dia: "Terça-feira", isChecked: false, blocos: [] },
{ dia: "Terça-feira", weekday: 2, isChecked: false, blocos: [] }, { dia: "Quarta-feira", isChecked: false, blocos: [] },
{ dia: "Quarta-feira", weekday: 3, isChecked: false, blocos: [] }, { dia: "Quinta-feira", isChecked: false, blocos: [] },
{ dia: "Quinta-feira", weekday: 4, isChecked: false, blocos: [] }, { dia: "Sexta-feira", isChecked: false, blocos: [] },
{ dia: "Sexta-feira", weekday: 5, isChecked: false, blocos: [] }, { dia: "Sábado", isChecked: false, blocos: [] },
{ dia: "Sábado", weekday: 6, isChecked: false, blocos: [] }, { dia: "Domingo", isChecked: false, blocos: [] },
]; ];
const HorariosDisponibilidade = ({ const HorariosDisponibilidade = ({
@ -35,18 +34,11 @@ const HorariosDisponibilidade = ({
} }
}, [initialAvailability]); }, [initialAvailability]);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
if (onUpdate) onUpdate(availability);
}, [availability, onUpdate]);
const handleDayCheck = useCallback((dayIndex, currentIsChecked) => { const handleDayCheck = useCallback((dayIndex, currentIsChecked) => {
const isChecked = !currentIsChecked; const isChecked = !currentIsChecked;
setAvailability((prev) => {
const updated = prev.map((day, i) => setAvailability((prev) =>
prev.map((day, i) =>
i === dayIndex i === dayIndex
? { ? {
...day, ...day,
@ -64,15 +56,14 @@ const HorariosDisponibilidade = ({
: [], : [],
} }
: day : day
)
); );
console.log('handleDayCheck - updated availability:', updated);
return updated;
});
}, []); }, []);
const handleAddBlock = useCallback((dayIndex) => { const handleAddBlock = useCallback((dayIndex) => {
const tempId = Date.now() + Math.random(); const tempId = Date.now() + Math.random();
const newBlock = { ...initialBlockTemplate, id: tempId, isNew: true }; const newBlock = { ...initialBlockTemplate, id: tempId, isNew: true };
setAvailability((prev) => setAvailability((prev) =>
prev.map((day, i) => prev.map((day, i) =>
i === dayIndex i === dayIndex
@ -117,90 +108,299 @@ const HorariosDisponibilidade = ({
); );
}, []); }, []);
const handleSave = useCallback(() => {
if (onUpdate) onUpdate(availability);
}, [availability, onUpdate]);
const renderTimeBlock = (dayIndex, bloco) => (
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) => (
<div <div
key={bloco.id} 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: Início:
<div className="input-wrapper"> </label>
<div style={{ position: "relative" }}>
<input <input
id={`inicio-${dayIndex}-${bloco.id}`}
type="time" type="time"
value={bloco.inicio} value={bloco.inicio}
onChange={(e) => onChange={(e) =>
handleTimeChange( handleTimeChange(dayIndex, bloco.id, "inicio", e.target.value)
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> </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: Término:
<div className="input-wrapper"> </label>
<div style={{ position: "relative" }}>
<input <input
id={`termino-${dayIndex}-${bloco.id}`}
type="time" type="time"
value={bloco.termino} value={bloco.termino}
onChange={(e) => onChange={(e) =>
handleTimeChange( handleTimeChange(dayIndex, bloco.id, "termino", e.target.value)
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>
</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> </label>
</div> </div>
<button {isChecked && (
className="btn-remove" <div style={{ marginTop: "16px" }}>
onClick={() => handleRemoveBlock(dayIndex, bloco.id)} {day.blocos.length === 0 && (
<p
style={{
color: "#6b7280",
fontStyle: "italic",
marginBottom: "16px",
}}
> >
Remover Nenhum bloco de horário definido.
</button> </p>
</div> )}
))}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "16px",
}}
>
{day.blocos.map((bloco) =>
renderTimeBlock(dayIndex, bloco)
)}
</div> </div>
<button <button
className="btn-add"
onClick={() => handleAddBlock(dayIndex)} 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 + Adicionar novo bloco
</button> </button>
</div> </div>
)} )}
</div> </div>
))} );
})}
</div>
</div> </div>
); );
}; };

View File

@ -52,8 +52,6 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
})); }));
}; };
useEffect(() => { useEffect(() => {
const peso = parseFloat(formData.weight_kg); const peso = parseFloat(formData.weight_kg);
const altura = parseFloat(formData.height_m); const altura = parseFloat(formData.height_m);
@ -65,20 +63,9 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
} }
}, [formData.weight_kg, formData.height_m, setFormData]); }, [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 handleChange = (e) => {
const { name, value, type, checked, files } = e.target; const { name, value, type, checked, files } = e.target;
console.log(name, value)
if (value && emptyFields.includes(name)) { if (value && emptyFields.includes(name)) {
setEmptyFields(prev => prev.filter(field => field !== name)); setEmptyFields(prev => prev.filter(field => field !== name));
} }
@ -115,10 +102,7 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
setFormData(prev => ({ ...prev, [name]: FormatPeso(value) })); setFormData(prev => ({ ...prev, [name]: FormatPeso(value) }));
} else if (name === 'rn_in_insurance' || name === 'vip' || name === 'validadeIndeterminada') { } else if (name === 'rn_in_insurance' || name === 'vip' || name === 'validadeIndeterminada') {
setFormData(prev => ({ ...prev, [name]: checked })); setFormData(prev => ({ ...prev, [name]: checked }));
} else if(name === 'cep'){ } else {
handleCep(value)
setFormData(prev => ({...prev, [name]: value}))
}else {
setFormData(prev => ({ ...prev, [name]: value })); setFormData(prev => ({ ...prev, [name]: value }));
} }
}; };
@ -205,6 +189,9 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
} }
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 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 }); await onSave({ ...formData, bmi: parseFloat(formData.bmi) || null });
}; };
@ -645,7 +632,7 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
)} )}
{/* BOTÕES DE AÇÃO */} {/* BOTÕES DE AÇÃO */}
<div className="btns-container"> <div className="actions-container">
<button className="btn btn-success btn-submit" onClick={handleSubmit} disabled={isLoading}> <button className="btn btn-success btn-submit" onClick={handleSubmit} disabled={isLoading}>
{isLoading ? 'Salvando...' : 'Salvar Paciente'} {isLoading ? 'Salvando...' : 'Salvar Paciente'}
</button> </button>
@ -655,7 +642,6 @@ function PatientForm({ onSave, onCancel, formData, setFormData, isLoading }) {
</button> </button>
</Link> </Link>
</div> </div>
</div> </div>
); );
} }

View File

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

View File

@ -1,47 +1,32 @@
import API_KEY from "../apiKeys"; import API_KEY from "../apiKeys";
// Função para pegar as informações do usuário logado const UserInfos = async (access_token) => {
const UserInfos = async (access_token) => {
if (!access_token) throw new Error("access_token é obrigatório em UserInfos");
// Normaliza o formato do token let Token = access_token.replace('bearer', 'Bearer')
const Token = access_token.replace(/^bearer/i, "Bearer");
const myHeaders = new Headers();
var myHeaders = new Headers();
myHeaders.append("apikey", API_KEY); myHeaders.append("apikey", API_KEY);
myHeaders.append("Authorization", Token); myHeaders.append("Authorization", Token);
const requestOptions = { var requestOptions = {
method: "GET", method: 'GET',
headers: myHeaders, 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 userInfoData = await userInfo.json(); const userInfo = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/user-info`, requestOptions)
console.log("Dados do usuário:", userInfoData); const userInfoData = await userInfo.json()
return userInfoData; console.log(userInfoData, "Dados do usuário")
} catch (error) { return userInfoData
console.error("Erro na função UserInfos:", error);
throw error;
}
};
const SearchCep = async (cep) => {
fetch(`https://brasilapi.com.br/api/cep/v1/${cep}`)
.then(response => console.log(response))
} }
const UploadFotoAvatar = ( userID,access_token,file) => {
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') console.log('uaua')
}else if(ErrorData.httpStatus === 404){ }else if(ErrorData.httpStatus === 404){
setModalMensagem("Erro interno do sistema") setModalMensagem("Erro interno do sistema")
}else if(ErrorData.httpStatus === undefined){ }else{setModalMensagem(ErrorData.mensagem)}
setModalMensagem("Erro operacional no sistema")
}
else{setModalMensagem(ErrorData.mensagem)}
}, [ErrorData]) }, [ErrorData])
@ -27,7 +24,7 @@ return(
<div> <div>
{showModal === "modal"? {showModal ?
<div className="modal-overlay"> <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", "name": "Lista de Pacientes",
"icon": "clipboard-heart-fill", "icon": "clipboard-heart-fill",
@ -21,14 +25,16 @@
"icon": "table", "icon": "table",
"url": "/admin/laudo" "url": "/admin/laudo"
}, },
{
"name": "Gestão de Usuários",
"icon": "person-badge-fill",
"url": "/admin/gestao"
},
{ {
"name": "Painel Administrativo", "name": "Painel Administrativo",
"icon": "file-bar-graph-fill", "icon": "file-bar-graph-fill",
"url": "/admin/painel" "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", "name": "Controle Financeiro",
"icon": "cash-coin", "icon": "cash-coin",

View File

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

View File

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

View File

@ -1,4 +1,9 @@
[ [
{
"name": "Menu",
"isTitle": true
},
{ {
"name":"Início", "name":"Início",
"url": "/secretaria/", "url": "/secretaria/",
@ -20,5 +25,11 @@
"name": "Agendar consulta", "name": "Agendar consulta",
"icon": "calendar-plus-fill", "icon": "calendar-plus-fill",
"url": "/secretaria/agendamento" "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 { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 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 React from 'react'
import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta'; import FormNovaConsulta from '../components/AgendarConsulta/FormNovaConsulta'
import API_KEY from '../components/utils/apiKeys'; import API_KEY from '../components/utils/apiKeys'
import { useAuth } from '../components/utils/AuthProvider'; import { useAuth } from '../components/utils/AuthProvider'
import dayjs from 'dayjs'; import { useEffect,useState } from 'react'
import { UserInfos } from '../components/utils/Functions-Endpoints/General'; import dayjs from 'dayjs'
import { UserInfos } from '../components/utils/Functions-Endpoints/General'
const AgendamentoCadastroManager = ({setPageConsulta}) => {
const AgendamentoCadastroManager = ({ setPageConsulta, Dict, onSaved }) => { const {getAuthorizationHeader} = useAuth()
const { getAuthorizationHeader, user } = useAuth(); const [agendamento, setAgendamento] = useState({status:'confirmed'})
const [agendamento, setAgendamento] = useState({ status: 'confirmed' }); const [idUsuario, setIDusuario] = useState('0')
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(() => { useEffect(() => {
if (!Dict) {
setAgendamento({ status: 'confirmed' }); const ColherInfoUsuario =async () => {
} else { const result = await UserInfos(authHeader)
setAgendamento(Dict);
setIDusuario(result?.profile?.id)
} }
ColherInfoUsuario()
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);
}
};
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({ const handleSave = (Dict) => {
patient_id: patientId, // paciente logado let DataAtual = dayjs()
doctor_id: DictForm.doctor_id, var myHeaders = new Headers();
scheduled_at: `${DictForm.dataAtendimento}T${DictForm.horarioInicio}:00`, myHeaders.append("apikey", API_KEY);
duration_minutes: 30, myHeaders.append("Authorization", authHeader);
appointment_type: DictForm.tipo_consulta, myHeaders.append("Content-Type", "application/json");
patient_notes: '',
insurance_provider: DictForm.convenio, var raw = JSON.stringify({
status: 'confirmed', // ou 'confirmed' "patient_id": Dict.patient_id,
created_by: idUsuario, "doctor_id": Dict.doctor_id,
created_at: dayjs().toISOString(), "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', method: 'POST',
headers: myHeaders, headers: myHeaders,
body: raw, body: raw,
redirect: 'follow', redirect: 'follow'
}; };
try { fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments", requestOptions)
const response = await fetch( .then(response => response.text())
'https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/appointments', .then(result => console.log(result))
requestOptions .catch(error => console.log('error', error));
);
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.');
} }
} catch (error) {
console.error('Erro de rede:', error);
alert('Erro de rede ao salvar agendamento.');
}
};
return ( return (
<div> <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() //let DataAtual = dayjs()
const {getAuthorizationHeader} = useAuth() const {getAuthorizationHeader} = useAuth()
const params = useParams() const params = useParams()
const [PatientToPatch, setPatientToPatch] = useState({})
let id = params.id let id = params.id
console.log(DictInfo, "DENTRO DO EDITAR")
//console.log(DictInfo, 'aqui') //console.log(DictInfo, 'aqui')
useEffect(() => { useEffect(() => {
setDictInfo({...DictInfo, dataAtendimento:DictInfo.scheduled_at.split("T")[0]}) setDictInfo({...DictInfo?.Infos,...DictInfo?.agendamento})
const ColherInfoUsuario =async () => { const ColherInfoUsuario =async () => {
const result = await UserInfos(authHeader) const result = await UserInfos(authHeader)
@ -51,7 +53,9 @@ const AgendamentoEditPage = ({setDictInfo, DictInfo}) => {
"doctor_id": DictParaPatch.doctor_id, "doctor_id": DictParaPatch.doctor_id,
"duration_minutes": 30, "duration_minutes": 30,
"chief_complaint": "Dor de cabeça há 3 ", "chief_complaint": "Dor de cabeça há 3 ",
"created_by": idUsuario, "created_by": idUsuario,
"scheduled_at": `${DictParaPatch.dataAtendimento}T${DictParaPatch.horarioInicio}:00.000Z`, "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"; import { useAuth } from "../components/utils/AuthProvider";
const Details = (DictInfo) => { const Details = () => {
const parametros = useParams(); const parametros = useParams();
const {getAuthorizationHeader, isAuthenticated} = useAuth(); const {getAuthorizationHeader, isAuthenticated} = useAuth();
const [paciente, setPaciente] = useState({}); const [paciente, setPaciente] = useState({});
@ -22,29 +22,19 @@ const Details = (DictInfo) => {
navigate(`/${prefixo}/pacientes`); navigate(`/${prefixo}/pacientes`);
} }
const navigateEdit = () => {
const prefixo = location.pathname.split("/")[1];
navigate(`/${prefixo}/medicos/edit`);
}
useEffect(() => { useEffect(() => {
if (!DictInfo) return; if (!patientID) return;
console.log(patientID, 'teu id') console.log(patientID, 'teu id')
const authHeader = getAuthorizationHeader() const authHeader = getAuthorizationHeader()
GetPatientByID(DictInfo.DictInfo.id, authHeader) GetPatientByID(patientID, authHeader)
.then((data) => { .then((data) => {
console.log(data, "paciente vindo da API"); console.log(data, "paciente vindo da API");
setPaciente(data[0]); // supabase retorna array setPaciente(data[0]); // supabase retorna array
}) })
.catch((err) => console.error("Erro ao buscar paciente:", err)); .catch((err) => console.error("Erro ao buscar paciente:", err));
}, [DictInfo]); }, [patientID]);
const handleDelete = async (anexoId) => { const handleDelete = async (anexoId) => {
@ -92,9 +82,11 @@ const navigateEdit = () => {
</div> </div>
</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 <i className="bi bi-pencil-square"></i> Editar
</button> </button>
</Link>
</div> </div>
</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 HorariosDisponibilidade from "../components/doctors/HorariosDisponibilidade";
import { useAuth } from "../components/utils/AuthProvider"; const ENDPOINT =
import API_KEY from "../components/utils/apiKeys"; "https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability";
import "./style/DisponibilidadesDoctorPage.css";
const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability"; const MEDICOS_MOCKADOS = [
const DOCTORS_ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors"; { id: 53, nome: "João Silva" },
{ id: 19, nome: "Ana Costa" },
const diasDaSemana = [ { id: 11, nome: "Pedro Santos" },
"Domingo",
"Segunda",
"Terça",
"Quarta",
"Quinta",
"Sexta",
"Sábado"
]; ];
const weekdayNumToStr = {
0: "sunday", const diasDaSemana = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];
1: "monday",
2: "tuesday", const formatarDataHora = (isoString) => {
3: "wednesday", if (!isoString) return "N/A";
4: "thursday", try {
5: "friday", const data = new Date(isoString);
6: "saturday", // 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 DisponibilidadesDoctorPage = () => {
const { getAuthorizationHeader } = useAuth();
const [disponibilidades, setDisponibilidades] = useState([]); const [disponibilidades, setDisponibilidades] = useState([]);
const [doctors, setDoctors] = useState([]); const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [filtroMedicoNome, setFiltroMedicoNome] = useState("");
const [editando, setEditando] = useState(null); const [gerenciarModo, setGerenciarModo] = useState(false);
const [expandedDoctors, setExpandedDoctors] = useState({}); const [editando, setEditando] = useState(null); // ID da disponibilidade sendo editada
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 getHeaders = () => { const encontrarMedicoIdPorNome = (nome) => {
const myHeaders = new Headers(); if (!nome) return null;
const authHeader = getAuthorizationHeader(); const termo = nome.toLowerCase();
if (authHeader) myHeaders.append("Authorization", authHeader); const medico = MEDICOS_MOCKADOS.find((m) =>
myHeaders.append("Content-Type", "application/json"); m.nome.toLowerCase().includes(termo)
if (API_KEY) myHeaders.append("apikey", API_KEY); );
myHeaders.append("Prefer", "return=representation"); return medico ? medico.id : null;
return myHeaders;
}; };
useEffect(() => { const fetchDisponibilidades = useCallback(async (nome) => {
const fetchDoctors = async () => { setLoading(true);
try {
const requestOptions = { const doctorId = encontrarMedicoIdPorNome(nome);
method: "GET", if (!doctorId) {
headers: getHeaders(), setLoading(false);
}; return;
const response = await fetch(DOCTORS_ENDPOINT, requestOptions);
const result = await response.json();
setDoctors(Array.isArray(result) ? result : []);
} catch (error) {
setDoctors([]);
} }
};
fetchDoctors();
}, [getAuthorizationHeader]);
useEffect(() => {
const fetchDisponibilidades = async () => {
try { try {
const res = await fetch(ENDPOINT, { method: "GET", headers: getHeaders() }); const res = await fetch(`${ENDPOINT}?doctor_id=eq.${doctorId}`);
if (res.ok) {
const data = await res.json(); 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([]); setDisponibilidades([]);
} }
}; }, [filtroMedicoNome, fetchDisponibilidades, editando]);
fetchDisponibilidades();
}, [getAuthorizationHeader]);
const toggleExpandDoctor = (doctorId) => { const atualizarDisponibilidade = async (id, novoIntervalo) => {
setExpandedDoctors((prev) => ({ ...prev, [doctorId]: !prev[doctorId] }));
};
const salvarTodasDisponibilidades = async (doctorId, horariosAtualizados) => {
try { try {
const headers = getHeaders(); const res = await fetch(`${ENDPOINT}?id=eq.${id}`, {
const promises = []; method: "PUT",
const currentIds = new Set(); headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slot_minutes: novoIntervalo }),
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 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) { if (res.ok) {
const data = await res.json(); alert("Disponibilidade atualizada com sucesso!");
setDisponibilidades(Array.isArray(data) ? data : []); setEditando(null);
fetchDisponibilidades(filtroMedicoNome);
} else {
alert("Erro ao atualizar disponibilidade");
} }
} catch (error) { } catch {
console.error(error); alert("Falha ao conectar com o servidor");
} }
}; };
const deletarDisponibilidade = async (id) => { const deletarDisponibilidade = async (id) => {
if (!window.confirm("Deseja realmente excluir esta disponibilidade?")) return; if (!window.confirm("Deseja realmente excluir esta disponibilidade?"))
return;
try { 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) { if (res.ok) {
alert("Disponibilidade excluída!");
setDisponibilidades((prev) => prev.filter((d) => d.id !== id)); setDisponibilidades((prev) => prev.filter((d) => d.id !== id));
setShowDeleteModal(false); } else {
setSelectedDisponibilidadeId(null); alert("Erro ao excluir disponibilidade");
} }
} catch (error) { } catch {
console.error("Erro ao excluir disponibilidade:", error); alert("Erro ao conectar com o servidor");
} }
}; };
const handleOpenDeleteModal = (id) => { const disponibilidadeParaEdicao = editando
setSelectedDisponibilidadeId(id); ? disponibilidades.find((d) => d.id === editando)
setShowDeleteModal(true); : null;
};
const handleCloseDeleteModal = () => { const initialAvailabilityParaEdicao = diasDaSemana.map((dia, weekdayIndex) => {
setShowDeleteModal(false); const blocosDoDia = disponibilidades
setSelectedDisponibilidadeId(null); .filter(d => d.weekday === weekdayIndex)
}; .map(d => ({
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({
id: d.id, id: d.id,
inicio: formatTime(d.start_time) || "07:00", inicio: d.start_time
termino: formatTime(d.end_time) || "17:00", ? new Date(d.start_time).toISOString().substring(11, 16)
slot_minutes: d.slot_minutes || 30, : "07:00",
appointment_type: d.appointment_type || "presencial", termino: d.end_time
? new Date(d.end_time).toISOString().substring(11, 16)
: "17:00",
isNew: false, isNew: false,
}); slot_minutes: d.slot_minutes,
} }));
});
const resultado = [1, 2, 3, 4, 5, 6, 0].map((weekday) => {
const blocosDoDia = blocosPorDia[weekday] || [];
return { return {
dia: diasDaSemana[weekday], dia,
weekday: weekday,
isChecked: blocosDoDia.length > 0, isChecked: blocosDoDia.length > 0,
blocos: blocos: blocosDoDia,
blocosDoDia.length > 0
? blocosDoDia
: [
{
id: null,
inicio: "07:00",
termino: "17:00",
slot_minutes: 30,
appointment_type: "presencial",
isNew: true,
},
],
}; };
}); });
return resultado;
}, [disponibilidades, editando]);
const handleUpdateHorarios = (horariosAtualizados) => { const handleUpdateHorarios = (horariosAtualizados) => {
if (!editando) return; console.log("Horários editados:", horariosAtualizados);
setAvailabilityEdit(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); setEditando(null);
setAvailabilityEdit([]); fetchDisponibilidades(filtroMedicoNome);
};
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";
}; };
return ( return (
<div className="disponibilidades-container"> <div id="main-content">
<h1 className="disponibilidades-title">Disponibilidades dos Médicos</h1> {/* 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"> {/* Botão Voltar/Gerenciar */}
<div className="search-input-container"> <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 <input
type="text" type="text"
placeholder="Buscar médico por nome..." placeholder="Filtrar por Nome do Médico..."
value={searchTerm} value={filtroMedicoNome}
onChange={(e) => { onChange={(e) => setFiltroMedicoNome(e.target.value)}
setSearchTerm(e.target.value); style={{
setShowSuggestions(true); 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> </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>
)} )}
</div>
<section className="calendario-ou-filaespera"> <section className="calendario-ou-filaespera">
<div className="fila-container"> <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 ? ( {loading ? (
<p className="loading-text">Carregando médicos...</p> <p>Carregando...</p>
) : disponibilidades.length === 0 ? (
<p>Nenhuma disponibilidade encontrada.</p>
) : editando ? ( ) : editando ? (
<> <>
<div className="edit-container"> <HorariosDisponibilidade
{initialAvailabilityParaEdicao.length > 0 ? ( initialAvailability={initialAvailabilityParaEdicao}
<HorariosDisponibilidade initialAvailability={initialAvailabilityParaEdicao} onUpdate={handleUpdateHorarios} onCancel={handleCancelarEdicao} /> onUpdate={handleUpdateHorarios}
) : ( />
<p className="loading-text">Carregando horários para edição...</p>
)}
</div>
<div className="disp-buttons-container">
<button <button
onClick={() => 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 Salvar Alterações
</button> </button>
<button onClick={handleCancelarEdicao} className="disp-btn-danger">
Cancelar
</button>
</div>
</> </>
) : ( ) : (
<div className="doctor-group-container"> <table style={{ width: "100%", borderCollapse: "collapse" }}>
{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">
<thead> <thead>
<tr> <tr>
<th>Dia da Semana</th> <th>Dia da Semana</th>
<th>Início</th> <th>Início</th>
<th>Término</th> <th>Término</th>
<th>Intervalo (min)</th> <th>Intervalo</th>
<th>Tipo</th> <th>Tipo Consulta</th>
<th>Status</th> {gerenciarModo && <th>Ações</th>}
<th>Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{grupo.disponibilidades.map((disp) => ( {disponibilidades.map((disp) => (
<tr key={disp.id}> <tr key={disp.id}>
<td>{disp.is_empty ? "Nenhum horário cadastrado" : getDiaSemana(disp.weekday)}</td> <td>{diasDaSemana[disp.weekday]}</td>
<td>{disp.is_empty ? "-" : formatTime(disp.start_time)}</td> <td>{formatarDataHora(disp.start_time)}</td>
<td>{disp.is_empty ? "-" : formatTime(disp.end_time)}</td> <td>{formatarDataHora(disp.end_time)}</td>
<td>{disp.is_empty ? "-" : disp.slot_minutes || 30}</td> <td>{disp.slot_minutes}</td>
<td>{disp.is_empty ? "-" : disp.appointment_type || "presencial"}</td> <td>{disp.appointment_type}</td>
{gerenciarModo && (
<td> <td>
<span className={getStatusBadgeClass(disp)}>{getStatusText(disp)}</span>
</td>
<td>
{!disp.is_empty && (
<button <button
onClick={() => handleOpenDeleteModal(disp.id)} onClick={() => setEditando(disp.id)}
className="disp-btn-delete" style={{
backgroundColor: "#10b981",
color: "white",
borderRadius: "6px",
}}
>
Editar
</button>{" "}
<button
onClick={() => deletarDisponibilidade(disp.id)}
style={{
backgroundColor: "#c72f2f",
color: "white",
borderRadius: "6px",
}}
> >
Excluir Excluir
</button> </button>
)}
</td> </td>
)}
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div>
</div>
)}
</div>
))
)}
</div>
)} )}
</div> </div>
</section> </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> </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 { GetDoctorByID } from "../components/utils/Functions-Endpoints/Doctor";
import { useAuth } from "../components/utils/AuthProvider"; import { useAuth } from "../components/utils/AuthProvider";
const DoctorDetails = ({DictInfo}) => { const Details = () => {
const {getAuthorizationHeader} = useAuth(); const {getAuthorizationHeader} = useAuth();
const [doctor, setDoctor] = useState({});
const Parametros = useParams() const Parametros = useParams()
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const navigateEdit = () => { const Voltar = () => {
const prefixo = location.pathname.split("/")[1];
navigate(`/${prefixo}/medicos/edit`);
}
const Voltar = () => {
const prefixo = location.pathname.split("/")[1]; const prefixo = location.pathname.split("/")[1];
navigate(`/${prefixo}/medicos`); 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 ( return (
<> <>
<div className="card p-3 shadow-sm"> <div className="card p-3 shadow-sm">
@ -37,13 +50,15 @@ const Voltar = () => {
<img src={avatarPlaceholder} alt="" /> <img src={avatarPlaceholder} alt="" />
</div> </div>
<div className="media-body ms-3 font-extrabold"> <div className="media-body ms-3 font-extrabold">
<span>{DictInfo.full_name || "Nome Completo"}</span> <span>{doctor.nome || "Nome Completo"}</span>
<p>{DictInfo.cpf || "CPF"}</p> <p>{doctor.cpf || "CPF"}</p>
</div> </div>
</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 <i className="bi bi-pencil-square"></i> Editar
</button> </button>
</Link>
</div> </div>
</div> </div>
@ -54,29 +69,29 @@ const Voltar = () => {
<div className="row"> <div className="row">
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<label className="font-extrabold">Nome:</label> <label className="font-extrabold">Nome:</label>
<p>{DictInfo.full_name || "-"}</p> <p>{doctor.full_name || "-"}</p>
</div> </div>
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<label className="font-extrabold">Data de nascimento:</label> <label className="font-extrabold">Data de nascimento:</label>
<p>{DictInfo.birth_date || "-"}</p> <p>{doctor.birth_date || "-"}</p>
</div> </div>
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<label className="font-extrabold">CPF:</label> <label className="font-extrabold">CPF:</label>
<p>{DictInfo.cpf || "-"}</p> <p>{doctor.cpf || "-"}</p>
</div> </div>
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<label className="font-extrabold">CRM:</label> <label className="font-extrabold">CRM:</label>
<p>{DictInfo.crm || "-"}</p> <p>{doctor.crm || "-"}</p>
</div> </div>
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<label className="font-extrabold">Estado do CRM:</label> <label className="font-extrabold">Estado do CRM:</label>
<p>{DictInfo.crm_uf || "-"}</p> <p>{doctor.crm_uf || "-"}</p>
</div> </div>
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<label className="font-extrabold">Especialização:</label> <label className="font-extrabold">Especialização:</label>
<p>{DictInfo.specialty || "-"}</p> <p>{doctor.specialty || "-"}</p>
</div> </div>
</div> </div>
</div> </div>
@ -88,31 +103,31 @@ const Voltar = () => {
<div className="row"> <div className="row">
<div className="col-md-4 mb-3"> <div className="col-md-4 mb-3">
<label className="font-extrabold">CEP:</label> <label className="font-extrabold">CEP:</label>
<p>{DictInfo.cep || "-"}</p> <p>{doctor.cep || "-"}</p>
</div> </div>
<div className="col-md-8 mb-3"> <div className="col-md-8 mb-3">
<label className="font-extrabold">Rua:</label> <label className="font-extrabold">Rua:</label>
<p>{DictInfo.street || "-"}</p> <p>{doctor.street || "-"}</p>
</div> </div>
<div className="col-md-4 mb-3"> <div className="col-md-4 mb-3">
<label className="font-extrabold">Bairro:</label> <label className="font-extrabold">Bairro:</label>
<p>{DictInfo.neighborhood || "-"}</p> <p>{doctor.neighborhood || "-"}</p>
</div> </div>
<div className="col-md-4 mb-3"> <div className="col-md-4 mb-3">
<label className="font-extrabold">Cidade:</label> <label className="font-extrabold">Cidade:</label>
<p>{DictInfo.city || "-"}</p> <p>{doctor.city || "-"}</p>
</div> </div>
<div className="col-md-2 mb-3"> <div className="col-md-2 mb-3">
<label className="font-extrabold">Estado:</label> <label className="font-extrabold">Estado:</label>
<p>{DictInfo.state || "-"}</p> <p>{doctor.state || "-"}</p>
</div> </div>
<div className="col-md-4 mb-3"> <div className="col-md-4 mb-3">
<label className="font-extrabold">Número:</label> <label className="font-extrabold">Número:</label>
<p>{DictInfo.number || "-"}</p> <p>{doctor.number || "-"}</p>
</div> </div>
<div className="col-md-8 mb-3"> <div className="col-md-8 mb-3">
<label className="font-extrabold">Complemento:</label> <label className="font-extrabold">Complemento:</label>
<p>{DictInfo.complement || "-"}</p> <p>{doctor.complement || "-"}</p>
</div> </div>
</div> </div>
</div> </div>
@ -124,15 +139,15 @@ const Voltar = () => {
<div className="row"> <div className="row">
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<label className="font-extrabold">Email:</label> <label className="font-extrabold">Email:</label>
<p>{DictInfo.email || "-"}</p> <p>{doctor.email || "-"}</p>
</div> </div>
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<label className="font-extrabold">Telefone:</label> <label className="font-extrabold">Telefone:</label>
<p>{DictInfo.phone_mobile || "-"}</p> <p>{doctor.phone_mobile || "-"}</p>
</div> </div>
<div className="col-md-6 mb-3"> <div className="col-md-6 mb-3">
<label className="font-extrabold">Telefone 2:</label> <label className="font-extrabold">Telefone 2:</label>
<p>{DictInfo.phone2 || "-"}</p> <p>{doctor.phone2 || "-"}</p>
</div> </div>
</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 React, { useEffect, useState, useCallback } from "react";
import { useParams, useNavigate, useLocation } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import { GetDoctorByID } from "../components/utils/Functions-Endpoints/Doctor";
import DoctorForm from "../components/doctors/DoctorForm"; import DoctorForm from "../components/doctors/DoctorForm";
import { useAuth } from "../components/utils/AuthProvider"; import { useAuth } from "../components/utils/AuthProvider";
import API_KEY from "../components/utils/apiKeys"; import API_KEY from "../components/utils/apiKeys";
const ENDPOINT = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors"; const ENDPOINT_AVAILABILITY =
const ENDPOINT_AVAILABILITY = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability"; "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
const diasDaSemana = ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"]; const DoctorEditPage = () => {
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 { getAuthorizationHeader } = useAuth(); const { getAuthorizationHeader } = useAuth();
const [DoctorToPUT, setDoctorPUT] = useState({});
const [doctor, setDoctor] = useState(null); const Parametros = useParams();
const [availability, setAvailability] = useState([]); const [searchParams] = useSearchParams();
const [isLoading, setIsLoading] = useState(true); const DoctorID = Parametros.id;
const [isSaving, setIsSaving] = useState(false); const availabilityId = searchParams.get("availabilityId");
const effectiveId = id; const [availabilityToPATCH, setAvailabilityToPATCH] = useState(null);
const [mode, setMode] = useState("doctor");
const getHeaders = () => { useEffect(() => {
const myHeaders = new Headers();
const authHeader = getAuthorizationHeader(); 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) => { if (availabilityId) {
try { setMode("availability");
const headers = getHeaders();
const promises = [];
const currentIds = new Set();
for (const dia of horariosAtualizados) { fetch(`${ENDPOINT_AVAILABILITY}?id=eq.${availabilityId}&select=*`, {
if (dia.isChecked && dia.blocos.length > 0) { method: "GET",
for (const bloco of dia.blocos) { headers: {
const inicio = bloco.inicio.includes(":") ? bloco.inicio : bloco.inicio + ":00"; apikey: API_KEY,
const termino = bloco.termino.includes(":") ? bloco.termino : bloco.termino + ":00"; Authorization: authHeader,
},
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),
}) })
.then((res) => res.json()) .then((res) => res.json())
.then((data) => { .then((data) => {
const createdItem = Array.isArray(data) ? data[0] : data; if (data && data.length > 0) {
if (createdItem && createdItem.id) { setAvailabilityToPATCH(data[0]);
return { type: "POST", id: createdItem.id }; console.log("Disponibilidade vinda da API:", data[0]);
} }
return { type: "POST", id: null };
}) })
); .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");
const results = await Promise.all(promises); setDoctorPUT(data[0]);
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 paciente:", err));
await Promise.all(deletePromises);
} }
}, [DoctorID, availabilityId, getAuthorizationHeader]);
const updatedResponse = await fetch( const HandlePutDoctor = async () => {
`${ENDPOINT_AVAILABILITY}?doctor_id=eq.${doctorId}&order=weekday.asc,start_time.asc`, const authHeader = getAuthorizationHeader();
{ method: "GET", headers }
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
); );
console.log("Resposta PUT Doutor:", response);
if (updatedResponse.ok) { alert("Dados do médico atualizados com sucesso!");
const updatedData = await updatedResponse.json();
setAvailability(updatedData);
}
} catch (error) { } catch (error) {
console.error("Erro ao atualizar médico:", error);
alert("Erro ao atualizar dados do médico.");
throw error; throw error;
} }
}; };
const normalizeAvailabilityForForm = (availabilityData) => { // 2. Função para Atualizar DISPONIBILIDADE (PATCH)
if (!Array.isArray(availabilityData)) return []; const HandlePatchAvailability = async (data) => {
const authHeader = getAuthorizationHeader();
const disponibilidadesMedico = availabilityData.filter((d) => var myHeaders = new Headers();
String(d.doctor_id) === String(effectiveId) && d.active !== false myHeaders.append("apikey", API_KEY);
); myHeaders.append("Authorization", authHeader);
const blocosPorDia = {}; myHeaders.append("Content-Type", "application/json");
disponibilidadesMedico.forEach((d) => { var raw = JSON.stringify(data);
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,
});
});
const resultado = [1, 2, 3, 4, 5, 6, 0].map((weekday) => { console.log("Enviando disponibilidade para atualização (PATCH):", data);
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,
},
],
};
});
return resultado; var requestOptions = {
};
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}`, {
method: "PATCH", method: "PATCH",
headers: getHeaders(), headers: myHeaders,
body: JSON.stringify(doctorDataToSave), body: raw,
}); redirect: "follow",
};
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");
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) { } catch (error) {
alert(`Erro ao salvar dados: ${error.message}`); console.error("Erro ao atualizar disponibilidade:", error);
} finally { alert("Erro ao atualizar disponibilidade.");
setIsSaving(false); 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 ( return (
<div className="container mt-4"> <div>
<div className="row"> <h1 className="text-2xl font-bold mb-4">
<div className="col-12"> {mode === "availability"
<h1>Editar Médico</h1> ? `Editar Horário Disponível (ID: ${availabilityId.substring(0, 8)})`
: `Editar Médico (ID: ${DoctorID})`}
</h1>
<DoctorForm <DoctorForm
formData={formData} onSave={
setFormData={setDoctor} mode === "availability" ? HandlePatchAvailability : HandlePutDoctor
onSave={handleSave} }
onCancel={handleCancel} formData={mode === "availability" ? availabilityToPATCH : DoctorToPUT}
isLoading={isSaving} setFormData={
isEditing={true} mode === "availability" ? setAvailabilityToPATCH : setDoctorPUT
}
isEditingAvailability={mode === "availability"}
/> />
</div> </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 { Link } from "react-router-dom";
import "./style/TableDoctor.css"; import "./style/TableDoctor.css";
function TableDoctor({setDictInfo}) { function TableDoctor() {
const { getAuthorizationHeader, isAuthenticated } = useAuth(); const { getAuthorizationHeader, isAuthenticated } = useAuth();
const [medicos, setMedicos] = useState([]); const [medicos, setMedicos] = useState([]);
@ -12,6 +12,7 @@ function TableDoctor({setDictInfo}) {
const [filtroEspecialidade, setFiltroEspecialidade] = useState("Todos"); const [filtroEspecialidade, setFiltroEspecialidade] = useState("Todos");
const [filtroAniversariante, setFiltroAniversariante] = useState(false); const [filtroAniversariante, setFiltroAniversariante] = useState(false);
const [showFiltrosAvancados, setShowFiltrosAvancados] = useState(false); const [showFiltrosAvancados, setShowFiltrosAvancados] = useState(false);
const [filtroCidade, setFiltroCidade] = useState(""); const [filtroCidade, setFiltroCidade] = useState("");
const [filtroEstado, setFiltroEstado] = useState(""); const [filtroEstado, setFiltroEstado] = useState("");
@ -21,15 +22,9 @@ function TableDoctor({setDictInfo}) {
const [dataFinal, setDataFinal] = useState(""); const [dataFinal, setDataFinal] = useState("");
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedDoctorId, setSelectedDoctorId] = useState(null); const [selectedDoctorId, setSelectedDoctorId] = useState(null);
const [sortKey, setSortKey] = useState(null);
const [sortDir, setSortDir] = useState('asc');
const limparFiltros = () => { const limparFiltros = () => {
setSearch(""); setSearch("");
setFiltroEspecialidade("Todos"); setFiltroEspecialidade("Todos");
@ -41,9 +36,9 @@ function TableDoctor({setDictInfo}) {
setIdadeMaxima(""); setIdadeMaxima("");
setDataInicial(""); setDataInicial("");
setDataFinal(""); setDataFinal("");
setPaginaAtual(1);
}; };
const deleteDoctor = async (id) => { const deleteDoctor = async (id) => {
const authHeader = getAuthorizationHeader() const authHeader = getAuthorizationHeader()
console.log(id, 'teu id') console.log(id, 'teu id')
@ -68,6 +63,7 @@ function TableDoctor({setDictInfo}) {
} }
}; };
const ehAniversariante = (dataNascimento) => { const ehAniversariante = (dataNascimento) => {
if (!dataNascimento) return false; if (!dataNascimento) return false;
const hoje = new Date(); const hoje = new Date();
@ -79,6 +75,7 @@ function TableDoctor({setDictInfo}) {
); );
}; };
const calcularIdade = (dataNascimento) => { const calcularIdade = (dataNascimento) => {
if (!dataNascimento) return 0; if (!dataNascimento) return 0;
const hoje = new Date(); const hoje = new Date();
@ -107,16 +104,18 @@ function TableDoctor({setDictInfo}) {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions) fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions)
.then(response => response.json()) .then(response => response.json())
.then(result => setMedicos(result)) .then(result => {setMedicos(result); console.log(result)})
.catch(error => console.log('error', error)); .catch(error => console.log('error', error));
}, [isAuthenticated, getAuthorizationHeader]); }, [isAuthenticated, getAuthorizationHeader]);
const medicosFiltrados = Array.isArray(medicos) ? medicos.filter((medico) => { const medicosFiltrados = Array.isArray(medicos) ? medicos.filter((medico) => {
const buscaNome = medico.full_name?.toLowerCase().includes(search.toLowerCase()); const buscaNome = medico.full_name?.toLowerCase().includes(search.toLowerCase());
const buscaCPF = medico.cpf?.toLowerCase().includes(search.toLowerCase()); const buscaCPF = medico.cpf?.toLowerCase().includes(search.toLowerCase());
const buscaEmail = medico.email?.toLowerCase().includes(search.toLowerCase()); const buscaEmail = medico.email?.toLowerCase().includes(search.toLowerCase());
const passaBusca = search === "" || buscaNome || buscaCPF || buscaEmail; const passaBusca = search === "" || buscaNome || buscaCPF || buscaEmail;
const passaEspecialidade = filtroEspecialidade === "Todos" || medico.specialty === filtroEspecialidade; const passaEspecialidade = filtroEspecialidade === "Todos" || medico.specialty === filtroEspecialidade;
const passaAniversario = filtroAniversariante const passaAniversario = filtroAniversariante
@ -133,75 +132,23 @@ function TableDoctor({setDictInfo}) {
const passaIdadeMinima = idadeMinima ? idade >= parseInt(idadeMinima) : true; const passaIdadeMinima = idadeMinima ? idade >= parseInt(idadeMinima) : true;
const passaIdadeMaxima = idadeMaxima ? idade <= parseInt(idadeMaxima) : true; const passaIdadeMaxima = idadeMaxima ? idade <= parseInt(idadeMaxima) : true;
const passaDataInicial = dataInicial ? const passaDataInicial = dataInicial ?
medico.created_at && new Date(medico.created_at) >= new Date(dataInicial) : true; medico.created_at && new Date(medico.created_at) >= new Date(dataInicial) : true;
const passaDataFinal = dataFinal ? const passaDataFinal = dataFinal ?
medico.created_at && new Date(medico.created_at) <= new Date(dataFinal) : true; medico.created_at && new Date(medico.created_at) <= new Date(dataFinal) : true;
const resultado = passaBusca && passaEspecialidade && passaAniversario && const resultado = passaBusca && passaEspecialidade && passaAniversario &&
passaCidade && passaEstado && passaIdadeMinima && passaIdadeMaxima && passaCidade && passaEstado && passaIdadeMinima && passaIdadeMaxima &&
passaDataInicial && passaDataFinal; passaDataInicial && passaDataFinal;
return resultado; return resultado;
}) : []; }) : [];
const 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(() => { useEffect(() => {
setPaginaAtual(1); console.log(` Médicos totais: ${medicos.length}, Filtrados: ${medicosFiltrados.length}`);
}, [search, filtroEspecialidade, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal, sortKey, sortDir]); }, [medicos, medicosFiltrados, search]);
return ( return (
<> <>
@ -222,6 +169,7 @@ function TableDoctor({setDictInfo}) {
</div> </div>
<div className="card-body"> <div className="card-body">
<div className="card p-3 mb-3 table-doctor-filters"> <div className="card p-3 mb-3 table-doctor-filters">
<h5 className="mb-3"> <h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i>{" "} <i className="bi bi-funnel-fill me-2 text-primary"></i>{" "}
@ -232,15 +180,16 @@ function TableDoctor({setDictInfo}) {
<input <input
type="text" type="text"
className="form-control" className="form-control"
placeholder="Buscar por nome, CPF ou email..." placeholder="Buscar por nome ou CPF..."
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
<small className="text-muted"> <small className="text-muted">
Digite o nome completo, CPF ou email Digite o nome completo ou número do CPF
</small> </small>
</div> </div>
<div className="filtros-basicos"> <div className="filtros-basicos">
<select <select
className="form-select filter-especialidade" className="form-select filter-especialidade"
@ -264,6 +213,7 @@ function TableDoctor({setDictInfo}) {
</select> </select>
<div className="filter-buttons-container"> <div className="filter-buttons-container">
<button <button
className={`btn filter-btn ${filtroAniversariante className={`btn filter-btn ${filtroAniversariante
? "btn-primary" ? "btn-primary"
@ -274,34 +224,6 @@ function TableDoctor({setDictInfo}) {
<i className="bi bi-calendar me-1"></i> Aniversariantes <i className="bi bi-calendar me-1"></i> Aniversariantes
</button> </button>
</div> </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>
<div className="d-flex justify-content-between align-items-center mt-3"> <div className="d-flex justify-content-between align-items-center mt-3">
@ -321,11 +243,13 @@ function TableDoctor({setDictInfo}) {
</button> </button>
</div> </div>
{showFiltrosAvancados && ( {showFiltrosAvancados && (
<div className="mt-3 p-3 border rounded advanced-filters"> <div className="mt-3 p-3 border rounded advanced-filters">
<h6 className="mb-3">Filtros Avançados</h6> <h6 className="mb-3">Filtros Avançados</h6>
<div className="row g-3"> <div className="row g-3">
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label fw-bold">Cidade</label> <label className="form-label fw-bold">Cidade</label>
<input <input
@ -372,6 +296,7 @@ function TableDoctor({setDictInfo}) {
/> />
</div> </div>
{/* Data de Cadastro */}
<div className="col-md-6"> <div className="col-md-6">
<label className="form-label fw-bold">Data inicial</label> <label className="form-label fw-bold">Data inicial</label>
<input <input
@ -393,14 +318,36 @@ function TableDoctor({setDictInfo}) {
</div> </div>
</div> </div>
)} )}
</div>
<div className="mt-3">
<div className="contador-medicos"> {(search || filtroEspecialidade !== "Todos" || filtroAniversariante || // filtroVIP removido
{medicosFiltrados.length} DE {medicos.length} MÉDICOS ENCONTRADOS 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> </div>
)}
<div className="mb-3">
<span className="badge results-badge">
{medicosFiltrados.length} de {medicos.length} médicos encontrados
</span>
</div> </div>
<div className="table-responsive"> <div className="table-responsive">
<table className="table table-striped table-hover table-doctor-table"> <table className="table table-striped table-hover table-doctor-table">
<thead> <thead>
@ -413,8 +360,8 @@ function TableDoctor({setDictInfo}) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{medicosPaginados.length > 0 ? ( {medicosFiltrados.length > 0 ? (
medicosPaginados.map((medico) => ( medicosFiltrados.map((medico) => (
<tr key={medico.id}> <tr key={medico.id}>
<td> <td>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
@ -424,6 +371,7 @@ function TableDoctor({setDictInfo}) {
<i className="bi bi-gift"></i> <i className="bi bi-gift"></i>
</span> </span>
)} )}
</div> </div>
</td> </td>
<td>{medico.cpf}</td> <td>{medico.cpf}</td>
@ -435,14 +383,14 @@ function TableDoctor({setDictInfo}) {
<td>{medico.email || 'Não informado'}</td> <td>{medico.email || 'Não informado'}</td>
<td> <td>
<div className="d-flex gap-2"> <div className="d-flex gap-2">
<Link to={`details/${medico.id}`}> <Link to={`${medico.id}`}>
<button className="btn btn-sm btn-view" onClick={() => setDictInfo({...medico})}> <button className="btn btn-sm btn-view">
<i className="bi bi-eye me-1"></i> Ver Detalhes <i className="bi bi-eye me-1"></i> Ver Detalhes
</button> </button>
</Link> </Link>
<Link to={`edit/${medico.id}`}> <Link to={`${medico.id}/edit`}>
<button className="btn btn-sm btn-edit" onClick={() => setDictInfo({...medico})}> <button className="btn btn-sm btn-edit">
<i className="bi bi-pencil me-1"></i> Editar <i className="bi bi-pencil me-1"></i> Editar
</button> </button>
</Link> </Link>
@ -462,74 +410,13 @@ function TableDoctor({setDictInfo}) {
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan="5" className="text-center py-4"> <td colSpan="5" className="empty-state">
<div className="text-muted"> Nenhum médico encontrado.
<i className="bi bi-search display-4"></i>
<p className="mt-2">Nenhum médico encontrado com os filtros aplicados.</p>
{(search || filtroEspecialidade !== "Todos" || filtroAniversariante ||
filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
<button className="btn btn-outline-primary btn-sm mt-2" onClick={limparFiltros}>
Limpar filtros
</button>
)}
</div>
</td> </td>
</tr> </tr>
)} )}
</tbody> </tbody>
</table> </table>
{medicosFiltrados.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, medicosFiltrados.length)} de {medicosFiltrados.length} médicos
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -551,10 +438,15 @@ function TableDoctor({setDictInfo}) {
> >
<div className="modal-dialog modal-dialog-centered"> <div className="modal-dialog modal-dialog-centered">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#dc3545', color: 'white' }}> <div className="modal-header">
<h5 className="modal-title"> <h5 className="modal-title">
Confirmação de Exclusão Confirmação de Exclusão
</h5> </h5>
<button
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
></button>
</div> </div>
<div className="modal-body"> <div className="modal-body">

View File

@ -1,21 +1,34 @@
import React from 'react' import React from 'react'
import PatientForm from '../components/patients/PatientForm' import PatientForm from '../components/patients/PatientForm'
import {useEffect, useState} from 'react' import {useEffect, useState} from 'react'
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient' import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient'
import API_KEY from '../components/utils/apiKeys' import API_KEY from '../components/utils/apiKeys'
import {useNavigate, useParams } from 'react-router-dom' import {useNavigate, useParams } from 'react-router-dom'
import { useAuth } from '../components/utils/AuthProvider' import { useAuth } from '../components/utils/AuthProvider'
const EditPage = () => {
const EditPage = ({DictInfo}) => {
const navigate = useNavigate() const navigate = useNavigate()
const Parametros = useParams()
const [PatientToPUT, setPatientPUT] = useState({}) const [PatientToPUT, setPatientPUT] = useState({})
const [showSuccessModal, setShowSuccessModal] = useState(false)
const { getAuthorizationHeader, isAuthenticated } = useAuth(); const { getAuthorizationHeader, isAuthenticated } = useAuth();
const PatientID = Parametros.id
useEffect(() => { useEffect(() => {
setPatientPUT(DictInfo) const authHeader = getAuthorizationHeader()
}, [DictInfo])
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 HandlePutPatient = async () => {
const authHeader = getAuthorizationHeader() const authHeader = getAuthorizationHeader()
@ -26,9 +39,9 @@ const HandlePutPatient = async () => {
myHeaders.append("Authorization", authHeader); myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json"); 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 = { var requestOptions = {
method: 'PATCH', method: 'PATCH',
@ -37,16 +50,25 @@ const HandlePutPatient = async () => {
redirect: 'follow' redirect: 'follow'
}; };
fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients?id=eq.${PatientToPUT.id}`,requestOptions) try {
.then(response => { const response = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients?id=eq.${PatientID}`,requestOptions);
console.log(response) console.log(response)
if (response.ok) {
setShowSuccessModal(true)
if(response.ok === false){
console.error("Erro ao atualizar paciente:");
} }
return response else{
}) console.log("ATUALIZADO COM SUCESSO");
.then(result => console.log(result)) navigate('/secretaria/pacientes')
.catch(error => console.log("erro", error)) }
return response;
} catch (error) {
console.error("Erro ao atualizar paciente:", error);
throw error;
}
}; };
@ -60,46 +82,6 @@ const HandlePutPatient = async () => {
setFormData={setPatientPUT} 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> </div>
) )
} }

View File

@ -1,5 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/pt-br'; import 'dayjs/locale/pt-br';
import weekday from 'dayjs/plugin/weekday'; import weekday from 'dayjs/plugin/weekday';
@ -24,10 +23,12 @@ const getDateRange = (date, view) => {
toDate = startDayjs.format('YYYY-MM-DD'); toDate = startDayjs.format('YYYY-MM-DD');
titleRange = startDayjs.format('DD/MM/YYYY'); titleRange = startDayjs.format('DD/MM/YYYY');
} else if (view === 'semanal') { } 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'); let weekStart = startDayjs.startOf('week');
if (weekStart.day() !== 1) { if (weekStart.day() !== 1) { // Se não for segunda-feira (1), ajusta
weekStart = startDayjs.weekday(1); weekStart = startDayjs.weekday(1); // Vai para a segunda-feira desta semana
} }
const weekEnd = weekStart.add(6, 'day'); const weekEnd = weekStart.add(6, 'day');
@ -51,21 +52,12 @@ const getDateRange = (date, view) => {
const ExcecoesDisponibilidade = () => { const ExcecoesDisponibilidade = () => {
const { getAuthorizationHeader } = useAuth(); const { getAuthorizationHeader } = useAuth();
const navigate = useNavigate();
const [pageNovaExcecao, setPageNovaExcecao] = useState(false); const [pageNovaExcecao, setPageNovaExcecao] = useState(false);
const [excecoes, setExcecoes] = useState([]); const [excecoes, setExcecoes] = useState([]);
const [loading, setLoading] = useState(false); 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 [filtroMedicoId, setFiltroMedicoId] = useState('');
const [filtroData, setFiltroData] = useState(dayjs().format('YYYY-MM-DD')); 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'); const [visualizacao, setVisualizacao] = useState('diario');
@ -131,51 +123,8 @@ const ExcecoesDisponibilidade = () => {
fetchExcecoes(fromDate, toDate, filtroMedicoId); fetchExcecoes(fromDate, toDate, filtroMedicoId);
}, [fetchExcecoes, filtroMedicoId, fromDate, toDate]); }, [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) => { const deleteExcecao = async (id) => {
if (!window.confirm("Confirma exclusão desta exceção?")) return;
const myHeaders = new Headers(); const myHeaders = new Headers();
const authHeader = resolveAuthHeader(); const authHeader = resolveAuthHeader();
if (authHeader) myHeaders.append("Authorization", authHeader); if (authHeader) myHeaders.append("Authorization", authHeader);
@ -190,9 +139,6 @@ const ExcecoesDisponibilidade = () => {
}); });
if (res.ok) { if (res.ok) {
setExcecoes(prev => prev.filter(x => x.id !== id)); setExcecoes(prev => prev.filter(x => x.id !== id));
setShowDeleteModal(false);
setSuccessMessage('Exceção excluída com sucesso!');
setShowSuccessModal(true);
} else { } else {
const text = await res.text(); const text = await res.text();
console.error('Erro ao deletar exceção', res.status, text); console.error('Erro ao deletar exceção', res.status, text);
@ -217,7 +163,7 @@ const ExcecoesDisponibilidade = () => {
return ( return (
<div> <div>
{/* Título e Botão de Criação */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<h1>Gerenciar Exceções de Disponibilidade</h1> <h1>Gerenciar Exceções de Disponibilidade</h1>
<button <button
@ -229,80 +175,30 @@ const ExcecoesDisponibilidade = () => {
</button> </button>
</div> </div>
<div className="card p-3 mb-3" style={{ marginTop: '20px' }}> <div className='atendimento-eprocura'>
<h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i>
Filtros
</h5>
<div className="row g-3 mb-3"> {/* Filtros de Médico e Data */}
<div className="col-md-6"> <div className='busca-atendimento'>
<label className="form-label fw-bold">Buscar Médico</label> <div>
<i className="fa-solid fa-user-doctor"></i>
<input <input
type="text" type="text"
className="form-control" placeholder="Filtrar por ID do Médico..."
placeholder="Digite o nome do médico..." value={filtroMedicoId}
value={searchTermDoctor} onChange={(e) => setFiltroMedicoId(e.target.value)}
onChange={(e) => handleSearchDoctors(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>
</div> <i className="fa-solid fa-calendar"></i>
<div className="col-md-6">
<label className="form-label fw-bold">Data de Referência</label>
<input <input
type="date" type="date"
className="form-control"
value={filtroData} value={filtroData}
onChange={(e) => setFiltroData(e.target.value)} onChange={(e) => setFiltroData(e.target.value)}
/> />
<small className="text-muted">Selecione a data base para visualização</small>
</div> </div>
</div> </div>
<div className="d-flex justify-content-between align-items-center"> {/* Botões de Visualização (Dia/Semana/Mês) */}
<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'>
<div className='container-btns-agenda-fila_esepera'> <div className='container-btns-agenda-fila_esepera'>
<button <button
className={`btn-agenda ${visualizacao === "diario" ? "opc-agenda-ativo" : ""}`} className={`btn-agenda ${visualizacao === "diario" ? "opc-agenda-ativo" : ""}`}
@ -324,7 +220,7 @@ const ExcecoesDisponibilidade = () => {
</button> </button>
</div> </div>
{/* Tabela de Exceções (Título usa o titleRange calculado) */}
<section className='calendario-ou-filaespera'> <section className='calendario-ou-filaespera'>
<div className="fila-container"> <div className="fila-container">
<h2 className="fila-titulo">Exceções em {titleRange} ({excecoes.length})</h2> <h2 className="fila-titulo">Exceções em {titleRange} ({excecoes.length})</h2>
@ -368,10 +264,7 @@ const ExcecoesDisponibilidade = () => {
<button <button
className="btn btn-sm btn-delete" className="btn btn-sm btn-delete"
onClick={() => { onClick={() => deleteExcecao(exc.id)}
setSelectedExceptionId(exc.id);
setShowDeleteModal(true);
}}
> >
<i className="bi bi-trash me-1"></i> Excluir <i className="bi bi-trash me-1"></i> Excluir
</button> </button>
@ -385,89 +278,6 @@ const ExcecoesDisponibilidade = () => {
</div> </div>
</section> </section>
</div> </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> </div>
); );
} }

View File

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

View File

@ -1,8 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { FaUser, FaUserPlus, FaCalendarAlt, FaCalendarCheck } from 'react-icons/fa'; 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 './style/Inicio.css';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@ -10,142 +8,19 @@ import { Link } from 'react-router-dom';
function Inicio() { function Inicio() {
const navigate = useNavigate(); const navigate = useNavigate();
const { getAuthorizationHeader, isAuthenticated } = useAuth();
const [pacientes, setPacientes] = useState([]); const [pacientes, setPacientes] = useState([]);
const [medicos, setMedicos] = useState([]);
const [agendamentos, setAgendamentos] = 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 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(); const hoje = new Date();
hoje.setHours(0, 0, 0, 0); const agendamentosDoDia = agendamentos.filter(
a => a.data && new Date(a.data).getDate() === hoje.getDate()
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 agendamentosHoje = agendamentosDoDia.length; const agendamentosHoje = agendamentosDoDia.length;
const pendencias = agendamentos.filter(a => a.status === 'pending' || a.status === 'scheduled').length;
return ( return (
<div className="dashboard-container"> <div className="dashboard-container">
<div className="dashboard-header"> <div className="dashboard-header">
@ -182,7 +57,7 @@ function Inicio() {
<div className="stat-card"> <div className="stat-card">
<div className="stat-info"> <div className="stat-info">
<span className="stat-label">PENDÊNCIAS</span> <span className="stat-label">PENDÊNCIAS</span>
<span className="stat-value">{loading ? '...' : pendencias}</span> <span className="stat-value">0</span>
</div> </div>
<div className="stat-icon-wrapper orange"><FaCalendarAlt className="stat-icon" /></div> <div className="stat-icon-wrapper orange"><FaCalendarAlt className="stat-icon" /></div>
</div> </div>
@ -217,54 +92,14 @@ function Inicio() {
<div className="appointments-section"> <div className="appointments-section">
<h2>Próximos Agendamentos</h2> <h2>Próximos Agendamentos</h2>
{loading ? ( {agendamentosHoje > 0 ? (
<div className="no-appointments-content"> <div>
<p>Carregando agendamentos...</p> {agendamentosDoDia.map(agendamento => (
</div>
) : agendamentosHoje > 0 ? (
<div className="agendamentos-list">
{agendamentosDoDia.slice(0, 5).map(agendamento => (
<div key={agendamento.id} className="agendamento-item"> <div key={agendamento.id} className="agendamento-item">
<div className="agendamento-info"> <p>{agendamento.nomePaciente}</p>
<div className="agendamento-time-date"> <p>{new Date(agendamento.data).toLocaleTimeString()}</p>
<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>
</div> </div>
))} ))}
{agendamentosHoje > 5 && (
<button className="view-all-button" onClick={() => navigate('/secretaria/agendamento')}>
Ver todos os {agendamentosHoje} agendamentos
</button>
)}
</div> </div>
) : ( ) : (
<div className="no-appointments-content"> <div className="no-appointments-content">

View File

@ -1,511 +1,337 @@
// src/pages/LaudoManager.jsx // src/pages/LaudoManager.jsx
import API_KEY from '../components/utils/apiKeys'; import React, { useState, useEffect } from "react";
import { Link } from 'react-router-dom'; import "./LaudoStyle.css"; // Importa o CSS externo
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';
const LaudoManager = () => { /* ===== Mock data (simula APIDOG) ===== */
const navigate = useNavigate(); function mockFetchLaudos() {
const { getAuthorizationHeader } = useAuth(); return [
const authHeader = getAuthorizationHeader(); {
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([]); function mockDeleteLaudo(id) {
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]); return new Promise((res) => setTimeout(() => res({ ok: true }), 500));
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);
const [showProtocolModal, setShowProtocolModal] = useState(false); /* ===== Componente ===== */
const [protocolForIndex, setProtocolForIndex] = useState(null); export default function LaudoManager() {
const [laudos, setLaudos] = useState([]);
const [openDropdownId, setOpenDropdownId] = useState(null);
const [paginaAtual, setPaginaAtual] = useState(1); /* viewerLaudo é usado para mostrar o editor/leitura;
const [itensPorPagina, setItensPorPagina] = useState(10); 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)); /* pesquisa */
const indiceInicial = (paginaAtual - 1) * itensPorPagina; const [query, setQuery] = useState("");
const indiceFinal = indiceInicial + itensPorPagina;
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal); /* Para simplificar: eu assumo aqui que estamos na visão da secretaria */
const isSecretary = true; // permanece true (somente leitura)
useEffect(() => { useEffect(() => {
let mounted = true; // Importa os dados mock apenas
const data = mockFetchLaudos();
const fetchReports = async () => { setLaudos(data);
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]);
// Fecha dropdown ao clicar fora
useEffect(() => { useEffect(() => {
const fetchRelData = async () => { function onDocClick(e) {
const pacientes = []; if (e.target.closest && e.target.closest('.action-btn')) return;
const medicos = []; if (e.target.closest && e.target.closest('.dropdown')) return;
for (let i = 0; i < relatoriosFiltrados.length; i++) { setOpenDropdownId(null);
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);
} }
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 { try {
const doctorId = rel.created_by || rel.requested_by || null; const resp = await mockDeleteLaudo(toDelete.id);
if (doctorId) { if (resp.ok || resp === true) {
const docRes = await GetDoctorByID(doctorId, authHeader); // removo o laudo da lista local
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes); setLaudos(curr => curr.filter(l => l.id !== toDelete.id));
setShowConfirmDelete(false);
setToDelete(null);
alert("Laudo excluído com sucesso.");
} else { } else {
medicos.push({ full_name: rel.requested_by || '' }); alert("Erro ao excluir. Tente novamente.");
} }
} catch (err) { } 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) => { /* filtro de pesquisa (por pedido ou nome do paciente) */
setRelatorioModal(relatorio); const normalized = (s = "") => String(s).toLowerCase();
setModalIndex(index); const filteredLaudos = laudos.filter(l => {
setShowModal(true); const q = normalized(query).trim();
}; if (!q) return true;
if (normalized(l.pedido).includes(q)) return true;
const limparFiltros = () => { if (normalized(l.paciente?.nome).includes(q)) return true;
setTermoPesquisa(''); return false;
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
}); });
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 ( return (
<div className="laudo-wrap">
<div className="left-col">
<div className="title-row">
<div> <div>
<div className="page-heading"><h3>Lista de Relatórios</h3></div> <div className="page-title">Gerenciamento de Laudo</div>
<div className="page-content"> {/* removi a linha "Visualização: Secretaria" conforme pedido */}
<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> </div>
</div> </div>
<div className="card-body"> <div style={{ marginBottom:12 }}>
<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 <input
type="text" placeholder="Pesquisar paciente ou pedido..."
className="form-control" value={query}
placeholder="Digite nome ou CPF do paciente..." onChange={e => setQuery(e.target.value)}
value={termoPesquisa} style={{ width:"100%", padding:12, borderRadius:8, border:"1px solid #e6eef8" }}
onChange={(e) => setTermoPesquisa(e.target.value)}
/> />
</div> </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"> {filteredLaudos.length === 0 ? (
<table className="table table-striped table-hover"> <div className="empty">Nenhum laudo encontrado.</div>
<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>
);
})
) : ( ) : (
<tr><td colSpan="4" className="text-center">Nenhum relatório encontrado.</td></tr> <div style={{ borderRadius:8, overflow:"visible", boxShadow:"0 0 0 1px #eef6ff" }}>
)} {filteredLaudos.map((l) => (
</tbody> <div className="laudo-row" key={l.id}>
</table> <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="row-actions">
<div className="d-flex justify-content-between align-items-center mt-3"> <div className="action-btn" onClick={(e)=> toggleDropdown(l.id, e)} title="Ações">
<div className="d-flex align-items-center"> <i class="bi bi-three-dots-vertical"></i>
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div> </div>
<div className="d-flex align-items-center"> {openDropdownId === l.id && (
<span className="me-3 text-muted"> <div className="dropdown" data-laudo-dropdown={l.id}>
Página {paginaAtual} de {totalPaginas} <div className="item" onClick={() => handleOpenViewer(l)}>Editar</div>
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, relatoriosFinais.length)} de {relatoriosFinais.length} itens <div className="item" onClick={() => handlePrint(l)}>Imprimir</div>
</span> <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>
<nav> <div className="item" onClick={() => handleRequestDelete(l)} style={{ color:"#c23b3b" }}>Excluir laudo</div>
<ul className="pagination pagination-sm mb-0"> </div>
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}> )}
<button className="page-link" onClick={voltarPagina}> </div>
<i className="bi bi-chevron-left"></i> </div>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))} ))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div> </div>
)} )}
</div> </div>
</div>
</div>
</div>
</section>
</div>
{/* Viewer modal (modo leitura) — só abre para quem tem permissão */}
{showModal && relatorioModal && ( {viewerLaudo && !showPreview && !isSecretary && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1"> <div className="viewer-modal" style={{ pointerEvents:"auto" }}>
<div className="modal-dialog modal-dialog-centered modal-lg"> <div className="modal-backdrop" onClick={() => setViewerLaudo(null)} />
<div className="modal-content"> <div className="modal-card" role="dialog" aria-modal="true">
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}> <div className="viewer-header">
<h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name || relatorioModal.patient_name || 'Paciente'}</h5> <div>
</div> <div style={{ fontSize:18, fontWeight:700 }}>{viewerLaudo.paciente.nome}</div>
<div className="patient-info">
<div className="modal-body"> Nasc.: {viewerLaudo.paciente.nascimento} {computeAge(viewerLaudo.paciente.nascimento)} anos {viewerLaudo.paciente.cpf} {viewerLaudo.paciente.convenio}
<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.'} />
</div> </div>
</div> </div>
<div style={{ marginTop: 20, padding: '0 6px' }}> <div style={{ display:"flex", gap:8 }}>
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatorioModal?.requested_by || '—'}</p> <button className="tool-btn" onClick={() => { setPreviewLaudo(viewerLaudo); setShowPreview(true); setViewerLaudo(null); }}>Pré-visualizar / Imprimir</button>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatorioModal?.created_at || '—'}</p> <button className="tool-btn" onClick={() => setViewerLaudo(null)}>Fechar</button>
</div>
</div> </div>
</div> </div>
<div className="modal-footer"> <div className="toolbar">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name || 'paciente', modalIndex)}> <div className="tool-btn">B</div>
<i className='bi bi-file-pdf-fill me-1'></i> Baixar em PDF <div className="tool-btn"><i>I</i></div>
</button> <div className="tool-btn"><u>U</u></div>
<button type="button" className="btn btn-secondary" onClick={() => { setShowModal(false) }}> <div className="tool-btn">Fonte</div>
Fechar <div className="tool-btn">Tamanho</div>
</button> <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>
</div> </div>
</div> </div>
)} )}
{/* Preview modal — agora não bloqueia a tela (sem backdrop escuro), botão imprimir é interativo */}
{showProtocolModal && protocolForIndex && ( {showPreview && previewLaudo && (
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1"> <div className="preview-modal" style={{ pointerEvents:"none" /* container não bloqueia */ }}>
<div className="modal-dialog modal-dialog-centered"> <div /* sem backdrop, assim não deixa a tela escura/blocked */ />
<div className="modal-content"> <div className="modal-card" style={{ maxWidth:900, pointerEvents:"auto" }}>
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}> <div style={{ display:"flex", justifyContent:"space-between", alignItems:"center", marginBottom:12 }}>
<h5 className="modal-title">Protocolo de Entrega - {protocolForIndex.relatorio?.patient_name || 'Paciente'}</h5> <div style={{ fontWeight:700 }}>Pré-visualização - {previewLaudo.paciente.nome}</div>
</div> <div style={{ display:"flex", gap:8 }}>
<button className="tool-btn" onClick={() => alert("Imprimir (simulado).")}>Imprimir / Download</button>
<div className="modal-body"> <button className="tool-btn" onClick={() => { setShowPreview(false); setPreviewLaudo(null); }}>Fechar</button>
<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>
</div> </div>
</div> </div>
<div className="modal-footer"> <div style={{ border: "1px solid #e6eef8", borderRadius:6, padding:18, background:"#fff" }}>
<button className="btn btn-primary" onClick={() => { <div style={{ marginBottom:8, fontSize:14, color:"#33475b" }}>
const idx = protocolForIndex.index ?? 0; <strong>RELATÓRIO MÉDICO</strong>
BaixarPDFdoRelatorio(protocolForIndex.relatorio?.patient_name || 'paciente', idx); </div>
}}> <div style={{ marginBottom:14, fontSize:13, color:"#546b7f" }}>
<i className='bi bi-file-earmark-pdf-fill me-1'></i> Baixar Protocolo (PDF) {previewLaudo.paciente.nome} Nasc.: {previewLaudo.paciente.nascimento} CPF: {previewLaudo.paciente.cpf}
</button> </div>
<button type="button" className="btn btn-secondary" onClick={() => setShowProtocolModal(false)}>
Fechar <div style={{ whiteSpace:"pre-wrap", fontSize:15, color:"#1f2d3d", lineHeight:1.5 }}>
</button> {previewLaudo.conteudo}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
)} )}
{noPermissionText && ( {/* Notificação simples: Sem permissão (exibe sem backdrop escuro) - centralizada */}
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1"> {showNoPermission && (
<div className="modal-dialog modal-dialog-centered"> <div className="notice-card" role="alert" aria-live="polite">
<div className="modal-content"> <div style={{ fontWeight:700, marginBottom:6 }}>Sem permissão para editar</div>
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}> <div style={{ marginBottom:10, color:"#5a6f80" }}>Você está na visualização da secretaria. Edição disponível somente para médicos autorizados.</div>
<h5 className="modal-title">Aviso</h5> <div style={{ textAlign:"right" }}>
</div> <button className="tool-btn" onClick={() => setShowNoPermission(false)}>Fechar</button>
<div className="modal-body">
<p>{noPermissionText}</p>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => setNoPermissionText(null)}>Fechar</button>
</div>
</div>
</div> </div>
</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> </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; color: #e0e0e0 !important;
box-shadow: 0 8px 30px rgba(10,20,40,0.32) !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 { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../components/utils/AuthProvider"; import { useAuth } from "../components/utils/AuthProvider";
import API_KEY from "../components/utils/apiKeys"; import API_KEY from "../components/utils/apiKeys";
import { UserInfos } from "../components/utils/Functions-Endpoints/General"; import { UserInfos } from "../components/utils/Functions-Endpoints/General";
import CabecalhoError from "../components/utils/fetchErros/CabecalhoError";
function Login({ onEnterSystem }) { function Login({ onEnterSystem }) {
const { setAuthTokens } = useAuth(); const { setAuthTokens } = useAuth();
const [showCabecalho, setShowCabecalho ] = useState(false)
const navigate = useNavigate(); const navigate = useNavigate();
const [form, setForm] = useState({ const [form, setForm] = useState({
username: "", username: "",
@ -118,7 +116,6 @@ function Login({ onEnterSystem }) {
if (data.access_token) { if (data.access_token) {
const UserData = await UserInfos(`bearer ${data.access_token}`); const UserData = await UserInfos(`bearer ${data.access_token}`);
console.log(UserData, "Dados do usuário"); console.log(UserData, "Dados do usuário");
localStorage.setItem("roleUser", UserData.roles)
if (UserData?.roles?.includes("admin")) { if (UserData?.roles?.includes("admin")) {
navigate(`/admin/`); navigate(`/admin/`);
@ -128,12 +125,7 @@ function Login({ onEnterSystem }) {
navigate(`/medico/`); navigate(`/medico/`);
} else if (UserData?.roles?.includes("financeiro")) { } else if (UserData?.roles?.includes("financeiro")) {
navigate(`/financeiro/`); navigate(`/financeiro/`);
} else if (UserData?.roles?.includes("paciente")) {
navigate(`/paciente/`);
} }
}else{
console.log("Erro na tentativa de login")
setShowCabecalho(true)
} }
} else { } else {
setAlert("Preencha todos os campos!"); setAlert("Preencha todos os campos!");
@ -156,7 +148,11 @@ function Login({ onEnterSystem }) {
<p className="auth-subtitle mb-5"> <p className="auth-subtitle mb-5">
Entre com os dados que você inseriu durante o registro. Entre com os dados que você inseriu durante o registro.
</p> </p>
<CabecalhoError showCabecalho={showCabecalho} message={"E-mail ou senha incorretos."}/> {alert && (
<div className="alert alert-info" role="alert">
{alert}
</div>
)}
<form onSubmit={handleLogin}> <form onSubmit={handleLogin}>
<div className="form-group position-relative has-icon-left mb-4"> <div className="form-group position-relative has-icon-left mb-4">
<input <input

View File

@ -1,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 { useLocation, useNavigate } from "react-router-dom";
import "./style/ProfilePage.css"; import "./style/ProfilePage.css";
const ROLES = { const simulatedUserData = {
ADMIN: "Administrador", email: "admin@squad23.com",
SECRETARY: "Secretária", role: "Administrador",
DOCTOR: "Médico",
FINANCIAL: "Financeiro"
}; };
const ProfilePage = () => { const ProfilePage = () => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const getRoleFromPath = useCallback(() => { const getRoleFromPath = () => {
const path = location.pathname; const path = location.pathname;
if (path.includes("/admin")) return ROLES.ADMIN; if (path.includes("/admin")) return "Administrador";
if (path.includes("/secretaria")) return ROLES.SECRETARY; if (path.includes("/secretaria")) return "Secretária";
if (path.includes("/medico")) return ROLES.DOCTOR; if (path.includes("/medico")) return "Médico";
if (path.includes("/financeiro")) return ROLES.FINANCIAL; if (path.includes("/financeiro")) return "Financeiro";
return "Usuário"; return "Usuário Padrão";
}, [location.pathname]); };
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 [userName, setUserName] = useState("Admin Padrão");
const [userEmail, setUserEmail] = useState("admin@squad23.com");
const [avatarUrl, setAvatarUrl] = useState(null);
const [isEditingName, setIsEditingName] = useState(false); const [isEditingName, setIsEditingName] = useState(false);
const [error, setError] = useState(null);
const handleNameKeyDown = (e) => {
useEffect(() => { if (e.key === "Enter") setIsEditingName(false);
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 handleClose = () => navigate(-1); const handleClose = () => navigate(-1);
@ -95,92 +47,54 @@ const ProfilePage = () => {
<div className="profile-content"> <div className="profile-content">
<div className="profile-left"> <div className="profile-left">
<div className="avatar-wrapper"> <div className="avatar-wrapper">
<div className="avatar-square"> <div className="avatar-square" />
{avatarUrl ? ( <button
<img className="avatar-edit-btn"
src={avatarUrl} title="Editar foto"
alt="Avatar do usuário" aria-label="Editar foto"
className="avatar-img" type="button"
onError={() => { >
setAvatarUrl(null);
localStorage.removeItem('user_avatar'); </button>
}}
/>
) : (
<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> </div>
</div> </div>
<div className="profile-right"> <div className="profile-right">
<div className="profile-name-row"> <div className="profile-name-row">
{isEditingName ? ( {isEditingName ? (
<div className="name-edit-wrapper">
<input <input
className="profile-name-input" className="profile-name-input"
value={userName} value={userName}
onChange={(e) => setUserName(e.target.value)} onChange={(e) => setUserName(e.target.value)}
onBlur={handleNameSave} onBlur={() => setIsEditingName(false)}
onKeyDown={handleNameKeyDown} onKeyDown={handleNameKeyDown}
autoFocus autoFocus
maxLength={50}
/> />
<div className="name-edit-hint">
Pressione Enter para salvar, ESC para cancelar
</div>
</div>
) : ( ) : (
<h2 className="profile-username"> <h2 className="profile-username">{userName}</h2>
{userName}
</h2>
)} )}
<button <button
className="profile-edit-inline" className="profile-edit-inline"
onClick={() => setIsEditingName(!isEditingName)} onClick={() => setIsEditingName(!isEditingName)}
aria-label="Editar nome"
type="button" type="button"
aria-label={isEditingName ? 'Cancelar edição' : 'Editar nome'}
> >
{isEditingName ? 'Cancelar' : 'Editar'}
</button> </button>
</div> </div>
{error && (
<div className="error-message">
{error}
</div>
)}
<div className="profile-info">
<p className="profile-email"> <p className="profile-email">
<span>Email:</span> Email: <strong>{userEmail}</strong>
<strong>{userEmail}</strong>
</p> </p>
<p className="profile-role"> <p className="profile-role">
<span>Cargo:</span> Cargo: <strong>{userRole}</strong>
<strong>{userRole}</strong>
</p> </p>
</div>
<div className="profile-actions"> <div className="profile-actions-row">
<button <button className="btn btn-close" onClick={handleClose}>
className="btn btn-close" Fechar
onClick={handleClose}
>
Fechar Perfil
</button> </button>
</div> </div>
</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 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 API_KEY from "../components/utils/apiKeys";
import { useAuth } from "../components/utils/AuthProvider"; import { useAuth } from "../components/utils/AuthProvider";
import "./style/TablePaciente.css"; import "./style/TablePaciente.css";
import ModalErro from "../components/utils/fetchErros/ModalErro"; import ModalErro from "../components/utils/fetchErros/ModalErro";
import manager from "../components/utils/fetchErros/ManagerFunction"; function TablePaciente({ setCurrentPage, setPatientID }) {
function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) { const { getAuthorizationHeader, isAuthenticated, RefreshingToken } = useAuth();
const { getAuthorizationHeader, isAuthenticated } = useAuth();
const navigate = useNavigate();
const [pacientes, setPacientes] = useState([]); const [pacientes, setPacientes] = useState([]);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
@ -24,20 +21,12 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
const [dataInicial, setDataInicial] = useState(""); const [dataInicial, setDataInicial] = useState("");
const [dataFinal, setDataFinal] = 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 [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedPatientId, setSelectedPatientId] = useState(null); const [selectedPatientId, setSelectedPatientId] = useState(null);
const [showModalError, setShowModalError] = useState(""); const [showModalError, setShowModalError] = useState(false);
const [ErrorInfo, setErrorInfo] = useState({}) const [ ErrorInfo, setErrorInfo] = useState({})
const GetAnexos = async (id) => { const GetAnexos = async (id) => {
var myHeaders = new Headers(); var myHeaders = new Headers();
@ -114,12 +103,7 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
} }
}; };
const RefreshingToken = () => {
console.log("Refreshing token...");
};
useEffect(() => { useEffect(() => {
const authHeader = getAuthorizationHeader() const authHeader = getAuthorizationHeader()
console.log(authHeader, 'aqui autorização') console.log(authHeader, 'aqui autorização')
@ -136,7 +120,7 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions) fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions)
.then(response => { .then(response => {
// 1. VERIFICAÇÃO DO STATUS HTTP (Se não for 2xx)
if (!response.ok) { if (!response.ok) {
return response.json().then(errorData => { return response.json().then(errorData => {
@ -152,23 +136,27 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
console.error("ERRO DETALHADO:", errorObject); console.error("ERRO DETALHADO:", errorObject);
throw errorObject; throw errorObject;
}); });
} }
// 3. Se a resposta for OK (2xx), processamos o JSON normalmente
return response.json(); return response.json();
}) })
.then(result => { .then(result => {
// 4. Bloco de SUCESSO
setPacientes(result); setPacientes(result);
console.log("Sucesso:", result); console.log("Sucesso:", result);
// IMPORTANTE: Se o modal estava aberto, feche-o no sucesso
setShowModalError(false); setShowModalError(false);
}) })
.catch(error => { .catch(error => {
console.error(error, "deu erro") // 5. Bloco de ERRO (Captura erros de rede ou o erro lançado pelo 'throw')
manager(setShowModalError, RefreshingToken, setErrorInfo, error) //console.error('Falha na requisição:', error.message);
if(error.httpStatus === 401){
RefreshingToken()
}
setErrorInfo(error)
setShowModalError(true);
}); });
}, [isAuthenticated, getAuthorizationHeader]); }, [isAuthenticated, getAuthorizationHeader]);
@ -207,7 +195,6 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
setIdadeMaxima(""); setIdadeMaxima("");
setDataInicial(""); setDataInicial("");
setDataFinal(""); setDataFinal("");
setPaginaAtual(1);
}; };
const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => { const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => {
@ -251,67 +238,13 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
return resultado; 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(() => { useEffect(() => {
setPaginaAtual(1); console.log(` Pacientes totais: ${pacientes?.length}, Filtrados: ${pacientesFiltrados?.length}`);
}, [search, filtroConvenio, filtroVIP, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal, sortKey, sortDir]); }, [pacientes, pacientesFiltrados, search]);
return ( return (
<> <>
<ModalErro showModal={showModalError} setShowModal={setShowModalError} ErrorData={ErrorInfo}/>
<div className="page-heading"> <div className="page-heading">
<h3>Lista de Pacientes</h3> <h3>Lista de Pacientes</h3>
</div> </div>
@ -363,7 +296,6 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
<button <button
className={`btn btn-sm ${filtroVIP ? "btn-primary" : "btn-outline-primary"}`} className={`btn btn-sm ${filtroVIP ? "btn-primary" : "btn-outline-primary"}`}
onClick={() => setFiltroVIP(!filtroVIP)}
style={{ padding: "0.25rem 0.5rem" }} style={{ padding: "0.25rem 0.5rem" }}
> >
<i className="bi bi-award me-1"></i> VIP <i className="bi bi-award me-1"></i> VIP
@ -377,33 +309,6 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
> >
<i className="bi bi-calendar me-1"></i> Aniversariantes <i className="bi bi-calendar me-1"></i> Aniversariantes
</button> </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>
<div className="d-flex justify-content-between align-items-center"> <div className="d-flex justify-content-between align-items-center">
@ -495,12 +400,31 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
</div> </div>
</div> </div>
)} )}
</div>
<div className="mt-3"> {(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante ||
<div className="contador-pacientes"> filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
{pacientesFiltrados.length} DE {pacientes.length} PACIENTES ENCONTRADOS <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> </div>
)}
<div className="mb-3">
<span className="badge results-badge">
{pacientesFiltrados?.length} de {pacientes?.length} pacientes encontrados
</span>
</div> </div>
<div className="table-responsive"> <div className="table-responsive">
@ -515,8 +439,8 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{pacientesPaginados.length > 0 ? ( {pacientesFiltrados.length > 0 ? (
pacientesPaginados.map((paciente) => ( pacientesFiltrados.map((paciente) => (
<tr key={paciente.id}> <tr key={paciente.id}>
<td> <td>
<div className="d-flex align-items-center patient-name-container"> <div className="d-flex align-items-center patient-name-container">
@ -544,21 +468,17 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
<td>{paciente.email || 'Não informado'}</td> <td>{paciente.email || 'Não informado'}</td>
<td> <td>
<div className="d-flex gap-2"> <div className="d-flex gap-2">
<Link to={"details"}> <Link to={`${paciente.id}`}>
<button className="btn btn-sm btn-view" onClick={() => setDictInfo(paciente)}> <button className="btn btn-sm btn-view">
<i className="bi bi-eye me-1"></i> Ver Detalhes <i className="bi bi-eye me-1"></i> Ver Detalhes
</button> </button>
</Link> </Link>
<button <Link to={`${paciente.id}/edit`}>
className="btn btn-sm btn-edit" <button className="btn btn-sm btn-edit">
onClick={() => {
setDictInfo(paciente);
navigate('edit');
}}
>
<i className="bi bi-pencil me-1"></i> Editar <i className="bi bi-pencil me-1"></i> Editar
</button> </button>
</Link>
<button <button
className="btn btn-sm btn-delete" className="btn btn-sm btn-delete"
@ -575,75 +495,13 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
)) ))
) : ( ) : (
<tr> <tr>
<td colSpan="5" className="text-center py-4"> <td colSpan="5" className="empty-state">
<div className="text-muted"> Nenhum paciente encontrado.
<i className="bi bi-search display-4"></i>
<p className="mt-2">Nenhum paciente encontrado com os filtros aplicados.</p>
{(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante ||
filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
<button className="btn btn-outline-primary btn-sm mt-2" onClick={limparFiltros}>
Limpar filtros
</button>
)}
</div>
</td> </td>
</tr> </tr>
)} )}
</tbody> </tbody>
</table> </table>
{pacientesFiltrados.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, pacientesFiltrados.length)} de {pacientesFiltrados.length} pacientes
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@ -665,10 +523,15 @@ function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
> >
<div className="modal-dialog modal-dialog-centered"> <div className="modal-dialog modal-dialog-centered">
<div className="modal-content"> <div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#dc3545', color: 'white' }}> <div className="modal-header">
<h5 className="modal-title"> <h5 className="modal-title">
Confirmação de Exclusão Confirmação de Exclusão
</h5> </h5>
<button
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
></button>
</div> </div>
<div className="modal-body"> <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 select,
.filtros-container input { .filtros-container input {
padding: 0.5rem; padding: 0.5rem;
@ -16,404 +14,416 @@
cursor: pointer; cursor: pointer;
} }
.unidade-selecionarprofissional { .unidade-selecionarprofissional{
background-color: #ffffff; background-color: #fdfdfdde;
padding: 20px 20px; padding: 20px 10px;
display: flex; display: flex;
justify-content: flex-start; border-radius:10px ;
align-items: center; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
} }
.busca-atendimento { .unidade-selecionarprofissional input, .unidade-selecionarprofissional select {
display: flex; margin-left: 8px;
flex-direction: row;
margin: 0;
justify-content: flex-start;
}
.busca-atendimento input {
border: 2px solid #000000;
border-radius: 8px; border-radius: 8px;
padding: 10px 15px; padding: 5px;
width: 100%; width: 20%;
font-size: 1rem;
margin-left: 0;
} }
.container-btns-agenda-fila_esepera { .unidade-selecionarprofissional select{
margin-top: 20px; width: 7%;
margin-left: 0; }
.busca-atendimento{
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 0; margin:10px;
border-bottom: 2px solid #E2E8F0; justify-content: flex-start;
margin-bottom: 20px;
} }
.btn-fila-espera, .btn-agenda { .busca-atendimento select{
background-color: transparent; padding:5px;
border: 0; border-radius:8px ;
border-bottom: 2px solid transparent; margin-left: 15px;
padding: 10px 12px; background-color: #0078d7;
border-radius: 0; color: white;
font-weight: 600; font-weight: bold;
color: #718096;
cursor: pointer;
margin-bottom: -2px;
transition: color 0.2s, border-color 0.2s;
} }
.btn-fila-espera:hover, .btn-agenda:hover { .busca-atendimento input{
color: #2B6CB0; margin-left: 8px;
} border-radius: 8px;
padding: 5px;
.opc-filaespera-ativo, .opc-agenda-ativo {
color: #4299E1;
background-color: transparent;
border-bottom: 2px solid #4299E1;
}
.input-e-dropdown-wrapper {
position: relative;
width: 100%; width: 100%;
margin: 0;
} }
.dropdown-medicos { .btn-selecionar-tabeladia, .btn-selecionar-tabelasemana, .btn-selecionar-tabelames {
position: absolute; background-color: rgba(231, 231, 231, 0.808);
top: 100%; padding:8px 10px;
left: 0; font-size: larger;
width: 100%; font-weight: bold;
z-index: 1000; border-style: hidden;
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 { .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; 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; } margin-top: 30px;
margin-bottom: 10px;
.info-date-display { background-color: #EDF2F7; border-radius: 8px; padding: 12px; text-align: center; margin-bottom: 16px; } gap: 15px;
.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; justify-content: flex-end;
gap: 12px;
margin-bottom: 16px;
} }
.legend-item { .legenda-item-realizado{
padding: 6px 12px; background-color: #2c5e37;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
color: #333;
} }
.legend-item[data-status="completed"] { .legenda-item-confirmed{
background-color: #C6F6D5; background-color: #1e90ff;
border: 1px solid #9AE6B4;
color: #2F855A;
} }
.legend-item[data-status="confirmed"] { .legenda-item-cancelado{
background-color: #EBF8FF; background-color: #d9534f;
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 { .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; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
flex-direction: row;
margin-top: 10px;
} }
.item-details { .calendario {
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; border-collapse: collapse;
min-width: 600px; 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);
} }
@media (max-width: 768px) { .calendario-ou-filaespera{
.busca-atendimento { margin-top: 0;
flex-direction: column; }
gap: 10px;
.container-btns-agenda-fila_esepera{
margin-top: 30px;
display: flex;
flex-direction: row;
gap: 20px;
margin-left:20px ;
}
.btn-fila-espera, .btn-agenda{
background-color: transparent;
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: white;
background-color: #5980fd;
}
html[data-bs-theme="dark"] {
body {
background-color: #121212;
color: #e0e0e0;
} }
.container-btns-agenda-fila_esepera { .calendario {
flex-direction: column; background-color: #1e1e1e;
align-items: flex-start; border: 10px solid #333;
gap: 10px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
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;
} }
.unidade-selecionarprofissional { .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; flex-direction: column;
align-items: stretch; }
#tabela-seletor-container {
display: flex;
align-items: center;
justify-content: center;
gap: 12px; gap: 12px;
}
.calendar-wrapper { background-color: #fff;
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;
border-radius: 8px; border-radius: 8px;
margin-bottom: 16px; padding: 6px 12px;
} box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
table { font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto;
min-width: 600px; width: fit-content;
font-size: 0.875rem; margin: 0 auto;
} }
table th, #tabela-seletor-container p {
table td { margin: 0;
padding: 8px; font-size: 23px;
font-weight: 500;
color: #4085f6;
text-align: center;
white-space: nowrap; 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 */ #tabela-seletor-container button {
.btns-gerenciamento-e-consulta { background: transparent;
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;
border: none; border: none;
} color: #555;
font-size: 20px;
/* garante mesma cor dos blocos da secretaria */ cursor: pointer;
.btn-consulta-paciente.btn-primary { padding: 4px 6px;
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;
border-radius: 6px; border-radius: 6px;
font-weight: 500; transition: all 0.2s ease-in-out;
border: none; }
#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 { .modal-header-success {
background-color: #1e3a8a !important; background-color: #28a745 !important;
} }
.modal-header-error { .modal-header-error {
@ -104,12 +104,12 @@
} }
.modal-button-success { .modal-button-success {
background-color: #1e3a8a; background-color: #28a745;
color: #fff; color: #fff;
} }
.modal-button-success:hover { .modal-button-success:hover {
background-color: #1e3a8a; background-color: #218838;
} }
.modal-button-error { .modal-button-error {
@ -186,8 +186,10 @@
outline: 2px solid #0056b3; outline: 2px solid #0056b3;
outline-offset: 2px; outline-offset: 2px;
} }
/* Garantir que as cores dos cabeçalhos sejam aplicadas */
.modal-overlay .modal-container .modal-header.modal-header-success { .modal-overlay .modal-container .modal-header.modal-header-success {
background-color: #1e3a8a !important; background-color: #28a745 !important;
} }
.modal-overlay .modal-container .modal-header.modal-header-error { .modal-overlay .modal-container .modal-header.modal-header-error {
@ -256,7 +258,7 @@
} }
.modal-header-success { .modal-header-success {
background-color: #1e3a8a !important; background-color: #006400 !important;
} }
.modal-header-error { .modal-header-error {

View File

@ -190,12 +190,6 @@ html, body {
} }
/* ===== Fila de Espera ===== */ /* ===== Fila de Espera ===== */
@media (max-width: 992px) {
.fila-container {
overflow-x: auto;
}
}
.fila-container { .fila-container {
width: 100%; width: 100%;
max-width: none; max-width: none;
@ -262,15 +256,6 @@ html, body {
font-size: 1.5rem; 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 { .fila-header {
position: relative; position: relative;
display: flex; display: flex;
@ -292,13 +277,6 @@ html, body {
transition: border-color 0.2s; transition: border-color 0.2s;
} }
@media (max-width: 768px) {
.busca-fila-espera {
width: 100%;
position: static;
}
}
.busca-fila-espera:focus { .busca-fila-espera:focus {
border-color: #888; border-color: #888;
} }

View File

@ -21,12 +21,6 @@
margin-bottom: 10px; margin-bottom: 10px;
} }
@media (max-width: 1200px) {
.summary-card {
min-width: 180px;
}
}
.summary-card { .summary-card {
flex: 1; flex: 1;
min-width: 200px; min-width: 200px;
@ -45,7 +39,6 @@
margin: 0 0 8px 0; margin: 0 0 8px 0;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #fff;
opacity: 0.9; opacity: 0.9;
} }
@ -114,87 +107,43 @@
} }
/* Botões de ação */ /* Botões de ação */
.action-group {
display: flex;
gap: 8px;
align-items: center;
}
.btn-view { .action-btn {
background-color: #E6F2FF !important;
color: #004085 !important;
border: 1px solid #B8D4F0 !important;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
border-radius: 6px;
cursor: pointer; cursor: pointer;
transition: all 0.15s ease-in-out; padding: 6px 12px;
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;
border-radius: 6px; border-radius: 6px;
cursor: pointer; border: 1px solid #d7e6fb;
transition: all 0.15s ease-in-out; background: #fff;
text-decoration: none; transition: all 0.2s ease;
display: inline-block; font-size: 13px;
text-align: center;
} }
.btn-edit:hover { .action-btn:hover {
background-color: #FFEEBA !important; background: #f6f9fc;
border-color: #FFE087 !important; border-color: #93c5fd;
} }
.btn-delete:hover { .action-btn.delete {
background-color: #F1B0B7 !important; border-color: #fca5a5;
border-color: #ED969E !important; color: #b91c1c;
}
.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;
} }
html[data-bs-theme="dark"] .btn-view { .action-btn.delete:hover {
background-color: #1e3a8a !important; background: #fee2e2;
color: #e0e0e0 !important; border-color: #ef4444;
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;
} }
/* Badges de status */ /* Badges de status */
.badge { .badge {
display: inline-block; display: inline-block;
padding: 8px 18px !important; padding: 4px 10px;
border-radius: 9999px; border-radius: 9999px;
font-size: 14px !important; font-size: 12px;
font-weight: 600; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
} }
@ -233,18 +182,12 @@ html[data-bs-theme="dark"] .btn-delete {
padding: 24px; padding: 24px;
width: 100%; width: 100%;
max-width: 550px; max-width: 550px;
max-height: 85vh; max-height: 90vh;
overflow-y: auto;
box-sizing: border-box; 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); 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 { .modal-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -255,7 +198,7 @@ html[data-bs-theme="dark"] .btn-delete {
.modal-header h2 { .modal-header h2 {
font-size: 20px; font-size: 20px;
font-weight: 700; font-weight: 700;
color: #fff; color: #1f2937;
margin: 0; margin: 0;
} }
@ -265,12 +208,6 @@ html[data-bs-theme="dark"] .btn-delete {
gap: 16px; gap: 16px;
} }
.modal-card .input-field,
.modal-card .select-field,
.modal-card textarea {
width: 100%;
}
.form-group { .form-group {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -304,23 +241,12 @@ html[data-bs-theme="dark"] .btn-delete {
gap: 10px; gap: 10px;
margin-top: 24px; 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 */ /* Inputs e selects */
.input-field, .input-field,
.select-field, .select-field,
textarea { textarea {
width: 100%;
padding: 10px 12px; padding: 10px 12px;
border: 1px solid #d1d5db; border: 1px solid #d1d5db;
border-radius: 8px; border-radius: 8px;
@ -343,26 +269,6 @@ textarea {
min-height: 80px; 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 */ /* Mensagem quando não há pagamentos */
.empty { .empty {
text-align: center; 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 */ /* Container Principal */
.dashboard-container { .dashboard-container {
padding: 2rem; padding: 2rem;
@ -201,10 +70,10 @@
} }
/* Cores dos ícones */ /* Cores dos ícones */
.stat-icon-wrapper.blue { background-color: #1D3B88; } .stat-icon-wrapper.blue { background-color: #5d5dff; }
.stat-icon-wrapper.green { background-color: #399CE5; } .stat-icon-wrapper.green { background-color: #30d158; }
.stat-icon-wrapper.purple { background-color: #5F5DF2; } .stat-icon-wrapper.purple { background-color: #a272ff; }
.stat-icon-wrapper.orange { background-color: #051AFF; } .stat-icon-wrapper.orange { background-color: #f1952e; }
/* Seção de Ações Rápidas */ /* Seção de Ações Rápidas */
.quick-actions h2 { .quick-actions h2 {
@ -360,191 +229,3 @@ html[data-bs-theme="dark"] .manage-button {
html[data-bs-theme="dark"] .manage-button:hover { html[data-bs-theme="dark"] .manage-button:hover {
background-color: #2323b0; 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 { .modal-header.success {
background-color: #1e3a8a !important; background-color: #28a745 !important;
} }
.modal-header.error { .modal-header.error {
@ -168,11 +168,11 @@
} }
.modal-confirm-button.success { .modal-confirm-button.success {
background-color: #1e3a8a !important; background-color: #28a745 !important;
} }
.modal-confirm-button.success:hover { .modal-confirm-button.success:hover {
background-color: #1e3a8a !important; background-color: #218838 !important;
} }
.modal-confirm-button.error { .modal-confirm-button.error {

View File

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

View File

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

View File

@ -1,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 { .table-paciente-container {
line-height: 2.5; line-height: 2.5;
@ -59,6 +49,7 @@
background-color: rgba(0, 0, 0, 0.025); background-color: rgba(0, 0, 0, 0.025);
} }
.insurance-badge { .insurance-badge {
background-color: #6c757d !important; background-color: #6c757d !important;
color: white !important; color: white !important;
@ -90,6 +81,7 @@
font-size: 0.75em; font-size: 0.75em;
} }
.btn-view { .btn-view {
background-color: #E6F2FF !important; background-color: #E6F2FF !important;
color: #004085 !important; color: #004085 !important;
@ -129,6 +121,7 @@
border-color: #ED969E; border-color: #ED969E;
} }
.advanced-filters { .advanced-filters {
border: 1px solid #dee2e6; border: 1px solid #dee2e6;
border-radius: 0.375rem; border-radius: 0.375rem;
@ -155,6 +148,7 @@
font-weight: 600; font-weight: 600;
} }
.empty-state { .empty-state {
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
@ -171,6 +165,7 @@
padding: 0.4em 0.65em; padding: 0.4em 0.65em;
} }
.table-paciente-table tbody tr { .table-paciente-table tbody tr {
transition: background-color 0.15s ease-in-out; transition: background-color 0.15s ease-in-out;
} }
@ -181,6 +176,7 @@
transition: all 0.15s ease-in-out; transition: all 0.15s ease-in-out;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.table-paciente-table { .table-paciente-table {
font-size: 0.875rem; font-size: 0.875rem;
@ -217,7 +213,6 @@
margin-left: 0 !important; margin-left: 0 !important;
} }
} }
.compact-select { .compact-select {
font-size: 1.0rem; font-size: 1.0rem;
padding: 0.45rem 0.5rem; padding: 0.45rem 0.5rem;
@ -232,130 +227,8 @@
white-space: nowrap; white-space: nowrap;
} }
.table-paciente-filters .d-flex { .table-paciente-filters .d-flex {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
} }
/* ===== ESTILOS PARA PAGINAÇÃO ===== */
.contador-pacientes {
background-color: #1e3a8a;
color: white;
padding: 0.5em 0.75em;
font-size: 0.875em;
font-weight: 500;
border-radius: 0.375rem;
text-align: center;
display: inline-block;
}
/* Estilos para a paginação */
.pagination {
margin-bottom: 0;
}
.page-link {
color: #495057;
border: 1px solid #dee2e6;
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.page-link:hover {
color: #1e3a8a;
background-color: #e9ecef;
border-color: #dee2e6;
}
.page-item.active .page-link {
background-color: #1e3a8a;
border-color: #1e3a8a;
color: white;
}
.page-item.disabled .page-link {
color: #6c757d;
background-color: #f8f9fa;
border-color: #dee2e6;
}
/* Ajustes para a seção de paginação */
.d-flex.justify-content-between.align-items-center {
border-top: 1px solid #dee2e6;
padding-top: 1rem;
margin-top: 1rem;
}
/* Estilos para empty state */
.text-center.py-4 .text-muted {
padding: 2rem;
}
.text-center.py-4 .bi-search {
font-size: 3rem;
opacity: 0.5;
}
.text-center.py-4 p {
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.text-center.py-4 td {
border-bottom: none;
padding: 2rem !important;
}
/* Responsividade para paginação */
@media (max-width: 768px) {
.d-flex.justify-content-between.align-items-center {
flex-direction: column;
gap: 1rem;
align-items: stretch !important;
}
.d-flex.justify-content-between.align-items-center > div {
justify-content: center !important;
}
.pagination {
flex-wrap: wrap;
justify-content: center;
}
.me-3.text-muted {
text-align: center;
margin-bottom: 0.5rem;
font-size: 0.8rem;
}
.contador-pacientes {
font-size: 0.8rem;
padding: 0.4em 0.6em;
}
}
/* Ajuste para o select de itens por página */
.form-select.form-select-sm.w-auto {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
font-size: 0.875rem;
}
/* Melhorar a aparência dos badges de filtros ativos */
.filters-active .badge {
font-size: 0.75em;
padding: 0.4em 0.65em;
margin-bottom: 0.25rem;
}
@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 UserDashboard from '../../PagesAdm/gestao.jsx';
import PainelAdministrativo from '../../PagesAdm/painel.jsx'; import PainelAdministrativo from '../../PagesAdm/painel.jsx';
import admItems from "../../data/sidebar-items-adm.json"; import admItems from "../../data/sidebar-items-adm.json";
import {useState} from "react"
// ...restante do código... // ...restante do código...
function Perfiladm() { function Perfiladm() {
const [DictInfo, setDictInfo] = useState({})
return ( return (
<div id="app" className="active"> <div id="app" className="active">
@ -30,12 +27,12 @@ function Perfiladm() {
<Route path="/" element={<UserDashboard />} /> <Route path="/" element={<UserDashboard />} />
<Route path="/pacientes/cadastro" element={<PatientCadastroManager />} /> <Route path="/pacientes/cadastro" element={<PatientCadastroManager />} />
<Route path="/medicos/cadastro" element={<DoctorCadastroManager />} /> <Route path="/medicos/cadastro" element={<DoctorCadastroManager />} />
<Route path="/pacientes" element={<TablePaciente setDictInfo={setDictInfo}/>} /> <Route path="/pacientes" element={<TablePaciente />} />
<Route path="/medicos" element={<DoctorTable setDictInfo={setDictInfo} />} /> <Route path="/medicos" element={<DoctorTable />} />
<Route path="/pacientes/details" element={<Details DictInfo={DictInfo} />} /> <Route path="/pacientes/:id" element={<Details />} />
<Route path="/pacientes/edit" element={<EditPage DictInfo={DictInfo} />} /> <Route path="/pacientes/:id/edit" element={<EditPage />} />
<Route path="/medicos/details" element={<DoctorDetails DictInfo={DictInfo}/>} /> <Route path="/medicos/:id" element={<DoctorDetails />} />
<Route path="/medicos/edit" element={<DoctorEditPage DictInfo={DictInfo}/>} /> <Route path="/medicos/:id/edit" element={<DoctorEditPage />} />
<Route path="/agendamento" element={<Agendamento />} /> <Route path="/agendamento" element={<Agendamento />} />
<Route path="/laudo" element={<LaudoManager />} /> <Route path="/laudo" element={<LaudoManager />} />

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