feat: configurar APIs funcionais - appointments com scheduled_at e availability com weekday string
This commit is contained in:
parent
f479dcde7d
commit
a4851de2e0
315
MEDICONNECT 2/API-CONFIG.md
Normal file
315
MEDICONNECT 2/API-CONFIG.md
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
# Configuração das APIs - MediConnect
|
||||||
|
|
||||||
|
## ✅ APIs Testadas e Funcionando
|
||||||
|
|
||||||
|
### 1. Autenticação (Auth API)
|
||||||
|
|
||||||
|
**Base URL:** `https://yuanqfswhberkoevtmfr.supabase.co/auth/v1`
|
||||||
|
|
||||||
|
#### Endpoints Funcionais:
|
||||||
|
|
||||||
|
- **Login** ✅
|
||||||
|
- `POST /token?grant_type=password`
|
||||||
|
- Body: `{ email, password }`
|
||||||
|
- Retorna: `{ access_token, refresh_token, user }`
|
||||||
|
|
||||||
|
- **Recuperação de Senha** ✅
|
||||||
|
- `POST /recover`
|
||||||
|
- Body: `{ email, options: { redirectTo: url } }`
|
||||||
|
- Envia email com link de recuperação
|
||||||
|
|
||||||
|
- **Atualizar Senha** ✅
|
||||||
|
- `PUT /user`
|
||||||
|
- Headers: `Authorization: Bearer <access_token>`
|
||||||
|
- Body: `{ password: "nova_senha" }`
|
||||||
|
- **IMPORTANTE:** Nova senha deve ser diferente da anterior (erro 422 se for igual)
|
||||||
|
|
||||||
|
### 2. REST API
|
||||||
|
|
||||||
|
**Base URL:** `https://yuanqfswhberkoevtmfr.supabase.co/rest/v1`
|
||||||
|
|
||||||
|
#### Tabelas e Campos Corretos:
|
||||||
|
|
||||||
|
##### **appointments** ✅
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string (UUID)
|
||||||
|
order_number: string (auto-gerado: APT-YYYY-NNNN)
|
||||||
|
patient_id: string (UUID)
|
||||||
|
doctor_id: string (UUID)
|
||||||
|
scheduled_at: string (ISO 8601 DateTime)
|
||||||
|
duration_minutes: number
|
||||||
|
appointment_type: "presencial" | "telemedicina"
|
||||||
|
status: "requested" | "confirmed" | "checked_in" | "in_progress" | "completed" | "cancelled" | "no_show"
|
||||||
|
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 (UUID)
|
||||||
|
updated_by: string | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Criar Consulta:**
|
||||||
|
```bash
|
||||||
|
POST /rest/v1/appointments
|
||||||
|
Headers:
|
||||||
|
- apikey: <SUPABASE_ANON_KEY>
|
||||||
|
- Authorization: Bearer <user_access_token>
|
||||||
|
- Content-Type: application/json
|
||||||
|
- Prefer: return=representation
|
||||||
|
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"patient_id": "uuid",
|
||||||
|
"doctor_id": "uuid",
|
||||||
|
"scheduled_at": "2025-11-03T10:00:00.000Z",
|
||||||
|
"duration_minutes": 30,
|
||||||
|
"appointment_type": "presencial",
|
||||||
|
"chief_complaint": "Motivo da consulta"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### **doctor_availability** ✅
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string (UUID)
|
||||||
|
doctor_id: string (UUID)
|
||||||
|
weekday: "sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday"
|
||||||
|
start_time: string (HH:MM:SS, ex: "07:00:00")
|
||||||
|
end_time: string (HH:MM:SS, ex: "19:00:00")
|
||||||
|
slot_duration_minutes: number (ex: 30)
|
||||||
|
appointment_type: "presencial" | "telemedicina"
|
||||||
|
is_active: boolean
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
created_by: string (UUID)
|
||||||
|
updated_by: string | null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Criar Disponibilidade:**
|
||||||
|
```bash
|
||||||
|
POST /rest/v1/doctor_availability
|
||||||
|
Headers:
|
||||||
|
- apikey: <SUPABASE_ANON_KEY>
|
||||||
|
- Authorization: Bearer <admin_access_token>
|
||||||
|
- Content-Type: application/json
|
||||||
|
- Prefer: return=representation
|
||||||
|
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"doctor_id": "uuid",
|
||||||
|
"weekday": "monday", // ⚠️ Texto, não número!
|
||||||
|
"start_time": "07:00:00",
|
||||||
|
"end_time": "19:00:00",
|
||||||
|
"slot_duration_minutes": 30,
|
||||||
|
"appointment_type": "presencial",
|
||||||
|
"is_active": true,
|
||||||
|
"created_by": "admin_user_id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### **patients** ✅
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string (UUID)
|
||||||
|
user_id: string (UUID) // ⚠️ Deve estar vinculado ao auth.users
|
||||||
|
full_name: string
|
||||||
|
email: string
|
||||||
|
cpf: string
|
||||||
|
phone_mobile: string
|
||||||
|
// ... outros campos
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Atualizar Patient:**
|
||||||
|
```bash
|
||||||
|
PATCH /rest/v1/patients?id=eq.<patient_id>
|
||||||
|
Headers:
|
||||||
|
- apikey: <SUPABASE_ANON_KEY>
|
||||||
|
- Authorization: Bearer <admin_access_token>
|
||||||
|
- Content-Type: application/json
|
||||||
|
|
||||||
|
Body:
|
||||||
|
{
|
||||||
|
"user_id": "auth_user_id"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### **doctors** ✅
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
id: string (UUID)
|
||||||
|
user_id: string (UUID)
|
||||||
|
full_name: string
|
||||||
|
email: string
|
||||||
|
crm: string
|
||||||
|
crm_uf: string
|
||||||
|
specialty: string
|
||||||
|
// ... outros campos
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Edge Functions
|
||||||
|
|
||||||
|
**Base URL:** `https://yuanqfswhberkoevtmfr.supabase.co/functions/v1`
|
||||||
|
|
||||||
|
#### Funcionais:
|
||||||
|
|
||||||
|
- **create-user-with-password** ✅
|
||||||
|
- `POST /functions/v1/create-user-with-password`
|
||||||
|
- Cria usuário com senha e perfil completo
|
||||||
|
- Body:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"email": "email@example.com",
|
||||||
|
"password": "senha123",
|
||||||
|
"full_name": "Nome Completo",
|
||||||
|
"phone_mobile": "(11) 99999-9999",
|
||||||
|
"cpf": "12345678900",
|
||||||
|
"create_patient_record": true,
|
||||||
|
"role": "paciente"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Com Problemas:
|
||||||
|
|
||||||
|
- **request-password-reset** ❌
|
||||||
|
- CORS blocking - não usar
|
||||||
|
- Usar diretamente `/auth/v1/recover` em vez disso
|
||||||
|
|
||||||
|
## 🔑 Chaves de API
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co"
|
||||||
|
SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 👥 Usuários de Teste
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
- Email: `riseup@popcode.com.br`
|
||||||
|
- Senha: `riseup`
|
||||||
|
|
||||||
|
### Dr. Fernando Pirichowski
|
||||||
|
- Email: `fernando.pirichowski@souunit.com.br`
|
||||||
|
- Senha: `fernando123`
|
||||||
|
- User ID: `38aca60d-7418-4c35-95b6-cb206bb18a0a`
|
||||||
|
- Doctor ID: `6dad001d-229b-40b5-80f3-310243c4599c`
|
||||||
|
- CRM: `24245`
|
||||||
|
- Disponibilidade: Segunda a Domingo, 07:00-19:00
|
||||||
|
|
||||||
|
### Aurora Sabrina Clara Nascimento (Paciente)
|
||||||
|
- Email: `aurora-nascimento94@gmx.com`
|
||||||
|
- Senha: `auroranasc94`
|
||||||
|
- User ID: `6dc15cc5-7dae-4b30-924a-a4b4fa142f24`
|
||||||
|
- Patient ID: `b85486f7-9135-4b67-9aa7-b884d9603d12`
|
||||||
|
- CPF: `66864784231`
|
||||||
|
- Telefone: `(21) 99856-3014`
|
||||||
|
|
||||||
|
## ⚠️ Pontos de Atenção
|
||||||
|
|
||||||
|
### 1. Weekday no doctor_availability
|
||||||
|
- ❌ **NÃO** usar números (0-6)
|
||||||
|
- ✅ **USAR** strings em inglês: `"sunday"`, `"monday"`, `"tuesday"`, `"wednesday"`, `"thursday"`, `"friday"`, `"saturday"`
|
||||||
|
|
||||||
|
### 2. scheduled_at em appointments
|
||||||
|
- ❌ **NÃO** usar campos separados `appointment_date` e `appointment_time`
|
||||||
|
- ✅ **USAR** campo único `scheduled_at` com ISO 8601 DateTime
|
||||||
|
- Exemplo: `"2025-11-03T10:00:00.000Z"`
|
||||||
|
|
||||||
|
### 3. user_id nas tabelas patients e doctors
|
||||||
|
- ⚠️ Sempre vincular ao `auth.users.id`
|
||||||
|
- Sem esse vínculo, queries por `user_id` não funcionam
|
||||||
|
|
||||||
|
### 4. Senha na recuperação
|
||||||
|
- ⚠️ Nova senha DEVE ser diferente da anterior
|
||||||
|
- Erro 422 com `error_code: "same_password"` se tentar usar a mesma
|
||||||
|
|
||||||
|
### 5. redirectTo no password recovery
|
||||||
|
- ⚠️ Supabase pode ignorar o parâmetro `redirectTo`
|
||||||
|
- ✅ Implementar detecção de token no lado do cliente
|
||||||
|
- Verificar tanto query string `?token=` quanto hash `#access_token=`
|
||||||
|
|
||||||
|
## 📦 Estrutura de Serviços no Frontend
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Tudo configurado em:
|
||||||
|
src/services/api/config.ts // URLs e chaves
|
||||||
|
src/services/api/client.ts // Cliente axios
|
||||||
|
src/services/appointments/ // Serviço de consultas
|
||||||
|
src/services/availability/ // Disponibilidade médicos
|
||||||
|
src/services/auth/ // Autenticação
|
||||||
|
src/services/doctors/ // Médicos
|
||||||
|
src/services/patients/ // Pacientes
|
||||||
|
src/services/index.ts // Exportações centralizadas
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Status Atual
|
||||||
|
|
||||||
|
- [x] Autenticação funcionando
|
||||||
|
- [x] Recuperação de senha funcionando
|
||||||
|
- [x] Criação de usuários funcionando
|
||||||
|
- [x] Criação de pacientes funcionando
|
||||||
|
- [x] Criação de disponibilidade médica funcionando
|
||||||
|
- [x] Criação de consultas funcionando
|
||||||
|
- [x] Vinculação user_id ↔ patient_id corrigida
|
||||||
|
- [x] Todos os serviços usando campos corretos
|
||||||
|
|
||||||
|
## 🚀 Próximos Passos
|
||||||
|
|
||||||
|
1. Testar agendamento completo no frontend
|
||||||
|
2. Verificar listagem de consultas
|
||||||
|
3. Testar cancelamento e atualização de consultas
|
||||||
|
4. Verificar notificações SMS
|
||||||
|
5. Testar fluxo completo de check-in e prontuário
|
||||||
|
|
||||||
|
## 📝 Exemplos de Uso
|
||||||
|
|
||||||
|
### Criar Consulta
|
||||||
|
```typescript
|
||||||
|
import { appointmentService } from '@/services';
|
||||||
|
|
||||||
|
const appointment = await appointmentService.create({
|
||||||
|
patient_id: 'patient-uuid',
|
||||||
|
doctor_id: 'doctor-uuid',
|
||||||
|
scheduled_at: '2025-11-03T10:00:00.000Z',
|
||||||
|
duration_minutes: 30,
|
||||||
|
appointment_type: 'presencial',
|
||||||
|
chief_complaint: 'Consulta de rotina'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Criar Disponibilidade
|
||||||
|
```typescript
|
||||||
|
import { availabilityService } from '@/services';
|
||||||
|
|
||||||
|
const availability = await availabilityService.create({
|
||||||
|
doctor_id: 'doctor-uuid',
|
||||||
|
weekday: 'monday',
|
||||||
|
start_time: '07:00:00',
|
||||||
|
end_time: '19:00:00',
|
||||||
|
slot_duration_minutes: 30,
|
||||||
|
appointment_type: 'presencial'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Login
|
||||||
|
```typescript
|
||||||
|
import { authService } from '@/services';
|
||||||
|
|
||||||
|
const response = await authService.login({
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'senha123'
|
||||||
|
});
|
||||||
|
|
||||||
|
// response.access_token - JWT token
|
||||||
|
// response.user - dados do usuário
|
||||||
|
```
|
||||||
@ -58,6 +58,7 @@ npx wrangler pages deploy dist --project-name=mediconnect --branch=production
|
|||||||
### Autenticação JWT com Supabase
|
### Autenticação JWT com Supabase
|
||||||
|
|
||||||
O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna:
|
O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna:
|
||||||
|
|
||||||
- `access_token` (JWT, expira em 1 hora)
|
- `access_token` (JWT, expira em 1 hora)
|
||||||
- `refresh_token` (para renovação automática)
|
- `refresh_token` (para renovação automática)
|
||||||
|
|
||||||
@ -65,47 +66,51 @@ O sistema usa **Supabase Auth** com tokens JWT. Todo login retorna:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Adiciona token automaticamente em todas as requisições
|
// Adiciona token automaticamente em todas as requisições
|
||||||
axios.interceptors.request.use(config => {
|
axios.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('access_token')
|
const token = localStorage.getItem("access_token");
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
return config
|
return config;
|
||||||
})
|
});
|
||||||
|
|
||||||
// Refresh automático quando token expira
|
// Refresh automático quando token expira
|
||||||
axios.interceptors.response.use(
|
axios.interceptors.response.use(
|
||||||
response => response,
|
(response) => response,
|
||||||
async error => {
|
async (error) => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
const refreshToken = localStorage.getItem('refresh_token')
|
const refreshToken = localStorage.getItem("refresh_token");
|
||||||
const newTokens = await authService.refreshToken(refreshToken)
|
const newTokens = await authService.refreshToken(refreshToken);
|
||||||
// Retry request original
|
// Retry request original
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Roles e Permissões (RLS)
|
### Roles e Permissões (RLS)
|
||||||
|
|
||||||
#### 👑 Admin/Gestor:
|
#### 👑 Admin/Gestor:
|
||||||
|
|
||||||
- ✅ **Acesso completo a todos os recursos**
|
- ✅ **Acesso completo a todos os recursos**
|
||||||
- ✅ Criar/editar/deletar usuários, médicos, pacientes
|
- ✅ Criar/editar/deletar usuários, médicos, pacientes
|
||||||
- ✅ Visualizar todos os agendamentos e prontuários
|
- ✅ Visualizar todos os agendamentos e prontuários
|
||||||
|
|
||||||
#### 👨⚕️ Médicos:
|
#### 👨⚕️ Médicos:
|
||||||
|
|
||||||
- ✅ Veem **todos os pacientes**
|
- ✅ Veem **todos os pacientes**
|
||||||
- ✅ Veem apenas **seus próprios laudos** (filtro: `created_by = médico`)
|
- ✅ Veem apenas **seus próprios laudos** (filtro: `created_by = médico`)
|
||||||
- ✅ Veem apenas **seus próprios agendamentos** (filtro: `doctor_id = médico`)
|
- ✅ Veem apenas **seus próprios agendamentos** (filtro: `doctor_id = médico`)
|
||||||
- ✅ Editam apenas **seus próprios laudos e agendamentos**
|
- ✅ Editam apenas **seus próprios laudos e agendamentos**
|
||||||
|
|
||||||
#### 👤 Pacientes:
|
#### 👤 Pacientes:
|
||||||
|
|
||||||
- ✅ Veem apenas **seus próprios dados**
|
- ✅ Veem apenas **seus próprios dados**
|
||||||
- ✅ Veem apenas **seus próprios laudos** (filtro: `patient_id = paciente`)
|
- ✅ Veem apenas **seus próprios laudos** (filtro: `patient_id = paciente`)
|
||||||
- ✅ Veem apenas **seus próprios agendamentos**
|
- ✅ Veem apenas **seus próprios agendamentos**
|
||||||
- ✅ Podem agendar consultas
|
- ✅ Podem agendar consultas
|
||||||
|
|
||||||
#### 👩💼 Secretárias:
|
#### 👩💼 Secretárias:
|
||||||
|
|
||||||
- ✅ Veem **todos os pacientes**
|
- ✅ Veem **todos os pacientes**
|
||||||
- ✅ Veem **todos os agendamentos**
|
- ✅ Veem **todos os agendamentos**
|
||||||
- ✅ Veem **todos os laudos**
|
- ✅ Veem **todos os laudos**
|
||||||
@ -145,96 +150,96 @@ src/services/
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Login
|
// Login
|
||||||
await authService.login({ email, password })
|
await authService.login({ email, password });
|
||||||
// Retorna: { access_token, refresh_token, user }
|
// Retorna: { access_token, refresh_token, user }
|
||||||
|
|
||||||
// Recuperação de senha
|
// Recuperação de senha
|
||||||
await authService.requestPasswordReset(email)
|
await authService.requestPasswordReset(email);
|
||||||
// Envia email com link de reset
|
// Envia email com link de reset
|
||||||
|
|
||||||
// Atualizar senha
|
// Atualizar senha
|
||||||
await authService.updatePassword(accessToken, newPassword)
|
await authService.updatePassword(accessToken, newPassword);
|
||||||
// Usado na página /reset-password
|
// Usado na página /reset-password
|
||||||
|
|
||||||
// Refresh token
|
// Refresh token
|
||||||
await authService.refreshToken(refreshToken)
|
await authService.refreshToken(refreshToken);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 👤 Usuários (userService)
|
#### 👤 Usuários (userService)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Buscar informações do usuário autenticado (com roles)
|
// Buscar informações do usuário autenticado (com roles)
|
||||||
const userInfo = await userService.getUserInfo()
|
const userInfo = await userService.getUserInfo();
|
||||||
// Retorna: { id, email, full_name, roles: ['medico', 'admin'] }
|
// Retorna: { id, email, full_name, roles: ['medico', 'admin'] }
|
||||||
|
|
||||||
// Criar usuário com role
|
// Criar usuário com role
|
||||||
await userService.createUser({
|
await userService.createUser({
|
||||||
email: 'user@example.com',
|
email: "user@example.com",
|
||||||
full_name: 'Nome Completo',
|
full_name: "Nome Completo",
|
||||||
role: 'medico' // ou 'admin', 'paciente', 'secretaria'
|
role: "medico", // ou 'admin', 'paciente', 'secretaria'
|
||||||
})
|
});
|
||||||
|
|
||||||
// Deletar usuário
|
// Deletar usuário
|
||||||
await userService.deleteUser(userId)
|
await userService.deleteUser(userId);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 🏥 Pacientes (patientService)
|
#### 🏥 Pacientes (patientService)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Listar pacientes
|
// Listar pacientes
|
||||||
const patients = await patientService.list()
|
const patients = await patientService.list();
|
||||||
|
|
||||||
// Buscar por ID
|
// Buscar por ID
|
||||||
const patient = await patientService.getById(id)
|
const patient = await patientService.getById(id);
|
||||||
|
|
||||||
// Criar paciente
|
// Criar paciente
|
||||||
await patientService.create({
|
await patientService.create({
|
||||||
email: 'paciente@example.com',
|
email: "paciente@example.com",
|
||||||
full_name: 'Nome Paciente',
|
full_name: "Nome Paciente",
|
||||||
cpf: '12345678900',
|
cpf: "12345678900",
|
||||||
phone_mobile: '11999999999'
|
phone_mobile: "11999999999",
|
||||||
})
|
});
|
||||||
|
|
||||||
// Atualizar paciente
|
// Atualizar paciente
|
||||||
await patientService.update(id, { phone_mobile: '11888888888' })
|
await patientService.update(id, { phone_mobile: "11888888888" });
|
||||||
|
|
||||||
// Deletar paciente
|
// Deletar paciente
|
||||||
await patientService.delete(id)
|
await patientService.delete(id);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 👨⚕️ Médicos (doctorService)
|
#### 👨⚕️ Médicos (doctorService)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Listar médicos
|
// Listar médicos
|
||||||
const doctors = await doctorService.list()
|
const doctors = await doctorService.list();
|
||||||
|
|
||||||
// Buscar por ID
|
// Buscar por ID
|
||||||
const doctor = await doctorService.getById(id)
|
const doctor = await doctorService.getById(id);
|
||||||
|
|
||||||
// Buscar disponibilidade
|
// Buscar disponibilidade
|
||||||
const slots = await doctorService.getAvailableSlots(doctorId, date)
|
const slots = await doctorService.getAvailableSlots(doctorId, date);
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 📅 Agendamentos (appointmentService)
|
#### 📅 Agendamentos (appointmentService)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Listar agendamentos (filtrado por role automaticamente)
|
// Listar agendamentos (filtrado por role automaticamente)
|
||||||
const appointments = await appointmentService.list()
|
const appointments = await appointmentService.list();
|
||||||
|
|
||||||
// Criar agendamento
|
// Criar agendamento
|
||||||
await appointmentService.create({
|
await appointmentService.create({
|
||||||
patient_id: 'uuid-paciente',
|
patient_id: "uuid-paciente",
|
||||||
doctor_id: 'uuid-medico',
|
doctor_id: "uuid-medico",
|
||||||
scheduled_at: '2025-10-25T10:00:00',
|
scheduled_at: "2025-10-25T10:00:00",
|
||||||
reason: 'Consulta de rotina'
|
reason: "Consulta de rotina",
|
||||||
})
|
});
|
||||||
|
|
||||||
// Atualizar status
|
// Atualizar status
|
||||||
await appointmentService.updateStatus(id, 'confirmed')
|
await appointmentService.updateStatus(id, "confirmed");
|
||||||
// Status: requested, confirmed, completed, cancelled
|
// Status: requested, confirmed, completed, cancelled
|
||||||
|
|
||||||
// Cancelar
|
// Cancelar
|
||||||
await appointmentService.cancel(id, 'Motivo do cancelamento')
|
await appointmentService.cancel(id, "Motivo do cancelamento");
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -253,9 +258,9 @@ export const API_CONFIG = {
|
|||||||
STORAGE_KEYS: {
|
STORAGE_KEYS: {
|
||||||
ACCESS_TOKEN: "mediconnect_access_token",
|
ACCESS_TOKEN: "mediconnect_access_token",
|
||||||
REFRESH_TOKEN: "mediconnect_refresh_token",
|
REFRESH_TOKEN: "mediconnect_refresh_token",
|
||||||
USER: "mediconnect_user"
|
USER: "mediconnect_user",
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -292,11 +297,11 @@ npx wrangler pages deploy dist --project-name=mediconnect --commit-dirty=true
|
|||||||
|
|
||||||
## 1. Variáveis de Ambiente (`.env` / `.env.local`)
|
## 1. Variáveis de Ambiente (`.env` / `.env.local`)
|
||||||
|
|
||||||
| Variável | Obrigatória | Descrição |
|
| Variável | Obrigatória | Descrição |
|
||||||
| ------------------------ | ---------------- | --------------------------------------------------------------- |
|
| ------------------------ | ----------- | --------------------------------------------------------------- |
|
||||||
| `VITE_SUPABASE_URL` | Sim | URL base do projeto Supabase (`https://<ref>.supabase.co`) |
|
| `VITE_SUPABASE_URL` | Sim | URL base do projeto Supabase (`https://<ref>.supabase.co`) |
|
||||||
| `VITE_SUPABASE_ANON_KEY` | Sim | Chave pública (anon) usada para Auth password grant e PostgREST |
|
| `VITE_SUPABASE_ANON_KEY` | Sim | Chave pública (anon) usada para Auth password grant e PostgREST |
|
||||||
| `VITE_APP_ENV` | Não | Identifica ambiente (ex: `dev`, `staging`, `prod`) |
|
| `VITE_APP_ENV` | Não | Identifica ambiente (ex: `dev`, `staging`, `prod`) |
|
||||||
|
|
||||||
**Nota:** As variáveis já estão configuradas no código em `src/services/api/config.ts`. Não é necessário criar arquivo `.env` para desenvolvimento local.
|
**Nota:** As variáveis já estão configuradas no código em `src/services/api/config.ts`. Não é necessário criar arquivo `.env` para desenvolvimento local.
|
||||||
|
|
||||||
@ -314,38 +319,37 @@ Boas práticas:
|
|||||||
```typescript
|
```typescript
|
||||||
// pages/LoginMedico.tsx
|
// pages/LoginMedico.tsx
|
||||||
const handleLogin = async (e: FormEvent) => {
|
const handleLogin = async (e: FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Autenticar com Supabase
|
// 1. Autenticar com Supabase
|
||||||
const loginResponse = await authService.login({ email, password })
|
const loginResponse = await authService.login({ email, password });
|
||||||
|
|
||||||
// 2. Salvar tokens
|
// 2. Salvar tokens
|
||||||
localStorage.setItem('access_token', loginResponse.access_token)
|
localStorage.setItem("access_token", loginResponse.access_token);
|
||||||
localStorage.setItem('refresh_token', loginResponse.refresh_token)
|
localStorage.setItem("refresh_token", loginResponse.refresh_token);
|
||||||
|
|
||||||
// 3. Buscar informações do usuário com roles
|
// 3. Buscar informações do usuário com roles
|
||||||
const userInfo = await userService.getUserInfo()
|
const userInfo = await userService.getUserInfo();
|
||||||
const roles = userInfo.roles || []
|
const roles = userInfo.roles || [];
|
||||||
|
|
||||||
// 4. Validar permissões
|
// 4. Validar permissões
|
||||||
const isAdmin = roles.includes('admin')
|
const isAdmin = roles.includes("admin");
|
||||||
const isGestor = roles.includes('gestor')
|
const isGestor = roles.includes("gestor");
|
||||||
const isMedico = roles.includes('medico')
|
const isMedico = roles.includes("medico");
|
||||||
|
|
||||||
if (!isAdmin && !isGestor && !isMedico) {
|
if (!isAdmin && !isGestor && !isMedico) {
|
||||||
toast.error("Você não tem permissão para acessar esta área")
|
toast.error("Você não tem permissão para acessar esta área");
|
||||||
await authService.logout()
|
await authService.logout();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Redirecionar para o painel
|
// 5. Redirecionar para o painel
|
||||||
navigate('/painel-medico')
|
navigate("/painel-medico");
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Email ou senha incorretos")
|
toast.error("Email ou senha incorretos");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -367,35 +371,35 @@ const handleLogin = async (e: FormEvent) => {
|
|||||||
// Solicitar recuperação (páginas de login)
|
// Solicitar recuperação (páginas de login)
|
||||||
const handlePasswordReset = async () => {
|
const handlePasswordReset = async () => {
|
||||||
try {
|
try {
|
||||||
await authService.requestPasswordReset(email)
|
await authService.requestPasswordReset(email);
|
||||||
toast.success("Link de recuperação enviado para seu email")
|
toast.success("Link de recuperação enviado para seu email");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Erro ao enviar email de recuperação")
|
toast.error("Erro ao enviar email de recuperação");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Página de reset (ResetPassword.tsx)
|
// Página de reset (ResetPassword.tsx)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Extrair token do URL
|
// Extrair token do URL
|
||||||
const hash = window.location.hash
|
const hash = window.location.hash;
|
||||||
const params = new URLSearchParams(hash.substring(1))
|
const params = new URLSearchParams(hash.substring(1));
|
||||||
const token = params.get('access_token')
|
const token = params.get("access_token");
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
setAccessToken(token)
|
setAccessToken(token);
|
||||||
setIsLoading(false)
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent) => {
|
const handleSubmit = async (e: FormEvent) => {
|
||||||
try {
|
try {
|
||||||
await authService.updatePassword(accessToken, newPassword)
|
await authService.updatePassword(accessToken, newPassword);
|
||||||
toast.success("Senha atualizada com sucesso!")
|
toast.success("Senha atualizada com sucesso!");
|
||||||
navigate('/login-paciente')
|
navigate("/login-paciente");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Erro ao atualizar senha")
|
toast.error("Erro ao atualizar senha");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -405,6 +409,7 @@ const handleSubmit = async (e: FormEvent) => {
|
|||||||
### Tabelas Principais
|
### Tabelas Principais
|
||||||
|
|
||||||
#### `profiles`
|
#### `profiles`
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
- id (uuid, PK)
|
- id (uuid, PK)
|
||||||
- email (text, unique)
|
- email (text, unique)
|
||||||
@ -415,6 +420,7 @@ const handleSubmit = async (e: FormEvent) => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### `patients`
|
#### `patients`
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
- id (uuid, PK, FK -> profiles)
|
- id (uuid, PK, FK -> profiles)
|
||||||
- email (text, unique)
|
- email (text, unique)
|
||||||
@ -427,6 +433,7 @@ const handleSubmit = async (e: FormEvent) => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### `doctors`
|
#### `doctors`
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
- id (uuid, PK, FK -> profiles)
|
- id (uuid, PK, FK -> profiles)
|
||||||
- email (text, unique)
|
- email (text, unique)
|
||||||
@ -438,6 +445,7 @@ const handleSubmit = async (e: FormEvent) => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### `appointments`
|
#### `appointments`
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
- id (uuid, PK)
|
- id (uuid, PK)
|
||||||
- patient_id (uuid, FK -> patients)
|
- patient_id (uuid, FK -> patients)
|
||||||
@ -450,6 +458,7 @@ const handleSubmit = async (e: FormEvent) => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### `user_roles`
|
#### `user_roles`
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
- user_id (uuid, FK -> profiles)
|
- user_id (uuid, FK -> profiles)
|
||||||
- role (text: admin, gestor, medico, secretaria, paciente)
|
- role (text: admin, gestor, medico, secretaria, paciente)
|
||||||
@ -462,13 +471,14 @@ const handleSubmit = async (e: FormEvent) => {
|
|||||||
|
|
||||||
O sistema usa uma estratégia híbrida para máxima segurança:
|
O sistema usa uma estratégia híbrida para máxima segurança:
|
||||||
|
|
||||||
| Tipo | Local | Expiração Natural |
|
| Tipo | Local | Expiração Natural |
|
||||||
| -------------- | ----------------- | ------------------------------------ |
|
| ------------- | ------------ | --------------------------------- |
|
||||||
| Access Token | localStorage | 1 hora (renovado automaticamente) |
|
| Access Token | localStorage | 1 hora (renovado automaticamente) |
|
||||||
| Refresh Token | localStorage | 30 dias (ou revogação backend) |
|
| Refresh Token | localStorage | 30 dias (ou revogação backend) |
|
||||||
| User Snapshot | localStorage | Limpo em logout |
|
| User Snapshot | localStorage | Limpo em logout |
|
||||||
|
|
||||||
**Segurança:**
|
**Segurança:**
|
||||||
|
|
||||||
- Tokens são limpos automaticamente no logout
|
- Tokens são limpos automaticamente no logout
|
||||||
- Refresh automático quando access_token expira (401)
|
- Refresh automático quando access_token expira (401)
|
||||||
- Interceptors garantem tokens válidos em todas as requisições
|
- Interceptors garantem tokens válidos em todas as requisições
|
||||||
@ -497,6 +507,7 @@ Fluxo de Refresh:
|
|||||||
|
|
||||||
1. Requisição falha com 401.
|
1. Requisição falha com 401.
|
||||||
2. Wrapper (`http.ts`) obtém refresh do `tokenStore`.
|
2. Wrapper (`http.ts`) obtém refresh do `tokenStore`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Scripts Utilitários
|
## 7. Scripts Utilitários
|
||||||
@ -545,9 +556,9 @@ node search-fernando.cjs
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Imports
|
// Imports
|
||||||
import React, { useState, useEffect } from "react"
|
import React, { useState, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom";
|
||||||
import { serviceImport } from "../services"
|
import { serviceImport } from "../services";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -557,28 +568,24 @@ interface Props {
|
|||||||
// Component
|
// Component
|
||||||
const ComponentName: React.FC<Props> = ({ ...props }) => {
|
const ComponentName: React.FC<Props> = ({ ...props }) => {
|
||||||
// Hooks
|
// Hooks
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
const [state, setState] = useState()
|
const [state, setState] = useState();
|
||||||
|
|
||||||
// Effects
|
// Effects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// ...
|
// ...
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const handleAction = async () => {
|
const handleAction = async () => {
|
||||||
// ...
|
// ...
|
||||||
}
|
};
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
return (
|
return <div>{/* JSX */}</div>;
|
||||||
<div>
|
};
|
||||||
{/* JSX */}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ComponentName
|
export default ComponentName;
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -586,6 +593,7 @@ export default ComponentName
|
|||||||
## 9. Tecnologias Utilizadas
|
## 9. Tecnologias Utilizadas
|
||||||
|
|
||||||
### Frontend
|
### Frontend
|
||||||
|
|
||||||
- **React** 18.3.1 - Biblioteca UI
|
- **React** 18.3.1 - Biblioteca UI
|
||||||
- **TypeScript** 5.9.3 - Tipagem estática
|
- **TypeScript** 5.9.3 - Tipagem estática
|
||||||
- **Vite** 7.1.10 - Build tool
|
- **Vite** 7.1.10 - Build tool
|
||||||
@ -596,11 +604,13 @@ export default ComponentName
|
|||||||
- **date-fns** 4.1.0 - Manipulação de datas
|
- **date-fns** 4.1.0 - Manipulação de datas
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|
||||||
- **Supabase** - Backend as a Service
|
- **Supabase** - Backend as a Service
|
||||||
- **PostgreSQL** - Banco de dados relacional
|
- **PostgreSQL** - Banco de dados relacional
|
||||||
- **Supabase Auth** - Autenticação JWT
|
- **Supabase Auth** - Autenticação JWT
|
||||||
|
|
||||||
### Deploy
|
### Deploy
|
||||||
|
|
||||||
- **Cloudflare Pages** - Hospedagem frontend
|
- **Cloudflare Pages** - Hospedagem frontend
|
||||||
- **Wrangler** 4.44.0 - CLI Cloudflare
|
- **Wrangler** 4.44.0 - CLI Cloudflare
|
||||||
|
|
||||||
@ -615,4 +625,3 @@ export default ComponentName
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Desenvolvido com ❤️ pela Squad 18**
|
**Desenvolvido com ❤️ pela Squad 18**
|
||||||
|
|
||||||
|
|||||||
121
MEDICONNECT 2/create-aurora-appointment.cjs
Normal file
121
MEDICONNECT 2/create-aurora-appointment.cjs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
|
||||||
|
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
|
||||||
|
|
||||||
|
async function createAppointment() {
|
||||||
|
try {
|
||||||
|
console.log('🔐 Fazendo login como Aurora...');
|
||||||
|
|
||||||
|
// Login como Aurora
|
||||||
|
const loginResponse = await axios.post(
|
||||||
|
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
||||||
|
{
|
||||||
|
email: 'aurora-nascimento94@gmx.com',
|
||||||
|
password: 'auroranasc94'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const auroraToken = loginResponse.data.access_token;
|
||||||
|
console.log('✅ Login realizado como Aurora');
|
||||||
|
|
||||||
|
// Buscar o patient_id da Aurora
|
||||||
|
console.log('\n👤 Buscando dados da paciente...');
|
||||||
|
const patientResponse = await axios.get(
|
||||||
|
`${SUPABASE_URL}/rest/v1/patients?user_id=eq.${loginResponse.data.user.id}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${auroraToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!patientResponse.data || patientResponse.data.length === 0) {
|
||||||
|
throw new Error('Paciente não encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
const patientId = patientResponse.data[0].id;
|
||||||
|
console.log(`✅ Patient ID: ${patientId}`);
|
||||||
|
|
||||||
|
// Buscar disponibilidade do Dr. Fernando para segunda-feira
|
||||||
|
console.log('\n📅 Buscando disponibilidade do Dr. Fernando...');
|
||||||
|
const availabilityResponse = await axios.get(
|
||||||
|
`${SUPABASE_URL}/rest/v1/doctor_availability?doctor_id=eq.6dad001d-229b-40b5-80f3-310243c4599c&weekday=eq.monday`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${auroraToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!availabilityResponse.data || availabilityResponse.data.length === 0) {
|
||||||
|
throw new Error('Disponibilidade não encontrada');
|
||||||
|
}
|
||||||
|
|
||||||
|
const availability = availabilityResponse.data[0];
|
||||||
|
console.log(`✅ Disponibilidade encontrada: ${availability.start_time} - ${availability.end_time}`);
|
||||||
|
|
||||||
|
// Criar consulta para próxima segunda-feira às 10:00
|
||||||
|
const today = new Date();
|
||||||
|
const daysUntilMonday = (1 - today.getDay() + 7) % 7 || 7; // Próxima segunda
|
||||||
|
const appointmentDate = new Date(today);
|
||||||
|
appointmentDate.setDate(today.getDate() + daysUntilMonday);
|
||||||
|
appointmentDate.setHours(10, 0, 0, 0);
|
||||||
|
|
||||||
|
const scheduledAt = appointmentDate.toISOString();
|
||||||
|
|
||||||
|
console.log(`\n📝 Criando consulta para ${scheduledAt}...`);
|
||||||
|
|
||||||
|
const appointmentData = {
|
||||||
|
patient_id: patientId,
|
||||||
|
doctor_id: '6dad001d-229b-40b5-80f3-310243c4599c',
|
||||||
|
scheduled_at: scheduledAt,
|
||||||
|
duration_minutes: 30,
|
||||||
|
appointment_type: 'presencial',
|
||||||
|
chief_complaint: 'Consulta de rotina'
|
||||||
|
};
|
||||||
|
|
||||||
|
const appointmentResponse = await axios.post(
|
||||||
|
`${SUPABASE_URL}/rest/v1/appointments`,
|
||||||
|
appointmentData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${auroraToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Prefer': 'return=representation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('\n✅ Consulta criada com sucesso!');
|
||||||
|
console.log('\n📋 Detalhes da consulta:');
|
||||||
|
console.log(` - Paciente: Aurora Sabrina Clara Nascimento`);
|
||||||
|
console.log(` - Médico: Dr. Fernando Pirichowski`);
|
||||||
|
console.log(` - Data/Hora: ${scheduledAt}`);
|
||||||
|
console.log(` - Duração: 30 minutos`);
|
||||||
|
console.log(` - Tipo: presencial`);
|
||||||
|
|
||||||
|
if (appointmentResponse.data && appointmentResponse.data.length > 0) {
|
||||||
|
console.log(` - ID da consulta: ${appointmentResponse.data[0].id}`);
|
||||||
|
console.log(` - Order Number: ${appointmentResponse.data[0].order_number}`);
|
||||||
|
console.log(` - Status: ${appointmentResponse.data[0].status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro ao criar consulta:', error.response?.data || error.message);
|
||||||
|
if (error.response?.data) {
|
||||||
|
console.error('Detalhes:', JSON.stringify(error.response.data, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createAppointment();
|
||||||
91
MEDICONNECT 2/create-fernando-availability.cjs
Normal file
91
MEDICONNECT 2/create-fernando-availability.cjs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
|
||||||
|
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
|
||||||
|
|
||||||
|
// IDs
|
||||||
|
const DOCTOR_ID = '6dad001d-229b-40b5-80f3-310243c4599c'; // Fernando (CRM 24245)
|
||||||
|
const ADMIN_ID = 'c7fcd702-9a6e-4b7c-abd3-956b25af407d'; // Admin (riseup)
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔐 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\n');
|
||||||
|
|
||||||
|
console.log('📅 Criando disponibilidade para Dr. Fernando...');
|
||||||
|
console.log('⏰ Horário: 07:00 às 19:00');
|
||||||
|
console.log('📆 Dias: Segunda a Domingo\n');
|
||||||
|
|
||||||
|
const weekdays = [
|
||||||
|
{ num: 'sunday', name: 'Domingo' },
|
||||||
|
{ num: 'monday', name: 'Segunda-feira' },
|
||||||
|
{ num: 'tuesday', name: 'Terça-feira' },
|
||||||
|
{ num: 'wednesday', name: 'Quarta-feira' },
|
||||||
|
{ num: 'thursday', name: 'Quinta-feira' },
|
||||||
|
{ num: 'friday', name: 'Sexta-feira' },
|
||||||
|
{ num: 'saturday', name: 'Sábado' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const day of weekdays) {
|
||||||
|
try {
|
||||||
|
const availabilityData = {
|
||||||
|
doctor_id: DOCTOR_ID,
|
||||||
|
weekday: day.num,
|
||||||
|
start_time: '07:00:00',
|
||||||
|
end_time: '19:00:00',
|
||||||
|
slot_minutes: 30,
|
||||||
|
appointment_type: 'presencial',
|
||||||
|
active: true,
|
||||||
|
created_by: ADMIN_ID
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${SUPABASE_URL}/rest/v1/doctor_availability`,
|
||||||
|
availabilityData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${adminToken}`,
|
||||||
|
'Prefer': 'return=representation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`✅ ${day.name}: Disponibilidade criada`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ ${day.name}: Erro -`, error.response?.data?.message || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n🎉 Disponibilidade criada com sucesso!');
|
||||||
|
console.log('\n📋 Resumo:');
|
||||||
|
console.log('- Médico: Dr. Fernando Pirichowski');
|
||||||
|
console.log('- Dias: Todos os dias da semana (Domingo a Sábado)');
|
||||||
|
console.log('- Horário: 07:00 às 19:00');
|
||||||
|
console.log('- Duração consulta: 30 minutos');
|
||||||
|
console.log('- Tipo: Presencial');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro geral:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
78
MEDICONNECT 2/create-patient-with-password.cjs
Normal file
78
MEDICONNECT 2/create-patient-with-password.cjs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
// Configuração
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Dados do paciente (Aurora Sabrina Clara Nascimento)
|
||||||
|
const PATIENT_DATA = {
|
||||||
|
email: 'aurora-nascimento94@gmx.com',
|
||||||
|
password: 'auroranasc94',
|
||||||
|
full_name: 'Aurora Sabrina Clara Nascimento',
|
||||||
|
phone_mobile: '(21) 99856-3014',
|
||||||
|
cpf: '66864784231', // CPF sem pontuação
|
||||||
|
create_patient_record: true,
|
||||||
|
role: 'paciente'
|
||||||
|
};
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔐 1. 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: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'apikey': SUPABASE_ANON_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const adminToken = loginResponse.data.access_token;
|
||||||
|
console.log('✅ Login realizado com sucesso!');
|
||||||
|
console.log('🔑 Token:', adminToken.substring(0, 30) + '...');
|
||||||
|
|
||||||
|
console.log('\n👤 2. Criando paciente...');
|
||||||
|
console.log('Dados:', JSON.stringify(PATIENT_DATA, null, 2));
|
||||||
|
|
||||||
|
// 2. Criar paciente
|
||||||
|
const createResponse = await axios.post(
|
||||||
|
`${SUPABASE_URL}/functions/v1/create-user-with-password`,
|
||||||
|
PATIENT_DATA,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${adminToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('\n✅ Paciente criado com sucesso!');
|
||||||
|
console.log('Resposta:', JSON.stringify(createResponse.data, null, 2));
|
||||||
|
|
||||||
|
if (createResponse.data.patient_id) {
|
||||||
|
console.log('\n📋 ID do paciente:', createResponse.data.patient_id);
|
||||||
|
console.log('✉️ Email de confirmação enviado para:', PATIENT_DATA.email);
|
||||||
|
console.log('🔐 Senha temporária:', PATIENT_DATA.password);
|
||||||
|
console.log('\n⚠️ O usuário precisa confirmar o email antes de fazer login!');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro:', error.response?.data || error.message);
|
||||||
|
console.error('Status:', error.response?.status);
|
||||||
|
console.error('URL:', error.config?.url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
72
MEDICONNECT 2/fix-aurora-user-id.cjs
Normal file
72
MEDICONNECT 2/fix-aurora-user-id.cjs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
|
||||||
|
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
|
||||||
|
|
||||||
|
async function fixAuroraUserId() {
|
||||||
|
try {
|
||||||
|
console.log('🔐 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: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const adminToken = loginResponse.data.access_token;
|
||||||
|
console.log('✅ Login realizado');
|
||||||
|
|
||||||
|
// Fazer login como Aurora para pegar o user_id
|
||||||
|
console.log('\n👤 Fazendo login como Aurora...');
|
||||||
|
const auroraLoginResponse = await axios.post(
|
||||||
|
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
|
||||||
|
{
|
||||||
|
email: 'aurora-nascimento94@gmx.com',
|
||||||
|
password: 'auroranasc94'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const auroraUserId = auroraLoginResponse.data.user.id;
|
||||||
|
console.log(`✅ User ID da Aurora: ${auroraUserId}`);
|
||||||
|
|
||||||
|
// Atualizar patient com user_id
|
||||||
|
console.log('\n📝 Atualizando registro da paciente...');
|
||||||
|
const updateResponse = await axios.patch(
|
||||||
|
`${SUPABASE_URL}/rest/v1/patients?id=eq.b85486f7-9135-4b67-9aa7-b884d9603d12`,
|
||||||
|
{
|
||||||
|
user_id: auroraUserId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${adminToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Prefer': 'return=representation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('✅ Registro atualizado com sucesso!');
|
||||||
|
console.log(` - Patient ID: b85486f7-9135-4b67-9aa7-b884d9603d12`);
|
||||||
|
console.log(` - User ID: ${auroraUserId}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fixAuroraUserId();
|
||||||
59
MEDICONNECT 2/get-aurora-info.cjs
Normal file
59
MEDICONNECT 2/get-aurora-info.cjs
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
|
||||||
|
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔐 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\n');
|
||||||
|
|
||||||
|
console.log('👤 Buscando dados de Aurora na tabela patients...');
|
||||||
|
|
||||||
|
const patientsResponse = await axios.get(
|
||||||
|
`${SUPABASE_URL}/rest/v1/patients?email=eq.aurora-nascimento94@gmx.com`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${adminToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (patientsResponse.data.length > 0) {
|
||||||
|
const patient = patientsResponse.data[0];
|
||||||
|
console.log('✅ Paciente encontrada!\n');
|
||||||
|
console.log('📋 DADOS DA AURORA:\n');
|
||||||
|
console.log('User ID (auth):', patient.id);
|
||||||
|
console.log('Patient ID:', patient.id); // Em patients, o id é o mesmo do auth
|
||||||
|
console.log('Nome:', patient.full_name);
|
||||||
|
console.log('Email:', patient.email);
|
||||||
|
console.log('CPF:', patient.cpf);
|
||||||
|
console.log('Telefone:', patient.phone_mobile);
|
||||||
|
console.log('\n📄 Dados completos:', JSON.stringify(patient, null, 2));
|
||||||
|
} else {
|
||||||
|
console.log('❌ Paciente não encontrada na tabela patients');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
76
MEDICONNECT 2/get-fernando-complete.cjs
Normal file
76
MEDICONNECT 2/get-fernando-complete.cjs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
|
||||||
|
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔐 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\n');
|
||||||
|
|
||||||
|
console.log('🔍 Buscando Fernando em profiles...');
|
||||||
|
|
||||||
|
const profilesResponse = await axios.get(
|
||||||
|
`${SUPABASE_URL}/rest/v1/profiles?email=eq.fernando.pirichowski@souunit.com.br`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${adminToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (profilesResponse.data.length > 0) {
|
||||||
|
console.log(`✅ ${profilesResponse.data.length} perfil(is) encontrado(s)!\n`);
|
||||||
|
|
||||||
|
profilesResponse.data.forEach((profile, index) => {
|
||||||
|
console.log(`📋 PERFIL ${index + 1}:\n`);
|
||||||
|
console.log('User ID:', profile.id);
|
||||||
|
console.log('Email:', profile.email);
|
||||||
|
console.log('Nome:', profile.full_name);
|
||||||
|
console.log('Telefone:', profile.phone || 'Não informado');
|
||||||
|
console.log('\n' + '='.repeat(60) + '\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pegar roles do primeiro perfil
|
||||||
|
const userId = profilesResponse.data[0].id;
|
||||||
|
console.log('🔍 Buscando roles...');
|
||||||
|
|
||||||
|
const rolesResponse = await axios.get(
|
||||||
|
`${SUPABASE_URL}/rest/v1/user_roles?user_id=eq.${userId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${adminToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('📌 Roles:', rolesResponse.data.map(r => r.role).join(', '));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.log('❌ Nenhum perfil encontrado');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
78
MEDICONNECT 2/get-fernando-doctor-id.cjs
Normal file
78
MEDICONNECT 2/get-fernando-doctor-id.cjs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
|
||||||
|
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log('🔐 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\n');
|
||||||
|
|
||||||
|
console.log('👨⚕️ Buscando dados de Fernando na tabela doctors...');
|
||||||
|
|
||||||
|
const doctorsResponse = await axios.get(
|
||||||
|
`${SUPABASE_URL}/rest/v1/doctors?email=eq.fernando.pirichowski@souunit.com.br`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${adminToken}`,
|
||||||
|
'Prefer': 'return=representation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (doctorsResponse.data.length > 0) {
|
||||||
|
console.log(`✅ ${doctorsResponse.data.length} médico(s) encontrado(s)!\n`);
|
||||||
|
|
||||||
|
doctorsResponse.data.forEach((doctor, index) => {
|
||||||
|
console.log(`📋 MÉDICO ${index + 1}:\n`);
|
||||||
|
console.log('Doctor ID:', doctor.id);
|
||||||
|
console.log('Nome:', doctor.full_name);
|
||||||
|
console.log('Email:', doctor.email);
|
||||||
|
console.log('CRM:', doctor.crm);
|
||||||
|
console.log('Especialidade:', doctor.specialty || 'Não informada');
|
||||||
|
console.log('Telefone:', doctor.phone || 'Não informado');
|
||||||
|
console.log('\n' + '='.repeat(60) + '\n');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('❌ Nenhum médico chamado Fernando encontrado');
|
||||||
|
console.log('\n🔍 Buscando todos os médicos...');
|
||||||
|
|
||||||
|
const allDoctorsResponse = await axios.get(
|
||||||
|
`${SUPABASE_URL}/rest/v1/doctors`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'apikey': SUPABASE_ANON_KEY,
|
||||||
|
'Authorization': `Bearer ${adminToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`\n📊 Total de médicos cadastrados: ${allDoctorsResponse.data.length}`);
|
||||||
|
allDoctorsResponse.data.forEach((doctor, index) => {
|
||||||
|
console.log(`${index + 1}. ${doctor.full_name} - ${doctor.email}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
38
MEDICONNECT 2/get-fernando-user-id.cjs
Normal file
38
MEDICONNECT 2/get-fernando-user-id.cjs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
const SUPABASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
|
||||||
|
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
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: 'fernando123'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'apikey': SUPABASE_ANON_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const userData = loginResponse.data;
|
||||||
|
console.log('✅ Login realizado\n');
|
||||||
|
|
||||||
|
console.log('📋 DADOS DO FERNANDO (AUTH):\n');
|
||||||
|
console.log('User ID:', userData.user.id);
|
||||||
|
console.log('Email:', userData.user.email);
|
||||||
|
console.log('Role:', userData.user.role);
|
||||||
|
console.log('\n' + '='.repeat(60));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erro:', error.response?.data || error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
54
MEDICONNECT 2/src/components/auth/RecoveryRedirect.tsx
Normal file
54
MEDICONNECT 2/src/components/auth/RecoveryRedirect.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Componente que detecta tokens de recuperação na URL e redireciona para /reset-password
|
||||||
|
* Funciona tanto com query string (?token=xxx) quanto com hash (#access_token=xxx)
|
||||||
|
*/
|
||||||
|
const RecoveryRedirect: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("[RecoveryRedirect] Verificando URL...");
|
||||||
|
console.log("[RecoveryRedirect] Pathname:", location.pathname);
|
||||||
|
console.log("[RecoveryRedirect] Search:", location.search);
|
||||||
|
console.log("[RecoveryRedirect] Hash:", location.hash);
|
||||||
|
|
||||||
|
let shouldRedirect = false;
|
||||||
|
|
||||||
|
// Verificar query string: ?token=xxx&type=recovery
|
||||||
|
const searchParams = new URLSearchParams(location.search);
|
||||||
|
const queryToken = searchParams.get("token");
|
||||||
|
const queryType = searchParams.get("type");
|
||||||
|
|
||||||
|
if (queryToken && queryType === "recovery") {
|
||||||
|
console.log("[RecoveryRedirect] ✅ Token de recovery no query string detectado");
|
||||||
|
shouldRedirect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar hash: #access_token=xxx&type=recovery
|
||||||
|
if (location.hash) {
|
||||||
|
const hashParams = new URLSearchParams(location.hash.substring(1));
|
||||||
|
const hashToken = hashParams.get("access_token");
|
||||||
|
const hashType = hashParams.get("type");
|
||||||
|
|
||||||
|
if (hashToken && hashType === "recovery") {
|
||||||
|
console.log("[RecoveryRedirect] ✅ Token de recovery no hash detectado");
|
||||||
|
shouldRedirect = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRedirect) {
|
||||||
|
console.log("[RecoveryRedirect] 🔄 Redirecionando para /reset-password");
|
||||||
|
// Preservar os parâmetros e redirecionar
|
||||||
|
navigate(`/reset-password${location.search}${location.hash}`, { replace: true });
|
||||||
|
} else {
|
||||||
|
console.log("[RecoveryRedirect] ℹ️ Nenhum token de recovery detectado");
|
||||||
|
}
|
||||||
|
}, [location, navigate]);
|
||||||
|
|
||||||
|
return null; // Componente invisível
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RecoveryRedirect;
|
||||||
@ -21,7 +21,32 @@ export default function AuthCallback() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleCallback = async () => {
|
const handleCallback = async () => {
|
||||||
try {
|
try {
|
||||||
console.log("[AuthCallback] Iniciando processamento do magic link");
|
console.log("[AuthCallback] Iniciando processamento");
|
||||||
|
|
||||||
|
// Verificar se é um token de recovery
|
||||||
|
const hash = window.location.hash;
|
||||||
|
const hashParams = new URLSearchParams(hash.substring(1));
|
||||||
|
const accessToken = hashParams.get("access_token");
|
||||||
|
const type = hashParams.get("type");
|
||||||
|
|
||||||
|
console.log("[AuthCallback] Hash:", hash);
|
||||||
|
console.log("[AuthCallback] Type:", type);
|
||||||
|
console.log("[AuthCallback] Access Token presente:", !!accessToken);
|
||||||
|
|
||||||
|
// Se for recovery, redirecionar para página de reset
|
||||||
|
if (type === "recovery" && accessToken) {
|
||||||
|
console.log("[AuthCallback] ✅ Token de recovery detectado, redirecionando para /reset-password");
|
||||||
|
setStatus("success");
|
||||||
|
setMessage("Redirecionando para página de redefinição de senha...");
|
||||||
|
|
||||||
|
// Redirecionar preservando o hash com o token
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate(`/reset-password${hash}`, { replace: true });
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[AuthCallback] Processando magic link normal");
|
||||||
|
|
||||||
// Supabase automaticamente processa os query params
|
// Supabase automaticamente processa os query params
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { MetricCard } from "../components/MetricCard";
|
|||||||
import { HeroBanner } from "../components/HeroBanner";
|
import { HeroBanner } from "../components/HeroBanner";
|
||||||
import { i18n } from "../i18n";
|
import { i18n } from "../i18n";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
|
import RecoveryRedirect from "../components/auth/RecoveryRedirect";
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
const [stats, setStats] = useState({
|
const [stats, setStats] = useState({
|
||||||
@ -97,6 +98,9 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8" id="main-content">
|
<div className="space-y-8" id="main-content">
|
||||||
|
{/* Componente invisível que detecta tokens de recuperação e redireciona */}
|
||||||
|
<RecoveryRedirect />
|
||||||
|
|
||||||
{/* Hero Section com Background Rotativo */}
|
{/* Hero Section com Background Rotativo */}
|
||||||
<HeroBanner />
|
<HeroBanner />
|
||||||
|
|
||||||
|
|||||||
@ -14,35 +14,48 @@ const ResetPassword: React.FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Extrair access_token do hash da URL
|
// Extrair access_token do hash da URL (#access_token=...)
|
||||||
|
// OU do query string (?token=...&type=recovery)
|
||||||
|
|
||||||
|
let token: string | null = null;
|
||||||
|
let type: string | null = null;
|
||||||
|
|
||||||
|
// Primeiro tenta no hash (formato padrão do Supabase após redirect)
|
||||||
const hash = window.location.hash;
|
const hash = window.location.hash;
|
||||||
console.log("[ResetPassword] Hash completo:", hash);
|
console.log("[ResetPassword] Hash completo:", hash);
|
||||||
|
|
||||||
if (hash) {
|
if (hash) {
|
||||||
const params = new URLSearchParams(hash.substring(1));
|
const hashParams = new URLSearchParams(hash.substring(1));
|
||||||
const token = params.get("access_token");
|
token = hashParams.get("access_token");
|
||||||
const type = params.get("type");
|
type = hashParams.get("type");
|
||||||
|
|
||||||
console.log(
|
console.log("[ResetPassword] Token do hash:", token ? token.substring(0, 20) + "..." : "null");
|
||||||
"[ResetPassword] Token extraído:",
|
console.log("[ResetPassword] Type do hash:", type);
|
||||||
token ? token.substring(0, 20) + "..." : "null"
|
}
|
||||||
);
|
|
||||||
console.log("[ResetPassword] Type:", type);
|
|
||||||
|
|
||||||
if (token) {
|
// Se não encontrou no hash, tenta no query string
|
||||||
setAccessToken(token);
|
if (!token) {
|
||||||
console.log(
|
const search = window.location.search;
|
||||||
"[ResetPassword] ✅ Token de recuperação detectado e armazenado"
|
console.log("[ResetPassword] Query string completo:", search);
|
||||||
);
|
|
||||||
} else {
|
if (search) {
|
||||||
console.error("[ResetPassword] ❌ Token não encontrado no hash");
|
const queryParams = new URLSearchParams(search);
|
||||||
toast.error("Link de recuperação inválido ou expirado");
|
token = queryParams.get("token");
|
||||||
setTimeout(() => navigate("/"), 3000);
|
type = queryParams.get("type");
|
||||||
|
|
||||||
|
console.log("[ResetPassword] Token do query:", token ? token.substring(0, 20) + "..." : "null");
|
||||||
|
console.log("[ResetPassword] Type do query:", type);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
setAccessToken(token);
|
||||||
|
console.log("[ResetPassword] ✅ Token de recuperação detectado e armazenado");
|
||||||
|
console.log("[ResetPassword] Type:", type);
|
||||||
} else {
|
} else {
|
||||||
console.error("[ResetPassword] ❌ Nenhum hash encontrado na URL");
|
console.error("[ResetPassword] ❌ Token não encontrado no hash nem no query string");
|
||||||
console.log("[ResetPassword] URL completa:", window.location.href);
|
console.log("[ResetPassword] URL completa:", window.location.href);
|
||||||
toast.error("Link de recuperação inválido");
|
toast.error("Link de recuperação inválido ou expirado");
|
||||||
setTimeout(() => navigate("/"), 3000);
|
setTimeout(() => navigate("/"), 3000);
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
@ -93,10 +106,26 @@ const ResetPassword: React.FC = () => {
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error("[ResetPassword] Erro ao atualizar senha:", error);
|
console.error("[ResetPassword] Erro ao atualizar senha:", error);
|
||||||
const err = error as {
|
const err = error as {
|
||||||
response?: { data?: { error_description?: string; message?: string } };
|
response?: {
|
||||||
|
data?: {
|
||||||
|
error_description?: string;
|
||||||
|
message?: string;
|
||||||
|
msg?: string;
|
||||||
|
error_code?: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mensagem específica para senha igual
|
||||||
|
if (err?.response?.data?.error_code === "same_password") {
|
||||||
|
toast.error("A nova senha deve ser diferente da senha atual");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
|
err?.response?.data?.msg ||
|
||||||
err?.response?.data?.error_description ||
|
err?.response?.data?.error_description ||
|
||||||
err?.response?.data?.message ||
|
err?.response?.data?.message ||
|
||||||
err?.message ||
|
err?.message ||
|
||||||
|
|||||||
@ -154,20 +154,31 @@ class AuthService {
|
|||||||
/**
|
/**
|
||||||
* Solicita reset de senha via email (público)
|
* Solicita reset de senha via email (público)
|
||||||
* POST /auth/v1/recover
|
* POST /auth/v1/recover
|
||||||
|
*
|
||||||
|
* Envia redirectTo dentro de options para controlar para onde o Supabase redireciona
|
||||||
*/
|
*/
|
||||||
async requestPasswordReset(
|
async requestPasswordReset(
|
||||||
email: string,
|
email: string
|
||||||
redirectUrl?: string
|
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
|
const redirectUrl = `${API_CONFIG.APP_URL}/reset-password`;
|
||||||
|
|
||||||
|
console.log("[authService.requestPasswordReset] Email:", email);
|
||||||
|
console.log("[authService.requestPasswordReset] Redirect URL:", redirectUrl);
|
||||||
|
console.log("[authService.requestPasswordReset] Usando endpoint /auth/v1/recover");
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
email,
|
||||||
|
options: {
|
||||||
|
redirectTo: redirectUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[authService.requestPasswordReset] Payload:", JSON.stringify(payload, null, 2));
|
||||||
|
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${API_CONFIG.AUTH_URL}/recover`,
|
`${API_CONFIG.AUTH_URL}/recover`,
|
||||||
{
|
payload,
|
||||||
email,
|
|
||||||
options: {
|
|
||||||
redirectTo: redirectUrl || `${API_CONFIG.APP_URL}/reset-password`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -176,13 +187,15 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("[authService.requestPasswordReset] ✅ Email de recuperação enviado");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message:
|
message: "Email de recuperação de senha enviado com sucesso. Verifique sua caixa de entrada.",
|
||||||
"Email de recuperação de senha enviado com sucesso. Verifique sua caixa de entrada.",
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Erro ao solicitar reset de senha:", error);
|
console.error("[authService.requestPasswordReset] ❌ Erro:", error);
|
||||||
|
console.error("[authService.requestPasswordReset] Response:", error.response?.data);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,7 +209,12 @@ class AuthService {
|
|||||||
newPassword: string
|
newPassword: string
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
await axios.put(
|
console.log("[authService.updatePassword] Iniciando atualização de senha");
|
||||||
|
console.log("[authService.updatePassword] Token (primeiros 30 chars):", accessToken.substring(0, 30));
|
||||||
|
console.log("[authService.updatePassword] Nova senha length:", newPassword.length);
|
||||||
|
console.log("[authService.updatePassword] URL:", `${API_CONFIG.AUTH_URL}/user`);
|
||||||
|
|
||||||
|
const response = await axios.put(
|
||||||
`${API_CONFIG.AUTH_URL}/user`,
|
`${API_CONFIG.AUTH_URL}/user`,
|
||||||
{
|
{
|
||||||
password: newPassword,
|
password: newPassword,
|
||||||
@ -210,12 +228,35 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log("[authService.updatePassword] ✅ Senha atualizada com sucesso");
|
||||||
|
console.log("[authService.updatePassword] Response status:", response.status);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: "Senha atualizada com sucesso",
|
message: "Senha atualizada com sucesso",
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error("Erro ao atualizar senha:", error);
|
console.error("[authService.updatePassword] ❌ Erro ao atualizar senha:", error);
|
||||||
|
console.error("[authService.updatePassword] Error response:", error.response?.data);
|
||||||
|
console.error("[authService.updatePassword] Error response (stringified):", JSON.stringify(error.response?.data, null, 2));
|
||||||
|
console.error("[authService.updatePassword] Error status:", error.response?.status);
|
||||||
|
console.error("[authService.updatePassword] Error statusText:", error.response?.statusText);
|
||||||
|
console.error("[authService.updatePassword] Error headers:", error.response?.headers);
|
||||||
|
console.error("[authService.updatePassword] Request URL:", error.config?.url);
|
||||||
|
console.error("[authService.updatePassword] Request headers:", error.config?.headers);
|
||||||
|
console.error("[authService.updatePassword] Request data:", error.config?.data);
|
||||||
|
|
||||||
|
// Se for 422, mostrar mensagem específica
|
||||||
|
if (error.response?.status === 422) {
|
||||||
|
const errorMsg = error.response?.data?.msg || error.response?.data?.message || error.response?.data?.error_description || "Token inválido ou expirado";
|
||||||
|
console.error("[authService.updatePassword] ⚠️ Erro 422 - Possíveis causas:");
|
||||||
|
console.error(" 1. Token de recuperação expirado (válido por 1 hora)");
|
||||||
|
console.error(" 2. Token já foi usado");
|
||||||
|
console.error(" 3. Formato do token incorreto");
|
||||||
|
console.error(" 4. Usuário não existe");
|
||||||
|
console.error("[authService.updatePassword] Mensagem do servidor:", errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user