Compare commits

...

111 Commits

Author SHA1 Message Date
0c633f57fd consertando algumas coisas 2025-12-03 22:34:14 -03:00
17a69ed57b agendamento de consulta do medico e paciente 2025-12-03 22:25:55 -03:00
94c0dd13dc agendamento de consulta do medico e paciente 2025-12-03 21:55:15 -03:00
7a9f2b3da2 responsividade agendamento 2025-12-03 16:52:11 -03:00
cfd4790bf1 responsividade agendamento 2025-12-03 16:21:31 -03:00
eff11fc075 resolucao de erros consulta 2025-12-02 18:14:25 -03:00
6f3a49575c Arummando o agendamento 2025-12-02 14:04:36 -03:00
07ed113291 adicionando paciente 2025-12-02 04:16:13 -03:00
0de772457e ajuste gerais 2025-12-02 03:57:57 -03:00
c736f44604 Responsividade do projeto 2025-12-02 03:16:26 -03:00
3b8e73535b Merge branch 'ajuste-nas-tabelas' 2025-12-01 20:42:07 -03:00
e5e5d0928b Ajustes nas tabelas 2025-12-01 19:49:57 -03:00
e5f260e7c3 adicçoes na consulta 2025-12-01 18:04:38 -03:00
RafaelMTA13
b208d2ac73 Resolvendo conflito no package-lock 2025-11-27 13:59:45 -03:00
b1ac9ea3ff Um pequeno detalhe 2025-11-27 12:23:06 -03:00
d67f4d6db4 Ultimas correções 2025-11-27 12:05:41 -03:00
176489f9fd Merge branch 'main' into aperfeicoamento-do-design 2025-11-27 01:17:34 -03:00
b46da18c45 Correções de design 2025-11-27 01:02:34 -03:00
ef7ef93887 Resolucao de conflitos 2025-11-26 22:37:53 -03:00
6312a72895 Minhas alterações nos detalhes 2025-11-26 22:20:38 -03:00
57f8024bb2 Minhas alterações nos detalhes 2025-11-26 22:15:58 -03:00
RafaelMTA13
220e436fa0 Criação da tela de relatório com IA e integração backend 2025-11-26 15:50:28 -03:00
b0ba36507b ajuste no calendario 2025-11-25 18:48:47 -03:00
d979105ad1 merge loginesuporte 2025-11-20 16:16:05 -03:00
e11bda0d96 resolução de erros 2025-11-20 16:14:33 -03:00
a94a0caee6 Merge branch 'main' of https://git.popcode.com.br/RiseUP/riseup-squad23 2025-11-20 15:03:25 -03:00
5aaaf7d3e5 Disponibilidades Atualizadas 2025-11-20 14:58:38 -03:00
5d1751b7f9 pequenas mudanças no perfil e no suporte 2025-11-20 11:27:31 -03:00
496a83ecc1 Atualizacao de modais e ajuste de tabela 2025-11-19 19:50:55 -03:00
6ccb0992c3 ajeitei a solicitar agendamento e fila de espera 2025-11-19 16:29:26 -03:00
63121d6702 Merge branch 'main' of https://git.popcode.com.br/RiseUP/riseup-squad23 into Disponibilidades5 2025-11-19 12:26:07 -03:00
3dac02b650 Melhorias da disponibilidade 2025-11-19 12:19:45 -03:00
joao_pedro
ad7c300a44 concertos no ver detalhes e editar 2025-11-19 09:01:44 -03:00
joao_pedro
1d048023b1 bug: detalhes e editar do doctor 2025-11-15 19:04:09 -03:00
815ce759cd agendamento resolvido 2025-11-14 21:27:04 -03:00
23a912e1c0 agendamento resolvido 2025-11-14 21:06:51 -03:00
8879c43fa8 agendamento resolvido 2025-11-14 21:05:01 -03:00
joao_pedro
b64b664621 design: mudança no botão de alternar no agendamento e mudança na sidebar 2025-11-13 10:20:04 -03:00
bff5c42a4e Paciente-inicio 2025-11-13 09:30:03 -03:00
joao_pedro
981c4bac6e bug: solução do edit para o paciente 2025-11-13 08:35:29 -03:00
2647691ae2 novo agendamento 2025-11-12 18:11:55 -03:00
d6bd5b955a merge detalhesrelatorio 2025-11-12 12:24:45 -03:00
dd598a4ce3 relatorio secretaria e vizualização dos relatorios medicos 2025-11-12 12:05:15 -03:00
9c9f551ca6 Merge remote-tracking branch 'origin/alteracoes-de-estilizacoes' 2025-11-10 19:25:14 -03:00
2f2f2964ba mudança na sidebar e nas tabelas 2025-11-10 19:07:57 -03:00
joao_pedro
03c32138c6 tirar o id da url quando entra no ver detalhes ou edit 2025-11-10 16:10:48 -03:00
joao_pedro
251aa95f63 dropdown para selecionar pacientes no agendamento 2025-11-10 09:33:53 -03:00
joao_pedro
f9db6c4eec Mudanças na Sidebar 2025-11-08 09:58:53 -03:00
4d1522fac5 Alterações do conflito 2025-11-06 21:56:07 -03:00
246f7cebe5 Merge da branch atualizacao-toggle 2025-11-06 15:26:18 -03:00
310bccbe6f Erro no doctorform 2025-11-06 11:00:46 -03:00
fa3c9fea16 mergerelatoriosnovo 2025-11-06 09:38:51 -03:00
d327b226c9 apirelatorio 2025-11-06 09:37:27 -03:00
f332ee0147 Merge branch 'main' of https://git.popcode.com.br/RiseUP/riseup-squad23 2025-11-05 23:25:02 -03:00
bbb61fab06 Merge branch 'Disponibilidade5' 2025-11-05 23:20:12 -03:00
b334fc82c7 Alterações nas Disponibilidades 2025-11-05 23:18:04 -03:00
cfdfd61040 Modificacoes-da-Tabela 2025-11-05 21:47:04 -03:00
e007c167e7 Adicionado-novas-tabelas 2025-11-05 21:27:13 -03:00
joao_pedro
a3f1116608 merge com melhorias agendamentos 2025-11-05 21:03:56 -03:00
joao_pedro
bd1ea9a206 Funcionalidade para mudar os botões se for cancelled ou confirmed 2025-11-05 20:32:27 -03:00
joao_pedro
0129482b7c concerto do bug na pagina de atendimento para o paciente e melhoria do design 2025-11-05 19:13:26 -03:00
joao_pedro
41d46cc148 Bug resolvido com o status da consulta e checkbox melhorado 2025-11-05 18:32:35 -03:00
joao_pedro
f4ac580b83 Detalhes esteticos finalizados 2025-11-05 16:58:12 -03:00
2d926ecbcd Merge branch 'main' of https://git.popcode.com.br/RiseUP/riseup-squad23 2025-11-05 15:02:59 -03:00
joao_pedro
c1d4e81acf Merge com melhoriasAgendamento 2025-11-05 12:58:21 -03:00
9f4e288a02 Merge branch 'main' of https://git.popcode.com.br/RiseUP/riseup-squad23 into Disponibilidade5 2025-11-05 00:04:45 -03:00
b29dac619c Salvamrnto padrao 2025-11-05 00:03:43 -03:00
joao_pedro
0f177a05a4 Spinner para representar carregamento e ele de inicio agora ele carrega sem precisar interagir 2025-11-04 19:29:18 -03:00
Eduarda-SS
a709dffde8 Merge branch 'main' of https://git.popcode.com.br/RiseUP/riseup-squad23 2025-11-04 16:40:42 -03:00
Eduarda-SS
f03fc733c2 Buscar médico pelo nome exceções 2025-11-04 16:20:14 -03:00
d8e63f8abe Atualizações de estilos 2025-11-04 15:39:02 -03:00
joao_pedro
0b5ae1f2cb Adicao de erros 2025-11-04 11:54:19 -03:00
joao_pedro
cbfca9d6b4 Bug concertado nos agendamentos dos perfis 2025-11-04 11:53:45 -03:00
joao_pedro
7557aa28ea Trocando cor quando deleta ou confirma, no medico e paciente 2025-11-04 10:23:36 -03:00
joao_pedro
49e25c0511 Possibilidade do paciente apagar e editar uma consulta 2025-11-04 08:30:57 -03:00
joao_pedro
c502b73a95 O medico pode alterar as consultas 2025-11-04 07:37:53 -03:00
035b4ff5d0 Merge branch 'main' of https://git.popcode.com.br/RiseUP/riseup-squad23 2025-11-02 18:10:47 -03:00
joao_pedro
0b7e863461 acesso de agendamento para o médico 2025-11-02 14:34:23 -03:00
joao_pedro
0f94fc4446 Alterações 2025-11-01 17:36:33 -03:00
joao_pedro
65b7590bee Consegui trocar pela tabela 2025-11-01 16:17:33 -03:00
19718d5eed Merge branch 'toggleSidebar' of https://git.popcode.com.br/RiseUP/riseup-squad23 2025-11-01 11:51:04 -03:00
joao_pedro
30af16708e Arqivos não salvos antes 2025-10-31 20:13:50 -03:00
joao_pedro
ce3f8e23dd Toggle finalizado sem estilização 2025-10-31 20:12:42 -03:00
RafaelMTA13
55e66657ba Adição de chamada por video 2025-10-31 17:52:34 -03:00
77b934636f Mudanças design 2025-10-30 17:23:23 -03:00
52ae210ff7 mergerelatoriosnovo 2025-10-30 11:34:58 -03:00
0957eee3a7 Merge pull request 'Implementa ajustes na disponibilidade' (#9) from Disponibilidade4 into main
Reviewed-on: #9
2025-10-30 13:53:11 +00:00
e11d86db67 Resolução dos problemas na logica da pagina de Disponibiidades 2025-10-30 10:46:22 -03:00
5a493d8776 Implementa ajustes na disponibilidade 2025-10-30 10:43:01 -03:00
joao_pedro
22fd2bf847 bug no fetchErros encontrado e resolvido 2025-10-30 10:01:13 -03:00
joao_pedro
a3bd204b88 bug da tabela de paciente encontrado e resolvido 2025-10-30 10:01:13 -03:00
joao_pedro
8b0fd68d6a Bug nos cards de agendamento encontrado e resolvido 2025-10-30 10:01:13 -03:00
dcaae077c4 endpoints medico e paciente 2025-10-29 23:38:42 -03:00
c180e9a5c9 Atualização semanal 2025-10-29 21:11:13 -03:00
joao_pedro
274bbef699 Mergin com melhoriasAgendamentos 2025-10-29 20:30:13 -03:00
joao_pedro
2ab1c875b3 Voltei a lógica normal dos botões dos cards, precisa melhorar 2025-10-29 18:45:34 -03:00
joao_pedro
bf467a44f4 Mudar cores dos cards de consulta dinamicamente 2025-10-29 18:34:09 -03:00
RafaelMTA13
f80e0eee81 Implementação da IA 2025-10-29 16:02:47 -03:00
joao_pedro
dbfc0de85d Começo da melhoria para troca de cores 2025-10-29 10:45:34 -03:00
Eduarda-SS
626fcc8124 Updates de exceção 2025-10-28 22:57:08 -03:00
Eduarda-SS
a996de3edd Merge branch 'ExcecoesUpdate' into main 2025-10-28 22:36:59 -03:00
Eduarda-SS
6255ebfb9d Update de exceções para API de produção 2025-10-28 22:33:24 -03:00
joao_pedro
dc9ae674fc Alterações que não foram mandadas 2025-10-28 15:45:07 -03:00
joao_pedro
ede2be7edd Salvar pequenas alterações 2025-10-28 15:42:21 -03:00
joao_pedro
f11bf05cc0 Botão para transformar agendamentos cancelados em aprovados 2025-10-28 11:36:35 -03:00
joao_pedro
b4d5ed76a9 Mudança que não foi no ultimo commit 2025-10-28 08:22:32 -03:00
joao_pedro
15062ca32e filtro de médico resolvido e mostrar os dias ao lado da data na tabela diaria 2025-10-28 08:18:20 -03:00
joao_pedro
8a3d6e0305 Impedimento de refetching e melhoria do filtro do medico 2025-10-27 10:44:09 -03:00
joao_pedro
a5cd4d3447 erros 2025-10-27 08:39:56 -03:00
joao_pedro
a8fc3eb397 Criacao manager e erro login 2025-10-25 17:04:39 -03:00
joao_pedro
95054bb9c1 Erro de login 2025-10-25 17:01:39 -03:00
102 changed files with 18467 additions and 7207 deletions

2
.env Normal file
View File

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

View File

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

View File

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

6941
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,16 @@
"dependencies": {
"@ckeditor/ckeditor5-build-classic": "^41.4.2",
"@ckeditor/ckeditor5-react": "^11.0.0",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-solid-svg-icons": "^7.1.0",
"@fortawesome/react-fontawesome": "^3.1.0",
"@fullcalendar/core": "^6.1.19",
"@fullcalendar/daygrid": "^6.1.19",
"@fullcalendar/interaction": "^6.1.19",
"@fullcalendar/react": "^6.1.19",
"@fullcalendar/timegrid": "^6.1.19",
"@jitsi/react-sdk": "^1.4.0",
"@supabase/supabase-js": "^2.86.0",
"@sweetalert2/theme-dark": "^5.0.27",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
@ -18,10 +28,17 @@
"apexcharts": "^5.3.4",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"dayjs": "^1.11.18",
"cors": "^2.8.5",
"dayjs": "^1.11.19",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"firebase": "^12.5.0",
"flatpickr": "^4.6.13",
"helmet": "^8.1.0",
"html2pdf.js": "^0.12.1",
"lucide-react": "^0.543.0",
"node-fetch": "^3.3.2",
"openai": "^6.7.0",
"perfect-scrollbar": "^1.5.6",
"powershell": "^2.3.3",
"quill": "^2.0.3",
@ -33,9 +50,11 @@
"react-flatpickr": "^4.0.11",
"react-icons": "^5.5.0",
"react-input-mask": "^2.0.4",
"react-is": "^19.2.0",
"react-quill": "^2.0.0",
"react-router-dom": "^7.9.2",
"react-scripts": "5.0.1",
"react-toastify": "^11.0.5",
"recharts": "^3.1.2",
"sweetalert2": "^11.22.4",
"tiptap": "^1.32.2",
@ -72,5 +91,8 @@
"sass": "^1.91.0",
"sass-loader": "^16.0.5",
"tailwindcss": "^4.1.13"
},
"overrides": {
"react": "$react"
}
}

38
server.js Normal file
View File

@ -0,0 +1,38 @@
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import OpenAI from "openai";
dotenv.config();
const app = express();
app.use(cors());
app.use(express.json());
const client = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // Coloque sua chave no .env
});
app.post("/api/chat", async (req, res) => {
try {
const { message } = req.body;
const completion = await client.chat.completions.create({
model: "gpt-4o-mini", // modelo rápido e leve
messages: [
{
role: "system",
content: "Você é a assistente virtual do site Mediconnect, chamada Ágatha. Responda de forma amigável e informativa, explicando sobre o funcionamento do site, cadastro, agendamento, e suporte técnico.",
},
{ role: "user", content: message },
],
});
const resposta = completion.choices[0].message.content;
res.json({ resposta });
} catch (error) {
console.error("Erro no servidor:", error);
res.status(500).json({ erro: "Erro ao conectar com a IA" });
}
});
app.listen(5000, () => console.log("Servidor rodando na porta 5000"));

View File

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

View File

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

View File

@ -0,0 +1,90 @@
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,32 +5,109 @@ import { useState, useEffect } from 'react';
import { useAuth } from '../components/utils/AuthProvider';
import { GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
import { useNavigate } from 'react-router-dom';
import html2pdf from 'html2pdf.js';
import TiptapViewer from './TiptapViewer';
import './styleMedico/DoctorRelatorioManager.css';
const DoctorRelatorioManager = () => {
const navigate = useNavigate();
const { getAuthorizationHeader } = useAuth();
const authHeader = getAuthorizationHeader();
const [RelatoriosFiltrados, setRelatorios] = useState([]);
const [PacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [MedicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [relatoriosOriginais, setRelatoriosOriginais] = useState([]);
const [relatoriosFiltrados, setRelatoriosFiltrados] = useState([]);
const [relatoriosFinais, setRelatoriosFinais] = useState([]);
const [pacientesComRelatorios, setPacientesComRelatorios] = useState([]);
const [medicosComRelatorios, setMedicosComRelatorios] = useState([]);
const [showModal, setShowModal] = useState(false);
const [index, setIndex] = useState();
const [relatorioModal, setRelatorioModal] = useState(null);
const [termoPesquisa, setTermoPesquisa] = useState('');
const [filtroExame, setFiltroExame] = useState('');
const [examesDisponiveis, setExamesDisponiveis] = useState([]);
const [modalIndex, setModalIndex] = useState(0);
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const totalPaginas = Math.max(1, Math.ceil(relatoriosFinais.length / itensPorPagina));
const indiceInicial = (paginaAtual - 1) * itensPorPagina;
const indiceFinal = indiceInicial + itensPorPagina;
const relatoriosPaginados = relatoriosFinais.slice(indiceInicial, indiceFinal);
useEffect(() => {
let mounted = true;
const fetchReports = async () => {
try {
var myHeaders = new Headers();
const myHeaders = new Headers();
myHeaders.append('apikey', API_KEY);
if (authHeader) myHeaders.append('Authorization', authHeader);
var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
const requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow' };
let userId = null;
let userFullName = null;
try {
const token = authHeader ? authHeader.replace(/^Bearer\s+/i, '') : '';
if (token) {
const userInfo = await UserInfos(token);
userId = userInfo?.id || userInfo?.user?.id || userInfo?.sub || null;
userFullName = userInfo?.full_name || (userInfo?.user && userInfo.user.full_name) || null;
}
} catch (err) {
console.warn('Não foi possível obter UserInfos (pode não estar logado):', err);
}
const baseUrl = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?select=*";
let data = [];
if (userId) {
try {
const res = await fetch(`${baseUrl}&doctor_id=eq.${userId}`, requestOptions);
data = await res.json();
} catch (e) {
console.warn('Erro ao buscar por doctor_id:', e);
data = [];
}
if ((!Array.isArray(data) || data.length === 0) && userId) {
try {
const res2 = await fetch(`${baseUrl}&created_by=eq.${userId}`, requestOptions);
data = await res2.json();
} catch (e) {
console.warn('Erro ao buscar por created_by:', e);
data = [];
}
}
if ((!Array.isArray(data) || data.length === 0) && userFullName) {
try {
const encodedName = encodeURIComponent(userFullName);
const res3 = await fetch(`${baseUrl}&requested_by=eq.${encodedName}`, requestOptions);
data = await res3.json();
} catch (e) {
console.warn('Erro ao buscar por requested_by:', e);
data = [];
}
}
}
if (!userId || (!Array.isArray(data) || data.length === 0)) {
try {
const resAll = await fetch(baseUrl, requestOptions);
data = await resAll.json();
} catch (e) {
console.error('Erro listar relatórios (busca completa):', e);
data = [];
}
}
const 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 => {
@ -39,10 +116,18 @@ const DoctorRelatorioManager = () => {
const unique = Array.from(uniqueMap.values())
.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0));
if (mounted) setRelatorios(unique);
if (mounted) {
setRelatoriosOriginais(unique);
setRelatoriosFiltrados(unique);
setRelatoriosFinais(unique);
}
} catch (err) {
console.error('Erro listar relatórios', err);
if (mounted) setRelatorios([]);
console.error('Erro listar relatórios (catch):', err);
if (mounted) {
setRelatoriosOriginais([]);
setRelatoriosFiltrados([]);
setRelatoriosFinais([]);
}
}
};
@ -57,25 +142,33 @@ const DoctorRelatorioManager = () => {
};
}, [authHeader]);
useEffect(() => {
const fetchRelData = async () => {
const pacientes = [];
const medicos = [];
for (let i = 0; i < RelatoriosFiltrados.length; i++) {
const rel = RelatoriosFiltrados[i];
for (let i = 0; i < relatoriosFinais.length; i++) {
const rel = relatoriosFinais[i];
try {
const pacienteRes = await GetPatientByID(rel.patient_id, authHeader);
pacientes.push(Array.isArray(pacienteRes) ? pacienteRes[0] : pacienteRes);
} catch (err) {
pacientes.push(null);
}
try {
const doctorId = rel.created_by || rel.requested_by || null;
if (doctorId) {
const docRes = await GetDoctorByID(doctorId, authHeader);
if (rel.doctor_id) {
const docRes = await GetDoctorByID(rel.doctor_id, authHeader);
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
} else if (rel.created_by) {
const docRes = await GetDoctorByID(rel.created_by, authHeader);
medicos.push(Array.isArray(docRes) ? docRes[0] : docRes);
} else if (rel.requested_by) {
medicos.push({ full_name: rel.requested_by });
} else {
medicos.push({ full_name: rel.requested_by || '' });
medicos.push({ full_name: '' });
}
} catch (err) {
medicos.push({ full_name: rel.requested_by || '' });
@ -84,12 +177,29 @@ const DoctorRelatorioManager = () => {
setPacientesComRelatorios(pacientes);
setMedicosComRelatorios(medicos);
};
if (RelatoriosFiltrados.length > 0) fetchRelData();
if (relatoriosFinais.length > 0) fetchRelData();
else {
setPacientesComRelatorios([]);
setMedicosComRelatorios([]);
}
}, [RelatoriosFiltrados, authHeader]);
}, [relatoriosFinais, authHeader]);
const abrirModal = (relatorio, pageIndex) => {
const globalIndex = relatoriosFinais.findIndex(r => r.id === relatorio.id);
const indexToUse = globalIndex >= 0 ? globalIndex : (indiceInicial + pageIndex);
setRelatorioModal(relatorio);
setModalIndex(indexToUse);
setShowModal(true);
};
const limparFiltros = () => {
setTermoPesquisa('');
setFiltroExame('');
setRelatoriosFinais(relatoriosOriginais);
setPaginaAtual(1);
};
const BaixarPDFdoRelatorio = (nome_paciente, idx) => {
const elemento = document.getElementById(`folhaA4-${idx}`);
@ -106,20 +216,50 @@ const DoctorRelatorioManager = () => {
html2pdf().set(opt).from(elemento).save();
};
const irParaPagina = (pagina) => {
setPaginaAtual(pagina);
};
const avancarPagina = () => {
if (paginaAtual < totalPaginas) {
setPaginaAtual(paginaAtual + 1);
}
};
const voltarPagina = () => {
if (paginaAtual > 1) {
setPaginaAtual(paginaAtual - 1);
}
};
const gerarNumerosPaginas = () => {
const paginas = [];
const paginasParaMostrar = 5;
let inicio = Math.max(1, paginaAtual - Math.floor(paginasParaMostrar / 2));
let fim = Math.min(totalPaginas, inicio + paginasParaMostrar - 1);
inicio = Math.max(1, fim - paginasParaMostrar + 1);
for (let i = inicio; i <= fim; i++) {
paginas.push(i);
}
return paginas;
};
return (
<div>
{showModal && (
<div className="modal modal-centered" role="dialog" aria-modal="true" onClick={() => setShowModal(false)}>
{/* aqui: classe modal-dialog-square para ficar quadrado */}
<div className="modal-dialog modal-dialog-square" role="document" onClick={(e) => e.stopPropagation()}>
<div className="modal fade show" style={{ display: "block", backgroundColor: "rgba(0, 0, 0, 0.5)" }} tabIndex="-1">
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content">
<div className="modal-header custom-modal-header">
<h5 className="modal-title">Relatório de {PacientesComRelatorios[index]?.full_name}</h5>
<button type="button" className="btn-close modal-close-btn" aria-label="Close" onClick={() => setShowModal(false)}></button>
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
<h5 className="modal-title">Relatório de {pacientesComRelatorios[modalIndex]?.full_name}</h5>
</div>
<div className="modal-body">
<div id={`folhaA4-${index}`} className="folhaA4">
<div id={`folhaA4-${modalIndex}`} className="folhaA4">
<div id='header-relatorio' style={{ textAlign: 'center', marginBottom: 24 }}>
<p style={{ margin: 0 }}>Clinica Rise up</p>
<p style={{ margin: 0 }}>Dr - CRM/SP 123456</p>
@ -127,28 +267,28 @@ const DoctorRelatorioManager = () => {
</div>
<div id='infoPaciente' style={{ padding: '0 6px' }}>
<p><strong>Paciente:</strong> {PacientesComRelatorios[index]?.full_name}</p>
<p><strong>Data de nascimento:</strong> {PacientesComRelatorios[index]?.birth_date || '—'}</p>
<p><strong>Data do exame:</strong> {RelatoriosFiltrados[index]?.due_at || '—'}</p>
<p><strong>Paciente:</strong> {pacientesComRelatorios[modalIndex]?.full_name}</p>
<p><strong>Data de nascimento:</strong> {pacientesComRelatorios[modalIndex]?.birth_date || '—'}</p>
<p><strong>Data do exame:</strong> {relatoriosFinais[modalIndex]?.due_at || '—'}</p>
<p style={{ marginTop: 12, fontWeight: '700' }}>Conteúdo do Relatório:</p>
<div className="tiptap-viewer-wrapper">
<TiptapViewer htmlContent={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 style={{ marginTop: 20, padding: '0 6px' }}>
<p>Dr {MedicosComRelatorios[index]?.full_name || RelatoriosFiltrados[index]?.requested_by}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {RelatoriosFiltrados[index]?.created_at || '—'}</p>
<p>Dr {medicosComRelatorios[modalIndex]?.full_name || relatoriosFinais[modalIndex]?.requested_by}</p>
<p style={{ color: '#6c757d', fontSize: '0.95rem' }}>Emitido em: {relatoriosFinais[modalIndex]?.created_at || '—'}</p>
</div>
</div>
</div>
<div className="modal-footer custom-modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(PacientesComRelatorios[index]?.full_name, index)}>
<i className='bi bi-file-pdf-fill'></i> baixar em pdf
<div className="modal-footer">
<button className="btn btn-primary" onClick={() => BaixarPDFdoRelatorio(pacientesComRelatorios[modalIndex]?.full_name, modalIndex)}>
<i className='bi bi-file-pdf-fill me-1'></i> Baixar em PDF
</button>
<button type="button" className="btn btn-outline-secondary" onClick={() => { setShowModal(false) }}>
<button type="button" className="btn btn-secondary" onClick={() => { setShowModal(false) }}>
Fechar
</button>
</div>
@ -157,7 +297,6 @@ const DoctorRelatorioManager = () => {
</div>
)}
{/* restante da página (lista) permanece igual */}
<div className="page-heading"><h3>Lista de Relatórios</h3></div>
<div className="page-content">
<section className="row">
@ -166,14 +305,51 @@ const DoctorRelatorioManager = () => {
<div className="card-header d-flex justify-content-between align-items-center">
<h4 className="card-title mb-0">Relatórios Cadastrados</h4>
<Link to={'criar'}>
<button className="btn btn-primary"><i className="bi bi-plus-circle"></i> Adicionar Relatório</button>
<button className="btn btn-primary">
<i className="bi bi-plus-circle"></i> Adicionar Relatório
</button>
</Link>
</div>
<div className="card-body">
<div className="card p-3 mb-3">
<h5 className="mb-3"><i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros</h5>
<div className="d-flex flex-nowrap align-items-center gap-2" style={{ overflowX: "auto", paddingBottom: "6px" }}>
<input type="text" className="form-control" placeholder="Buscar por nome..." style={{ minWidth: 250, maxWidth: 300, width: 260, flex: "0 0 auto" }} />
<h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i> Filtros
</h5>
<div className="row">
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Buscar por nome ou CPF do paciente</label>
<input
type="text"
className="form-control"
placeholder="Digite nome ou CPF do paciente..."
value={termoPesquisa}
onChange={(e) => setTermoPesquisa(e.target.value)}
/>
</div>
</div>
<div className="col-md-5">
<div className="mb-3">
<label className="form-label">Filtrar por tipo de exame</label>
<input
type="text"
className="form-control"
placeholder="Digite o tipo de exame..."
value={filtroExame}
onChange={(e) => setFiltroExame(e.target.value)}
/>
</div>
</div>
<div className="col-md-2 d-flex align-items-end">
<button className="btn btn-outline-secondary w-100" onClick={limparFiltros}>
<i className="bi bi-arrow-clockwise"></i> Limpar
</button>
</div>
</div>
<div className="mt-2">
<div className="contador-relatorios">
{relatoriosFinais.length} DE {relatoriosOriginais.length} RELATÓRIOS ENCONTRADOS
</div>
</div>
</div>
@ -182,36 +358,92 @@ const DoctorRelatorioManager = () => {
<thead>
<tr>
<th>Paciente</th>
<th>Doutor</th>
<th>CPF</th>
<th>Exame</th>
<th></th>
</tr>
</thead>
<tbody>
{RelatoriosFiltrados.length > 0 ? (
RelatoriosFiltrados.map((relatorio, idx) => (
<tr key={relatorio.id}>
<td className='infos-paciente'>{PacientesComRelatorios[idx]?.full_name}</td>
<td className='infos-paciente'>{MedicosComRelatorios[idx]?.full_name || relatorio.requested_by || '-'}</td>
<td>
<div className="d-flex gap-2">
<button className="btn btn-sm" style={{ backgroundColor: "#E6F2FF", color: "#004085" }} onClick={() => { setShowModal(true); setIndex(idx); }}>
<i className="bi bi-eye me-1"></i> Ver Detalhes
</button>
<button className="btn btn-sm" style={{ backgroundColor: "#FFF3CD", color: "#856404" }} onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<i className="bi bi-pencil me-1"></i> Editar
</button>
</div>
</td>
</tr>
))
{relatoriosPaginados.length > 0 ? (
relatoriosPaginados.map((relatorio, index) => {
const globalIndex = relatoriosFinais.findIndex(r => r.id === relatorio.id);
const paciente = pacientesComRelatorios[globalIndex];
return (
<tr key={relatorio.id}>
<td>{paciente?.full_name || 'Carregando...'}</td>
<td>{paciente?.cpf || 'Carregando...'}</td>
<td>{relatorio.exam}</td>
<td>
<div className="d-flex gap-2">
<button className="btn btn-sm btn-ver-detalhes" onClick={() => abrirModal(relatorio, index)}>
<i className="bi bi-eye me-1"></i> Ver Detalhes
</button>
<button className="btn btn-sm btn-editar" onClick={() => navigate(`/medico/relatorios/${relatorio.id}/edit`)}>
<i className="bi bi-pencil me-1"></i> Editar
</button>
</div>
</td>
</tr>
);
})
) : (
<tr><td colSpan="3" className="text-center">Nenhum paciente encontrado.</td></tr>
<tr><td colSpan="4" className="text-center">Nenhum relatório encontrado.</td></tr>
)}
</tbody>
</table>
</div>
{relatoriosFinais.length > 0 && (
<div className="d-flex justify-content-between align-items-center mt-3">
<div className="d-flex align-items-center">
<span className="me-2 text-muted">Itens por página:</span>
<select
className="form-select form-select-sm w-auto"
value={itensPorPagina}
onChange={(e) => {
setItensPorPagina(Number(e.target.value));
setPaginaAtual(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
</select>
</div>
<div className="d-flex align-items-center">
<span className="me-3 text-muted">
Página {paginaAtual} de {totalPaginas}
Mostrando {indiceInicial + 1}-{Math.min(indiceFinal, relatoriosFinais.length)} de {relatoriosFinais.length} itens
</span>
<nav>
<ul className="pagination pagination-sm mb-0">
<li className={`page-item ${paginaAtual === 1 ? 'disabled' : ''}`}>
<button className="page-link" onClick={voltarPagina}>
<i className="bi bi-chevron-left"></i>
</button>
</li>
{gerarNumerosPaginas().map(pagina => (
<li key={pagina} className={`page-item ${pagina === paginaAtual ? 'active' : ''}`}>
<button className="page-link" onClick={() => irParaPagina(pagina)}>
{pagina}
</button>
</li>
))}
<li className={`page-item ${paginaAtual === totalPaginas ? 'disabled' : ''}`}>
<button className="page-link" onClick={avancarPagina}>
<i className="bi bi-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
)}
</div>
</div>
</div>
</div>

View File

@ -109,7 +109,6 @@ const EditPageRelatorio = () => {
const body = JSON.stringify({ content_html: html });
// supabase: PATCH via query id=eq.<id>
const res = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/reports?id=eq.${params.id}`, {
method: 'PATCH',
headers: myHeaders,
@ -123,8 +122,23 @@ const EditPageRelatorio = () => {
throw new Error('Erro na API');
}
// Recebe o dado atualizado e atualiza o estado do componente
let updatedData;
try {
updatedData = await res.json();
} catch (e) {
updatedData = null;
}
const updatedReport = Array.isArray(updatedData) ? updatedData[0] : updatedData;
if (updatedReport) {
setReport(updatedReport);
setHtml(updatedReport.content_html || '');
}
alert('Relatório atualizado com sucesso!');
navigate('/medico/relatorios');
} catch (err) {
console.error(err);
alert('Erro ao salvar. Veja console.');

View File

@ -1,24 +1,25 @@
// src/PagesMedico/FormNovoRelatorio.jsx
import React, { useEffect, useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import API_KEY from '../components/utils/apiKeys';
import { useAuth } from '../components/utils/AuthProvider';
import TiptapEditor from './TiptapEditor';
import { GetAllPatients, GetPatientByID } from '../components/utils/Functions-Endpoints/Patient';
import { GetAllDoctors, GetDoctorByID } from '../components/utils/Functions-Endpoints/Doctor';
import { GetAllPatients } from '../components/utils/Functions-Endpoints/Patient';
import { GetAllDoctors } from '../components/utils/Functions-Endpoints/Doctor';
import { UserInfos } from '../components/utils/Functions-Endpoints/General';
import './styleMedico/FormNovoRelatorio.css';
const FormNovoRelatorio = () => {
const { getAuthorizationHeader } = useAuth();
const authHeader = getAuthorizationHeader();
const navigate = useNavigate();
const location = useLocation();
const [patients, setPatients] = useState([]);
const [doctors, setDoctors] = useState([]);
const [loadingPatients, setLoadingPatients] = useState(true);
const [loadingDoctors, setLoadingDoctors] = useState(true);
// formulário
const [form, setForm] = useState({
patient_id: '',
patient_name: '',
@ -28,19 +29,15 @@ const FormNovoRelatorio = () => {
contentHtml: '',
});
// campos de busca (texto)
const [patientQuery, setPatientQuery] = useState('');
const [doctorQuery, setDoctorQuery] = useState('');
// dropdown control
const [showPatientDropdown, setShowPatientDropdown] = useState(false);
const [showDoctorDropdown, setShowDoctorDropdown] = useState(false);
const patientRef = useRef();
const doctorRef = useRef();
const [lockedFromAppointment, setLockedFromAppointment] = useState(false);
useEffect(() => {
// carregar pacientes e médicos
let mounted = true;
const loadPatients = async () => {
setLoadingPatients(true);
@ -69,7 +66,6 @@ const FormNovoRelatorio = () => {
return () => { mounted = false; };
}, [authHeader]);
// fechar dropdowns quando clicar fora
useEffect(() => {
const handleClick = (e) => {
if (patientRef.current && !patientRef.current.contains(e.target)) setShowPatientDropdown(false);
@ -103,7 +99,6 @@ const FormNovoRelatorio = () => {
`;
};
// escolher paciente (clicando na lista)
const choosePatient = async (patient) => {
setForm(prev => ({
...prev,
@ -127,7 +122,6 @@ const FormNovoRelatorio = () => {
setShowDoctorDropdown(false);
};
// filtrar pela query (startsWith)
const filteredPatients = patientQuery
? patients.filter(p => (p.full_name || '').toLowerCase().startsWith(patientQuery.toLowerCase())).slice(0, 40)
: [];
@ -138,7 +132,24 @@ const FormNovoRelatorio = () => {
const handleEditorChange = (html) => setForm(prev => ({ ...prev, contentHtml: html }));
// salvar novo relatório (agora com Prefer: return=representation e dispatch para refresh)
useEffect(() => {
if (location && location.state && location.state.appointment) {
const appt = location.state.appointment;
const paciente_nome = location.state.paciente_nome || appt.paciente_nome || '';
const medico_nome = location.state.medico_nome || appt.medico_nome || '';
setForm(prev => ({
...prev,
patient_id: appt.patient_id || prev.patient_id,
patient_name: paciente_nome || prev.patient_name,
patient_birth: prev.patient_birth || '',
doctor_id: appt.doctor_id || prev.doctor_id,
doctor_name: medico_nome || prev.doctor_name,
contentHtml: generateTemplate(paciente_nome, prev.patient_birth || '', medico_nome)
}));
setLockedFromAppointment(true);
}
}, [location]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!form.patient_id) return alert('Selecione o paciente (clicando no item) antes de salvar.');
@ -150,18 +161,33 @@ const FormNovoRelatorio = () => {
if (authHeader) myHeaders.append('Authorization', authHeader);
myHeaders.append('Content-Type', 'application/json');
myHeaders.append('Accept', 'application/json');
// pedir que o Supabase retorne a representação do registro criado
myHeaders.append('Prefer', 'return=representation');
// monta o payload apenas com campos válidos
const payload = {
patient_id: form.patient_id,
content: form.contentHtml,
content_html: form.contentHtml,
requested_by: form.doctor_name || ''
};
// só inclui created_by se tiver um id válido
if (form.doctor_id) payload.created_by = form.doctor_id;
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', {
@ -171,11 +197,10 @@ const FormNovoRelatorio = () => {
});
if (!res.ok) {
// tenta ler JSON, se não for JSON lê o texto
let txt;
try {
txt = await res.json();
} catch (err) {
} catch {
txt = await res.text();
}
console.error('Erro POST criar relatório:', res.status, txt);
@ -183,11 +208,7 @@ const FormNovoRelatorio = () => {
}
const created = await res.json();
console.log('Relatório criado:', created);
// dispara refresh global para a lista (DoctorRelatorioManager está escutando)
window.dispatchEvent(new Event('reports:refresh'));
alert('Relatório criado com sucesso!');
navigate('/medico/relatorios');
} catch (err) {
@ -207,11 +228,12 @@ const FormNovoRelatorio = () => {
<input
className="form-control"
placeholder="Comece a digitar (ex.: m para pacientes que começam com m)"
value={patientQuery}
onChange={(e) => { setPatientQuery(e.target.value); setShowPatientDropdown(true); }}
onFocus={() => setShowPatientDropdown(true)}
value={lockedFromAppointment ? form.patient_name : patientQuery}
onChange={(e) => { if (!lockedFromAppointment) { setPatientQuery(e.target.value); setShowPatientDropdown(true); } }}
onFocus={() => { if (!lockedFromAppointment) setShowPatientDropdown(true); }}
disabled={lockedFromAppointment}
/>
{showPatientDropdown && patientQuery && (
{!lockedFromAppointment && showPatientDropdown && patientQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}>
{filteredPatients.length > 0 ? filteredPatients.map(p => (
<li key={p.id} className="list-group-item list-group-item-action" onClick={() => choosePatient(p)}>
@ -228,11 +250,12 @@ const FormNovoRelatorio = () => {
<input
className="form-control"
placeholder="Comece a digitar o nome do médico"
value={doctorQuery}
onChange={(e) => { setDoctorQuery(e.target.value); setShowDoctorDropdown(true); }}
onFocus={() => setShowDoctorDropdown(true)}
value={lockedFromAppointment ? form.doctor_name : doctorQuery}
onChange={(e) => { if (!lockedFromAppointment) { setDoctorQuery(e.target.value); setShowDoctorDropdown(true); } }}
onFocus={() => { if (!lockedFromAppointment) setShowDoctorDropdown(true); }}
disabled={lockedFromAppointment}
/>
{showDoctorDropdown && doctorQuery && (
{!lockedFromAppointment && showDoctorDropdown && doctorQuery && (
<ul className="list-group position-absolute" style={{ zIndex: 50, maxHeight: 220, overflowY: 'auto', width: '100%' }}>
{filteredDoctors.length > 0 ? filteredDoctors.map(d => (
<li key={d.id} className="list-group-item list-group-item-action" onClick={() => chooseDoctor(d)}>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,20 +4,42 @@ import "./style/styleTabelas/tabelasemana.css";
import dayjs from 'dayjs';
import { useEffect, useState, useMemo } from 'react';
import weekOfYear from 'dayjs/plugin/weekOfYear'
import Spinner from '../Spinner';
dayjs.extend(weekOfYear)
const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteModal ,setSelectedId ,setDictInfo}) => {
const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteModal ,setSelectedId ,setDictInfo, setShowConfirmModal, coresConsultas ,setListaConsultaID, listaConsultasID}) => {
// Armazena o objeto COMPLETO das semanas organizadas
const [semanasOrganizadas, setSemanasOrganizadas] = useState({});
// Controla qual semana está sendo exibida (o índice da chave no objeto)
const [Indice, setIndice] = useState(0);
const [showSpinner, setShowSpinner] = useState(true)
useEffect(() => {
if (!agendamentos) return;
const semanas = Object.keys(agendamentos);
if (semanas.length > 0) {
setIndice(0)
setShowSpinner(false)
}
}, [agendamentos]);
console.log(agendamentos, "agendamentos diarios")
const dataHoje = dayjs();
const AnoAtual = dataHoje.year();
const mes = dataHoje.month() + 1;
let DiasdoMes = ListarDiasdoMes(AnoAtual, mes)
// Array de chaves (ex: ['semana40', 'semana41', ...])
const chavesDasSemanas = Object.keys(semanasOrganizadas);
@ -46,30 +68,34 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
segunda: [], terça: [], quarta: [], quinta: [], sexta: []
}
}
console.log(diaSemana)
switch (diaSemana) {
case 'Monday':
case 'segunda-feira':
console.log("segunda")
semanas[semanaKey].segunda.push(...agendamentos[DiaComAtendimento])
break
case 'Tuesday':
case 'terça-feira':
semanas[semanaKey].terça.push(...agendamentos[DiaComAtendimento])
break
case 'Wednesday':
case 'quarta-feira':
semanas[semanaKey].quarta.push(...agendamentos[DiaComAtendimento])
break
case 'Thursday':
case 'quinta-feira':
semanas[semanaKey].quinta.push(...agendamentos[DiaComAtendimento])
break
case 'Friday':
case 'sexta-feira':
semanas[semanaKey].sexta.push(...agendamentos[DiaComAtendimento])
break
default:
break
}
}
console.log(semanas, "agendamentos semanais")
return semanas
}, [agendamentos, AnoAtual]) // Adicionei AnoAtual como dependência por segurança
}, [agendamentos, AnoAtual])
// --- EFEITO PARA POPULAR O ESTADO ---
@ -123,10 +149,10 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
? `Semana ${chaveDaSemanaAtual.replace('semana', '')} / ${AnoAtual}`
: 'Nenhuma semana encontrada';
// --- RENDERIZAÇÃO ---
return (
<div>
{/* Container de Navegação */}
<div id='tabela-seletor-container'>
<button
@ -159,54 +185,65 @@ const TabelaAgendamentoSemana = ({ agendamentos, ListarDiasdoMes, setShowDeleteM
<tbody>
{indicesDeLinha.map((indiceLinha) => {
let schedulet_at = semanaParaRenderizar.segunda[indiceLinha].scheduled_at.split("T")
//let schedulet_at = semanaParaRenderizar.segunda[indiceLinha].scheduled_at.split("T")
let horario = schedulet_at[1].split(":")
// let horario = schedulet_at[1].split(":")
console.log(horario)
console.log(semanaParaRenderizar, "aqui")
return(
<tr key={indiceLinha}>
{/* Célula para Horário (Pode ser ajustado para mostrar o horário real) */}
<td>
<p className='horario-texto'> {`${horario[0]}:${horario[1]}`} </p>
{/* <p className='horario-texto'> {`${horario[0]}:${horario[1]}`} </p>*/}
</td>
{/* Mapeamento de COLUNAS (dias) */}
<td>
{semanaParaRenderizar.segunda[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.segunda[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />
{semanaParaRenderizar?.segunda[indiceLinha]
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar?.segunda[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
: null
}
</td>
<td>
{semanaParaRenderizar.terça[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.terça[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/>
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.terça[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
: null
}
</td>
<td>
{semanaParaRenderizar.quarta[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.quarta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo}/>
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.quarta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
: null
}
</td>
<td>
{semanaParaRenderizar.quinta[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.quinta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.quinta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID}/>
: null
}
</td>
<td>
{semanaParaRenderizar.sexta[indiceLinha]
? <CardConsulta DadosConsulta={semanaParaRenderizar.sexta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} />
? <CardConsulta TabelaAgendamento={'semana'} DadosConsulta={semanaParaRenderizar.sexta[indiceLinha]} setShowDeleteModal={setShowDeleteModal} setSelectedId={setSelectedId} setDictInfo={setDictInfo} setShowConfirmModal={setShowConfirmModal} coresConsultas={coresConsultas} setListaConsultaID={setListaConsultaID} listaConsultasID={listaConsultasID} />
: null
}
</td>
</tr>
)})}
{showSpinner &&
<tr>
<td colspan='6'>
<Spinner/>
</td>
</tr>
}
</tbody>
</table>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,339 @@
/* ========== 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,14 @@
// src/components/FormCriarExcecao.jsx
import React, { useState } from "react";
// Assumindo que você usa o mesmo estilo
import React, { useState, useEffect } from "react";
import { useAuth } from "./utils/AuthProvider";
import API_KEY from "./utils/apiKeys";
import "./AgendarConsulta/style/formagendamentos.css";
import { GetAllDoctors } from './utils/Functions-Endpoints/Doctor';
const ENDPOINT_CRIAR_EXCECAO = "https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_exceptions";
const ENDPOINT_CRIAR_EXCECAO = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_exceptions";
const FormCriarExcecao = ({ onCancel, doctorID }) => {
const { getAuthorizationHeader, user, getUserInfo } = useAuth();
const [dadosAtendimento, setDadosAtendimento] = useState({
profissional: doctorID || '',
tipoAtendimento: '',
@ -17,6 +18,13 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
motivo: ''
});
const [todosProfissionais, setTodosProfissionais] = useState([]);
const [profissionaisFiltrados, setProfissionaisFiltrados] = useState([]);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [doctorSearchName, setDoctorSearchName] = useState('');
const [searchingDoctor, setSearchingDoctor] = useState(false);
const handleAtendimentoChange = (e) => {
const { value, name } = e.target;
setDadosAtendimento(prev => ({
@ -25,38 +33,119 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
}));
};
useEffect(() => {
const loadDoctors = async () => {
setSearchingDoctor(true);
let authHeader = '';
try { authHeader = getAuthorizationHeader ? getAuthorizationHeader() : ''; } catch {}
try {
const Medicos = await GetAllDoctors(authHeader);
setTodosProfissionais(Array.isArray(Medicos) ? Medicos : []);
} catch (err) {
console.error('Erro ao carregar médicos:', err);
setTodosProfissionais([]);
} finally {
setSearchingDoctor(false);
}
};
loadDoctors();
}, [getAuthorizationHeader]);
const handleSearchProfissional = (e) => {
const term = e.target.value;
setDoctorSearchName(term);
if (term.trim() === '') {
setProfissionaisFiltrados([]);
setIsDropdownOpen(false);
return;
}
const filtered = todosProfissionais.filter(p =>
(p.full_name || '').toLowerCase().includes(term.toLowerCase())
);
setProfissionaisFiltrados(filtered);
setIsDropdownOpen(filtered.length > 0);
};
const handleSelectProfissional = (profissional) => {
setDadosAtendimento(prev => ({
...prev,
profissional: profissional.id
}));
setDoctorSearchName(profissional.full_name || '');
setProfissionaisFiltrados([]);
setIsDropdownOpen(false);
};
// lista simples de valores permitidos
const ALLOWED_KINDS = ['disponibilidade_extra', 'bloqueio'];
const handleSubmitExcecao = async (e) => {
e.preventDefault();
console.log("Tentando criar Exceção.");
const { profissional, dataAtendimento, tipoAtendimento, inicio, termino, motivo } = dadosAtendimento;
// Validação
if (!profissional || !dataAtendimento || !tipoAtendimento || !motivo) {
alert("Por favor, preencha o ID do Profissional, Data, Tipo e Motivo.");
return;
}
// Adiciona ":00" se o campo de hora estiver preenchido
const startTime = inicio ? inicio + ":00" : undefined;
const endTime = termino ? termino + ":00" : undefined;
// 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 payload = {
const startTime = inicio ? inicio + ":00" : null;
const endTime = termino ? termino + ":00" : null;
let authHeader = "";
try {
authHeader = getAuthorizationHeader ? getAuthorizationHeader() : "";
} catch (err) {
console.warn("Não foi possível obter Authorization header via useAuth()", err);
}
let createdBy = user?.id || null;
if (!createdBy && typeof getUserInfo === "function") {
try {
const info = await getUserInfo();
createdBy = info?.id || info?.profile?.id || null;
} catch (err) {
console.warn("getUserInfo falhou:", err);
}
}
if (!createdBy) {
try {
const stored = localStorage.getItem("user");
if (stored) {
const parsed = JSON.parse(stored);
createdBy = parsed?.id || parsed?.user?.id || null;
}
} catch {}
}
const raw = JSON.stringify({
doctor_id: profissional,
date: dataAtendimento,
kind: mappedKind,
start_time: startTime,
end_time: endTime,
kind: tipoAtendimento,
reason: motivo,
};
created_by: createdBy
});
var myHeaders = new Headers();
if (authHeader) myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
if (API_KEY) myHeaders.append("apikey", API_KEY);
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: JSON.stringify(payload),
body: raw,
redirect: 'follow'
};
@ -73,7 +162,7 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
if (response.ok || response.status === 201) {
console.log("Exceção criada com sucesso:", result);
alert(`Exceção criada! Detalhes: ${result.id || JSON.stringify(result)}`);
onCancel(true); // Indica sucesso para o componente pai recarregar
onCancel(true);
} else {
console.error("Erro ao criar exceção:", result);
alert(`Erro ao criar exceção. Status: ${response.status}. Detalhes: ${result.message || JSON.stringify(result)}`);
@ -90,7 +179,30 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
<h2 className="section-title">Informações da Nova Exceção</h2>
<div className="campo-informacoes-atendimento">
{/* Busca por nome usando filtragem local */}
<div className="campo-de-input campo-de-input-container">
<label>Nome do médico</label>
<input
type="text"
name="doctorSearchName"
placeholder="Digite o nome do médico"
value={doctorSearchName}
onChange={handleSearchProfissional}
autoComplete="off"
/>
{isDropdownOpen && profissionaisFiltrados.length > 0 && (
<div className="dropdown-profissionais">
{profissionaisFiltrados.map(p => (
<div key={p.id} className="dropdown-item" onClick={() => handleSelectProfissional(p)}>
{p.full_name}
</div>
))}
</div>
)}
{searchingDoctor && <small>Carregando médicos...</small>}
</div>
{/* ID do profissional (preenchido ao selecionar) */}
<div className="campo-de-input">
<label>ID do profissional *</label>
<input
@ -105,12 +217,11 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
<div className="campo-de-input">
<label>Tipo de exceção *</label>
<select name="tipoAtendimento" onChange={handleAtendimentoChange} value={dadosAtendimento.tipoAtendimento} required>
<option value="" disabled selected>Selecione o tipo de exceção</option>
<option value="liberacao" >Liberação (Criar Slot)</option>
<option value="bloqueio" >Bloqueio (Remover Slot)</option>
<option value="" disabled>Selecione o tipo de exceção</option>
<option value="disponibilidade_extra" >Liberação</option>
<option value="bloqueio" >Bloqueio</option>
</select>
</div>
</div>
<section id="informacoes-atendimento-segunda-linha">
@ -150,10 +261,6 @@ const FormCriarExcecao = ({ onCancel, doctorID }) => {
onChange={handleAtendimentoChange}
/>
</div>
<div className="campo-de-input">
{/* Removendo o campo solicitante, pois não está no payload da API de exceções */}
</div>
</div>
</section>

View File

@ -1,55 +1,97 @@
import React from 'react'
import React, { useState } from 'react'
import '../PagesMedico/styleMedico/FormNovoRelatorio.css'
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAuth } from '../components/utils/AuthProvider'
import { GetPatientByCPF } from '../components/utils/Functions-Endpoints/Patient'
import { FormatCPF } from '../components/utils/Formatar/Format'
import html2pdf from 'html2pdf.js'
const FormRelatorio = ({onSave, DictInfo, setDictInfo }) => {
const {getAuthorizationHeader} = useAuth()
const FormRelatorio = ({ onSave, DictInfo, setDictInfo }) => {
const { getAuthorizationHeader } = useAuth()
let authHeader = getAuthorizationHeader()
const navigate= useNavigate()
const navigate = useNavigate()
const [showModal, setShowModal] = useState(false)
// --- NOVO: Estado para controlar o loading da transcrição ---
const [isTranscribing, setIsTranscribing] = useState(false);
// --- NOVA FUNÇÃO: Envia o áudio e preenche o formulário ---
const handleAudioUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
setIsTranscribing(true); // Ativa o spinner
const formData = new FormData();
formData.append('audio', file); // 'audio' deve ser o nome esperado no backend
try {
// ATENÇÃO: Substitua essa URL pela rota do seu backend que criamos
const response = await fetch('http://localhost:3001/api/transcrever-relatorio', {
method: 'POST',
body: formData,
// headers: { 'Authorization': authHeader } // Descomente se seu backend precisar de token
});
if (!response.ok) throw new Error("Falha na transcrição");
const data = await response.json();
// Atualiza o DictInfo com os dados vindos da IA
setDictInfo((prev) => ({
...prev,
exam: data.exam || prev.exam, // Preenche se a IA achou, senão mantém o antigo
diagnostico: data.diagnostico || prev.diagnostico,
conclusao: data.conclusao || prev.conclusao
}));
} catch (error) {
console.error("Erro no upload de áudio:", error);
alert("Não foi possível gerar o relatório por áudio. Verifique o backend.");
} finally {
setIsTranscribing(false); // Desativa o spinner
e.target.value = null; // Limpa o input para permitir enviar o mesmo arquivo novamente se quiser
}
};
// -----------------------------------------------------------
const BaixarPDFdoRelatorio = () => {
const elemento = document.getElementById("folhaA4"); // tua div do relatório
const opt = {
margin: 0,
filename: `relatorio_${DictInfo?.paciente_nome || "paciente"}.pdf`,
html2canvas: { scale: 2 },
jsPDF: { unit: "mm", format: "a4", orientation: "portrait" },
};
const opt = {
margin: 0,
filename: `relatorio_${DictInfo?.paciente_nome || "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 handleChange = (e) => {
const { name, value } = e.target;
console.log(name, value)
if(name === 'paciente_cpf') {
const formattedCPF = FormatCPF(value);
setDictInfo((prev) => ({ ...prev, [name]: formattedCPF }));
if (name === 'paciente_cpf') {
const formattedCPF = FormatCPF(value);
setDictInfo((prev) => ({ ...prev, [name]: formattedCPF }));
const fetchPatient = async () => {
const patientData = await GetPatientByCPF(formattedCPF, authHeader);
if (patientData) {
setDictInfo((prev) => ({
...prev,
paciente_cpf:value,
paciente_nome: patientData.full_name,
paciente_id: patientData.id
}));
const fetchPatient = async () => {
const patientData = await GetPatientByCPF(formattedCPF, authHeader);
if (patientData) {
setDictInfo((prev) => ({
...prev,
paciente_cpf: value,
paciente_nome: patientData.full_name,
paciente_id: patientData.id
}));
}
};
if (formattedCPF.length === 14) {
fetchPatient();
}
};
if(formattedCPF.length === 14){
fetchPatient();
}
}else{
setDictInfo((prev) => ({ ...prev, [name]: value }));
} else {
setDictInfo((prev) => ({ ...prev, [name]: value }));
}
}
@ -58,144 +100,163 @@ const FormRelatorio = ({onSave, DictInfo, setDictInfo }) => {
console.log(DictInfo)
setShowModal(true)
onSave({
"patient_id": DictInfo.paciente_id,
"exam": DictInfo.exam,
"diagnosis": DictInfo.diagnosis,
"conclusion": DictInfo.conclusao,
"status": "draft",
"requested_by": DictInfo.requested_by,
"hide_date": false,
"hide_signature": false,
});
onSave({
"patient_id": DictInfo.paciente_id,
"exam": DictInfo.exam,
"diagnosis": DictInfo.diagnostico, // Garanta que o backend espera 'diagnosis' mas seu state usa 'diagnostico'
"conclusion": DictInfo.conclusao,
"status": "draft",
"requested_by": DictInfo.requested_by,
"hide_date": false,
"hide_signature": false,
});
}
return (
<div>
{showModal &&(
<div className="modal" style={{ display: 'block', backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header bg-success text-white">
<h5 className="modal-title ">Relatório criado com sucesso</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowModal(false)}
></button>
return (
<div>
{showModal && (
<div className="modal" style={{ display: 'block', backgroundColor: 'rgba(0,0,0,0.5)' }}>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header bg-success text-white">
<h5 className="modal-title ">Relatório criado com sucesso</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowModal(false)}
></button>
</div>
<div className="modal-body">
<p>Você também pode baixa-lo agora em pdf</p>
</div>
<div className="modal-footer">
<button className="btn btn-primary" onClick={BaixarPDFdoRelatorio}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button>
<button
type="button"
className="btn btn-primary"
onClick={() => { setShowModal(false); navigate(('/medico/relatorios')) }}
>
Fechar
</button>
</div>
</div>
</div>
</div>
<div className="modal-body">
<p>Você também pode baixa-lo agora em pdf</p>
)}
<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>
<div className="modal-footer">
<button className="btn btn-primary" onClick={ BaixarPDFdoRelatorio}><i className='bi bi-file-pdf-fill'></i> baixar em pdf</button>
{/* ----------------------------------------------- */}
<button
type="button"
className="btn btn-primary"
onClick={() => {setShowModal(false); navigate(('/medico/relatorios'))}}
>
Fechar
</button>
</div>
</div>
</div>
</div>
)}
<form action="" onSubmit={handleSubmit}>
<div id='primeiraLinha'>
<div className="col-md-2 mb-3">
<label >Nome do paciente:</label>
<input type="text" step="0.1" className="form-control" name="paciente_nome" onChange={handleChange} value={DictInfo.paciente_nome || ''} required />
</div>
<div className="col-md-2 mb-3">
<label >CPF do paciente:</label>
<input type="text" step="0.1" className="form-control" name="paciente_cpf" onChange={handleChange} value={DictInfo.paciente_cpf || ''} required />
</div>
<div className='card'>
<div className="col-md-2 mb-3">
<label >Nome do médico:</label>
<input type="text" step="0.1" className="form-control" name="requested_by" onChange={handleChange} value={DictInfo.requested_by || ''} required />
</div>
<form action="" onSubmit={handleSubmit}>
<div id='primeiraLinha'>
<div className="col-md-2 mb-3">
<label >Nome do paciente:</label>
<input type="text" step="0.1" className="form-control" name="paciente_nome" onChange={handleChange} value={DictInfo.paciente_nome || ''} required />
</div>
<div className="col-md-2 mb-3">
<label >CPF do paciente:</label>
<input type="text" step="0.1" className="form-control" name="paciente_cpf" onChange={handleChange} value={DictInfo.paciente_cpf || ''} required />
</div>
<div className="col-md-2 mb-3">
<label >Exame:</label>
<input type="text" className="form-control" name="exam" onChange={handleChange} value={DictInfo.exam || ''} />
</div>
<div className="col-md-2 mb-3">
<label >Nome do médico:</label>
<input type="text" step="0.1" className="form-control" name="requested_by" onChange={handleChange} value={DictInfo.requested_by || ''} required />
<div className="col-md-2 mb-3">
<label >Data do exame:</label>
<input type="date" className="form-control" name="data_exame" onChange={handleChange} value={DictInfo.data_exame || ''} />
</div>
</div>
<div className='row'>
<div className="col-md-2 mb-3">
<label htmlFor='diagnostico'>Diagnostico:</label>
<textarea name="diagnostico" id="diagnostico" onChange={handleChange} cols="30" rows="5" value={DictInfo.diagnostico || ''}></textarea>
</div>
<div className="col-md-2 mb-3">
<label htmlFor='conclusao'>Conclusão:</label>
<textarea name="conclusao" id="conclusao" onChange={handleChange} cols="30" rows="5" value={DictInfo.conclusao || ''}></textarea>
</div>
</div>
<button
className="btn btn-success submitButton"
type='submit'
>
Salvar
</button>
</form>
</div>
<div className="col-md-2 mb-3">
<label >Exame:</label>
<input type="text" className="form-control" name="exam" onChange={handleChange} />
</div>
<h3>Modelo do relatório</h3>
<div id="folhaA4">
<div id='header-relatorio'>
<p>Clinica Rise up</p>
<p>Dr {DictInfo.requested_by} - CRM/SP 123456</p>
<p>Avenida - (79) 9 4444-4444</p>
</div>
<div id='infoPaciente'>
<p>Paciente: {DictInfo?.paciente_nome}</p>
<p>Data de nascimento: </p>
{/* Corrigi de data_exam para data_exame para bater com o state */}
<p>Data do exame: {DictInfo.data_exame}</p>
<p>Exame: {DictInfo.exam}</p>
<p>Diagnostico: {DictInfo.diagnostico}</p>
<p>Conclusão: {DictInfo.conclusao}</p>
</div>
<div>
<p>Dr {DictInfo.requested_by}</p>
<p>Emitido em: {new Date().toLocaleDateString()}</p>
</div>
<div className="col-md-2 mb-3">
<label >Data do exame:</label>
<input type="date" className="form-control" name="data_exame" onChange={handleChange} value={DictInfo.data_exame || ''} />
</div>
</div>
<div className='row'>
<div className="col-md-2 mb-3">
<label htmlFor='diagnostico'>Diagnostico:</label>
<textarea name="diagnostico" id="diagnostico" onChange={handleChange} cols="30" rows="5" value={DictInfo.diagnostico || ''}></textarea>
</div>
<div className="col-md-2 mb-3">
<label htmlFor='conclusao'>Conclusão:</label>
<textarea name="conclusao" id="conclusao" onChange={handleChange} cols="30" rows="5" value={DictInfo.conclusao || ''}></textarea>
</div>
</div>
<button
className="btn btn-success submitButton"
type='submit'
>
Salvar
</button>
</form>
</div>
<h3>Modelo do relatório</h3>
<div id="folhaA4">
<div id='header-relatorio'>
<p>Clinica Rise up</p>
<p>Dr {DictInfo.requested_by} - CRM/SP 123456</p>
<p>Avenida - (79) 9 4444-4444</p>
</div>
<div id='infoPaciente'>
<p>Paciente: {DictInfo?.paciente_nome}</p>
<p>Data de nascimento: </p>
<p>Data do exame: {DictInfo.data_exam}</p>
<p>Exame: {DictInfo.exam}</p>
<p>Diagnostico: {DictInfo.diagnostico}</p>
<p>Conclusão: {DictInfo.conclusao}</p>
</div>
<div>
<p>Dr {DictInfo.requested_by}</p>
<p>Emitido em: 0</p>
</div>
</div>
</div>
)
)
}
export default FormRelatorio

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,75 @@
.container-perfis-toggle {
display: flex;
flex-direction: column;
max-width: 300px;
background-color: #ffffff;
border-radius: 8px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.toggle-button {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
cursor: pointer;
background-color: #f0f0f0;
border-radius: 8px;
transition: background-color 0.2s;
outline: none;
}
.toggle-button:hover {
background-color: #e0e0e0;
}
.acesso-text {
font-size: 1.1em;
font-weight: 600;
color: #333;
margin: 0;
}
.perfil-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 20px 20px 20px;
border-top: 1px solid #eee;
}
.perfil-item {
padding: 10px 15px;
background-color: #007bff;
color: white;
border-radius: 4px;
cursor: pointer;
text-align: center;
font-weight: 500;
transition: background-color 0.2s ease, transform 0.1s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
outline: none;
}
.perfil-item:hover {
background-color: #0056b3;
transform: translateY(-1px);
}
.perfil-item:focus {
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.5);
}
.perfil-item:active {
background-color: #004085;
transform: translateY(0);
}
.no-profiles {
padding: 10px;
text-align: center;
color: #888;
font-style: italic;
}

View File

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

View File

@ -199,3 +199,14 @@
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,14 +2,15 @@ import React, { useState, useRef, useCallback } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom";
import "./DoctorForm.css";
import HorariosDisponibilidade from "../doctors/HorariosDisponibilidade";
import { useAuth } from '../utils/AuthProvider';
import API_KEY from '../utils/apiKeys';
const ENDPOINT_AVAILABILITY =
"https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_availability";
const ENDPOINT_AVAILABILITY = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability";
function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
const navigate = useNavigate();
const location = useLocation();
const { getAuthorizationHeader } = useAuth();
const FormatTelefones = (valor) => {
const digits = String(valor).replace(/\D/g, "").slice(0, 11);
@ -54,7 +55,6 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
);
};
const [avatarUrl, setAvatarUrl] = useState(null);
const [showRequiredModal, setShowRequiredModal] = useState(false);
const [emptyFields, setEmptyFields] = useState([]);
@ -74,6 +74,15 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
horarios: false,
});
const resolveAuthHeader = () => {
try {
const h = getAuthorizationHeader();
return h || '';
} catch {
return '';
}
}
const handleToggleCollapse = (section) => {
setCollapsedSections((prevState) => ({
...prevState,
@ -126,12 +135,11 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
}
};
const handleAvailabilityUpdate = useCallback(
(newAvailability) => {
setFormData((prev) => ({ ...prev, availability: newAvailability }));
},
[setFormData]
);
const handleAvailabilityUpdate = useCallback((newAvailability) => {
setFormData((prev) => {
return { ...prev, availability: newAvailability };
});
}, [setFormData]);
const handleCepBlur = async () => {
const cep = formData.cep?.replace(/\D/g, "");
@ -211,41 +219,69 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
}
}, 300);
};
const handleCreateAvailability = async (newAvailability) => {
const handleCreateAvailability = async (doctorId, availabilityData) => {
try {
const myHeaders = new Headers();
const authHeader = resolveAuthHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
myHeaders.append("Content-Type", "application/json");
if (API_KEY) myHeaders.append("apikey", API_KEY);
const response = await fetch(ENDPOINT_AVAILABILITY, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newAvailability),
headers: myHeaders,
body: JSON.stringify({
doctor_id: doctorId,
availability: availabilityData,
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();
console.log("Disponibilidade criada :", data);
alert("Disponibilidade criada com sucesso!");
console.log("Disponibilidade criada:", data);
return data;
} catch (error) {
console.error("Erro ao criar disponibilidade:", error);
alert("Erro ao criar disponibilidade.");
throw error;
}
};
const handlePatchAvailability = async (id, updatedAvailability) => {
try {
const response = await fetch(`${ENDPOINT_AVAILABILITY}?id=${id}`, {
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: {
"Content-Type": "application/json",
},
body: JSON.stringify(updatedAvailability),
headers: myHeaders,
body: JSON.stringify({
availability: updatedAvailability,
updated_at: new Date().toISOString()
}),
});
const data = await response.json();
console.log("Disponibilidade atualizada:", data);
alert("Disponibilidade atualizada com sucesso!");
} catch (error) {
console.error("Erro ao atualizar disponibilidade:", error);
alert("Erro ao atualizar disponibilidade.");
}
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;
}
};
const handleSubmit = async () => {
@ -287,31 +323,20 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
}
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 });
if (formData.availability && formData.availability.length > 0) {
if (formData.availabilityId) {
await handlePatchAvailability(
formData.availabilityId,
formData.availability
);
} else {
await handleCreateAvailability(formData.availability);
}
}
alert("Médico salvo e disponibilidade enviada ao mock com sucesso!");
} catch (error) {
console.error("Erro ao salvar médico ou disponibilidade:", error);
alert("Erro ao salvar médico ou disponibilidade.");
};
}
};
const handleModalClose = () => {
setShowRequiredModal(false);
};
return (
<>
{/* Modal de Alerta */}
@ -724,7 +749,7 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
Defina seus horários de atendimento para cada dia da semana.
Marque um dia para começar a adicionar blocos de tempo.
</p>
<HorariosDisponibilidade
<HorariosDisponibilidade
initialAvailability={formData.availability}
onUpdate={handleAvailabilityUpdate}
/>
@ -734,7 +759,7 @@ function DoctorForm({ onSave, onCancel, formData, setFormData, isLoading }) {
</div>
{/* BOTÕES DE AÇÃO */}
<div className="actions-container">
<div className="btns-container">
<button
className="btn btn-success btn-submit"
onClick={handleSubmit}

View File

@ -0,0 +1,168 @@
.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,5 +1,6 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useRef } from "react";
import { Clock } from "lucide-react";
import "./HorariosDisponibilidade.css";
const initialBlockTemplate = {
id: null,
@ -9,38 +10,43 @@ const initialBlockTemplate = {
};
const emptyAvailabilityTemplate = [
{ dia: "Segunda-feira", isChecked: false, blocos: [] },
{ dia: "Terça-feira", isChecked: false, blocos: [] },
{ dia: "Quarta-feira", isChecked: false, blocos: [] },
{ dia: "Quinta-feira", isChecked: false, blocos: [] },
{ dia: "Sexta-feira", isChecked: false, blocos: [] },
{ dia: "Sábado", isChecked: false, blocos: [] },
{ dia: "Domingo", isChecked: false, blocos: [] },
{ dia: "Domingo", weekday: 0, isChecked: false, blocos: [] },
{ dia: "Segunda-feira", weekday: 1, isChecked: false, blocos: [] },
{ dia: "Terça-feira", weekday: 2, isChecked: false, blocos: [] },
{ dia: "Quarta-feira", weekday: 3, isChecked: false, blocos: [] },
{ dia: "Quinta-feira", weekday: 4, isChecked: false, blocos: [] },
{ dia: "Sexta-feira", weekday: 5, isChecked: false, blocos: [] },
{ dia: "Sábado", weekday: 6, isChecked: false, blocos: [] },
];
const HorariosDisponibilidade = ({
initialAvailability = emptyAvailabilityTemplate,
onUpdate,
onCancel,
}) => {
const [availability, setAvailability] = useState(initialAvailability);
const isFirstRun = useRef(true);
useEffect(() => {
if (initialAvailability !== emptyAvailabilityTemplate) {
if (initialAvailability && initialAvailability.length > 0) {
setAvailability(initialAvailability);
} else {
setAvailability(emptyAvailabilityTemplate);
}
}, [initialAvailability]);
useEffect(() => {
if (onUpdate) {
onUpdate(availability);
if (isFirstRun.current) {
isFirstRun.current = false;
return;
}
if (onUpdate) onUpdate(availability);
}, [availability, onUpdate]);
const handleDayCheck = useCallback((dayIndex, currentIsChecked) => {
const isChecked = !currentIsChecked;
setAvailability((prev) =>
prev.map((day, i) =>
setAvailability((prev) => {
const updated = prev.map((day, i) =>
i === dayIndex
? {
...day,
@ -58,14 +64,15 @@ const HorariosDisponibilidade = ({
: [],
}
: day
)
);
);
console.log('handleDayCheck - updated availability:', updated);
return updated;
});
}, []);
const handleAddBlock = useCallback((dayIndex) => {
const tempId = Date.now() + Math.random();
const newBlock = { ...initialBlockTemplate, id: tempId, isNew: true };
setAvailability((prev) =>
prev.map((day, i) =>
i === dayIndex
@ -110,291 +117,90 @@ const HorariosDisponibilidade = ({
);
}, []);
const renderTimeBlock = (dayIndex, bloco) => (
<div
key={bloco.id}
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" }}
>
Início:
</label>
<div style={{ position: "relative" }}>
<input
id={`inicio-${dayIndex}-${bloco.id}`}
type="time"
value={bloco.inicio}
onChange={(e) =>
handleTimeChange(dayIndex, bloco.id, "inicio", e.target.value)
}
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",
}}
/>
</div>
</div>
<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:
</label>
<div style={{ position: "relative" }}>
<input
id={`termino-${dayIndex}-${bloco.id}`}
type="time"
value={bloco.termino}
onChange={(e) =>
handleTimeChange(dayIndex, bloco.id, "termino", e.target.value)
}
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",
}}
/>
</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;
<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>
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",
};
{day.isChecked && (
<div className="blocks-section">
<div className="blocks-grid">
{day.blocos.map((bloco) => (
<div
key={bloco.id}
className={`time-block ${bloco.isNew ? "new" : ""}`}
>
<div className="time-inputs">
<label>
Início:
<div className="input-wrapper">
<input
type="time"
value={bloco.inicio}
onChange={(e) =>
handleTimeChange(
dayIndex,
bloco.id,
"inicio",
e.target.value
)
}
/>
<Clock className="clock-icon" size={14} />
</div>
</label>
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>
Término:
<div className="input-wrapper">
<input
type="time"
value={bloco.termino}
onChange={(e) =>
handleTimeChange(
dayIndex,
bloco.id,
"termino",
e.target.value
)
}
/>
<Clock className="clock-icon" size={14} />
</div>
</label>
</div>
<button
className="btn-remove"
onClick={() => handleRemoveBlock(dayIndex, bloco.id)}
>
Remover
</button>
</div>
))}
</div>
{isChecked && (
<div style={{ marginTop: "16px" }}>
{day.blocos.length === 0 && (
<p
style={{
color: "#6b7280",
fontStyle: "italic",
marginBottom: "16px",
}}
>
Nenhum bloco de horário definido.
</p>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: "16px",
}}
>
{day.blocos.map((bloco) =>
renderTimeBlock(dayIndex, bloco)
)}
</div>
<button
onClick={() => handleAddBlock(dayIndex)}
style={{
marginTop: "15px",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "10px 22px",
backgroundColor: "#10b981",
color: "white",
fontWeight: "bold",
borderRadius: "12px",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1)",
transition: "all 0.3s",
cursor: "pointer",
border: "none",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#059669")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "#10b981")
}
>
+ Adicionar novo bloco
</button>
</div>
)}
<button
className="btn-add"
onClick={() => handleAddBlock(dayIndex)}
>
+ Adicionar novo bloco
</button>
</div>
);
})}
</div>
)}
</div>
))}
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

20
src/firebaseConfig.js Normal file
View File

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

View File

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

13
src/openaiService.js Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -13,17 +13,15 @@ const AgendamentoEditPage = ({setDictInfo, DictInfo}) => {
//let DataAtual = dayjs()
const {getAuthorizationHeader} = useAuth()
const params = useParams()
const [PatientToPatch, setPatientToPatch] = useState({})
let id = params.id
console.log(DictInfo, "DENTRO DO EDITAR")
//console.log(DictInfo, 'aqui')
useEffect(() => {
setDictInfo({...DictInfo?.Infos,...DictInfo?.agendamento})
setDictInfo({...DictInfo, dataAtendimento:DictInfo.scheduled_at.split("T")[0]})
const ColherInfoUsuario =async () => {
const result = await UserInfos(authHeader)
@ -53,9 +51,7 @@ const AgendamentoEditPage = ({setDictInfo, DictInfo}) => {
"doctor_id": DictParaPatch.doctor_id,
"duration_minutes": 30,
"chief_complaint": "Dor de cabeça há 3 ",
"created_by": idUsuario,
"scheduled_at": `${DictParaPatch.dataAtendimento}T${DictParaPatch.horarioInicio}:00.000Z`,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,40 +1,108 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import dayjs from 'dayjs';
import 'dayjs/locale/pt-br';
import weekday from 'dayjs/plugin/weekday';
import FormCriarExcecao from '../components/FormCriarExcecao';
import "../components/AgendarConsulta/style/formagendamentos.css";
import "./style/Agendamento.css";
import './style/FilaEspera.css';
import { useAuth } from '../components/utils/AuthProvider';
import API_KEY from '../components/utils/apiKeys';
const ENDPOINT_LISTAR = "https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_exceptions";
const ENDPOINT_DELETAR = "https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctor_exceptions/";
dayjs.extend(weekday);
dayjs.locale('pt-br');
const ENDPOINT_BASE = "https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_exceptions";
const getDateRange = (date, view) => {
const startDayjs = dayjs(date);
let fromDate, toDate, titleRange;
if (view === 'diario') {
fromDate = startDayjs.format('YYYY-MM-DD');
toDate = startDayjs.format('YYYY-MM-DD');
titleRange = startDayjs.format('DD/MM/YYYY');
} else if (view === 'semanal') {
let weekStart = startDayjs.startOf('week');
if (weekStart.day() !== 1) {
weekStart = startDayjs.weekday(1);
}
const weekEnd = weekStart.add(6, 'day');
fromDate = weekStart.format('YYYY-MM-DD');
toDate = weekEnd.format('YYYY-MM-DD');
titleRange = `Semana de ${weekStart.format('DD/MM')} a ${weekEnd.format('DD/MM')}`;
} else if (view === 'mensal') {
const monthStart = startDayjs.startOf('month');
const monthEnd = startDayjs.endOf('month');
fromDate = monthStart.format('YYYY-MM-DD');
toDate = monthEnd.format('YYYY-MM-DD');
titleRange = startDayjs.format('MMMM/YYYY').toUpperCase();
}
return { fromDate, toDate, titleRange };
};
const ExcecoesDisponibilidade = () => {
const { getAuthorizationHeader } = useAuth();
const navigate = useNavigate();
const [pageNovaExcecao, setPageNovaExcecao] = useState(false);
const [excecoes, setExcecoes] = useState([]);
const [loading, setLoading] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedExceptionId, setSelectedExceptionId] = useState(null);
const [showSuccessModal, setShowSuccessModal] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
// Filtros
const [filtroMedicoId, setFiltroMedicoId] = useState('');
const [filtroData, setFiltroData] = useState(dayjs().format('YYYY-MM-DD'));
const [listaDeMedicos, setListaDeMedicos] = useState([]);
const [searchTermDoctor, setSearchTermDoctor] = useState('');
const [filteredDoctors, setFilteredDoctors] = useState([]);
const [selectedDoctor, setSelectedDoctor] = useState(null);
// Estado para controlar a visualização (Diário, Semanal)
const [visualizacao, setVisualizacao] = useState('diario');
// Função para buscar as exceções
const fetchExcecoes = useCallback(async (doctorId, date) => {
const resolveAuthHeader = () => {
try {
const h = getAuthorizationHeader();
return h || '';
} catch {
return '';
}
}
const fetchExcecoes = useCallback(async (fromDate, toDate, doctorId) => {
setLoading(true);
let url = `${ENDPOINT_LISTAR}?select=*`;
let url = `${ENDPOINT_BASE}?select=*`;
if (doctorId) {
url += `&doctor_id=eq.${doctorId}`; // Assume filtro por igualdade de ID
}
if (date) {
url += `&date=eq.${date}`; // Assume filtro por igualdade de data
url += `&doctor_id=eq.${doctorId}`;
}
url += `&date=gte.${fromDate}&date=lte.${toDate}`;
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);
try {
const requestOptions = { method: 'GET', redirect: 'follow' };
const requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow'
};
const response = await fetch(url, requestOptions);
const result = await response.json();
@ -42,8 +110,8 @@ const ExcecoesDisponibilidade = () => {
setExcecoes(result);
} else {
setExcecoes([]);
console.error("Erro ao listar exceções:", result);
alert("Erro ao carregar lista de exceções.");
console.error("Erro ao listar exceções (Status:", response.status, "):", result);
alert(`Erro ao carregar lista de exceções. Status: ${response.status}. Detalhes: ${result.message || JSON.stringify(result)}`);
}
} catch (error) {
console.error('Erro na requisição de listagem de exceções:', error);
@ -52,86 +120,189 @@ const ExcecoesDisponibilidade = () => {
} finally {
setLoading(false);
}
}, [getAuthorizationHeader]);
const { fromDate, toDate, titleRange } = useMemo(() =>
getDateRange(filtroData, visualizacao),
[filtroData, visualizacao]
);
useEffect(() => {
fetchExcecoes(fromDate, toDate, filtroMedicoId);
}, [fetchExcecoes, filtroMedicoId, fromDate, toDate]);
useEffect(() => {
const fetchDoctors = async () => {
const myHeaders = new Headers();
const authHeader = resolveAuthHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
if (API_KEY) myHeaders.append("apikey", API_KEY);
try {
const response = await fetch('https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?select=id,full_name', {
method: 'GET',
headers: myHeaders
});
if (response.ok) {
const doctors = await response.json();
setListaDeMedicos(doctors);
}
} catch (error) {
console.error('Erro ao buscar médicos:', error);
}
};
fetchDoctors();
}, []);
// Função para deletar uma exceção
const deletarExcecao = async (id) => {
if (!window.confirm(`Tem certeza que deseja deletar a exceção com ID: ${id}?`)) return;
try {
const requestOptions = { method: 'DELETE', redirect: 'follow' };
const response = await fetch(`${ENDPOINT_DELETAR}${id}`, requestOptions);
if (response.ok || response.status === 204) {
alert(`Exceção ${id} deletada com sucesso.`);
fetchExcecoes(filtroMedicoId, filtroData); // Recarrega a lista
} else {
const result = await response.json();
alert(`Erro ao deletar exceção. Detalhes: ${result.message || JSON.stringify(result)}`);
}
} catch (error) {
console.error('Erro na requisição de deleção:', error);
alert('Erro de comunicação ao tentar deletar a exceção.');
const handleSearchDoctors = (term) => {
setSearchTermDoctor(term);
if (term.trim() === '') {
setFilteredDoctors([]);
return;
}
const filtered = listaDeMedicos.filter(doc =>
doc.full_name.toLowerCase().includes(term.toLowerCase())
);
setFilteredDoctors(filtered);
};
// Efeito para carregar exceções quando os filtros mudam
useEffect(() => {
fetchExcecoes(filtroMedicoId, filtroData);
}, [fetchExcecoes, filtroMedicoId, filtroData]);
const limparFiltros = () => {
setSearchTermDoctor('');
setFilteredDoctors([]);
setSelectedDoctor(null);
setFiltroMedicoId('');
setFiltroData(dayjs().format('YYYY-MM-DD'));
setVisualizacao('diario');
};
// Handler de cancelamento do formulário de criação
const handleCancelForm = (recarregar = false) => {
setPageNovaExcecao(false);
if (recarregar) {
fetchExcecoes(filtroMedicoId, filtroData); // Recarrega se a criação foi bem-sucedida
const deleteExcecao = async (id) => {
const myHeaders = new Headers();
const authHeader = resolveAuthHeader();
if (authHeader) myHeaders.append("Authorization", authHeader);
if (API_KEY) myHeaders.append("apikey", API_KEY);
myHeaders.append("Content-Type", "application/json");
try {
const res = await fetch(`${ENDPOINT_BASE}?id=eq.${id}`, {
method: 'DELETE',
headers: myHeaders,
redirect: 'follow'
});
if (res.ok) {
setExcecoes(prev => prev.filter(x => x.id !== id));
setShowDeleteModal(false);
setSuccessMessage('Exceção excluída com sucesso!');
setShowSuccessModal(true);
} else {
const text = await res.text();
console.error('Erro ao deletar exceção', res.status, text);
alert(`Erro ao excluir exceção. Status: ${res.status}. ${text}`);
}
} catch (err) {
console.error('Erro na requisição de exclusão:', err);
alert('Erro ao excluir exceção.');
}
}
const handleCancelForm = (recarregar = false) => {
setPageNovaExcecao(false);
if (recarregar) {
fetchExcecoes(fromDate, toDate, filtroMedicoId);
}
}
// Se o formulário de criação estiver aberto, renderiza apenas ele
if (pageNovaExcecao) {
return <FormCriarExcecao onCancel={handleCancelForm} doctorID={filtroMedicoId} />;
}
// Renderiza a tela de listagem (layout da agenda)
return (
<div>
<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>
<button
className="btn-primary"
onClick={() => setPageNovaExcecao(true)}
// Use a classe btn-primary que deve estar funcionando
style={{ padding: '10px 20px', fontSize: '14px', whiteSpace: 'nowrap' }}
>
+ Criar Nova Exceção
+ Criar Nova Exceção
</button>
</div>
<div className='atendimento-eprocura'>
{/* Filtros e Busca (Adaptados do Agendamento) */}
<div className='busca-atendimento'>
<div>
<i className="fa-solid fa-user-doctor"></i>
<div className="card p-3 mb-3" style={{ marginTop: '20px' }}>
<h5 className="mb-3">
<i className="bi bi-funnel-fill me-2 text-primary"></i>
Filtros
</h5>
<div className="row g-3 mb-3">
<div className="col-md-6">
<label className="form-label fw-bold">Buscar Médico</label>
<input
type="text"
placeholder="Filtrar por ID do Médico..."
value={filtroMedicoId}
onChange={(e) => setFiltroMedicoId(e.target.value)}
className="form-control"
placeholder="Digite o nome do médico..."
value={searchTermDoctor}
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>
<i className="fa-solid fa-calendar"></i>
<input
<div className="col-md-6">
<label className="form-label fw-bold">Data de Referência</label>
<input
type="date"
className="form-control"
value={filtroData}
onChange={(e) => setFiltroData(e.target.value)}
/>
<small className="text-muted">Selecione a data base para visualização</small>
</div>
</div>
{/* Botões de Visualização (Dia/Semana/Mês) - Adaptados */}
<div className="d-flex justify-content-between align-items-center">
<div>
{selectedDoctor && (
<span className="badge bg-primary me-2">
<i className="bi bi-person-fill me-1"></i>
{selectedDoctor.full_name}
</span>
)}
<div className="contador-pacientes" style={{ display: 'inline-block' }}>
{excecoes.length} DE {excecoes.length} EXCEÇÕES ENCONTRADAS
</div>
</div>
<button
className="btn btn-outline-secondary btn-sm"
onClick={limparFiltros}
>
<i className="bi bi-arrow-clockwise me-1"></i> Limpar Filtros
</button>
</div>
</div>
<div className='atendimento-eprocura'>
<div className='container-btns-agenda-fila_esepera'>
<button
className={`btn-agenda ${visualizacao === "diario" ? "opc-agenda-ativo" : ""}`}
@ -153,51 +324,58 @@ const ExcecoesDisponibilidade = () => {
</button>
</div>
{/* Tabela de Exceções (Simulando a Tabela de Agendamentos) */}
<section className='calendario-ou-filaespera'>
<div className="fila-container">
<h2 className="fila-titulo">Exceções em {filtroData} ({excecoes.length})</h2>
<h2 className="fila-titulo">Exceções em {titleRange} ({excecoes.length})</h2>
{loading ? (
<p>Carregando exceções...</p>
) : excecoes.length === 0 ? (
<p>Nenhuma exceção encontrada para os filtros aplicados.</p>
) : (
<table className="fila-tabela">
<table className="fila-tabela">
<thead>
<tr>
<th>ID Exceção</th>
<th>ID Médico</th>
<th>Médico (ID)</th>
<th>Data</th>
<th>Início</th>
<th>Término</th>
<th>Tipo</th>
<th>Motivo</th>
<th>Criado por</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
{excecoes.map((excecao, index) => (
<tr key={excecao.id || index}>
<td>{excecao.id || 'N/A'}</td>
<td>{excecao.doctor_id}</td>
<td>{excecao.date}</td>
<td>{excecao.start_time ? excecao.start_time.substring(0, 5) : 'Dia Todo'}</td>
<td>{excecao.end_time ? excecao.end_time.substring(0, 5) : 'Dia Todo'}</td>
{excecoes.map((exc) => (
<tr key={exc.id}>
<td><p>{exc.doctor_id}</p></td>
<td>{dayjs(exc.date).format('DD/MM/YYYY')}</td>
<td>{exc.start_time ? dayjs(exc.start_time, 'HH:mm:ss').format('HH:mm') : '—'}</td>
<td>{exc.end_time ? dayjs(exc.end_time, 'HH:mm:ss').format('HH:mm') : '—'}</td>
<td><p>{exc.reason}</p></td>
<td>{exc.created_by || '—'}</td>
<td>
<span className={`status-tag ${excecao.kind === 'bloqueio' ? 'legenda-item-cancelado' : 'legenda-item-realizado'}`}>
{excecao.kind}
</span>
</td>
<td>{excecao.reason}</td>
<td>
{excecao.id && (
<div className="d-flex gap-2">
<button
onClick={() => deletarExcecao(excecao.id)}
style={{ background: '#dc3545', color: 'white', border: 'none', padding: '5px 10px', cursor: 'pointer', borderRadius: '4px' }}
className="btn btn-sm btn-edit"
onClick={() => {
setFiltroMedicoId(exc.doctor_id || '');
setPageNovaExcecao(true);
}}
>
Deletar
<i className="bi bi-pencil me-1"></i> Editar
</button>
)}
<button
className="btn btn-sm btn-delete"
onClick={() => {
setSelectedExceptionId(exc.id);
setShowDeleteModal(true);
}}
>
<i className="bi bi-trash me-1"></i> Excluir
</button>
</div>
</td>
</tr>
))}
@ -207,6 +385,89 @@ const ExcecoesDisponibilidade = () => {
</div>
</section>
</div>
{showDeleteModal && (
<div
className="modal fade show delete-modal"
style={{
display: "block",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
tabIndex="-1"
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#dc3545', color: 'white' }}>
<h5 className="modal-title">
Confirmação de Exclusão
</h5>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
Tem certeza que deseja excluir esta exceção?
</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() => setShowDeleteModal(false)}
>
Cancelar
</button>
<button
type="button"
className="btn btn-danger"
onClick={() => deleteExcecao(selectedExceptionId)}
>
Excluir
</button>
</div>
</div>
</div>
</div>
)}
{showSuccessModal && (
<div
className="modal fade show delete-modal"
style={{
display: "block",
backgroundColor: "rgba(0, 0, 0, 0.5)",
}}
tabIndex="-1"
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header" style={{ backgroundColor: '#1e3a8a', color: 'white' }}>
<h5 className="modal-title">
Sucesso
</h5>
</div>
<div className="modal-body">
<p className="mb-0 fs-5">
{successMessage}
</p>
</div>
<div className="modal-footer">
<button
type="button"
className="btn btn-primary"
onClick={() => setShowSuccessModal(false)}
>
OK
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,132 @@
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,12 +1,15 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import API_KEY from "../components/utils/apiKeys";
import { useAuth } from "../components/utils/AuthProvider";
import "./style/TablePaciente.css";
import ModalErro from "../components/utils/fetchErros/ModalErro";
function TablePaciente({ setCurrentPage, setPatientID }) {
import manager from "../components/utils/fetchErros/ManagerFunction";
const { getAuthorizationHeader, isAuthenticated, RefreshingToken } = useAuth();
function TablePaciente({ setCurrentPage, setPatientID,setDictInfo }) {
const { getAuthorizationHeader, isAuthenticated } = useAuth();
const navigate = useNavigate();
const [pacientes, setPacientes] = useState([]);
const [search, setSearch] = useState("");
@ -21,12 +24,20 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
const [dataInicial, setDataInicial] = useState("");
const [dataFinal, setDataFinal] = useState("");
const [sortKey, setSortKey] = useState(null);
const [sortDir, setSortDir] = useState('asc');
const [paginaAtual, setPaginaAtual] = useState(1);
const [itensPorPagina, setItensPorPagina] = useState(10);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [selectedPatientId, setSelectedPatientId] = useState(null);
const [showModalError, setShowModalError] = useState(false);
const [showModalError, setShowModalError] = useState("");
const [ ErrorInfo, setErrorInfo] = useState({})
const [ErrorInfo, setErrorInfo] = useState({})
const GetAnexos = async (id) => {
var myHeaders = new Headers();
@ -103,7 +114,12 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
}
};
const RefreshingToken = () => {
console.log("Refreshing token...");
};
useEffect(() => {
const authHeader = getAuthorizationHeader()
console.log(authHeader, 'aqui autorização')
@ -120,7 +136,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/patients", requestOptions)
.then(response => {
// 1. VERIFICAÇÃO DO STATUS HTTP (Se não for 2xx)
if (!response.ok) {
return response.json().then(errorData => {
@ -136,27 +152,23 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
console.error("ERRO DETALHADO:", errorObject);
throw errorObject;
});
}
// 3. Se a resposta for OK (2xx), processamos o JSON normalmente
return response.json();
})
.then(result => {
// 4. Bloco de SUCESSO
setPacientes(result);
console.log("Sucesso:", result);
// IMPORTANTE: Se o modal estava aberto, feche-o no sucesso
setShowModalError(false);
})
.catch(error => {
// 5. Bloco de ERRO (Captura erros de rede ou o erro lançado pelo 'throw')
//console.error('Falha na requisição:', error.message);
if(error.httpStatus === 401){
RefreshingToken()
}
setErrorInfo(error)
setShowModalError(true);
console.error(error, "deu erro")
manager(setShowModalError, RefreshingToken, setErrorInfo, error)
});
}, [isAuthenticated, getAuthorizationHeader]);
@ -195,6 +207,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
setIdadeMaxima("");
setDataInicial("");
setDataFinal("");
setPaginaAtual(1);
};
const pacientesFiltrados = Array.isArray(pacientes) ? pacientes.filter((paciente) => {
@ -238,13 +251,67 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
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(() => {
console.log(` Pacientes totais: ${pacientes?.length}, Filtrados: ${pacientesFiltrados?.length}`);
}, [pacientes, pacientesFiltrados, search]);
setPaginaAtual(1);
}, [search, filtroConvenio, filtroVIP, filtroAniversariante, filtroCidade, filtroEstado, idadeMinima, idadeMaxima, dataInicial, dataFinal, sortKey, sortDir]);
return (
<>
<ModalErro showModal={showModalError} setShowModal={setShowModalError} ErrorData={ErrorInfo}/>
<div className="page-heading">
<h3>Lista de Pacientes</h3>
</div>
@ -296,6 +363,7 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
<button
className={`btn btn-sm ${filtroVIP ? "btn-primary" : "btn-outline-primary"}`}
onClick={() => setFiltroVIP(!filtroVIP)}
style={{ padding: "0.25rem 0.5rem" }}
>
<i className="bi bi-award me-1"></i> VIP
@ -309,6 +377,33 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
>
<i className="bi bi-calendar me-1"></i> Aniversariantes
</button>
<div className="vr mx-2 d-none d-md-block" />
<div className="d-flex align-items-center gap-2">
<span className="text-muted small">Ordenar por:</span>
{(() => {
const sortValue = sortKey ? `${sortKey}-${sortDir}` : '';
return (
<select
className="form-select compact-select sort-select w-auto"
value={sortValue}
onChange={(e) => {
const v = e.target.value;
if (!v) { setSortKey(null); setSortDir('asc'); return; }
const [k, d] = v.split('-');
setSortKey(k);
setSortDir(d);
}}
>
<option value="">Sem ordenação</option>
<option value="nome-asc">Nome (A-Z)</option>
<option value="nome-desc">Nome (Z-A)</option>
<option value="idade-asc">Idade (crescente)</option>
<option value="idade-desc">Idade (decrescente)</option>
</select>
);
})()}
</div>
</div>
<div className="d-flex justify-content-between align-items-center">
@ -400,31 +495,12 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</div>
</div>
)}
</div>
{(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante ||
filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
<div className="alert alert-info mb-3 filters-active">
<strong>Filtros ativos:</strong>
<div className="mt-1">
{search && <span className="badge bg-primary me-2">Busca: "{search}"</span>}
{filtroConvenio !== "Todos" && <span className="badge bg-primary me-2">Convênio: {filtroConvenio}</span>}
{filtroVIP && <span className="badge bg-primary me-2">VIP</span>}
{filtroAniversariante && <span className="badge bg-primary me-2">Aniversariantes</span>}
{filtroCidade && <span className="badge bg-primary me-2">Cidade: {filtroCidade}</span>}
{filtroEstado && <span className="badge bg-primary me-2">Estado: {filtroEstado}</span>}
{idadeMinima && <span className="badge bg-primary me-2">Idade mín: {idadeMinima}</span>}
{idadeMaxima && <span className="badge bg-primary me-2">Idade máx: {idadeMaxima}</span>}
{dataInicial && <span className="badge bg-primary me-2">Data inicial: {dataInicial}</span>}
{dataFinal && <span className="badge bg-primary me-2">Data final: {dataFinal}</span>}
</div>
<div className="mt-3">
<div className="contador-pacientes">
{pacientesFiltrados.length} DE {pacientes.length} PACIENTES ENCONTRADOS
</div>
)}
<div className="mb-3">
<span className="badge results-badge">
{pacientesFiltrados?.length} de {pacientes?.length} pacientes encontrados
</span>
</div>
</div>
<div className="table-responsive">
@ -439,8 +515,8 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
</tr>
</thead>
<tbody>
{pacientesFiltrados.length > 0 ? (
pacientesFiltrados.map((paciente) => (
{pacientesPaginados.length > 0 ? (
pacientesPaginados.map((paciente) => (
<tr key={paciente.id}>
<td>
<div className="d-flex align-items-center patient-name-container">
@ -468,17 +544,21 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
<td>{paciente.email || 'Não informado'}</td>
<td>
<div className="d-flex gap-2">
<Link to={`${paciente.id}`}>
<button className="btn btn-sm btn-view">
<Link to={"details"}>
<button className="btn btn-sm btn-view" onClick={() => setDictInfo(paciente)}>
<i className="bi bi-eye me-1"></i> Ver Detalhes
</button>
</Link>
<Link to={`${paciente.id}/edit`}>
<button className="btn btn-sm btn-edit">
<i className="bi bi-pencil me-1"></i> Editar
</button>
</Link>
<button
className="btn btn-sm btn-edit"
onClick={() => {
setDictInfo(paciente);
navigate('edit');
}}
>
<i className="bi bi-pencil me-1"></i> Editar
</button>
<button
className="btn btn-sm btn-delete"
@ -495,13 +575,75 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
))
) : (
<tr>
<td colSpan="5" className="empty-state">
Nenhum paciente encontrado.
<td colSpan="5" className="text-center py-4">
<div className="text-muted">
<i className="bi bi-search display-4"></i>
<p className="mt-2">Nenhum paciente encontrado com os filtros aplicados.</p>
{(search || filtroConvenio !== "Todos" || filtroVIP || filtroAniversariante ||
filtroCidade || filtroEstado || idadeMinima || idadeMaxima || dataInicial || dataFinal) && (
<button className="btn btn-outline-primary btn-sm mt-2" onClick={limparFiltros}>
Limpar filtros
</button>
)}
</div>
</td>
</tr>
)}
</tbody>
</table>
{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>
@ -523,15 +665,10 @@ function TablePaciente({ setCurrentPage, setPatientID }) {
>
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header">
<div className="modal-header" style={{ backgroundColor: '#dc3545', color: 'white' }}>
<h5 className="modal-title">
Confirmação de Exclusão
</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowDeleteModal(false)}
></button>
</div>
<div className="modal-body">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Routes, Route } from "react-router-dom";
import Sidebar from "../../components/Sidebar";
import { useState } from "react";
import DoctorRelatorioManager from "../../PagesMedico/DoctorRelatorioManager";
import Prontuario from "../../PagesMedico/prontuario";
import Relatorio from "../../PagesMedico/relatorio";
@ -9,9 +9,16 @@ import Chat from "../../PagesMedico/Chat";
import DoctorItems from "../../data/sidebar-items-medico.json";
import FormNovoRelatorio from "../../PagesMedico/FormNovoRelatorio";
import EditPageRelatorio from "../../PagesMedico/EditPageRelatorio";
// ...existing code...
import NovoRelatorioAudio from "../../PagesMedico/NovoRelatorioAudio";
import BotaoVideoChamada from '../../components/BotaoVideoChamada';
import DoctorAgendamentoEditPage from "../../PagesMedico/DoctorAgendamentoEditPage";
function PerfilMedico() {
const [dictInfo, setDictInfo] = useState({})
return (
<div id="app" className="active">
@ -19,14 +26,21 @@ function PerfilMedico() {
<div id="main">
<Routes>
<Route path="/" element={<DoctorRelatorioManager />} />
<Route path="/novo-relatorio" element={<FormNovoRelatorio />} />
<Route path="/novo-relatorio-audio" element={<NovoRelatorioAudio />} />
<Route path="/relatorios/criar" element={<FormNovoRelatorio />} />
<Route path="/relatorios/:id/edit" element={<EditPageRelatorio />} />
<Route path="/prontuario" element={<Prontuario />} />
<Route path="/relatorios" element={<DoctorRelatorioManager />} />
<Route path="/agendamentoMedico" element={<DoctorAgendamentoManager />} />
<Route path="/agendamento" element={<DoctorAgendamentoManager setDictInfo={setDictInfo}/>} />
<Route path="/agendamento/edit" element={<DoctorAgendamentoEditPage DictInfo={dictInfo} setDictInfo={setDictInfo}/>} />
<Route path="/chat" element={<Chat />} />
</Routes>
</div>
{/* ADICIONADO AQUI */}
<BotaoVideoChamada />
</div>
);

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