Atualizar
This commit is contained in:
parent
62b741ff7a
commit
e7aa76df75
3
MEDICONNECT 2.zip
Normal file
3
MEDICONNECT 2.zip
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:50fbfa0b9343001fbf0b803364ec8a0fe48bcc62537eb59a31abc74674d80d0a
|
||||||
|
size 807015226
|
||||||
@ -1,218 +0,0 @@
|
|||||||
# 👤 Usuário Guilherme - Configuração Completa
|
|
||||||
|
|
||||||
## ✅ Status: CONFIGURADO E TESTADO
|
|
||||||
|
|
||||||
### 📋 Credenciais de Login
|
|
||||||
|
|
||||||
- **Email:** `guilhermesilvagomes1020@gmail.com`
|
|
||||||
- **Senha:** `guilherme123`
|
|
||||||
- **Role:** `user` (acesso ao painel do paciente)
|
|
||||||
|
|
||||||
### 👨⚕️ Dados do Paciente
|
|
||||||
|
|
||||||
- **Nome:** Guilherme Silva Gomes - SQUAD 18
|
|
||||||
- **Telefone:** 79999521847
|
|
||||||
- **CPF:** 11144477735
|
|
||||||
- **Email original:** guilherme@paciente.com
|
|
||||||
- **Patient ID:** `864b1785-461f-4e92-8b74-2a6f17c58a80`
|
|
||||||
- **User ID:** `0550f1dc-649a-4186-a256-3bd4e50e5bdc`
|
|
||||||
|
|
||||||
### 🩺 Médico Responsável
|
|
||||||
|
|
||||||
- **Nome:** Fernando Pirichowski - Squad 18
|
|
||||||
- **Médico ID:** `be1e3cba-534e-48c3-9590-b7e55861cade`
|
|
||||||
|
|
||||||
## 📅 Consultas de Demonstração
|
|
||||||
|
|
||||||
O sistema possui **3 consultas** criadas para demonstração:
|
|
||||||
|
|
||||||
### Consulta 1 - Agendada
|
|
||||||
|
|
||||||
- **Data/Hora:** 05/10/2025 às 10:00
|
|
||||||
- **Status:** Agendada
|
|
||||||
- **Tipo:** Consulta
|
|
||||||
- **Observações:** Primeira consulta - Check-up geral
|
|
||||||
|
|
||||||
### Consulta 2 - Realizada
|
|
||||||
|
|
||||||
- **Data/Hora:** 28/09/2025 às 14:30
|
|
||||||
- **Status:** Realizada
|
|
||||||
- **Tipo:** Retorno
|
|
||||||
- **Observações:** Consulta de retorno - Avaliação de exames
|
|
||||||
|
|
||||||
### Consulta 3 - Confirmada
|
|
||||||
|
|
||||||
- **Data/Hora:** 10/10/2025 às 09:00
|
|
||||||
- **Status:** Confirmada
|
|
||||||
- **Tipo:** Consulta
|
|
||||||
- **Observações:** Consulta de acompanhamento mensal
|
|
||||||
|
|
||||||
## 🚀 Como Usar
|
|
||||||
|
|
||||||
### 1. Acessar o Login do Paciente
|
|
||||||
|
|
||||||
```
|
|
||||||
http://localhost:5173/paciente
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Fazer Login
|
|
||||||
|
|
||||||
Use as credenciais:
|
|
||||||
|
|
||||||
- Email: `guilhermesilvagomes1020@gmail.com`
|
|
||||||
- Senha: `guilherme123`
|
|
||||||
|
|
||||||
### 3. Visualizar as Consultas
|
|
||||||
|
|
||||||
Após o login, você será redirecionado para o painel do paciente onde verá:
|
|
||||||
|
|
||||||
- Dashboard com estatísticas das consultas
|
|
||||||
- Lista completa de consultas (agendadas, realizadas, confirmadas)
|
|
||||||
- Filtros por status e período
|
|
||||||
- Cards informativos com totais
|
|
||||||
|
|
||||||
## 📂 Arquivos Relacionados
|
|
||||||
|
|
||||||
### Dados
|
|
||||||
|
|
||||||
- **Consultas:** `src/data/consultas-demo.json`
|
|
||||||
- **Utilitário:** `src/lib/consultasDemo.ts`
|
|
||||||
|
|
||||||
### Scripts
|
|
||||||
|
|
||||||
- **Criar usuário:** `scripts/criar-guilherme-completo.js`
|
|
||||||
- **Testar acesso:** `scripts/testar-guilherme.js`
|
|
||||||
|
|
||||||
## 🔧 Comandos Úteis
|
|
||||||
|
|
||||||
### Recriar o usuário
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node scripts/criar-guilherme-completo.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Testar o acesso
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node scripts/testar-guilherme.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Onde as Consultas Aparecem
|
|
||||||
|
|
||||||
As consultas do Guilherme com o Dr. Fernando aparecerão em:
|
|
||||||
|
|
||||||
1. **✅ Painel do Paciente (Guilherme)**
|
|
||||||
|
|
||||||
- Login: guilhermesilvagomes1020@gmail.com
|
|
||||||
- URL: `/paciente` → `/acompanhamento`
|
|
||||||
|
|
||||||
2. **✅ Painel do Médico (Fernando)**
|
|
||||||
|
|
||||||
- Login: fernando.pirichowski@souunit.com.br
|
|
||||||
- URL: `/painel-medico`
|
|
||||||
|
|
||||||
3. **✅ Painel da Secretária**
|
|
||||||
- Login com usuário de secretária
|
|
||||||
- URL: `/painel-secretaria`
|
|
||||||
|
|
||||||
## 🔐 Configuração Técnica
|
|
||||||
|
|
||||||
### Tabela `auth.users`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "0550f1dc-649a-4186-a256-3bd4e50e5bdc",
|
|
||||||
"email": "guilhermesilvagomes1020@gmail.com",
|
|
||||||
"role": "user"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tabela `patients`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "864b1785-461f-4e92-8b74-2a6f17c58a80",
|
|
||||||
"full_name": "Guilherme Silva Gomes - SQUAD 18",
|
|
||||||
"email": "guilherme@paciente.com",
|
|
||||||
"phone_mobile": "79999521847",
|
|
||||||
"cpf": "11144477735"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tabela `patient_assignments`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"user_id": "0550f1dc-649a-4186-a256-3bd4e50e5bdc",
|
|
||||||
"patient_id": "864b1785-461f-4e92-8b74-2a6f17c58a80",
|
|
||||||
"role": "user"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 💾 Armazenamento Local
|
|
||||||
|
|
||||||
As consultas são armazenadas em:
|
|
||||||
|
|
||||||
- **Arquivo:** `src/data/consultas-demo.json`
|
|
||||||
- **LocalStorage:** `consultas_local` (carregado automaticamente)
|
|
||||||
|
|
||||||
### Carregar consultas manualmente no navegador
|
|
||||||
|
|
||||||
Abra o console (F12) e execute:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
fetch("/src/data/consultas-demo.json")
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((consultas) => {
|
|
||||||
localStorage.setItem("consultas_local", JSON.stringify(consultas));
|
|
||||||
console.log("✅ Consultas carregadas!");
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Limpar consultas
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
localStorage.removeItem("consultas_local");
|
|
||||||
location.reload();
|
|
||||||
```
|
|
||||||
|
|
||||||
## ✅ Checklist de Verificação
|
|
||||||
|
|
||||||
- [x] Usuário criado com role "user"
|
|
||||||
- [x] Paciente Guilherme cadastrado
|
|
||||||
- [x] Atribuição paciente → usuário configurada
|
|
||||||
- [x] 3 consultas de demonstração criadas
|
|
||||||
- [x] Consultas vinculadas ao Dr. Fernando
|
|
||||||
- [x] Arquivo JSON de consultas criado
|
|
||||||
- [x] Utilitário de carregamento criado
|
|
||||||
- [x] Login testado e funcionando
|
|
||||||
- [x] Pacientes atribuídos verificados
|
|
||||||
|
|
||||||
## 🎯 Resultado Esperado
|
|
||||||
|
|
||||||
Ao fazer login como Guilherme, você deverá ver:
|
|
||||||
|
|
||||||
1. **Header personalizado:** "Olá, Guilherme Silva Gomes - SQUAD 18!"
|
|
||||||
2. **4 cards de estatísticas:**
|
|
||||||
- Total: 3 consultas
|
|
||||||
- Agendadas: 1
|
|
||||||
- Realizadas: 1
|
|
||||||
- Canceladas: 0
|
|
||||||
3. **Lista de consultas** com as 3 consultas criadas
|
|
||||||
4. **Filtros funcionais** por status e período
|
|
||||||
|
|
||||||
## 📞 Suporte
|
|
||||||
|
|
||||||
Se houver algum problema:
|
|
||||||
|
|
||||||
1. Verifique se o servidor está rodando: `npm run dev`
|
|
||||||
2. Execute o teste: `node scripts/testar-guilherme.js`
|
|
||||||
3. Recarregue as consultas no localStorage
|
|
||||||
4. Verifique o console do navegador para erros
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Criado em:** 02/10/2025
|
|
||||||
**Última atualização:** 02/10/2025
|
|
||||||
**Status:** ✅ Operacional
|
|
||||||
@ -1,129 +0,0 @@
|
|||||||
# ✅ LIMPEZA COMPLETA - MEDICONNECT
|
|
||||||
|
|
||||||
## 🎉 TUDO PRONTO!
|
|
||||||
|
|
||||||
Todo o site está **100% conectado à API** e o código foi completamente limpo e otimizado!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 O QUE FOI FEITO:
|
|
||||||
|
|
||||||
### 1. ✅ Arquivos Obsoletos Removidos
|
|
||||||
|
|
||||||
**16 arquivos deletados:**
|
|
||||||
|
|
||||||
- `api.js`, `api.js.d.ts`, `api.d.ts`, `api.types.d.ts` ❌
|
|
||||||
- `pacientes.js`, `listarPacientes.js`, `listarPacientes.d.ts` ❌
|
|
||||||
- 8 arquivos de documentação obsoletos ❌
|
|
||||||
|
|
||||||
### 2. ✅ Logs de Debug Limpos
|
|
||||||
|
|
||||||
**90% dos logs removidos:**
|
|
||||||
|
|
||||||
- Antes: ~10 logs por requisição 😵
|
|
||||||
- Depois: 0-2 logs (apenas erros críticos) 😎
|
|
||||||
|
|
||||||
### 3. ✅ Código Otimizado
|
|
||||||
|
|
||||||
- Headers `apikey` e `Authorization` sempre presentes ✅
|
|
||||||
- Interceptors funcionando perfeitamente ✅
|
|
||||||
- Não há mais conflitos entre .js e .ts ✅
|
|
||||||
- Validação de token expirado antes de enviar ✅
|
|
||||||
|
|
||||||
### 4. ✅ Documentação Consolidada
|
|
||||||
|
|
||||||
- **TECH_SUMMARY.md** - Resumo técnico completo
|
|
||||||
- **CLEANUP_REPORT.md** - Relatório detalhado da limpeza
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 ESTRUTURA FINAL:
|
|
||||||
|
|
||||||
```
|
|
||||||
src/services/
|
|
||||||
├── api.ts ✅ Instância axios configurada (COM apikey)
|
|
||||||
├── http.ts ✅ Wrapper com retry e refresh automático
|
|
||||||
├── authService.ts ✅ Login, logout, refresh token
|
|
||||||
├── medicoService.ts ✅ CRUD de médicos
|
|
||||||
├── pacienteService.ts ✅ CRUD de pacientes
|
|
||||||
├── consultaService.ts ✅ CRUD de consultas
|
|
||||||
└── ...outros services ✅ Todos usando api.ts corretamente
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 BENEFÍCIOS:
|
|
||||||
|
|
||||||
### Performance:
|
|
||||||
|
|
||||||
- ⚡ Console 90% mais limpo
|
|
||||||
- ⚡ Bundler mais rápido (menos arquivos)
|
|
||||||
- ⚡ Menos operações de I/O
|
|
||||||
|
|
||||||
### Confiabilidade:
|
|
||||||
|
|
||||||
- ✅ Headers sempre configurados
|
|
||||||
- ✅ Interceptors sempre executados
|
|
||||||
- ✅ Não há mais conflitos de código
|
|
||||||
|
|
||||||
### Manutenibilidade:
|
|
||||||
|
|
||||||
- 📝 Documentação consolidada
|
|
||||||
- 🔍 Erros fáceis de identificar
|
|
||||||
- 🧹 Código limpo e organizado
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 ESTATÍSTICAS:
|
|
||||||
|
|
||||||
| Item | Antes | Depois | Melhoria |
|
|
||||||
| ------------------- | ----- | ---------- | -------- |
|
|
||||||
| Arquivos .js | 7 | 0 | 100% ✅ |
|
|
||||||
| Logs por request | ~10 | 0-2 | 90% ✅ |
|
|
||||||
| Docs obsoletos | 8 | 0 | 100% ✅ |
|
|
||||||
| Erros de compilação | 389 | 0 críticos | ✅ |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ VALIDAÇÃO:
|
|
||||||
|
|
||||||
### Tudo Funcionando:
|
|
||||||
|
|
||||||
- [x] API conectada corretamente
|
|
||||||
- [x] Headers `apikey` + `Authorization` presentes
|
|
||||||
- [x] Token expirado detectado antes de enviar
|
|
||||||
- [x] Refresh automático funcionando
|
|
||||||
- [x] Console limpo (apenas erros essenciais)
|
|
||||||
- [x] Sem arquivos obsoletos
|
|
||||||
- [x] Zero erros de compilação críticos
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 PRÓXIMOS PASSOS (OPCIONAL):
|
|
||||||
|
|
||||||
Se quiser ir além:
|
|
||||||
|
|
||||||
1. Testar com diferentes usuários
|
|
||||||
2. Validar RLS policies no Supabase
|
|
||||||
3. Adicionar testes automatizados
|
|
||||||
4. Implementar cache de requisições
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 ESTÁ PRONTO PARA USAR!
|
|
||||||
|
|
||||||
O sistema está:
|
|
||||||
|
|
||||||
- ✅ Limpo
|
|
||||||
- ✅ Otimizado
|
|
||||||
- ✅ Funcionando perfeitamente
|
|
||||||
- ✅ Pronto para produção
|
|
||||||
|
|
||||||
**Pode usar tranquilo!** 🎉
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Dúvidas?** Consulte:
|
|
||||||
|
|
||||||
- `TECH_SUMMARY.md` - Documentação técnica
|
|
||||||
- `CLEANUP_REPORT.md` - Detalhes da limpeza
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,112 +0,0 @@
|
|||||||
// Script para listar todos os usuários/pacientes na API Supabase
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
async function listarPacientes() {
|
|
||||||
try {
|
|
||||||
console.log("\n🔍 Buscando pacientes na API Supabase...\n");
|
|
||||||
|
|
||||||
// Primeiro, fazer login como admin para obter token
|
|
||||||
console.log("1️⃣ Fazendo login como admin...");
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: "riseup@popcode.com.br",
|
|
||||||
password: "riseup",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const adminToken = loginResponse.data.access_token;
|
|
||||||
console.log("✅ Login realizado com sucesso!\n");
|
|
||||||
|
|
||||||
// Tentar buscar na tabela de profiles ou users
|
|
||||||
console.log("2️⃣ Buscando usuários na tabela profiles...");
|
|
||||||
try {
|
|
||||||
const profilesResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("\n📊 USUÁRIOS ENCONTRADOS NA TABELA PROFILES:");
|
|
||||||
console.log("Total:", profilesResponse.data.length);
|
|
||||||
console.log("\n" + "=".repeat(80) + "\n");
|
|
||||||
|
|
||||||
profilesResponse.data.forEach((user, index) => {
|
|
||||||
console.log(`${index + 1}. ${user.full_name || "Sem nome"}`);
|
|
||||||
console.log(` 📧 Email: ${user.email}`);
|
|
||||||
console.log(` 🆔 ID: ${user.id}`);
|
|
||||||
console.log(` 👤 Role: ${user.role || "Não definido"}`);
|
|
||||||
console.log(` 📞 Telefone: ${user.phone || "Não informado"}`);
|
|
||||||
console.log(` 📅 Criado em: ${user.created_at}`);
|
|
||||||
console.log("");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filtrar apenas pacientes
|
|
||||||
const pacientes = profilesResponse.data.filter(
|
|
||||||
(u) => u.role === "paciente" || u.role === "user"
|
|
||||||
);
|
|
||||||
console.log(`\n👥 TOTAL DE PACIENTES: ${pacientes.length}`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response && error.response.status === 404) {
|
|
||||||
console.log('❌ Tabela "profiles" não existe\n');
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tentar buscar usuários via função
|
|
||||||
console.log("\n3️⃣ Tentando buscar via função list-users...");
|
|
||||||
try {
|
|
||||||
const usersResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/functions/v1/list-users`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("\n📊 USUÁRIOS VIA FUNÇÃO:");
|
|
||||||
console.log(JSON.stringify(usersResponse.data, null, 2));
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response && error.response.status === 404) {
|
|
||||||
console.log('❌ Função "list-users" não existe\n');
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"⚠️ Erro ao buscar via função:",
|
|
||||||
error.response?.data || error.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n" + "=".repeat(80));
|
|
||||||
console.log("✨ Busca concluída!\n");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro ao listar pacientes:");
|
|
||||||
if (error.response) {
|
|
||||||
console.error(" Status:", error.response.status);
|
|
||||||
console.error(" Dados:", JSON.stringify(error.response.data, null, 2));
|
|
||||||
} else {
|
|
||||||
console.error(" Mensagem:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listarPacientes();
|
|
||||||
@ -20,7 +20,7 @@ const consultas: Consulta[] = [];
|
|||||||
|
|
||||||
const handler: Handler = async (
|
const handler: Handler = async (
|
||||||
event: HandlerEvent,
|
event: HandlerEvent,
|
||||||
_context: HandlerContext
|
context: HandlerContext
|
||||||
) => {
|
) => {
|
||||||
const headers = {
|
const headers = {
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
@ -34,6 +34,7 @@ const handler: Handler = async (
|
|||||||
return { statusCode: 204, headers, body: "" };
|
return { statusCode: 204, headers, body: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void context; // not used currently
|
||||||
const path = event.path.replace("/.netlify/functions/consultas", "");
|
const path = event.path.replace("/.netlify/functions/consultas", "");
|
||||||
const method = event.httpMethod;
|
const method = event.httpMethod;
|
||||||
|
|
||||||
|
|||||||
@ -8,15 +8,8 @@
|
|||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"export:guia": "node scripts/export-guia.mjs",
|
|
||||||
"diagnose:login": "node --experimental-fetch scripts/diagnose-login.ts",
|
|
||||||
"deploy:netlify": "netlify deploy --prod --dir=dist",
|
"deploy:netlify": "netlify deploy --prod --dir=dist",
|
||||||
"deploy:netlify:build": "pnpm build && netlify deploy --prod --dir=dist",
|
"deploy:netlify:build": "pnpm build && netlify deploy --prod --dir=dist"
|
||||||
"gen:api-types": "openapi-typescript docs/api/openapi.partial.json --output src/types/api.d.ts",
|
|
||||||
"test": "vitest run",
|
|
||||||
"test:watch": "vitest",
|
|
||||||
"test:e2e-menu": "vitest run src/__tests__/accessibilityMenu.e2e.test.ts",
|
|
||||||
"check:api-drift": "node scripts/check-api-drift.cjs"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lumi.new/sdk": "^0.1.5",
|
"@lumi.new/sdk": "^0.1.5",
|
||||||
@ -32,34 +25,28 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/react": "^4.8.3",
|
|
||||||
"@eslint/js": "^9.9.1",
|
"@eslint/js": "^9.9.1",
|
||||||
"@netlify/functions": "^4.2.7",
|
"@netlify/functions": "^4.2.7",
|
||||||
"@testing-library/dom": "^10.4.0",
|
|
||||||
"@testing-library/react": "^16.0.0",
|
|
||||||
"@testing-library/user-event": "^14.5.2",
|
|
||||||
"@types/node": "^24.6.1",
|
"@types/node": "^24.6.1",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "4.3.2",
|
||||||
"@vitest/coverage-v8": "^2.1.4",
|
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"axe-core": "^4.10.0",
|
|
||||||
"eslint": "^9.9.1",
|
"eslint": "^9.9.1",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.11",
|
"eslint-plugin-react-refresh": "^0.4.11",
|
||||||
"finalhandler": "^1.2.0",
|
|
||||||
"globals": "^15.9.0",
|
"globals": "^15.9.0",
|
||||||
"happy-dom": "^19.0.2",
|
|
||||||
"jsdom": "^25.0.0",
|
|
||||||
"openapi-typescript": "^7.5.2",
|
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"puppeteer": "^22.15.0",
|
|
||||||
"serve-static": "^1.15.0",
|
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.5.3",
|
"typescript": "^5.5.3",
|
||||||
"typescript-eslint": "^8.3.0",
|
"typescript-eslint": "^8.3.0",
|
||||||
"vite": "5.4.10",
|
"vite": "5.4.10"
|
||||||
"vitest": "^2.1.4"
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"lru-cache": "7.18.3",
|
||||||
|
"@babel/helper-compilation-targets": "7.25.9",
|
||||||
|
"@asamuzakjp/css-color": "3.2.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2359
MEDICONNECT 2/pnpm-lock.yaml
generated
2359
MEDICONNECT 2/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,186 +0,0 @@
|
|||||||
// Script para atribuir o paciente Guilherme ao Fernando
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Admin credentials
|
|
||||||
const ADMIN_EMAIL = "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD = "riseup";
|
|
||||||
|
|
||||||
// Fernando user ID
|
|
||||||
const FERNANDO_USER_ID = "be1e3cba-534e-48c3-9590-b7e55861cade";
|
|
||||||
|
|
||||||
// Guilherme patient ID (do teste anterior)
|
|
||||||
const GUILHERME_ID = "864b1785-461f-4e92-8b74-2a6f17c58a80";
|
|
||||||
const GUILHERME_NOME = "Guilherme Silva Gomes - SQUAD 18";
|
|
||||||
|
|
||||||
async function atribuirGuilherme() {
|
|
||||||
try {
|
|
||||||
console.log("\n🔐 === ATRIBUIR GUILHERME AO FERNANDO ===\n");
|
|
||||||
|
|
||||||
// 1. Login como admin
|
|
||||||
console.log("1️⃣ Fazendo login como admin...");
|
|
||||||
const loginResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: ADMIN_EMAIL,
|
|
||||||
password: ADMIN_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Erro no login: ${loginResponse.status} - ${await loginResponse.text()}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
const accessToken = loginData.access_token;
|
|
||||||
const adminUserId = loginData.user.id;
|
|
||||||
|
|
||||||
console.log(`✅ Login admin realizado!`);
|
|
||||||
console.log(` Admin User ID: ${adminUserId}`);
|
|
||||||
|
|
||||||
// 2. Verificar se a atribuição já existe
|
|
||||||
console.log(`\n2️⃣ Verificando atribuições existentes...`);
|
|
||||||
|
|
||||||
const checkResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments?user_id=eq.${FERNANDO_USER_ID}&patient_id=eq.${GUILHERME_ID}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (checkResponse.ok) {
|
|
||||||
const existing = await checkResponse.json();
|
|
||||||
if (existing.length > 0) {
|
|
||||||
console.log(`⚠️ Atribuição já existe!`);
|
|
||||||
console.log(` Assignment ID: ${existing[0].id}`);
|
|
||||||
console.log(` Criado em: ${existing[0].created_at}`);
|
|
||||||
console.log(`\n✅ Guilherme já está atribuído ao Fernando!`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` ℹ️ Nenhuma atribuição existente encontrada.`);
|
|
||||||
|
|
||||||
// 3. Criar nova atribuição
|
|
||||||
console.log(`\n3️⃣ Criando nova atribuição...`);
|
|
||||||
console.log(` Paciente: ${GUILHERME_NOME}`);
|
|
||||||
console.log(` Médico: Fernando (${FERNANDO_USER_ID})`);
|
|
||||||
|
|
||||||
const atribuicao = {
|
|
||||||
patient_id: GUILHERME_ID,
|
|
||||||
user_id: FERNANDO_USER_ID,
|
|
||||||
role: "medico",
|
|
||||||
created_by: adminUserId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const createResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(atribuicao),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!createResponse.ok) {
|
|
||||||
const errorText = await createResponse.text();
|
|
||||||
throw new Error(
|
|
||||||
`Erro ao criar atribuição: ${createResponse.status} - ${errorText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await createResponse.json();
|
|
||||||
const assignment = Array.isArray(result) ? result[0] : result;
|
|
||||||
|
|
||||||
console.log(`✅ Atribuição criada com sucesso!`);
|
|
||||||
console.log(` Assignment ID: ${assignment.id}`);
|
|
||||||
console.log(` Patient ID: ${assignment.patient_id}`);
|
|
||||||
console.log(` User ID: ${assignment.user_id}`);
|
|
||||||
console.log(` Role: ${assignment.role}`);
|
|
||||||
console.log(` Created At: ${assignment.created_at}`);
|
|
||||||
|
|
||||||
// 4. Verificar todas as atribuições do Fernando
|
|
||||||
console.log(`\n4️⃣ Verificando todas as atribuições do Fernando...`);
|
|
||||||
|
|
||||||
const allAssignmentsResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments?user_id=eq.${FERNANDO_USER_ID}&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allAssignmentsResponse.ok) {
|
|
||||||
const assignments = await allAssignmentsResponse.json();
|
|
||||||
console.log(
|
|
||||||
`✅ Fernando possui ${assignments.length} paciente(s) atribuído(s):`
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = 0; i < assignments.length; i++) {
|
|
||||||
const a = assignments[i];
|
|
||||||
|
|
||||||
// Buscar nome do paciente
|
|
||||||
const patientResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?id=eq.${a.patient_id}&select=full_name`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let patientName = "Nome não encontrado";
|
|
||||||
if (patientResponse.ok) {
|
|
||||||
const patients = await patientResponse.json();
|
|
||||||
if (patients.length > 0) {
|
|
||||||
patientName = patients[0].full_name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` ${i + 1}. ${patientName}`);
|
|
||||||
console.log(` ID: ${a.patient_id}`);
|
|
||||||
console.log(` Role: ${a.role}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n🎉 SUCESSO!`);
|
|
||||||
console.log(` Guilherme agora está atribuído ao Fernando!`);
|
|
||||||
console.log(` Fernando pode vê-lo no painel médico.`);
|
|
||||||
console.log(`\n Para testar:`);
|
|
||||||
console.log(
|
|
||||||
` 1. Faça login: fernando.pirichowski@souunit.com.br / fernando`
|
|
||||||
);
|
|
||||||
console.log(` 2. Acesse o painel médico`);
|
|
||||||
console.log(` 3. Clique em "Novo Relatório"`);
|
|
||||||
console.log(` 4. Guilherme deve aparecer na lista de pacientes!`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ Erro:", error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(" Mensagem:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executar
|
|
||||||
atribuirGuilherme();
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script para cadastrar o paciente Guilherme Silva Gomes - SQUAD 18
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Credenciais do admin
|
|
||||||
const ADMIN_EMAIL = "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD = "riseup";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log("🔐 Fazendo login como admin...");
|
|
||||||
|
|
||||||
// 1. Login do admin
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: ADMIN_EMAIL,
|
|
||||||
password: ADMIN_PASSWORD,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = loginResponse.data.access_token;
|
|
||||||
console.log("✅ Login realizado com sucesso!\n");
|
|
||||||
|
|
||||||
// 2. Dados do paciente Guilherme Silva Gomes - SQUAD 18
|
|
||||||
const pacienteData = {
|
|
||||||
full_name: "Guilherme Silva Gomes - SQUAD 18",
|
|
||||||
email: "guilherme@paciente.com",
|
|
||||||
phone_mobile: "79999521847",
|
|
||||||
cpf: "11144477735", // CPF válido para teste (validado por algoritmo)
|
|
||||||
birth_date: "2000-01-01",
|
|
||||||
sex: "M",
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("📝 Cadastrando paciente:");
|
|
||||||
console.log(JSON.stringify(pacienteData, null, 2));
|
|
||||||
console.log("");
|
|
||||||
|
|
||||||
// 3. Cadastrar o paciente
|
|
||||||
const cadastroResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients`,
|
|
||||||
pacienteData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ Paciente cadastrado com sucesso!");
|
|
||||||
console.log("Dados retornados:");
|
|
||||||
console.log(JSON.stringify(cadastroResponse.data, null, 2));
|
|
||||||
console.log("");
|
|
||||||
|
|
||||||
// 4. Verificar se o paciente aparece na API
|
|
||||||
console.log("🔍 Verificando se o paciente aparece na lista...");
|
|
||||||
|
|
||||||
const listaResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const guilherme = listaResponse.data.find(
|
|
||||||
(p) => p.email === "guilherme@paciente.com"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (guilherme) {
|
|
||||||
console.log("✅ SUCESSO! Paciente encontrado na API:");
|
|
||||||
console.log(JSON.stringify(guilherme, null, 2));
|
|
||||||
} else {
|
|
||||||
console.log("❌ Paciente não encontrado na lista.");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("");
|
|
||||||
console.log(`📊 Total de pacientes na base: ${listaResponse.data.length}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.response?.data || error.message);
|
|
||||||
if (error.response) {
|
|
||||||
console.error("Status:", error.response.status);
|
|
||||||
console.error("Headers:", error.response.headers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/*
|
|
||||||
Verifica se a saída gerada de openapi-typescript difere do arquivo commitado.
|
|
||||||
Estratégia:
|
|
||||||
1. Gera tipos em memória (spawn openapi-typescript) para stdout.
|
|
||||||
2. Lê conteúdo atual de src/types/api.d.ts.
|
|
||||||
3. Compara strings normalizando quebras de linha.
|
|
||||||
4. Se diferente -> exit 1 com mensagem.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { spawnSync } = require("node:child_process");
|
|
||||||
const { readFileSync } = require("node:fs");
|
|
||||||
const path = require("node:path");
|
|
||||||
|
|
||||||
const SPEC = path.resolve(process.cwd(), "docs/api/openapi.partial.json");
|
|
||||||
const TARGET = path.resolve(process.cwd(), "src/types/api.d.ts");
|
|
||||||
|
|
||||||
function generateTypes() {
|
|
||||||
const result = spawnSync("npx", ["openapi-typescript", SPEC], {
|
|
||||||
encoding: "utf-8",
|
|
||||||
});
|
|
||||||
if (result.status !== 0) {
|
|
||||||
console.error(
|
|
||||||
"[check:api-drift] Falha ao gerar tipos:",
|
|
||||||
result.stderr || result.stdout
|
|
||||||
);
|
|
||||||
process.exit(2);
|
|
||||||
}
|
|
||||||
return result.stdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalize(str) {
|
|
||||||
return str.replace(/\r\n?/g, "\n").trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const generated = normalize(generateTypes());
|
|
||||||
const current = normalize(readFileSync(TARGET, "utf-8"));
|
|
||||||
if (generated !== current) {
|
|
||||||
console.error(
|
|
||||||
"\n[check:api-drift] Diferença detectada entre spec e tipos commitados."
|
|
||||||
);
|
|
||||||
console.error("Execute: pnpm gen:api-types");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
console.log("[check:api-drift] OK - tipos sincronizados.");
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[check:api-drift] Erro inesperado:", e.message);
|
|
||||||
process.exit(2);
|
|
||||||
}
|
|
||||||
@ -1,58 +0,0 @@
|
|||||||
// Script para diagnosticar localStorage e limpar tokens expirados
|
|
||||||
console.log("\n========== DIAGNÓSTICO LOCALSTORAGE ==========");
|
|
||||||
|
|
||||||
const keys = ["authToken", "token", "refreshToken", "authUser", "appSession"];
|
|
||||||
keys.forEach((k) => {
|
|
||||||
const val = localStorage.getItem(k);
|
|
||||||
if (val) {
|
|
||||||
console.log(
|
|
||||||
`${k}:`,
|
|
||||||
val.length > 100 ? val.substring(0, 100) + "..." : val
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(`${k}: (ausente)`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Decode JWT se existir
|
|
||||||
function decodeJwt(token: string | null): {
|
|
||||||
valid: boolean;
|
|
||||||
payload?: { exp?: number; role?: string; sub?: string };
|
|
||||||
expired?: boolean;
|
|
||||||
} {
|
|
||||||
if (!token) return { valid: false };
|
|
||||||
try {
|
|
||||||
const parts = token.split(".");
|
|
||||||
if (parts.length !== 3) return { valid: false };
|
|
||||||
const payload = JSON.parse(atob(parts[1]));
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const expired = payload.exp ? payload.exp < now : false;
|
|
||||||
return { valid: true, payload, expired };
|
|
||||||
} catch {
|
|
||||||
return { valid: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tok = localStorage.getItem("authToken") || localStorage.getItem("token");
|
|
||||||
if (tok) {
|
|
||||||
const decoded = decodeJwt(tok);
|
|
||||||
console.log("\n[Decode token]", decoded);
|
|
||||||
if (decoded.expired) {
|
|
||||||
console.warn("⚠️ Token expirado! Limpando...");
|
|
||||||
localStorage.removeItem("authToken");
|
|
||||||
localStorage.removeItem("token");
|
|
||||||
localStorage.removeItem("refreshToken");
|
|
||||||
localStorage.removeItem("authUser");
|
|
||||||
console.log(
|
|
||||||
"✅ Tokens removidos. Recarregue a página e faça login novamente."
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log("✅ Token ainda válido.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"\n[Diagnóstico] Nenhum authToken encontrado. Usuário não autenticado."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("==============================================\n");
|
|
||||||
@ -1,116 +0,0 @@
|
|||||||
-- =========================================
|
|
||||||
-- POLÍTICAS RLS PARA SECRETÁRIA CADASTRAR
|
|
||||||
-- =========================================
|
|
||||||
-- Execute este SQL no Supabase SQL Editor
|
|
||||||
-- URL: https://app.supabase.com/project/yuanqfswhberkoevtmfr/sql/new
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- TABELA DOCTORS (Médicos)
|
|
||||||
-- =========================================
|
|
||||||
|
|
||||||
-- SELECT: Qualquer um autenticado pode ler
|
|
||||||
DROP POLICY IF EXISTS "doctors_select_authenticated" ON doctors;
|
|
||||||
CREATE POLICY "doctors_select_authenticated"
|
|
||||||
ON doctors FOR SELECT
|
|
||||||
TO authenticated
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- INSERT: Qualquer usuário autenticado pode criar
|
|
||||||
DROP POLICY IF EXISTS "doctors_insert_authenticated" ON doctors;
|
|
||||||
CREATE POLICY "doctors_insert_authenticated"
|
|
||||||
ON doctors FOR INSERT
|
|
||||||
TO authenticated
|
|
||||||
WITH CHECK (true);
|
|
||||||
|
|
||||||
-- UPDATE: Qualquer usuário autenticado pode atualizar
|
|
||||||
DROP POLICY IF EXISTS "doctors_update_authenticated" ON doctors;
|
|
||||||
CREATE POLICY "doctors_update_authenticated"
|
|
||||||
ON doctors FOR UPDATE
|
|
||||||
TO authenticated
|
|
||||||
USING (true)
|
|
||||||
WITH CHECK (true);
|
|
||||||
|
|
||||||
-- DELETE: Qualquer usuário autenticado pode deletar
|
|
||||||
DROP POLICY IF EXISTS "doctors_delete_authenticated" ON doctors;
|
|
||||||
CREATE POLICY "doctors_delete_authenticated"
|
|
||||||
ON doctors FOR DELETE
|
|
||||||
TO authenticated
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- TABELA PATIENTS (Pacientes)
|
|
||||||
-- =========================================
|
|
||||||
|
|
||||||
-- SELECT: Qualquer um autenticado pode ler
|
|
||||||
DROP POLICY IF EXISTS "patients_select_authenticated" ON patients;
|
|
||||||
CREATE POLICY "patients_select_authenticated"
|
|
||||||
ON patients FOR SELECT
|
|
||||||
TO authenticated
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- INSERT: Qualquer usuário autenticado pode criar
|
|
||||||
DROP POLICY IF EXISTS "patients_insert_authenticated" ON patients;
|
|
||||||
CREATE POLICY "patients_insert_authenticated"
|
|
||||||
ON patients FOR INSERT
|
|
||||||
TO authenticated
|
|
||||||
WITH CHECK (true);
|
|
||||||
|
|
||||||
-- UPDATE: Qualquer usuário autenticado pode atualizar
|
|
||||||
DROP POLICY IF EXISTS "patients_update_authenticated" ON patients;
|
|
||||||
CREATE POLICY "patients_update_authenticated"
|
|
||||||
ON patients FOR UPDATE
|
|
||||||
TO authenticated
|
|
||||||
USING (true)
|
|
||||||
WITH CHECK (true);
|
|
||||||
|
|
||||||
-- DELETE: Qualquer usuário autenticado pode deletar
|
|
||||||
DROP POLICY IF EXISTS "patients_delete_authenticated" ON patients;
|
|
||||||
CREATE POLICY "patients_delete_authenticated"
|
|
||||||
ON patients FOR DELETE
|
|
||||||
TO authenticated
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- TABELA PROFILES (Perfis - se existir)
|
|
||||||
-- =========================================
|
|
||||||
|
|
||||||
-- SELECT: Qualquer um autenticado pode ler
|
|
||||||
DROP POLICY IF EXISTS "profiles_select_authenticated" ON profiles;
|
|
||||||
CREATE POLICY "profiles_select_authenticated"
|
|
||||||
ON profiles FOR SELECT
|
|
||||||
TO authenticated
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- INSERT: Pode criar próprio perfil
|
|
||||||
DROP POLICY IF EXISTS "profiles_insert_own" ON profiles;
|
|
||||||
CREATE POLICY "profiles_insert_own"
|
|
||||||
ON profiles FOR INSERT
|
|
||||||
TO authenticated
|
|
||||||
WITH CHECK (auth.uid() = id);
|
|
||||||
|
|
||||||
-- UPDATE: Pode atualizar próprio perfil ou qualquer se for admin
|
|
||||||
DROP POLICY IF EXISTS "profiles_update_own_or_admin" ON profiles;
|
|
||||||
CREATE POLICY "profiles_update_own_or_admin"
|
|
||||||
ON profiles FOR UPDATE
|
|
||||||
TO authenticated
|
|
||||||
USING (auth.uid() = id OR true)
|
|
||||||
WITH CHECK (auth.uid() = id OR true);
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- GARANTIR QUE RLS ESTÁ ATIVADO
|
|
||||||
-- =========================================
|
|
||||||
|
|
||||||
ALTER TABLE doctors ENABLE ROW LEVEL SECURITY;
|
|
||||||
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
|
|
||||||
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- RESULTADO ESPERADO
|
|
||||||
-- =========================================
|
|
||||||
-- Após executar este script:
|
|
||||||
-- ✅ Secretária pode cadastrar médicos
|
|
||||||
-- ✅ Secretária pode cadastrar pacientes
|
|
||||||
-- ✅ Secretária pode editar médicos e pacientes
|
|
||||||
-- ✅ Secretária pode deletar médicos e pacientes
|
|
||||||
-- ✅ Admin pode fazer tudo
|
|
||||||
-- ✅ RLS continua protegendo acesso não autenticado
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
// Script para criar atribuições de pacientes para o Fernando
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Admin credentials
|
|
||||||
const ADMIN_EMAIL = "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD = "riseup";
|
|
||||||
|
|
||||||
// Fernando user ID (do teste anterior)
|
|
||||||
const FERNANDO_USER_ID = "be1e3cba-534e-48c3-9590-b7e55861cade";
|
|
||||||
|
|
||||||
// IDs dos pacientes (do teste anterior)
|
|
||||||
const PACIENTES = [
|
|
||||||
{
|
|
||||||
id: "27aff771-8297-4ab2-8886-de8cf09c3895",
|
|
||||||
nome: "Isaac Kauã Barrozo Oliveira",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "5236952f-efdd-4af6-b94b-0b28a89cb06c",
|
|
||||||
nome: "João Pedro Lima dos Santos",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "7ddbd1e2-1aee-4f7a-94f9-ee4c735ca276",
|
|
||||||
nome: "Gabriel Nascimento Correia",
|
|
||||||
},
|
|
||||||
{ id: "1f5ac462-faf1-4290-ac55-d1900afb074e", nome: "Danilo Santos" },
|
|
||||||
{
|
|
||||||
id: "cf835709-616f-428f-8055-1acf53ee24bb",
|
|
||||||
nome: "Jonas Francisco Nascimento Bonfim",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
async function criarAtribuicoes() {
|
|
||||||
try {
|
|
||||||
console.log("\n🔐 === CRIAR ATRIBUIÇÕES PARA FERNANDO ===\n");
|
|
||||||
|
|
||||||
// 1. Login como admin
|
|
||||||
console.log("1️⃣ Fazendo login como admin...");
|
|
||||||
const loginResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: ADMIN_EMAIL,
|
|
||||||
password: ADMIN_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Erro no login: ${loginResponse.status} - ${await loginResponse.text()}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
const accessToken = loginData.access_token;
|
|
||||||
const adminUserId = loginData.user.id;
|
|
||||||
|
|
||||||
console.log(`✅ Login admin realizado!`);
|
|
||||||
console.log(` Admin User ID: ${adminUserId}`);
|
|
||||||
|
|
||||||
// 2. Criar atribuições para cada paciente
|
|
||||||
console.log(
|
|
||||||
`\n2️⃣ Criando atribuições para Fernando (${FERNANDO_USER_ID})...\n`
|
|
||||||
);
|
|
||||||
|
|
||||||
let sucessos = 0;
|
|
||||||
let erros = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < PACIENTES.length; i++) {
|
|
||||||
const paciente = PACIENTES[i];
|
|
||||||
console.log(
|
|
||||||
` [${i + 1}/${PACIENTES.length}] Atribuindo: ${paciente.nome}...`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const atribuicao = {
|
|
||||||
patient_id: paciente.id,
|
|
||||||
user_id: FERNANDO_USER_ID,
|
|
||||||
role: "medico",
|
|
||||||
created_by: adminUserId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(atribuicao),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
throw new Error(`${response.status} - ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log(
|
|
||||||
` ✅ Sucesso! Assignment ID: ${
|
|
||||||
result[0]?.id || result.id || "N/A"
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
sucessos++;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
` ❌ Erro:`,
|
|
||||||
error instanceof Error ? error.message : error
|
|
||||||
);
|
|
||||||
erros++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Verificar atribuições criadas
|
|
||||||
console.log(`\n3️⃣ Verificando atribuições criadas...\n`);
|
|
||||||
|
|
||||||
const verificarResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments?user_id=eq.${FERNANDO_USER_ID}&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (verificarResponse.ok) {
|
|
||||||
const assignments = await verificarResponse.json();
|
|
||||||
console.log(`✅ Total de atribuições do Fernando: ${assignments.length}`);
|
|
||||||
|
|
||||||
assignments.forEach((a, i) => {
|
|
||||||
console.log(` ${i + 1}. Patient: ${a.patient_id} | Role: ${a.role}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Resumo
|
|
||||||
console.log(`\n📊 === RESUMO ===`);
|
|
||||||
console.log(` ✅ Sucessos: ${sucessos}`);
|
|
||||||
console.log(` ❌ Erros: ${erros}`);
|
|
||||||
console.log(` 📋 Total tentados: ${PACIENTES.length}`);
|
|
||||||
|
|
||||||
if (sucessos > 0) {
|
|
||||||
console.log(
|
|
||||||
`\n🎉 Fernando agora pode ver ${sucessos} pacientes no painel médico!`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Faça login com: fernando.pirichowski@souunit.com.br / fernando`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ Erro geral:", error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(" Mensagem:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executar
|
|
||||||
criarAtribuicoes();
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script para criar 3 consultas de exemplo para o usuário/paciente Pedro Araujo.
|
|
||||||
* Credenciais locais fornecidas: Email: pedro.araujo@mediconnect.com Senha: local123
|
|
||||||
* Este script NÃO cria o usuário nem o paciente se não existirem; apenas tenta
|
|
||||||
* localizar o paciente por email e gerar um arquivo local de demonstração
|
|
||||||
* (src/data/consultas-pedro.json) e opcionalmente mesclar no consultas-demo.json.
|
|
||||||
*
|
|
||||||
* Modo 1 (arquivo local): Gera JSON com consultas fictícias.
|
|
||||||
* Modo 2 (Supabase) - opcional futuro: Inserir via REST (requer tabela appointments e RLS configurada).
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import fetch from 'node-fetch';
|
|
||||||
|
|
||||||
const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
|
|
||||||
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
|
|
||||||
|
|
||||||
const PEDRO_EMAIL = 'pedro.araujo@mediconnect.com';
|
|
||||||
// Placeholder: se souber o ID real do paciente no Supabase, coloque aqui para futura inserção
|
|
||||||
let pedroPatientId = null;
|
|
||||||
|
|
||||||
async function tentarLocalizarPaciente() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${SUPABASE_URL}/rest/v1/patients?select=id,email&email=eq.${encodeURIComponent(PEDRO_EMAIL)}`, {
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!res.ok) return null;
|
|
||||||
const data = await res.json();
|
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
|
||||||
return data[0].id;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function criarConsultasLocais(patientIdOrEmail) {
|
|
||||||
const agora = new Date();
|
|
||||||
const isoFuturo = (dias, hora) => {
|
|
||||||
const d = new Date(agora.getTime() + dias * 86400000);
|
|
||||||
d.setHours(hora, 0, 0, 0);
|
|
||||||
return d.toISOString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const medicoFernandoId = 'be1e3cba-534e-48c3-9590-b7e55861cade';
|
|
||||||
const medicoFernandoNome = 'Fernando Pirichowski - Squad 18';
|
|
||||||
const pacientePedroNome = 'Pedro Araujo';
|
|
||||||
|
|
||||||
const consultas = [
|
|
||||||
{
|
|
||||||
id: 'consulta-demo-pedro-001',
|
|
||||||
pacienteId: patientIdOrEmail,
|
|
||||||
medicoId: medicoFernandoId,
|
|
||||||
pacienteNome: pacientePedroNome,
|
|
||||||
medicoNome: medicoFernandoNome,
|
|
||||||
dataHora: isoFuturo(2, 10),
|
|
||||||
status: 'agendada',
|
|
||||||
tipo: 'Consulta',
|
|
||||||
observacoes: 'Primeira avaliação clínica do Pedro.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'consulta-demo-pedro-002',
|
|
||||||
pacienteId: patientIdOrEmail,
|
|
||||||
medicoId: medicoFernandoId,
|
|
||||||
pacienteNome: pacientePedroNome,
|
|
||||||
medicoNome: medicoFernandoNome,
|
|
||||||
dataHora: isoFuturo(7, 9),
|
|
||||||
status: 'confirmada',
|
|
||||||
tipo: 'Retorno',
|
|
||||||
observacoes: 'Retorno para revisar sintomas.'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'consulta-demo-pedro-003',
|
|
||||||
pacienteId: patientIdOrEmail,
|
|
||||||
medicoId: medicoFernandoId,
|
|
||||||
pacienteNome: pacientePedroNome,
|
|
||||||
medicoNome: medicoFernandoNome,
|
|
||||||
dataHora: isoFuturo(14, 11),
|
|
||||||
status: 'agendada',
|
|
||||||
tipo: 'Exame',
|
|
||||||
observacoes: 'Agendamento de exame complementar.'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
return consultas;
|
|
||||||
}
|
|
||||||
|
|
||||||
function salvarArquivoJson(fileName, data) {
|
|
||||||
const dataDir = path.join(process.cwd(), 'src', 'data');
|
|
||||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
|
||||||
const fullPath = path.join(dataDir, fileName);
|
|
||||||
fs.writeFileSync(fullPath, JSON.stringify(data, null, 2));
|
|
||||||
console.log(`✅ Arquivo gerado: ${fullPath}`);
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mesclarNoConsultasDemo(novas) {
|
|
||||||
const demoPath = path.join(process.cwd(), 'src', 'data', 'consultas-demo.json');
|
|
||||||
if (!fs.existsSync(demoPath)) {
|
|
||||||
console.log('ℹ️ consultas-demo.json não encontrado, pulando mescla.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const atual = JSON.parse(fs.readFileSync(demoPath, 'utf-8'));
|
|
||||||
const idsExistentes = new Set(atual.map(c => c.id));
|
|
||||||
const filtradas = novas.filter(c => !idsExistentes.has(c.id));
|
|
||||||
const combinado = [...atual, ...filtradas];
|
|
||||||
fs.writeFileSync(demoPath, JSON.stringify(combinado, null, 2));
|
|
||||||
console.log(`✅ ${filtradas.length} consultas adicionadas a consultas-demo.json`);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('⚠️ Falha ao mesclar no consultas-demo.json:', e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('\n📁 Criando consultas de exemplo para Pedro...');
|
|
||||||
const pacienteId = await tentarLocalizarPaciente();
|
|
||||||
if (pacienteId) {
|
|
||||||
pedroPatientId = pacienteId;
|
|
||||||
console.log(`✅ Paciente encontrado no Supabase: ${pacienteId}`);
|
|
||||||
} else {
|
|
||||||
console.log('ℹ️ Paciente Pedro não encontrado no Supabase — usando email como identificador local.');
|
|
||||||
}
|
|
||||||
const ident = pedroPatientId || PEDRO_EMAIL;
|
|
||||||
const consultas = criarConsultasLocais(ident);
|
|
||||||
salvarArquivoJson('consultas-pedro.json', consultas);
|
|
||||||
mesclarNoConsultasDemo(consultas);
|
|
||||||
console.log('\n✨ Concluído. Você pode agora:');
|
|
||||||
console.log(' 1. Rodar a aplicação (pnpm dev)');
|
|
||||||
console.log(' 2. Verificar se seu código carrega dados de src/data/consultas-demo.json');
|
|
||||||
console.log(' 3. (Se não carregar automaticamente) Injetar via console usando snippet que fornecerei.');
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(e => {
|
|
||||||
console.error('❌ Erro no script:', e.message);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Credenciais admin para realizar INSERTs autenticados (RLS exige usuário autenticado)
|
|
||||||
const ADMIN_EMAIL = process.env.TEST_ADMIN_EMAIL || "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD =
|
|
||||||
process.env.TEST_ADMIN_PASSWORD || "riseup";
|
|
||||||
|
|
||||||
console.log("\n🔧 CRIANDO DADOS DE TESTE\n");
|
|
||||||
|
|
||||||
async function loginAdmin() {
|
|
||||||
console.log("🔐 Fazendo login como admin para inserir dados (RLS)...");
|
|
||||||
const res = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email: ADMIN_EMAIL, password: ADMIN_PASSWORD }),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!res.ok) {
|
|
||||||
const txt = await res.text();
|
|
||||||
throw new Error(`Falha no login admin (${res.status}): ${txt}`);
|
|
||||||
}
|
|
||||||
const data = await res.json();
|
|
||||||
console.log("✅ Login admin OK\n");
|
|
||||||
return data.access_token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function criarMedicoTeste(adminToken) {
|
|
||||||
console.log("👨⚕️ Criando médico de teste...");
|
|
||||||
|
|
||||||
const medico = {
|
|
||||||
full_name: "Dr. João Silva",
|
|
||||||
email: "drjoao@mediconnect.com",
|
|
||||||
crm: "12345",
|
|
||||||
crm_uf: "SE",
|
|
||||||
specialty: "Cardiologia",
|
|
||||||
phone_mobile: "79999999999",
|
|
||||||
cpf: "12345678900",
|
|
||||||
active: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${SUPABASE_URL}/rest/v1/doctors`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
// IMPORTANTE: usar token do admin autenticado para permitir INSERT (RLS)
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(medico),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
console.log("✅ Médico criado com sucesso!");
|
|
||||||
console.log(" ID:", data[0]?.id);
|
|
||||||
console.log(" Nome:", data[0]?.full_name);
|
|
||||||
return data[0];
|
|
||||||
} else {
|
|
||||||
console.log("❌ Erro ao criar médico:", response.status);
|
|
||||||
const error = await response.text();
|
|
||||||
console.log(error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function criarPacienteTeste(adminToken) {
|
|
||||||
console.log("\n👤 Criando paciente de teste...");
|
|
||||||
|
|
||||||
const paciente = {
|
|
||||||
full_name: "Maria Santos",
|
|
||||||
email: "maria@example.com",
|
|
||||||
phone_mobile: "79988888888",
|
|
||||||
cpf: "98765432100",
|
|
||||||
birth_date: "1990-05-15",
|
|
||||||
street: "Rua das Flores",
|
|
||||||
number: "100",
|
|
||||||
neighborhood: "Centro",
|
|
||||||
city: "Aracaju",
|
|
||||||
state: "SE",
|
|
||||||
cep: "49000-000",
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${SUPABASE_URL}/rest/v1/patients`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
// IMPORTANTE: usar token do admin autenticado para permitir INSERT (RLS)
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(paciente),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
console.log("✅ Paciente criado com sucesso!");
|
|
||||||
console.log(" ID:", data[0]?.id);
|
|
||||||
console.log(" Nome:", data[0]?.full_name);
|
|
||||||
return data[0];
|
|
||||||
} else {
|
|
||||||
console.log("❌ Erro ao criar paciente:", response.status);
|
|
||||||
const error = await response.text();
|
|
||||||
console.log(error);
|
|
||||||
|
|
||||||
if (response.status === 403 || response.status === 401) {
|
|
||||||
console.log("\n⚠️ RLS está bloqueando a inserção anônima!");
|
|
||||||
console.log(" Você precisa:");
|
|
||||||
console.log(" 1. Criar uma política RLS que permita INSERT público");
|
|
||||||
console.log(
|
|
||||||
" 2. Ou usar a service_role key (não recomendado para front-end)"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" 3. Ou criar através da interface de cadastro (com autenticação)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function criar() {
|
|
||||||
const adminToken = await loginAdmin();
|
|
||||||
await criarMedicoTeste(adminToken);
|
|
||||||
await criarPacienteTeste(adminToken);
|
|
||||||
|
|
||||||
console.log("\n\n📊 VERIFICANDO RESULTADOS...\n");
|
|
||||||
|
|
||||||
// Verificar médicos
|
|
||||||
const respMedicos = await fetch(`${SUPABASE_URL}/rest/v1/doctors?select=*`, {
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (respMedicos.ok) {
|
|
||||||
const medicos = await respMedicos.json();
|
|
||||||
console.log(`✅ Médicos cadastrados: ${medicos.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verificar pacientes
|
|
||||||
const respPacientes = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (respPacientes.ok) {
|
|
||||||
const pacientes = await respPacientes.json();
|
|
||||||
console.log(`✅ Pacientes cadastrados: ${pacientes.length}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n✨ Pronto! Agora os painéis devem mostrar os dados.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
criar();
|
|
||||||
@ -1,413 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script completo para criar usuário Guilherme com role "user"
|
|
||||||
* Email: guilhermesilvagomes1020@gmail.com
|
|
||||||
* Telefone: 79999521847
|
|
||||||
*/
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Admin credentials
|
|
||||||
const ADMIN_EMAIL = "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD = "riseup";
|
|
||||||
|
|
||||||
// Guilherme dados atualizados
|
|
||||||
const GUILHERME_EMAIL = "guilhermesilvagomes1020@gmail.com";
|
|
||||||
const GUILHERME_PASSWORD = "guilherme123";
|
|
||||||
const GUILHERME_NOME = "Guilherme Silva Gomes - SQUAD 18";
|
|
||||||
const GUILHERME_TELEFONE = "79999521847";
|
|
||||||
const GUILHERME_CPF = "11144477735"; // CPF válido para teste
|
|
||||||
|
|
||||||
// Fernando dados
|
|
||||||
const FERNANDO_USER_ID = "be1e3cba-534e-48c3-9590-b7e55861cade";
|
|
||||||
const FERNANDO_NOME = "Fernando Pirichowski - Squad 18";
|
|
||||||
|
|
||||||
async function criarGuilhermeCompleto() {
|
|
||||||
try {
|
|
||||||
console.log("\n🔐 === CRIAR GUILHERME COMPLETO ===\n");
|
|
||||||
|
|
||||||
// 1. Login como admin
|
|
||||||
console.log("1️⃣ Fazendo login como admin...");
|
|
||||||
const loginResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: ADMIN_EMAIL,
|
|
||||||
password: ADMIN_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
|
||||||
throw new Error(`Erro no login: ${loginResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
const adminToken = loginData.access_token;
|
|
||||||
console.log("✅ Login admin realizado!\n");
|
|
||||||
|
|
||||||
// 2. Criar paciente Guilherme
|
|
||||||
console.log("2️⃣ Criando paciente Guilherme...");
|
|
||||||
console.log(` Nome: ${GUILHERME_NOME}`);
|
|
||||||
console.log(` Email: ${GUILHERME_EMAIL}`);
|
|
||||||
console.log(` Telefone: ${GUILHERME_TELEFONE}`);
|
|
||||||
console.log(` CPF: ${GUILHERME_CPF}\n`);
|
|
||||||
|
|
||||||
// Verificar se paciente já existe
|
|
||||||
const checkPatientResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?email=eq.${encodeURIComponent(
|
|
||||||
GUILHERME_EMAIL
|
|
||||||
)}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let existingPatients = await checkPatientResponse.json();
|
|
||||||
let guilhermePatientId;
|
|
||||||
|
|
||||||
// Verificar por email ou CPF
|
|
||||||
if (!existingPatients || existingPatients.length === 0) {
|
|
||||||
const checkByCpfResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?cpf=eq.${GUILHERME_CPF}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
existingPatients = await checkByCpfResponse.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingPatients && existingPatients.length > 0) {
|
|
||||||
guilhermePatientId = existingPatients[0].id;
|
|
||||||
console.log("✅ Paciente já existe!");
|
|
||||||
console.log(` Patient ID: ${guilhermePatientId}`);
|
|
||||||
console.log(` Nome: ${existingPatients[0].full_name}`);
|
|
||||||
console.log(` Email: ${existingPatients[0].email}\n`);
|
|
||||||
} else {
|
|
||||||
// Criar paciente
|
|
||||||
const createPatientResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
full_name: GUILHERME_NOME,
|
|
||||||
email: GUILHERME_EMAIL,
|
|
||||||
phone_mobile: GUILHERME_TELEFONE,
|
|
||||||
cpf: GUILHERME_CPF,
|
|
||||||
birth_date: "2000-10-20",
|
|
||||||
sex: "M",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!createPatientResponse.ok) {
|
|
||||||
const error = await createPatientResponse.text();
|
|
||||||
console.error("❌ Erro ao criar paciente:", error);
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const patientData = await createPatientResponse.json();
|
|
||||||
guilhermePatientId = patientData[0]?.id || patientData.id;
|
|
||||||
console.log("✅ Paciente criado!");
|
|
||||||
console.log(` Patient ID: ${guilhermePatientId}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Criar usuário com role "user"
|
|
||||||
console.log("3️⃣ Criando usuário com role 'user'...");
|
|
||||||
console.log(` Email: ${GUILHERME_EMAIL}`);
|
|
||||||
console.log(` Senha: ${GUILHERME_PASSWORD}`);
|
|
||||||
console.log(` Role: user\n`);
|
|
||||||
|
|
||||||
// Verificar se usuário já existe
|
|
||||||
try {
|
|
||||||
const checkUserLogin = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: GUILHERME_EMAIL,
|
|
||||||
password: GUILHERME_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (checkUserLogin.ok) {
|
|
||||||
const userData = await checkUserLogin.json();
|
|
||||||
console.log("✅ Usuário já existe!");
|
|
||||||
console.log(` User ID: ${userData.user.id}\n`);
|
|
||||||
|
|
||||||
// Atribuir paciente ao usuário
|
|
||||||
await atribuirPaciente(
|
|
||||||
adminToken,
|
|
||||||
userData.user.id,
|
|
||||||
guilhermePatientId
|
|
||||||
);
|
|
||||||
await criarConsultas(guilhermePatientId);
|
|
||||||
mostrarResumo();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log("ℹ️ Usuário não existe, criando...\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Criar usuário via Edge Function
|
|
||||||
const createUserResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/functions/v1/create-user`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: GUILHERME_EMAIL,
|
|
||||||
password: GUILHERME_PASSWORD,
|
|
||||||
full_name: GUILHERME_NOME,
|
|
||||||
role: "user",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const createUserText = await createUserResponse.text();
|
|
||||||
console.log(" Resposta da criação:", createUserText);
|
|
||||||
|
|
||||||
let createUserData;
|
|
||||||
try {
|
|
||||||
createUserData = JSON.parse(createUserText);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("❌ Erro ao parsear resposta:", createUserText);
|
|
||||||
throw new Error("Resposta inválida da API");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!createUserResponse.ok) {
|
|
||||||
console.error("❌ Erro ao criar usuário:", createUserData);
|
|
||||||
throw new Error(JSON.stringify(createUserData));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tentar obter user_id de várias formas
|
|
||||||
let guilhermeUserId =
|
|
||||||
createUserData.user_id ||
|
|
||||||
createUserData.id ||
|
|
||||||
createUserData.userId ||
|
|
||||||
createUserData.user?.id;
|
|
||||||
|
|
||||||
if (!guilhermeUserId) {
|
|
||||||
// Tentar fazer login para obter o ID
|
|
||||||
console.log(" Tentando obter ID via login...");
|
|
||||||
const loginGuilherme = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: GUILHERME_EMAIL,
|
|
||||||
password: GUILHERME_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loginGuilherme.ok) {
|
|
||||||
const loginData = await loginGuilherme.json();
|
|
||||||
guilhermeUserId = loginData.user.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!guilhermeUserId) {
|
|
||||||
console.error("❌ Não foi possível obter o User ID!");
|
|
||||||
console.error(" Resposta:", createUserData);
|
|
||||||
throw new Error("User ID não disponível");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Usuário criado com sucesso!");
|
|
||||||
console.log(` User ID: ${guilhermeUserId}\n`);
|
|
||||||
|
|
||||||
// 4. Atribuir paciente ao usuário
|
|
||||||
await atribuirPaciente(adminToken, guilhermeUserId, guilhermePatientId);
|
|
||||||
|
|
||||||
// 5. Criar consultas
|
|
||||||
await criarConsultas(guilhermePatientId);
|
|
||||||
|
|
||||||
// 6. Mostrar resumo
|
|
||||||
mostrarResumo();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ ERRO:", error.message);
|
|
||||||
if (error.stack) {
|
|
||||||
console.error(error.stack);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function atribuirPaciente(adminToken, userId, patientId) {
|
|
||||||
console.log("4️⃣ Atribuindo paciente ao usuário...");
|
|
||||||
|
|
||||||
// Verificar se atribuição já existe
|
|
||||||
const checkResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments?user_id=eq.${userId}&patient_id=eq.${patientId}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const existing = await checkResponse.json();
|
|
||||||
|
|
||||||
if (existing && existing.length > 0) {
|
|
||||||
console.log("✅ Atribuição já existe!\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Criar atribuição
|
|
||||||
const assignResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
user_id: userId,
|
|
||||||
patient_id: patientId,
|
|
||||||
role: "user", // Adicionar role na atribuição
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!assignResponse.ok) {
|
|
||||||
const error = await assignResponse.text();
|
|
||||||
console.error("⚠️ Erro ao criar atribuição:", error);
|
|
||||||
} else {
|
|
||||||
console.log("✅ Paciente atribuído ao usuário!\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function criarConsultas(guilhermePatientId) {
|
|
||||||
console.log("5️⃣ Criando consultas de demonstração...\n");
|
|
||||||
|
|
||||||
const consultas = [
|
|
||||||
{
|
|
||||||
id: "consulta-demo-guilherme-001",
|
|
||||||
pacienteId: guilhermePatientId,
|
|
||||||
medicoId: FERNANDO_USER_ID,
|
|
||||||
pacienteNome: GUILHERME_NOME,
|
|
||||||
medicoNome: FERNANDO_NOME,
|
|
||||||
dataHora: "2025-10-05T10:00:00",
|
|
||||||
status: "agendada",
|
|
||||||
tipo: "Consulta",
|
|
||||||
observacoes: "Primeira consulta - Check-up geral",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "consulta-demo-guilherme-002",
|
|
||||||
pacienteId: guilhermePatientId,
|
|
||||||
medicoId: FERNANDO_USER_ID,
|
|
||||||
pacienteNome: GUILHERME_NOME,
|
|
||||||
medicoNome: FERNANDO_NOME,
|
|
||||||
dataHora: "2025-09-28T14:30:00",
|
|
||||||
status: "realizada",
|
|
||||||
tipo: "Retorno",
|
|
||||||
observacoes: "Consulta de retorno - Avaliação de exames",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "consulta-demo-guilherme-003",
|
|
||||||
pacienteId: guilhermePatientId,
|
|
||||||
medicoId: FERNANDO_USER_ID,
|
|
||||||
pacienteNome: GUILHERME_NOME,
|
|
||||||
medicoNome: FERNANDO_NOME,
|
|
||||||
dataHora: "2025-10-10T09:00:00",
|
|
||||||
status: "confirmada",
|
|
||||||
tipo: "Consulta",
|
|
||||||
observacoes: "Consulta de acompanhamento mensal",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Usar import dinâmico para módulos ES
|
|
||||||
const fs = await import("fs");
|
|
||||||
const path = await import("path");
|
|
||||||
const { fileURLToPath } = await import("url");
|
|
||||||
const { dirname } = await import("path");
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
const dataDir = path.join(__dirname, "..", "src", "data");
|
|
||||||
|
|
||||||
if (!fs.existsSync(dataDir)) {
|
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
|
||||||
console.log(" 📁 Diretório src/data criado");
|
|
||||||
}
|
|
||||||
|
|
||||||
const consultasPath = path.join(dataDir, "consultas-demo.json");
|
|
||||||
fs.writeFileSync(consultasPath, JSON.stringify(consultas, null, 2));
|
|
||||||
|
|
||||||
console.log(" ✅ Consultas salvas em src/data/consultas-demo.json");
|
|
||||||
console.log(` 📊 ${consultas.length} consultas criadas:`);
|
|
||||||
consultas.forEach((c, i) => {
|
|
||||||
console.log(` ${i + 1}. ${c.dataHora} - ${c.status} - ${c.tipo}`);
|
|
||||||
});
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
|
|
||||||
function mostrarResumo() {
|
|
||||||
console.log("\n✅ === CONFIGURAÇÃO CONCLUÍDA COM SUCESSO! ===\n");
|
|
||||||
console.log("📋 CREDENCIAIS DE LOGIN:\n");
|
|
||||||
console.log(" Email: guilhermesilvagomes1020@gmail.com");
|
|
||||||
console.log(" Senha: guilherme123");
|
|
||||||
console.log(" Role: user (acesso ao painel paciente)\n");
|
|
||||||
console.log("📱 DADOS DO PACIENTE:\n");
|
|
||||||
console.log(" Nome: Guilherme Silva Gomes - SQUAD 18");
|
|
||||||
console.log(" Telefone: 79999521847");
|
|
||||||
console.log(" Médico: Fernando Pirichowski - Squad 18\n");
|
|
||||||
console.log("🔗 PRÓXIMOS PASSOS:\n");
|
|
||||||
console.log(" 1. Acesse http://localhost:5173/paciente no navegador");
|
|
||||||
console.log(
|
|
||||||
" 2. Faça login com: guilhermesilvagomes1020@gmail.com / guilherme123"
|
|
||||||
);
|
|
||||||
console.log(" 3. Você verá o painel do paciente com as consultas");
|
|
||||||
console.log(" 4. As consultas também aparecem no painel do Dr. Fernando");
|
|
||||||
console.log(" 5. E no painel da secretária\n");
|
|
||||||
console.log("💡 PARA CARREGAR AS CONSULTAS NO NAVEGADOR:\n");
|
|
||||||
console.log(" - Abra o console (F12)");
|
|
||||||
console.log(
|
|
||||||
" - Execute: fetch('/src/data/consultas-demo.json').then(r=>r.json()).then(c=>{"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" localStorage.setItem('consultas_local', JSON.stringify(c));"
|
|
||||||
);
|
|
||||||
console.log(" location.reload();");
|
|
||||||
console.log(" })");
|
|
||||||
console.log();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executar
|
|
||||||
criarGuilhermeCompleto();
|
|
||||||
@ -1,220 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
async function criarJuliaComAdmin() {
|
|
||||||
try {
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("🔐 CRIANDO USUÁRIA JULIA CARVALHO");
|
|
||||||
console.log("═══════════════════════════════════════════════════\n");
|
|
||||||
|
|
||||||
// 1. Login como admin
|
|
||||||
console.log("🔑 Fazendo login como admin (riseup@popcode.com.br)...");
|
|
||||||
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: "riseup@popcode.com.br",
|
|
||||||
password: "riseup",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const adminToken = loginResponse.data.access_token;
|
|
||||||
const adminUserId = loginResponse.data.user.id;
|
|
||||||
|
|
||||||
console.log("✅ Login admin realizado com sucesso!");
|
|
||||||
console.log(` Admin ID: ${adminUserId}\n`);
|
|
||||||
|
|
||||||
// 2. Criar usuário Julia no Supabase Auth
|
|
||||||
console.log("👤 Criando usuária Julia na autenticação...");
|
|
||||||
|
|
||||||
const signupResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/signup`,
|
|
||||||
{
|
|
||||||
email: "secretaria.mediconnect@gmail.com",
|
|
||||||
password: "secretaria@mediconnect",
|
|
||||||
data: {
|
|
||||||
full_name: "Julia Carvalho",
|
|
||||||
role: "admin",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const juliaUserId = signupResponse.data.user?.id;
|
|
||||||
const juliaToken = signupResponse.data.access_token;
|
|
||||||
|
|
||||||
if (!juliaUserId) {
|
|
||||||
throw new Error("Não foi possível obter o ID da usuária criada");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Usuária criada na autenticação!");
|
|
||||||
console.log(` Julia ID: ${juliaUserId}\n`);
|
|
||||||
|
|
||||||
// 3. Criar perfil na tabela profiles usando token admin
|
|
||||||
console.log("📋 Criando perfil na tabela profiles...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles`,
|
|
||||||
{
|
|
||||||
id: juliaUserId,
|
|
||||||
email: "secretaria.mediconnect@gmail.com",
|
|
||||||
full_name: "Julia Carvalho",
|
|
||||||
is_admin: true,
|
|
||||||
is_secretary: true,
|
|
||||||
is_admin_or_manager: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("✅ Perfil criado com sucesso!\n");
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.status === 409) {
|
|
||||||
console.log("⚠️ Perfil já existe, atualizando...");
|
|
||||||
await axios.patch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles?id=eq.${juliaUserId}`,
|
|
||||||
{
|
|
||||||
full_name: "Julia Carvalho",
|
|
||||||
is_admin: true,
|
|
||||||
is_secretary: true,
|
|
||||||
is_admin_or_manager: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("✅ Perfil atualizado!\n");
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Adicionar role admin na tabela user_roles
|
|
||||||
console.log("🎭 Adicionando role admin...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/user_roles`,
|
|
||||||
{
|
|
||||||
user_id: juliaUserId,
|
|
||||||
role: "admin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("✅ Role admin adicionada!\n");
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.status === 409) {
|
|
||||||
console.log("⚠️ Role admin já existe!\n");
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Verificar se Julia consegue acessar pacientes
|
|
||||||
console.log("🏥 Testando acesso aos pacientes...");
|
|
||||||
|
|
||||||
const pacientesResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=id,full_name,email&limit=5`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${juliaToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`✅ Julia consegue acessar pacientes! (${pacientesResponse.data.length} encontrados)\n`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pacientesResponse.data.length > 0) {
|
|
||||||
console.log("📋 Pacientes acessíveis:");
|
|
||||||
pacientesResponse.data.forEach((p) => {
|
|
||||||
console.log(
|
|
||||||
` • ${p.full_name || "Sem nome"} - ${p.email || "Sem email"}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
console.log("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Resumo final
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("✅ USUÁRIA JULIA CRIADA COM SUCESSO!");
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("");
|
|
||||||
console.log("👤 Nome: Julia Carvalho");
|
|
||||||
console.log("📧 Email: secretaria.mediconnect@gmail.com");
|
|
||||||
console.log("🔑 Senha: secretaria@mediconnect");
|
|
||||||
console.log("🎭 Role: admin");
|
|
||||||
console.log("");
|
|
||||||
console.log("✨ Permissões:");
|
|
||||||
console.log(" ✅ is_admin: true");
|
|
||||||
console.log(" ✅ is_secretary: true");
|
|
||||||
console.log(" ✅ is_admin_or_manager: true");
|
|
||||||
console.log(" ✅ Acesso completo aos pacientes");
|
|
||||||
console.log("");
|
|
||||||
console.log("🌐 Faça login em:");
|
|
||||||
console.log(" http://localhost:5173/login-secretaria");
|
|
||||||
console.log("");
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"\n❌ ERRO ao criar usuária:",
|
|
||||||
error.response?.data || error.message
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error.response?.data?.code === "23505") {
|
|
||||||
console.log("\n⚠️ USUÁRIA JÁ EXISTE!");
|
|
||||||
console.log("");
|
|
||||||
console.log("Você pode fazer login com:");
|
|
||||||
console.log("📧 Email: secretaria.mediconnect@gmail.com");
|
|
||||||
console.log("🔑 Senha: secretaria@mediconnect");
|
|
||||||
console.log("🌐 URL: http://localhost:5173/login-secretaria");
|
|
||||||
} else if (error.response?.status === 422) {
|
|
||||||
console.log("\n⚠️ USUÁRIA JÁ EXISTE (email já cadastrado)");
|
|
||||||
console.log("");
|
|
||||||
console.log("Tente fazer login com:");
|
|
||||||
console.log("📧 Email: secretaria.mediconnect@gmail.com");
|
|
||||||
console.log("🔑 Senha: secretaria@mediconnect");
|
|
||||||
} else if (error.code === "ENOTFOUND") {
|
|
||||||
console.log("\n⚠️ ERRO DE CONEXÃO");
|
|
||||||
console.log("Verifique sua conexão com a internet e tente novamente.");
|
|
||||||
} else {
|
|
||||||
console.log("\n📋 Detalhes do erro:");
|
|
||||||
console.log(JSON.stringify(error.response?.data, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
criarJuliaComAdmin();
|
|
||||||
@ -1,188 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
async function criarJuliaSecretaria() {
|
|
||||||
try {
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("🔐 CRIANDO JULIA CARVALHO - ROLE SECRETARIA");
|
|
||||||
console.log("═══════════════════════════════════════════════════\n");
|
|
||||||
|
|
||||||
// Login como admin
|
|
||||||
console.log("🔑 Login admin...");
|
|
||||||
const login = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{ email: "riseup@popcode.com.br", password: "riseup" },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const adminToken = login.data.access_token;
|
|
||||||
console.log("✅ Admin logado!\n");
|
|
||||||
|
|
||||||
// Criar Julia
|
|
||||||
console.log("👤 Criando Julia...");
|
|
||||||
let juliaUserId;
|
|
||||||
let juliaToken;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const signup = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/signup`,
|
|
||||||
{
|
|
||||||
email: "secretaria.mediconnect@gmail.com",
|
|
||||||
password: "secretaria@mediconnect",
|
|
||||||
data: { full_name: "Julia Carvalho", role: "secretaria" },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
juliaUserId = signup.data.user.id;
|
|
||||||
juliaToken = signup.data.access_token;
|
|
||||||
console.log("✅ Julia criada!");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.response?.status === 422) {
|
|
||||||
console.log("⚠️ Julia já existe, fazendo login...");
|
|
||||||
const juliaLogin = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: "secretaria.mediconnect@gmail.com",
|
|
||||||
password: "secretaria@mediconnect",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
juliaUserId = juliaLogin.data.user.id;
|
|
||||||
juliaToken = juliaLogin.data.access_token;
|
|
||||||
console.log("✅ Julia já existe!");
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` ID: ${juliaUserId}\n`);
|
|
||||||
|
|
||||||
// Criar/atualizar perfil
|
|
||||||
console.log("📋 Criando perfil...");
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles`,
|
|
||||||
{
|
|
||||||
id: juliaUserId,
|
|
||||||
email: "secretaria.mediconnect@gmail.com",
|
|
||||||
full_name: "Julia Carvalho",
|
|
||||||
is_admin: false,
|
|
||||||
is_secretary: true,
|
|
||||||
is_admin_or_manager: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("✅ Perfil criado!\n");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.response?.status === 409) {
|
|
||||||
console.log("⚠️ Perfil existe, atualizando...");
|
|
||||||
await axios.patch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles?id=eq.${juliaUserId}`,
|
|
||||||
{
|
|
||||||
full_name: "Julia Carvalho",
|
|
||||||
is_admin: false,
|
|
||||||
is_secretary: true,
|
|
||||||
is_admin_or_manager: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("✅ Perfil atualizado!\n");
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"⚠️ Aviso perfil:",
|
|
||||||
err.response?.data?.message || err.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adicionar role
|
|
||||||
console.log("🎭 Adicionando role secretaria...");
|
|
||||||
try {
|
|
||||||
await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/user_roles`,
|
|
||||||
{ user_id: juliaUserId, role: "secretaria" },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("✅ Role adicionada!\n");
|
|
||||||
} catch (err) {
|
|
||||||
if (err.response?.status === 409) {
|
|
||||||
console.log("⚠️ Role já existe!\n");
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"⚠️ Aviso role:",
|
|
||||||
err.response?.data?.message || err.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Testar acesso
|
|
||||||
console.log("🏥 Testando acesso aos pacientes...");
|
|
||||||
const pacientes = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=id,full_name,email&limit=3`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${juliaToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log(`✅ Acesso OK! (${pacientes.data.length} pacientes)\n`);
|
|
||||||
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("✅ JULIA CRIADA COM SUCESSO!");
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("");
|
|
||||||
console.log("👤 Nome: Julia Carvalho");
|
|
||||||
console.log("📧 Email: secretaria.mediconnect@gmail.com");
|
|
||||||
console.log("🔑 Senha: secretaria@mediconnect");
|
|
||||||
console.log("🎭 Role: secretaria");
|
|
||||||
console.log("");
|
|
||||||
console.log("🌐 Login: http://localhost:5173/login-secretaria");
|
|
||||||
console.log("");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ ERRO:", error.response?.data || error.message);
|
|
||||||
if (error.code === "ENOTFOUND") {
|
|
||||||
console.log("\n⚠️ Problema de conexão com Supabase");
|
|
||||||
console.log("Use a página HTML: criar-julia.html");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
criarJuliaSecretaria();
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- Script SQL para criar usuária Julia Carvalho com role ADMIN
|
|
||||||
-- Execute este script no Supabase Dashboard > SQL Editor
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- 1. Criar usuário no auth.users (SUBSTITUA O ID abaixo por um UUID gerado)
|
|
||||||
-- Você pode gerar um UUID em: https://www.uuidgenerator.net/
|
|
||||||
-- Ou usar: gen_random_uuid()
|
|
||||||
|
|
||||||
INSERT INTO auth.users (
|
|
||||||
id,
|
|
||||||
instance_id,
|
|
||||||
email,
|
|
||||||
encrypted_password,
|
|
||||||
email_confirmed_at,
|
|
||||||
created_at,
|
|
||||||
updated_at,
|
|
||||||
raw_app_meta_data,
|
|
||||||
raw_user_meta_data,
|
|
||||||
role,
|
|
||||||
aud
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
gen_random_uuid(), -- Gera um UUID automaticamente
|
|
||||||
'00000000-0000-0000-0000-000000000000',
|
|
||||||
'secretaria.mediconnect@gmail.com',
|
|
||||||
crypt('secretaria@mediconnect', gen_salt('bf')), -- Hash bcrypt da senha
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
NOW(),
|
|
||||||
'{"provider": "email", "providers": ["email"]}',
|
|
||||||
'{"full_name": "Julia Carvalho", "role": "admin"}',
|
|
||||||
'authenticated',
|
|
||||||
'authenticated'
|
|
||||||
)
|
|
||||||
ON CONFLICT (email) DO NOTHING
|
|
||||||
RETURNING id;
|
|
||||||
|
|
||||||
-- 2. Obter o ID do usuário criado (copie este ID para usar nos próximos passos)
|
|
||||||
-- Execute esta query separadamente e copie o resultado:
|
|
||||||
SELECT id, email, raw_user_meta_data
|
|
||||||
FROM auth.users
|
|
||||||
WHERE email = 'secretaria.mediconnect@gmail.com';
|
|
||||||
|
|
||||||
-- 3. Criar perfil na tabela users (SUBSTITUA 'UUID_AQUI' pelo ID obtido acima)
|
|
||||||
INSERT INTO public.users (
|
|
||||||
id,
|
|
||||||
email,
|
|
||||||
full_name,
|
|
||||||
is_admin,
|
|
||||||
is_secretary,
|
|
||||||
is_admin_or_manager,
|
|
||||||
created_at,
|
|
||||||
updated_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'secretaria.mediconnect@gmail.com'),
|
|
||||||
'secretaria.mediconnect@gmail.com',
|
|
||||||
'Julia Carvalho',
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
NOW(),
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
|
||||||
is_admin = true,
|
|
||||||
is_secretary = true,
|
|
||||||
is_admin_or_manager = true;
|
|
||||||
|
|
||||||
-- 4. Adicionar role admin na tabela user_roles
|
|
||||||
INSERT INTO public.user_roles (
|
|
||||||
user_id,
|
|
||||||
role,
|
|
||||||
created_at
|
|
||||||
)
|
|
||||||
VALUES (
|
|
||||||
(SELECT id FROM auth.users WHERE email = 'secretaria.mediconnect@gmail.com'),
|
|
||||||
'admin',
|
|
||||||
NOW()
|
|
||||||
)
|
|
||||||
ON CONFLICT (user_id, role) DO NOTHING;
|
|
||||||
|
|
||||||
-- 5. Verificar criação
|
|
||||||
SELECT
|
|
||||||
u.id,
|
|
||||||
u.email,
|
|
||||||
u.full_name,
|
|
||||||
u.is_admin,
|
|
||||||
u.is_secretary,
|
|
||||||
ur.role
|
|
||||||
FROM public.users u
|
|
||||||
LEFT JOIN public.user_roles ur ON ur.user_id = u.id
|
|
||||||
WHERE u.email = 'secretaria.mediconnect@gmail.com';
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- CREDENCIAIS PARA LOGIN:
|
|
||||||
-- Email: secretaria.mediconnect@gmail.com
|
|
||||||
-- Senha: secretaria@mediconnect
|
|
||||||
-- ============================================================
|
|
||||||
@ -1,260 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script para criar médico Fernando Pirichowski - Squad 18
|
|
||||||
* Cria usuário auth + registro na tabela doctors + atualiza profile com role
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Credenciais do admin para operações autenticadas
|
|
||||||
const ADMIN_EMAIL = "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD = "riseup";
|
|
||||||
|
|
||||||
// Dados do médico Fernando
|
|
||||||
const FERNANDO_EMAIL = "fernando.pirichowski@souunit.com.br";
|
|
||||||
const FERNANDO_PASSWORD = "fernando";
|
|
||||||
const FERNANDO_NOME = "Fernando Pirichowski - Squad 18";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log("🔐 Fazendo login como admin...\n");
|
|
||||||
|
|
||||||
// 1. Login do admin
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: ADMIN_EMAIL,
|
|
||||||
password: ADMIN_PASSWORD,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const adminToken = loginResponse.data.access_token;
|
|
||||||
console.log("✅ Login admin realizado com sucesso!\n");
|
|
||||||
|
|
||||||
// 2. Verificar se usuário já existe tentando fazer login primeiro
|
|
||||||
console.log("👤 Verificando se usuário Fernando já existe...\n");
|
|
||||||
|
|
||||||
let fernandoUserId;
|
|
||||||
let fernandoToken;
|
|
||||||
let usuarioJaExiste = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Tentar login primeiro
|
|
||||||
const loginFernando = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: FERNANDO_EMAIL,
|
|
||||||
password: FERNANDO_PASSWORD,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
fernandoUserId = loginFernando.data.user.id;
|
|
||||||
fernandoToken = loginFernando.data.access_token;
|
|
||||||
usuarioJaExiste = true;
|
|
||||||
console.log("✅ Usuário já existe! Login realizado com sucesso.");
|
|
||||||
console.log(` User ID: ${fernandoUserId}\n`);
|
|
||||||
} catch (loginError) {
|
|
||||||
// Se login falhar, tentar criar
|
|
||||||
console.log(" Usuário não existe, criando novo...\n");
|
|
||||||
try {
|
|
||||||
const signupResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/signup`,
|
|
||||||
{
|
|
||||||
email: FERNANDO_EMAIL,
|
|
||||||
password: FERNANDO_PASSWORD,
|
|
||||||
data: {
|
|
||||||
full_name: FERNANDO_NOME,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
fernandoUserId = signupResponse.data.user?.id || signupResponse.data.id;
|
|
||||||
fernandoToken =
|
|
||||||
signupResponse.data.access_token ||
|
|
||||||
signupResponse.data.session?.access_token;
|
|
||||||
|
|
||||||
if (!fernandoUserId) {
|
|
||||||
throw new Error("Não foi possível obter o User ID do signup");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Usuário criado com sucesso!");
|
|
||||||
console.log(` User ID: ${fernandoUserId}`);
|
|
||||||
console.log(` Email: ${FERNANDO_EMAIL}\n`);
|
|
||||||
} catch (signupError) {
|
|
||||||
throw signupError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Criar registro na tabela doctors
|
|
||||||
console.log("🏥 Criando registro na tabela doctors...\n");
|
|
||||||
|
|
||||||
const doctorData = {
|
|
||||||
user_id: fernandoUserId,
|
|
||||||
full_name: FERNANDO_NOME,
|
|
||||||
email: FERNANDO_EMAIL,
|
|
||||||
cpf: "12345678901", // CPF válido para teste
|
|
||||||
crm: "SQUAD18",
|
|
||||||
crm_uf: "SE",
|
|
||||||
specialty: "Clínico Geral",
|
|
||||||
phone_mobile: "79999999999",
|
|
||||||
active: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
let doctorId;
|
|
||||||
try {
|
|
||||||
const doctorResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/doctors`,
|
|
||||||
doctorData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
doctorId = Array.isArray(doctorResponse.data)
|
|
||||||
? doctorResponse.data[0].id
|
|
||||||
: doctorResponse.data.id;
|
|
||||||
|
|
||||||
console.log("✅ Médico cadastrado na tabela doctors!");
|
|
||||||
console.log(` Doctor ID: ${doctorId}`);
|
|
||||||
console.log(` Nome: ${FERNANDO_NOME}`);
|
|
||||||
console.log(` CRM: ${doctorData.crm}-${doctorData.crm_uf}\n`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.data?.message?.includes("duplicate key")) {
|
|
||||||
console.log("⚠️ Registro de médico já existe na tabela doctors\n");
|
|
||||||
// Buscar o ID do médico existente
|
|
||||||
const existingDoctor = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/doctors?email=eq.${FERNANDO_EMAIL}&select=id`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (existingDoctor.data.length > 0) {
|
|
||||||
doctorId = existingDoctor.data[0].id;
|
|
||||||
console.log(` Doctor ID existente: ${doctorId}\n`);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Atualizar profile com role 'medico'
|
|
||||||
console.log('🔧 Atualizando profile com role "medico"...\n');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.patch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles?id=eq.${fernandoUserId}`,
|
|
||||||
{
|
|
||||||
role: "medico",
|
|
||||||
full_name: FERNANDO_NOME,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log('✅ Profile atualizado com role "medico"!\n');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
"⚠️ Erro ao atualizar profile:",
|
|
||||||
error.response?.data?.message || error.message
|
|
||||||
);
|
|
||||||
console.log(" (Profile pode ter sido criado automaticamente)\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Verificar criação
|
|
||||||
console.log("🔍 VERIFICANDO CADASTRO COMPLETO:\n");
|
|
||||||
|
|
||||||
const verificarDoctor = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/doctors?user_id=eq.${fernandoUserId}&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const verificarProfile = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles?id=eq.${fernandoUserId}&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ MÉDICO FERNANDO CRIADO COM SUCESSO!\n");
|
|
||||||
console.log("📋 Detalhes do cadastro:\n");
|
|
||||||
console.log("Auth User:");
|
|
||||||
console.log(` - ID: ${fernandoUserId}`);
|
|
||||||
console.log(` - Email: ${FERNANDO_EMAIL}`);
|
|
||||||
console.log(` - Senha: ${FERNANDO_PASSWORD}\n`);
|
|
||||||
|
|
||||||
if (verificarDoctor.data.length > 0) {
|
|
||||||
console.log("Tabela Doctors:");
|
|
||||||
console.log(` - ID: ${verificarDoctor.data[0].id}`);
|
|
||||||
console.log(` - Nome: ${verificarDoctor.data[0].full_name}`);
|
|
||||||
console.log(
|
|
||||||
` - CRM: ${verificarDoctor.data[0].crm}-${verificarDoctor.data[0].crm_uf}`
|
|
||||||
);
|
|
||||||
console.log(` - Especialidade: ${verificarDoctor.data[0].specialty}`);
|
|
||||||
console.log(
|
|
||||||
` - Ativo: ${verificarDoctor.data[0].active ? "Sim" : "Não"}\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verificarProfile.data.length > 0) {
|
|
||||||
console.log("Tabela Profiles:");
|
|
||||||
console.log(` - User ID: ${verificarProfile.data[0].id}`);
|
|
||||||
console.log(` - Nome: ${verificarProfile.data[0].full_name}`);
|
|
||||||
console.log(
|
|
||||||
` - Role: ${verificarProfile.data[0].role || "não definida"}\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎉 Agora você pode fazer login com:");
|
|
||||||
console.log(` Email: ${FERNANDO_EMAIL}`);
|
|
||||||
console.log(` Senha: ${FERNANDO_PASSWORD}\n`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.response?.data || error.message);
|
|
||||||
if (error.response) {
|
|
||||||
console.error("Status:", error.response.status);
|
|
||||||
console.error("Data:", JSON.stringify(error.response.data, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
console.log("\n👩💼 CRIAR USUÁRIO SECRETÁRIA\n");
|
|
||||||
|
|
||||||
async function criarSecretaria() {
|
|
||||||
// Dados da secretária
|
|
||||||
const secretariaData = {
|
|
||||||
email: "secretaria@mediconnect.com",
|
|
||||||
password: "secretaria123",
|
|
||||||
nome: "Maria Secretária",
|
|
||||||
telefone: "79999998888",
|
|
||||||
cpf: "11111111111",
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("📝 Criando secretária...");
|
|
||||||
console.log(` Email: ${secretariaData.email}`);
|
|
||||||
console.log(` Senha: ${secretariaData.password}`);
|
|
||||||
console.log(` Nome: ${secretariaData.nome}\n`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// PASSO 1: Criar usuário no auth
|
|
||||||
console.log("🔐 Criando usuário de autenticação...\n");
|
|
||||||
|
|
||||||
const signupResponse = await fetch(`${SUPABASE_URL}/auth/v1/signup`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: secretariaData.email,
|
|
||||||
password: secretariaData.password,
|
|
||||||
data: {
|
|
||||||
full_name: secretariaData.nome,
|
|
||||||
phone: secretariaData.telefone,
|
|
||||||
cpf: secretariaData.cpf,
|
|
||||||
role: "secretaria",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!signupResponse.ok) {
|
|
||||||
const error = await signupResponse.text();
|
|
||||||
console.log("❌ Erro ao criar usuário:", signupResponse.status);
|
|
||||||
console.log(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signupData = await signupResponse.json();
|
|
||||||
const userId = signupData.user?.id;
|
|
||||||
const accessToken = signupData.access_token;
|
|
||||||
|
|
||||||
console.log("✅ Usuário criado com sucesso!");
|
|
||||||
console.log(` User ID: ${userId}`);
|
|
||||||
console.log(` Token: ${accessToken?.substring(0, 50)}...\n`);
|
|
||||||
|
|
||||||
// PASSO 2: Criar perfil na tabela profiles (se existir)
|
|
||||||
console.log("📋 Criando perfil...\n");
|
|
||||||
|
|
||||||
const profileResponse = await fetch(`${SUPABASE_URL}/rest/v1/profiles`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
id: userId,
|
|
||||||
full_name: secretariaData.nome,
|
|
||||||
email: secretariaData.email,
|
|
||||||
phone: secretariaData.telefone,
|
|
||||||
role: "secretaria",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (profileResponse.ok || profileResponse.status === 201) {
|
|
||||||
console.log("✅ Perfil criado com sucesso!\n");
|
|
||||||
} else if (profileResponse.status === 409) {
|
|
||||||
console.log("⚠️ Perfil já existe (isso é normal)\n");
|
|
||||||
} else {
|
|
||||||
const error = await profileResponse.text();
|
|
||||||
console.log("⚠️ Aviso ao criar perfil:", profileResponse.status);
|
|
||||||
console.log(error);
|
|
||||||
console.log(
|
|
||||||
"(Isso pode ser normal se a tabela profiles não existir ou tiver trigger)\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// PASSO 3: Verificar se foi criado
|
|
||||||
console.log("📊 RESUMO:\n");
|
|
||||||
console.log("✅ Secretária criada com sucesso!");
|
|
||||||
console.log("\n📝 Credenciais para login:");
|
|
||||||
console.log(` Email: ${secretariaData.email}`);
|
|
||||||
console.log(` Senha: ${secretariaData.password}`);
|
|
||||||
console.log("\n🔗 Acesse: http://localhost:5173/secretaria");
|
|
||||||
console.log("\n");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
criarSecretaria();
|
|
||||||
@ -1,266 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script para criar usuário com role "user" para o paciente Guilherme
|
|
||||||
* e configurar consultas de demonstração
|
|
||||||
*/
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Admin credentials
|
|
||||||
const ADMIN_EMAIL = "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD = "riseup";
|
|
||||||
|
|
||||||
// Guilherme dados
|
|
||||||
const GUILHERME_ID = "864b1785-461f-4e92-8b74-2a6f17c58a80";
|
|
||||||
const GUILHERME_EMAIL = "guilherme@paciente.com";
|
|
||||||
const GUILHERME_PASSWORD = "guilherme123";
|
|
||||||
const GUILHERME_NOME = "Guilherme Silva Gomes - SQUAD 18";
|
|
||||||
|
|
||||||
// Fernando dados
|
|
||||||
const FERNANDO_USER_ID = "be1e3cba-534e-48c3-9590-b7e55861cade";
|
|
||||||
const FERNANDO_NOME = "Fernando Pirichowski - Squad 18";
|
|
||||||
|
|
||||||
async function criarUsuarioGuilherme() {
|
|
||||||
try {
|
|
||||||
console.log("\n🔐 === CRIAR USUÁRIO GUILHERME COM ROLE USER ===\n");
|
|
||||||
|
|
||||||
// 1. Login como admin
|
|
||||||
console.log("1️⃣ Fazendo login como admin...");
|
|
||||||
const loginResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: ADMIN_EMAIL,
|
|
||||||
password: ADMIN_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
|
||||||
throw new Error(`Erro no login: ${loginResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
const adminToken = loginData.access_token;
|
|
||||||
console.log("✅ Login admin realizado com sucesso!\n");
|
|
||||||
|
|
||||||
// 2. Verificar se usuário Guilherme já existe
|
|
||||||
console.log("2️⃣ Verificando se usuário Guilherme já existe...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const loginGuilherme = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: GUILHERME_EMAIL,
|
|
||||||
password: GUILHERME_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (loginGuilherme.ok) {
|
|
||||||
const guilhermeData = await loginGuilherme.json();
|
|
||||||
console.log("✅ Usuário Guilherme já existe!");
|
|
||||||
console.log(` User ID: ${guilhermeData.user.id}`);
|
|
||||||
console.log(` Email: ${guilhermeData.user.email}\n`);
|
|
||||||
return guilhermeData.user.id;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("ℹ️ Usuário não existe, criando...\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Criar usuário Guilherme via Edge Function
|
|
||||||
console.log("3️⃣ Criando usuário Guilherme...");
|
|
||||||
console.log(` Email: ${GUILHERME_EMAIL}`);
|
|
||||||
console.log(` Senha: ${GUILHERME_PASSWORD}`);
|
|
||||||
console.log(` Role: user\n`);
|
|
||||||
|
|
||||||
const createUserResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/functions/v1/create-user`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: GUILHERME_EMAIL,
|
|
||||||
password: GUILHERME_PASSWORD,
|
|
||||||
full_name: GUILHERME_NOME,
|
|
||||||
role: "user", // Role "user" para paciente
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const createUserData = await createUserResponse.json();
|
|
||||||
|
|
||||||
if (!createUserResponse.ok) {
|
|
||||||
console.error("❌ Erro ao criar usuário:", createUserData);
|
|
||||||
throw new Error(JSON.stringify(createUserData));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
" Resposta da criação:",
|
|
||||||
JSON.stringify(createUserData, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
const guilhermeUserId =
|
|
||||||
createUserData.user_id || createUserData.id || createUserData.userId;
|
|
||||||
|
|
||||||
if (!guilhermeUserId) {
|
|
||||||
console.error("❌ User ID não encontrado na resposta!");
|
|
||||||
console.error(" Resposta completa:", createUserData);
|
|
||||||
throw new Error("User ID não retornado pela API");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Usuário criado com sucesso!");
|
|
||||||
console.log(` User ID: ${guilhermeUserId}\n`);
|
|
||||||
|
|
||||||
// 4. Atribuir paciente ao usuário
|
|
||||||
console.log("4️⃣ Atribuindo paciente ao usuário...");
|
|
||||||
|
|
||||||
// Verificar se atribuição já existe
|
|
||||||
const checkAssignment = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments?user_id=eq.${guilhermeUserId}&patient_id=eq.${GUILHERME_ID}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const existingAssignments = await checkAssignment.json();
|
|
||||||
|
|
||||||
if (existingAssignments.length > 0) {
|
|
||||||
console.log("✅ Atribuição já existe!\n");
|
|
||||||
} else {
|
|
||||||
const assignResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
user_id: guilhermeUserId,
|
|
||||||
patient_id: GUILHERME_ID,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!assignResponse.ok) {
|
|
||||||
const error = await assignResponse.text();
|
|
||||||
console.error("❌ Erro ao criar atribuição:", error);
|
|
||||||
} else {
|
|
||||||
console.log("✅ Paciente atribuído ao usuário!\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Criar consultas de demonstração
|
|
||||||
console.log("5️⃣ Criando consultas de demonstração...\n");
|
|
||||||
await criarConsultasDemo();
|
|
||||||
|
|
||||||
console.log("\n✅ === CONFIGURAÇÃO CONCLUÍDA COM SUCESSO! ===\n");
|
|
||||||
console.log("📋 INFORMAÇÕES PARA LOGIN:\n");
|
|
||||||
console.log(" Email: guilherme@paciente.com");
|
|
||||||
console.log(" Senha: guilherme123");
|
|
||||||
console.log(" Role: user (acesso ao painel paciente)\n");
|
|
||||||
console.log("🔗 Próximos passos:");
|
|
||||||
console.log(" 1. Acesse /paciente no navegador");
|
|
||||||
console.log(" 2. Faça login com as credenciais acima");
|
|
||||||
console.log(" 3. Você verá as consultas no painel do paciente");
|
|
||||||
console.log(
|
|
||||||
" 4. As consultas também aparecerão no painel do médico Fernando"
|
|
||||||
);
|
|
||||||
console.log(" 5. E no painel da secretária\n");
|
|
||||||
|
|
||||||
return guilhermeUserId;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ Erro:", error.message);
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function criarConsultasDemo() {
|
|
||||||
const fs = await import("fs");
|
|
||||||
const path = await import("path");
|
|
||||||
|
|
||||||
// Criar arquivo de consultas locais para demonstração
|
|
||||||
const consultas = [
|
|
||||||
{
|
|
||||||
id: "consulta-demo-001",
|
|
||||||
pacienteId: GUILHERME_ID,
|
|
||||||
medicoId: FERNANDO_USER_ID,
|
|
||||||
pacienteNome: GUILHERME_NOME,
|
|
||||||
medicoNome: FERNANDO_NOME,
|
|
||||||
dataHora: "2025-10-05T10:00:00",
|
|
||||||
status: "agendada",
|
|
||||||
tipo: "Consulta",
|
|
||||||
observacoes: "Primeira consulta - Check-up geral",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "consulta-demo-002",
|
|
||||||
pacienteId: GUILHERME_ID,
|
|
||||||
medicoId: FERNANDO_USER_ID,
|
|
||||||
pacienteNome: GUILHERME_NOME,
|
|
||||||
medicoNome: FERNANDO_NOME,
|
|
||||||
dataHora: "2025-09-28T14:30:00",
|
|
||||||
status: "realizada",
|
|
||||||
tipo: "Retorno",
|
|
||||||
observacoes: "Consulta de retorno - Avaliação de exames",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "consulta-demo-003",
|
|
||||||
pacienteId: GUILHERME_ID,
|
|
||||||
medicoId: FERNANDO_USER_ID,
|
|
||||||
pacienteNome: GUILHERME_NOME,
|
|
||||||
medicoNome: FERNANDO_NOME,
|
|
||||||
dataHora: "2025-10-10T09:00:00",
|
|
||||||
status: "confirmada",
|
|
||||||
tipo: "Consulta",
|
|
||||||
observacoes: "Consulta de acompanhamento mensal",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Caminho para a pasta src/data
|
|
||||||
const dataDir = path.join(process.cwd(), "src", "data");
|
|
||||||
|
|
||||||
// Criar diretório se não existir
|
|
||||||
if (!fs.existsSync(dataDir)) {
|
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
|
||||||
console.log(" 📁 Diretório src/data criado");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Salvar consultas
|
|
||||||
const consultasPath = path.join(dataDir, "consultas-demo.json");
|
|
||||||
fs.writeFileSync(consultasPath, JSON.stringify(consultas, null, 2));
|
|
||||||
console.log(" ✅ Consultas salvas em src/data/consultas-demo.json");
|
|
||||||
console.log(` 📊 ${consultas.length} consultas criadas\n`);
|
|
||||||
|
|
||||||
// Também salvar no localStorage (simulado)
|
|
||||||
console.log(" 💡 Para usar as consultas:");
|
|
||||||
console.log(" - Importe de src/data/consultas-demo.json");
|
|
||||||
console.log(
|
|
||||||
" - Ou use localStorage.setItem('consultas_local', JSON.stringify(consultas))"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executar
|
|
||||||
criarUsuarioGuilherme();
|
|
||||||
@ -1,158 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://rjzjnbzjsdxgidxvmsmx.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InJqempuYnpqc2R4Z2lkeHZtc214Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDUwNzIyNzYsImV4cCI6MjA2MDY0ODI3Nn0.S6xtAkEZZq5W2qjSFu9xoTQCrJ8VJpIoRiDn65gvZNM";
|
|
||||||
|
|
||||||
async function criarUsuarioJulia() {
|
|
||||||
try {
|
|
||||||
console.log("📝 Criando usuária Julia Carvalho...\n");
|
|
||||||
|
|
||||||
// 1. Criar usuário no Supabase Auth
|
|
||||||
console.log("🔐 Criando usuário na autenticação...");
|
|
||||||
|
|
||||||
const signupResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/signup`,
|
|
||||||
{
|
|
||||||
email: "secretaria.mediconnect@gmail.com",
|
|
||||||
password: "secretaria@mediconnect",
|
|
||||||
data: {
|
|
||||||
full_name: "Julia Carvalho",
|
|
||||||
role: "admin",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const userId = signupResponse.data.user?.id;
|
|
||||||
const accessToken = signupResponse.data.access_token;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error("Não foi possível obter o ID do usuário criado");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`✅ Usuário criado com sucesso!`);
|
|
||||||
console.log(` ID: ${userId}`);
|
|
||||||
console.log(` Email: secretaria.mediconnect@gmail.com\n`);
|
|
||||||
|
|
||||||
// 2. Criar perfil do usuário na tabela users
|
|
||||||
console.log("👤 Criando perfil na tabela users...");
|
|
||||||
|
|
||||||
const userResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/users`,
|
|
||||||
{
|
|
||||||
id: userId,
|
|
||||||
email: "secretaria.mediconnect@gmail.com",
|
|
||||||
full_name: "Julia Carvalho",
|
|
||||||
is_admin: true,
|
|
||||||
is_secretary: true,
|
|
||||||
is_admin_or_manager: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ Perfil criado com sucesso!\n");
|
|
||||||
|
|
||||||
// 3. Adicionar role na tabela user_roles
|
|
||||||
console.log("🎭 Adicionando role admin...");
|
|
||||||
|
|
||||||
const roleResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/user_roles`,
|
|
||||||
{
|
|
||||||
user_id: userId,
|
|
||||||
role: "admin",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ Role admin adicionada com sucesso!\n");
|
|
||||||
|
|
||||||
// 4. Testar login
|
|
||||||
console.log("🔑 Testando login...");
|
|
||||||
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: "secretaria.mediconnect@gmail.com",
|
|
||||||
password: "secretaria@mediconnect",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ Login realizado com sucesso!\n");
|
|
||||||
|
|
||||||
// 5. Verificar permissões de acesso aos pacientes
|
|
||||||
console.log("🏥 Verificando acesso aos pacientes...");
|
|
||||||
|
|
||||||
const pacientesResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=id,full_name,email&limit=5`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${loginResponse.data.access_token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`✅ Acesso aos pacientes OK! (${pacientesResponse.data.length} pacientes encontrados)\n`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pacientesResponse.data.length > 0) {
|
|
||||||
console.log("📋 Primeiros pacientes:");
|
|
||||||
pacientesResponse.data.forEach((p) => {
|
|
||||||
console.log(` • ${p.full_name} - ${p.email}`);
|
|
||||||
});
|
|
||||||
console.log("");
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("✅ USUÁRIA JULIA CARVALHO CRIADA COM SUCESSO!");
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("");
|
|
||||||
console.log("📧 Email: secretaria.mediconnect@gmail.com");
|
|
||||||
console.log("🔑 Senha: secretaria@mediconnect");
|
|
||||||
console.log("👤 Nome: Julia Carvalho");
|
|
||||||
console.log("🎭 Role: admin (permissões completas)");
|
|
||||||
console.log("");
|
|
||||||
console.log("🌐 Login em: http://localhost:5173/login-secretaria");
|
|
||||||
console.log("");
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"❌ Erro ao criar usuária:",
|
|
||||||
error.response?.data || error.message
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error.response?.data?.code === "23505") {
|
|
||||||
console.log("\n⚠️ Usuária já existe! Tente fazer login com:");
|
|
||||||
console.log(" Email: secretaria.mediconnect@gmail.com");
|
|
||||||
console.log(" Senha: secretaria@mediconnect");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
criarUsuarioJulia();
|
|
||||||
@ -1,159 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Credenciais de admin
|
|
||||||
const ADMIN_EMAIL = "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD = "riseup";
|
|
||||||
|
|
||||||
console.log("\n🗑️ DELETAR USUÁRIOS DE TESTE COM ADMIN\n");
|
|
||||||
|
|
||||||
async function deletarUsuariosTeste() {
|
|
||||||
// PASSO 1: Fazer login como admin
|
|
||||||
console.log("🔐 Fazendo login como admin...\n");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const loginResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: ADMIN_EMAIL,
|
|
||||||
password: ADMIN_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
|
||||||
console.log("❌ Login falhou:", loginResponse.status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
const adminToken = loginData.access_token;
|
|
||||||
console.log("✅ Login admin bem-sucedido!\n");
|
|
||||||
|
|
||||||
// PASSO 2: Buscar pacientes de teste
|
|
||||||
console.log('📋 Buscando pacientes de teste (email contém "teste")...\n');
|
|
||||||
|
|
||||||
const pacientesResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?email=ilike.*teste*&select=id,full_name,email`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!pacientesResponse.ok) {
|
|
||||||
console.log("❌ Erro ao buscar pacientes:", pacientesResponse.status);
|
|
||||||
const error = await pacientesResponse.text();
|
|
||||||
console.log(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pacientes = await pacientesResponse.json();
|
|
||||||
console.log(`Encontrados ${pacientes.length} paciente(s) de teste:\n`);
|
|
||||||
|
|
||||||
if (pacientes.length > 0) {
|
|
||||||
pacientes.forEach((p, index) => {
|
|
||||||
console.log(`${index + 1}. ${p.full_name || "Sem nome"}`);
|
|
||||||
console.log(` Email: ${p.email}`);
|
|
||||||
console.log(` ID: ${p.id}\n`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// PASSO 3: Deletar pacientes de teste
|
|
||||||
console.log("🗑️ Deletando pacientes de teste...\n");
|
|
||||||
|
|
||||||
for (const paciente of pacientes) {
|
|
||||||
try {
|
|
||||||
const deleteResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?id=eq.${paciente.id}`,
|
|
||||||
{
|
|
||||||
method: "DELETE",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (deleteResponse.ok || deleteResponse.status === 204) {
|
|
||||||
console.log(`✅ Deletado: ${paciente.email}`);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`❌ Erro ao deletar ${paciente.email}:`,
|
|
||||||
deleteResponse.status
|
|
||||||
);
|
|
||||||
const error = await deleteResponse.text();
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ Erro ao deletar ${paciente.email}:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("✅ Nenhum paciente de teste encontrado!\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// PASSO 4: Tentar deletar usuários de auth (pode não funcionar sem service_role)
|
|
||||||
console.log("\n📋 Tentando deletar usuários do auth.users...\n");
|
|
||||||
console.log(
|
|
||||||
"⚠️ NOTA: A API pública normalmente NÃO permite deletar usuários."
|
|
||||||
);
|
|
||||||
console.log(" Isso requer service_role key ou acesso ao Dashboard.\n");
|
|
||||||
|
|
||||||
const emailsParaDeletar = [
|
|
||||||
"testefinal@gmail.com",
|
|
||||||
"teste1759356178698@gmail.com",
|
|
||||||
"pacienteteste",
|
|
||||||
];
|
|
||||||
|
|
||||||
console.log("Emails que deveriam ser deletados manualmente no Dashboard:");
|
|
||||||
emailsParaDeletar.forEach((email) => {
|
|
||||||
console.log(` - ${email}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("\n💡 Para deletar usuários do auth.users:");
|
|
||||||
console.log(
|
|
||||||
" 1. Acesse: https://app.supabase.com/project/yuanqfswhberkoevtmfr/auth/users"
|
|
||||||
);
|
|
||||||
console.log(" 2. Busque pelos emails acima");
|
|
||||||
console.log(" 3. Clique nos 3 pontos → Delete user\n");
|
|
||||||
|
|
||||||
// Verificar resultado
|
|
||||||
console.log("\n📊 VERIFICANDO RESULTADO...\n");
|
|
||||||
|
|
||||||
const verificarResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?email=ilike.*teste*&select=count`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${adminToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "count=exact",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (verificarResponse.ok) {
|
|
||||||
const countHeader = verificarResponse.headers.get("content-range");
|
|
||||||
console.log(`✅ Pacientes de teste restantes: ${countHeader || "0"}\n`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deletarUsuariosTeste();
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
console.log("\n🗑️ DELETAR USUÁRIOS DE TESTE\n");
|
|
||||||
console.log(
|
|
||||||
"❌ ATENÇÃO: A API pública do Supabase não permite deletar usuários!"
|
|
||||||
);
|
|
||||||
console.log("");
|
|
||||||
console.log("Para deletar usuários de teste, você precisa:");
|
|
||||||
console.log("");
|
|
||||||
console.log("1️⃣ Acessar o Dashboard do Supabase:");
|
|
||||||
console.log(
|
|
||||||
" https://app.supabase.com/project/yuanqfswhberkoevtmfr/auth/users"
|
|
||||||
);
|
|
||||||
console.log("");
|
|
||||||
console.log('2️⃣ Na aba "Authentication" → "Users"');
|
|
||||||
console.log("");
|
|
||||||
console.log("3️⃣ Buscar pelos usuários de teste e deletar manualmente:");
|
|
||||||
console.log(' - Emails com "pacienteteste" ou "teste"');
|
|
||||||
console.log(" - testefinal@gmail.com");
|
|
||||||
console.log(" - teste1759356178698@gmail.com");
|
|
||||||
console.log("");
|
|
||||||
console.log("📋 Listando usuários de teste nos registros de pacientes...\n");
|
|
||||||
|
|
||||||
async function listarPacientesTeste() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=id,full_name,email&email=ilike.*teste*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const pacientes = await response.json();
|
|
||||||
|
|
||||||
if (pacientes.length === 0) {
|
|
||||||
console.log(
|
|
||||||
"✅ Nenhum paciente de teste encontrado na tabela patients\n"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
`📊 ${pacientes.length} paciente(s) de teste encontrado(s):\n`
|
|
||||||
);
|
|
||||||
pacientes.forEach((p, index) => {
|
|
||||||
console.log(`${index + 1}. ${p.full_name || "Sem nome"}`);
|
|
||||||
console.log(` Email: ${p.email}`);
|
|
||||||
console.log(` ID: ${p.id}\n`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"ℹ️ Para deletar esses registros de pacientes, você pode:"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
' - Deletar via Dashboard do Supabase na tabela "patients"'
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" - Ou criar um Edge Function com permissões de service_role\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("❌ Erro ao listar pacientes:", response.status);
|
|
||||||
const error = await response.text();
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listarPacientesTeste();
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
// Script diagnóstico para testar login Supabase password grant
|
|
||||||
// Executar com: npx ts-node scripts/diagnose-login.ts (ou adicionar script no package.json)
|
|
||||||
|
|
||||||
// Node 18+ possui fetch nativo; sem dependência externa
|
|
||||||
// Declaração mínima para evitar erro de tipos sem adicionar @types/node
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
declare const process: any | undefined;
|
|
||||||
|
|
||||||
const SUPABASE_URL =
|
|
||||||
(typeof process !== "undefined" && process.env.VITE_SUPABASE_URL) ||
|
|
||||||
"https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const ANON_KEY =
|
|
||||||
(typeof process !== "undefined" && process.env.VITE_SUPABASE_ANON_KEY) ||
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Credenciais admin de desenvolvimento (fornecidas)
|
|
||||||
const EMAIL =
|
|
||||||
(typeof process !== "undefined" && process.env.TEST_ADMIN_EMAIL) ||
|
|
||||||
"riseup@popcode.com.br";
|
|
||||||
const PASSWORD =
|
|
||||||
(typeof process !== "undefined" && process.env.TEST_ADMIN_PASSWORD) ||
|
|
||||||
"riseup";
|
|
||||||
|
|
||||||
async function attemptLogin() {
|
|
||||||
const url = `${SUPABASE_URL}/auth/v1/token?grant_type=password`;
|
|
||||||
const body = { email: EMAIL, password: PASSWORD };
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: ANON_KEY,
|
|
||||||
Authorization: `Bearer ${ANON_KEY}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
const text = await res.text();
|
|
||||||
let parsed: unknown = null;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(text);
|
|
||||||
} catch {
|
|
||||||
/* plain text */
|
|
||||||
}
|
|
||||||
console.log("STATUS", res.status);
|
|
||||||
console.log("RAW", text);
|
|
||||||
if (
|
|
||||||
res.ok &&
|
|
||||||
typeof parsed === "object" &&
|
|
||||||
parsed &&
|
|
||||||
"access_token" in parsed
|
|
||||||
) {
|
|
||||||
const token = (parsed as { access_token: string }).access_token;
|
|
||||||
console.log("LOGIN OK: access_token prefix", token.slice(0, 20));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Erro comum: user not confirmed / invalid login
|
|
||||||
if (parsed && typeof parsed === "object") {
|
|
||||||
const p = parsed as Record<string, unknown>;
|
|
||||||
if (p.error) console.log("ERROR CODE:", p.error);
|
|
||||||
if (p.msg) console.log("MSG:", p.msg);
|
|
||||||
}
|
|
||||||
if (/email/i.test(text) && /confirm/i.test(text)) {
|
|
||||||
console.log(
|
|
||||||
"Possível conta não confirmada. Verifique no painel Supabase se o email foi confirmado."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Falha inesperada:", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const ok = await attemptLogin();
|
|
||||||
if (!ok && typeof process !== "undefined") process.exit(1);
|
|
||||||
})();
|
|
||||||
@ -1,82 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
console.log("\n🔍 DIAGNOSTICANDO PROBLEMAS DE LISTAGEM\n");
|
|
||||||
|
|
||||||
async function testarEndpoint(nome, url) {
|
|
||||||
console.log(`\n📋 Testando ${nome}: ${url}`);
|
|
||||||
console.log("─".repeat(60));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Status: ${response.status} ${response.statusText}`);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
const count = Array.isArray(data) ? data.length : "Não é array";
|
|
||||||
console.log(`✅ SUCESSO - Registros: ${count}`);
|
|
||||||
|
|
||||||
if (Array.isArray(data) && data.length > 0) {
|
|
||||||
console.log("\n📄 Primeiro registro:");
|
|
||||||
console.log(JSON.stringify(data[0], null, 2));
|
|
||||||
} else if (Array.isArray(data)) {
|
|
||||||
console.log("⚠️ Array vazio - tabela não tem registros");
|
|
||||||
} else {
|
|
||||||
console.log("📄 Resposta:");
|
|
||||||
console.log(JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("❌ ERRO");
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.log(errorText);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("❌ ERRO DE CONEXÃO");
|
|
||||||
console.error(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function diagnosticar() {
|
|
||||||
// Testar pacientes
|
|
||||||
await testarEndpoint("PATIENTS", `${SUPABASE_URL}/rest/v1/patients?select=*`);
|
|
||||||
await testarEndpoint(
|
|
||||||
"PACIENTES (alternativa)",
|
|
||||||
`${SUPABASE_URL}/rest/v1/pacientes?select=*`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Testar médicos
|
|
||||||
await testarEndpoint("DOCTORS", `${SUPABASE_URL}/rest/v1/doctors?select=*`);
|
|
||||||
await testarEndpoint(
|
|
||||||
"MEDICOS (alternativa)",
|
|
||||||
`${SUPABASE_URL}/rest/v1/medicos?select=*`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Testar profiles
|
|
||||||
await testarEndpoint("PROFILES", `${SUPABASE_URL}/rest/v1/profiles?select=*`);
|
|
||||||
|
|
||||||
console.log("\n\n📊 RESUMO DO DIAGNÓSTICO");
|
|
||||||
console.log("═".repeat(60));
|
|
||||||
console.log("Se alguma tabela retornou 404, ela não existe no Supabase.");
|
|
||||||
console.log(
|
|
||||||
"Se retornou 200 mas array vazio, a tabela existe mas não tem dados."
|
|
||||||
);
|
|
||||||
console.log("Se retornou 401/403, há problema de permissões (RLS).");
|
|
||||||
console.log("\n💡 PRÓXIMOS PASSOS:");
|
|
||||||
console.log("1. Verifique quais tabelas existem no Supabase Dashboard");
|
|
||||||
console.log("2. Se necessário, crie as tabelas doctors/patients");
|
|
||||||
console.log("3. Configure as políticas RLS para permitir SELECT público");
|
|
||||||
console.log("4. Insira dados de teste nas tabelas\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
diagnosticar();
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
async function listarPacientes() {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=id,full_name,email,cpf&limit=10`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("📋 Pacientes cadastrados:\n");
|
|
||||||
if (response.data.length === 0) {
|
|
||||||
console.log("❌ Nenhum paciente encontrado!");
|
|
||||||
} else {
|
|
||||||
response.data.forEach((p) => {
|
|
||||||
console.log(`• ${p.full_name} - ${p.email} - CPF: ${p.cpf}`);
|
|
||||||
console.log(` ID: ${p.id}\n`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.response?.data || error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listarPacientes();
|
|
||||||
@ -1,146 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script para listar todos os usuários do sistema
|
|
||||||
* Lista informações de auth.users, doctors e patients
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Credenciais do admin
|
|
||||||
const ADMIN_EMAIL = "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD = "riseup";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log("🔐 Fazendo login como admin...\n");
|
|
||||||
|
|
||||||
// 1. Login do admin
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: ADMIN_EMAIL,
|
|
||||||
password: ADMIN_PASSWORD,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = loginResponse.data.access_token;
|
|
||||||
const userId = loginResponse.data.user.id;
|
|
||||||
console.log("✅ Login realizado com sucesso!");
|
|
||||||
console.log(`User ID: ${userId}\n`);
|
|
||||||
|
|
||||||
// 2. Listar todos os médicos
|
|
||||||
console.log("👨⚕️ LISTANDO MÉDICOS:\n");
|
|
||||||
const medicosResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/doctors?select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Total de médicos: ${medicosResponse.data.length}\n`);
|
|
||||||
medicosResponse.data.forEach((medico, index) => {
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${medico.full_name || medico.nome || "Sem nome"}`
|
|
||||||
);
|
|
||||||
console.log(` ID: ${medico.id}`);
|
|
||||||
console.log(` User ID: ${medico.user_id || "não vinculado"}`);
|
|
||||||
console.log(` Email: ${medico.email}`);
|
|
||||||
console.log(` CRM: ${medico.crm} - ${medico.crm_uf || ""}`);
|
|
||||||
console.log(
|
|
||||||
` Especialidade: ${medico.specialty || medico.especialidade}`
|
|
||||||
);
|
|
||||||
console.log(` Ativo: ${medico.active ? "Sim" : "Não"}`);
|
|
||||||
console.log("");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 3. Listar todos os pacientes
|
|
||||||
console.log("👥 LISTANDO PACIENTES:\n");
|
|
||||||
const pacientesResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Total de pacientes: ${pacientesResponse.data.length}\n`);
|
|
||||||
pacientesResponse.data.forEach((paciente, index) => {
|
|
||||||
console.log(`${index + 1}. ${paciente.full_name}`);
|
|
||||||
console.log(` ID: ${paciente.id}`);
|
|
||||||
console.log(` Email: ${paciente.email}`);
|
|
||||||
console.log(` CPF: ${paciente.cpf}`);
|
|
||||||
console.log(` Telefone: ${paciente.phone_mobile}`);
|
|
||||||
console.log("");
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Verificar se existe tabela de roles/profiles
|
|
||||||
console.log("🔍 VERIFICANDO ESTRUTURA DE ROLES:\n");
|
|
||||||
try {
|
|
||||||
const profilesResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles?select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`✅ Tabela profiles encontrada com ${profilesResponse.data.length} registros`
|
|
||||||
);
|
|
||||||
console.log("Profiles:");
|
|
||||||
profilesResponse.data.forEach((profile) => {
|
|
||||||
console.log(` - User ID: ${profile.id || profile.user_id}`);
|
|
||||||
console.log(` Role: ${profile.role || "não definida"}`);
|
|
||||||
console.log(` Nome: ${profile.full_name || "não definido"}`);
|
|
||||||
console.log("");
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.status === 404) {
|
|
||||||
console.log("⚠️ Tabela profiles não encontrada ou não acessível");
|
|
||||||
console.log(
|
|
||||||
"💡 Sugestão: Criar tabela profiles com campos: id (uuid), user_id (uuid), role (text), full_name (text)\n"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"❌ Erro ao acessar profiles:",
|
|
||||||
error.response?.data?.message || error.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Resumo
|
|
||||||
console.log("📊 RESUMO:\n");
|
|
||||||
console.log(`✅ ${medicosResponse.data.length} médicos cadastrados`);
|
|
||||||
console.log(`✅ ${pacientesResponse.data.length} pacientes cadastrados`);
|
|
||||||
|
|
||||||
const medicosComUser = medicosResponse.data.filter((m) => m.user_id).length;
|
|
||||||
console.log(`\n🔗 ${medicosComUser} médicos vinculados a usuários auth`);
|
|
||||||
console.log(
|
|
||||||
`⚠️ ${
|
|
||||||
medicosResponse.data.length - medicosComUser
|
|
||||||
} médicos SEM vinculação auth\n`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.response?.data || error.message);
|
|
||||||
if (error.response) {
|
|
||||||
console.error("Status:", error.response.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
-- =========================================
|
|
||||||
-- POLÍTICAS RLS PARA MEDICONNECT
|
|
||||||
-- =========================================
|
|
||||||
-- Execute este SQL no SQL Editor do Supabase Dashboard:
|
|
||||||
-- https://app.supabase.com/project/yuanqfswhberkoevtmfr/sql/new
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- 1. TABELA DOCTORS (Médicos)
|
|
||||||
-- =========================================
|
|
||||||
|
|
||||||
-- Remover políticas antigas se existirem
|
|
||||||
DROP POLICY IF EXISTS "doctors_select_all" ON doctors;
|
|
||||||
DROP POLICY IF EXISTS "doctors_insert_authenticated" ON doctors;
|
|
||||||
DROP POLICY IF EXISTS "doctors_update_authenticated" ON doctors;
|
|
||||||
|
|
||||||
-- SELECT: Todos podem ler médicos (necessário para listagens públicas)
|
|
||||||
CREATE POLICY "doctors_select_all"
|
|
||||||
ON doctors FOR SELECT
|
|
||||||
TO public
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- INSERT: Apenas usuários autenticados podem criar médicos
|
|
||||||
CREATE POLICY "doctors_insert_authenticated"
|
|
||||||
ON doctors FOR INSERT
|
|
||||||
TO authenticated
|
|
||||||
WITH CHECK (true);
|
|
||||||
|
|
||||||
-- UPDATE: Apenas usuários autenticados podem atualizar médicos
|
|
||||||
CREATE POLICY "doctors_update_authenticated"
|
|
||||||
ON doctors FOR UPDATE
|
|
||||||
TO authenticated
|
|
||||||
USING (true)
|
|
||||||
WITH CHECK (true);
|
|
||||||
|
|
||||||
-- DELETE: Apenas usuários autenticados podem deletar médicos
|
|
||||||
CREATE POLICY "doctors_delete_authenticated"
|
|
||||||
ON doctors FOR DELETE
|
|
||||||
TO authenticated
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- 2. TABELA PATIENTS (Pacientes)
|
|
||||||
-- =========================================
|
|
||||||
|
|
||||||
-- Remover políticas antigas se existirem
|
|
||||||
DROP POLICY IF EXISTS "patients_select_all" ON patients;
|
|
||||||
DROP POLICY IF EXISTS "patients_insert_authenticated" ON patients;
|
|
||||||
DROP POLICY IF EXISTS "patients_update_authenticated" ON patients;
|
|
||||||
DROP POLICY IF EXISTS "patients_update_own" ON patients;
|
|
||||||
|
|
||||||
-- SELECT: Todos podem ler pacientes (necessário para listagens)
|
|
||||||
CREATE POLICY "patients_select_all"
|
|
||||||
ON patients FOR SELECT
|
|
||||||
TO public
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- INSERT: Usuários autenticados podem criar pacientes
|
|
||||||
CREATE POLICY "patients_insert_authenticated"
|
|
||||||
ON patients FOR INSERT
|
|
||||||
TO authenticated
|
|
||||||
WITH CHECK (true);
|
|
||||||
|
|
||||||
-- UPDATE: Usuários autenticados podem atualizar qualquer paciente
|
|
||||||
-- (ideal para secretárias e médicos)
|
|
||||||
CREATE POLICY "patients_update_authenticated"
|
|
||||||
ON patients FOR UPDATE
|
|
||||||
TO authenticated
|
|
||||||
USING (true)
|
|
||||||
WITH CHECK (true);
|
|
||||||
|
|
||||||
-- DELETE: Apenas usuários autenticados podem deletar
|
|
||||||
CREATE POLICY "patients_delete_authenticated"
|
|
||||||
ON patients FOR DELETE
|
|
||||||
TO authenticated
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- 3. TABELA PROFILES (Se existir)
|
|
||||||
-- =========================================
|
|
||||||
|
|
||||||
-- SELECT: Todos podem ler profiles
|
|
||||||
DROP POLICY IF EXISTS "profiles_select_all" ON profiles;
|
|
||||||
CREATE POLICY "profiles_select_all"
|
|
||||||
ON profiles FOR SELECT
|
|
||||||
TO public
|
|
||||||
USING (true);
|
|
||||||
|
|
||||||
-- INSERT: Apenas ao criar próprio perfil
|
|
||||||
DROP POLICY IF EXISTS "profiles_insert_own" ON profiles;
|
|
||||||
CREATE POLICY "profiles_insert_own"
|
|
||||||
ON profiles FOR INSERT
|
|
||||||
TO authenticated
|
|
||||||
WITH CHECK (auth.uid() = id);
|
|
||||||
|
|
||||||
-- UPDATE: Apenas próprio perfil
|
|
||||||
DROP POLICY IF EXISTS "profiles_update_own" ON profiles;
|
|
||||||
CREATE POLICY "profiles_update_own"
|
|
||||||
ON profiles FOR UPDATE
|
|
||||||
TO authenticated
|
|
||||||
USING (auth.uid() = id)
|
|
||||||
WITH CHECK (auth.uid() = id);
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- VERIFICAR SE RLS ESTÁ ATIVADO
|
|
||||||
-- =========================================
|
|
||||||
|
|
||||||
ALTER TABLE doctors ENABLE ROW LEVEL SECURITY;
|
|
||||||
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
|
|
||||||
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- =========================================
|
|
||||||
-- RESULTADO ESPERADO
|
|
||||||
-- =========================================
|
|
||||||
-- Após executar este script:
|
|
||||||
-- ✅ Qualquer um pode LER médicos e pacientes (necessário para UI pública)
|
|
||||||
-- ✅ Apenas usuários AUTENTICADOS podem CRIAR/EDITAR/DELETAR
|
|
||||||
-- ✅ A secretária poderá adicionar médicos e pacientes quando estiver logada
|
|
||||||
-- ✅ O painel mostrará os dados corretamente
|
|
||||||
@ -1,126 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const email = `teste${timestamp}@gmail.com`;
|
|
||||||
const password = "SenhaSegura123!";
|
|
||||||
|
|
||||||
async function cadastrarUsuario() {
|
|
||||||
console.log("\n📝 ETAPA 1: Cadastrando novo usuário...\n");
|
|
||||||
console.log(`Email: ${email}`);
|
|
||||||
console.log(`Senha: ${password}\n`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${SUPABASE_URL}/auth/v1/signup`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
data: {
|
|
||||||
nome: "Teste Login",
|
|
||||||
telefone: "79999999999",
|
|
||||||
cpf: "12345678900",
|
|
||||||
dataNascimento: "1990-01-01",
|
|
||||||
endereco: JSON.stringify({
|
|
||||||
rua: "Rua Teste",
|
|
||||||
numero: "123",
|
|
||||||
bairro: "Centro",
|
|
||||||
cidade: "Aracaju",
|
|
||||||
estado: "SE",
|
|
||||||
cep: "49000-000",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
console.log("✅ CADASTRO SUCESSO!");
|
|
||||||
console.log(`User ID: ${data.user?.id}`);
|
|
||||||
console.log(`Email: ${data.user?.email}`);
|
|
||||||
console.log(
|
|
||||||
`Email confirmado: ${data.user?.email_confirmed_at ? "SIM" : "NÃO"}`
|
|
||||||
);
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
console.log("❌ CADASTRO FALHOU");
|
|
||||||
console.log(JSON.stringify(data, null, 2));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro no cadastro:", error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fazerLogin() {
|
|
||||||
console.log("\n\n🔐 ETAPA 2: Fazendo login com o usuário cadastrado...\n");
|
|
||||||
console.log(`Email: ${email}`);
|
|
||||||
console.log(`Senha: ${password}\n`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
console.log(`Status: ${response.status}\n`);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
console.log("✅ LOGIN SUCESSO!");
|
|
||||||
console.log(`\nToken JWT: ${data.access_token?.substring(0, 50)}...`);
|
|
||||||
console.log(`User ID: ${data.user?.id}`);
|
|
||||||
console.log(`Email: ${data.user?.email}`);
|
|
||||||
console.log(
|
|
||||||
`Email confirmado: ${data.user?.email_confirmed_at ? "SIM" : "NÃO"}`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
"\n✅ CONCLUSÃO: Sistema funcionando 100%! Login imediato após cadastro.\n"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log("❌ LOGIN FALHOU");
|
|
||||||
console.log("\nResposta completa:");
|
|
||||||
console.log(JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro ao fazer login:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testarFluxoCompleto() {
|
|
||||||
const cadastroResult = await cadastrarUsuario();
|
|
||||||
|
|
||||||
if (cadastroResult) {
|
|
||||||
// Aguardar 2 segundos para garantir que o usuário está no banco
|
|
||||||
console.log("\n⏳ Aguardando 2 segundos...");
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
await fazerLogin();
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"\n❌ Não foi possível prosseguir com o login porque o cadastro falhou."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testarFluxoCompleto();
|
|
||||||
@ -1,338 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script de teste: Cadastro completo de paciente
|
|
||||||
* Verifica se:
|
|
||||||
* 1. Paciente é cadastrado via signup
|
|
||||||
* 2. Usuário é criado automaticamente no Supabase Auth
|
|
||||||
* 3. Registro do paciente é criado na tabela patients
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Gerar dados únicos para o teste
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const testEmail = `pacienteteste${timestamp}@gmail.com`;
|
|
||||||
const testPassword = "TestePaciente123!";
|
|
||||||
|
|
||||||
console.log("\n🧪 TESTE DE CADASTRO COMPLETO DE PACIENTE\n");
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
console.log(`Email de teste: ${testEmail}`);
|
|
||||||
console.log(`Senha: ${testPassword}`);
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
|
|
||||||
async function signupPaciente() {
|
|
||||||
console.log("\n📝 ETAPA 1: Cadastrar paciente via /auth/v1/signup...");
|
|
||||||
|
|
||||||
const signupData = {
|
|
||||||
email: testEmail,
|
|
||||||
password: testPassword,
|
|
||||||
options: {
|
|
||||||
data: {
|
|
||||||
role: "paciente",
|
|
||||||
full_name: "Paciente Teste Automático",
|
|
||||||
cpf: "12345678901",
|
|
||||||
telefone: "11999999999",
|
|
||||||
data_nascimento: "1990-01-01",
|
|
||||||
endereco: {
|
|
||||||
rua: "Rua de Teste",
|
|
||||||
numero: "123",
|
|
||||||
bairro: "Centro",
|
|
||||||
cidade: "São Paulo",
|
|
||||||
estado: "SP",
|
|
||||||
cep: "01000-000",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${SUPABASE_URL}/auth/v1/signup`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(signupData),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("❌ Erro no signup:", data);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Signup bem-sucedido!");
|
|
||||||
console.log(" User ID:", data.id);
|
|
||||||
console.log(" Email:", data.email);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro na requisição de signup:", error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createPatient(userId) {
|
|
||||||
console.log("\n📝 ETAPA 2: Criar registro na tabela patients...");
|
|
||||||
console.log(
|
|
||||||
" ℹ️ Nota: Removendo user_id do payload (não existe na tabela)"
|
|
||||||
);
|
|
||||||
|
|
||||||
const patientData = {
|
|
||||||
full_name: "Paciente Teste Automático",
|
|
||||||
cpf: "12345678901",
|
|
||||||
email: testEmail,
|
|
||||||
phone_mobile: "11999999999",
|
|
||||||
birth_date: "1990-01-01",
|
|
||||||
street: "Rua de Teste",
|
|
||||||
number: "123",
|
|
||||||
neighborhood: "Centro",
|
|
||||||
city: "São Paulo",
|
|
||||||
state: "SP",
|
|
||||||
cep: "01000-000",
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${SUPABASE_URL}/rest/v1/patients`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(patientData),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("❌ Erro ao criar patient:", data);
|
|
||||||
console.log(
|
|
||||||
" ℹ️ Isso é normal - a tabela pode ter estrutura diferente"
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Registro do paciente criado!");
|
|
||||||
console.log(" Patient ID:", data[0]?.id || data.id);
|
|
||||||
console.log(" Nome:", data[0]?.full_name || data.full_name);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"❌ Erro na requisição de criação do patient:",
|
|
||||||
error.message
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loginPaciente() {
|
|
||||||
console.log("\n🔐 ETAPA 3: Fazer login com o paciente criado...");
|
|
||||||
|
|
||||||
const loginData = {
|
|
||||||
email: testEmail,
|
|
||||||
password: testPassword,
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(loginData),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("❌ Erro no login:", data);
|
|
||||||
if (data.error_code === "email_not_confirmed") {
|
|
||||||
console.log(
|
|
||||||
" ℹ️ Email não confirmado - isso é configuração do Supabase"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" ℹ️ Para produção, configure SMTP ou desabilite confirmação"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Login bem-sucedido!");
|
|
||||||
console.log(" Access Token:", data.access_token.substring(0, 30) + "...");
|
|
||||||
console.log(" Token Type:", data.token_type);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro na requisição de login:", error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getUserInfo(accessToken) {
|
|
||||||
console.log("\n👤 ETAPA 4: Buscar informações do usuário autenticado...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("❌ Erro ao buscar user info:", data);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Informações do usuário obtidas!");
|
|
||||||
console.log(" ID:", data.id);
|
|
||||||
console.log(" Email:", data.email);
|
|
||||||
console.log(" Role:", data.user_metadata?.role);
|
|
||||||
console.log(" Nome:", data.user_metadata?.full_name);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro na requisição de user info:", error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listPatients(accessToken) {
|
|
||||||
console.log("\n📋 ETAPA 5: Verificar se paciente aparece na lista...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?email=eq.${testEmail}`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("❌ Erro ao listar patients:", data);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
console.log("⚠️ Paciente não encontrado na lista!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("✅ Paciente encontrado na lista!");
|
|
||||||
console.log(" Total de registros:", data.length);
|
|
||||||
console.log(" Dados:", JSON.stringify(data[0], null, 2));
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro na requisição de listagem:", error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runTest() {
|
|
||||||
try {
|
|
||||||
// NOVA ORDEM: Criar paciente PRIMEIRO, depois usuário
|
|
||||||
|
|
||||||
// Etapa 1: Criar registro do paciente (SEM autenticação)
|
|
||||||
console.log("\n📝 NOVA ESTRATÉGIA: Criando paciente ANTES do usuário...");
|
|
||||||
const patientResult = await createPatient(null);
|
|
||||||
if (!patientResult) {
|
|
||||||
console.log("\n⚠️ Não foi possível criar registro do paciente");
|
|
||||||
console.log(" ℹ️ Tentando criar usuário mesmo assim...");
|
|
||||||
} else {
|
|
||||||
console.log("\n✅ Paciente criado com sucesso!");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aguardar um pouco
|
|
||||||
console.log("\n⏳ Aguardando 2 segundos...");
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// Etapa 2: Signup (criar usuário de autenticação)
|
|
||||||
const signupResult = await signupPaciente();
|
|
||||||
if (!signupResult || !signupResult.id) {
|
|
||||||
console.log("\n❌ TESTE FALHOU: Não foi possível criar o usuário");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userId = signupResult.id;
|
|
||||||
console.log("\n✅ Usuário criado após paciente!");
|
|
||||||
|
|
||||||
// Etapa 3: Login
|
|
||||||
const loginResult = await loginPaciente();
|
|
||||||
if (!loginResult || !loginResult.access_token) {
|
|
||||||
console.log("\n❌ TESTE FALHOU: Não foi possível fazer login");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const accessToken = loginResult.access_token;
|
|
||||||
|
|
||||||
// Etapa 4: Buscar informações do usuário
|
|
||||||
const userInfo = await getUserInfo(accessToken);
|
|
||||||
if (!userInfo) {
|
|
||||||
console.log(
|
|
||||||
"\n⚠️ Login bem-sucedido, mas não foi possível buscar informações do usuário"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Etapa 5: Verificar se aparece na lista de pacientes
|
|
||||||
const patients = await listPatients(accessToken);
|
|
||||||
|
|
||||||
// Resumo final
|
|
||||||
console.log("\n" + "=".repeat(60));
|
|
||||||
console.log("📊 RESUMO DO TESTE");
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
console.log(
|
|
||||||
`✅ Usuário criado no Supabase Auth: ${signupResult ? "SIM" : "NÃO"}`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`✅ Registro criado na tabela patients: ${patientResult ? "SIM" : "NÃO"}`
|
|
||||||
);
|
|
||||||
console.log(`✅ Login funciona: ${loginResult ? "SIM" : "NÃO"}`);
|
|
||||||
console.log(`✅ Dados do usuário recuperados: ${userInfo ? "SIM" : "NÃO"}`);
|
|
||||||
console.log(
|
|
||||||
`✅ Paciente aparece na lista: ${
|
|
||||||
patients && patients.length > 0 ? "SIM" : "NÃO"
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
|
|
||||||
if (signupResult && patientResult && loginResult && userInfo && patients) {
|
|
||||||
console.log("\n🎉 TESTE COMPLETO BEM-SUCEDIDO! 🎉");
|
|
||||||
console.log("\nO paciente foi cadastrado corretamente e:");
|
|
||||||
console.log(" 1. Usuário criado no Supabase Auth ✅");
|
|
||||||
console.log(" 2. Registro na tabela patients ✅");
|
|
||||||
console.log(" 3. Login funciona ✅");
|
|
||||||
console.log(" 4. Dados acessíveis via API ✅");
|
|
||||||
} else {
|
|
||||||
console.log("\n⚠️ TESTE PARCIALMENTE BEM-SUCEDIDO");
|
|
||||||
console.log("Algumas etapas falharam. Verifique os logs acima.");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ ERRO GERAL NO TESTE:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executar teste
|
|
||||||
runTest();
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const myHeaders = {
|
|
||||||
apikey:
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ",
|
|
||||||
Authorization:
|
|
||||||
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ",
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("🔍 Testando GET /doctors com token...\n");
|
|
||||||
|
|
||||||
fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", {
|
|
||||||
method: "GET",
|
|
||||||
headers: myHeaders,
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
console.log(`Status: ${response.status} ${response.statusText}`);
|
|
||||||
return response.text();
|
|
||||||
})
|
|
||||||
.then((result) => {
|
|
||||||
console.log("\n📄 Resposta:");
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(result);
|
|
||||||
if (Array.isArray(json)) {
|
|
||||||
console.log(`✅ Array com ${json.length} registro(s)`);
|
|
||||||
if (json.length > 0) {
|
|
||||||
console.log("\n📋 Médicos encontrados:");
|
|
||||||
json.forEach((medico, index) => {
|
|
||||||
console.log(
|
|
||||||
`\n${index + 1}. ${medico.full_name || medico.nome || "Sem nome"}`
|
|
||||||
);
|
|
||||||
console.log(` ID: ${medico.id}`);
|
|
||||||
console.log(` CRM: ${medico.crm}`);
|
|
||||||
console.log(
|
|
||||||
` Especialidade: ${medico.specialty || medico.especialidade}`
|
|
||||||
);
|
|
||||||
console.log(` Email: ${medico.email}`);
|
|
||||||
console.log(` Ativo: ${medico.active}`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log("⚠️ Tabela vazia - sem médicos cadastrados");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(JSON.stringify(json, null, 2));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log(result);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => console.log("❌ Erro:", error));
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
async function testLogin() {
|
|
||||||
console.log("\n🔐 Testando login na API do Supabase...\n");
|
|
||||||
|
|
||||||
const email = "testefinal@gmail.com";
|
|
||||||
const password = "Teste123!";
|
|
||||||
|
|
||||||
console.log(`Email: ${email}`);
|
|
||||||
console.log(`Password: ${password}\n`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
console.log(`Status: ${response.status}\n`);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
console.log("✅ LOGIN SUCESSO!");
|
|
||||||
console.log(`\nToken JWT: ${data.access_token?.substring(0, 50)}...`);
|
|
||||||
console.log(`User ID: ${data.user?.id}`);
|
|
||||||
console.log(`Email: ${data.user?.email}`);
|
|
||||||
console.log(
|
|
||||||
`Email confirmado: ${data.user?.email_confirmed_at ? "SIM" : "NÃO"}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log("❌ LOGIN FALHOU");
|
|
||||||
console.log("\nResposta completa:");
|
|
||||||
console.log(JSON.stringify(data, null, 2));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro ao fazer login:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testLogin();
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
/**
|
|
||||||
* Teste simplificado de signup via Supabase
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const testEmail = `pacienteteste${timestamp}@gmail.com`;
|
|
||||||
|
|
||||||
console.log("Testando signup com:", testEmail);
|
|
||||||
|
|
||||||
async function testSignup() {
|
|
||||||
const url = `${SUPABASE_URL}/auth/v1/signup`;
|
|
||||||
|
|
||||||
console.log("URL:", url);
|
|
||||||
|
|
||||||
const body = {
|
|
||||||
email: testEmail,
|
|
||||||
password: "Senha123!@#",
|
|
||||||
options: {
|
|
||||||
data: {
|
|
||||||
role: "paciente",
|
|
||||||
full_name: "Teste Automático",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("Body:", JSON.stringify(body, null, 2));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Status:", response.status);
|
|
||||||
console.log("Headers:", Object.fromEntries(response.headers.entries()));
|
|
||||||
|
|
||||||
const text = await response.text();
|
|
||||||
console.log("Response (text):", text.substring(0, 500));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(text);
|
|
||||||
console.log("Response (JSON):", JSON.stringify(data, null, 2));
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Não é JSON válido");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testSignup();
|
|
||||||
@ -1,178 +0,0 @@
|
|||||||
// Script para testar patient_assignments do Fernando
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Credenciais do Fernando
|
|
||||||
const FERNANDO_EMAIL = "fernando.pirichowski@souunit.com.br";
|
|
||||||
const FERNANDO_PASSWORD = "fernando";
|
|
||||||
|
|
||||||
async function testarAtribuicoes() {
|
|
||||||
try {
|
|
||||||
console.log("\n🔐 === TESTE DE PATIENT_ASSIGNMENTS ===\n");
|
|
||||||
|
|
||||||
// 1. Login do Fernando
|
|
||||||
console.log("1️⃣ Fazendo login com Fernando...");
|
|
||||||
const loginResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: FERNANDO_EMAIL,
|
|
||||||
password: FERNANDO_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Erro no login: ${loginResponse.status} - ${await loginResponse.text()}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
const accessToken = loginData.access_token;
|
|
||||||
const fernandoUserId = loginData.user.id;
|
|
||||||
|
|
||||||
console.log(`✅ Login realizado com sucesso!`);
|
|
||||||
console.log(` User ID: ${fernandoUserId}`);
|
|
||||||
console.log(` Email: ${loginData.user.email}`);
|
|
||||||
|
|
||||||
// 2. Buscar perfil do Fernando
|
|
||||||
console.log("\n2️⃣ Buscando perfil no profiles...");
|
|
||||||
const profileResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles?id=eq.${fernandoUserId}&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!profileResponse.ok) {
|
|
||||||
throw new Error(`Erro ao buscar perfil: ${profileResponse.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const profiles = await profileResponse.json();
|
|
||||||
if (profiles.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`✅ Perfil encontrado: ${
|
|
||||||
profiles[0].full_name || profiles[0].name || "Sem nome"
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Buscar atribuições do Fernando
|
|
||||||
console.log("\n3️⃣ Buscando patient_assignments...");
|
|
||||||
console.log(` Query: user_id=eq.${fernandoUserId}&role=eq.medico`);
|
|
||||||
|
|
||||||
const assignmentsResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments?user_id=eq.${fernandoUserId}&role=eq.medico&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!assignmentsResponse.ok) {
|
|
||||||
const errorText = await assignmentsResponse.text();
|
|
||||||
throw new Error(
|
|
||||||
`Erro ao buscar atribuições: ${assignmentsResponse.status} - ${errorText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const assignments = await assignmentsResponse.json();
|
|
||||||
console.log(`✅ ${assignments.length} atribuições encontradas!`);
|
|
||||||
|
|
||||||
if (assignments.length === 0) {
|
|
||||||
console.log(
|
|
||||||
"\n⚠️ Fernando NÃO tem atribuições na tabela patient_assignments!"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
" Isso significa que ele não conseguirá ver pacientes no painel médico."
|
|
||||||
);
|
|
||||||
console.log("\n💡 Solução:");
|
|
||||||
console.log(" 1. Criar atribuições manualmente no Supabase");
|
|
||||||
console.log(" 2. OU usar o script criar-atribuicao-fernando.js");
|
|
||||||
} else {
|
|
||||||
console.log("\n📋 Atribuições encontradas:");
|
|
||||||
assignments.forEach((a, i) => {
|
|
||||||
console.log(`\n ${i + 1}. Atribuição ID: ${a.id}`);
|
|
||||||
console.log(` Patient ID: ${a.patient_id}`);
|
|
||||||
console.log(` Role: ${a.role}`);
|
|
||||||
console.log(` Created At: ${a.created_at}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. Buscar detalhes dos pacientes atribuídos
|
|
||||||
console.log("\n4️⃣ Buscando detalhes dos pacientes...");
|
|
||||||
|
|
||||||
for (let i = 0; i < assignments.length; i++) {
|
|
||||||
const assignment = assignments[i];
|
|
||||||
const patientResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?id=eq.${assignment.patient_id}&select=id,full_name,email,phone_mobile`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (patientResponse.ok) {
|
|
||||||
const patients = await patientResponse.json();
|
|
||||||
if (patients.length > 0) {
|
|
||||||
const p = patients[0];
|
|
||||||
console.log(` ${i + 1}. ${p.full_name || "Sem nome"}`);
|
|
||||||
console.log(` Email: ${p.email || "N/A"}`);
|
|
||||||
console.log(` Tel: ${p.phone_mobile || "N/A"}`);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
` ${i + 1}. ⚠️ Paciente ${
|
|
||||||
assignment.patient_id
|
|
||||||
} não encontrado!`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Listar TODOS os pacientes (para referência)
|
|
||||||
console.log("\n5️⃣ Listando TODOS os pacientes (para referência)...");
|
|
||||||
const allPatientsResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=id,full_name&limit=10`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allPatientsResponse.ok) {
|
|
||||||
const allPatients = await allPatientsResponse.json();
|
|
||||||
console.log(
|
|
||||||
`📊 Total de pacientes no sistema: ${allPatients.length} (primeiros 10)`
|
|
||||||
);
|
|
||||||
allPatients.forEach((p, i) => {
|
|
||||||
console.log(` ${i + 1}. ${p.full_name} (${p.id})`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n✅ Teste concluído!");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ Erro no teste:", error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(" Mensagem:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Executar
|
|
||||||
testarAtribuicoes();
|
|
||||||
@ -1,143 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
console.log("\n🔐 TESTANDO COM AUTENTICAÇÃO\n");
|
|
||||||
console.log("Precisamos de um usuário válido para fazer login.");
|
|
||||||
console.log("Digite o email e senha de um usuário que você sabe que existe:\n");
|
|
||||||
|
|
||||||
// Credenciais fornecidas pelo usuário
|
|
||||||
const EMAIL_TESTE = "riseup@popcode.com.br";
|
|
||||||
const SENHA_TESTE = "riseup";
|
|
||||||
|
|
||||||
async function testarComAutenticacao() {
|
|
||||||
console.log(`📧 Tentando login com: ${EMAIL_TESTE}\n`);
|
|
||||||
|
|
||||||
// PASSO 1: Fazer login
|
|
||||||
try {
|
|
||||||
const loginResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: EMAIL_TESTE,
|
|
||||||
password: SENHA_TESTE,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
|
||||||
console.log("❌ Login falhou:", loginResponse.status);
|
|
||||||
const error = await loginResponse.text();
|
|
||||||
console.log(error);
|
|
||||||
console.log(
|
|
||||||
"\n💡 SOLUÇÃO: Use um email/senha de usuário que você já cadastrou!"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
const accessToken = loginData.access_token;
|
|
||||||
|
|
||||||
console.log("✅ Login bem-sucedido!");
|
|
||||||
console.log(`👤 User ID: ${loginData.user?.id}`);
|
|
||||||
console.log(`🔑 Token: ${accessToken.substring(0, 50)}...\n`);
|
|
||||||
|
|
||||||
// PASSO 2: Buscar médicos COM o token
|
|
||||||
console.log("📋 Buscando médicos COM autenticação...\n");
|
|
||||||
|
|
||||||
const medicosResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/doctors?select=*`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (medicosResponse.ok) {
|
|
||||||
const medicos = await medicosResponse.json();
|
|
||||||
console.log(`✅ MÉDICOS ENCONTRADOS: ${medicos.length}\n`);
|
|
||||||
|
|
||||||
if (medicos.length > 0) {
|
|
||||||
console.log("📋 Lista de médicos:\n");
|
|
||||||
medicos.forEach((medico, index) => {
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${medico.full_name || medico.nome || "Sem nome"}`
|
|
||||||
);
|
|
||||||
console.log(` CRM: ${medico.crm}`);
|
|
||||||
console.log(
|
|
||||||
` Especialidade: ${medico.specialty || medico.especialidade}`
|
|
||||||
);
|
|
||||||
console.log(` Email: ${medico.email}`);
|
|
||||||
console.log("");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("❌ Erro ao buscar médicos:", medicosResponse.status);
|
|
||||||
const error = await medicosResponse.text();
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// PASSO 3: Buscar pacientes COM o token
|
|
||||||
console.log("\n📋 Buscando pacientes COM autenticação...\n");
|
|
||||||
|
|
||||||
const pacientesResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=*`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${accessToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pacientesResponse.ok) {
|
|
||||||
const pacientes = await pacientesResponse.json();
|
|
||||||
console.log(`✅ PACIENTES ENCONTRADOS: ${pacientes.length}\n`);
|
|
||||||
|
|
||||||
if (pacientes.length > 0) {
|
|
||||||
console.log("📋 Lista de pacientes:\n");
|
|
||||||
pacientes.slice(0, 5).forEach((paciente, index) => {
|
|
||||||
console.log(
|
|
||||||
`${index + 1}. ${paciente.full_name || paciente.nome || "Sem nome"}`
|
|
||||||
);
|
|
||||||
console.log(` Email: ${paciente.email}`);
|
|
||||||
console.log(` CPF: ${paciente.cpf}`);
|
|
||||||
console.log("");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (pacientes.length > 5) {
|
|
||||||
console.log(`... e mais ${pacientes.length - 5} pacientes\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("❌ Erro ao buscar pacientes:", pacientesResponse.status);
|
|
||||||
const error = await pacientesResponse.text();
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"\n✅ SUCESSO! Os dados ESTÃO no Supabase e são acessíveis com autenticação!\n"
|
|
||||||
);
|
|
||||||
console.log("🎯 CONCLUSÃO:");
|
|
||||||
console.log(" - RLS está configurado corretamente");
|
|
||||||
console.log(" - Dados precisam de autenticação para serem lidos");
|
|
||||||
console.log(" - A aplicação funciona porque o usuário está logado\n");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ Erro:", error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testarComAutenticacao();
|
|
||||||
@ -1,178 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script para testar criação de relatório com estrutura correta
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
const FERNANDO_EMAIL = "fernando.pirichowski@souunit.com.br";
|
|
||||||
const FERNANDO_PASSWORD = "fernando";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log("🔐 Fazendo login como médico Fernando...\n");
|
|
||||||
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: FERNANDO_EMAIL,
|
|
||||||
password: FERNANDO_PASSWORD,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = loginResponse.data.access_token;
|
|
||||||
const userId = loginResponse.data.user.id;
|
|
||||||
|
|
||||||
console.log("✅ Login realizado com sucesso!");
|
|
||||||
console.log(` User ID: ${userId}\n`);
|
|
||||||
|
|
||||||
// Buscar primeiro paciente disponível
|
|
||||||
console.log("🔍 Buscando pacientes...\n");
|
|
||||||
const pacientesResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=*&limit=1&order=created_at.desc`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pacientesResponse.data.length === 0) {
|
|
||||||
console.log("❌ Nenhum paciente encontrado!");
|
|
||||||
console.log("Execute primeiro o script cadastrar-guilherme.js\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const guilherme = pacientesResponse.data[0];
|
|
||||||
console.log("✅ Paciente encontrado:");
|
|
||||||
console.log(` ID: ${guilherme.id}`);
|
|
||||||
console.log(` Nome: ${guilherme.full_name}\n`);
|
|
||||||
|
|
||||||
// Criar relatório de teste
|
|
||||||
console.log("📝 Criando relatório médico...\n");
|
|
||||||
|
|
||||||
const relatorioData = {
|
|
||||||
patient_id: guilherme.id,
|
|
||||||
order_number: `REL-2025-10-TEST-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substr(2, 4)
|
|
||||||
.toUpperCase()}`,
|
|
||||||
exam: "Consulta Clínica Geral",
|
|
||||||
diagnosis:
|
|
||||||
"Paciente apresenta quadro de check-up de rotina sem alterações significativas.",
|
|
||||||
conclusion:
|
|
||||||
"Exame físico dentro dos padrões normais. Paciente orientado sobre hábitos saudáveis e prevenção de doenças.",
|
|
||||||
cid_code: "Z00.0",
|
|
||||||
content_html: `<div>
|
|
||||||
<h2>Relatório Médico - Consulta Clínica</h2>
|
|
||||||
<p><strong>Paciente:</strong> ${guilherme.full_name}</p>
|
|
||||||
<p><strong>Data:</strong> ${new Date().toLocaleDateString("pt-BR")}</p>
|
|
||||||
<h3>Anamnese:</h3>
|
|
||||||
<p>Paciente compareceu para consulta de check-up de rotina. Nega queixas específicas.</p>
|
|
||||||
<h3>Exame Físico:</h3>
|
|
||||||
<p>
|
|
||||||
- Estado geral: Bom<br>
|
|
||||||
- Pressão arterial: 120/80 mmHg<br>
|
|
||||||
- Frequência cardíaca: 72 bpm<br>
|
|
||||||
- Ausculta cardíaca e pulmonar: Sem alterações
|
|
||||||
</p>
|
|
||||||
<h3>Diagnóstico:</h3>
|
|
||||||
<p>Check-up de rotina sem alterações</p>
|
|
||||||
<h3>Conduta:</h3>
|
|
||||||
<p>
|
|
||||||
- Manter hábitos saudáveis<br>
|
|
||||||
- Retornar em 6 meses para novo check-up<br>
|
|
||||||
- Atividade física regular
|
|
||||||
</p>
|
|
||||||
</div>`,
|
|
||||||
content_json: {
|
|
||||||
blocks: [
|
|
||||||
{
|
|
||||||
type: "heading",
|
|
||||||
level: 2,
|
|
||||||
text: "Relatório Médico - Consulta Clínica",
|
|
||||||
},
|
|
||||||
{ type: "paragraph", text: `Paciente: ${guilherme.full_name}` },
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
text: `Data: ${new Date().toLocaleDateString("pt-BR")}`,
|
|
||||||
},
|
|
||||||
{ type: "heading", level: 3, text: "Anamnese" },
|
|
||||||
{
|
|
||||||
type: "paragraph",
|
|
||||||
text: "Paciente compareceu para consulta de check-up de rotina.",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
status: "final",
|
|
||||||
requested_by: "Dr. Fernando Pirichowski - Squad 18",
|
|
||||||
due_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
|
||||||
hide_date: false,
|
|
||||||
hide_signature: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const createResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/reports`,
|
|
||||||
relatorioData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const relatorio = Array.isArray(createResponse.data)
|
|
||||||
? createResponse.data[0]
|
|
||||||
: createResponse.data;
|
|
||||||
|
|
||||||
console.log("✅ RELATÓRIO CRIADO COM SUCESSO!\n");
|
|
||||||
console.log("📋 Detalhes do relatório:");
|
|
||||||
console.log(` ID: ${relatorio.id}`);
|
|
||||||
console.log(` Número do Pedido: ${relatorio.order_number}`);
|
|
||||||
console.log(` Paciente ID: ${relatorio.patient_id}`);
|
|
||||||
console.log(` Exame: ${relatorio.exam}`);
|
|
||||||
console.log(` Status: ${relatorio.status}`);
|
|
||||||
console.log(` Diagnóstico: ${relatorio.diagnosis.substring(0, 50)}...`);
|
|
||||||
console.log(` Conclusão: ${relatorio.conclusion.substring(0, 50)}...`);
|
|
||||||
console.log(` CID: ${relatorio.cid_code}`);
|
|
||||||
console.log(` Solicitado por: ${relatorio.requested_by}`);
|
|
||||||
console.log(` Vencimento: ${relatorio.due_at}`);
|
|
||||||
console.log(` Criado em: ${relatorio.created_at}\n`);
|
|
||||||
|
|
||||||
console.log("🎉 TESTE COMPLETO!\n");
|
|
||||||
console.log('✅ Botão "Novo Relatório" no painel médico está funcionando');
|
|
||||||
console.log("✅ API de relatórios totalmente integrada");
|
|
||||||
console.log(
|
|
||||||
"✅ Estrutura de dados correta (patient_id, exam, diagnosis, etc.)\n"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("📝 Próximos passos:");
|
|
||||||
console.log("1. Acesse http://localhost:5173/login-medico");
|
|
||||||
console.log(
|
|
||||||
"2. Faça login com: fernando.pirichowski@souunit.com.br / fernando"
|
|
||||||
);
|
|
||||||
console.log('3. Clique no botão "Novo Relatório" (verde)');
|
|
||||||
console.log("4. Preencha o formulário e teste a criação!\n");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ ERRO:", error.response?.data || error.message);
|
|
||||||
if (error.response) {
|
|
||||||
console.error("Status:", error.response.status);
|
|
||||||
console.error("Data:", JSON.stringify(error.response.data, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@ -1,156 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script de teste completo para verificar se Guilherme pode acessar o sistema
|
|
||||||
*/
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
const GUILHERME_EMAIL = "guilhermesilvagomes1020@gmail.com";
|
|
||||||
const GUILHERME_PASSWORD = "guilherme123";
|
|
||||||
|
|
||||||
async function testarGuilherme() {
|
|
||||||
try {
|
|
||||||
console.log("\n🧪 === TESTANDO ACESSO DO GUILHERME ===\n");
|
|
||||||
|
|
||||||
// 1. Testar login
|
|
||||||
console.log("1️⃣ Testando login do Guilherme...");
|
|
||||||
console.log(` Email: ${GUILHERME_EMAIL}`);
|
|
||||||
console.log(` Senha: ${GUILHERME_PASSWORD}\n`);
|
|
||||||
|
|
||||||
const loginResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: GUILHERME_EMAIL,
|
|
||||||
password: GUILHERME_PASSWORD,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!loginResponse.ok) {
|
|
||||||
const error = await loginResponse.text();
|
|
||||||
console.error("❌ Erro no login:", error);
|
|
||||||
throw new Error("Login falhou");
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginData = await loginResponse.json();
|
|
||||||
const token = loginData.access_token;
|
|
||||||
const userId = loginData.user.id;
|
|
||||||
|
|
||||||
console.log("✅ Login realizado com sucesso!");
|
|
||||||
console.log(` User ID: ${userId}`);
|
|
||||||
console.log(` Email verificado: ${loginData.user.email}\n`);
|
|
||||||
|
|
||||||
// 2. Verificar pacientes atribuídos
|
|
||||||
console.log("2️⃣ Verificando pacientes atribuídos...");
|
|
||||||
const assignmentsResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patient_assignments?user_id=eq.${userId}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const assignments = await assignmentsResponse.json();
|
|
||||||
console.log(` ✅ ${assignments.length} paciente(s) atribuído(s)`);
|
|
||||||
|
|
||||||
if (assignments.length > 0) {
|
|
||||||
for (const assignment of assignments) {
|
|
||||||
console.log(`\n 📋 Atribuição:`);
|
|
||||||
console.log(` Patient ID: ${assignment.patient_id}`);
|
|
||||||
console.log(` Role: ${assignment.role}`);
|
|
||||||
|
|
||||||
// Buscar dados do paciente
|
|
||||||
const patientResponse = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?id=eq.${assignment.patient_id}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const patients = await patientResponse.json();
|
|
||||||
if (patients && patients.length > 0) {
|
|
||||||
const patient = patients[0];
|
|
||||||
console.log(` Nome: ${patient.full_name}`);
|
|
||||||
console.log(` Email: ${patient.email}`);
|
|
||||||
console.log(` Telefone: ${patient.phone_mobile}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Verificar consultas (localStorage simulation)
|
|
||||||
console.log("\n3️⃣ Verificando consultas de demonstração...");
|
|
||||||
const fs = await import("fs");
|
|
||||||
const path = await import("path");
|
|
||||||
const { fileURLToPath } = await import("url");
|
|
||||||
const { dirname } = await import("path");
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
const consultasPath = path.join(
|
|
||||||
__dirname,
|
|
||||||
"..",
|
|
||||||
"src",
|
|
||||||
"data",
|
|
||||||
"consultas-demo.json"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (fs.existsSync(consultasPath)) {
|
|
||||||
const consultasData = fs.readFileSync(consultasPath, "utf-8");
|
|
||||||
const consultas = JSON.parse(consultasData);
|
|
||||||
|
|
||||||
console.log(` ✅ ${consultas.length} consultas encontradas\n`);
|
|
||||||
|
|
||||||
consultas.forEach((consulta, index) => {
|
|
||||||
console.log(` 📅 Consulta ${index + 1}:`);
|
|
||||||
console.log(` Data/Hora: ${consulta.dataHora}`);
|
|
||||||
console.log(` Status: ${consulta.status}`);
|
|
||||||
console.log(` Tipo: ${consulta.tipo}`);
|
|
||||||
console.log(` Médico: ${consulta.medicoNome}`);
|
|
||||||
console.log(` Observações: ${consulta.observacoes}\n`);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(" ⚠️ Arquivo de consultas não encontrado");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Resumo final
|
|
||||||
console.log("\n✅ === TODOS OS TESTES PASSARAM! ===\n");
|
|
||||||
console.log("📋 RESUMO:");
|
|
||||||
console.log(` ✅ Login funcionando`);
|
|
||||||
console.log(` ✅ Paciente atribuído ao usuário`);
|
|
||||||
console.log(` ✅ Consultas de demonstração criadas`);
|
|
||||||
console.log(` ✅ Role: user (acesso ao painel paciente)\n`);
|
|
||||||
|
|
||||||
console.log("🎯 PRÓXIMA AÇÃO:");
|
|
||||||
console.log(" 1. Inicie o servidor de desenvolvimento: npm run dev");
|
|
||||||
console.log(" 2. Acesse: http://localhost:5173/paciente");
|
|
||||||
console.log(" 3. Faça login com:");
|
|
||||||
console.log(` Email: ${GUILHERME_EMAIL}`);
|
|
||||||
console.log(` Senha: ${GUILHERME_PASSWORD}`);
|
|
||||||
console.log(" 4. Você verá as 3 consultas no painel!\n");
|
|
||||||
|
|
||||||
console.log("💡 DICA:");
|
|
||||||
console.log(" As consultas são carregadas automaticamente do arquivo");
|
|
||||||
console.log(" src/data/consultas-demo.json para o localStorage\n");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ ERRO NO TESTE:", error.message);
|
|
||||||
if (error.stack) {
|
|
||||||
console.error(error.stack);
|
|
||||||
}
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testarGuilherme();
|
|
||||||
@ -1,130 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script para testar login do médico Fernando
|
|
||||||
* Verifica autenticação e se o usuário é identificado como médico
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
const FERNANDO_EMAIL = "fernando.pirichowski@souunit.com.br";
|
|
||||||
const FERNANDO_PASSWORD = "fernando";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log("🔐 TESTANDO LOGIN DO MÉDICO FERNANDO\n");
|
|
||||||
console.log("Credenciais:");
|
|
||||||
console.log(` Email: ${FERNANDO_EMAIL}`);
|
|
||||||
console.log(` Senha: ${FERNANDO_PASSWORD}\n`);
|
|
||||||
|
|
||||||
// 1. Fazer login
|
|
||||||
console.log("1️⃣ Fazendo login...\n");
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: FERNANDO_EMAIL,
|
|
||||||
password: FERNANDO_PASSWORD,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = loginResponse.data.access_token;
|
|
||||||
const userId = loginResponse.data.user.id;
|
|
||||||
|
|
||||||
console.log("✅ Login realizado com sucesso!");
|
|
||||||
console.log(` User ID: ${userId}`);
|
|
||||||
console.log(` Email: ${loginResponse.data.user.email}\n`);
|
|
||||||
|
|
||||||
// 2. Verificar se é médico (consultar tabela doctors)
|
|
||||||
console.log("2️⃣ Verificando se usuário é médico...\n");
|
|
||||||
const doctorResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/doctors?user_id=eq.${userId}&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (doctorResponse.data.length > 0) {
|
|
||||||
const doctor = doctorResponse.data[0];
|
|
||||||
console.log("✅ USUÁRIO É MÉDICO!");
|
|
||||||
console.log("\n📋 Dados do médico:");
|
|
||||||
console.log(` ID: ${doctor.id}`);
|
|
||||||
console.log(` Nome: ${doctor.full_name}`);
|
|
||||||
console.log(` Email: ${doctor.email}`);
|
|
||||||
console.log(` CRM: ${doctor.crm}-${doctor.crm_uf}`);
|
|
||||||
console.log(` Especialidade: ${doctor.specialty}`);
|
|
||||||
console.log(` Ativo: ${doctor.active ? "Sim" : "Não"}`);
|
|
||||||
console.log(` User ID: ${doctor.user_id}\n`);
|
|
||||||
|
|
||||||
console.log("✅ LOGIN VÁLIDO - Pode acessar painel médico!");
|
|
||||||
console.log("🎯 Redirecionamento: /painel-medico\n");
|
|
||||||
} else {
|
|
||||||
console.log("❌ USUÁRIO NÃO É MÉDICO");
|
|
||||||
console.log(" Este usuário não tem registro na tabela doctors");
|
|
||||||
console.log(" Acesso ao painel médico será negado.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Buscar consultas do médico (se aplicável)
|
|
||||||
if (doctorResponse.data.length > 0) {
|
|
||||||
console.log("3️⃣ Buscando consultas do médico...\n");
|
|
||||||
try {
|
|
||||||
const consultasResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/appointments?doctor_id=eq.${doctorResponse.data[0].id}&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
` Total de consultas: ${consultasResponse.data.length}\n`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(" ⚠️ Tabela appointments não encontrada ou sem dados\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Resumo
|
|
||||||
console.log("📊 RESUMO DO TESTE:\n");
|
|
||||||
console.log("✅ Autenticação funcionando corretamente");
|
|
||||||
console.log("✅ Verificação de role médico implementada");
|
|
||||||
console.log("✅ Token JWT válido gerado");
|
|
||||||
console.log(
|
|
||||||
`✅ Médico: ${doctorResponse.data.length > 0 ? "SIM" : "NÃO"}\n`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (doctorResponse.data.length > 0) {
|
|
||||||
console.log("🎉 TESTE BEM-SUCEDIDO!");
|
|
||||||
console.log(
|
|
||||||
"O médico Fernando pode fazer login e acessar o painel médico.\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ ERRO NO TESTE:", error.response?.data || error.message);
|
|
||||||
if (error.response) {
|
|
||||||
console.error("Status:", error.response.status);
|
|
||||||
|
|
||||||
if (error.response.status === 400) {
|
|
||||||
console.error("\n💡 Possíveis causas:");
|
|
||||||
console.error(" - Email ou senha incorretos");
|
|
||||||
console.error(" - Usuário não existe");
|
|
||||||
console.error(
|
|
||||||
" - Email não confirmado (verificar configurações Supabase)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@ -1,195 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script para testar a criação de relatórios na API
|
|
||||||
* Verifica se a tabela reports existe e testa criação de relatório
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Credenciais do médico Fernando
|
|
||||||
const FERNANDO_EMAIL = "fernando.pirichowski@souunit.com.br";
|
|
||||||
const FERNANDO_PASSWORD = "fernando";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log("🔐 Fazendo login como médico Fernando...\n");
|
|
||||||
|
|
||||||
// 1. Login do médico
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: FERNANDO_EMAIL,
|
|
||||||
password: FERNANDO_PASSWORD,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = loginResponse.data.access_token;
|
|
||||||
const userId = loginResponse.data.user.id;
|
|
||||||
|
|
||||||
console.log("✅ Login realizado com sucesso!");
|
|
||||||
console.log(` User ID: ${userId}\n`);
|
|
||||||
|
|
||||||
// 2. Verificar se tabela reports existe
|
|
||||||
console.log("🔍 Verificando se tabela reports existe...\n");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const checkTableResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/reports?select=id&limit=1`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ Tabela reports existe!");
|
|
||||||
console.log(
|
|
||||||
` Registros encontrados: ${checkTableResponse.data.length}\n`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.status === 404) {
|
|
||||||
console.log("❌ ERRO: Tabela reports NÃO existe no Supabase!\n");
|
|
||||||
console.log(
|
|
||||||
"💡 SOLUÇÃO: Execute o SQL abaixo no Supabase SQL Editor:\n"
|
|
||||||
);
|
|
||||||
console.log("```sql");
|
|
||||||
console.log(`CREATE TABLE IF NOT EXISTS public.reports (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
titulo TEXT NOT NULL,
|
|
||||||
tipo TEXT CHECK (tipo IN ('consultas', 'pacientes', 'financeiro', 'medicos')) NOT NULL,
|
|
||||||
descricao TEXT,
|
|
||||||
data_inicio DATE NOT NULL,
|
|
||||||
data_fim DATE NOT NULL,
|
|
||||||
dados JSONB DEFAULT '{}'::jsonb,
|
|
||||||
gerado_por UUID REFERENCES auth.users(id),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Habilitar RLS
|
|
||||||
ALTER TABLE public.reports ENABLE ROW LEVEL SECURITY;
|
|
||||||
|
|
||||||
-- Políticas de acesso
|
|
||||||
CREATE POLICY "reports_select_authenticated" ON public.reports
|
|
||||||
FOR SELECT TO authenticated USING (true);
|
|
||||||
|
|
||||||
CREATE POLICY "reports_insert_authenticated" ON public.reports
|
|
||||||
FOR INSERT TO authenticated WITH CHECK (true);
|
|
||||||
|
|
||||||
CREATE POLICY "reports_update_own" ON public.reports
|
|
||||||
FOR UPDATE TO authenticated USING (gerado_por = auth.uid());
|
|
||||||
|
|
||||||
CREATE POLICY "reports_delete_own" ON public.reports
|
|
||||||
FOR DELETE TO authenticated USING (gerado_por = auth.uid());
|
|
||||||
\`\`\`\n`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Criar relatório de teste
|
|
||||||
console.log("📝 Criando relatório de teste...\n");
|
|
||||||
|
|
||||||
const relatorioData = {
|
|
||||||
titulo: "Relatório de Teste - Consultas Outubro 2025",
|
|
||||||
tipo: "consultas",
|
|
||||||
descricao:
|
|
||||||
"Relatório gerado automaticamente para testar a funcionalidade",
|
|
||||||
data_inicio: "2025-10-01",
|
|
||||||
data_fim: "2025-10-31",
|
|
||||||
dados: {
|
|
||||||
medicoId: userId,
|
|
||||||
medicoNome: "Fernando Pirichowski - Squad 18",
|
|
||||||
totalConsultas: 0,
|
|
||||||
testScript: true,
|
|
||||||
},
|
|
||||||
gerado_por: userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const createResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/reports`,
|
|
||||||
relatorioData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const relatorio = Array.isArray(createResponse.data)
|
|
||||||
? createResponse.data[0]
|
|
||||||
: createResponse.data;
|
|
||||||
|
|
||||||
console.log("✅ Relatório criado com sucesso!\n");
|
|
||||||
console.log("📋 Detalhes do relatório:");
|
|
||||||
console.log(` ID: ${relatorio.id}`);
|
|
||||||
console.log(` Título: ${relatorio.titulo}`);
|
|
||||||
console.log(` Tipo: ${relatorio.tipo}`);
|
|
||||||
console.log(` Período: ${relatorio.data_inicio} a ${relatorio.data_fim}`);
|
|
||||||
console.log(` Gerado por: ${relatorio.gerado_por}`);
|
|
||||||
console.log(` Criado em: ${relatorio.created_at}\n`);
|
|
||||||
|
|
||||||
// 4. Listar todos os relatórios
|
|
||||||
console.log("📊 Listando todos os relatórios...\n");
|
|
||||||
|
|
||||||
const listResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/reports?select=*&order=created_at.desc`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`Total de relatórios: ${listResponse.data.length}\n`);
|
|
||||||
listResponse.data.forEach((rel, index) => {
|
|
||||||
console.log(`${index + 1}. ${rel.titulo}`);
|
|
||||||
console.log(
|
|
||||||
` Tipo: ${rel.tipo} | Período: ${rel.data_inicio} a ${rel.data_fim}`
|
|
||||||
);
|
|
||||||
console.log(` Criado em: ${rel.created_at}\n`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. Resumo
|
|
||||||
console.log("✅ TESTE COMPLETO!\n");
|
|
||||||
console.log("🎉 Sistema de relatórios funcionando corretamente!");
|
|
||||||
console.log(
|
|
||||||
'✅ Botão "Novo Relatório" no painel médico está conectado à API\n'
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ ERRO:", error.response?.data || error.message);
|
|
||||||
if (error.response) {
|
|
||||||
console.error("Status:", error.response.status);
|
|
||||||
|
|
||||||
if (error.response.status === 404) {
|
|
||||||
console.error("\n⚠️ Tabela reports não encontrada!");
|
|
||||||
console.error("Execute o SQL de criação da tabela mostrado acima.");
|
|
||||||
} else if (error.response.status === 401) {
|
|
||||||
console.error("\n⚠️ Erro de autenticação");
|
|
||||||
console.error("Verifique se o token JWT está válido");
|
|
||||||
} else if (error.response.status === 400) {
|
|
||||||
console.error("\n⚠️ Erro de validação");
|
|
||||||
console.error(
|
|
||||||
"Detalhes:",
|
|
||||||
JSON.stringify(error.response.data, null, 2)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@ -1,91 +0,0 @@
|
|||||||
/**
|
|
||||||
* Script para verificar estrutura da tabela reports
|
|
||||||
*/
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
const ADMIN_EMAIL = "riseup@popcode.com.br";
|
|
||||||
const ADMIN_PASSWORD = "riseup";
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
try {
|
|
||||||
console.log("🔐 Fazendo login...\n");
|
|
||||||
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: ADMIN_EMAIL,
|
|
||||||
password: ADMIN_PASSWORD,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = loginResponse.data.access_token;
|
|
||||||
console.log("✅ Login OK\n");
|
|
||||||
|
|
||||||
console.log("🔍 Listando relatórios existentes para ver estrutura...\n");
|
|
||||||
|
|
||||||
const listResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/reports?select=*&limit=1`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (listResponse.data.length > 0) {
|
|
||||||
console.log("✅ Estrutura encontrada:");
|
|
||||||
console.log(JSON.stringify(listResponse.data[0], null, 2));
|
|
||||||
console.log(
|
|
||||||
"\nCampos disponíveis:",
|
|
||||||
Object.keys(listResponse.data[0]).join(", ")
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"⚠️ Nenhum relatório existente. Tentando criar com campos básicos...\n"
|
|
||||||
);
|
|
||||||
|
|
||||||
const relatorioMinimo = {
|
|
||||||
titulo: "Teste Estrutura",
|
|
||||||
tipo: "consultas",
|
|
||||||
};
|
|
||||||
|
|
||||||
const createResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/rest/v1/reports`,
|
|
||||||
relatorioMinimo,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("✅ Relatório criado com sucesso!\n");
|
|
||||||
console.log("📋 Estrutura da tabela reports:");
|
|
||||||
console.log(JSON.stringify(createResponse.data, null, 2));
|
|
||||||
} catch (error) {
|
|
||||||
console.error("❌ ERRO:", error.response?.data || error.message);
|
|
||||||
if (error.response) {
|
|
||||||
console.error("Status:", error.response.status);
|
|
||||||
console.error("Data:", JSON.stringify(error.response.data, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
async function verificarPermissoesFernando() {
|
|
||||||
try {
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("🔍 VERIFICANDO PERMISSÕES DE FERNANDO");
|
|
||||||
console.log("═══════════════════════════════════════════════════\n");
|
|
||||||
|
|
||||||
// 1. Login como Fernando
|
|
||||||
console.log("🔑 Fazendo login como Fernando...");
|
|
||||||
const loginResponse = await axios.post(
|
|
||||||
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
|
||||||
{
|
|
||||||
email: "fernando.pirichowski@souunit.com.br",
|
|
||||||
password: "fernando",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const token = loginResponse.data.access_token;
|
|
||||||
const userId = loginResponse.data.user.id;
|
|
||||||
const userEmail = loginResponse.data.user.email;
|
|
||||||
|
|
||||||
console.log("✅ Login realizado com sucesso!");
|
|
||||||
console.log(` User ID: ${userId}`);
|
|
||||||
console.log(` Email: ${userEmail}\n`);
|
|
||||||
|
|
||||||
// 2. Buscar dados do usuário na tabela profiles
|
|
||||||
console.log("👤 Buscando dados na tabela profiles...");
|
|
||||||
const userResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles?id=eq.${userId}&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userResponse.data.length === 0) {
|
|
||||||
console.log("❌ Usuário não encontrado na tabela profiles!\n");
|
|
||||||
} else {
|
|
||||||
const user = userResponse.data[0];
|
|
||||||
console.log("✅ Dados do usuário:");
|
|
||||||
console.log(` Nome: ${user.full_name || "N/A"}`);
|
|
||||||
console.log(` Email: ${user.email}`);
|
|
||||||
console.log(` is_admin: ${user.is_admin}`);
|
|
||||||
console.log(` is_secretary: ${user.is_secretary}`);
|
|
||||||
console.log(` is_admin_or_manager: ${user.is_admin_or_manager}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Buscar roles na tabela user_roles
|
|
||||||
console.log("🎭 Buscando roles na tabela user_roles...");
|
|
||||||
const rolesResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/user_roles?user_id=eq.${userId}&select=*`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (rolesResponse.data.length === 0) {
|
|
||||||
console.log("❌ Nenhuma role encontrada!\n");
|
|
||||||
} else {
|
|
||||||
console.log("✅ Roles encontradas:");
|
|
||||||
rolesResponse.data.forEach((role) => {
|
|
||||||
console.log(` • ${role.role}`);
|
|
||||||
});
|
|
||||||
console.log("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Testar acesso aos pacientes
|
|
||||||
console.log("🏥 Testando acesso aos pacientes...");
|
|
||||||
try {
|
|
||||||
const pacientesResponse = await axios.get(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?select=id,full_name,email&limit=5`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`✅ ACESSO PERMITIDO! (${pacientesResponse.data.length} pacientes encontrados)`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pacientesResponse.data.length > 0) {
|
|
||||||
console.log("\n📋 Pacientes acessíveis:");
|
|
||||||
pacientesResponse.data.forEach((p) => {
|
|
||||||
console.log(
|
|
||||||
` • ${p.full_name || "Sem nome"} - ${p.email || "Sem email"}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log("");
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`❌ ACESSO NEGADO!`);
|
|
||||||
console.log(
|
|
||||||
` Erro: ${error.response?.data?.message || error.message}\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Testar criação de relatório
|
|
||||||
console.log("📝 Testando permissão para criar relatório...");
|
|
||||||
try {
|
|
||||||
// Não vou criar de fato, só testar se tem permissão
|
|
||||||
const testReportData = {
|
|
||||||
patient_id: "00000000-0000-0000-0000-000000000000", // ID fake para teste
|
|
||||||
exam: "Teste de permissão",
|
|
||||||
diagnosis: "Teste",
|
|
||||||
conclusion: "Teste",
|
|
||||||
order_number: "TEST-001",
|
|
||||||
status: "draft",
|
|
||||||
};
|
|
||||||
|
|
||||||
await axios.post(`${SUPABASE_URL}/rest/v1/reports`, testReportData, {
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Prefer: "return=representation",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("✅ PERMISSÃO PARA CRIAR RELATÓRIOS: SIM\n");
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.status === 403) {
|
|
||||||
console.log("❌ PERMISSÃO PARA CRIAR RELATÓRIOS: NEGADA");
|
|
||||||
console.log(
|
|
||||||
` Erro: ${error.response?.data?.message || "Acesso negado"}\n`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
"⚠️ Erro ao testar (pode ser FK constraint, não necessariamente permissão)"
|
|
||||||
);
|
|
||||||
console.log(` ${error.response?.data?.message || error.message}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Resumo
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
console.log("📊 RESUMO DAS PERMISSÕES DE FERNANDO");
|
|
||||||
console.log("═══════════════════════════════════════════════════");
|
|
||||||
|
|
||||||
const userData = userResponse.data[0] || {};
|
|
||||||
const roles = rolesResponse.data.map((r) => r.role);
|
|
||||||
|
|
||||||
console.log("\n🎭 Roles:", roles.length > 0 ? roles.join(", ") : "Nenhuma");
|
|
||||||
console.log("👑 Is Admin:", userData.is_admin || false);
|
|
||||||
console.log("👔 Is Secretary:", userData.is_secretary || false);
|
|
||||||
console.log("👨💼 Is Admin/Manager:", userData.is_admin_or_manager || false);
|
|
||||||
console.log("");
|
|
||||||
|
|
||||||
if (userData.is_admin || roles.includes("admin")) {
|
|
||||||
console.log("✅ Fernando TEM permissões de ADMIN");
|
|
||||||
} else {
|
|
||||||
console.log("❌ Fernando NÃO TEM permissões de ADMIN");
|
|
||||||
console.log("\n💡 Para adicionar permissões de admin:");
|
|
||||||
console.log(" 1. Execute: node scripts/dar-admin-fernando.js");
|
|
||||||
console.log(" 2. Ou use o painel do Supabase");
|
|
||||||
}
|
|
||||||
console.log("");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("\n❌ ERRO:", error.response?.data || error.message);
|
|
||||||
|
|
||||||
if (error.code === "ENOTFOUND") {
|
|
||||||
console.log("\n⚠️ Problema de conexão com Supabase");
|
|
||||||
} else if (error.response?.status === 400) {
|
|
||||||
console.log("\n⚠️ Credenciais inválidas ou usuário não existe");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verificarPermissoesFernando();
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"🔍 DIAGNÓSTICO COMPLETO - Verificando todas as tabelas possíveis\n"
|
|
||||||
);
|
|
||||||
|
|
||||||
async function testarVariacoes() {
|
|
||||||
const testes = [
|
|
||||||
// Médicos
|
|
||||||
{ nome: "doctors", url: `${SUPABASE_URL}/rest/v1/doctors?select=*` },
|
|
||||||
{
|
|
||||||
nome: "doctors (count)",
|
|
||||||
url: `${SUPABASE_URL}/rest/v1/doctors?select=count`,
|
|
||||||
},
|
|
||||||
{ nome: "medicos", url: `${SUPABASE_URL}/rest/v1/medicos?select=*` },
|
|
||||||
{
|
|
||||||
nome: "user_directory",
|
|
||||||
url: `${SUPABASE_URL}/rest/v1/user_directory?select=*`,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Pacientes
|
|
||||||
{ nome: "patients", url: `${SUPABASE_URL}/rest/v1/patients?select=*` },
|
|
||||||
{
|
|
||||||
nome: "patients (count)",
|
|
||||||
url: `${SUPABASE_URL}/rest/v1/patients?select=count`,
|
|
||||||
},
|
|
||||||
{ nome: "pacientes", url: `${SUPABASE_URL}/rest/v1/pacientes?select=*` },
|
|
||||||
|
|
||||||
// Outras tabelas possíveis
|
|
||||||
{ nome: "profiles", url: `${SUPABASE_URL}/rest/v1/profiles?select=*` },
|
|
||||||
{ nome: "users", url: `${SUPABASE_URL}/rest/v1/users?select=*` },
|
|
||||||
{
|
|
||||||
nome: "appointments",
|
|
||||||
url: `${SUPABASE_URL}/rest/v1/appointments?select=*`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const teste of testes) {
|
|
||||||
console.log(`\n📋 Testando: ${teste.nome}`);
|
|
||||||
console.log("─".repeat(60));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(teste.url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Status: ${response.status}`);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
console.log(`✅ ENCONTRADO! ${data.length} registro(s)`);
|
|
||||||
|
|
||||||
if (data.length > 0) {
|
|
||||||
console.log("\n📄 Primeiro registro:");
|
|
||||||
const primeiro = data[0];
|
|
||||||
const campos = Object.keys(primeiro);
|
|
||||||
console.log(`Campos disponíveis: ${campos.join(", ")}`);
|
|
||||||
console.log("\nDados:");
|
|
||||||
console.log(JSON.stringify(primeiro, null, 2).substring(0, 500));
|
|
||||||
}
|
|
||||||
} else if (data.count !== undefined) {
|
|
||||||
console.log(`✅ COUNT: ${data.count} registro(s)`);
|
|
||||||
} else {
|
|
||||||
console.log("✅ Resposta:", JSON.stringify(data).substring(0, 200));
|
|
||||||
}
|
|
||||||
} else if (response.status === 404) {
|
|
||||||
console.log("❌ Tabela não existe");
|
|
||||||
} else if (response.status === 401 || response.status === 403) {
|
|
||||||
console.log("🔒 Bloqueado por RLS (precisa autenticação)");
|
|
||||||
} else {
|
|
||||||
const error = await response.text();
|
|
||||||
console.log("❌ Erro:", error.substring(0, 200));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("❌ Erro de conexão:", error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pequeno delay entre requests
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("\n\n" + "=".repeat(60));
|
|
||||||
console.log("🎯 RESUMO");
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
console.log("Se alguma tabela mostrou registros > 0, os dados EXISTEM!");
|
|
||||||
console.log("Se todas mostraram 0, pode ser:");
|
|
||||||
console.log(" 1. Dados realmente não existem");
|
|
||||||
console.log(" 2. RLS está bloqueando a leitura");
|
|
||||||
console.log(" 3. Tabelas têm nomes diferentes");
|
|
||||||
console.log("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
testarVariacoes();
|
|
||||||
@ -1,160 +0,0 @@
|
|||||||
/**
|
|
||||||
* Verificar se um usuário/paciente foi criado na API
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fetch from "node-fetch";
|
|
||||||
|
|
||||||
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const SUPABASE_ANON_KEY =
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
|
||||||
|
|
||||||
// Pegar email da linha de comando ou usar um padrão
|
|
||||||
const emailToSearch = process.argv[2] || "paciente.teste";
|
|
||||||
|
|
||||||
console.log("\n🔍 VERIFICANDO CRIAÇÃO DE USUÁRIO\n");
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
console.log(`Buscando por: ${emailToSearch}`);
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
|
|
||||||
async function checkProfiles() {
|
|
||||||
console.log("\n📋 Verificando tabela profiles...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/profiles?email=ilike.*${emailToSearch}*&select=*`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.log(" ⚠️ Erro ao acessar profiles:", data);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` ✅ Encontrados ${data.length} registro(s) em profiles`);
|
|
||||||
data.forEach((profile, i) => {
|
|
||||||
console.log(`\n 👤 Usuário ${i + 1}:`);
|
|
||||||
console.log(` ID: ${profile.id}`);
|
|
||||||
console.log(` Nome: ${profile.full_name || profile.name}`);
|
|
||||||
console.log(` Email: ${profile.email}`);
|
|
||||||
console.log(
|
|
||||||
` Telefone: ${profile.phone_mobile || profile.phone || "N/A"}`
|
|
||||||
);
|
|
||||||
console.log(` Criado em: ${profile.created_at}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(" ❌ Erro:", error.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkPatients() {
|
|
||||||
console.log("\n📋 Verificando tabela patients...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(
|
|
||||||
`${SUPABASE_URL}/rest/v1/patients?email=ilike.*${emailToSearch}*&select=*`,
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.log(" ⚠️ Erro ao acessar patients:", data);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` ✅ Encontrados ${data.length} registro(s) em patients`);
|
|
||||||
data.forEach((patient, i) => {
|
|
||||||
console.log(`\n 🏥 Paciente ${i + 1}:`);
|
|
||||||
console.log(` ID: ${patient.id}`);
|
|
||||||
console.log(` Nome: ${patient.full_name}`);
|
|
||||||
console.log(` Email: ${patient.email}`);
|
|
||||||
console.log(` CPF: ${patient.cpf || "N/A"}`);
|
|
||||||
console.log(` Telefone: ${patient.phone_mobile || "N/A"}`);
|
|
||||||
console.log(` Criado em: ${patient.created_at}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(" ❌ Erro:", error.message);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkUsers() {
|
|
||||||
console.log(
|
|
||||||
"\n📋 Tentando verificar auth.users (pode falhar por permissões)..."
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${SUPABASE_URL}/auth/v1/admin/users`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
apikey: SUPABASE_ANON_KEY,
|
|
||||||
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.log(" ⚠️ Sem permissão para acessar auth.users (normal)");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const filtered =
|
|
||||||
data.users?.filter((u) => u.email?.includes(emailToSearch)) || [];
|
|
||||||
console.log(
|
|
||||||
` ✅ Encontrados ${filtered.length} usuário(s) em auth.users`
|
|
||||||
);
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(" ⚠️ Sem acesso a auth.users (normal para anon key)");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const profiles = await checkProfiles();
|
|
||||||
const patients = await checkPatients();
|
|
||||||
await checkUsers();
|
|
||||||
|
|
||||||
console.log("\n" + "=".repeat(60));
|
|
||||||
console.log("📊 RESUMO");
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
console.log(`Registros em profiles: ${profiles.length}`);
|
|
||||||
console.log(`Registros em patients: ${patients.length}`);
|
|
||||||
|
|
||||||
if (profiles.length > 0 && patients.length > 0) {
|
|
||||||
console.log("\n✅ SUCESSO! Usuário criado em ambas as tabelas!");
|
|
||||||
} else if (profiles.length > 0) {
|
|
||||||
console.log("\n⚠️ Usuário criado em profiles, mas não em patients");
|
|
||||||
} else if (patients.length > 0) {
|
|
||||||
console.log("\n⚠️ Registro em patients, mas não em profiles");
|
|
||||||
} else {
|
|
||||||
console.log("\n❌ Nenhum registro encontrado");
|
|
||||||
}
|
|
||||||
console.log("=".repeat(60));
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
@ -24,13 +24,20 @@ import TokenInspector from "./pages/TokenInspector";
|
|||||||
import AdminDiagnostico from "./pages/AdminDiagnostico";
|
import AdminDiagnostico from "./pages/AdminDiagnostico";
|
||||||
import TesteCadastroSquad18 from "./pages/TesteCadastroSquad18";
|
import TesteCadastroSquad18 from "./pages/TesteCadastroSquad18";
|
||||||
import PainelAdmin from "./pages/PainelAdmin";
|
import PainelAdmin from "./pages/PainelAdmin";
|
||||||
|
import CentralAjudaRouter from "./pages/CentralAjudaRouter";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="app-root min-h-screen bg-gray-50 dark:bg-slate-900 dark:bg-gradient-to-br dark:from-slate-900 dark:to-slate-800 transition-colors duration-300">
|
<div className="app-root min-h-screen bg-gray-50 dark:bg-slate-900 dark:bg-gradient-to-br dark:from-slate-900 dark:to-slate-800 transition-colors duration-300">
|
||||||
|
<a
|
||||||
|
href="#main-content"
|
||||||
|
className="fixed -top-20 left-4 z-50 px-3 py-2 bg-blue-600 text-white rounded shadow transition-all focus:top-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||||
|
>
|
||||||
|
Pular para o conteúdo
|
||||||
|
</a>
|
||||||
<Header />
|
<Header />
|
||||||
<main className="container mx-auto px-4 py-8">
|
<main id="main-content" className="container mx-auto px-4 py-8">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/paciente" element={<LoginPaciente />} />
|
<Route path="/paciente" element={<LoginPaciente />} />
|
||||||
@ -42,6 +49,7 @@ function App() {
|
|||||||
<Route path="/admin/diagnostico" element={<AdminDiagnostico />} />
|
<Route path="/admin/diagnostico" element={<AdminDiagnostico />} />
|
||||||
<Route path="/teste-squad18" element={<TesteCadastroSquad18 />} />
|
<Route path="/teste-squad18" element={<TesteCadastroSquad18 />} />
|
||||||
<Route path="/cadastro" element={<CadastroSecretaria />} />
|
<Route path="/cadastro" element={<CadastroSecretaria />} />
|
||||||
|
<Route path="/ajuda" element={<CentralAjudaRouter />} />
|
||||||
<Route element={<ProtectedRoute roles={["admin", "gestor"]} />}>
|
<Route element={<ProtectedRoute roles={["admin", "gestor"]} />}>
|
||||||
<Route path="/admin" element={<PainelAdmin />} />
|
<Route path="/admin" element={<PainelAdmin />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@ -1,131 +0,0 @@
|
|||||||
// Ambiente jsdom para testar hooks que manipulam document.documentElement
|
|
||||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
||||||
import {
|
|
||||||
STORAGE_KEY,
|
|
||||||
DEFAULT_ACCESSIBILITY_PREFS,
|
|
||||||
applyAccessibilityPrefsForTest,
|
|
||||||
} from "../hooks/useAccessibilityPrefs";
|
|
||||||
import * as pacienteService from "../services/pacienteService";
|
|
||||||
|
|
||||||
// Pequeno mock de localStorage para ambiente de teste jsdom
|
|
||||||
|
|
||||||
describe("useAccessibilityPrefs", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Limpa storage entre testes
|
|
||||||
for (let i = 0; i < global.localStorage.length; i++) {
|
|
||||||
const key = global.localStorage.key(i);
|
|
||||||
if (key) global.localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
document.documentElement.className = "";
|
|
||||||
document.documentElement.style.fontSize = "";
|
|
||||||
});
|
|
||||||
|
|
||||||
it("aplica classe dark ao ativar darkMode", () => {
|
|
||||||
const prefs = { ...DEFAULT_ACCESSIBILITY_PREFS, darkMode: true };
|
|
||||||
applyAccessibilityPrefsForTest(prefs);
|
|
||||||
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("aplica/remover classes para cada preferência boolean", () => {
|
|
||||||
const mapping: Array<
|
|
||||||
[keyof typeof DEFAULT_ACCESSIBILITY_PREFS, string | null]
|
|
||||||
> = [
|
|
||||||
["highContrast", "high-contrast"],
|
|
||||||
["darkMode", "dark"],
|
|
||||||
["dyslexicFont", "dyslexic-font"],
|
|
||||||
["lineSpacing", "line-spacing"],
|
|
||||||
["reducedMotion", "reduced-motion"],
|
|
||||||
["lowBlueLight", "low-blue-light"],
|
|
||||||
["focusMode", "focus-mode"],
|
|
||||||
];
|
|
||||||
for (const [key, className] of mapping) {
|
|
||||||
if (!className) continue;
|
|
||||||
const prefsOn = {
|
|
||||||
...DEFAULT_ACCESSIBILITY_PREFS,
|
|
||||||
[key]: true,
|
|
||||||
} as typeof DEFAULT_ACCESSIBILITY_PREFS;
|
|
||||||
applyAccessibilityPrefsForTest(prefsOn);
|
|
||||||
expect(document.documentElement.classList.contains(className)).toBe(true);
|
|
||||||
const prefsOff = {
|
|
||||||
...DEFAULT_ACCESSIBILITY_PREFS,
|
|
||||||
[key]: false,
|
|
||||||
} as typeof DEFAULT_ACCESSIBILITY_PREFS;
|
|
||||||
applyAccessibilityPrefsForTest(prefsOff);
|
|
||||||
expect(document.documentElement.classList.contains(className)).toBe(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("persiste alterações no localStorage", () => {
|
|
||||||
const updated = {
|
|
||||||
...DEFAULT_ACCESSIBILITY_PREFS,
|
|
||||||
highContrast: true,
|
|
||||||
fontSize: 120,
|
|
||||||
};
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
|
||||||
expect(raw).not.toBeNull();
|
|
||||||
const parsed = JSON.parse(raw!);
|
|
||||||
expect(parsed.highContrast).toBe(true);
|
|
||||||
expect(parsed.fontSize).toBe(120);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reset volta ao estado padrão removendo classes e restaurando font-size", () => {
|
|
||||||
const modified = {
|
|
||||||
...DEFAULT_ACCESSIBILITY_PREFS,
|
|
||||||
darkMode: true,
|
|
||||||
highContrast: true,
|
|
||||||
fontSize: 150,
|
|
||||||
};
|
|
||||||
applyAccessibilityPrefsForTest(modified);
|
|
||||||
expect(document.documentElement.classList.contains("dark")).toBe(true);
|
|
||||||
expect(document.documentElement.style.fontSize).toBe("150%");
|
|
||||||
// Aplica defaults
|
|
||||||
applyAccessibilityPrefsForTest(DEFAULT_ACCESSIBILITY_PREFS);
|
|
||||||
expect(document.documentElement.classList.contains("dark")).toBe(false);
|
|
||||||
expect(document.documentElement.classList.contains("high-contrast")).toBe(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
expect(document.documentElement.style.fontSize).toBe("100%");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("pacienteService normalização", () => {
|
|
||||||
it("remove formatação de cpf, telefone e cep em createPatient", async () => {
|
|
||||||
const originalPost = (await import("../services/http")).http.post;
|
|
||||||
const mockPost = vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValue({
|
|
||||||
success: true,
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
id: "abc",
|
|
||||||
full_name: "Fulano",
|
|
||||||
cpf: "12345678909",
|
|
||||||
phone_mobile: "11988887777",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
// Monkey patch simples
|
|
||||||
// Type assertion específica para sobrescrever somente durante o teste
|
|
||||||
(await import("../services/http")).http.post =
|
|
||||||
mockPost as unknown as typeof originalPost;
|
|
||||||
|
|
||||||
await pacienteService.createPatient({
|
|
||||||
nome: "Fulano",
|
|
||||||
cpf: "123.456.789-09",
|
|
||||||
email: "fulano@example.com",
|
|
||||||
telefone: "(11) 98888-7777",
|
|
||||||
endereco: { cep: "01001-000" },
|
|
||||||
});
|
|
||||||
expect(mockPost).toHaveBeenCalledTimes(1);
|
|
||||||
const bodyArg = mockPost.mock.calls[0][1];
|
|
||||||
expect(bodyArg.cpf).toBe("12345678909");
|
|
||||||
expect(bodyArg.phone_mobile).toBe("11988887777");
|
|
||||||
expect(bodyArg.cep).toBe("01001000");
|
|
||||||
|
|
||||||
// restore
|
|
||||||
(await import("../services/http")).http.post = originalPost;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { describe, it, expect, beforeAll } from "vitest";
|
|
||||||
import { render } from "@testing-library/react";
|
|
||||||
import AccessibilityMenu from "../components/AccessibilityMenu";
|
|
||||||
import {
|
|
||||||
DEFAULT_ACCESSIBILITY_PREFS,
|
|
||||||
applyAccessibilityPrefsForTest,
|
|
||||||
} from "../hooks/useAccessibilityPrefs";
|
|
||||||
import axe from "axe-core";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as ReactDOM from "react-dom";
|
|
||||||
import axeReact from "@axe-core/react";
|
|
||||||
|
|
||||||
// Teste básico: montar o menu e verificar ausência de violações "serious" ou "critical"
|
|
||||||
|
|
||||||
describe("AccessibilityMenu a11y", () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
// Mock minimal de speechSynthesis para evitar erros em jsdom
|
|
||||||
// @ts-expect-error mocking
|
|
||||||
global.window.speechSynthesis = {
|
|
||||||
cancel: () => {},
|
|
||||||
speak: () => {},
|
|
||||||
paused: false,
|
|
||||||
pending: false,
|
|
||||||
speaking: false,
|
|
||||||
addEventListener: () => {},
|
|
||||||
removeEventListener: () => {},
|
|
||||||
dispatchEvent: () => true,
|
|
||||||
};
|
|
||||||
axeReact(React, ReactDOM, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("não possui violações sérias/criticas no estado inicial", async () => {
|
|
||||||
applyAccessibilityPrefsForTest(DEFAULT_ACCESSIBILITY_PREFS);
|
|
||||||
const { container } = render(<AccessibilityMenu />);
|
|
||||||
// Espera para que listeners/efeitos terminem
|
|
||||||
await new Promise((r) => setTimeout(r, 20));
|
|
||||||
const results = await axe.run(container, {
|
|
||||||
runOnly: ["wcag2a", "wcag2aa"],
|
|
||||||
});
|
|
||||||
const serious = results.violations.filter((v) =>
|
|
||||||
["serious", "critical"].includes(v.impact || "")
|
|
||||||
);
|
|
||||||
if (serious.length) {
|
|
||||||
console.error(
|
|
||||||
"A11y Violations:",
|
|
||||||
serious.map((v) => ({
|
|
||||||
id: v.id,
|
|
||||||
impact: v.impact,
|
|
||||||
nodes: v.nodes.length,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
expect(serious.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,143 +0,0 @@
|
|||||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
||||||
import puppeteer, { Browser, Page } from "puppeteer";
|
|
||||||
import * as net from "net";
|
|
||||||
import { build, preview } from "vite";
|
|
||||||
|
|
||||||
// Porta padrão do Vite
|
|
||||||
const PORT = 5173;
|
|
||||||
const ORIGIN = `http://127.0.0.1:${PORT}`;
|
|
||||||
|
|
||||||
function waitForPort(port: number, timeoutMs = 20000): Promise<void> {
|
|
||||||
const start = Date.now();
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const tryOnce = () => {
|
|
||||||
const socket = net.connect(port, "127.0.0.1");
|
|
||||||
socket.on("connect", () => {
|
|
||||||
socket.end();
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
socket.on("error", () => {
|
|
||||||
socket.destroy();
|
|
||||||
if (Date.now() - start > timeoutMs)
|
|
||||||
reject(new Error("Timeout aguardando Vite dev server"));
|
|
||||||
else setTimeout(tryOnce, 300);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
tryOnce();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let browser: Browser;
|
|
||||||
let page: Page;
|
|
||||||
let previewServer: Awaited<ReturnType<typeof preview>> | undefined;
|
|
||||||
let built = false;
|
|
||||||
|
|
||||||
async function ensurePreviewServer() {
|
|
||||||
// Se já existe algo na porta (ex dev aberto manualmente), apenas usa
|
|
||||||
try {
|
|
||||||
await waitForPort(PORT, 800);
|
|
||||||
return;
|
|
||||||
} catch {
|
|
||||||
/* inicia preview */
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!built) {
|
|
||||||
await build();
|
|
||||||
built = true;
|
|
||||||
}
|
|
||||||
previewServer = await preview({
|
|
||||||
preview: { port: PORT, host: "127.0.0.1" },
|
|
||||||
server: { middlewareMode: false },
|
|
||||||
} as unknown as Parameters<typeof preview>[0]);
|
|
||||||
await waitForPort(PORT);
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("E2E Accessibility Menu", () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await ensurePreviewServer();
|
|
||||||
browser = await puppeteer.launch({ headless: true });
|
|
||||||
page = await browser.newPage();
|
|
||||||
await page.goto(ORIGIN, { waitUntil: "domcontentloaded" });
|
|
||||||
}, 90000);
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
if (browser) await browser.close();
|
|
||||||
if (previewServer) {
|
|
||||||
// @ts-expect-error acesso interno não tipado
|
|
||||||
const httpServer =
|
|
||||||
previewServer.httpServer || previewServer.server?.httpServer;
|
|
||||||
if (httpServer) httpServer.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it("abre e fecha o diálogo de acessibilidade", async () => {
|
|
||||||
// Botão flutuante
|
|
||||||
await page.waitForSelector('button[aria-label="Menu de Acessibilidade"]', {
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
await page.click('button[aria-label="Menu de Acessibilidade"]');
|
|
||||||
await page.waitForSelector('div[role="dialog"][aria-modal="true"]', {
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
const exists = await page.$('div[role="dialog"][aria-modal="true"]');
|
|
||||||
expect(exists).not.toBeNull();
|
|
||||||
// Pressiona ESC para fechar
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
// Pequeno delay
|
|
||||||
await new Promise((r) => setTimeout(r, 150));
|
|
||||||
const still = await page.$('div[role="dialog"][aria-modal="true"]');
|
|
||||||
expect(still).toBeNull();
|
|
||||||
}, 30000);
|
|
||||||
|
|
||||||
it("ativa dark mode e alto contraste e persiste após reload", async () => {
|
|
||||||
// Abre menu (caso esteja fechado)
|
|
||||||
const trigger = await page.$('button[aria-label="Menu de Acessibilidade"]');
|
|
||||||
if (trigger) {
|
|
||||||
await trigger.click();
|
|
||||||
await page.waitForSelector('div[role="dialog"][aria-modal="true"]', {
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper para clicar botão pelo texto visível interno
|
|
||||||
async function clickToggleByAria(label: string) {
|
|
||||||
const selector = `button[aria-label="${label}"]`;
|
|
||||||
await page.waitForSelector(selector, { timeout: 5000 });
|
|
||||||
await page.click(selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ativa Modo Escuro e Alto Contraste (aria-label fica igual ao label)
|
|
||||||
await clickToggleByAria("Modo Escuro");
|
|
||||||
await clickToggleByAria("Alto Contraste");
|
|
||||||
|
|
||||||
// Verifica classes aplicadas
|
|
||||||
const classesBefore = await page.evaluate(() =>
|
|
||||||
Array.from(document.documentElement.classList)
|
|
||||||
);
|
|
||||||
expect(classesBefore).toContain("dark");
|
|
||||||
expect(classesBefore).toContain("high-contrast");
|
|
||||||
|
|
||||||
// Recarrega página para validar persistência (localStorage -> rehidratação)
|
|
||||||
await page.reload({ waitUntil: "domcontentloaded" });
|
|
||||||
|
|
||||||
const classesAfter = await page.evaluate(() =>
|
|
||||||
Array.from(document.documentElement.classList)
|
|
||||||
);
|
|
||||||
expect(classesAfter).toContain("dark");
|
|
||||||
expect(classesAfter).toContain("high-contrast");
|
|
||||||
|
|
||||||
// (Opcional) Reabre menu e desfaz para não impactar execuções subsequentes
|
|
||||||
const trigger2 = await page.$(
|
|
||||||
'button[aria-label="Menu de Acessibilidade"]'
|
|
||||||
);
|
|
||||||
if (trigger2) {
|
|
||||||
await trigger2.click();
|
|
||||||
await page.waitForSelector('div[role="dialog"][aria-modal="true"]', {
|
|
||||||
timeout: 5000,
|
|
||||||
});
|
|
||||||
await clickToggleByAria("Modo Escuro");
|
|
||||||
await clickToggleByAria("Alto Contraste");
|
|
||||||
await page.keyboard.press("Escape");
|
|
||||||
}
|
|
||||||
}, 45000);
|
|
||||||
});
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { render, fireEvent, act } from "@testing-library/react";
|
|
||||||
import AccessibilityMenu from "../components/AccessibilityMenu";
|
|
||||||
// Diagnostics
|
|
||||||
console.log(
|
|
||||||
"AccessibilityMenu import type:",
|
|
||||||
typeof AccessibilityMenu,
|
|
||||||
AccessibilityMenu && Object.keys(AccessibilityMenu || {})
|
|
||||||
);
|
|
||||||
import {
|
|
||||||
DEFAULT_ACCESSIBILITY_PREFS,
|
|
||||||
applyAccessibilityPrefsForTest,
|
|
||||||
} from "../hooks/useAccessibilityPrefs";
|
|
||||||
|
|
||||||
// Teste sem dependência de axe-core garantindo semântica mínima do diálogo
|
|
||||||
|
|
||||||
describe.skip("AccessibilityMenu semântica (skip – aguardando correção de pipeline React)", () => {
|
|
||||||
it("abre e fecha mantendo atributos ARIA corretos", () => {
|
|
||||||
const Dummy = () => <button data-testid="dummy-test">Dummy</button>;
|
|
||||||
const dummyRender = render(<Dummy />);
|
|
||||||
console.log("DUMMY_HTML", dummyRender.container.innerHTML);
|
|
||||||
dummyRender.unmount();
|
|
||||||
applyAccessibilityPrefsForTest(DEFAULT_ACCESSIBILITY_PREFS);
|
|
||||||
const { getByTestId, queryByRole, container } = render(
|
|
||||||
<AccessibilityMenu />
|
|
||||||
);
|
|
||||||
expect(container).toBeTruthy();
|
|
||||||
console.log("DEBUG_HTML", container.innerHTML);
|
|
||||||
const trigger = getByTestId("a11y-menu-trigger");
|
|
||||||
act(() => {
|
|
||||||
fireEvent.click(trigger);
|
|
||||||
});
|
|
||||||
const dialog = queryByRole("dialog");
|
|
||||||
expect(dialog).not.toBeNull();
|
|
||||||
expect(dialog?.getAttribute("aria-modal")).toBe("true");
|
|
||||||
act(() => {
|
|
||||||
fireEvent.keyDown(document, { key: "Escape" });
|
|
||||||
});
|
|
||||||
expect(queryByRole("dialog")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("aplica foco inicial ao abrir", () => {
|
|
||||||
const { getByTestId, queryByRole, container } = render(
|
|
||||||
<AccessibilityMenu />
|
|
||||||
);
|
|
||||||
expect(container).toBeTruthy();
|
|
||||||
console.log("DEBUG_HTML", container.innerHTML);
|
|
||||||
act(() => {
|
|
||||||
fireEvent.click(getByTestId("a11y-menu-trigger"));
|
|
||||||
});
|
|
||||||
const dialog = queryByRole("dialog") as HTMLElement;
|
|
||||||
expect(dialog).not.toBeNull();
|
|
||||||
const active = document.activeElement as HTMLElement;
|
|
||||||
expect(dialog.contains(active)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as ReactDOMClient from "react-dom/client";
|
|
||||||
|
|
||||||
describe.skip("Manual root render (skip temporário – pipeline React em Vitest quebrado)", () => {
|
|
||||||
it("render via createRoot directly", async () => {
|
|
||||||
const host = document.createElement("div");
|
|
||||||
document.body.appendChild(host);
|
|
||||||
const App = () => <span data-testid="mark">OK</span>;
|
|
||||||
const root = ReactDOMClient.createRoot(host);
|
|
||||||
root.render(<App />);
|
|
||||||
await Promise.resolve();
|
|
||||||
console.log("HOST_HTML_AFTER", host.innerHTML);
|
|
||||||
expect(host.innerHTML).toContain("data-testid");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
import { render } from "@testing-library/react";
|
|
||||||
import * as React from "react";
|
|
||||||
import * as ReactDOMClient from "react-dom/client";
|
|
||||||
|
|
||||||
const Mini = () => <button data-testid="mini">Oi</button>;
|
|
||||||
|
|
||||||
describe.skip("Mini sanity (skip temporário – pipeline React em Vitest quebrado)", () => {
|
|
||||||
it("renderiza componente simples", async () => {
|
|
||||||
console.log("React version:", React.version);
|
|
||||||
console.log("ReactDOMClient keys:", Object.keys(ReactDOMClient));
|
|
||||||
const { getByTestId, container } = render(<Mini />);
|
|
||||||
await Promise.resolve();
|
|
||||||
console.log("MINI_HTML_AFTER_MT", container.innerHTML);
|
|
||||||
expect(container.innerHTML).toContain("button");
|
|
||||||
expect(getByTestId("mini").textContent).toBe("Oi");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
|
||||||
|
|
||||||
describe("Plain DOM sanity", () => {
|
|
||||||
it("manipula DOM sem React", () => {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.id = "x";
|
|
||||||
div.textContent = "hello";
|
|
||||||
document.body.appendChild(div);
|
|
||||||
expect(document.getElementById("x")?.textContent).toBe("hello");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -146,7 +146,7 @@ const AccessibilityMenu: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen((o) => !o)}
|
onClick={() => setIsOpen((o) => !o)}
|
||||||
className="fixed bottom-6 right-6 z-50 bg-blue-600 text-white p-4 rounded-full shadow-lg hover:bg-blue-700 transition-all duration-300 hover:scale-110 focus:outline-none focus:ring-4 focus:ring-blue-300"
|
className="fixed bottom-6 right-6 z-50 bg-blue-600 text-white p-4 rounded-full shadow-lg hover:bg-blue-700 transition-all duration-300 hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||||
aria-label="Menu de Acessibilidade"
|
aria-label="Menu de Acessibilidade"
|
||||||
title="Abrir menu de acessibilidade"
|
title="Abrir menu de acessibilidade"
|
||||||
data-testid="a11y-menu-trigger"
|
data-testid="a11y-menu-trigger"
|
||||||
@ -176,7 +176,7 @@ const AccessibilityMenu: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(false)}
|
onClick={() => setIsOpen(false)}
|
||||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors"
|
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||||
aria-label="Fechar menu"
|
aria-label="Fechar menu"
|
||||||
>
|
>
|
||||||
<X className="w-5 h-5" />
|
<X className="w-5 h-5" />
|
||||||
|
|||||||
324
MEDICONNECT 2/src/components/agenda/AvailabilityManager.tsx
Normal file
324
MEDICONNECT 2/src/components/agenda/AvailabilityManager.tsx
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { availabilityService } from "../../services";
|
||||||
|
import type {
|
||||||
|
DoctorAvailability,
|
||||||
|
Weekday,
|
||||||
|
AppointmentType,
|
||||||
|
} from "../../services/availabilityService";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
doctorId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WEEKDAYS: Weekday[] = [
|
||||||
|
"segunda",
|
||||||
|
"terca",
|
||||||
|
"quarta",
|
||||||
|
"quinta",
|
||||||
|
"sexta",
|
||||||
|
"sabado",
|
||||||
|
"domingo",
|
||||||
|
];
|
||||||
|
|
||||||
|
const AvailabilityManager: React.FC<Props> = ({ doctorId }) => {
|
||||||
|
const [list, setList] = useState<DoctorAvailability[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
weekday: "segunda" as Weekday,
|
||||||
|
start_time: "09:00:00",
|
||||||
|
end_time: "17:00:00",
|
||||||
|
slot_minutes: 30,
|
||||||
|
appointment_type: "presencial" as AppointmentType,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
const canSave = useMemo(() => {
|
||||||
|
return (
|
||||||
|
!!doctorId &&
|
||||||
|
!!form.weekday &&
|
||||||
|
!!form.start_time &&
|
||||||
|
!!form.end_time &&
|
||||||
|
Number(form.slot_minutes) > 0
|
||||||
|
);
|
||||||
|
}, [doctorId, form]);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (!doctorId) return;
|
||||||
|
setLoading(true);
|
||||||
|
const res = await availabilityService.listDoctorActiveAvailability(
|
||||||
|
doctorId
|
||||||
|
);
|
||||||
|
if (res.success && res.data) setList(res.data);
|
||||||
|
else toast.error(res.error || "Erro ao carregar disponibilidades");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [doctorId]);
|
||||||
|
|
||||||
|
async function addAvailability(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!canSave) {
|
||||||
|
toast.error("Preencha todos os campos obrigatórios");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar formato de tempo
|
||||||
|
const timeRegex = /^\d{2}:\d{2}:\d{2}$/;
|
||||||
|
if (!timeRegex.test(form.start_time) || !timeRegex.test(form.end_time)) {
|
||||||
|
toast.error("Formato de horário inválido. Use HH:MM:SS");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validar que o horário de fim é depois do início
|
||||||
|
if (form.start_time >= form.end_time) {
|
||||||
|
toast.error("Horário de fim deve ser posterior ao horário de início");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
doctor_id: doctorId,
|
||||||
|
weekday: form.weekday,
|
||||||
|
start_time: form.start_time,
|
||||||
|
end_time: form.end_time,
|
||||||
|
slot_minutes: Number(form.slot_minutes) || 30,
|
||||||
|
appointment_type: form.appointment_type,
|
||||||
|
active: form.active,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[AvailabilityManager] Enviando payload:", payload);
|
||||||
|
|
||||||
|
setSaving(true);
|
||||||
|
const res = await availabilityService.createAvailability(payload);
|
||||||
|
setSaving(false);
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
toast.success("Disponibilidade criada com sucesso!");
|
||||||
|
setForm((f) => ({ ...f, start_time: "09:00:00", end_time: "17:00:00" }));
|
||||||
|
void load();
|
||||||
|
} else {
|
||||||
|
console.error("[AvailabilityManager] Erro ao criar:", res.error);
|
||||||
|
toast.error(res.error || "Falha ao criar disponibilidade");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleActive(item: DoctorAvailability) {
|
||||||
|
if (!item.id) return;
|
||||||
|
const res = await availabilityService.updateAvailability(item.id, {
|
||||||
|
active: !item.active,
|
||||||
|
});
|
||||||
|
if (res.success) {
|
||||||
|
toast.success("Atualizado");
|
||||||
|
void load();
|
||||||
|
} else toast.error(res.error || "Falha ao atualizar");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(item: DoctorAvailability) {
|
||||||
|
if (!item.id) return;
|
||||||
|
const ok = confirm("Remover disponibilidade?");
|
||||||
|
if (!ok) return;
|
||||||
|
const res = await availabilityService.deleteAvailability(item.id);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success("Removido");
|
||||||
|
void load();
|
||||||
|
} else toast.error(res.error || "Falha ao remover");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-200">
|
||||||
|
{/* Título mais destacado para leitura escaneável */}
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
|
Disponibilidade Semanal
|
||||||
|
</h3>
|
||||||
|
<form onSubmit={addAvailability} className="mb-6">
|
||||||
|
{/* Grid responsivo com espaçamento consistente */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4 mb-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Dia da Semana
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={form.weekday}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({ ...f, weekday: e.target.value as Weekday }))
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
>
|
||||||
|
{WEEKDAYS.map((d) => (
|
||||||
|
<option key={d} value={d}>
|
||||||
|
{d.charAt(0).toUpperCase() + d.slice(1)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Horário Início
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
step={60}
|
||||||
|
value={form.start_time?.slice(0, 5)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({ ...f, start_time: `${e.target.value}:00` }))
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Horário Fim
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
step={60}
|
||||||
|
value={form.end_time?.slice(0, 5)}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({ ...f, end_time: `${e.target.value}:00` }))
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Duração (min)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={5}
|
||||||
|
step={5}
|
||||||
|
value={form.slot_minutes}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({ ...f, slot_minutes: Number(e.target.value) }))
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Tipo Atendimento
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={form.appointment_type}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({
|
||||||
|
...f,
|
||||||
|
appointment_type: e.target.value as AppointmentType,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
>
|
||||||
|
<option value="presencial">Presencial</option>
|
||||||
|
<option value="telemedicina">Telemedicina</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!canSave || saving}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
>
|
||||||
|
{saving ? "Salvando..." : "Adicionar"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||||
|
</div>
|
||||||
|
) : list.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-sm text-gray-500">
|
||||||
|
Nenhuma disponibilidade cadastrada. Use o formulário acima para
|
||||||
|
adicionar horários.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto rounded-lg border border-gray-200">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50 sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Dia da Semana
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Horário Início
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Horário Fim
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Duração
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Tipo
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Ações
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{list.map((item) => (
|
||||||
|
<tr
|
||||||
|
key={item.id}
|
||||||
|
className="odd:bg-white even:bg-gray-50 hover:bg-blue-50/40 transition-colors"
|
||||||
|
>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{item.weekday
|
||||||
|
? item.weekday.charAt(0).toUpperCase() +
|
||||||
|
item.weekday.slice(1)
|
||||||
|
: "-"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{item.start_time?.slice(0, 5)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{item.end_time?.slice(0, 5)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{item.slot_minutes || 30} min
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{item.appointment_type === "presencial"
|
||||||
|
? "Presencial"
|
||||||
|
: "Telemedicina"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap">
|
||||||
|
<button
|
||||||
|
className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||||
|
item.active
|
||||||
|
? "bg-green-100 text-green-800 ring-1 ring-green-600/20 hover:bg-green-200"
|
||||||
|
: "bg-gray-100 text-gray-800 ring-1 ring-gray-600/20 hover:bg-gray-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => void toggleActive(item)}
|
||||||
|
>
|
||||||
|
{item.active ? "Ativo" : "Inativo"}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-red-700 bg-red-50 hover:bg-red-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
onClick={() => void remove(item)}
|
||||||
|
>
|
||||||
|
Remover
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AvailabilityManager;
|
||||||
79
MEDICONNECT 2/src/components/agenda/AvailableSlotsPicker.tsx
Normal file
79
MEDICONNECT 2/src/components/agenda/AvailableSlotsPicker.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { appointmentService } from "../../services";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
doctorId: string;
|
||||||
|
date: string; // YYYY-MM-DD
|
||||||
|
onSelect: (time: string) => void; // HH:MM
|
||||||
|
appointment_type?: "presencial" | "telemedicina";
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvailableSlotsPicker: React.FC<Props> = ({
|
||||||
|
doctorId,
|
||||||
|
date,
|
||||||
|
onSelect,
|
||||||
|
appointment_type,
|
||||||
|
}) => {
|
||||||
|
const [slots, setSlots] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const range = useMemo(() => {
|
||||||
|
if (!date) return null;
|
||||||
|
const start = new Date(`${date}T00:00:00Z`).toISOString();
|
||||||
|
const end = new Date(`${date}T23:59:59Z`).toISOString();
|
||||||
|
return { start, end };
|
||||||
|
}, [date]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchSlots() {
|
||||||
|
if (!doctorId || !range) return;
|
||||||
|
setLoading(true);
|
||||||
|
const res = await appointmentService.getAvailableSlots({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
start_date: range.start,
|
||||||
|
end_date: range.end,
|
||||||
|
appointment_type,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const times = res.data.slots
|
||||||
|
.filter((s) => s.available)
|
||||||
|
.map((s) => s.datetime.slice(11, 16));
|
||||||
|
setSlots(times);
|
||||||
|
} else {
|
||||||
|
toast.error(res.error || "Erro ao buscar horários");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void fetchSlots();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [doctorId, date, appointment_type]);
|
||||||
|
|
||||||
|
if (!date || !doctorId) return null;
|
||||||
|
|
||||||
|
if (loading)
|
||||||
|
return <div className="text-sm text-gray-500">Carregando horários...</div>;
|
||||||
|
|
||||||
|
if (!slots.length)
|
||||||
|
return (
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Nenhum horário disponível para a data selecionada.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-3 md:grid-cols-6 gap-2">
|
||||||
|
{slots.map((t) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
onClick={() => onSelect(t)}
|
||||||
|
className="px-3 py-2 rounded bg-blue-50 hover:bg-blue-100 text-blue-700 text-sm"
|
||||||
|
>
|
||||||
|
{t}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AvailableSlotsPicker;
|
||||||
459
MEDICONNECT 2/src/components/agenda/DoctorCalendar.tsx
Normal file
459
MEDICONNECT 2/src/components/agenda/DoctorCalendar.tsx
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
// UI/UX refresh: melhorias visuais e de acessibilidade sem alterar a lógica
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { appointmentService } from "../../services";
|
||||||
|
import pacienteService from "../../services/pacienteService";
|
||||||
|
import type { Appointment } from "../../services/appointmentService";
|
||||||
|
import { ChevronLeft, ChevronRight, X } from "lucide-react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
doctorId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CalendarDay {
|
||||||
|
date: Date;
|
||||||
|
dateStr: string;
|
||||||
|
isCurrentMonth: boolean;
|
||||||
|
isToday: boolean;
|
||||||
|
appointments: Appointment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const WEEKDAYS = ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"];
|
||||||
|
const MONTHS = [
|
||||||
|
"Janeiro",
|
||||||
|
"Fevereiro",
|
||||||
|
"Março",
|
||||||
|
"Abril",
|
||||||
|
"Maio",
|
||||||
|
"Junho",
|
||||||
|
"Julho",
|
||||||
|
"Agosto",
|
||||||
|
"Setembro",
|
||||||
|
"Outubro",
|
||||||
|
"Novembro",
|
||||||
|
"Dezembro",
|
||||||
|
];
|
||||||
|
|
||||||
|
const DoctorCalendar: React.FC<Props> = ({ doctorId }) => {
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedDay, setSelectedDay] = useState<CalendarDay | null>(null);
|
||||||
|
const [patientsById, setPatientsById] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (doctorId) {
|
||||||
|
loadAppointments();
|
||||||
|
loadPatients();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [doctorId, currentDate]);
|
||||||
|
|
||||||
|
async function loadAppointments() {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await appointmentService.listAppointments();
|
||||||
|
if (response.success && response.data) {
|
||||||
|
// Filtrar apenas do médico selecionado
|
||||||
|
const filtered = response.data.filter(
|
||||||
|
(apt) => apt.doctor_id === doctorId
|
||||||
|
);
|
||||||
|
setAppointments(filtered);
|
||||||
|
} else {
|
||||||
|
toast.error(response.error || "Erro ao carregar agendamentos");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar agendamentos:", error);
|
||||||
|
toast.error("Erro ao carregar agendamentos");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPatients() {
|
||||||
|
// Carrega pacientes para mapear nome pelo id (render amigável)
|
||||||
|
try {
|
||||||
|
const res = await pacienteService.listPatients();
|
||||||
|
if (res && Array.isArray(res.data)) {
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
for (const p of res.data) {
|
||||||
|
if (p?.id) map[p.id] = p.nome || p.email || p.cpf || p.id;
|
||||||
|
}
|
||||||
|
setPatientsById(map);
|
||||||
|
} else if (
|
||||||
|
res &&
|
||||||
|
typeof (res as unknown) === "object" &&
|
||||||
|
(
|
||||||
|
res as {
|
||||||
|
data?: {
|
||||||
|
data?: Array<{
|
||||||
|
id?: string;
|
||||||
|
nome?: string;
|
||||||
|
email?: string;
|
||||||
|
cpf?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).data?.data
|
||||||
|
) {
|
||||||
|
const list =
|
||||||
|
(
|
||||||
|
res as {
|
||||||
|
data?: {
|
||||||
|
data?: Array<{
|
||||||
|
id?: string;
|
||||||
|
nome?: string;
|
||||||
|
email?: string;
|
||||||
|
cpf?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).data?.data || [];
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
for (const p of list) {
|
||||||
|
if (p?.id) map[p.id] = p.nome || p.email || p.cpf || p.id;
|
||||||
|
}
|
||||||
|
setPatientsById(map);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// silencioso; não bloqueia calendário
|
||||||
|
} finally {
|
||||||
|
/* no-op */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPatientName(id?: string) {
|
||||||
|
if (!id) return "";
|
||||||
|
return patientsById[id] || id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCalendarDays(): CalendarDay[] {
|
||||||
|
const year = currentDate.getFullYear();
|
||||||
|
const month = currentDate.getMonth();
|
||||||
|
|
||||||
|
// Primeiro dia do mês
|
||||||
|
const firstDay = new Date(year, month, 1);
|
||||||
|
// Último dia do mês
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
|
||||||
|
// Dia da semana do primeiro dia (0 = domingo)
|
||||||
|
const startingDayOfWeek = firstDay.getDay();
|
||||||
|
|
||||||
|
const days: CalendarDay[] = [];
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Adicionar dias do mês anterior
|
||||||
|
const prevMonthLastDay = new Date(year, month, 0);
|
||||||
|
for (let i = startingDayOfWeek - 1; i >= 0; i--) {
|
||||||
|
const date = new Date(year, month - 1, prevMonthLastDay.getDate() - i);
|
||||||
|
const dateStr = formatDateISO(date);
|
||||||
|
days.push({
|
||||||
|
date,
|
||||||
|
dateStr,
|
||||||
|
isCurrentMonth: false,
|
||||||
|
isToday: false,
|
||||||
|
appointments: getAppointmentsForDate(dateStr),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adicionar dias do mês atual
|
||||||
|
for (let day = 1; day <= lastDay.getDate(); day++) {
|
||||||
|
const date = new Date(year, month, day);
|
||||||
|
const dateStr = formatDateISO(date);
|
||||||
|
const isToday = date.getTime() === today.getTime();
|
||||||
|
days.push({
|
||||||
|
date,
|
||||||
|
dateStr,
|
||||||
|
isCurrentMonth: true,
|
||||||
|
isToday,
|
||||||
|
appointments: getAppointmentsForDate(dateStr),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adicionar dias do próximo mês para completar a grade
|
||||||
|
const remainingDays = 42 - days.length; // 6 semanas x 7 dias
|
||||||
|
for (let day = 1; day <= remainingDays; day++) {
|
||||||
|
const date = new Date(year, month + 1, day);
|
||||||
|
const dateStr = formatDateISO(date);
|
||||||
|
days.push({
|
||||||
|
date,
|
||||||
|
dateStr,
|
||||||
|
isCurrentMonth: false,
|
||||||
|
isToday: false,
|
||||||
|
appointments: getAppointmentsForDate(dateStr),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateISO(date: Date): string {
|
||||||
|
return date.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppointmentsForDate(dateStr: string): Appointment[] {
|
||||||
|
return appointments.filter((apt) => {
|
||||||
|
if (!apt.scheduled_at) return false;
|
||||||
|
const aptDate = apt.scheduled_at.split("T")[0];
|
||||||
|
return aptDate === dateStr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousMonth() {
|
||||||
|
setCurrentDate(
|
||||||
|
new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextMonth() {
|
||||||
|
setCurrentDate(
|
||||||
|
new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToToday() {
|
||||||
|
setCurrentDate(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status?: string): string {
|
||||||
|
switch (status) {
|
||||||
|
case "confirmed":
|
||||||
|
return "bg-blue-500";
|
||||||
|
case "completed":
|
||||||
|
return "bg-green-500";
|
||||||
|
case "cancelled":
|
||||||
|
return "bg-red-500";
|
||||||
|
case "no_show":
|
||||||
|
return "bg-gray-500";
|
||||||
|
case "checked_in":
|
||||||
|
return "bg-purple-500";
|
||||||
|
case "in_progress":
|
||||||
|
return "bg-yellow-500";
|
||||||
|
default:
|
||||||
|
return "bg-orange-500"; // requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusLabel(status?: string): string {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
requested: "Solicitado",
|
||||||
|
confirmed: "Confirmado",
|
||||||
|
checked_in: "Check-in",
|
||||||
|
in_progress: "Em andamento",
|
||||||
|
completed: "Concluído",
|
||||||
|
cancelled: "Cancelado",
|
||||||
|
no_show: "Faltou",
|
||||||
|
};
|
||||||
|
return labels[status || "requested"] || status || "Solicitado";
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarDays = getCalendarDays();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-200">
|
||||||
|
{/* Cabeçalho modernizado: melhor contraste, foco e navegação */}
|
||||||
|
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between mb-6">
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900">
|
||||||
|
Calendário de Consultas
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={goToToday}
|
||||||
|
className="px-3 py-1.5 text-sm font-medium text-blue-700 bg-blue-50 hover:bg-blue-100 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
>
|
||||||
|
Hoje
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={previousMonth}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
aria-label="Mês anterior"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<span className="text-lg font-medium min-w-[200px] text-center">
|
||||||
|
{MONTHS[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={nextMonth}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
aria-label="Próximo mês"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-600" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Cabeçalhos dos dias da semana */}
|
||||||
|
<div className="grid grid-cols-7 gap-1 mb-2">
|
||||||
|
{WEEKDAYS.map((day) => (
|
||||||
|
<div
|
||||||
|
key={day}
|
||||||
|
className="text-center text-sm font-semibold text-gray-600 py-2"
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid do calendário com células interativas acessíveis */}
|
||||||
|
<div className="grid grid-cols-7 gap-1">
|
||||||
|
{calendarDays.map((day, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
// UI: estados visuais modernizados, mantendo a interação por clique
|
||||||
|
className={`group min-h-[110px] border rounded-lg p-2 transition-colors ${
|
||||||
|
day.isCurrentMonth
|
||||||
|
? "bg-white border-gray-200"
|
||||||
|
: "bg-gray-50 border-gray-100"
|
||||||
|
} ${day.isToday ? "ring-2 ring-blue-500" : ""} ${
|
||||||
|
day.appointments.length > 0
|
||||||
|
? "cursor-pointer hover:bg-blue-50"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
onClick={() =>
|
||||||
|
day.appointments.length > 0 && setSelectedDay(day)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Número do dia com destaque para hoje */}
|
||||||
|
<div
|
||||||
|
className={`text-sm font-medium mb-2 ${
|
||||||
|
day.isCurrentMonth ? "text-gray-900" : "text-gray-400"
|
||||||
|
} ${day.isToday ? "text-blue-600 font-bold" : ""}`}
|
||||||
|
>
|
||||||
|
{day.date.getDate()}
|
||||||
|
</div>
|
||||||
|
{/* Chips de horários com cores por status */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
{day.appointments.slice(0, 3).map((apt, idx) => (
|
||||||
|
<div
|
||||||
|
key={apt.id || idx}
|
||||||
|
className={`text-xs px-1 py-0.5 rounded text-white ${getStatusColor(
|
||||||
|
apt.status
|
||||||
|
)} truncate`}
|
||||||
|
title={`${apt.scheduled_at?.slice(
|
||||||
|
11,
|
||||||
|
16
|
||||||
|
)} - ${getStatusLabel(apt.status)}`}
|
||||||
|
>
|
||||||
|
{apt.scheduled_at?.slice(11, 16)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{day.appointments.length > 3 && (
|
||||||
|
<div className="text-xs text-gray-500 font-medium">
|
||||||
|
+{day.appointments.length - 3} mais
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal de detalhes do dia - melhorado com acessibilidade e botão de fechar */}
|
||||||
|
{selectedDay && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-[2px] flex items-center justify-center z-50"
|
||||||
|
onClick={() => setSelectedDay(null)}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Consultas do dia selecionado"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-white rounded-xl shadow-2xl max-w-2xl w-full mx-4 max-h-[80vh] overflow-auto ring-1 ring-black/5"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900">
|
||||||
|
Consultas de{" "}
|
||||||
|
{selectedDay.date.toLocaleDateString("pt-BR", {
|
||||||
|
weekday: "long",
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedDay(null)}
|
||||||
|
aria-label="Fechar"
|
||||||
|
className="p-2 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-3">
|
||||||
|
{selectedDay.appointments.length === 0 ? (
|
||||||
|
<p className="text-gray-500 text-center py-4">
|
||||||
|
Nenhuma consulta agendada para este dia.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
selectedDay.appointments.map((apt) => (
|
||||||
|
<div
|
||||||
|
key={apt.id}
|
||||||
|
className="border border-gray-200 rounded-lg p-4 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="font-medium text-gray-900">
|
||||||
|
{apt.scheduled_at?.slice(11, 16)}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`px-2 py-0.5 rounded-full text-xs font-medium text-white ${getStatusColor(
|
||||||
|
apt.status
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{getStatusLabel(apt.status)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600 space-y-1">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Paciente:</span>{" "}
|
||||||
|
{getPatientName(apt.patient_id)}
|
||||||
|
</div>
|
||||||
|
{apt.appointment_type && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Tipo:</span>{" "}
|
||||||
|
{apt.appointment_type === "presencial"
|
||||||
|
? "Presencial"
|
||||||
|
: "Telemedicina"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{apt.chief_complaint && (
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">Queixa:</span>{" "}
|
||||||
|
{apt.chief_complaint}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="p-6 border-t border-gray-200 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedDay(null)}
|
||||||
|
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
>
|
||||||
|
Fechar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DoctorCalendar;
|
||||||
265
MEDICONNECT 2/src/components/agenda/ExceptionsManager.tsx
Normal file
265
MEDICONNECT 2/src/components/agenda/ExceptionsManager.tsx
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { exceptionService } from "../../services";
|
||||||
|
import type {
|
||||||
|
DoctorException,
|
||||||
|
ExceptionKind,
|
||||||
|
} from "../../services/exceptionService";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
doctorId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ExceptionsManager: React.FC<Props> = ({ doctorId }) => {
|
||||||
|
const [list, setList] = useState<DoctorException[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
date: "",
|
||||||
|
start_time: "",
|
||||||
|
end_time: "",
|
||||||
|
kind: "bloqueio" as ExceptionKind,
|
||||||
|
reason: "",
|
||||||
|
});
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
if (!doctorId) return;
|
||||||
|
setLoading(true);
|
||||||
|
const res = await exceptionService.listExceptions({ doctor_id: doctorId });
|
||||||
|
if (res.success && res.data) setList(res.data);
|
||||||
|
else toast.error(res.error || "Erro ao carregar exceções");
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [doctorId]);
|
||||||
|
|
||||||
|
async function addException(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!doctorId || !form.date || !form.kind) {
|
||||||
|
toast.error("Preencha data e tipo");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSaving(true);
|
||||||
|
const res = await exceptionService.createException({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
date: form.date,
|
||||||
|
start_time: form.start_time || undefined,
|
||||||
|
end_time: form.end_time || undefined,
|
||||||
|
kind: form.kind,
|
||||||
|
reason: form.reason || undefined,
|
||||||
|
});
|
||||||
|
setSaving(false);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success("Exceção criada");
|
||||||
|
setForm({
|
||||||
|
date: "",
|
||||||
|
start_time: "",
|
||||||
|
end_time: "",
|
||||||
|
kind: "bloqueio",
|
||||||
|
reason: "",
|
||||||
|
});
|
||||||
|
void load();
|
||||||
|
} else toast.error(res.error || "Falha ao criar");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(item: DoctorException) {
|
||||||
|
if (!item.id) return;
|
||||||
|
const ok = confirm("Remover exceção?");
|
||||||
|
if (!ok) return;
|
||||||
|
const res = await exceptionService.deleteException(item.id);
|
||||||
|
if (res.success) {
|
||||||
|
toast.success("Removida");
|
||||||
|
void load();
|
||||||
|
} else toast.error(res.error || "Falha ao remover");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white rounded-xl shadow-md p-6 border border-gray-200">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||||
|
Exceções (Bloqueios/Liberações)
|
||||||
|
</h3>
|
||||||
|
<form onSubmit={addException} className="mb-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4 mb-4">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Data
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={form.date}
|
||||||
|
onChange={(e) => setForm((f) => ({ ...f, date: e.target.value }))}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Início{" "}
|
||||||
|
<span className="text-gray-400 font-normal">(opcional)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={form.start_time}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({ ...f, start_time: e.target.value }))
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
placeholder="Dia todo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Fim <span className="text-gray-400 font-normal">(opcional)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={form.end_time}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({ ...f, end_time: e.target.value }))
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
placeholder="Dia todo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Tipo
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={form.kind}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({
|
||||||
|
...f,
|
||||||
|
kind: e.target.value as ExceptionKind,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
>
|
||||||
|
<option value="bloqueio">Bloqueio</option>
|
||||||
|
<option value="liberacao">Liberação</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:col-span-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Motivo{" "}
|
||||||
|
<span className="text-gray-400 font-normal">(opcional)</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={form.reason}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm((f) => ({ ...f, reason: e.target.value }))
|
||||||
|
}
|
||||||
|
className="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
placeholder="Ex.: Férias, Reunião, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
>
|
||||||
|
{saving ? "Salvando..." : "Adicionar"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||||
|
</div>
|
||||||
|
) : list.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-sm text-gray-500">
|
||||||
|
Nenhuma exceção cadastrada. Use o formulário acima para
|
||||||
|
bloquear/liberar horários.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto rounded-lg border border-gray-200">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50 sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Data
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Horário Início
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Horário Fim
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Tipo
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Motivo
|
||||||
|
</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Ações
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{list.map((item) => (
|
||||||
|
<tr
|
||||||
|
key={item.id}
|
||||||
|
className="odd:bg-white even:bg-gray-50 hover:bg-blue-50/40 transition-colors"
|
||||||
|
>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
|
{item.date
|
||||||
|
? new Date(item.date + "T00:00:00").toLocaleDateString(
|
||||||
|
"pt-BR"
|
||||||
|
)
|
||||||
|
: "—"}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{item.start_time ? (
|
||||||
|
item.start_time.slice(0, 5)
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">Dia todo</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{item.end_time ? (
|
||||||
|
item.end_time.slice(0, 5)
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400">Dia todo</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
className={`px-3 py-1 rounded-full text-xs font-medium ring-1 ${
|
||||||
|
item.kind === "bloqueio"
|
||||||
|
? "bg-red-50 text-red-700 ring-red-600/20"
|
||||||
|
: "bg-green-50 text-green-700 ring-green-600/20"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.kind === "bloqueio" ? "Bloqueio" : "Liberação"}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 text-sm text-gray-900">
|
||||||
|
{item.reason || <span className="text-gray-400">—</span>}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
|
<button
|
||||||
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-red-700 bg-red-50 hover:bg-red-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
onClick={() => void remove(item)}
|
||||||
|
>
|
||||||
|
Remover
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExceptionsManager;
|
||||||
428
MEDICONNECT 2/src/components/agenda/ScheduleAppointmentModal.tsx
Normal file
428
MEDICONNECT 2/src/components/agenda/ScheduleAppointmentModal.tsx
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
// UI/UX: adiciona refs e ícones para melhorar acessibilidade e feedback visual
|
||||||
|
import React, { useState, useEffect, useMemo, useRef } from "react";
|
||||||
|
import {
|
||||||
|
Calendar as CalendarIcon,
|
||||||
|
Clock,
|
||||||
|
Loader2,
|
||||||
|
Stethoscope,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { appointmentService } from "../../services";
|
||||||
|
import medicoService, { type Medico } from "../../services/medicoService";
|
||||||
|
import pacienteService from "../../services/pacienteService";
|
||||||
|
import type { Paciente as PacienteModel } from "../../services/pacienteService";
|
||||||
|
import AvailableSlotsPicker from "./AvailableSlotsPicker";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
patientId?: string; // opcional: quando não informado, seleciona paciente no modal
|
||||||
|
patientName?: string; // opcional
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ScheduleAppointmentModal: React.FC<Props> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
patientId,
|
||||||
|
patientName,
|
||||||
|
onSuccess,
|
||||||
|
}) => {
|
||||||
|
const [doctors, setDoctors] = useState<Medico[]>([]);
|
||||||
|
const [loadingDoctors, setLoadingDoctors] = useState(false);
|
||||||
|
const [patients, setPatients] = useState<PacienteModel[]>([]);
|
||||||
|
const [loadingPatients, setLoadingPatients] = useState(false);
|
||||||
|
|
||||||
|
const [selectedDoctorId, setSelectedDoctorId] = useState("");
|
||||||
|
const [selectedDate, setSelectedDate] = useState("");
|
||||||
|
const [selectedTime, setSelectedTime] = useState("");
|
||||||
|
const [appointmentType, setAppointmentType] = useState<
|
||||||
|
"presencial" | "telemedicina"
|
||||||
|
>("presencial");
|
||||||
|
const [reason, setReason] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedPatientId, setSelectedPatientId] = useState("");
|
||||||
|
const [selectedPatientName, setSelectedPatientName] = useState("");
|
||||||
|
|
||||||
|
// A11y & UX: refs para foco inicial e fechamento via overlay/ESC
|
||||||
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const dialogRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const firstFieldRef = useRef<HTMLSelectElement | null>(null);
|
||||||
|
const closeBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
// A11y: IDs para aria-labelledby/aria-describedby
|
||||||
|
const titleId = useMemo(
|
||||||
|
() => `schedule-modal-title-${patientId ?? "novo"}`,
|
||||||
|
[patientId]
|
||||||
|
);
|
||||||
|
const descId = useMemo(
|
||||||
|
() => `schedule-modal-desc-${patientId ?? "novo"}`,
|
||||||
|
[patientId]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
loadDoctors();
|
||||||
|
if (!patientId) {
|
||||||
|
loadPatients();
|
||||||
|
} else {
|
||||||
|
// Garantir estados internos alinhados com props
|
||||||
|
setSelectedPatientId(patientId);
|
||||||
|
setSelectedPatientName(patientName || "");
|
||||||
|
}
|
||||||
|
// UX: foco no primeiro campo quando abrir
|
||||||
|
setTimeout(() => firstFieldRef.current?.focus(), 0);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
async function loadDoctors() {
|
||||||
|
setLoadingDoctors(true);
|
||||||
|
const res = await medicoService.listarMedicos();
|
||||||
|
setLoadingDoctors(false);
|
||||||
|
if (res.success && res.data) {
|
||||||
|
setDoctors(res.data.data); // res.data é MedicoListResponse, res.data.data é Medico[]
|
||||||
|
} else {
|
||||||
|
toast.error("Erro ao carregar médicos");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPatients() {
|
||||||
|
setLoadingPatients(true);
|
||||||
|
try {
|
||||||
|
const res = await pacienteService.listPatients();
|
||||||
|
setLoadingPatients(false);
|
||||||
|
if (res && Array.isArray(res.data)) {
|
||||||
|
setPatients(res.data);
|
||||||
|
} else if (
|
||||||
|
res &&
|
||||||
|
typeof (res as unknown) === "object" &&
|
||||||
|
(res as { data?: { data?: PacienteModel[] } }).data?.data
|
||||||
|
) {
|
||||||
|
// fallback caso formato mude (evita any explícito)
|
||||||
|
setPatients(
|
||||||
|
(res as { data?: { data?: PacienteModel[] } }).data?.data || []
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error("Erro ao carregar pacientes");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setLoadingPatients(false);
|
||||||
|
toast.error("Erro ao carregar pacientes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const finalPatientId = patientId || selectedPatientId;
|
||||||
|
if (
|
||||||
|
!selectedDoctorId ||
|
||||||
|
!selectedDate ||
|
||||||
|
!selectedTime ||
|
||||||
|
!finalPatientId
|
||||||
|
) {
|
||||||
|
toast.error("Preencha médico, data e horário");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const datetime = `${selectedDate}T${selectedTime}:00`;
|
||||||
|
|
||||||
|
const res = await appointmentService.createAppointment({
|
||||||
|
patient_id: finalPatientId,
|
||||||
|
doctor_id: selectedDoctorId,
|
||||||
|
scheduled_at: datetime,
|
||||||
|
appointment_type: appointmentType,
|
||||||
|
chief_complaint: reason || undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
toast.success("Agendamento criado com sucesso!");
|
||||||
|
onSuccess?.();
|
||||||
|
handleClose();
|
||||||
|
} else {
|
||||||
|
toast.error(res.error || "Erro ao criar agendamento");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
setSelectedDoctorId("");
|
||||||
|
setSelectedDate("");
|
||||||
|
setSelectedTime("");
|
||||||
|
setAppointmentType("presencial");
|
||||||
|
setReason("");
|
||||||
|
setSelectedPatientId("");
|
||||||
|
setSelectedPatientName("");
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const selectedDoctor = doctors.find((d) => d.id === selectedDoctorId);
|
||||||
|
const patientPreselected = !!patientId;
|
||||||
|
const effectivePatientName = patientPreselected
|
||||||
|
? patientName
|
||||||
|
: selectedPatientName ||
|
||||||
|
(patients.find((p) => p.id === selectedPatientId)?.nome ?? "");
|
||||||
|
|
||||||
|
// UX: handlers para ESC e clique fora
|
||||||
|
function onKeyDown(e: React.KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onOverlayClick(e: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
if (e.target === overlayRef.current) handleClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={overlayRef}
|
||||||
|
className="fixed inset-0 bg-black/50 backdrop-blur-[2px] flex items-center justify-center z-50 p-4"
|
||||||
|
onClick={onOverlayClick}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby={titleId}
|
||||||
|
aria-describedby={descId}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={dialogRef}
|
||||||
|
className="bg-white rounded-xl shadow-2xl max-w-3xl w-full max-h-[90vh] overflow-y-auto ring-1 ring-black/5 animate-in fade-in zoom-in duration-150"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="sticky top-0 bg-gradient-to-r from-blue-50 to-white border-b px-6 py-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Stethoscope className="w-5 h-5 text-blue-600" aria-hidden="true" />
|
||||||
|
<h2
|
||||||
|
id={titleId}
|
||||||
|
className="text-lg md:text-xl font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Agendar consulta •{" "}
|
||||||
|
<span className="font-normal text-gray-700">
|
||||||
|
{effectivePatientName}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
ref={closeBtnRef}
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="Fechar modal de agendamento"
|
||||||
|
className="inline-flex items-center justify-center rounded-md p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id={descId} className="sr-only">
|
||||||
|
Selecione o médico, a data, o tipo de consulta e um horário disponível
|
||||||
|
para criar um novo agendamento.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="p-6 space-y-6"
|
||||||
|
aria-busy={loading}
|
||||||
|
>
|
||||||
|
{/* Paciente (apenas quando não veio por props) */}
|
||||||
|
{!patientPreselected && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Paciente *
|
||||||
|
</label>
|
||||||
|
{loadingPatients ? (
|
||||||
|
// Skeleton para carregamento de pacientes
|
||||||
|
<div
|
||||||
|
className="h-10 w-full rounded-lg bg-gray-100 animate-pulse"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label="Carregando pacientes"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
value={selectedPatientId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedPatientId(e.target.value);
|
||||||
|
const p = patients.find((px) => px.id === e.target.value);
|
||||||
|
setSelectedPatientName(p?.nome || "");
|
||||||
|
}}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">-- Selecione um paciente --</option>
|
||||||
|
{patients.map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.nome} {p.cpf ? `- ${p.cpf}` : ""}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Médico */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Médico{" "}
|
||||||
|
<span className="text-red-500" aria-hidden="true">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{loadingDoctors ? (
|
||||||
|
<div
|
||||||
|
className="h-10 w-full rounded-lg bg-gray-100 animate-pulse"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label="Carregando médicos"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
value={selectedDoctorId}
|
||||||
|
onChange={(e) => setSelectedDoctorId(e.target.value)}
|
||||||
|
ref={firstFieldRef}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">-- Selecione um médico --</option>
|
||||||
|
{doctors.map((doc) => (
|
||||||
|
<option key={doc.id} value={doc.id}>
|
||||||
|
{doc.nome} - {doc.especialidade}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
{selectedDoctor && (
|
||||||
|
<div className="mt-2 text-sm text-gray-600">
|
||||||
|
CRM: {selectedDoctor.crm}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Data */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Data{" "}
|
||||||
|
<span className="text-red-500" aria-hidden="true">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={selectedDate}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSelectedDate(e.target.value);
|
||||||
|
setSelectedTime(""); // Limpa o horário ao mudar a data
|
||||||
|
}}
|
||||||
|
min={new Date().toISOString().split("T")[0]}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500 flex items-center gap-1">
|
||||||
|
<CalendarIcon className="w-3.5 h-3.5" /> Selecione uma data para
|
||||||
|
ver os horários disponíveis.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tipo de Consulta */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Tipo de Consulta{" "}
|
||||||
|
<span className="text-red-500" aria-hidden="true">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={appointmentType}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAppointmentType(
|
||||||
|
e.target.value as "presencial" | "telemedicina"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="presencial">Presencial</option>
|
||||||
|
<option value="telemedicina">Telemedicina</option>
|
||||||
|
</select>
|
||||||
|
<p className="mt-1 text-xs text-gray-500 flex items-center gap-1">
|
||||||
|
<Clock className="w-3.5 h-3.5" /> O tipo de consulta pode alterar
|
||||||
|
a disponibilidade de horários.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Horários Disponíveis */}
|
||||||
|
{selectedDoctorId && selectedDate && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Horários Disponíveis *
|
||||||
|
</label>
|
||||||
|
<AvailableSlotsPicker
|
||||||
|
doctorId={selectedDoctorId}
|
||||||
|
date={selectedDate}
|
||||||
|
appointment_type={appointmentType}
|
||||||
|
onSelect={(time) => setSelectedTime(time)}
|
||||||
|
/>
|
||||||
|
{selectedTime && (
|
||||||
|
<div className="mt-2 inline-flex items-center gap-2 rounded-md bg-green-50 px-3 py-1.5 text-sm text-green-700 ring-1 ring-green-600/20">
|
||||||
|
<span aria-hidden>✓</span> Horário selecionado:{" "}
|
||||||
|
<span className="font-semibold">{selectedTime}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Motivo */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Motivo da Consulta (opcional)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={reason}
|
||||||
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
|
placeholder="Ex: Consulta de rotina, dor de cabeça..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Botões */}
|
||||||
|
<div className="flex flex-col-reverse sm:flex-row gap-3 pt-4 border-t">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
disabled={
|
||||||
|
loading ||
|
||||||
|
!selectedDoctorId ||
|
||||||
|
!selectedDate ||
|
||||||
|
!selectedTime ||
|
||||||
|
(!patientPreselected && !selectedPatientId)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" aria-hidden />{" "}
|
||||||
|
Agendando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Confirmar Agendamento"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScheduleAppointmentModal;
|
||||||
@ -62,14 +62,19 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
|||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
setLoadingLists(true);
|
setLoadingLists(true);
|
||||||
const [pacs, medsResp] = await Promise.all([
|
const [pacsResp, medsResp] = await Promise.all([
|
||||||
listPatients({ limit: 500 }).catch(() => []),
|
listPatients().catch(() => ({
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
per_page: 0,
|
||||||
|
})),
|
||||||
medicoService
|
medicoService
|
||||||
.listarMedicos()
|
.listarMedicos()
|
||||||
.catch(() => ({ success: false, data: undefined })),
|
.catch(() => ({ success: false, data: undefined })),
|
||||||
]);
|
]);
|
||||||
if (!active) return;
|
if (!active) return;
|
||||||
setPacientes(pacs);
|
setPacientes(pacsResp.data);
|
||||||
if (medsResp && medsResp.success && medsResp.data) {
|
if (medsResp && medsResp.success && medsResp.data) {
|
||||||
setMedicos(medsResp.data.data);
|
setMedicos(medsResp.data.data);
|
||||||
}
|
}
|
||||||
@ -229,7 +234,7 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
|||||||
>
|
>
|
||||||
<option value="">Selecione...</option>
|
<option value="">Selecione...</option>
|
||||||
{pacientes.map((p) => (
|
{pacientes.map((p) => (
|
||||||
<option key={p._id} value={p._id}>
|
<option key={p.id} value={p.id}>
|
||||||
{p.nome}
|
{p.nome}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -71,9 +71,9 @@ const ConsultationList: React.FC<ConsultationListProps> = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto rounded-lg border border-gray-200">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50 sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Data/Hora
|
Data/Hora
|
||||||
@ -128,11 +128,14 @@ const ConsultationList: React.FC<ConsultationListProps> = ({
|
|||||||
className: "bg-gray-100 text-gray-800",
|
className: "bg-gray-100 text-gray-800",
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<tr key={c.id} className="hover:bg-gray-50">
|
<tr
|
||||||
|
key={c.id}
|
||||||
|
className="odd:bg-white even:bg-gray-50 hover:bg-blue-50/40 transition-colors"
|
||||||
|
>
|
||||||
<td className="px-4 py-3 text-sm">
|
<td className="px-4 py-3 text-sm">
|
||||||
<button
|
<button
|
||||||
onClick={() => onSelect?.(c.id)}
|
onClick={() => onSelect?.(c.id)}
|
||||||
className="text-left hover:underline"
|
className="text-left hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60 rounded"
|
||||||
>
|
>
|
||||||
{formatDateTime(c.dataHora)}
|
{formatDateTime(c.dataHora)}
|
||||||
</button>
|
</button>
|
||||||
@ -157,7 +160,9 @@ const ConsultationList: React.FC<ConsultationListProps> = ({
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm">
|
<td className="px-4 py-3 text-sm">
|
||||||
<span
|
<span
|
||||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${s.className}`}
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ring-1 ring-inset ${s.className
|
||||||
|
.replace("bg-", "bg-")
|
||||||
|
.replace("text-", "text-")}`}
|
||||||
>
|
>
|
||||||
{s.label}
|
{s.label}
|
||||||
</span>
|
</span>
|
||||||
@ -169,7 +174,7 @@ const ConsultationList: React.FC<ConsultationListProps> = ({
|
|||||||
c.status === "agendada" && (
|
c.status === "agendada" && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onChangeStatus?.(c.id, "confirmada")}
|
onClick={() => onChangeStatus?.(c.id, "confirmada")}
|
||||||
className="inline-flex items-center text-green-600 hover:text-green-800 text-xs"
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-green-700 bg-green-50 hover:bg-green-100 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
<Check className="w-4 h-4 mr-1" /> Confirmar
|
<Check className="w-4 h-4 mr-1" /> Confirmar
|
||||||
</button>
|
</button>
|
||||||
@ -177,7 +182,7 @@ const ConsultationList: React.FC<ConsultationListProps> = ({
|
|||||||
{allowStatusChange && c.status === "confirmada" && (
|
{allowStatusChange && c.status === "confirmada" && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onChangeStatus?.(c.id, "realizada")}
|
onClick={() => onChangeStatus?.(c.id, "realizada")}
|
||||||
className="inline-flex items-center text-blue-600 hover:text-blue-800 text-xs"
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-blue-700 bg-blue-50 hover:bg-blue-100 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
<CalendarCheck className="w-4 h-4 mr-1" /> Realizar
|
<CalendarCheck className="w-4 h-4 mr-1" /> Realizar
|
||||||
</button>
|
</button>
|
||||||
@ -186,7 +191,7 @@ const ConsultationList: React.FC<ConsultationListProps> = ({
|
|||||||
["agendada", "confirmada"].includes(c.status) && (
|
["agendada", "confirmada"].includes(c.status) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onChangeStatus?.(c.id, "cancelada")}
|
onClick={() => onChangeStatus?.(c.id, "cancelada")}
|
||||||
className="inline-flex items-center text-red-600 hover:text-red-800 text-xs"
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-red-700 bg-red-50 hover:bg-red-100 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
<X className="w-4 h-4 mr-1" /> Cancelar
|
<X className="w-4 h-4 mr-1" /> Cancelar
|
||||||
</button>
|
</button>
|
||||||
@ -194,7 +199,7 @@ const ConsultationList: React.FC<ConsultationListProps> = ({
|
|||||||
{allowEdit && (
|
{allowEdit && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onEdit?.(c.id)}
|
onClick={() => onEdit?.(c.id)}
|
||||||
className="inline-flex items-center text-gray-600 hover:text-gray-800 text-xs"
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-gray-700 bg-gray-50 hover:bg-gray-100 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
<Pencil className="w-4 h-4 mr-1" /> Editar
|
<Pencil className="w-4 h-4 mr-1" /> Editar
|
||||||
</button>
|
</button>
|
||||||
@ -202,7 +207,7 @@ const ConsultationList: React.FC<ConsultationListProps> = ({
|
|||||||
{allowDelete && (
|
{allowDelete && (
|
||||||
<button
|
<button
|
||||||
onClick={() => onDelete?.(c.id)}
|
onClick={() => onDelete?.(c.id)}
|
||||||
className="inline-flex items-center text-gray-500 hover:text-red-700 text-xs"
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-gray-700 hover:text-red-700 bg-gray-50 hover:bg-red-50 text-xs focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4 mr-1" /> Excluir
|
<Trash2 className="w-4 h-4 mr-1" /> Excluir
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -88,7 +88,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={data.nome}
|
value={data.nome}
|
||||||
onChange={(e) => onChange({ nome: e.target.value })}
|
onChange={(e) => onChange({ nome: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
required
|
required
|
||||||
placeholder="Digite o nome completo"
|
placeholder="Digite o nome completo"
|
||||||
autoComplete="name"
|
autoComplete="name"
|
||||||
@ -106,7 +106,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={data.social_name}
|
value={data.social_name}
|
||||||
onChange={(e) => onChange({ social_name: e.target.value })}
|
onChange={(e) => onChange({ social_name: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
placeholder="Opcional"
|
placeholder="Opcional"
|
||||||
autoComplete="nickname"
|
autoComplete="nickname"
|
||||||
/>
|
/>
|
||||||
@ -125,7 +125,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={data.rg || ""}
|
value={data.rg || ""}
|
||||||
onChange={(e) => onChange({ rg: e.target.value })}
|
onChange={(e) => onChange({ rg: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
placeholder="RG"
|
placeholder="RG"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -140,7 +140,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
id="estado_civil"
|
id="estado_civil"
|
||||||
value={data.estado_civil || ""}
|
value={data.estado_civil || ""}
|
||||||
onChange={(e) => onChange({ estado_civil: e.target.value })}
|
onChange={(e) => onChange({ estado_civil: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
>
|
>
|
||||||
<option value="">Selecione</option>
|
<option value="">Selecione</option>
|
||||||
<option value="solteiro(a)">Solteiro(a)</option>
|
<option value="solteiro(a)">Solteiro(a)</option>
|
||||||
@ -181,7 +181,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={data.cpf}
|
value={data.cpf}
|
||||||
onChange={(e) => onCpfChange(e.target.value)}
|
onChange={(e) => onCpfChange(e.target.value)}
|
||||||
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent ${
|
className={`w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors ${
|
||||||
cpfError ? "border-red-500" : "border-gray-300"
|
cpfError ? "border-red-500" : "border-gray-300"
|
||||||
}`}
|
}`}
|
||||||
required
|
required
|
||||||
@ -214,7 +214,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
id="sexo"
|
id="sexo"
|
||||||
value={data.sexo}
|
value={data.sexo}
|
||||||
onChange={(e) => onChange({ sexo: e.target.value })}
|
onChange={(e) => onChange({ sexo: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">Selecione</option>
|
<option value="">Selecione</option>
|
||||||
@ -235,7 +235,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
type="date"
|
type="date"
|
||||||
value={data.dataNascimento}
|
value={data.dataNascimento}
|
||||||
onChange={(e) => onChange({ dataNascimento: e.target.value })}
|
onChange={(e) => onChange({ dataNascimento: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
required
|
required
|
||||||
autoComplete="bday"
|
autoComplete="bday"
|
||||||
/>
|
/>
|
||||||
@ -255,7 +255,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
<select
|
<select
|
||||||
value={data.codigoPais}
|
value={data.codigoPais}
|
||||||
onChange={(e) => onChange({ codigoPais: e.target.value })}
|
onChange={(e) => onChange({ codigoPais: e.target.value })}
|
||||||
className="w-28 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-28 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
required
|
required
|
||||||
aria-label="Código do país"
|
aria-label="Código do país"
|
||||||
>
|
>
|
||||||
@ -274,7 +274,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
ddd: e.target.value.replace(/\D/g, "").slice(0, 2),
|
ddd: e.target.value.replace(/\D/g, "").slice(0, 2),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-16 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-16 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
placeholder="DDD"
|
placeholder="DDD"
|
||||||
required
|
required
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
@ -292,7 +292,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
.slice(0, 9),
|
.slice(0, 9),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
placeholder="Número do telefone"
|
placeholder="Número do telefone"
|
||||||
required
|
required
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
@ -314,7 +314,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
type="email"
|
type="email"
|
||||||
value={data.email}
|
value={data.email}
|
||||||
onChange={(e) => onChange({ email: e.target.value })}
|
onChange={(e) => onChange({ email: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
required
|
required
|
||||||
placeholder="contato@paciente.com"
|
placeholder="contato@paciente.com"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
@ -333,7 +333,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
<select
|
<select
|
||||||
value={data.tipo_sanguineo}
|
value={data.tipo_sanguineo}
|
||||||
onChange={(e) => onChange({ tipo_sanguineo: e.target.value })}
|
onChange={(e) => onChange({ tipo_sanguineo: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
>
|
>
|
||||||
<option value="">Selecione</option>
|
<option value="">Selecione</option>
|
||||||
{bloodTypes.map((tipo) => (
|
{bloodTypes.map((tipo) => (
|
||||||
@ -354,7 +354,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
step="0.1"
|
step="0.1"
|
||||||
value={data.altura}
|
value={data.altura}
|
||||||
onChange={(e) => onChange({ altura: e.target.value })}
|
onChange={(e) => onChange({ altura: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
placeholder="170"
|
placeholder="170"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -369,7 +369,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
step="0.1"
|
step="0.1"
|
||||||
value={data.peso}
|
value={data.peso}
|
||||||
onChange={(e) => onChange({ peso: e.target.value })}
|
onChange={(e) => onChange({ peso: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
placeholder="70.5"
|
placeholder="70.5"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -380,7 +380,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
<select
|
<select
|
||||||
value={data.convenio}
|
value={data.convenio}
|
||||||
onChange={(e) => onChange({ convenio: e.target.value })}
|
onChange={(e) => onChange({ convenio: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
>
|
>
|
||||||
<option value="">Selecione</option>
|
<option value="">Selecione</option>
|
||||||
{convenios.map((c) => (
|
{convenios.map((c) => (
|
||||||
@ -398,7 +398,7 @@ const PacienteForm: React.FC<PacienteFormProps> = ({
|
|||||||
type="text"
|
type="text"
|
||||||
value={data.numeroCarteirinha}
|
value={data.numeroCarteirinha}
|
||||||
onChange={(e) => onChange({ numeroCarteirinha: e.target.value })}
|
onChange={(e) => onChange({ numeroCarteirinha: e.target.value })}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-600 focus:border-green-600/40 transition-colors"
|
||||||
placeholder="Informe se possuir convênio"
|
placeholder="Informe se possuir convênio"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { AvatarInitials } from "../AvatarInitials";
|
import { AvatarInitials } from "../AvatarInitials";
|
||||||
|
import { Calendar, Eye, Pencil, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
export interface PatientListItem {
|
export interface PatientListItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -11,7 +12,6 @@ export interface PatientListItem {
|
|||||||
vip?: boolean;
|
vip?: boolean;
|
||||||
cidade?: string;
|
cidade?: string;
|
||||||
estado?: string;
|
estado?: string;
|
||||||
// placeholders a serem preenchidos quando consultasService estiver pronto
|
|
||||||
ultimoAtendimento?: string | null; // ISO ou texto humanizado
|
ultimoAtendimento?: string | null; // ISO ou texto humanizado
|
||||||
proximoAtendimento?: string | null;
|
proximoAtendimento?: string | null;
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ const PatientListTable: React.FC<PatientListTableProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="overflow-x-auto"
|
className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm"
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Lista de pacientes"
|
aria-label="Lista de pacientes"
|
||||||
>
|
>
|
||||||
@ -43,47 +43,50 @@ const PatientListTable: React.FC<PatientListTableProps> = ({
|
|||||||
className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"
|
className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"
|
||||||
role="table"
|
role="table"
|
||||||
>
|
>
|
||||||
<thead className="bg-gray-50 dark:bg-gray-800" role="rowgroup">
|
<thead
|
||||||
|
className="bg-gray-50/90 dark:bg-gray-800/90 sticky top-0 backdrop-blur supports-[backdrop-filter]:backdrop-blur z-10"
|
||||||
|
role="rowgroup"
|
||||||
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"
|
className="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Paciente
|
Paciente
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"
|
className="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Contato
|
Contato
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"
|
className="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Local
|
Local
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"
|
className="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Último Atendimento
|
Último Atendimento
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"
|
className="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Próximo Atendimento
|
Próximo Atendimento
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"
|
className="px-6 py-3 text-left text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Convênio
|
Convênio
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider"
|
className="px-6 py-3 text-right text-xs font-semibold text-gray-600 dark:text-gray-300 uppercase tracking-wider"
|
||||||
>
|
>
|
||||||
Ações
|
Ações
|
||||||
</th>
|
</th>
|
||||||
@ -96,7 +99,7 @@ const PatientListTable: React.FC<PatientListTableProps> = ({
|
|||||||
{pacientes.map((p) => (
|
{pacientes.map((p) => (
|
||||||
<tr
|
<tr
|
||||||
key={p.id}
|
key={p.id}
|
||||||
className="hover:bg-gray-50 dark:hover:bg-gray-800"
|
className="odd:bg-white even:bg-gray-50/60 dark:even:bg-gray-800/50 hover:bg-blue-50/50 dark:hover:bg-gray-800 transition-colors"
|
||||||
role="row"
|
role="row"
|
||||||
>
|
>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
@ -104,7 +107,7 @@ const PatientListTable: React.FC<PatientListTableProps> = ({
|
|||||||
<AvatarInitials name={p.nome} size={40} />
|
<AvatarInitials name={p.nome} size={40} />
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className="text-sm font-medium text-gray-900 dark:text-gray-100 cursor-pointer hover:underline"
|
className="text-sm font-semibold text-gray-900 dark:text-gray-100 cursor-pointer hover:underline"
|
||||||
onClick={() => onView?.(p)}
|
onClick={() => onView?.(p)}
|
||||||
>
|
>
|
||||||
{p.nome || "Sem nome"}
|
{p.nome || "Sem nome"}
|
||||||
@ -114,10 +117,10 @@ const PatientListTable: React.FC<PatientListTableProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
{p.vip && (
|
{p.vip && (
|
||||||
<div
|
<div
|
||||||
className="mt-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800 dark:bg-yellow-200 dark:text-yellow-900"
|
className="mt-2 inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-yellow-100 text-yellow-800 ring-1 ring-yellow-700/20 dark:bg-yellow-200 dark:text-yellow-900"
|
||||||
aria-label="Paciente VIP"
|
aria-label="Paciente VIP"
|
||||||
>
|
>
|
||||||
VIP
|
<span aria-hidden>★</span> VIP
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -147,37 +150,62 @@ const PatientListTable: React.FC<PatientListTableProps> = ({
|
|||||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
{p.convenio || "Particular"}
|
{p.convenio || "Particular"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-right text-sm font-medium space-x-3">
|
<td className="px-6 py-4 text-right text-sm font-medium">
|
||||||
{onSchedule && (
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
{onView && (
|
||||||
|
<button
|
||||||
|
onClick={() => onView(p)}
|
||||||
|
title={`Ver ${p.nome}`}
|
||||||
|
aria-label={`Ver ${p.nome}`}
|
||||||
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-gray-700 hover:text-gray-900 hover:bg-gray-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Ver</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onSchedule && (
|
||||||
|
<button
|
||||||
|
onClick={() => onSchedule(p)}
|
||||||
|
title={`Agendar para ${p.nome}`}
|
||||||
|
aria-label={`Agendar para ${p.nome}`}
|
||||||
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-blue-700 bg-blue-50 hover:bg-blue-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
|
>
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Agendar</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => onSchedule(p)}
|
onClick={() => onEdit(p)}
|
||||||
className="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300"
|
title={`Editar ${p.nome}`}
|
||||||
|
aria-label={`Editar ${p.nome}`}
|
||||||
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-green-700 bg-green-50 hover:bg-green-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
Agendar
|
<Pencil className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Editar</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
<button
|
||||||
<button
|
onClick={() => onDelete(p)}
|
||||||
onClick={() => onEdit(p)}
|
title={`Excluir ${p.nome}`}
|
||||||
className="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300"
|
aria-label={`Excluir ${p.nome}`}
|
||||||
>
|
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-red-700 bg-red-50 hover:bg-red-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
Editar
|
>
|
||||||
</button>
|
<Trash2 className="w-4 h-4" />
|
||||||
<button
|
<span className="hidden sm:inline">Excluir</span>
|
||||||
onClick={() => onDelete(p)}
|
</button>
|
||||||
className="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
</div>
|
||||||
>
|
|
||||||
Excluir
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{pacientes.length === 0 && (
|
{pacientes.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td
|
||||||
colSpan={7}
|
colSpan={7}
|
||||||
className="px-6 py-10 text-center text-sm text-gray-500 dark:text-gray-400"
|
className="px-6 py-10 text-center text-sm text-gray-500 dark:text-gray-400"
|
||||||
>
|
>
|
||||||
{emptyMessage}
|
<span role="status" aria-live="polite">
|
||||||
|
{emptyMessage}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -8,6 +8,42 @@ import { AuthProvider } from "./context/AuthContext";
|
|||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
|
// Apply accessibility preferences before React mounts to avoid FOUC and ensure persistence across reloads.
|
||||||
|
// This also helps E2E test detect classes after reload.
|
||||||
|
(() => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem("accessibility-prefs");
|
||||||
|
if (!raw) return;
|
||||||
|
const prefs = JSON.parse(raw) as Partial<{
|
||||||
|
fontSize: number;
|
||||||
|
highContrast: boolean;
|
||||||
|
darkMode: boolean;
|
||||||
|
dyslexicFont: boolean;
|
||||||
|
lineSpacing: boolean;
|
||||||
|
reducedMotion: boolean;
|
||||||
|
lowBlueLight: boolean;
|
||||||
|
focusMode: boolean;
|
||||||
|
}>;
|
||||||
|
const root = document.documentElement;
|
||||||
|
if (typeof prefs.fontSize === "number") {
|
||||||
|
root.style.fontSize = `${prefs.fontSize}%`;
|
||||||
|
}
|
||||||
|
const toggle = (flag: boolean | undefined, cls: string) => {
|
||||||
|
if (flag) root.classList.add(cls);
|
||||||
|
else root.classList.remove(cls);
|
||||||
|
};
|
||||||
|
toggle(prefs.highContrast, "high-contrast");
|
||||||
|
toggle(prefs.darkMode, "dark");
|
||||||
|
toggle(prefs.dyslexicFont, "dyslexic-font");
|
||||||
|
toggle(prefs.lineSpacing, "line-spacing");
|
||||||
|
toggle(prefs.reducedMotion, "reduced-motion");
|
||||||
|
toggle(prefs.lowBlueLight, "low-blue-light");
|
||||||
|
toggle(prefs.focusMode, "focus-mode");
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
806
MEDICONNECT 2/src/pages/AcompanhamentoPacienteAntigo.tsx.bak
Normal file
806
MEDICONNECT 2/src/pages/AcompanhamentoPacienteAntigo.tsx.bak
Normal file
@ -0,0 +1,806 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
User,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle,
|
||||||
|
AlertCircle,
|
||||||
|
LogOut,
|
||||||
|
Eye,
|
||||||
|
Filter,
|
||||||
|
} from "lucide-react";
|
||||||
|
import consultaService from "../services/consultaService";
|
||||||
|
import medicoService from "../services/medicoService";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { format, isAfter, isBefore, isToday, addDays } from "date-fns";
|
||||||
|
import { ptBR } from "date-fns/locale";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
interface Consulta {
|
||||||
|
_id: string;
|
||||||
|
pacienteId: string;
|
||||||
|
medicoId: string;
|
||||||
|
dataHora: string;
|
||||||
|
status: "agendada" | "confirmada" | "realizada" | "cancelada" | "faltou";
|
||||||
|
tipoConsulta: string;
|
||||||
|
motivoConsulta: string;
|
||||||
|
observacoes?: string;
|
||||||
|
resultados?: string;
|
||||||
|
prescricoes?: string;
|
||||||
|
proximaConsulta?: string;
|
||||||
|
criadoEm: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Medico {
|
||||||
|
_id: string;
|
||||||
|
nome: string;
|
||||||
|
especialidade: string;
|
||||||
|
valorConsulta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Paciente {
|
||||||
|
_id: string;
|
||||||
|
nome: string;
|
||||||
|
cpf: string;
|
||||||
|
telefone: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AcompanhamentoPaciente: React.FC = () => {
|
||||||
|
const [consultas, setConsultas] = useState<Consulta[]>([]);
|
||||||
|
const [medicos, setMedicos] = useState<Medico[]>([]);
|
||||||
|
const [pacienteLogado, setPacienteLogado] = useState<Paciente | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [filtroStatus, setFiltroStatus] = useState<string>("todas");
|
||||||
|
const [filtroPeriodo, setFiltroPeriodo] = useState<string>("todos");
|
||||||
|
const [consultaSelecionada, setConsultaSelecionada] =
|
||||||
|
useState<Consulta | null>(null);
|
||||||
|
const [showDetalhes, setShowDetalhes] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// (Effect moved below callback declarations)
|
||||||
|
|
||||||
|
// Mesclar consultas locais do localStorage com as do backend (apenas visual)
|
||||||
|
interface LocalConsultaRaw {
|
||||||
|
id: string;
|
||||||
|
pacienteId: string;
|
||||||
|
medicoId: string;
|
||||||
|
pacienteNome?: string;
|
||||||
|
medicoNome?: string;
|
||||||
|
dataHora: string;
|
||||||
|
tipo?: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergeConsultasLocais = useCallback(
|
||||||
|
(pacienteId: string, pacienteEmail?: string) => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem("consultas_local");
|
||||||
|
if (!raw) return;
|
||||||
|
const arr: LocalConsultaRaw[] = JSON.parse(raw);
|
||||||
|
console.log("[mergeConsultasLocais] Filtrando consultas. Procurando:", {
|
||||||
|
pacienteId,
|
||||||
|
pacienteEmail,
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
"[mergeConsultasLocais] Total no localStorage:",
|
||||||
|
arr.length
|
||||||
|
);
|
||||||
|
const minhas = arr.filter((c) => {
|
||||||
|
const match =
|
||||||
|
c.pacienteId === pacienteId ||
|
||||||
|
(pacienteEmail && c.pacienteId === pacienteEmail) ||
|
||||||
|
c.pacienteId === pacienteEmail;
|
||||||
|
if (match) {
|
||||||
|
console.log(
|
||||||
|
"[mergeConsultasLocais] Match encontrado:",
|
||||||
|
c.id,
|
||||||
|
c.pacienteId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return match;
|
||||||
|
});
|
||||||
|
console.log(
|
||||||
|
"[mergeConsultasLocais] Consultas filtradas:",
|
||||||
|
minhas.length
|
||||||
|
);
|
||||||
|
if (!minhas.length) {
|
||||||
|
console.log(
|
||||||
|
"[mergeConsultasLocais] Nenhuma consulta encontrada para este paciente"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setConsultas((prev) => {
|
||||||
|
const existentes = new Set(prev.map((c) => c._id));
|
||||||
|
const extras: Consulta[] = minhas
|
||||||
|
.filter((c) => !existentes.has(c.id))
|
||||||
|
.map((c) => ({
|
||||||
|
_id: c.id,
|
||||||
|
pacienteId: c.pacienteId,
|
||||||
|
medicoId: c.medicoId,
|
||||||
|
dataHora: c.dataHora,
|
||||||
|
status: (c.status as Consulta["status"]) || "agendada",
|
||||||
|
tipoConsulta: c.tipo || "",
|
||||||
|
motivoConsulta: "",
|
||||||
|
criadoEm: c.dataHora,
|
||||||
|
}));
|
||||||
|
console.log(
|
||||||
|
"[mergeConsultasLocais] Adicionando",
|
||||||
|
extras.length,
|
||||||
|
"consultas ao estado"
|
||||||
|
);
|
||||||
|
return [...prev, ...extras];
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Carrega e injeta consultas de demonstração automaticamente se ainda não presentes
|
||||||
|
const ensureDemoConsultas = useCallback(
|
||||||
|
async (pacienteId: string, pacienteEmail?: string) => {
|
||||||
|
try {
|
||||||
|
const rawLocal = localStorage.getItem("consultas_local");
|
||||||
|
const existentes: LocalConsultaRaw[] = rawLocal
|
||||||
|
? JSON.parse(rawLocal)
|
||||||
|
: [];
|
||||||
|
const jaTem = existentes.some(
|
||||||
|
(c) =>
|
||||||
|
c.pacienteId === pacienteId ||
|
||||||
|
(pacienteEmail && c.pacienteId === pacienteEmail)
|
||||||
|
);
|
||||||
|
if (!jaTem) {
|
||||||
|
const resp = await fetch("/src/data/consultas-demo.json").catch(() =>
|
||||||
|
Promise.resolve(undefined)
|
||||||
|
);
|
||||||
|
if (resp && resp.ok) {
|
||||||
|
const demo: LocalConsultaRaw[] = await resp.json();
|
||||||
|
const candidatos = demo.filter(
|
||||||
|
(c) =>
|
||||||
|
c.pacienteId === pacienteId ||
|
||||||
|
(pacienteEmail && c.pacienteId === pacienteEmail)
|
||||||
|
);
|
||||||
|
if (candidatos.length) {
|
||||||
|
const idsExist = new Set(existentes.map((c) => c.id));
|
||||||
|
const novos = candidatos.filter((c) => !idsExist.has(c.id));
|
||||||
|
if (novos.length) {
|
||||||
|
localStorage.setItem(
|
||||||
|
"consultas_local",
|
||||||
|
JSON.stringify([...existentes, ...novos])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mergeConsultasLocais(pacienteId, pacienteEmail);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Erro ao carregar consultas de demonstração:", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mergeConsultasLocais]
|
||||||
|
); // Efetua carregamento inicial após definição das callbacks
|
||||||
|
useEffect(() => {
|
||||||
|
const pacienteData = localStorage.getItem("pacienteLogado");
|
||||||
|
if (!pacienteData) {
|
||||||
|
navigate("/paciente");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const paciente = JSON.parse(pacienteData);
|
||||||
|
setPacienteLogado(paciente);
|
||||||
|
fetchConsultas(paciente._id);
|
||||||
|
ensureDemoConsultas(paciente._id, paciente.email);
|
||||||
|
fetchMedicos();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar dados do paciente:", error);
|
||||||
|
navigate("/paciente");
|
||||||
|
}
|
||||||
|
}, [navigate, ensureDemoConsultas]);
|
||||||
|
|
||||||
|
const fetchConsultas = async (pacienteId: string) => {
|
||||||
|
try {
|
||||||
|
const response = await consultaService.listarConsultas({
|
||||||
|
paciente_id: pacienteId,
|
||||||
|
});
|
||||||
|
const list = response.data?.data || [];
|
||||||
|
const mapped: Consulta[] = list.map((c) => ({
|
||||||
|
_id: c.id || Math.random().toString(36).slice(2, 9),
|
||||||
|
pacienteId: c.paciente_id || "",
|
||||||
|
medicoId: c.medico_id || "",
|
||||||
|
dataHora: c.data_hora || new Date().toISOString(),
|
||||||
|
status: c.status || "agendada",
|
||||||
|
tipoConsulta: c.tipo_consulta || "",
|
||||||
|
motivoConsulta: c.motivo_consulta || "",
|
||||||
|
observacoes: c.observacoes,
|
||||||
|
resultados: "",
|
||||||
|
prescricoes: "",
|
||||||
|
proximaConsulta: "",
|
||||||
|
criadoEm: c.created_at || new Date().toISOString(),
|
||||||
|
}));
|
||||||
|
setConsultas(mapped);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar consultas:", error);
|
||||||
|
toast.error("Erro ao carregar suas consultas");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchMedicos = async () => {
|
||||||
|
try {
|
||||||
|
const response = await medicoService.listarMedicos();
|
||||||
|
const list = response.data?.data || [];
|
||||||
|
const mapped: Medico[] = list.map((m) => ({
|
||||||
|
_id: m.id || Math.random().toString(36).slice(2, 9),
|
||||||
|
nome: m.nome || "",
|
||||||
|
especialidade: m.especialidade || "",
|
||||||
|
valorConsulta: 0,
|
||||||
|
}));
|
||||||
|
setMedicos(mapped);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar médicos:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMedicoNome = (medicoId: string) => {
|
||||||
|
const medico = medicos.find((m) => m._id === medicoId);
|
||||||
|
return medico ? medico.nome : "Médico não encontrado";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMedicoEspecialidade = (medicoId: string) => {
|
||||||
|
const medico = medicos.find((m) => m._id === medicoId);
|
||||||
|
return medico ? medico.especialidade : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "agendada":
|
||||||
|
return "bg-blue-100 text-blue-800";
|
||||||
|
case "confirmada":
|
||||||
|
return "bg-green-100 text-green-800";
|
||||||
|
case "realizada":
|
||||||
|
return "bg-gray-100 text-gray-800";
|
||||||
|
case "cancelada":
|
||||||
|
return "bg-red-100 text-red-800";
|
||||||
|
case "faltou":
|
||||||
|
return "bg-orange-100 text-orange-800";
|
||||||
|
default:
|
||||||
|
return "bg-gray-100 text-gray-800";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "agendada":
|
||||||
|
return <Clock className="w-4 h-4" />;
|
||||||
|
case "confirmada":
|
||||||
|
return <CheckCircle className="w-4 h-4" />;
|
||||||
|
case "realizada":
|
||||||
|
return <CheckCircle className="w-4 h-4" />;
|
||||||
|
case "cancelada":
|
||||||
|
return <XCircle className="w-4 h-4" />;
|
||||||
|
case "faltou":
|
||||||
|
return <AlertCircle className="w-4 h-4" />;
|
||||||
|
default:
|
||||||
|
return <Clock className="w-4 h-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusTexto = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "agendada":
|
||||||
|
return "Agendada";
|
||||||
|
case "confirmada":
|
||||||
|
return "Confirmada";
|
||||||
|
case "realizada":
|
||||||
|
return "Realizada";
|
||||||
|
case "cancelada":
|
||||||
|
return "Cancelada";
|
||||||
|
case "faltou":
|
||||||
|
return "Faltou";
|
||||||
|
default:
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filtrarConsultas = () => {
|
||||||
|
let consultasFiltradas = [...consultas];
|
||||||
|
|
||||||
|
// Filtro por status
|
||||||
|
if (filtroStatus !== "todas") {
|
||||||
|
consultasFiltradas = consultasFiltradas.filter(
|
||||||
|
(c) => c.status === filtroStatus
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtro por período
|
||||||
|
const hoje = new Date();
|
||||||
|
switch (filtroPeriodo) {
|
||||||
|
case "proximas":
|
||||||
|
consultasFiltradas = consultasFiltradas.filter(
|
||||||
|
(c) =>
|
||||||
|
isAfter(new Date(c.dataHora), hoje) &&
|
||||||
|
(c.status === "agendada" || c.status === "confirmada")
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "hoje":
|
||||||
|
consultasFiltradas = consultasFiltradas.filter((c) =>
|
||||||
|
isToday(new Date(c.dataHora))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "semana":
|
||||||
|
{
|
||||||
|
const proximaSemana = addDays(hoje, 7);
|
||||||
|
consultasFiltradas = consultasFiltradas.filter(
|
||||||
|
(c) =>
|
||||||
|
isAfter(new Date(c.dataHora), hoje) &&
|
||||||
|
isBefore(new Date(c.dataHora), proximaSemana)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "historico":
|
||||||
|
consultasFiltradas = consultasFiltradas.filter((c) =>
|
||||||
|
isBefore(new Date(c.dataHora), hoje)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return consultasFiltradas;
|
||||||
|
};
|
||||||
|
|
||||||
|
const abrirDetalhes = (consulta: Consulta) => {
|
||||||
|
setConsultaSelecionada(consulta);
|
||||||
|
setShowDetalhes(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fecharDetalhes = () => {
|
||||||
|
setConsultaSelecionada(null);
|
||||||
|
setShowDetalhes(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const novoAgendamento = () => {
|
||||||
|
navigate("/agendamento");
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
localStorage.removeItem("pacienteLogado");
|
||||||
|
navigate("/paciente");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!pacienteLogado) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const consultasFiltradas = filtrarConsultas();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
|
{/* Header com Gradiente Aprimorado */}
|
||||||
|
<div className="bg-gradient-to-r from-blue-700 via-blue-600 to-blue-500 dark:from-blue-800 dark:via-blue-700 dark:to-blue-600 rounded-xl shadow-lg p-8">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
|
||||||
|
<div className="text-white">
|
||||||
|
<h1 className="text-4xl font-bold mb-2">
|
||||||
|
Olá, {pacienteLogado.nome}!
|
||||||
|
</h1>
|
||||||
|
<p className="text-blue-100 text-lg">
|
||||||
|
Gerencie suas consultas e acompanhe seu histórico médico
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-4 mt-3 text-sm text-blue-200">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
<span>Paciente</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
<span>{consultas.length} consultas registradas</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 w-full md:w-auto">
|
||||||
|
<button
|
||||||
|
onClick={novoAgendamento}
|
||||||
|
className="flex-1 md:flex-none flex items-center justify-center gap-2 bg-white hover:bg-blue-50 text-blue-700 px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-md hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Calendar className="w-5 h-5" />
|
||||||
|
<span>Nova Consulta</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={logout}
|
||||||
|
className="flex items-center justify-center gap-2 bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-md hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<LogOut className="w-5 h-5" />
|
||||||
|
<span>Sair</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards de Estatísticas */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Total
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-gray-900 dark:text-white mt-2">
|
||||||
|
{consultas.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-blue-100 dark:bg-blue-900/30 p-3 rounded-lg">
|
||||||
|
<Calendar className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Agendadas
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-blue-600 dark:text-blue-400 mt-2">
|
||||||
|
{consultas.filter((c) => c.status === "agendada").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-blue-100 dark:bg-blue-900/30 p-3 rounded-lg">
|
||||||
|
<Clock className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Realizadas
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-green-600 dark:text-green-400 mt-2">
|
||||||
|
{consultas.filter((c) => c.status === "realizada").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-100 dark:bg-green-900/30 p-3 rounded-lg">
|
||||||
|
<CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Canceladas
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-red-600 dark:text-red-400 mt-2">
|
||||||
|
{consultas.filter((c) => c.status === "cancelada").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-red-100 dark:bg-red-900/30 p-3 rounded-lg">
|
||||||
|
<XCircle className="w-8 h-8 text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filtros */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="bg-blue-100 dark:bg-blue-900/30 p-2 rounded-lg">
|
||||||
|
<Filter className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Filtrar Consultas
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Status da Consulta
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filtroStatus}
|
||||||
|
onChange={(e) => setFiltroStatus(e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="todas">Todas</option>
|
||||||
|
<option value="agendada">Agendadas</option>
|
||||||
|
<option value="confirmada">Confirmadas</option>
|
||||||
|
<option value="realizada">Realizadas</option>
|
||||||
|
<option value="cancelada">Canceladas</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Período
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={filtroPeriodo}
|
||||||
|
onChange={(e) => setFiltroPeriodo(e.target.value)}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="todos">Todos</option>
|
||||||
|
<option value="proximas">Próximas</option>
|
||||||
|
<option value="hoje">Hoje</option>
|
||||||
|
<option value="semana">Próximos 7 dias</option>
|
||||||
|
<option value="historico">Histórico</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lista de Consultas */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Suas Consultas
|
||||||
|
</h2>
|
||||||
|
<span className="bg-blue-100 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 text-sm font-medium px-3 py-1 rounded-full">
|
||||||
|
{consultasFiltradas.length}{" "}
|
||||||
|
{consultasFiltradas.length === 1 ? "consulta" : "consultas"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center items-center p-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
) : consultasFiltradas.length === 0 ? (
|
||||||
|
<div className="text-center p-12">
|
||||||
|
<div className="bg-gray-100 dark:bg-gray-700/30 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Calendar className="w-10 h-10 text-gray-400 dark:text-gray-500" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
|
||||||
|
Nenhuma consulta encontrada
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mb-6 max-w-md mx-auto">
|
||||||
|
{filtroStatus !== "todas" || filtroPeriodo !== "todos"
|
||||||
|
? "Tente ajustar os filtros para ver mais consultas."
|
||||||
|
: "Você ainda não tem consultas agendadas."}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={novoAgendamento}
|
||||||
|
className="btn-primary inline-flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Calendar className="w-4 h-4" />
|
||||||
|
Agendar Primeira Consulta
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
|
{consultasFiltradas.map((consulta) => (
|
||||||
|
<div
|
||||||
|
key={consulta._id}
|
||||||
|
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center space-x-4 mb-2">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center space-x-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(
|
||||||
|
consulta.status
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{getStatusIcon(consulta.status)}
|
||||||
|
<span>{getStatusTexto(consulta.status)}</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-gray-500">
|
||||||
|
{consulta.tipoConsulta}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<User className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">
|
||||||
|
{getMedicoNome(consulta.medicoId)}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{getMedicoEspecialidade(consulta.medicoId)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Calendar className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">
|
||||||
|
{format(new Date(consulta.dataHora), "dd/MM/yyyy", {
|
||||||
|
locale: ptBR,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{format(new Date(consulta.dataHora), "EEEE", {
|
||||||
|
locale: ptBR,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Clock className="w-4 h-4 text-gray-400" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-gray-900">
|
||||||
|
{format(new Date(consulta.dataHora), "HH:mm")}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
{consulta.motivoConsulta || "Consulta de rotina"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => abrirDetalhes(consulta)}
|
||||||
|
className="ml-4 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
|
aria-label="Ver detalhes da consulta"
|
||||||
|
>
|
||||||
|
<Eye className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal de Detalhes */}
|
||||||
|
{showDetalhes && consultaSelecionada && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="detalhes-consulta-title"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div className="p-6 border-b border-gray-200">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h3
|
||||||
|
id="detalhes-consulta-title"
|
||||||
|
className="text-lg font-semibold"
|
||||||
|
>
|
||||||
|
Detalhes da Consulta
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={fecharDetalhes}
|
||||||
|
aria-label="Fechar detalhes da consulta"
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-300 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<XCircle className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Informações Básicas */}
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-3">Informações da Consulta</h4>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Médico:</span>
|
||||||
|
<p className="font-medium">
|
||||||
|
{getMedicoNome(consultaSelecionada.medicoId)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Especialidade:</span>
|
||||||
|
<p className="font-medium">
|
||||||
|
{getMedicoEspecialidade(consultaSelecionada.medicoId)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Data:</span>
|
||||||
|
<p className="font-medium">
|
||||||
|
{format(
|
||||||
|
new Date(consultaSelecionada.dataHora),
|
||||||
|
"dd/MM/yyyy - HH:mm",
|
||||||
|
{ locale: ptBR }
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Status:</span>
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center space-x-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusColor(
|
||||||
|
consultaSelecionada.status
|
||||||
|
)}`}
|
||||||
|
>
|
||||||
|
{getStatusIcon(consultaSelecionada.status)}
|
||||||
|
<span>{getStatusTexto(consultaSelecionada.status)}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-gray-500">Tipo:</span>
|
||||||
|
<p className="font-medium">
|
||||||
|
{consultaSelecionada.tipoConsulta}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Motivo da Consulta */}
|
||||||
|
{consultaSelecionada.motivoConsulta && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Motivo da Consulta</h4>
|
||||||
|
<p className="text-gray-700 bg-gray-50 p-3 rounded-lg">
|
||||||
|
{consultaSelecionada.motivoConsulta}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Observações */}
|
||||||
|
{consultaSelecionada.observacoes && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Observações</h4>
|
||||||
|
<p className="text-gray-700 bg-gray-50 p-3 rounded-lg">
|
||||||
|
{consultaSelecionada.observacoes}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Resultados (só aparece se a consulta foi realizada) */}
|
||||||
|
{consultaSelecionada.status === "realizada" &&
|
||||||
|
consultaSelecionada.resultados && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">
|
||||||
|
Resultados da Consulta
|
||||||
|
</h4>
|
||||||
|
<p className="text-gray-700 bg-green-50 p-3 rounded-lg border-l-4 border-green-400">
|
||||||
|
{consultaSelecionada.resultados}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Prescrições */}
|
||||||
|
{consultaSelecionada.prescricoes && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">Prescrições Médicas</h4>
|
||||||
|
<p className="text-gray-700 bg-blue-50 p-3 rounded-lg border-l-4 border-blue-400">
|
||||||
|
{consultaSelecionada.prescricoes}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Próxima Consulta */}
|
||||||
|
{consultaSelecionada.proximaConsulta && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-2">
|
||||||
|
Próxima Consulta Recomendada
|
||||||
|
</h4>
|
||||||
|
<p className="text-gray-700 bg-yellow-50 p-3 rounded-lg border-l-4 border-yellow-400">
|
||||||
|
{consultaSelecionada.proximaConsulta}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Data de Criação */}
|
||||||
|
<div className="text-xs text-gray-500 pt-4 border-t">
|
||||||
|
Agendado em:{" "}
|
||||||
|
{format(
|
||||||
|
new Date(consultaSelecionada.criadoEm),
|
||||||
|
"dd/MM/yyyy às HH:mm",
|
||||||
|
{ locale: ptBR }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AcompanhamentoPaciente;
|
||||||
@ -26,7 +26,7 @@ export default function AdminDiagnostico() {
|
|||||||
const decodeJwt = (token: string) => {
|
const decodeJwt = (token: string) => {
|
||||||
try {
|
try {
|
||||||
const parts = token.split(".");
|
const parts = token.split(".");
|
||||||
if (parts.length !== 3) return { valid: false };
|
if (parts.length !== 3) return { valid: false } as const;
|
||||||
const payload = JSON.parse(atob(parts[1]));
|
const payload = JSON.parse(atob(parts[1]));
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
const expired = payload.exp ? payload.exp < now : false;
|
const expired = payload.exp ? payload.exp < now : false;
|
||||||
@ -37,9 +37,9 @@ export default function AdminDiagnostico() {
|
|||||||
sub: payload.sub,
|
sub: payload.sub,
|
||||||
email: payload.email,
|
email: payload.email,
|
||||||
role: payload.role,
|
role: payload.role,
|
||||||
};
|
} as const;
|
||||||
} catch {
|
} catch {
|
||||||
return { valid: false };
|
return { valid: false } as const;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,31 +157,31 @@ export default function AdminDiagnostico() {
|
|||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={scanTokens}
|
onClick={scanTokens}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
🔄 Escanear Tokens
|
🔄 Escanear Tokens
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={clearExpiredTokens}
|
onClick={clearExpiredTokens}
|
||||||
className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700"
|
className="px-4 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-yellow-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
🧹 Limpar Expirados
|
🧹 Limpar Expirados
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={clearAllTokens}
|
onClick={clearAllTokens}
|
||||||
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
🗑️ Limpar TODOS
|
🗑️ Limpar TODOS
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={testLogin}
|
onClick={testLogin}
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
|
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
🔐 Testar Login Admin
|
🔐 Testar Login Admin
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.reload()}
|
onClick={() => window.location.reload()}
|
||||||
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700"
|
className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
♻️ Recarregar Página
|
♻️ Recarregar Página
|
||||||
</button>
|
</button>
|
||||||
@ -191,13 +191,18 @@ export default function AdminDiagnostico() {
|
|||||||
{/* Tokens */}
|
{/* Tokens */}
|
||||||
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
<div className="bg-white rounded-lg shadow p-6 mb-6">
|
||||||
<h2 className="text-xl font-semibold mb-4">Tokens no localStorage</h2>
|
<h2 className="text-xl font-semibold mb-4">Tokens no localStorage</h2>
|
||||||
<div className="space-y-4">
|
<div
|
||||||
|
className="space-y-4"
|
||||||
|
role="list"
|
||||||
|
aria-label="Lista de tokens no armazenamento local"
|
||||||
|
>
|
||||||
{tokens.length === 0 && (
|
{tokens.length === 0 && (
|
||||||
<p className="text-gray-500">Nenhum token encontrado</p>
|
<p className="text-gray-500">Nenhum token encontrado</p>
|
||||||
)}
|
)}
|
||||||
{tokens.map((t) => (
|
{tokens.map((t) => (
|
||||||
<div
|
<div
|
||||||
key={t.key}
|
key={t.key}
|
||||||
|
role="listitem"
|
||||||
className={`p-4 rounded border-2 ${
|
className={`p-4 rounded border-2 ${
|
||||||
!t.present
|
!t.present
|
||||||
? "border-gray-300 bg-gray-50"
|
? "border-gray-300 bg-gray-50"
|
||||||
@ -276,8 +281,20 @@ export default function AdminDiagnostico() {
|
|||||||
|
|
||||||
{/* Log */}
|
{/* Log */}
|
||||||
<div className="bg-gray-900 text-green-400 rounded-lg shadow p-6 font-mono text-sm">
|
<div className="bg-gray-900 text-green-400 rounded-lg shadow p-6 font-mono text-sm">
|
||||||
<h2 className="text-xl font-semibold mb-4 text-white">📋 Log</h2>
|
<h2
|
||||||
<div className="space-y-1 max-h-96 overflow-y-auto">
|
id="diagnostic-log-title"
|
||||||
|
className="text-xl font-semibold mb-4 text-white"
|
||||||
|
>
|
||||||
|
📋 Log
|
||||||
|
</h2>
|
||||||
|
<div
|
||||||
|
className="space-y-1 max-h-96 overflow-y-auto focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2 rounded"
|
||||||
|
role="log"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="additions text"
|
||||||
|
aria-labelledby="diagnostic-log-title"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
{log.length === 0 && (
|
{log.length === 0 && (
|
||||||
<p className="text-gray-500">Nenhuma ação ainda</p>
|
<p className="text-gray-500">Nenhuma ação ainda</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Calendar, User, FileText, CheckCircle, LogOut } from "lucide-react";
|
import { Calendar, User, FileText, CheckCircle, LogOut } from "lucide-react";
|
||||||
import consultaService from "../services/consultaService";
|
// import consultaService from "../services/consultaService"; // não utilizado após integração com appointmentService
|
||||||
|
import { appointmentService } from "../services";
|
||||||
|
import AvailableSlotsPicker from "../components/agenda/AvailableSlotsPicker";
|
||||||
import medicoService from "../services/medicoService";
|
import medicoService from "../services/medicoService";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { format, addDays } from "date-fns";
|
import { format, addDays } from "date-fns";
|
||||||
@ -38,7 +40,7 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
observacoes: "",
|
observacoes: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [horariosDisponiveis, setHorariosDisponiveis] = useState<string[]>([]);
|
// Slots são carregados diretamente pelo AvailableSlotsPicker
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -135,60 +137,14 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const buscarHorariosDisponiveis = async (medicoId: string, data: string) => {
|
// Horários disponíveis agora são resolvidos no componente de slots
|
||||||
try {
|
|
||||||
const medico = medicos.find((m) => m._id === medicoId);
|
|
||||||
if (!medico) return;
|
|
||||||
|
|
||||||
const dataObj = new Date(data);
|
|
||||||
const diaSemana = [
|
|
||||||
"domingo",
|
|
||||||
"segunda",
|
|
||||||
"terca",
|
|
||||||
"quarta",
|
|
||||||
"quinta",
|
|
||||||
"sexta",
|
|
||||||
"sabado",
|
|
||||||
][dataObj.getDay()];
|
|
||||||
|
|
||||||
const horariosDoMedico = medico.horarioAtendimento[diaSemana] || [];
|
|
||||||
|
|
||||||
// Buscar consultas já agendadas nesta data
|
|
||||||
const response = await consultaService.listarConsultas({
|
|
||||||
medico_id: medicoId,
|
|
||||||
data_inicio: data,
|
|
||||||
data_fim: data,
|
|
||||||
});
|
|
||||||
|
|
||||||
const consultasAgendadas = response.data?.data || [];
|
|
||||||
const horariosOcupados = consultasAgendadas.map(
|
|
||||||
(consulta: { data_hora: string }) => {
|
|
||||||
const hora = new Date(consulta.data_hora).toTimeString().slice(0, 5);
|
|
||||||
return hora;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const horariosLivres = horariosDoMedico.filter(
|
|
||||||
(horario) => !horariosOcupados.includes(horario)
|
|
||||||
);
|
|
||||||
|
|
||||||
setHorariosDisponiveis(horariosLivres);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao buscar horários:", error);
|
|
||||||
toast.error("Erro ao carregar horários disponíveis");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMedicoChange = (medicoId: string) => {
|
const handleMedicoChange = (medicoId: string) => {
|
||||||
setAgendamento((prev) => ({ ...prev, medicoId, data: "", horario: "" }));
|
setAgendamento((prev) => ({ ...prev, medicoId, data: "", horario: "" }));
|
||||||
setHorariosDisponiveis([]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDataChange = (data: string) => {
|
const handleDataChange = (data: string) => {
|
||||||
setAgendamento((prev) => ({ ...prev, data, horario: "" }));
|
setAgendamento((prev) => ({ ...prev, data, horario: "" }));
|
||||||
if (agendamento.medicoId && data) {
|
|
||||||
buscarHorariosDisponiveis(agendamento.medicoId, data);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmarAgendamento = async () => {
|
const confirmarAgendamento = async () => {
|
||||||
@ -205,16 +161,12 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
`${agendamento.data}T${agendamento.horario}:00.000Z`
|
`${agendamento.data}T${agendamento.horario}:00.000Z`
|
||||||
);
|
);
|
||||||
|
|
||||||
await consultaService.criarConsulta({
|
await appointmentService.createAppointment({
|
||||||
paciente_id: pacienteLogado._id,
|
patient_id: pacienteLogado._id,
|
||||||
medico_id: agendamento.medicoId,
|
doctor_id: agendamento.medicoId,
|
||||||
data_hora: dataHora.toISOString(),
|
scheduled_at: dataHora.toISOString(),
|
||||||
tipo_consulta: agendamento.tipoConsulta as
|
appointment_type: "presencial",
|
||||||
| "primeira_vez"
|
chief_complaint: agendamento.motivoConsulta,
|
||||||
| "retorno"
|
|
||||||
| "emergencia"
|
|
||||||
| "rotina",
|
|
||||||
motivo_consulta: agendamento.motivoConsulta,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success("Consulta agendada com sucesso!");
|
toast.success("Consulta agendada com sucesso!");
|
||||||
@ -236,7 +188,6 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
motivoConsulta: "",
|
motivoConsulta: "",
|
||||||
observacoes: "",
|
observacoes: "",
|
||||||
});
|
});
|
||||||
setHorariosDisponiveis([]);
|
|
||||||
setEtapa(1);
|
setEtapa(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -320,7 +271,7 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
{/* Header com informações do paciente */}
|
{/* Header com informações do paciente */}
|
||||||
<div className="bg-gradient-to-r from-blue-700 to-blue-400 rounded-lg p-6 mb-8 text-white">
|
<div className="bg-gradient-to-r from-blue-700 to-blue-500 rounded-xl p-6 mb-8 text-white shadow">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-2xl font-bold">
|
||||||
@ -330,7 +281,7 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
className="flex items-center space-x-2 bg-white/20 hover:bg-white/30 px-4 py-2 rounded-lg transition-colors"
|
className="flex items-center space-x-2 bg-white/20 hover:bg-white/30 px-4 py-2 rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/70"
|
||||||
>
|
>
|
||||||
<LogOut className="w-4 h-4" />
|
<LogOut className="w-4 h-4" />
|
||||||
<span>Sair</span>
|
<span>Sair</span>
|
||||||
@ -364,7 +315,7 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg shadow-md p-6">
|
<div className="bg-white rounded-xl shadow border border-gray-200 p-6">
|
||||||
{/* Etapa 1: Seleção de Médico */}
|
{/* Etapa 1: Seleção de Médico */}
|
||||||
{etapa === 1 && (
|
{etapa === 1 && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -432,45 +383,32 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{agendamento.data && (
|
{agendamento.data && agendamento.medicoId && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Horário Disponível
|
Horários Disponíveis
|
||||||
</label>
|
</label>
|
||||||
{horariosDisponiveis.length > 0 ? (
|
<AvailableSlotsPicker
|
||||||
<div className="grid grid-cols-3 md:grid-cols-4 gap-3">
|
doctorId={agendamento.medicoId}
|
||||||
{horariosDisponiveis.map((horario) => (
|
date={agendamento.data}
|
||||||
<button
|
onSelect={(t) =>
|
||||||
key={horario}
|
setAgendamento((prev) => ({ ...prev, horario: t }))
|
||||||
onClick={() =>
|
}
|
||||||
setAgendamento((prev) => ({ ...prev, horario }))
|
/>
|
||||||
}
|
|
||||||
className={`p-3 border rounded-lg text-center transition-colors ${
|
|
||||||
agendamento.horario === horario
|
|
||||||
? "bg-blue-600 text-white border-blue-600"
|
|
||||||
: "bg-white text-gray-700 border-gray-300 hover:border-blue-300"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{horario}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-500 text-center py-4">
|
|
||||||
Nenhum horário disponível para esta data
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<button onClick={() => setEtapa(1)} className="btn-secondary">
|
<button
|
||||||
|
onClick={() => setEtapa(1)}
|
||||||
|
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300"
|
||||||
|
>
|
||||||
Voltar
|
Voltar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setEtapa(3)}
|
onClick={() => setEtapa(3)}
|
||||||
disabled={!agendamento.data || !agendamento.horario}
|
disabled={!agendamento.horario}
|
||||||
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
Próximo
|
Próximo
|
||||||
</button>
|
</button>
|
||||||
@ -543,7 +481,7 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Resumo do Agendamento */}
|
{/* Resumo do Agendamento */}
|
||||||
<div className="bg-gray-50 rounded-lg p-4">
|
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
|
||||||
<h3 className="font-semibold mb-3">Resumo do Agendamento:</h3>
|
<h3 className="font-semibold mb-3">Resumo do Agendamento:</h3>
|
||||||
<div className="space-y-1 text-sm">
|
<div className="space-y-1 text-sm">
|
||||||
<p>
|
<p>
|
||||||
@ -568,13 +506,16 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<button onClick={() => setEtapa(2)} className="btn-secondary">
|
<button
|
||||||
|
onClick={() => setEtapa(2)}
|
||||||
|
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300"
|
||||||
|
>
|
||||||
Voltar
|
Voltar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={confirmarAgendamento}
|
onClick={confirmarAgendamento}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
{loading ? "Agendando..." : "Confirmar Agendamento"}
|
{loading ? "Agendando..." : "Confirmar Agendamento"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -202,14 +202,14 @@ const CadastroMedico: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate("/login-medico")}
|
onClick={() => navigate("/login-medico")}
|
||||||
className="flex-1 bg-gray-100 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-200 transition-colors"
|
className="flex-1 bg-gray-100 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-200 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Voltar
|
Voltar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex-1 bg-gradient-to-r from-indigo-600 to-indigo-400 text-white py-3 px-4 rounded-lg font-medium hover:from-indigo-700 hover:to-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
className="flex-1 bg-gradient-to-r from-indigo-600 to-indigo-400 text-white py-3 px-4 rounded-lg font-medium hover:from-indigo-700 hover:to-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{loading ? "Cadastrando..." : "Cadastrar"}
|
{loading ? "Cadastrando..." : "Cadastrar"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -301,7 +301,7 @@ const CadastroPaciente: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleBuscarCEP}
|
onClick={handleBuscarCEP}
|
||||||
className="px-3 py-2 text-xs rounded-md bg-blue-100 hover:bg-blue-200 text-blue-700 font-medium"
|
className="px-3 py-2 text-xs rounded-md bg-blue-100 hover:bg-blue-200 text-blue-700 font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Buscar
|
Buscar
|
||||||
</button>
|
</button>
|
||||||
@ -384,14 +384,14 @@ const CadastroPaciente: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate("/paciente")}
|
onClick={() => navigate("/paciente")}
|
||||||
className="flex-1 bg-gray-100 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-200 transition-colors"
|
className="flex-1 bg-gray-100 text-gray-700 py-3 px-4 rounded-lg font-medium hover:bg-gray-200 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Voltar
|
Voltar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex-1 bg-gradient-to-r from-blue-600 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-700 hover:to-blue-500 disabled:opacity-50 transition-all"
|
className="flex-1 bg-gradient-to-r from-blue-600 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-700 hover:to-blue-500 disabled:opacity-50 transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{loading ? "Cadastrando..." : "Cadastrar"}
|
{loading ? "Cadastrando..." : "Cadastrar"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -258,7 +258,7 @@ const CadastroSecretaria: React.FC = () => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowForm(true)}
|
onClick={() => setShowForm(true)}
|
||||||
className="btn-primary mt-4 md:mt-0"
|
className="btn-primary mt-4 md:mt-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
<UserPlus className="w-5 h-5 mr-2" />
|
<UserPlus className="w-5 h-5 mr-2" />
|
||||||
Novo Paciente
|
Novo Paciente
|
||||||
@ -362,7 +362,7 @@ const CadastroSecretaria: React.FC = () => {
|
|||||||
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
<div className="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-gray-200">
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
<thead className="bg-gradient-to-l from-blue-700 to-blue-400">
|
<thead className="sticky top-0 z-10 bg-gradient-to-l from-blue-700 to-blue-400">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">
|
||||||
Paciente
|
Paciente
|
||||||
@ -387,7 +387,10 @@ const CadastroSecretaria: React.FC = () => {
|
|||||||
const imcStatus = imc ? getIMCStatus(parseFloat(imc)) : null;
|
const imcStatus = imc ? getIMCStatus(parseFloat(imc)) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={paciente._id} className="hover:bg-gray-50">
|
<tr
|
||||||
|
key={paciente._id}
|
||||||
|
className="odd:bg-white even:bg-gray-50 hover:bg-gray-100"
|
||||||
|
>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-gray-900">
|
<div className="text-sm font-medium text-gray-900">
|
||||||
@ -467,13 +470,15 @@ const CadastroSecretaria: React.FC = () => {
|
|||||||
<div className="flex space-x-2">
|
<div className="flex space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEdit(paciente)}
|
onClick={() => handleEdit(paciente)}
|
||||||
className="text-blue-600 hover:text-blue-900"
|
className="inline-flex items-center p-1.5 rounded-lg text-blue-600 hover:text-blue-900 hover:bg-blue-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||||
|
aria-label="Editar paciente"
|
||||||
>
|
>
|
||||||
<Edit className="w-4 h-4" />
|
<Edit className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(paciente._id)}
|
onClick={() => handleDelete(paciente._id)}
|
||||||
className="text-red-600 hover:text-red-900"
|
className="inline-flex items-center p-1.5 rounded-lg text-red-600 hover:text-red-900 hover:bg-red-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-red-500"
|
||||||
|
aria-label="Excluir paciente"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -490,10 +495,18 @@ const CadastroSecretaria: React.FC = () => {
|
|||||||
|
|
||||||
{/* Modal de Formulário */}
|
{/* Modal de Formulário */}
|
||||||
{showForm && (
|
{showForm && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="cadastro-secretaria-title"
|
||||||
|
>
|
||||||
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h3 className="text-lg font-semibold mb-6">
|
<h3
|
||||||
|
id="cadastro-secretaria-title"
|
||||||
|
className="text-lg font-semibold mb-6"
|
||||||
|
>
|
||||||
{editingPaciente ? "Editar Paciente" : "Novo Paciente"}
|
{editingPaciente ? "Editar Paciente" : "Novo Paciente"}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@ -788,14 +801,14 @@ const CadastroSecretaria: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
// onClick removido, resetForm não existe
|
// onClick removido, resetForm não existe
|
||||||
className="btn-secondary"
|
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="btn-primary disabled:opacity-50"
|
className="btn-primary disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
? "Salvando..."
|
? "Salvando..."
|
||||||
|
|||||||
411
MEDICONNECT 2/src/pages/CentralAjuda.tsx
Normal file
411
MEDICONNECT 2/src/pages/CentralAjuda.tsx
Normal file
@ -0,0 +1,411 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
MessageCircle,
|
||||||
|
Phone,
|
||||||
|
Mail,
|
||||||
|
Search,
|
||||||
|
ChevronDown,
|
||||||
|
BookOpen,
|
||||||
|
Video,
|
||||||
|
FileText,
|
||||||
|
Shield,
|
||||||
|
AlertCircle,
|
||||||
|
Headphones,
|
||||||
|
ArrowLeft,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface FAQ {
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const faqs: FAQ[] = [
|
||||||
|
{
|
||||||
|
category: "Agendamento",
|
||||||
|
question: "Como agendar uma consulta?",
|
||||||
|
answer:
|
||||||
|
"Para agendar uma consulta, vá até a seção 'Agendar Consulta' no menu lateral, selecione o médico desejado, escolha uma data e horário disponível, e confirme o agendamento. Você receberá uma confirmação por e-mail.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Agendamento",
|
||||||
|
question: "Posso cancelar ou remarcar uma consulta?",
|
||||||
|
answer:
|
||||||
|
"Sim, você pode cancelar ou remarcar suas consultas em 'Minhas Consultas'. Recomendamos fazer isso com pelo menos 24 horas de antecedência para evitar taxas de cancelamento.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Agendamento",
|
||||||
|
question: "Posso agendar consulta para outra pessoa?",
|
||||||
|
answer:
|
||||||
|
"Sim, você pode agendar consultas para dependentes cadastrados em seu perfil. Basta selecioná-los no momento do agendamento.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Consultas",
|
||||||
|
question: "Como funciona a teleconsulta?",
|
||||||
|
answer:
|
||||||
|
"A teleconsulta é realizada por videochamada. No horário da consulta, acesse 'Minhas Consultas' e clique em 'Iniciar Consulta Online'. Certifique-se de ter uma conexão estável de internet e permita o acesso à câmera e microfone.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Consultas",
|
||||||
|
question: "Quanto tempo antes devo chegar para a consulta presencial?",
|
||||||
|
answer:
|
||||||
|
"Recomendamos chegar com 15 minutos de antecedência para realizar o check-in e atualizar suas informações cadastrais se necessário.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Consultas",
|
||||||
|
question: "Como acesso o histórico das minhas consultas?",
|
||||||
|
answer:
|
||||||
|
"Seu histórico completo de consultas está disponível em 'Minhas Consultas'. Lá você pode ver consultas realizadas, relatórios médicos e prescrições anteriores.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Pagamento",
|
||||||
|
question: "Quais formas de pagamento são aceitas?",
|
||||||
|
answer:
|
||||||
|
"Aceitamos cartão de crédito, débito, PIX e boleto bancário. Os pagamentos são processados de forma segura através da nossa plataforma.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Pagamento",
|
||||||
|
question: "Posso parcelar o pagamento?",
|
||||||
|
answer:
|
||||||
|
"Sim, oferecemos parcelamento em até 3x sem juros no cartão de crédito para consultas acima de R$ 150,00.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Pagamento",
|
||||||
|
question: "Como funciona o reembolso em caso de cancelamento?",
|
||||||
|
answer:
|
||||||
|
"Cancelamentos feitos com mais de 24 horas de antecedência têm reembolso integral. Cancelamentos com menos de 24 horas podem ter retenção de 30% como taxa administrativa.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Informações Médicas",
|
||||||
|
question: "Como acesso meu histórico médico?",
|
||||||
|
answer:
|
||||||
|
"Seu histórico médico, incluindo consultas anteriores, exames e receitas, pode ser acessado na seção 'Meu Perfil'. Você pode fazer download de documentos quando necessário.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Informações Médicas",
|
||||||
|
question: "Os médicos têm acesso ao meu histórico completo?",
|
||||||
|
answer:
|
||||||
|
"Sim, os médicos da plataforma têm acesso ao seu histórico de consultas e exames realizados dentro do sistema para oferecer melhor atendimento.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Informações Médicas",
|
||||||
|
question: "Posso adicionar exames feitos fora da plataforma?",
|
||||||
|
answer:
|
||||||
|
"Sim, você pode fazer upload de exames e documentos médicos externos na seção 'Meu Perfil' > 'Documentos Médicos'.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Segurança",
|
||||||
|
question: "Meus dados estão seguros?",
|
||||||
|
answer:
|
||||||
|
"Sim, utilizamos criptografia de ponta a ponta e seguimos todos os protocolos da LGPD para proteger seus dados pessoais e médicos.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Segurança",
|
||||||
|
question: "Quem tem acesso às minhas informações?",
|
||||||
|
answer:
|
||||||
|
"Apenas você, os médicos que te atendem e profissionais autorizados da clínica têm acesso às suas informações. Nunca compartilhamos seus dados com terceiros sem autorização.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Suporte",
|
||||||
|
question: "Como entro em contato com o suporte?",
|
||||||
|
answer:
|
||||||
|
"Você pode entrar em contato através do chat online, telefone (0800-123-4567) ou e-mail (secretaria.mediconnect@gmail.com). Horário de atendimento: Segunda a Sexta, das 8h às 18h.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Suporte",
|
||||||
|
question: "O que fazer em caso de emergência?",
|
||||||
|
answer:
|
||||||
|
"Em casos de emergência, procure imediatamente o pronto-socorro mais próximo ou ligue para 192 (SAMU). Nossa plataforma é destinada a consultas agendadas e não substitui o atendimento de emergência.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Médicos",
|
||||||
|
question: "Posso escolher qualquer médico?",
|
||||||
|
answer:
|
||||||
|
"Sim, você pode escolher qualquer médico disponível na plataforma. Recomendamos verificar a especialidade, avaliações de outros pacientes e disponibilidade antes de agendar.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Médicos",
|
||||||
|
question: "Como avaliar um médico após a consulta?",
|
||||||
|
answer:
|
||||||
|
"Após cada consulta, você receberá um convite por e-mail para avaliar o atendimento. Você também pode avaliar acessando 'Minhas Consultas' e selecionando a consulta realizada.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CentralAjuda: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string>("Todas");
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
"Todas",
|
||||||
|
...Array.from(new Set(faqs.map((faq) => faq.category))),
|
||||||
|
];
|
||||||
|
|
||||||
|
const filteredFaqs = faqs.filter((faq) => {
|
||||||
|
const matchesSearch =
|
||||||
|
faq.question.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
faq.answer.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
const matchesCategory =
|
||||||
|
selectedCategory === "Todas" || faq.category === selectedCategory;
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleFaq = (index: number) => {
|
||||||
|
setExpandedIndex(expandedIndex === index ? null : index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-gradient-to-br from-blue-600 to-blue-400 dark:from-blue-700 dark:to-blue-500 text-white py-12">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate("/acompanhamento")}
|
||||||
|
className="absolute left-0 top-0 flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg transition-colors backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
<span>Voltar ao Painel</span>
|
||||||
|
</button>
|
||||||
|
<div className="text-center">
|
||||||
|
<Headphones className="h-16 w-16 mx-auto mb-4" />
|
||||||
|
<h1 className="text-4xl font-bold mb-2">Central de Ajuda</h1>
|
||||||
|
<p className="text-blue-100 text-lg">
|
||||||
|
Encontre respostas para suas dúvidas rapidamente
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{/* Quick Contact Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 -mt-16 mb-8">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="h-14 w-14 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mb-4">
|
||||||
|
<MessageCircle className="h-7 w-7 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Chat Online
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
|
||||||
|
Fale conosco em tempo real
|
||||||
|
</p>
|
||||||
|
<button className="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Iniciar Chat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="h-14 w-14 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center mb-4">
|
||||||
|
<Phone className="h-7 w-7 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Telefone
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
|
||||||
|
0800-123-4567
|
||||||
|
</p>
|
||||||
|
<button className="w-full border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Ligar Agora
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="h-14 w-14 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mb-4">
|
||||||
|
<Mail className="h-7 w-7 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
E-mail
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
|
||||||
|
secretaria.mediconnect@gmail.com
|
||||||
|
</p>
|
||||||
|
<button className="w-full border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Enviar E-mail
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filter */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 mb-8">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
Perguntas Frequentes
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
Busque por palavras-chave ou navegue pelas categorias
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Search Box */}
|
||||||
|
<div className="relative mb-6">
|
||||||
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar em perguntas frequentes..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pl-12 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<button
|
||||||
|
key={category}
|
||||||
|
onClick={() => setSelectedCategory(category)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
selectedCategory === category
|
||||||
|
? "bg-blue-600 text-white"
|
||||||
|
: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FAQ Accordion */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 mb-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredFaqs.map((faq, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleFaq(index)}
|
||||||
|
className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3 flex-1">
|
||||||
|
<span className="px-2 py-1 bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-400 text-xs font-medium rounded">
|
||||||
|
{faq.category}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-900 dark:text-white font-medium">
|
||||||
|
{faq.question}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-5 w-5 text-gray-400 transition-transform flex-shrink-0 ml-4 ${
|
||||||
|
expandedIndex === index ? "transform rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{expandedIndex === index && (
|
||||||
|
<div className="px-4 pb-4 pt-2 bg-gray-50 dark:bg-gray-700/50">
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 leading-relaxed pl-16">
|
||||||
|
{faq.answer}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredFaqs.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<AlertCircle className="h-12 w-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 text-lg">
|
||||||
|
Nenhuma pergunta encontrada para "{searchTerm}"
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-400 dark:text-gray-500 text-sm mt-2">
|
||||||
|
Tente outro termo de busca ou entre em contato conosco
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Additional Resources */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
Recursos Adicionais
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<button className="flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-left">
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-blue-100 dark:bg-blue-900 flex items-center justify-center flex-shrink-0">
|
||||||
|
<BookOpen className="h-6 w-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Guia do Usuário
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Manual completo da plataforma
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-left">
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-red-100 dark:bg-red-900 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Video className="h-6 w-6 text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Vídeos Tutoriais
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Aprenda assistindo
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-left">
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-green-100 dark:bg-green-900 flex items-center justify-center flex-shrink-0">
|
||||||
|
<FileText className="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Termos de Uso
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Leia nossos termos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-left">
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-purple-100 dark:bg-purple-900 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Shield className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Política de Privacidade
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Como protegemos seus dados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Section */}
|
||||||
|
<div className="bg-gradient-to-br from-blue-600 to-blue-400 dark:from-blue-700 dark:to-blue-500 rounded-xl shadow-md p-8 mt-8 text-center text-white">
|
||||||
|
<h2 className="text-2xl font-bold mb-3">Ainda tem dúvidas?</h2>
|
||||||
|
<p className="text-blue-100 mb-6">
|
||||||
|
Nossa equipe está pronta para ajudar você
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<button className="bg-white text-blue-600 hover:bg-blue-50 px-6 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
Falar com Suporte
|
||||||
|
</button>
|
||||||
|
<button className="border-2 border-white text-white hover:bg-white/10 px-6 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
Agendar Retorno de Ligação
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CentralAjuda;
|
||||||
415
MEDICONNECT 2/src/pages/CentralAjudaMedico.tsx
Normal file
415
MEDICONNECT 2/src/pages/CentralAjudaMedico.tsx
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
MessageCircle,
|
||||||
|
Phone,
|
||||||
|
Mail,
|
||||||
|
Search,
|
||||||
|
ChevronDown,
|
||||||
|
BookOpen,
|
||||||
|
Video,
|
||||||
|
FileText,
|
||||||
|
Shield,
|
||||||
|
AlertCircle,
|
||||||
|
Headphones,
|
||||||
|
ArrowLeft,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface FAQ {
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const faqsMedico: FAQ[] = [
|
||||||
|
{
|
||||||
|
category: "Agenda",
|
||||||
|
question: "Como gerenciar minha disponibilidade?",
|
||||||
|
answer:
|
||||||
|
"Acesse 'Disponibilidade' no menu lateral para configurar seus horários de atendimento. Você pode definir horários recorrentes e adicionar exceções para datas específicas.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Agenda",
|
||||||
|
question: "Posso bloquear horários específicos?",
|
||||||
|
answer:
|
||||||
|
"Sim, na seção 'Disponibilidade', você pode adicionar exceções para bloquear horários em datas específicas, como férias ou compromissos pessoais.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Agenda",
|
||||||
|
question: "Como visualizar minhas próximas consultas?",
|
||||||
|
answer:
|
||||||
|
"No Dashboard, você verá um resumo das próximas consultas. Para ver detalhes completos, acesse a seção 'Consultas' no menu lateral.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Consultas",
|
||||||
|
question: "Como realizar uma teleconsulta?",
|
||||||
|
answer:
|
||||||
|
"No horário da consulta, acesse 'Consultas', localize o agendamento e clique em 'Iniciar Consulta Online'. Certifique-se de ter uma boa conexão de internet.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Consultas",
|
||||||
|
question: "Posso cancelar ou remarcar uma consulta?",
|
||||||
|
answer:
|
||||||
|
"Sim, acesse 'Consultas', selecione o agendamento desejado e escolha a opção de cancelar ou remarcar. O paciente será notificado automaticamente.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Consultas",
|
||||||
|
question: "Como registro o prontuário após a consulta?",
|
||||||
|
answer:
|
||||||
|
"Após finalizar a consulta, clique em 'Adicionar Relatório' para registrar anamnese, diagnóstico, prescrição e orientações.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Relatórios",
|
||||||
|
question: "Como criar relatórios médicos?",
|
||||||
|
answer:
|
||||||
|
"Na seção 'Relatórios', você pode criar novos documentos médicos incluindo anamnese, exame físico, diagnóstico e prescrição. Os relatórios ficam vinculados ao histórico do paciente.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Relatórios",
|
||||||
|
question: "Os pacientes têm acesso aos relatórios?",
|
||||||
|
answer:
|
||||||
|
"Sim, após você criar e salvar um relatório, o paciente pode acessá-lo em seu painel pessoal.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Relatórios",
|
||||||
|
question: "Posso editar um relatório após criado?",
|
||||||
|
answer:
|
||||||
|
"Sim, você pode editar relatórios acessando a seção 'Relatórios' e selecionando o documento desejado.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Pacientes",
|
||||||
|
question: "Como acesso o histórico completo de um paciente?",
|
||||||
|
answer:
|
||||||
|
"Durante ou após uma consulta, clique no nome do paciente para ver seu histórico completo de consultas, exames e relatórios anteriores.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Pacientes",
|
||||||
|
question: "Como adicionar anotações privadas sobre um paciente?",
|
||||||
|
answer:
|
||||||
|
"No prontuário do paciente, você pode adicionar observações que ficam visíveis apenas para você e outros médicos autorizados.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Pagamento",
|
||||||
|
question: "Como recebo pelos atendimentos?",
|
||||||
|
answer:
|
||||||
|
"Os pagamentos são processados automaticamente pela plataforma. Você receberá seus honorários na conta bancária cadastrada de acordo com o cronograma estabelecido.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Pagamento",
|
||||||
|
question: "Posso definir valores diferentes por tipo de consulta?",
|
||||||
|
answer:
|
||||||
|
"Sim, você pode configurar valores específicos para consultas presenciais e teleconsultas em 'Configurações'.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Sistema",
|
||||||
|
question: "Como atualizar meus dados cadastrais?",
|
||||||
|
answer:
|
||||||
|
"Acesse 'Configurações' no menu lateral para atualizar suas informações pessoais, dados bancários, especialidades e outras configurações.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Sistema",
|
||||||
|
question: "Como funciona a notificação de novos agendamentos?",
|
||||||
|
answer:
|
||||||
|
"Você receberá notificações por e-mail e SMS sempre que um novo agendamento for realizado. Você pode configurar suas preferências em 'Configurações'.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Sistema",
|
||||||
|
question: "Meus dados estão seguros?",
|
||||||
|
answer:
|
||||||
|
"Sim, utilizamos criptografia de ponta a ponta e seguimos rigorosamente os protocolos da LGPD para proteger todos os dados médicos e pessoais.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Suporte",
|
||||||
|
question: "Como reportar problemas técnicos?",
|
||||||
|
answer:
|
||||||
|
"Entre em contato através do chat online, telefone (0800-123-4567) ou e-mail (secretaria.mediconnect@gmail.com). Nossa equipe técnica está disponível de segunda a sexta, das 8h às 18h.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: "Suporte",
|
||||||
|
question: "Existe treinamento para usar a plataforma?",
|
||||||
|
answer:
|
||||||
|
"Sim, oferecemos vídeos tutoriais e documentação completa. Você também pode agendar uma sessão de treinamento personalizada com nossa equipe.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const CentralAjudaMedico: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [expandedIndex, setExpandedIndex] = useState<number | null>(null);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string>("Todas");
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
"Todas",
|
||||||
|
...Array.from(new Set(faqsMedico.map((faq) => faq.category))),
|
||||||
|
];
|
||||||
|
|
||||||
|
const filteredFaqs = faqsMedico.filter((faq) => {
|
||||||
|
const matchesSearch =
|
||||||
|
faq.question.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
faq.answer.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
const matchesCategory =
|
||||||
|
selectedCategory === "Todas" || faq.category === selectedCategory;
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleFaq = (index: number) => {
|
||||||
|
setExpandedIndex(expandedIndex === index ? null : index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-gradient-to-br from-indigo-600 to-indigo-400 dark:from-indigo-700 dark:to-indigo-500 text-white py-12">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate("/painel-medico")}
|
||||||
|
className="absolute left-0 top-0 flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg transition-colors backdrop-blur-sm"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-5 w-5" />
|
||||||
|
<span>Voltar ao Painel</span>
|
||||||
|
</button>
|
||||||
|
<div className="text-center">
|
||||||
|
<Headphones className="h-16 w-16 mx-auto mb-4" />
|
||||||
|
<h1 className="text-4xl font-bold mb-2">
|
||||||
|
Central de Ajuda - Médicos
|
||||||
|
</h1>
|
||||||
|
<p className="text-indigo-100 text-lg">
|
||||||
|
Suporte especializado para profissionais de saúde
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
{/* Quick Contact Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 -mt-16 mb-8">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="h-14 w-14 rounded-full bg-indigo-100 dark:bg-indigo-900 flex items-center justify-center mb-4">
|
||||||
|
<MessageCircle className="h-7 w-7 text-indigo-600 dark:text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Chat Prioritário
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
|
||||||
|
Suporte especializado para médicos
|
||||||
|
</p>
|
||||||
|
<button className="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Iniciar Chat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="h-14 w-14 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center mb-4">
|
||||||
|
<Phone className="h-7 w-7 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
Linha Direta
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
|
||||||
|
0800-123-4567 (Opção 2)
|
||||||
|
</p>
|
||||||
|
<button className="w-full border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Ligar Agora
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow">
|
||||||
|
<div className="flex flex-col items-center text-center">
|
||||||
|
<div className="h-14 w-14 rounded-full bg-purple-100 dark:bg-purple-900 flex items-center justify-center mb-4">
|
||||||
|
<Mail className="h-7 w-7 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
|
||||||
|
E-mail Suporte
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm mb-4">
|
||||||
|
secretaria.mediconnect@gmail.com
|
||||||
|
</p>
|
||||||
|
<button className="w-full border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 py-2 px-4 rounded-lg transition-colors">
|
||||||
|
Enviar E-mail
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search and Filter */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 mb-8">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||||||
|
Perguntas Frequentes
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
Dúvidas comuns de médicos e profissionais de saúde
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Search Box */}
|
||||||
|
<div className="relative mb-6">
|
||||||
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Buscar em perguntas frequentes..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pl-12 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<button
|
||||||
|
key={category}
|
||||||
|
onClick={() => setSelectedCategory(category)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
selectedCategory === category
|
||||||
|
? "bg-indigo-600 text-white"
|
||||||
|
: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FAQ Accordion */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 mb-8">
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredFaqs.map((faq, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleFaq(index)}
|
||||||
|
className="w-full flex items-center justify-between p-4 text-left hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3 flex-1">
|
||||||
|
<span className="px-2 py-1 bg-indigo-100 dark:bg-indigo-900 text-indigo-600 dark:text-indigo-400 text-xs font-medium rounded">
|
||||||
|
{faq.category}
|
||||||
|
</span>
|
||||||
|
<span className="text-gray-900 dark:text-white font-medium">
|
||||||
|
{faq.question}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ChevronDown
|
||||||
|
className={`h-5 w-5 text-gray-400 transition-transform flex-shrink-0 ml-4 ${
|
||||||
|
expandedIndex === index ? "transform rotate-180" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{expandedIndex === index && (
|
||||||
|
<div className="px-4 pb-4 pt-2 bg-gray-50 dark:bg-gray-700/50">
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 leading-relaxed pl-16">
|
||||||
|
{faq.answer}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredFaqs.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<AlertCircle className="h-12 w-12 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 text-lg">
|
||||||
|
Nenhuma pergunta encontrada para "{searchTerm}"
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-400 dark:text-gray-500 text-sm mt-2">
|
||||||
|
Tente outro termo de busca ou entre em contato conosco
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Additional Resources */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 mb-8">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
Recursos para Médicos
|
||||||
|
</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<button className="flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-left">
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-indigo-100 dark:bg-indigo-900 flex items-center justify-center flex-shrink-0">
|
||||||
|
<BookOpen className="h-6 w-6 text-indigo-600 dark:text-indigo-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Manual do Médico
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Guia completo da plataforma
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-left">
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-red-100 dark:bg-red-900 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Video className="h-6 w-6 text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Treinamentos
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Vídeos e webinars
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-left">
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-green-100 dark:bg-green-900 flex items-center justify-center flex-shrink-0">
|
||||||
|
<FileText className="h-6 w-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Modelos de Relatórios
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Templates prontos
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="flex items-center gap-4 p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-left">
|
||||||
|
<div className="h-12 w-12 rounded-lg bg-purple-100 dark:bg-purple-900 flex items-center justify-center flex-shrink-0">
|
||||||
|
<Shield className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Conformidade LGPD
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Segurança e privacidade
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Section */}
|
||||||
|
<div className="bg-gradient-to-br from-indigo-600 to-indigo-400 dark:from-indigo-700 dark:to-indigo-500 rounded-xl shadow-md p-8 text-center text-white">
|
||||||
|
<h2 className="text-2xl font-bold mb-3">
|
||||||
|
Precisa de ajuda específica?
|
||||||
|
</h2>
|
||||||
|
<p className="text-indigo-100 mb-6">
|
||||||
|
Nossa equipe de suporte médico está à disposição
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<button className="bg-white text-indigo-600 hover:bg-indigo-50 px-6 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
Falar com Especialista
|
||||||
|
</button>
|
||||||
|
<button className="border-2 border-white text-white hover:bg-white/10 px-6 py-3 rounded-lg font-medium transition-colors">
|
||||||
|
Solicitar Treinamento
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CentralAjudaMedico;
|
||||||
16
MEDICONNECT 2/src/pages/CentralAjudaRouter.tsx
Normal file
16
MEDICONNECT 2/src/pages/CentralAjudaRouter.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useAuth } from "../hooks/useAuth";
|
||||||
|
import CentralAjuda from "./CentralAjuda";
|
||||||
|
import CentralAjudaMedico from "./CentralAjudaMedico";
|
||||||
|
|
||||||
|
const CentralAjudaRouter: React.FC = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
|
// Se for médico, gestor ou admin, mostra a central de ajuda para médicos
|
||||||
|
const isMedico =
|
||||||
|
user?.role && ["medico", "gestor", "admin"].includes(user.role);
|
||||||
|
|
||||||
|
return isMedico ? <CentralAjudaMedico /> : <CentralAjuda />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CentralAjudaRouter;
|
||||||
@ -153,7 +153,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={carregarUsuarios}
|
onClick={carregarUsuarios}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 disabled:opacity-50"
|
className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`w-4 h-4 ${loading ? "animate-spin" : ""}`}
|
className={`w-4 h-4 ${loading ? "animate-spin" : ""}`}
|
||||||
@ -170,7 +170,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
placeholder="Buscar por nome ou email..."
|
placeholder="Buscar por nome ou email..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:border-indigo-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -183,9 +183,9 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-auto max-h-[70vh]">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gradient-to-r from-indigo-600 to-purple-600 text-white">
|
<thead className="bg-gradient-to-r from-indigo-600 to-purple-600 text-white sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-6 py-3 text-left text-sm font-semibold">
|
<th className="px-6 py-3 text-left text-sm font-semibold">
|
||||||
Nome
|
Nome
|
||||||
@ -223,8 +223,13 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
usuariosFiltrados.map((user) => (
|
usuariosFiltrados.map((user, idx) => (
|
||||||
<tr key={user.user.id} className="hover:bg-gray-50">
|
<tr
|
||||||
|
key={user.user.id}
|
||||||
|
className={`hover:bg-gray-50 ${
|
||||||
|
idx % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
<div className="font-medium text-gray-900">
|
<div className="font-medium text-gray-900">
|
||||||
{user.profile?.full_name || "Sem nome"}
|
{user.profile?.full_name || "Sem nome"}
|
||||||
@ -284,7 +289,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEdit(user)}
|
onClick={() => handleEdit(user)}
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
title="Editar"
|
title="Editar"
|
||||||
>
|
>
|
||||||
<Edit className="w-4 h-4" />
|
<Edit className="w-4 h-4" />
|
||||||
@ -300,7 +305,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
setUserRoles(result.data);
|
setUserRoles(result.data);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="p-2 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
|
className="p-2 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
|
||||||
title="Gerenciar Roles"
|
title="Gerenciar Roles"
|
||||||
>
|
>
|
||||||
<Shield className="w-4 h-4" />
|
<Shield className="w-4 h-4" />
|
||||||
@ -312,10 +317,10 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
!!user.profile?.disabled
|
!!user.profile?.disabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className={`p-2 rounded-lg transition-colors ${
|
className={`p-2 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${
|
||||||
user.profile?.disabled
|
user.profile?.disabled
|
||||||
? "text-green-600 hover:bg-green-50"
|
? "text-green-600 hover:bg-green-50 focus-visible:ring-green-500"
|
||||||
: "text-orange-600 hover:bg-orange-50"
|
: "text-orange-600 hover:bg-orange-50 focus-visible:ring-orange-500"
|
||||||
}`}
|
}`}
|
||||||
title={
|
title={
|
||||||
user.profile?.disabled
|
user.profile?.disabled
|
||||||
@ -336,7 +341,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
user.profile?.full_name || user.user.email
|
user.profile?.full_name || user.user.email
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
|
||||||
title="Deletar"
|
title="Deletar"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
@ -355,9 +360,17 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
|
|
||||||
{/* Modal de Edição */}
|
{/* Modal de Edição */}
|
||||||
{editingUser && (
|
{editingUser && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="editar-usuario-title"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl border border-gray-200 max-w-md w-full p-6">
|
||||||
|
<h2
|
||||||
|
id="editar-usuario-title"
|
||||||
|
className="text-xl font-bold text-gray-900 mb-4"
|
||||||
|
>
|
||||||
Editar Usuário
|
Editar Usuário
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -372,7 +385,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditForm({ ...editForm, full_name: e.target.value })
|
setEditForm({ ...editForm, full_name: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:border-indigo-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -386,7 +399,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditForm({ ...editForm, email: e.target.value })
|
setEditForm({ ...editForm, email: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:border-indigo-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -400,7 +413,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditForm({ ...editForm, phone: e.target.value })
|
setEditForm({ ...editForm, phone: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:border-indigo-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -408,13 +421,13 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
<div className="flex gap-3 mt-6">
|
<div className="flex gap-3 mt-6">
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingUser(null)}
|
onClick={() => setEditingUser(null)}
|
||||||
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSaveEdit}
|
onClick={handleSaveEdit}
|
||||||
className="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
|
className="flex-1 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Salvar
|
Salvar
|
||||||
</button>
|
</button>
|
||||||
@ -425,9 +438,17 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
|
|
||||||
{/* Modal de Gerenciar Roles */}
|
{/* Modal de Gerenciar Roles */}
|
||||||
{managingRolesUser && (
|
{managingRolesUser && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-2 flex items-center gap-2">
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="gerenciar-roles-title"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl border border-gray-200 max-w-md w-full p-6">
|
||||||
|
<h2
|
||||||
|
id="gerenciar-roles-title"
|
||||||
|
className="text-xl font-bold text-gray-900 mb-2 flex items-center gap-2"
|
||||||
|
>
|
||||||
<Shield className="w-5 h-5 text-purple-600" />
|
<Shield className="w-5 h-5 text-purple-600" />
|
||||||
Gerenciar Roles
|
Gerenciar Roles
|
||||||
</h2>
|
</h2>
|
||||||
@ -501,7 +522,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
<select
|
<select
|
||||||
value={newRole}
|
value={newRole}
|
||||||
onChange={(e) => setNewRole(e.target.value)}
|
onChange={(e) => setNewRole(e.target.value)}
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-sm"
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Selecione um role...</option>
|
<option value="">Selecione um role...</option>
|
||||||
<option value="admin">Admin</option>
|
<option value="admin">Admin</option>
|
||||||
@ -542,7 +563,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!newRole}
|
disabled={!newRole}
|
||||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
Adicionar
|
Adicionar
|
||||||
@ -557,7 +578,7 @@ const GerenciarUsuarios: React.FC = () => {
|
|||||||
setUserRoles([]);
|
setUserRoles([]);
|
||||||
setNewRole("");
|
setNewRole("");
|
||||||
}}
|
}}
|
||||||
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Fechar
|
Fechar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -104,7 +104,7 @@ const Home: React.FC = () => {
|
|||||||
<div className="flex flex-col sm:flex-row gap-3 md:gap-4 justify-center items-center">
|
<div className="flex flex-col sm:flex-row gap-3 md:gap-4 justify-center items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleCTA("Agendar consulta", "/paciente")}
|
onClick={() => handleCTA("Agendar consulta", "/paciente")}
|
||||||
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-white text-blue-700 rounded-lg font-semibold hover:bg-blue-50 hover:shadow-xl hover:scale-105 active:scale-95 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600"
|
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-white text-blue-700 rounded-lg font-semibold hover:bg-blue-50 hover:shadow-xl hover:scale-105 active:scale-95 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/80 focus-visible:ring-offset-blue-600"
|
||||||
aria-label={i18n.t(
|
aria-label={i18n.t(
|
||||||
"home.actionCards.scheduleAppointment.ctaAriaLabel"
|
"home.actionCards.scheduleAppointment.ctaAriaLabel"
|
||||||
)}
|
)}
|
||||||
@ -122,7 +122,7 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => handleCTA("Ver próximas consultas", "/consultas")}
|
onClick={() => handleCTA("Ver próximas consultas", "/consultas")}
|
||||||
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-blue-700 text-white rounded-lg font-semibold hover:bg-blue-800 hover:shadow-xl hover:scale-105 active:scale-95 border-2 border-white/20 transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-white/50 focus:ring-offset-2 focus:ring-offset-blue-600"
|
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-blue-700 text-white rounded-lg font-semibold hover:bg-blue-800 hover:shadow-xl hover:scale-105 active:scale-95 border-2 border-white/20 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/80 focus-visible:ring-offset-blue-600"
|
||||||
aria-label="Ver lista de próximas consultas"
|
aria-label="Ver lista de próximas consultas"
|
||||||
>
|
>
|
||||||
<Clock className="w-5 h-5 mr-2" aria-hidden="true" />
|
<Clock className="w-5 h-5 mr-2" aria-hidden="true" />
|
||||||
@ -266,7 +266,7 @@ const ActionCard: React.FC<ActionCardProps> = ({
|
|||||||
onAction,
|
onAction,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg shadow-md p-5 md:p-6 hover:shadow-xl transition-all duration-200 group border border-gray-100">
|
<div className="bg-white rounded-lg shadow-md p-5 md:p-6 hover:shadow-xl transition-all duration-200 group border border-gray-100 focus-within:ring-2 focus-within:ring-blue-500/50 focus-within:ring-offset-2">
|
||||||
<div
|
<div
|
||||||
className={`w-12 h-12 ${iconBgColor} rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform`}
|
className={`w-12 h-12 ${iconBgColor} rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition-transform`}
|
||||||
>
|
>
|
||||||
@ -278,7 +278,7 @@ const ActionCard: React.FC<ActionCardProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={onAction}
|
onClick={onAction}
|
||||||
className="w-full inline-flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-blue-700 to-blue-400 hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 text-white rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-blue-300 focus:ring-offset-2 group-hover:shadow-lg"
|
className="w-full inline-flex items-center justify-center px-4 py-2.5 bg-gradient-to-r from-blue-700 to-blue-400 hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 text-white rounded-lg font-medium transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/80 group-hover:shadow-lg"
|
||||||
aria-label={ctaAriaLabel}
|
aria-label={ctaAriaLabel}
|
||||||
>
|
>
|
||||||
{ctaLabel}
|
{ctaLabel}
|
||||||
|
|||||||
@ -46,30 +46,38 @@ const ListaMedicos: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-2xl font-bold mb-4 flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Stethoscope className="w-6 h-6 text-indigo-600" /> Médicos Cadastrados
|
<Stethoscope className="w-6 h-6 text-indigo-600" />
|
||||||
</h2>
|
<h2 className="text-2xl font-bold">Médicos Cadastrados</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
{loading && <div className="text-gray-500">Carregando médicos...</div>}
|
{loading && <div className="text-gray-500">Carregando médicos...</div>}
|
||||||
|
|
||||||
{!loading && error && (
|
{!loading && error && (
|
||||||
<div className="flex items-center gap-2 text-red-600 bg-red-50 border border-red-200 p-3 rounded">
|
<div className="flex items-center gap-2 text-red-700 bg-red-50 border border-red-200 p-3 rounded-lg">
|
||||||
<AlertTriangle className="w-5 h-5" />
|
<AlertTriangle className="w-5 h-5" />
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && medicos.length === 0 && (
|
{!loading && !error && medicos.length === 0 && (
|
||||||
<div className="text-gray-500">Nenhum médico cadastrado.</div>
|
<div className="text-gray-500">Nenhum médico cadastrado.</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && medicos.length > 0 && (
|
{!loading && !error && medicos.length > 0 && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{medicos.map((medico) => (
|
{medicos.map((medico) => (
|
||||||
<div
|
<article
|
||||||
key={medico.id}
|
key={medico.id}
|
||||||
className="bg-white rounded-lg shadow-md p-6 flex flex-col gap-2"
|
className="bg-white rounded-xl shadow border border-gray-200 p-6 flex flex-col gap-3 hover:shadow-md transition-shadow focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<header className="flex items-center gap-2">
|
||||||
<Stethoscope className="w-5 h-5 text-indigo-600" />
|
<Stethoscope className="w-5 h-5 text-indigo-600" />
|
||||||
<span className="font-semibold text-lg">{medico.nome}</span>
|
<h3 className="font-semibold text-lg text-gray-900">
|
||||||
</div>
|
{medico.nome}
|
||||||
|
</h3>
|
||||||
|
</header>
|
||||||
<div className="text-sm text-gray-700">
|
<div className="text-sm text-gray-700">
|
||||||
<strong>Especialidade:</strong> {medico.especialidade}
|
<strong>Especialidade:</strong> {medico.especialidade}
|
||||||
</div>
|
</div>
|
||||||
@ -84,7 +92,7 @@ const ListaMedicos: React.FC = () => {
|
|||||||
<Phone className="w-4 h-4" /> {medico.telefone}
|
<Phone className="w-4 h-4" /> {medico.telefone}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -76,10 +76,13 @@ const ListaPacientes: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
{!loading && !error && pacientes.length > 0 && (
|
{!loading && !error && pacientes.length > 0 && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{pacientes.map((paciente) => (
|
{pacientes.map((paciente, idx) => (
|
||||||
<div
|
<div
|
||||||
key={paciente.id}
|
key={paciente.id}
|
||||||
className="bg-white rounded-lg shadow-md p-6 flex flex-col gap-2"
|
className={`rounded-lg p-6 flex flex-col gap-2 transition-colors border border-gray-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 ${
|
||||||
|
idx % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||||
|
}`}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Users className="w-5 h-5 text-blue-600" />
|
<Users className="w-5 h-5 text-blue-600" />
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from "react";
|
||||||
import { UserPlus, Mail, Phone } from 'lucide-react';
|
import { UserPlus, Mail, Phone } from "lucide-react";
|
||||||
|
|
||||||
interface Secretaria {
|
interface Secretaria {
|
||||||
nome: string;
|
nome: string;
|
||||||
@ -13,7 +13,7 @@ const ListaSecretarias: React.FC = () => {
|
|||||||
const [secretarias, setSecretarias] = useState<Secretaria[]>([]);
|
const [secretarias, setSecretarias] = useState<Secretaria[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const lista = JSON.parse(localStorage.getItem('secretarias') || '[]');
|
const lista = JSON.parse(localStorage.getItem("secretarias") || "[]");
|
||||||
setSecretarias(lista);
|
setSecretarias(lista);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -27,19 +27,29 @@ const ListaSecretarias: React.FC = () => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{secretarias.map((sec, idx) => (
|
{secretarias.map((sec, idx) => (
|
||||||
<div key={idx} className="bg-white rounded-lg shadow-md p-6 flex flex-col gap-2">
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`rounded-lg p-6 flex flex-col gap-2 transition-colors border border-gray-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-green-500/50 ${
|
||||||
|
idx % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||||
|
}`}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<UserPlus className="w-5 h-5 text-green-600" />
|
<UserPlus className="w-5 h-5 text-green-600" />
|
||||||
<span className="font-semibold text-lg">{sec.nome}</span>
|
<span className="font-semibold text-lg">{sec.nome}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-700"><strong>CPF:</strong> {sec.cpf}</div>
|
<div className="text-sm text-gray-700">
|
||||||
|
<strong>CPF:</strong> {sec.cpf}
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-700">
|
<div className="flex items-center gap-2 text-sm text-gray-700">
|
||||||
<Mail className="w-4 h-4" /> {sec.email}
|
<Mail className="w-4 h-4" /> {sec.email}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm text-gray-700">
|
<div className="flex items-center gap-2 text-sm text-gray-700">
|
||||||
<Phone className="w-4 h-4" /> {sec.telefone}
|
<Phone className="w-4 h-4" /> {sec.telefone}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500">Cadastrada em: {new Date(sec.criadoEm).toLocaleString()}</div>
|
<div className="text-xs text-gray-500">
|
||||||
|
Cadastrada em: {new Date(sec.criadoEm).toLocaleString()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -144,7 +144,7 @@ const LoginMedico: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full bg-gradient-to-r from-indigo-600 to-indigo-400 text-white py-3 px-4 rounded-lg font-medium hover:from-indigo-700 hover:to-indigo-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
|
className="w-full bg-gradient-to-r from-indigo-600 to-indigo-400 text-white py-3 px-4 rounded-lg font-medium hover:from-indigo-700 hover:to-indigo-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{loading ? "Entrando..." : "Entrar"}
|
{loading ? "Entrando..." : "Entrar"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -359,7 +359,7 @@ const LoginPaciente: React.FC = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleLoginLocal}
|
onClick={handleLoginLocal}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
|
className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{loading ? "Entrando..." : "Entrar"}
|
{loading ? "Entrando..." : "Entrar"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -144,7 +144,7 @@ const LoginSecretaria: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full bg-gradient-to-r from-green-600 to-green-400 text-white py-3 px-4 rounded-lg font-medium hover:from-green-700 hover:to-green-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200"
|
className="w-full bg-gradient-to-r from-green-600 to-green-400 text-white py-3 px-4 rounded-lg font-medium hover:from-green-700 hover:to-green-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{loading ? "Entrando..." : "Entrar"}
|
{loading ? "Entrando..." : "Entrar"}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -584,7 +584,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 p-6">
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 p-6">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
<div className="bg-white rounded-xl shadow p-6 mb-6 border border-gray-200">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
||||||
@ -599,11 +599,19 @@ const PainelAdmin: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="bg-white rounded-lg shadow-md mb-6">
|
<div className="bg-white rounded-xl shadow border border-gray-200 mb-6">
|
||||||
<div className="flex border-b">
|
<div
|
||||||
|
className="flex border-b"
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Seções do administrador"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("pacientes")}
|
onClick={() => setActiveTab("pacientes")}
|
||||||
className={`flex items-center gap-2 px-6 py-4 font-medium transition-colors ${
|
id="tab-pacientes-tab"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === "pacientes"}
|
||||||
|
aria-controls="tab-pacientes-panel"
|
||||||
|
className={`flex items-center gap-2 px-6 py-4 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
|
||||||
activeTab === "pacientes"
|
activeTab === "pacientes"
|
||||||
? "border-b-2 border-blue-600 text-blue-600"
|
? "border-b-2 border-blue-600 text-blue-600"
|
||||||
: "text-gray-600 hover:text-blue-600"
|
: "text-gray-600 hover:text-blue-600"
|
||||||
@ -614,7 +622,11 @@ const PainelAdmin: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("usuarios")}
|
onClick={() => setActiveTab("usuarios")}
|
||||||
className={`flex items-center gap-2 px-6 py-4 font-medium transition-colors ${
|
id="tab-usuarios-tab"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === "usuarios"}
|
||||||
|
aria-controls="tab-usuarios-panel"
|
||||||
|
className={`flex items-center gap-2 px-6 py-4 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
|
||||||
activeTab === "usuarios"
|
activeTab === "usuarios"
|
||||||
? "border-b-2 border-blue-600 text-blue-600"
|
? "border-b-2 border-blue-600 text-blue-600"
|
||||||
: "text-gray-600 hover:text-blue-600"
|
: "text-gray-600 hover:text-blue-600"
|
||||||
@ -625,7 +637,11 @@ const PainelAdmin: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("medicos")}
|
onClick={() => setActiveTab("medicos")}
|
||||||
className={`flex items-center gap-2 px-6 py-4 font-medium transition-colors ${
|
id="tab-medicos-tab"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={activeTab === "medicos"}
|
||||||
|
aria-controls="tab-medicos-panel"
|
||||||
|
className={`flex items-center gap-2 px-6 py-4 font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 ${
|
||||||
activeTab === "medicos"
|
activeTab === "medicos"
|
||||||
? "border-b-2 border-blue-600 text-blue-600"
|
? "border-b-2 border-blue-600 text-blue-600"
|
||||||
: "text-gray-600 hover:text-blue-600"
|
: "text-gray-600 hover:text-blue-600"
|
||||||
@ -639,12 +655,16 @@ const PainelAdmin: React.FC = () => {
|
|||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{activeTab === "pacientes" && (
|
{activeTab === "pacientes" && (
|
||||||
<div>
|
<div
|
||||||
|
id="tab-pacientes-panel"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="tab-pacientes-tab"
|
||||||
|
>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-xl font-bold">Pacientes Cadastrados</h2>
|
<h2 className="text-xl font-bold">Pacientes Cadastrados</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowPacienteModal(true)}
|
onClick={() => setShowPacienteModal(true)}
|
||||||
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors"
|
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
Novo Paciente
|
Novo Paciente
|
||||||
@ -660,10 +680,12 @@ const PainelAdmin: React.FC = () => {
|
|||||||
Nenhum paciente cadastrado
|
Nenhum paciente cadastrado
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
pacientes.map((p) => (
|
pacientes.map((p, idx) => (
|
||||||
<div
|
<div
|
||||||
key={p.id}
|
key={p.id}
|
||||||
className="bg-gray-50 rounded-lg p-4 flex items-center justify-between hover:bg-gray-100 transition-colors"
|
className={`rounded-lg p-4 flex items-center justify-between transition-colors ${
|
||||||
|
idx % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||||
|
} hover:bg-gray-100`}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-lg">{p.nome}</h3>
|
<h3 className="font-semibold text-lg">{p.nome}</h3>
|
||||||
@ -677,14 +699,14 @@ const PainelAdmin: React.FC = () => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEditPaciente(p)}
|
onClick={() => handleEditPaciente(p)}
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
title="Editar"
|
title="Editar"
|
||||||
>
|
>
|
||||||
<Edit className="w-4 h-4" />
|
<Edit className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeletePaciente(p.id, p.nome)}
|
onClick={() => handleDeletePaciente(p.id, p.nome)}
|
||||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
|
||||||
title="Deletar"
|
title="Deletar"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
@ -699,14 +721,18 @@ const PainelAdmin: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "usuarios" && (
|
{activeTab === "usuarios" && (
|
||||||
<div>
|
<div
|
||||||
|
id="tab-usuarios-panel"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="tab-usuarios-tab"
|
||||||
|
>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-xl font-bold">Gerenciar Usuários</h2>
|
<h2 className="text-xl font-bold">Gerenciar Usuários</h2>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={loadUsuarios}
|
onClick={loadUsuarios}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex items-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 disabled:opacity-50"
|
className="flex items-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<RefreshCw
|
<RefreshCw
|
||||||
className={`w-4 h-4 ${loading ? "animate-spin" : ""}`}
|
className={`w-4 h-4 ${loading ? "animate-spin" : ""}`}
|
||||||
@ -715,7 +741,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowUserModal(true)}
|
onClick={() => setShowUserModal(true)}
|
||||||
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
Novo Usuário
|
Novo Usuário
|
||||||
@ -731,16 +757,16 @@ const PainelAdmin: React.FC = () => {
|
|||||||
placeholder="Buscar por nome ou email..."
|
placeholder="Buscar por nome ou email..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40 transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-8">Carregando usuários...</div>
|
<div className="text-center py-8">Carregando usuários...</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="overflow-x-auto bg-white rounded-lg border">
|
<div className="overflow-auto max-h-[70vh] bg-white rounded-lg border border-gray-200">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-gradient-to-r from-blue-600 to-purple-600 text-white">
|
<thead className="bg-gradient-to-r from-blue-600 to-purple-600 text-white sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-sm font-semibold">
|
<th className="px-4 py-3 text-left text-sm font-semibold">
|
||||||
Nome
|
Nome
|
||||||
@ -804,7 +830,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
.map((user) => (
|
.map((user) => (
|
||||||
<tr
|
<tr
|
||||||
key={user.user.id}
|
key={user.user.id}
|
||||||
className="hover:bg-gray-50"
|
className="hover:bg-gray-50 odd:bg-white even:bg-gray-50"
|
||||||
>
|
>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="font-medium text-gray-900">
|
<div className="font-medium text-gray-900">
|
||||||
@ -862,7 +888,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEditUser(user)}
|
onClick={() => handleEditUser(user)}
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
title="Editar"
|
title="Editar"
|
||||||
>
|
>
|
||||||
<Edit className="w-4 h-4" />
|
<Edit className="w-4 h-4" />
|
||||||
@ -878,7 +904,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
setUserRoles(result.data);
|
setUserRoles(result.data);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="p-2 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
|
className="p-2 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
|
||||||
title="Gerenciar Roles"
|
title="Gerenciar Roles"
|
||||||
>
|
>
|
||||||
<Shield className="w-4 h-4" />
|
<Shield className="w-4 h-4" />
|
||||||
@ -890,10 +916,10 @@ const PainelAdmin: React.FC = () => {
|
|||||||
!!user.profile?.disabled
|
!!user.profile?.disabled
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className={`p-2 rounded-lg transition-colors ${
|
className={`p-2 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${
|
||||||
user.profile?.disabled
|
user.profile?.disabled
|
||||||
? "text-green-600 hover:bg-green-50"
|
? "text-green-600 hover:bg-green-50 focus-visible:ring-green-500"
|
||||||
: "text-orange-600 hover:bg-orange-50"
|
: "text-orange-600 hover:bg-orange-50 focus-visible:ring-orange-500"
|
||||||
}`}
|
}`}
|
||||||
title={
|
title={
|
||||||
user.profile?.disabled
|
user.profile?.disabled
|
||||||
@ -915,7 +941,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
user.user.email
|
user.user.email
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
|
||||||
title="Deletar"
|
title="Deletar"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
@ -933,12 +959,16 @@ const PainelAdmin: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "medicos" && (
|
{activeTab === "medicos" && (
|
||||||
<div>
|
<div
|
||||||
|
id="tab-medicos-panel"
|
||||||
|
role="tabpanel"
|
||||||
|
aria-labelledby="tab-medicos-tab"
|
||||||
|
>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-xl font-bold">Médicos Cadastrados</h2>
|
<h2 className="text-xl font-bold">Médicos Cadastrados</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowMedicoModal(true)}
|
onClick={() => setShowMedicoModal(true)}
|
||||||
className="flex items-center gap-2 bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors"
|
className="flex items-center gap-2 bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Plus className="w-5 h-5" />
|
<Plus className="w-5 h-5" />
|
||||||
Novo Médico
|
Novo Médico
|
||||||
@ -954,10 +984,12 @@ const PainelAdmin: React.FC = () => {
|
|||||||
Nenhum médico cadastrado
|
Nenhum médico cadastrado
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
medicos.map((m) => (
|
medicos.map((m, idx) => (
|
||||||
<div
|
<div
|
||||||
key={m.id}
|
key={m.id}
|
||||||
className="bg-gray-50 rounded-lg p-4 flex items-center justify-between hover:bg-gray-100 transition-colors"
|
className={`rounded-lg p-4 flex items-center justify-between transition-colors ${
|
||||||
|
idx % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||||
|
} hover:bg-gray-100`}
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="font-semibold text-lg">
|
<h3 className="font-semibold text-lg">
|
||||||
@ -982,7 +1014,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEditMedico(m)}
|
onClick={() => handleEditMedico(m)}
|
||||||
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
title="Editar"
|
title="Editar"
|
||||||
>
|
>
|
||||||
<Edit className="w-4 h-4" />
|
<Edit className="w-4 h-4" />
|
||||||
@ -994,7 +1026,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
m.full_name || "Médico sem nome"
|
m.full_name || "Médico sem nome"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
|
||||||
title="Deletar"
|
title="Deletar"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
@ -1013,10 +1045,17 @@ const PainelAdmin: React.FC = () => {
|
|||||||
|
|
||||||
{/* Modal Paciente */}
|
{/* Modal Paciente */}
|
||||||
{showPacienteModal && (
|
{showPacienteModal && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="paciente-modal-title"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h2 className="text-2xl font-bold mb-4">Novo Paciente</h2>
|
<h2 id="paciente-modal-title" className="text-2xl font-bold mb-4">
|
||||||
|
Novo Paciente
|
||||||
|
</h2>
|
||||||
<form onSubmit={handleSavePaciente} className="space-y-4">
|
<form onSubmit={handleSavePaciente} className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
@ -1033,7 +1072,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
full_name: e.target.value,
|
full_name: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1049,7 +1088,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
social_name: e.target.value,
|
social_name: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1066,7 +1105,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
cpf: e.target.value,
|
cpf: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
|
||||||
placeholder="00000000000"
|
placeholder="00000000000"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1084,7 +1123,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
email: e.target.value,
|
email: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1101,7 +1140,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
phone_mobile: e.target.value,
|
phone_mobile: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
|
||||||
placeholder="(00) 00000-0000"
|
placeholder="(00) 00000-0000"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1118,7 +1157,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
birth_date: e.target.value,
|
birth_date: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1133,7 +1172,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
sex: e.target.value,
|
sex: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
|
||||||
>
|
>
|
||||||
<option value="">Selecione</option>
|
<option value="">Selecione</option>
|
||||||
<option value="M">Masculino</option>
|
<option value="M">Masculino</option>
|
||||||
@ -1154,7 +1193,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
blood_type: e.target.value,
|
blood_type: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-green-600 focus:border-green-600/40"
|
||||||
placeholder="A+"
|
placeholder="A+"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1168,14 +1207,14 @@ const PainelAdmin: React.FC = () => {
|
|||||||
setEditingPaciente(null);
|
setEditingPaciente(null);
|
||||||
resetFormPaciente();
|
resetFormPaciente();
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 border rounded-lg hover:bg-gray-50"
|
className="px-4 py-2 border rounded-lg hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
? editingPaciente
|
? editingPaciente
|
||||||
@ -1194,10 +1233,17 @@ const PainelAdmin: React.FC = () => {
|
|||||||
|
|
||||||
{/* Modal Usuário */}
|
{/* Modal Usuário */}
|
||||||
{showUserModal && (
|
{showUserModal && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
<div className="bg-white rounded-lg max-w-md w-full">
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="usuario-modal-title"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 shadow-xl max-w-md w-full">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h2 className="text-2xl font-bold mb-4">Novo Usuário</h2>
|
<h2 id="usuario-modal-title" className="text-2xl font-bold mb-4">
|
||||||
|
Novo Usuário
|
||||||
|
</h2>
|
||||||
<form onSubmit={handleCreateUser} className="space-y-4">
|
<form onSubmit={handleCreateUser} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">
|
<label className="block text-sm font-medium mb-1">
|
||||||
@ -1210,7 +1256,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormUser({ ...formUser, full_name: e.target.value })
|
setFormUser({ ...formUser, full_name: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1224,7 +1270,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormUser({ ...formUser, email: e.target.value })
|
setFormUser({ ...formUser, email: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1239,7 +1285,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormUser({ ...formUser, password: e.target.value })
|
setFormUser({ ...formUser, password: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1252,7 +1298,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormUser({ ...formUser, phone: e.target.value })
|
setFormUser({ ...formUser, phone: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
|
||||||
placeholder="(00) 00000-0000"
|
placeholder="(00) 00000-0000"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1269,7 +1315,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
role: e.target.value as RoleType,
|
role: e.target.value as RoleType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
|
||||||
>
|
>
|
||||||
{availableRoles.map((role) => (
|
{availableRoles.map((role) => (
|
||||||
<option key={role} value={role}>
|
<option key={role} value={role}>
|
||||||
@ -1286,14 +1332,14 @@ const PainelAdmin: React.FC = () => {
|
|||||||
setShowUserModal(false);
|
setShowUserModal(false);
|
||||||
resetFormUser();
|
resetFormUser();
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 border rounded-lg hover:bg-gray-50"
|
className="px-4 py-2 border rounded-lg hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{loading ? "Criando..." : "Criar Usuário"}
|
{loading ? "Criando..." : "Criar Usuário"}
|
||||||
</button>
|
</button>
|
||||||
@ -1306,10 +1352,17 @@ const PainelAdmin: React.FC = () => {
|
|||||||
|
|
||||||
{/* Modal Médico */}
|
{/* Modal Médico */}
|
||||||
{showMedicoModal && (
|
{showMedicoModal && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
<div className="bg-white rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto">
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="medico-modal-title"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<h2 className="text-2xl font-bold mb-4">Novo Médico</h2>
|
<h2 id="medico-modal-title" className="text-2xl font-bold mb-4">
|
||||||
|
Novo Médico
|
||||||
|
</h2>
|
||||||
<form onSubmit={handleSaveMedico} className="space-y-4">
|
<form onSubmit={handleSaveMedico} className="space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
@ -1326,7 +1379,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
full_name: e.target.value,
|
full_name: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1342,7 +1395,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
specialty: e.target.value,
|
specialty: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
placeholder="Cardiologia"
|
placeholder="Cardiologia"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1357,7 +1410,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormMedico({ ...formMedico, crm: e.target.value })
|
setFormMedico({ ...formMedico, crm: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
placeholder="123456"
|
placeholder="123456"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1371,7 +1424,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormMedico({ ...formMedico, crm_uf: e.target.value })
|
setFormMedico({ ...formMedico, crm_uf: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
>
|
>
|
||||||
{estadosBR.map((uf) => (
|
{estadosBR.map((uf) => (
|
||||||
<option key={uf} value={uf}>
|
<option key={uf} value={uf}>
|
||||||
@ -1391,7 +1444,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormMedico({ ...formMedico, cpf: e.target.value })
|
setFormMedico({ ...formMedico, cpf: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
placeholder="000.000.000-00"
|
placeholder="000.000.000-00"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1403,7 +1456,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormMedico({ ...formMedico, rg: e.target.value })
|
setFormMedico({ ...formMedico, rg: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1417,7 +1470,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormMedico({ ...formMedico, email: e.target.value })
|
setFormMedico({ ...formMedico, email: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1433,7 +1486,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
phone_mobile: e.target.value,
|
phone_mobile: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
placeholder="(00) 00000-0000"
|
placeholder="(00) 00000-0000"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1450,7 +1503,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
birth_date: e.target.value,
|
birth_date: e.target.value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full border rounded-lg px-3 py-2"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -1464,7 +1517,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
active: e.target.checked,
|
active: e.target.checked,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-4 h-4"
|
className="w-4 h-4 focus:outline-none focus:ring-2 focus:ring-purple-600"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-medium">Ativo</span>
|
<span className="text-sm font-medium">Ativo</span>
|
||||||
</label>
|
</label>
|
||||||
@ -1479,14 +1532,14 @@ const PainelAdmin: React.FC = () => {
|
|||||||
setEditingMedico(null);
|
setEditingMedico(null);
|
||||||
resetFormMedico();
|
resetFormMedico();
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 border rounded-lg hover:bg-gray-50"
|
className="px-4 py-2 border rounded-lg hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{loading
|
{loading
|
||||||
? editingMedico
|
? editingMedico
|
||||||
@ -1505,9 +1558,17 @@ const PainelAdmin: React.FC = () => {
|
|||||||
|
|
||||||
{/* Modal de Edição de Usuário */}
|
{/* Modal de Edição de Usuário */}
|
||||||
{editingUser && (
|
{editingUser && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-4">
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="editar-usuario-title"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl border border-gray-200 max-w-md w-full p-6">
|
||||||
|
<h2
|
||||||
|
id="editar-usuario-title"
|
||||||
|
className="text-xl font-bold text-gray-900 mb-4"
|
||||||
|
>
|
||||||
Editar Usuário
|
Editar Usuário
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@ -1522,7 +1583,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditForm({ ...editForm, full_name: e.target.value })
|
setEditForm({ ...editForm, full_name: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1536,7 +1597,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditForm({ ...editForm, email: e.target.value })
|
setEditForm({ ...editForm, email: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1550,7 +1611,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setEditForm({ ...editForm, phone: e.target.value })
|
setEditForm({ ...editForm, phone: e.target.value })
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1558,13 +1619,13 @@ const PainelAdmin: React.FC = () => {
|
|||||||
<div className="flex gap-3 mt-6">
|
<div className="flex gap-3 mt-6">
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingUser(null)}
|
onClick={() => setEditingUser(null)}
|
||||||
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSaveEditUser}
|
onClick={handleSaveEditUser}
|
||||||
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Salvar
|
Salvar
|
||||||
</button>
|
</button>
|
||||||
@ -1575,9 +1636,17 @@ const PainelAdmin: React.FC = () => {
|
|||||||
|
|
||||||
{/* Modal de Gerenciar Roles */}
|
{/* Modal de Gerenciar Roles */}
|
||||||
{managingRolesUser && (
|
{managingRolesUser && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6">
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
<h2 className="text-xl font-bold text-gray-900 mb-2 flex items-center gap-2">
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="gerenciar-roles-title"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl border border-gray-200 max-w-md w-full p-6">
|
||||||
|
<h2
|
||||||
|
id="gerenciar-roles-title"
|
||||||
|
className="text-xl font-bold text-gray-900 mb-2 flex items-center gap-2"
|
||||||
|
>
|
||||||
<Shield className="w-5 h-5 text-purple-600" />
|
<Shield className="w-5 h-5 text-purple-600" />
|
||||||
Gerenciar Roles
|
Gerenciar Roles
|
||||||
</h2>
|
</h2>
|
||||||
@ -1651,7 +1720,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
<select
|
<select
|
||||||
value={newRole}
|
value={newRole}
|
||||||
onChange={(e) => setNewRole(e.target.value)}
|
onChange={(e) => setNewRole(e.target.value)}
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-sm"
|
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40 text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Selecione um role...</option>
|
<option value="">Selecione um role...</option>
|
||||||
<option value="admin">Admin</option>
|
<option value="admin">Admin</option>
|
||||||
@ -1692,7 +1761,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!newRole}
|
disabled={!newRole}
|
||||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
Adicionar
|
Adicionar
|
||||||
@ -1707,7 +1776,7 @@ const PainelAdmin: React.FC = () => {
|
|||||||
setUserRoles([]);
|
setUserRoles([]);
|
||||||
setNewRole("");
|
setNewRole("");
|
||||||
}}
|
}}
|
||||||
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200"
|
className="flex-1 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Fechar
|
Fechar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
759
MEDICONNECT 2/src/pages/PainelMedicoAntigo.tsx.bak
Normal file
759
MEDICONNECT 2/src/pages/PainelMedicoAntigo.tsx.bak
Normal file
@ -0,0 +1,759 @@
|
|||||||
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
CheckCircle,
|
||||||
|
AlertCircle,
|
||||||
|
FileText,
|
||||||
|
X,
|
||||||
|
} from "lucide-react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import ConsultationList from "../components/consultas/ConsultationList";
|
||||||
|
import ConsultaModal from "../components/consultas/ConsultaModal";
|
||||||
|
import AvailabilityManager from "../components/agenda/AvailabilityManager";
|
||||||
|
import ExceptionsManager from "../components/agenda/ExceptionsManager";
|
||||||
|
import consultasService, {
|
||||||
|
Consulta as ServiceConsulta,
|
||||||
|
} from "../services/consultasService";
|
||||||
|
import { listPatients } from "../services/pacienteService";
|
||||||
|
import { useAuth } from "../hooks/useAuth";
|
||||||
|
import relatorioService, {
|
||||||
|
RelatorioCreate,
|
||||||
|
} from "../services/relatorioService";
|
||||||
|
|
||||||
|
interface ConsultaUI {
|
||||||
|
id: string;
|
||||||
|
pacienteId: string;
|
||||||
|
medicoId: string;
|
||||||
|
pacienteNome: string;
|
||||||
|
medicoNome: string;
|
||||||
|
dataHora: string;
|
||||||
|
status: string;
|
||||||
|
tipo?: string;
|
||||||
|
observacoes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Paciente {
|
||||||
|
_id: string;
|
||||||
|
nome: string;
|
||||||
|
telefone: string;
|
||||||
|
email: string;
|
||||||
|
convenio: string;
|
||||||
|
observacoes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tipo Medico original removido (não necessário após auth)
|
||||||
|
|
||||||
|
// Antigos tipos Lumi removidos (não usados nesta refatoração)
|
||||||
|
|
||||||
|
const PainelMedico: React.FC = () => {
|
||||||
|
const { user, roles } = useAuth();
|
||||||
|
// Permite acesso se for médico ou admin
|
||||||
|
const temAcessoMedico =
|
||||||
|
user &&
|
||||||
|
(user.role === "medico" ||
|
||||||
|
roles.includes("medico") ||
|
||||||
|
roles.includes("admin"));
|
||||||
|
const medicoId = temAcessoMedico ? user.id : "";
|
||||||
|
const medicoNome = user?.nome || "Médico";
|
||||||
|
const [consultas, setConsultas] = useState<ConsultaUI[]>([]);
|
||||||
|
// pacientes detalhados não utilizados nesta versão simplificada
|
||||||
|
const [filtroData, setFiltroData] = useState("hoje");
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [editing, setEditing] = useState<ConsultaUI | null>(null);
|
||||||
|
const [relatorioModalOpen, setRelatorioModalOpen] = useState(false);
|
||||||
|
const [loadingRelatorio, setLoadingRelatorio] = useState(false);
|
||||||
|
const [pacientesDisponiveis, setPacientesDisponiveis] = useState<
|
||||||
|
Array<{ id: string; nome: string }>
|
||||||
|
>([]);
|
||||||
|
const [formRelatorio, setFormRelatorio] = useState({
|
||||||
|
patient_id: "",
|
||||||
|
order_number: "",
|
||||||
|
exam: "",
|
||||||
|
diagnosis: "",
|
||||||
|
conclusion: "",
|
||||||
|
cid_code: "",
|
||||||
|
content_html: "",
|
||||||
|
status: "draft" as "draft" | "pending" | "completed" | "cancelled",
|
||||||
|
requested_by: medicoNome,
|
||||||
|
due_at: format(new Date(), "yyyy-MM-dd'T'HH:mm"),
|
||||||
|
hide_date: false,
|
||||||
|
hide_signature: false,
|
||||||
|
});
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!medicoId) navigate("/login-medico");
|
||||||
|
}, [medicoId, navigate]);
|
||||||
|
|
||||||
|
const fetchConsultas = useCallback(async () => {
|
||||||
|
if (!medicoId) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem("consultas_local");
|
||||||
|
let lista: ServiceConsulta[] = [];
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
lista = JSON.parse(raw);
|
||||||
|
} catch {
|
||||||
|
lista = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let filtradas = lista.filter((c) => c.medicoId === medicoId);
|
||||||
|
const hoje = new Date();
|
||||||
|
if (filtroData === "hoje") {
|
||||||
|
const dStr = format(hoje, "yyyy-MM-dd");
|
||||||
|
filtradas = filtradas.filter((c) => c.dataHora.startsWith(dStr));
|
||||||
|
} else if (filtroData === "amanha") {
|
||||||
|
const amanha = new Date(hoje);
|
||||||
|
amanha.setDate(hoje.getDate() + 1);
|
||||||
|
const dStr = format(amanha, "yyyy-MM-dd");
|
||||||
|
filtradas = filtradas.filter((c) => c.dataHora.startsWith(dStr));
|
||||||
|
} else if (filtroData === "semana") {
|
||||||
|
const start = new Date(hoje);
|
||||||
|
start.setDate(hoje.getDate() - hoje.getDay());
|
||||||
|
const end = new Date(start);
|
||||||
|
end.setDate(start.getDate() + 6);
|
||||||
|
filtradas = filtradas.filter((c) => {
|
||||||
|
const d = new Date(c.dataHora);
|
||||||
|
return d >= start && d <= end;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const pacientesResponse = await listPatients({ per_page: 200 }).catch(
|
||||||
|
() => ({ data: [], total: 0, page: 1, per_page: 0 })
|
||||||
|
);
|
||||||
|
const pacMap: Record<string, Paciente> = {};
|
||||||
|
const pacientesLista =
|
||||||
|
"data" in pacientesResponse ? pacientesResponse.data : [];
|
||||||
|
pacientesLista.forEach((p) => {
|
||||||
|
pacMap[p.id] = {
|
||||||
|
_id: p.id,
|
||||||
|
nome: p.nome,
|
||||||
|
telefone: p.telefone || "",
|
||||||
|
email: p.email || "",
|
||||||
|
convenio: p.convenio || "",
|
||||||
|
observacoes: p.observacoes || "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setConsultas(
|
||||||
|
filtradas.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
pacienteId: c.pacienteId,
|
||||||
|
medicoId: c.medicoId,
|
||||||
|
pacienteNome: pacMap[c.pacienteId]?.nome || c.pacienteId,
|
||||||
|
medicoNome: medicoNome,
|
||||||
|
dataHora: c.dataHora,
|
||||||
|
status: c.status,
|
||||||
|
tipo: c.tipo,
|
||||||
|
observacoes: c.observacoes,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [medicoId, filtroData, medicoNome]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConsultas();
|
||||||
|
}, [fetchConsultas]);
|
||||||
|
|
||||||
|
// Carregar pacientes quando o modal de relatório abrir
|
||||||
|
useEffect(() => {
|
||||||
|
if (relatorioModalOpen && user?.id) {
|
||||||
|
const carregarPacientes = async () => {
|
||||||
|
try {
|
||||||
|
// Temporariamente buscando todos os pacientes para demonstração
|
||||||
|
const response = await listPatients({
|
||||||
|
per_page: 200,
|
||||||
|
// Filtro por médico removido temporariamente
|
||||||
|
});
|
||||||
|
if ("data" in response) {
|
||||||
|
setPacientesDisponiveis(
|
||||||
|
response.data.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
nome: p.nome,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.length === 0) {
|
||||||
|
toast("Nenhum paciente encontrado no sistema", {
|
||||||
|
icon: "ℹ️",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`✅ ${response.data.length} pacientes atribuídos carregados`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar pacientes:", error);
|
||||||
|
toast.error("Erro ao carregar lista de pacientes");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
carregarPacientes();
|
||||||
|
}
|
||||||
|
}, [relatorioModalOpen, user]);
|
||||||
|
|
||||||
|
// Removido: listagem de todos os médicos; painel bloqueado ao médico logado
|
||||||
|
|
||||||
|
// fetchConsultas substitui bloco anterior
|
||||||
|
|
||||||
|
const atualizarStatusConsulta = async (id: string, status: string) => {
|
||||||
|
try {
|
||||||
|
const resp = await consultasService.atualizar(id, { status });
|
||||||
|
if (resp.success && resp.data) {
|
||||||
|
setConsultas((prev) =>
|
||||||
|
prev.map((c) =>
|
||||||
|
c.id === id ? { ...c, status: resp.data!.status } : c
|
||||||
|
)
|
||||||
|
);
|
||||||
|
// persist back
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem("consultas_local");
|
||||||
|
if (raw) {
|
||||||
|
const arr = JSON.parse(raw);
|
||||||
|
if (Array.isArray(arr)) {
|
||||||
|
const upd = arr.map((x: Record<string, unknown>) =>
|
||||||
|
x && x.id === id ? { ...x, status: resp.data!.status } : x
|
||||||
|
);
|
||||||
|
localStorage.setItem("consultas_local", JSON.stringify(upd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
toast.success("Status atualizado");
|
||||||
|
} else toast.error(resp.error || "Falha ao atualizar");
|
||||||
|
} catch {
|
||||||
|
toast.error("Erro ao atualizar status");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitRelatorio = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!formRelatorio.patient_id) {
|
||||||
|
toast.error("Selecione um paciente");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingRelatorio(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Gerar número do relatório automaticamente
|
||||||
|
const orderNumber = `REL-${format(new Date(), "yyyy-MM")}-${Math.random()
|
||||||
|
.toString(36)
|
||||||
|
.substr(2, 6)
|
||||||
|
.toUpperCase()}`;
|
||||||
|
|
||||||
|
const relatorioData: RelatorioCreate = {
|
||||||
|
patient_id: formRelatorio.patient_id,
|
||||||
|
order_number: formRelatorio.order_number || orderNumber,
|
||||||
|
exam: formRelatorio.exam,
|
||||||
|
diagnosis: formRelatorio.diagnosis,
|
||||||
|
conclusion: formRelatorio.conclusion,
|
||||||
|
cid_code: formRelatorio.cid_code || undefined,
|
||||||
|
content_html:
|
||||||
|
formRelatorio.content_html ||
|
||||||
|
`<div>
|
||||||
|
<h2>${formRelatorio.exam}</h2>
|
||||||
|
<h3>Diagnóstico:</h3>
|
||||||
|
<p>${formRelatorio.diagnosis}</p>
|
||||||
|
<h3>Conclusão:</h3>
|
||||||
|
<p>${formRelatorio.conclusion}</p>
|
||||||
|
</div>`,
|
||||||
|
status: formRelatorio.status,
|
||||||
|
requested_by: formRelatorio.requested_by || medicoNome,
|
||||||
|
due_at: formRelatorio.due_at
|
||||||
|
? new Date(formRelatorio.due_at).toISOString()
|
||||||
|
: undefined,
|
||||||
|
hide_date: formRelatorio.hide_date,
|
||||||
|
hide_signature: formRelatorio.hide_signature,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await relatorioService.criarRelatorio(relatorioData);
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
toast.success("Relatório criado com sucesso!");
|
||||||
|
setRelatorioModalOpen(false);
|
||||||
|
// Reset form
|
||||||
|
setFormRelatorio({
|
||||||
|
patient_id: "",
|
||||||
|
order_number: "",
|
||||||
|
exam: "",
|
||||||
|
diagnosis: "",
|
||||||
|
conclusion: "",
|
||||||
|
cid_code: "",
|
||||||
|
content_html: "",
|
||||||
|
status: "draft",
|
||||||
|
requested_by: medicoNome,
|
||||||
|
due_at: format(new Date(), "yyyy-MM-dd'T'HH:mm"),
|
||||||
|
hide_date: false,
|
||||||
|
hide_signature: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error(response.error || "Erro ao criar relatório");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao criar relatório:", error);
|
||||||
|
toast.error("Erro ao criar relatório");
|
||||||
|
} finally {
|
||||||
|
setLoadingRelatorio(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// salvarConsulta substituído por onSaved direto no modal
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header com Gradiente */}
|
||||||
|
<div className="bg-gradient-to-r from-blue-700 via-blue-600 to-blue-500 dark:from-blue-800 dark:via-blue-700 dark:to-blue-600 rounded-xl shadow-lg p-8">
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||||
|
<div className="text-white">
|
||||||
|
<h1 className="text-4xl font-bold mb-2">Painel do Médico</h1>
|
||||||
|
<p className="text-blue-100 text-lg">
|
||||||
|
Bem-vindo, Dr(a). {medicoNome}
|
||||||
|
</p>
|
||||||
|
<p className="text-blue-200 text-sm mt-1">
|
||||||
|
Gerencie suas consultas e agenda
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row gap-4 mt-6 md:mt-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setRelatorioModalOpen(true)}
|
||||||
|
className="flex items-center justify-center gap-2 bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-md hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<FileText className="w-5 h-5" />
|
||||||
|
<span>Criar Relatório</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setModalOpen(true)}
|
||||||
|
className="flex items-center justify-center gap-2 bg-white hover:bg-blue-50 text-blue-700 px-6 py-3 rounded-lg font-medium transition-all duration-200 shadow-md hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Calendar className="w-5 h-5" />
|
||||||
|
<span>Nova Consulta</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cards de Estatísticas */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Total de Consultas
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-gray-900 dark:text-white mt-2">
|
||||||
|
{consultas.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-blue-100 dark:bg-blue-900/30 p-3 rounded-lg">
|
||||||
|
<Calendar className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Confirmadas
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-green-600 dark:text-green-400 mt-2">
|
||||||
|
{consultas.filter((c) => c.status === "confirmada").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-100 dark:bg-green-900/30 p-3 rounded-lg">
|
||||||
|
<CheckCircle className="w-8 h-8 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700 hover:shadow-lg transition-shadow">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Pendentes
|
||||||
|
</p>
|
||||||
|
<p className="text-3xl font-bold text-yellow-600 dark:text-yellow-400 mt-2">
|
||||||
|
{consultas.filter((c) => c.status === "agendada").length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-yellow-100 dark:bg-yellow-900/30 p-3 rounded-lg">
|
||||||
|
<Clock className="w-8 h-8 text-yellow-600 dark:text-yellow-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filtros e Ações */}
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||||
|
Suas Consultas
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row gap-4 w-full md:w-auto">
|
||||||
|
<select
|
||||||
|
value={filtroData}
|
||||||
|
onChange={(e) => setFiltroData(e.target.value)}
|
||||||
|
className="form-input min-w-[200px] focus:ring-2 focus:ring-blue-600 focus:border-blue-600/40"
|
||||||
|
>
|
||||||
|
<option value="hoje">Hoje</option>
|
||||||
|
<option value="amanha">Amanhã</option>
|
||||||
|
<option value="semana">Esta Semana</option>
|
||||||
|
<option value="todas">Todas</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gestão de Agenda do Médico */}
|
||||||
|
{temAcessoMedico && medicoId && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<AvailabilityManager doctorId={medicoId} />
|
||||||
|
<ExceptionsManager doctorId={medicoId} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6 border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Lista de Consultas
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<ConsultationList
|
||||||
|
itens={consultas.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
dataHora: c.dataHora,
|
||||||
|
pacienteNome: c.pacienteNome,
|
||||||
|
medicoNome: c.medicoNome,
|
||||||
|
status: c.status,
|
||||||
|
tipo: c.tipo,
|
||||||
|
observacoes: c.observacoes,
|
||||||
|
}))}
|
||||||
|
loading={false}
|
||||||
|
showPaciente
|
||||||
|
showMedico={false}
|
||||||
|
allowDelete={false}
|
||||||
|
onChangeStatus={(id, st) => atualizarStatusConsulta(id, st)}
|
||||||
|
onEdit={(id) => {
|
||||||
|
const found = consultas.find((c) => c.id === id) || null;
|
||||||
|
setEditing(found);
|
||||||
|
setModalOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{consultas.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-sm text-gray-500">
|
||||||
|
<AlertCircle className="w-6 h-6 mx-auto mb-2 text-gray-400" />
|
||||||
|
Nenhuma consulta encontrada para o período.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ConsultaModal
|
||||||
|
isOpen={modalOpen}
|
||||||
|
onClose={() => setModalOpen(false)}
|
||||||
|
editing={
|
||||||
|
editing
|
||||||
|
? ({
|
||||||
|
id: editing.id,
|
||||||
|
pacienteId: editing.pacienteId,
|
||||||
|
medicoId: editing.medicoId,
|
||||||
|
dataHora: editing.dataHora,
|
||||||
|
status: editing.status,
|
||||||
|
tipo: editing.tipo,
|
||||||
|
} as {
|
||||||
|
id: string;
|
||||||
|
pacienteId: string;
|
||||||
|
medicoId: string;
|
||||||
|
dataHora: string;
|
||||||
|
status: string;
|
||||||
|
tipo?: string;
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onSaved={() => {
|
||||||
|
setModalOpen(false);
|
||||||
|
fetchConsultas();
|
||||||
|
}}
|
||||||
|
defaultMedicoId={medicoId}
|
||||||
|
lockMedico
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal de Novo Relatório */}
|
||||||
|
{relatorioModalOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="novo-relatorio-title"
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto outline-none focus:outline-none">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2
|
||||||
|
id="novo-relatorio-title"
|
||||||
|
className="text-2xl font-bold text-gray-900"
|
||||||
|
>
|
||||||
|
Novo Relatório
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setRelatorioModalOpen(false)}
|
||||||
|
aria-label="Fechar modal de novo relatório"
|
||||||
|
className="text-gray-400 hover:text-gray-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-300 focus-visible:ring-offset-2 rounded"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmitRelatorio} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Paciente *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formRelatorio.patient_id}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
patient_id: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-input w-full"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Selecione um paciente</option>
|
||||||
|
{pacientesDisponiveis.map((p) => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.nome}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Número do Pedido
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formRelatorio.order_number}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
order_number: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-input w-full"
|
||||||
|
placeholder="Será gerado automaticamente"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Status *
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formRelatorio.status}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
status: e.target.value as
|
||||||
|
| "draft"
|
||||||
|
| "pending"
|
||||||
|
| "completed"
|
||||||
|
| "cancelled",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-input w-full"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="draft">Rascunho</option>
|
||||||
|
<option value="pending">Pendente</option>
|
||||||
|
<option value="completed">Concluído</option>
|
||||||
|
<option value="cancelled">Cancelado</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Exame/Procedimento *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formRelatorio.exam}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
exam: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-input w-full"
|
||||||
|
placeholder="Ex: Radiografia de Tórax, Ultrassom Abdominal"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Diagnóstico *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formRelatorio.diagnosis}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
diagnosis: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-input w-full"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Descreva o diagnóstico"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Conclusão *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={formRelatorio.conclusion}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
conclusion: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-input w-full"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Conclusão do exame/relatório"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Código CID
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={formRelatorio.cid_code}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
cid_code: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-input w-full"
|
||||||
|
placeholder="Ex: Z01.7, J00.0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Data de Vencimento
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="datetime-local"
|
||||||
|
value={formRelatorio.due_at}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
due_at: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-input w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formRelatorio.hide_date}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
hide_date: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-checkbox"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
Ocultar data no relatório
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={formRelatorio.hide_signature}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormRelatorio({
|
||||||
|
...formRelatorio,
|
||||||
|
hide_signature: e.target.checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="form-checkbox"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">
|
||||||
|
Ocultar assinatura
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
<strong>Solicitado por:</strong> {medicoNome}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-blue-600 mt-1">
|
||||||
|
Este relatório será associado ao médico logado
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setRelatorioModalOpen(false)}
|
||||||
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50"
|
||||||
|
disabled={loadingRelatorio}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
||||||
|
disabled={loadingRelatorio}
|
||||||
|
>
|
||||||
|
{loadingRelatorio ? "Gerando..." : "Gerar Relatório"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Observações agora integradas ao fluxo de edição no modal */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PainelMedico;
|
||||||
File diff suppressed because it is too large
Load Diff
@ -53,9 +53,13 @@ const ProntuarioPaciente = () => {
|
|||||||
if (!id) return;
|
if (!id) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const p = await getPatientById(id);
|
const respPaciente = await getPatientById(id);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setPaciente(p);
|
if (respPaciente.success && respPaciente.data) {
|
||||||
|
setPaciente(respPaciente.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(respPaciente.error || "Paciente não encontrado");
|
||||||
|
}
|
||||||
// metadata local
|
// metadata local
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem("pacientes_meta") || "{}";
|
const raw = localStorage.getItem("pacientes_meta") || "{}";
|
||||||
@ -68,10 +72,11 @@ const ProntuarioPaciente = () => {
|
|||||||
setMeta(null);
|
setMeta(null);
|
||||||
}
|
}
|
||||||
// consultas (últimas + futuras limitadas)
|
// consultas (últimas + futuras limitadas)
|
||||||
const resp = await consultasService.listarPorPaciente(id, {
|
const respConsultas = await consultasService.listarPorPaciente(id, {
|
||||||
limit: 20,
|
limit: 20,
|
||||||
});
|
});
|
||||||
if (resp.success && resp.data) setConsultas(resp.data);
|
if (respConsultas.success && respConsultas.data)
|
||||||
|
setConsultas(respConsultas.data);
|
||||||
// anexos
|
// anexos
|
||||||
try {
|
try {
|
||||||
const anexosList = await listPatientAttachments(id);
|
const anexosList = await listPatientAttachments(id);
|
||||||
@ -86,7 +91,7 @@ const ProntuarioPaciente = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
setHistorico([]);
|
setHistorico([]);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
toast.error("Paciente não encontrado");
|
toast.error("Paciente não encontrado");
|
||||||
navigate("/painel-secretaria");
|
navigate("/painel-secretaria");
|
||||||
} finally {
|
} finally {
|
||||||
@ -163,7 +168,7 @@ const ProntuarioPaciente = () => {
|
|||||||
const TabButton = ({ id: tabId, label }: { id: TabId; label: string }) => (
|
const TabButton = ({ id: tabId, label }: { id: TabId; label: string }) => (
|
||||||
<button
|
<button
|
||||||
onClick={() => setTab(tabId)}
|
onClick={() => setTab(tabId)}
|
||||||
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
|
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 rounded-t ${
|
||||||
tab === tabId
|
tab === tabId
|
||||||
? "border-blue-600 text-blue-600"
|
? "border-blue-600 text-blue-600"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700"
|
: "border-transparent text-gray-500 hover:text-gray-700"
|
||||||
@ -220,7 +225,7 @@ const ProntuarioPaciente = () => {
|
|||||||
|
|
||||||
{tab === "resumo" && (
|
{tab === "resumo" && (
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div className="p-4 bg-white rounded shadow">
|
<div className="p-4 bg-white rounded-xl shadow border border-gray-200">
|
||||||
<h2 className="font-semibold mb-3">Visão Geral</h2>
|
<h2 className="font-semibold mb-3">Visão Geral</h2>
|
||||||
<ul className="text-sm space-y-1">
|
<ul className="text-sm space-y-1">
|
||||||
<li>
|
<li>
|
||||||
@ -231,10 +236,10 @@ const ProntuarioPaciente = () => {
|
|||||||
</li>
|
</li>
|
||||||
<li>Convênio: {paciente.convenio || "Particular"}</li>
|
<li>Convênio: {paciente.convenio || "Particular"}</li>
|
||||||
<li>VIP: {paciente.vip ? "Sim" : "Não"}</li>
|
<li>VIP: {paciente.vip ? "Sim" : "Não"}</li>
|
||||||
<li>Tipo sanguíneo: {paciente.tipo_sanguineo || "—"}</li>
|
<li>Tipo sanguíneo: {paciente.tipoSanguineo || "—"}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-white rounded shadow">
|
<div className="p-4 bg-white rounded-xl shadow border border-gray-200">
|
||||||
<h2 className="font-semibold mb-3">Contato</h2>
|
<h2 className="font-semibold mb-3">Contato</h2>
|
||||||
<ul className="text-sm space-y-1">
|
<ul className="text-sm space-y-1">
|
||||||
<li>Email: {paciente.email || "—"}</li>
|
<li>Email: {paciente.email || "—"}</li>
|
||||||
@ -259,7 +264,7 @@ const ProntuarioPaciente = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "dados" && (
|
{tab === "dados" && (
|
||||||
<div className="p-4 bg-white rounded shadow space-y-4">
|
<div className="p-4 bg-white rounded-xl shadow border border-gray-200 space-y-4">
|
||||||
<h2 className="font-semibold">Dados Completos</h2>
|
<h2 className="font-semibold">Dados Completos</h2>
|
||||||
<div className="grid md:grid-cols-3 gap-4 text-sm">
|
<div className="grid md:grid-cols-3 gap-4 text-sm">
|
||||||
<div>
|
<div>
|
||||||
@ -305,7 +310,7 @@ const ProntuarioPaciente = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "consultas" && (
|
{tab === "consultas" && (
|
||||||
<div className="p-4 bg-white rounded shadow">
|
<div className="p-4 bg-white rounded-xl shadow border border-gray-200">
|
||||||
<h2 className="font-semibold mb-4">Consultas</h2>
|
<h2 className="font-semibold mb-4">Consultas</h2>
|
||||||
{consultasOrdenadas.length === 0 && (
|
{consultasOrdenadas.length === 0 && (
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
@ -339,10 +344,10 @@ const ProntuarioPaciente = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "anexos" && (
|
{tab === "anexos" && (
|
||||||
<div className="p-4 bg-white rounded shadow space-y-4">
|
<div className="p-4 bg-white rounded-xl shadow border border-gray-200 space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="font-semibold">Anexos</h2>
|
<h2 className="font-semibold">Anexos</h2>
|
||||||
<label className="text-sm bg-blue-600 text-white px-3 py-1 rounded cursor-pointer hover:bg-blue-700">
|
<label className="text-sm bg-blue-600 text-white px-3 py-1 rounded cursor-pointer hover:bg-blue-700 focus-within:outline-none focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2">
|
||||||
{uploading ? "Enviando..." : "Enviar Arquivos"}
|
{uploading ? "Enviando..." : "Enviar Arquivos"}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@ -375,14 +380,14 @@ const ProntuarioPaciente = () => {
|
|||||||
href={a.url}
|
href={a.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-blue-600 hover:underline"
|
className="text-blue-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 rounded"
|
||||||
>
|
>
|
||||||
Abrir
|
Abrir
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveAnexo(a)}
|
onClick={() => handleRemoveAnexo(a)}
|
||||||
className="text-red-600 hover:underline"
|
className="text-red-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2 rounded"
|
||||||
>
|
>
|
||||||
Remover
|
Remover
|
||||||
</button>
|
</button>
|
||||||
@ -394,7 +399,7 @@ const ProntuarioPaciente = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{tab === "historico" && (
|
{tab === "historico" && (
|
||||||
<div className="p-4 bg-white rounded shadow">
|
<div className="p-4 bg-white rounded-xl shadow border border-gray-200">
|
||||||
<h2 className="font-semibold mb-4">
|
<h2 className="font-semibold mb-4">
|
||||||
Histórico de Alterações (Local)
|
Histórico de Alterações (Local)
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@ -58,7 +58,8 @@ const TokenInspector: React.FC = () => {
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={load}
|
onClick={load}
|
||||||
className="text-xs px-3 py-1 rounded bg-gray-100 hover:bg-gray-200"
|
aria-label="Recarregar token do localStorage"
|
||||||
|
className="text-xs px-3 py-1 rounded bg-gray-100 hover:bg-gray-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
Recarregar
|
Recarregar
|
||||||
</button>
|
</button>
|
||||||
@ -104,7 +105,7 @@ const TokenInspector: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={refreshing}
|
disabled={refreshing}
|
||||||
className="px-4 py-2 rounded bg-indigo-600 text-white text-sm disabled:opacity-50"
|
className="px-4 py-2 rounded bg-indigo-600 text-white text-sm disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
{refreshing ? "Renovando..." : "Forçar Refresh"}
|
{refreshing ? "Renovando..." : "Forçar Refresh"}
|
||||||
</button>
|
</button>
|
||||||
@ -113,7 +114,8 @@ const TokenInspector: React.FC = () => {
|
|||||||
authService.logout();
|
authService.logout();
|
||||||
load();
|
load();
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 rounded bg-gray-200 text-sm hover:bg-gray-300"
|
className="px-4 py-2 rounded bg-gray-200 text-sm hover:bg-gray-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-400 focus-visible:ring-offset-2"
|
||||||
|
aria-label="Fazer logout e limpar token"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
@ -122,7 +124,8 @@ const TokenInspector: React.FC = () => {
|
|||||||
navigator.clipboard.writeText(token || "");
|
navigator.clipboard.writeText(token || "");
|
||||||
}}
|
}}
|
||||||
disabled={!token}
|
disabled={!token}
|
||||||
className="px-4 py-2 rounded bg-blue-100 text-blue-700 text-sm disabled:opacity-40"
|
className="px-4 py-2 rounded bg-blue-100 text-blue-700 text-sm disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 focus-visible:ring-offset-2"
|
||||||
|
aria-label="Copiar token para a área de transferência"
|
||||||
>
|
>
|
||||||
Copiar
|
Copiar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
345
MEDICONNECT 2/src/services/appointmentService.ts
Normal file
345
MEDICONNECT 2/src/services/appointmentService.ts
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
/**
|
||||||
|
* Service para gerenciar agendamentos (appointments)
|
||||||
|
* API completa com horários disponíveis, CRUD de agendamentos
|
||||||
|
*/
|
||||||
|
import { http, ApiResponse } from "./http";
|
||||||
|
import ENDPOINTS from "./endpoints";
|
||||||
|
import authService from "./authService";
|
||||||
|
|
||||||
|
export type AppointmentStatus =
|
||||||
|
| "requested"
|
||||||
|
| "confirmed"
|
||||||
|
| "checked_in"
|
||||||
|
| "in_progress"
|
||||||
|
| "completed"
|
||||||
|
| "cancelled"
|
||||||
|
| "no_show";
|
||||||
|
|
||||||
|
export type AppointmentType = "presencial" | "telemedicina";
|
||||||
|
|
||||||
|
export interface Appointment {
|
||||||
|
id?: string;
|
||||||
|
order_number?: string;
|
||||||
|
patient_id?: string;
|
||||||
|
doctor_id?: string;
|
||||||
|
scheduled_at?: string; // ISO 8601
|
||||||
|
duration_minutes?: number;
|
||||||
|
appointment_type?: AppointmentType;
|
||||||
|
status?: AppointmentStatus;
|
||||||
|
chief_complaint?: string | null;
|
||||||
|
patient_notes?: string | null;
|
||||||
|
notes?: string | null;
|
||||||
|
insurance_provider?: string | null;
|
||||||
|
checked_in_at?: string | null;
|
||||||
|
completed_at?: string | null;
|
||||||
|
cancelled_at?: string | null;
|
||||||
|
cancellation_reason?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
created_by?: string;
|
||||||
|
updated_by?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAppointmentInput {
|
||||||
|
patient_id: string;
|
||||||
|
doctor_id: string;
|
||||||
|
scheduled_at: string; // ISO 8601
|
||||||
|
duration_minutes?: number;
|
||||||
|
appointment_type?: AppointmentType;
|
||||||
|
chief_complaint?: string;
|
||||||
|
patient_notes?: string;
|
||||||
|
insurance_provider?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateAppointmentInput {
|
||||||
|
scheduled_at?: string;
|
||||||
|
duration_minutes?: number;
|
||||||
|
status?: AppointmentStatus;
|
||||||
|
chief_complaint?: string;
|
||||||
|
notes?: string;
|
||||||
|
patient_notes?: string;
|
||||||
|
insurance_provider?: string;
|
||||||
|
checked_in_at?: string;
|
||||||
|
completed_at?: string;
|
||||||
|
cancelled_at?: string;
|
||||||
|
cancellation_reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableSlotRequest {
|
||||||
|
doctor_id: string;
|
||||||
|
start_date: string; // ISO 8601
|
||||||
|
end_date: string; // ISO 8601
|
||||||
|
appointment_type?: AppointmentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableSlot {
|
||||||
|
datetime: string; // ISO 8601
|
||||||
|
available: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvailableSlotsResponse {
|
||||||
|
slots: AvailableSlot[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListAppointmentsParams {
|
||||||
|
select?: string;
|
||||||
|
doctor_id?: string;
|
||||||
|
patient_id?: string;
|
||||||
|
status?: AppointmentStatus;
|
||||||
|
scheduled_at?: string; // Usar operadores como gte.2025-10-10
|
||||||
|
order?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppointmentService {
|
||||||
|
async getAvailableSlots(
|
||||||
|
params: AvailableSlotRequest
|
||||||
|
): Promise<ApiResponse<AvailableSlotsResponse>> {
|
||||||
|
try {
|
||||||
|
const response = await http.post<AvailableSlotsResponse>(
|
||||||
|
ENDPOINTS.AVAILABLE_SLOTS,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return { success: true, data: response.data };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.error || "Erro ao buscar horários disponíveis",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listAppointments(
|
||||||
|
params?: ListAppointmentsParams
|
||||||
|
): Promise<ApiResponse<Appointment[]>> {
|
||||||
|
try {
|
||||||
|
const queryParams: Record<string, string> = {};
|
||||||
|
if (params?.select) queryParams.select = params.select;
|
||||||
|
if (params?.doctor_id)
|
||||||
|
queryParams["doctor_id"] = `eq.${params.doctor_id}`;
|
||||||
|
if (params?.patient_id)
|
||||||
|
queryParams["patient_id"] = `eq.${params.patient_id}`;
|
||||||
|
if (params?.status) queryParams["status"] = `eq.${params.status}`;
|
||||||
|
if (params?.scheduled_at)
|
||||||
|
queryParams["scheduled_at"] = params.scheduled_at;
|
||||||
|
if (params?.order) queryParams.order = params.order;
|
||||||
|
if (params?.limit) queryParams.limit = String(params.limit);
|
||||||
|
if (params?.offset) queryParams.offset = String(params.offset);
|
||||||
|
if (!queryParams.select) queryParams.select = "*";
|
||||||
|
if (!queryParams.order) queryParams.order = "scheduled_at.desc";
|
||||||
|
if (!queryParams.limit) queryParams.limit = "100";
|
||||||
|
const response = await http.get<Appointment[]>(ENDPOINTS.APPOINTMENTS, {
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
if (response.success && response.data) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: Array.isArray(response.data) ? response.data : [response.data],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.error || "Erro ao listar agendamentos",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAppointment(
|
||||||
|
data: CreateAppointmentInput
|
||||||
|
): Promise<ApiResponse<Appointment>> {
|
||||||
|
try {
|
||||||
|
// Pegar ID do usuário autenticado
|
||||||
|
const user = authService.getStoredUser();
|
||||||
|
if (!user?.id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Usuário não autenticado",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...data,
|
||||||
|
created_by: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await http.post<Appointment>(
|
||||||
|
ENDPOINTS.APPOINTMENTS,
|
||||||
|
payload,
|
||||||
|
{
|
||||||
|
headers: { Prefer: "return=representation" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const appointment = Array.isArray(response.data)
|
||||||
|
? response.data[0]
|
||||||
|
: response.data;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: appointment,
|
||||||
|
message: "Agendamento criado com sucesso",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.error || "Erro ao criar agendamento",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAppointmentById(id: string): Promise<ApiResponse<Appointment>> {
|
||||||
|
try {
|
||||||
|
const response = await http.get<Appointment[]>(
|
||||||
|
`${ENDPOINTS.APPOINTMENTS}?id=eq.${id}&select=*`
|
||||||
|
);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const list = Array.isArray(response.data)
|
||||||
|
? response.data
|
||||||
|
: [response.data];
|
||||||
|
if (list.length > 0) return { success: true, data: list[0] };
|
||||||
|
}
|
||||||
|
return { success: false, error: "Agendamento não encontrado" };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAppointment(
|
||||||
|
id: string,
|
||||||
|
updates: UpdateAppointmentInput
|
||||||
|
): Promise<ApiResponse<Appointment>> {
|
||||||
|
try {
|
||||||
|
const response = await http.patch<Appointment>(
|
||||||
|
`${ENDPOINTS.APPOINTMENTS}?id=eq.${id}`,
|
||||||
|
updates,
|
||||||
|
{
|
||||||
|
headers: { Prefer: "return=representation" },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.success && response.data) {
|
||||||
|
const appointment = Array.isArray(response.data)
|
||||||
|
? response.data[0]
|
||||||
|
: response.data;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: appointment,
|
||||||
|
message: "Agendamento atualizado com sucesso",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.error || "Erro ao atualizar agendamento",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAppointment(id: string): Promise<ApiResponse<void>> {
|
||||||
|
try {
|
||||||
|
const response = await http.delete<void>(
|
||||||
|
`${ENDPOINTS.APPOINTMENTS}?id=eq.${id}`
|
||||||
|
);
|
||||||
|
if (response.success)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: undefined,
|
||||||
|
message: "Agendamento deletado com sucesso",
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.error || "Erro ao deletar agendamento",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conveniências
|
||||||
|
confirmAppointment(id: string) {
|
||||||
|
return this.updateAppointment(id, { status: "confirmed" });
|
||||||
|
}
|
||||||
|
checkInAppointment(id: string) {
|
||||||
|
return this.updateAppointment(id, {
|
||||||
|
status: "checked_in",
|
||||||
|
checked_in_at: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
startAppointment(id: string) {
|
||||||
|
return this.updateAppointment(id, { status: "in_progress" });
|
||||||
|
}
|
||||||
|
completeAppointment(id: string, notes?: string) {
|
||||||
|
return this.updateAppointment(id, {
|
||||||
|
status: "completed",
|
||||||
|
completed_at: new Date().toISOString(),
|
||||||
|
notes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cancelAppointment(id: string, reason?: string) {
|
||||||
|
return this.updateAppointment(id, {
|
||||||
|
status: "cancelled",
|
||||||
|
cancelled_at: new Date().toISOString(),
|
||||||
|
cancellation_reason: reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
markAsNoShow(id: string) {
|
||||||
|
return this.updateAppointment(id, { status: "no_show" });
|
||||||
|
}
|
||||||
|
listDoctorAppointments(
|
||||||
|
doctorId: string,
|
||||||
|
params?: Omit<ListAppointmentsParams, "doctor_id">
|
||||||
|
) {
|
||||||
|
return this.listAppointments({ ...params, doctor_id: doctorId });
|
||||||
|
}
|
||||||
|
listPatientAppointments(
|
||||||
|
patientId: string,
|
||||||
|
params?: Omit<ListAppointmentsParams, "patient_id">
|
||||||
|
) {
|
||||||
|
return this.listAppointments({ ...params, patient_id: patientId });
|
||||||
|
}
|
||||||
|
listTodayAppointments(doctorId: string) {
|
||||||
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
return this.listAppointments({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
scheduled_at: `gte.${today}T00:00:00`,
|
||||||
|
order: "scheduled_at.asc",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
listUpcomingPatientAppointments(patientId: string) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
return this.listAppointments({
|
||||||
|
patient_id: patientId,
|
||||||
|
scheduled_at: `gte.${now}`,
|
||||||
|
order: "scheduled_at.asc",
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appointmentService = new AppointmentService();
|
||||||
|
export default appointmentService;
|
||||||
406
MEDICONNECT 2/src/services/availabilityService.ts
Normal file
406
MEDICONNECT 2/src/services/availabilityService.ts
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
/**
|
||||||
|
* Service para gerenciar disponibilidades dos médicos
|
||||||
|
* Configuração de horários de trabalho por dia da semana
|
||||||
|
*/
|
||||||
|
import { http, ApiResponse } from "./http";
|
||||||
|
import ENDPOINTS from "./endpoints";
|
||||||
|
import authService from "./authService";
|
||||||
|
|
||||||
|
export type Weekday =
|
||||||
|
| "segunda"
|
||||||
|
| "terca"
|
||||||
|
| "quarta"
|
||||||
|
| "quinta"
|
||||||
|
| "sexta"
|
||||||
|
| "sabado"
|
||||||
|
| "domingo";
|
||||||
|
|
||||||
|
// Tipo que o banco de dados realmente aceita (provavelmente em inglês ou números)
|
||||||
|
export type WeekdayDB =
|
||||||
|
| "monday"
|
||||||
|
| "tuesday"
|
||||||
|
| "wednesday"
|
||||||
|
| "thursday"
|
||||||
|
| "friday"
|
||||||
|
| "saturday"
|
||||||
|
| "sunday";
|
||||||
|
|
||||||
|
export type AppointmentType = "presencial" | "telemedicina";
|
||||||
|
|
||||||
|
export interface DoctorAvailability {
|
||||||
|
id?: string;
|
||||||
|
doctor_id?: string;
|
||||||
|
weekday?: Weekday;
|
||||||
|
start_time?: string; // "09:00:00"
|
||||||
|
end_time?: string; // "17:00:00"
|
||||||
|
slot_minutes?: number; // padrão: 30
|
||||||
|
appointment_type?: AppointmentType;
|
||||||
|
active?: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
created_by?: string;
|
||||||
|
updated_by?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAvailabilityInput {
|
||||||
|
doctor_id: string;
|
||||||
|
weekday: Weekday;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
slot_minutes?: number;
|
||||||
|
appointment_type?: AppointmentType;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateAvailabilityInput {
|
||||||
|
weekday?: Weekday;
|
||||||
|
start_time?: string;
|
||||||
|
end_time?: string;
|
||||||
|
slot_minutes?: number;
|
||||||
|
appointment_type?: AppointmentType;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListAvailabilityParams {
|
||||||
|
select?: string;
|
||||||
|
doctor_id?: string;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WEEKDAY_MAP = {
|
||||||
|
0: "domingo",
|
||||||
|
1: "segunda",
|
||||||
|
2: "terca",
|
||||||
|
3: "quarta",
|
||||||
|
4: "quinta",
|
||||||
|
5: "sexta",
|
||||||
|
6: "sabado",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const WEEKDAY_LABELS = {
|
||||||
|
segunda: "Segunda-feira",
|
||||||
|
terca: "Terça-feira",
|
||||||
|
quarta: "Quarta-feira",
|
||||||
|
quinta: "Quinta-feira",
|
||||||
|
sexta: "Sexta-feira",
|
||||||
|
sabado: "Sábado",
|
||||||
|
domingo: "Domingo",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Mapeamento PT-BR → EN (para o banco de dados)
|
||||||
|
export const WEEKDAY_PT_TO_EN: Record<Weekday, WeekdayDB> = {
|
||||||
|
segunda: "monday",
|
||||||
|
terca: "tuesday",
|
||||||
|
quarta: "wednesday",
|
||||||
|
quinta: "thursday",
|
||||||
|
sexta: "friday",
|
||||||
|
sabado: "saturday",
|
||||||
|
domingo: "sunday",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapeamento EN → PT-BR (do banco de dados)
|
||||||
|
export const WEEKDAY_EN_TO_PT: Record<WeekdayDB, Weekday> = {
|
||||||
|
monday: "segunda",
|
||||||
|
tuesday: "terca",
|
||||||
|
wednesday: "quarta",
|
||||||
|
thursday: "quinta",
|
||||||
|
friday: "sexta",
|
||||||
|
saturday: "sabado",
|
||||||
|
sunday: "domingo",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Converter para formato do banco
|
||||||
|
export function convertWeekdayToDB(weekday: Weekday): WeekdayDB {
|
||||||
|
return WEEKDAY_PT_TO_EN[weekday];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converter do formato do banco
|
||||||
|
export function convertWeekdayFromDB(weekday: WeekdayDB): Weekday {
|
||||||
|
return WEEKDAY_EN_TO_PT[weekday];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeekdayName(dayNumber: number): Weekday {
|
||||||
|
return WEEKDAY_MAP[dayNumber as keyof typeof WEEKDAY_MAP];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWeekdayFromDate(date: Date): Weekday {
|
||||||
|
return getWeekdayName(date.getDay());
|
||||||
|
}
|
||||||
|
|
||||||
|
class AvailabilityService {
|
||||||
|
async listAvailability(
|
||||||
|
params?: ListAvailabilityParams
|
||||||
|
): Promise<ApiResponse<DoctorAvailability[]>> {
|
||||||
|
try {
|
||||||
|
const q: Record<string, string> = {};
|
||||||
|
if (params?.select) q.select = params.select;
|
||||||
|
if (params?.doctor_id) q["doctor_id"] = `eq.${params.doctor_id}`;
|
||||||
|
if (params?.active !== undefined) q["active"] = `eq.${params.active}`;
|
||||||
|
if (!q.select) q.select = "*";
|
||||||
|
const res = await http.get<DoctorAvailability[]>(
|
||||||
|
ENDPOINTS.DOCTOR_AVAILABILITY,
|
||||||
|
{ params: q }
|
||||||
|
);
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const dataArray = Array.isArray(res.data) ? res.data : [res.data];
|
||||||
|
// Converter weekdays do banco (inglês) para PT-BR
|
||||||
|
const converted = dataArray.map((item) => {
|
||||||
|
if (item.weekday) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
weekday: convertWeekdayFromDB(
|
||||||
|
item.weekday as WeekdayDB
|
||||||
|
) as Weekday,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: converted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: res.error || "Erro ao listar disponibilidades",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e instanceof Error ? e.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAvailability(
|
||||||
|
data: CreateAvailabilityInput
|
||||||
|
): Promise<ApiResponse<DoctorAvailability>> {
|
||||||
|
try {
|
||||||
|
console.log(
|
||||||
|
"[AvailabilityService] Criando disponibilidade (PT-BR):",
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pegar ID do usuário autenticado
|
||||||
|
const user = authService.getStoredUser();
|
||||||
|
if (!user?.id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Usuário não autenticado",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converter weekday para inglês (formato do banco) e adicionar created_by
|
||||||
|
const payload = {
|
||||||
|
...data,
|
||||||
|
weekday: convertWeekdayToDB(data.weekday),
|
||||||
|
created_by: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[AvailabilityService] Payload convertido (EN):", payload);
|
||||||
|
console.log(
|
||||||
|
"[AvailabilityService] Endpoint:",
|
||||||
|
ENDPOINTS.DOCTOR_AVAILABILITY
|
||||||
|
);
|
||||||
|
|
||||||
|
const res = await http.post<DoctorAvailability>(
|
||||||
|
ENDPOINTS.DOCTOR_AVAILABILITY,
|
||||||
|
payload,
|
||||||
|
{ headers: { Prefer: "return=representation" } }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("[AvailabilityService] Resposta:", res);
|
||||||
|
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const item = Array.isArray(res.data) ? res.data[0] : res.data;
|
||||||
|
// Converter weekday de volta para PT-BR antes de retornar
|
||||||
|
if (item.weekday) {
|
||||||
|
item.weekday = convertWeekdayFromDB(
|
||||||
|
item.weekday as WeekdayDB
|
||||||
|
) as Weekday;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: item,
|
||||||
|
message: "Disponibilidade criada com sucesso",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: res.error || "Erro ao criar disponibilidade",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[AvailabilityService] Exceção:", e);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e instanceof Error ? e.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAvailability(
|
||||||
|
id: string,
|
||||||
|
updates: UpdateAvailabilityInput
|
||||||
|
): Promise<ApiResponse<DoctorAvailability>> {
|
||||||
|
try {
|
||||||
|
// Converter weekday se presente
|
||||||
|
const payload = updates.weekday
|
||||||
|
? { ...updates, weekday: convertWeekdayToDB(updates.weekday) }
|
||||||
|
: updates;
|
||||||
|
|
||||||
|
const res = await http.patch<DoctorAvailability>(
|
||||||
|
`${ENDPOINTS.DOCTOR_AVAILABILITY}?id=eq.${id}`,
|
||||||
|
payload,
|
||||||
|
{ headers: { Prefer: "return=representation" } }
|
||||||
|
);
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const item = Array.isArray(res.data) ? res.data[0] : res.data;
|
||||||
|
// Converter weekday de volta para PT-BR
|
||||||
|
if (item.weekday) {
|
||||||
|
item.weekday = convertWeekdayFromDB(
|
||||||
|
item.weekday as WeekdayDB
|
||||||
|
) as Weekday;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: item,
|
||||||
|
message: "Disponibilidade atualizada com sucesso",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: res.error || "Erro ao atualizar disponibilidade",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e instanceof Error ? e.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAvailability(id: string): Promise<ApiResponse<void>> {
|
||||||
|
try {
|
||||||
|
const res = await http.delete<void>(
|
||||||
|
`${ENDPOINTS.DOCTOR_AVAILABILITY}?id=eq.${id}`
|
||||||
|
);
|
||||||
|
if (res.success)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: undefined,
|
||||||
|
message: "Disponibilidade deletada com sucesso",
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: res.error || "Erro ao deletar disponibilidade",
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e instanceof Error ? e.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listDoctorActiveAvailability(doctorId: string) {
|
||||||
|
return this.listAvailability({ doctor_id: doctorId, active: true });
|
||||||
|
}
|
||||||
|
activateAvailability(id: string) {
|
||||||
|
return this.updateAvailability(id, { active: true });
|
||||||
|
}
|
||||||
|
deactivateAvailability(id: string) {
|
||||||
|
return this.updateAvailability(id, { active: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async createWeekSchedule(
|
||||||
|
doctorId: string,
|
||||||
|
weekdays: Weekday[],
|
||||||
|
startTime: string,
|
||||||
|
endTime: string,
|
||||||
|
slotMinutes = 30,
|
||||||
|
appointmentType: AppointmentType = "presencial"
|
||||||
|
) {
|
||||||
|
const results: DoctorAvailability[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
for (const weekday of weekdays) {
|
||||||
|
const res = await this.createAvailability({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
weekday,
|
||||||
|
start_time: startTime,
|
||||||
|
end_time: endTime,
|
||||||
|
slot_minutes: slotMinutes,
|
||||||
|
appointment_type: appointmentType,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
if (res.success && res.data) results.push(res.data);
|
||||||
|
else errors.push(`${weekday}: ${res.error}`);
|
||||||
|
}
|
||||||
|
if (errors.length)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Erros: ${errors.join(", ")}`,
|
||||||
|
} as ApiResponse<DoctorAvailability[]>;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: results,
|
||||||
|
message: "Horários da semana criados com sucesso",
|
||||||
|
} as ApiResponse<DoctorAvailability[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDoctorAvailabilityForDay(doctorId: string, weekday: Weekday) {
|
||||||
|
const res = await this.listAvailability({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
if (!res.success || !res.data)
|
||||||
|
return res as ApiResponse<DoctorAvailability[]>;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: res.data.filter((a) => a.weekday === weekday),
|
||||||
|
} as ApiResponse<DoctorAvailability[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isDoctorAvailableOnDay(doctorId: string, weekday: Weekday) {
|
||||||
|
const res = await this.getDoctorAvailabilityForDay(doctorId, weekday);
|
||||||
|
return !!(res.success && res.data && res.data.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDoctorScheduleSummary(
|
||||||
|
doctorId: string
|
||||||
|
): Promise<
|
||||||
|
ApiResponse<
|
||||||
|
Record<
|
||||||
|
Weekday,
|
||||||
|
Array<{ start: string; end: string; type: AppointmentType }>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
const res = await this.listDoctorActiveAvailability(doctorId);
|
||||||
|
if (!res.success || !res.data)
|
||||||
|
return { success: false, error: "Erro ao buscar horários" };
|
||||||
|
const summary: Record<
|
||||||
|
Weekday,
|
||||||
|
Array<{ start: string; end: string; type: AppointmentType }>
|
||||||
|
> = {
|
||||||
|
segunda: [],
|
||||||
|
terca: [],
|
||||||
|
quarta: [],
|
||||||
|
quinta: [],
|
||||||
|
sexta: [],
|
||||||
|
sabado: [],
|
||||||
|
domingo: [],
|
||||||
|
};
|
||||||
|
res.data.forEach((a) => {
|
||||||
|
if (a.weekday && a.start_time && a.end_time)
|
||||||
|
summary[a.weekday].push({
|
||||||
|
start: a.start_time,
|
||||||
|
end: a.end_time,
|
||||||
|
type: a.appointment_type || "presencial",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { success: true, data: summary };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const availabilityService = new AvailabilityService();
|
||||||
|
export default availabilityService;
|
||||||
@ -1,6 +1,7 @@
|
|||||||
// Centralização de endpoints REST (Supabase PostgREST)
|
// Centralização de endpoints REST (Supabase PostgREST)
|
||||||
// Ajuste AQUI se mudar o nome de tabelas / views no banco.
|
// Ajuste AQUI se mudar o nome de tabelas / views no banco.
|
||||||
// Exemplo: se sua tabela real é "medicos" em vez de "doctors", defina DOCTORS = "/rest/v1/medicos".
|
// Descoberta atual (scripts/verificar-todas-tabelas.js): existem doctors, patients, profiles, appointments.
|
||||||
|
// Tabelas em pt-BR (medicos, pacientes) não existem nesse projeto/instância.
|
||||||
// IMPORTANTE: manter sempre o prefixo "/rest/v1".
|
// IMPORTANTE: manter sempre o prefixo "/rest/v1".
|
||||||
|
|
||||||
export const ENDPOINTS = {
|
export const ENDPOINTS = {
|
||||||
@ -8,7 +9,13 @@ export const ENDPOINTS = {
|
|||||||
PATIENTS: "/rest/v1/patients", // ou "/rest/v1/pacientes"
|
PATIENTS: "/rest/v1/patients", // ou "/rest/v1/pacientes"
|
||||||
PROFILES: "/rest/v1/profiles",
|
PROFILES: "/rest/v1/profiles",
|
||||||
REPORTS: "/rest/v1/reports",
|
REPORTS: "/rest/v1/reports",
|
||||||
CONSULTATIONS: "/rest/v1/appointments", // Tabela appointments no Supabase (criar depois)
|
// Consultas/Agendamentos
|
||||||
|
APPOINTMENTS: "/rest/v1/appointments", // CRUD de agendamentos
|
||||||
|
CONSULTATIONS: "/rest/v1/appointments", // alias para compatibilidade
|
||||||
|
AVAILABLE_SLOTS: "/functions/v1/get-available-slots", // horários disponíveis
|
||||||
|
// Disponibilidade e Exceções do médico
|
||||||
|
DOCTOR_AVAILABILITY: "/rest/v1/doctor_availability",
|
||||||
|
DOCTOR_EXCEPTIONS: "/rest/v1/doctor_exceptions",
|
||||||
PATIENT_ASSIGNMENTS: "/rest/v1/patient_assignments",
|
PATIENT_ASSIGNMENTS: "/rest/v1/patient_assignments",
|
||||||
USER_ROLES: "/rest/v1/user_roles",
|
USER_ROLES: "/rest/v1/user_roles",
|
||||||
SMS: "/rest/v1/sms",
|
SMS: "/rest/v1/sms",
|
||||||
|
|||||||
333
MEDICONNECT 2/src/services/exceptionService.ts
Normal file
333
MEDICONNECT 2/src/services/exceptionService.ts
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
/**
|
||||||
|
* Service para gerenciar exceções na agenda dos médicos
|
||||||
|
* Bloqueios (férias, feriados) e liberações (horários extras)
|
||||||
|
*/
|
||||||
|
import { http, ApiResponse } from "./http";
|
||||||
|
import ENDPOINTS from "./endpoints";
|
||||||
|
import authService from "./authService";
|
||||||
|
|
||||||
|
export type ExceptionKind = "bloqueio" | "liberacao";
|
||||||
|
|
||||||
|
export interface DoctorException {
|
||||||
|
id?: string;
|
||||||
|
doctor_id?: string;
|
||||||
|
date?: string; // YYYY-MM-DD
|
||||||
|
start_time?: string | null; // HH:MM:SS
|
||||||
|
end_time?: string | null; // HH:MM:SS
|
||||||
|
kind?: ExceptionKind;
|
||||||
|
reason?: string | null;
|
||||||
|
created_at?: string;
|
||||||
|
created_by?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateExceptionInput {
|
||||||
|
doctor_id: string;
|
||||||
|
date: string;
|
||||||
|
start_time?: string;
|
||||||
|
end_time?: string;
|
||||||
|
kind: ExceptionKind;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListExceptionsParams {
|
||||||
|
select?: string;
|
||||||
|
doctor_id?: string;
|
||||||
|
date?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateForApi(date: Date): string {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTimeForApi(hours: number, minutes = 0): string {
|
||||||
|
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(
|
||||||
|
2,
|
||||||
|
"0"
|
||||||
|
)}:00`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFullDayException(exception: DoctorException): boolean {
|
||||||
|
return !exception.start_time && !exception.end_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExceptionKindLabel(kind: ExceptionKind): string {
|
||||||
|
return kind === "bloqueio" ? "Bloqueio" : "Liberação";
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExceptionService {
|
||||||
|
async listExceptions(
|
||||||
|
params?: ListExceptionsParams
|
||||||
|
): Promise<ApiResponse<DoctorException[]>> {
|
||||||
|
try {
|
||||||
|
const q: Record<string, string> = {};
|
||||||
|
if (params?.select) q.select = params.select;
|
||||||
|
if (params?.doctor_id) q["doctor_id"] = `eq.${params.doctor_id}`;
|
||||||
|
if (params?.date) q["date"] = `eq.${params.date}`;
|
||||||
|
if (!q.select) q.select = "*";
|
||||||
|
const res = await http.get<DoctorException[]>(
|
||||||
|
ENDPOINTS.DOCTOR_EXCEPTIONS,
|
||||||
|
{ params: q }
|
||||||
|
);
|
||||||
|
if (res.success && res.data) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: Array.isArray(res.data) ? res.data : [res.data],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { success: false, error: res.error || "Erro ao listar exceções" };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e instanceof Error ? e.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createException(
|
||||||
|
data: CreateExceptionInput
|
||||||
|
): Promise<ApiResponse<DoctorException>> {
|
||||||
|
try {
|
||||||
|
// Pegar ID do usuário autenticado
|
||||||
|
const user = authService.getStoredUser();
|
||||||
|
if (!user?.id) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Usuário não autenticado",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...data,
|
||||||
|
created_by: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await http.post<DoctorException>(
|
||||||
|
ENDPOINTS.DOCTOR_EXCEPTIONS,
|
||||||
|
payload,
|
||||||
|
{ headers: { Prefer: "return=representation" } }
|
||||||
|
);
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const item = Array.isArray(res.data) ? res.data[0] : res.data;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: item,
|
||||||
|
message: "Exceção criada com sucesso",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { success: false, error: res.error || "Erro ao criar exceção" };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e instanceof Error ? e.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteException(id: string): Promise<ApiResponse<void>> {
|
||||||
|
try {
|
||||||
|
const res = await http.delete<void>(
|
||||||
|
`${ENDPOINTS.DOCTOR_EXCEPTIONS}?id=eq.${id}`
|
||||||
|
);
|
||||||
|
if (res.success)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: undefined,
|
||||||
|
message: "Exceção deletada com sucesso",
|
||||||
|
};
|
||||||
|
return { success: false, error: res.error || "Erro ao deletar exceção" };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: e instanceof Error ? e.message : "Erro desconhecido",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conveniências - bloqueios
|
||||||
|
blockFullDay(doctorId: string, date: string, reason?: string) {
|
||||||
|
return this.createException({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
date,
|
||||||
|
kind: "bloqueio",
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
blockTimeSlot(
|
||||||
|
doctorId: string,
|
||||||
|
date: string,
|
||||||
|
startTime: string,
|
||||||
|
endTime: string,
|
||||||
|
reason?: string
|
||||||
|
) {
|
||||||
|
return this.createException({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
date,
|
||||||
|
start_time: startTime,
|
||||||
|
end_time: endTime,
|
||||||
|
kind: "bloqueio",
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async blockVacation(
|
||||||
|
doctorId: string,
|
||||||
|
startDate: string,
|
||||||
|
endDate: string,
|
||||||
|
reason = "Férias"
|
||||||
|
) {
|
||||||
|
const start = new Date(startDate);
|
||||||
|
const end = new Date(endDate);
|
||||||
|
const results: DoctorException[] = [];
|
||||||
|
const errors: string[] = [];
|
||||||
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
|
const ds = formatDateForApi(d);
|
||||||
|
const res = await this.blockFullDay(doctorId, ds, reason);
|
||||||
|
if (res.success && res.data) results.push(res.data);
|
||||||
|
else errors.push(`${ds}: ${res.error}`);
|
||||||
|
}
|
||||||
|
if (errors.length)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Erros ao bloquear férias: ${errors.join(", ")}`,
|
||||||
|
} as ApiResponse<DoctorException[]>;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: results,
|
||||||
|
message: `${results.length} dia(s) bloqueado(s)`,
|
||||||
|
} as ApiResponse<DoctorException[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conveniências - liberações
|
||||||
|
releaseFullDay(doctorId: string, date: string, reason?: string) {
|
||||||
|
return this.createException({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
date,
|
||||||
|
kind: "liberacao",
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
releaseTimeSlot(
|
||||||
|
doctorId: string,
|
||||||
|
date: string,
|
||||||
|
startTime: string,
|
||||||
|
endTime: string,
|
||||||
|
reason?: string
|
||||||
|
) {
|
||||||
|
return this.createException({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
date,
|
||||||
|
start_time: startTime,
|
||||||
|
end_time: endTime,
|
||||||
|
kind: "liberacao",
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consultas
|
||||||
|
async listDoctorBlocks(doctorId: string) {
|
||||||
|
const res = await this.listExceptions({ doctor_id: doctorId });
|
||||||
|
if (!res.success || !res.data) return res as ApiResponse<DoctorException[]>;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: res.data.filter((e) => e.kind === "bloqueio"),
|
||||||
|
} as ApiResponse<DoctorException[]>;
|
||||||
|
}
|
||||||
|
async listDoctorReleases(doctorId: string) {
|
||||||
|
const res = await this.listExceptions({ doctor_id: doctorId });
|
||||||
|
if (!res.success || !res.data) return res as ApiResponse<DoctorException[]>;
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: res.data.filter((e) => e.kind === "liberacao"),
|
||||||
|
} as ApiResponse<DoctorException[]>;
|
||||||
|
}
|
||||||
|
async isDayBlocked(doctorId: string, date: string) {
|
||||||
|
const res = await this.listExceptions({ doctor_id: doctorId, date });
|
||||||
|
if (!res.success || !res.data) return false;
|
||||||
|
return res.data.some((e) => e.kind === "bloqueio");
|
||||||
|
}
|
||||||
|
async isTimeSlotBlocked(doctorId: string, date: string, time: string) {
|
||||||
|
const res = await this.listExceptions({ doctor_id: doctorId, date });
|
||||||
|
if (!res.success || !res.data) return false;
|
||||||
|
for (const e of res.data) {
|
||||||
|
if (e.kind !== "bloqueio") continue;
|
||||||
|
if (!e.start_time && !e.end_time) return true;
|
||||||
|
if (e.start_time && e.end_time) {
|
||||||
|
if (time >= e.start_time && time <= e.end_time) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
async getFutureExceptions(doctorId: string) {
|
||||||
|
const res = await this.listExceptions({ doctor_id: doctorId });
|
||||||
|
if (!res.success || !res.data) return res as ApiResponse<DoctorException[]>;
|
||||||
|
const today = formatDateForApi(new Date());
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: res.data.filter((e) => e.date && e.date >= today),
|
||||||
|
} as ApiResponse<DoctorException[]>;
|
||||||
|
}
|
||||||
|
async getMonthExceptionsSummary(
|
||||||
|
doctorId: string,
|
||||||
|
year: number,
|
||||||
|
month: number
|
||||||
|
): Promise<
|
||||||
|
ApiResponse<{
|
||||||
|
blocks: DoctorException[];
|
||||||
|
releases: DoctorException[];
|
||||||
|
totalBlocks: number;
|
||||||
|
totalReleases: number;
|
||||||
|
}>
|
||||||
|
> {
|
||||||
|
const res = await this.listExceptions({ doctor_id: doctorId });
|
||||||
|
if (!res.success || !res.data)
|
||||||
|
return { success: false, error: "Erro ao buscar exceções" };
|
||||||
|
const prefix = `${year}-${String(month).padStart(2, "0")}`;
|
||||||
|
const list = res.data.filter((e) => e.date && e.date.startsWith(prefix));
|
||||||
|
const blocks = list.filter((e) => e.kind === "bloqueio");
|
||||||
|
const releases = list.filter((e) => e.kind === "liberacao");
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
blocks,
|
||||||
|
releases,
|
||||||
|
totalBlocks: blocks.length,
|
||||||
|
totalReleases: releases.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
async deleteExceptionsInRange(
|
||||||
|
doctorId: string,
|
||||||
|
startDate: string,
|
||||||
|
endDate: string
|
||||||
|
): Promise<ApiResponse<{ deletedCount: number }>> {
|
||||||
|
const res = await this.listExceptions({ doctor_id: doctorId });
|
||||||
|
if (!res.success || !res.data)
|
||||||
|
return { success: false, error: "Erro ao buscar exceções" };
|
||||||
|
const inRange = res.data.filter(
|
||||||
|
(e) => e.date && e.date >= startDate && e.date <= endDate
|
||||||
|
);
|
||||||
|
let deleted = 0;
|
||||||
|
const errors: string[] = [];
|
||||||
|
for (const e of inRange) {
|
||||||
|
if (!e.id) continue;
|
||||||
|
const r = await this.deleteException(e.id);
|
||||||
|
if (r.success) deleted++;
|
||||||
|
else errors.push(`${e.date}: ${r.error}`);
|
||||||
|
}
|
||||||
|
if (errors.length)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Erros ao deletar: ${errors.join(", ")}`,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { deletedCount: deleted },
|
||||||
|
message: `${deleted} exceção(ões) deletada(s)`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const exceptionService = new ExceptionService();
|
||||||
|
export default exceptionService;
|
||||||
@ -26,15 +26,36 @@ export function parseError(err: unknown): HttpErrorPayload {
|
|||||||
const axiosErr = err as AxiosError<unknown>;
|
const axiosErr = err as AxiosError<unknown>;
|
||||||
const status = axiosErr?.response?.status;
|
const status = axiosErr?.response?.status;
|
||||||
const data = (axiosErr?.response?.data as Record<string, unknown>) || {};
|
const data = (axiosErr?.response?.data as Record<string, unknown>) || {};
|
||||||
|
|
||||||
|
// Supabase pode retornar erro em diferentes formatos
|
||||||
const msg =
|
const msg =
|
||||||
(data["message"] as string) ||
|
(data["message"] as string) ||
|
||||||
|
(data["msg"] as string) ||
|
||||||
(data["error_description"] as string) ||
|
(data["error_description"] as string) ||
|
||||||
(data["error"] as string) ||
|
(data["error"] as string) ||
|
||||||
|
(data["hint"] as string) ||
|
||||||
axiosErr.message ||
|
axiosErr.message ||
|
||||||
"Erro desconhecido";
|
"Erro desconhecido";
|
||||||
|
|
||||||
|
// Adicionar detalhes do Supabase se disponível
|
||||||
|
const hint = data["hint"] as string | undefined;
|
||||||
|
const detail = data["details"] as string | undefined;
|
||||||
|
const finalMsg = hint
|
||||||
|
? `${msg} (${hint})`
|
||||||
|
: detail
|
||||||
|
? `${msg} (${detail})`
|
||||||
|
: msg;
|
||||||
|
|
||||||
|
console.error("[HTTP] Erro capturado:", {
|
||||||
|
status,
|
||||||
|
message: finalMsg,
|
||||||
|
code: data["code"],
|
||||||
|
details: data,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status,
|
status,
|
||||||
message: msg,
|
message: finalMsg,
|
||||||
code: data["code"] as string | undefined,
|
code: data["code"] as string | undefined,
|
||||||
details: data,
|
details: data,
|
||||||
};
|
};
|
||||||
@ -104,37 +125,27 @@ interface PendingRetry {
|
|||||||
originalErr: unknown;
|
originalErr: unknown;
|
||||||
attemptedRefresh: boolean;
|
attemptedRefresh: boolean;
|
||||||
}
|
}
|
||||||
const REFRESH_LOCK: { running: boolean; queue: Array<() => void> } = {
|
// Single-flight promise to deduplicate refresh attempts
|
||||||
running: false,
|
let ongoingRefresh: Promise<boolean> | null = null;
|
||||||
queue: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
// Contador global de 401 consecutivos sem sucesso (refresh ou request bem sucedido reseta)
|
// Contador global de 401 consecutivos sem sucesso (refresh ou request bem sucedido reseta)
|
||||||
let consecutive401 = 0;
|
let consecutive401 = 0;
|
||||||
const { MAX_401_BEFORE_FORCED_LOGOUT } = AUTH_SECURITY_CONFIG;
|
const { MAX_401_BEFORE_FORCED_LOGOUT } = AUTH_SECURITY_CONFIG;
|
||||||
|
|
||||||
async function runWithGlobalRefreshLock<T>(fn: () => Promise<T>): Promise<T> {
|
|
||||||
if (REFRESH_LOCK.running) {
|
|
||||||
await new Promise<void>((resolve) => REFRESH_LOCK.queue.push(resolve));
|
|
||||||
}
|
|
||||||
REFRESH_LOCK.running = true;
|
|
||||||
try {
|
|
||||||
return await fn();
|
|
||||||
} finally {
|
|
||||||
REFRESH_LOCK.running = false;
|
|
||||||
while (REFRESH_LOCK.queue.length) {
|
|
||||||
const r = REFRESH_LOCK.queue.shift();
|
|
||||||
if (r) r();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function attemptRefreshToken(): Promise<boolean> {
|
async function attemptRefreshToken(): Promise<boolean> {
|
||||||
// Tenta refresh via authService (se houver refresh token no localStorage)
|
if (ongoingRefresh) return ongoingRefresh;
|
||||||
const refreshToken = tokenStore.getRefreshToken();
|
ongoingRefresh = (async () => {
|
||||||
if (!refreshToken) return false;
|
// Tenta refresh via authService (se houver refresh token no storage)
|
||||||
const resp = await authService.refreshToken();
|
const refreshToken = tokenStore.getRefreshToken();
|
||||||
return resp.success;
|
if (!refreshToken) return false;
|
||||||
|
const resp = await authService.refreshToken();
|
||||||
|
return resp.success;
|
||||||
|
})();
|
||||||
|
try {
|
||||||
|
return await ongoingRefresh;
|
||||||
|
} finally {
|
||||||
|
ongoingRefresh = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function execWithRetry<T>(
|
async function execWithRetry<T>(
|
||||||
@ -162,9 +173,7 @@ async function execWithRetry<T>(
|
|||||||
if (is401 && !state.attemptedRefresh) {
|
if (is401 && !state.attemptedRefresh) {
|
||||||
state.attemptedRefresh = true;
|
state.attemptedRefresh = true;
|
||||||
try {
|
try {
|
||||||
const refreshed = await runWithGlobalRefreshLock(async () =>
|
const refreshed = await attemptRefreshToken();
|
||||||
attemptRefreshToken()
|
|
||||||
);
|
|
||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
// refresh ok, não conta como 401 final
|
// refresh ok, não conta como 401 final
|
||||||
if (consecutive401 > 0) consecutive401 = 0;
|
if (consecutive401 > 0) consecutive401 = 0;
|
||||||
|
|||||||
@ -10,5 +10,8 @@ export { default as userRoleService } from "./userRoleService";
|
|||||||
export { default as userAdminService } from "./userAdminService";
|
export { default as userAdminService } from "./userAdminService";
|
||||||
export { default as patientAssignmentService } from "./patientAssignmentService";
|
export { default as patientAssignmentService } from "./patientAssignmentService";
|
||||||
export { default as smsService } from "./smsService";
|
export { default as smsService } from "./smsService";
|
||||||
|
export { default as appointmentService } from "./appointmentService";
|
||||||
|
export { default as availabilityService } from "./availabilityService";
|
||||||
|
export { default as exceptionService } from "./exceptionService";
|
||||||
|
|
||||||
// Intencionalmente NÃO reexporta arquivos deprecated: consultaService, relatorioService, listarPacientes, pacientes.js, api.js
|
// Intencionalmente NÃO reexporta arquivos deprecated: consultaService, relatorioService, listarPacientes, pacientes.js, api.js
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { listPatients, type Paciente } from "./pacienteService";
|
import { listPatients, type Paciente } from "./pacienteService";
|
||||||
|
|
||||||
export async function listarPacientes(): Promise<Paciente[]> {
|
export async function listarPacientes(): Promise<Paciente[]> {
|
||||||
return listPatients();
|
const resp = await listPatients({ per_page: 100 });
|
||||||
|
if ("data" in resp) return resp.data;
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -157,10 +157,7 @@ class MedicoService {
|
|||||||
let remote: MedicoDetalhado[] = [];
|
let remote: MedicoDetalhado[] = [];
|
||||||
const endpointTried: string[] = [];
|
const endpointTried: string[] = [];
|
||||||
let lastError: unknown = null;
|
let lastError: unknown = null;
|
||||||
const candidates = [
|
const candidates = [ENDPOINTS.DOCTORS];
|
||||||
ENDPOINTS.DOCTORS,
|
|
||||||
ENDPOINTS.DOCTORS.replace("/doctors", "/medicos"),
|
|
||||||
];
|
|
||||||
for (const ep of candidates) {
|
for (const ep of candidates) {
|
||||||
try {
|
try {
|
||||||
// Construir params manualmente para evitar valores booleanos diretos
|
// Construir params manualmente para evitar valores booleanos diretos
|
||||||
@ -258,7 +255,15 @@ class MedicoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async criarMedico(
|
async criarMedico(
|
||||||
medico: MedicoCreate
|
medico: Partial<MedicoCreate> & {
|
||||||
|
nome: string;
|
||||||
|
email: string;
|
||||||
|
crm: string;
|
||||||
|
crmUf: string;
|
||||||
|
especialidade: string;
|
||||||
|
telefone: string;
|
||||||
|
status?: "ativo" | "inativo";
|
||||||
|
}
|
||||||
): Promise<ApiResponse<MedicoDetalhado>> {
|
): Promise<ApiResponse<MedicoDetalhado>> {
|
||||||
// Montar payload alinhado ao schema da tabela doctors
|
// Montar payload alinhado ao schema da tabela doctors
|
||||||
const apiPayload: Record<string, unknown> = {
|
const apiPayload: Record<string, unknown> = {
|
||||||
|
|||||||
@ -243,10 +243,7 @@ export async function listPatients(params?: {
|
|||||||
|
|
||||||
// Se não tem userId ou falhou, buscar todos os pacientes (comportamento original)
|
// Se não tem userId ou falhou, buscar todos os pacientes (comportamento original)
|
||||||
if (raw.length === 0) {
|
if (raw.length === 0) {
|
||||||
const candidates = [
|
const candidates = [ENDPOINTS.PATIENTS];
|
||||||
ENDPOINTS.PATIENTS,
|
|
||||||
ENDPOINTS.PATIENTS.replace("/patients", "/pacientes"),
|
|
||||||
];
|
|
||||||
for (const ep of candidates) {
|
for (const ep of candidates) {
|
||||||
try {
|
try {
|
||||||
const response = await http.get<
|
const response = await http.get<
|
||||||
|
|||||||
@ -194,8 +194,9 @@ export async function updateReport(
|
|||||||
Object.keys(body).forEach((k) => {
|
Object.keys(body).forEach((k) => {
|
||||||
if (body[k] === undefined || body[k] === "") delete body[k];
|
if (body[k] === undefined || body[k] === "") delete body[k];
|
||||||
});
|
});
|
||||||
|
// Supabase PostgREST: updates por query (id=eq.<id>)
|
||||||
const resp = await http.patch<unknown>(
|
const resp = await http.patch<unknown>(
|
||||||
`${ENDPOINTS.REPORTS}/${encodeURIComponent(id)}`,
|
`${ENDPOINTS.REPORTS}?id=eq.${encodeURIComponent(id)}`,
|
||||||
body,
|
body,
|
||||||
{ headers: { Prefer: "return=representation" } }
|
{ headers: { Prefer: "return=representation" } }
|
||||||
);
|
);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user