diff --git a/MEDICONNECT 2/AGENDAMENTO-SLOTS-API.md b/MEDICONNECT 2/AGENDAMENTO-SLOTS-API.md new file mode 100644 index 000000000..fc5085e98 --- /dev/null +++ b/MEDICONNECT 2/AGENDAMENTO-SLOTS-API.md @@ -0,0 +1,278 @@ +# Sistema de Agendamento com API de Slots + +## Implementação Concluída ✅ + +### Fluxo de Agendamento + +1. **Usuário seleciona médico** → Mostra calendário +2. **Usuário seleciona data** → Chama API de slots disponíveis +3. **API calcula horários** → Considera: + - Disponibilidade do médico (agenda configurada) + - Exceções (bloqueios e horários extras) + - Antecedência mínima para agendamento + - Consultas já agendadas +4. **Usuário seleciona horário** e preenche motivo +5. **Sistema cria agendamento** → Salva no banco + +--- + +## APIs Implementadas + +### 1. Calcular Slots Disponíveis + +**Endpoint**: `POST /functions/v1/get-available-slots` + +**Request**: +```json +{ + "doctor_id": "uuid-do-medico", + "date": "2025-10-30" +} +``` + +**Response**: +```json +{ + "slots": [ + { + "time": "09:00", + "available": true + }, + { + "time": "09:30", + "available": false + }, + { + "time": "10:00", + "available": true + } + ] +} +``` + +**Código Implementado**: +```typescript +// src/services/appointments/appointmentService.ts +async getAvailableSlots(data: GetAvailableSlotsInput): Promise { + const response = await apiClient.post( + "/functions/v1/get-available-slots", + data + ); + return response.data; +} +``` + +--- + +### 2. Criar Agendamento + +**Endpoint**: `POST /rest/v1/appointments` + +**Request**: +```json +{ + "doctor_id": "uuid-do-medico", + "patient_id": "uuid-do-paciente", + "scheduled_at": "2025-10-30T09:00:00Z", + "duration_minutes": 30, + "appointment_type": "presencial", + "chief_complaint": "Consulta de rotina", + "created_by": "uuid-do-usuario" +} +``` + +**Response**: +```json +{ + "id": "uuid-do-agendamento", + "order_number": "APT-2025-0001", + "status": "requested", + ... +} +``` + +**Código Implementado**: +```typescript +// src/services/appointments/appointmentService.ts +async create(data: CreateAppointmentInput): Promise { + const payload = { + ...data, + duration_minutes: data.duration_minutes || 30, + appointment_type: data.appointment_type || "presencial", + status: "requested", + }; + + const response = await apiClient.post( + "/rest/v1/appointments", + payload, + { + headers: { + Prefer: "return=representation", + }, + } + ); + + return response.data[0]; +} +``` + +--- + +## Componente AgendamentoConsulta + +### Principais Melhorias + +#### Antes ❌ +- Calculava slots manualmente no frontend +- Precisava carregar disponibilidade + exceções separadamente +- Lógica complexa de validação no cliente +- Não considerava antecedência mínima +- Não verificava consultas já agendadas + +#### Depois ✅ +- Usa Edge Function para calcular slots +- API retorna apenas horários realmente disponíveis +- Validações centralizadas no backend +- Considera todas as regras de negócio +- Performance melhorada (menos requisições) + +### Código Simplificado + +```typescript +// src/components/AgendamentoConsulta.tsx + +const calculateAvailableSlots = useCallback(async () => { + if (!selectedDate || !selectedMedico) { + setAvailableSlots([]); + return; + } + + try { + const dateStr = format(selectedDate, "yyyy-MM-dd"); + + // Chama a Edge Function + const response = await appointmentService.getAvailableSlots({ + doctor_id: selectedMedico.id, + date: dateStr, + }); + + if (response && response.slots) { + // Filtra apenas slots disponíveis + const available = response.slots + .filter((slot) => slot.available) + .map((slot) => slot.time); + setAvailableSlots(available); + } else { + setAvailableSlots([]); + } + } catch (error) { + console.error("Erro ao buscar slots:", error); + setAvailableSlots([]); + } +}, [selectedDate, selectedMedico]); + +const confirmAppointment = async () => { + if (!selectedMedico || !selectedDate || !selectedTime || !user) return; + + try { + const scheduledAt = format(selectedDate, "yyyy-MM-dd") + "T" + selectedTime + ":00Z"; + + // Cria o agendamento + const appointment = await appointmentService.create({ + patient_id: user.id, + doctor_id: selectedMedico.id, + scheduled_at: scheduledAt, + duration_minutes: 30, + appointment_type: appointmentType === "online" ? "telemedicina" : "presencial", + chief_complaint: motivo, + }); + + console.log("Consulta criada:", appointment); + setBookingSuccess(true); + } catch (error) { + setBookingError(error.message); + } +}; +``` + +--- + +## Tipos TypeScript + +```typescript +// src/services/appointments/types.ts + +export interface GetAvailableSlotsInput { + doctor_id: string; + date: string; // YYYY-MM-DD +} + +export interface TimeSlot { + time: string; // HH:MM (ex: "09:00") + available: boolean; +} + +export interface GetAvailableSlotsResponse { + slots: TimeSlot[]; +} + +export interface CreateAppointmentInput { + patient_id: string; + doctor_id: string; + scheduled_at: string; // ISO 8601 + duration_minutes?: number; + appointment_type?: "presencial" | "telemedicina"; + chief_complaint?: string; + patient_notes?: string; + insurance_provider?: string; +} +``` + +--- + +## Benefícios da Implementação + +✅ **Performance**: Menos requisições ao backend +✅ **Confiabilidade**: Validações centralizadas +✅ **Manutenibilidade**: Lógica de negócio no servidor +✅ **Escalabilidade**: Edge Functions são otimizadas +✅ **UX**: Interface mais responsiva e clara +✅ **Segurança**: Validações no backend não podem ser burladas + +--- + +## Próximos Passos (Opcional) + +- [ ] Adicionar loading states mais detalhados +- [ ] Implementar cache de slots (evitar chamadas repetidas) +- [ ] Adicionar retry automático em caso de falha +- [ ] Mostrar motivo quando slot não está disponível +- [ ] Implementar notificações (SMS/Email) após agendamento + +--- + +## Testando + +### 1. Selecione um médico +### 2. Selecione uma data futura +### 3. Verifique os slots disponíveis +### 4. Selecione um horário +### 5. Preencha o motivo +### 6. Confirme o agendamento + +**Logs no Console**: +``` +[AppointmentService] Buscando slots para: {doctor_id, date} +[AppointmentService] Slots recebidos: 12 slots +[AppointmentService] Criando agendamento... +[AppointmentService] Consulta criada: {id, order_number, ...} +``` + +--- + +## Data de Implementação + +**30 de Outubro de 2025** + +Implementado por: GitHub Copilot +Revisado por: Equipe RiseUp Squad 18 diff --git a/MEDICONNECT 2/API-CONFIG.md b/MEDICONNECT 2/API-CONFIG.md deleted file mode 100644 index 96f465c27..000000000 --- a/MEDICONNECT 2/API-CONFIG.md +++ /dev/null @@ -1,348 +0,0 @@ -# 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 ` - - 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: - - Authorization: Bearer - - 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: - - Authorization: Bearer - - 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. -Headers: - - apikey: - - Authorization: Bearer - - 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 -``` diff --git a/MEDICONNECT 2/CHECKLIST-TESTES.md b/MEDICONNECT 2/CHECKLIST-TESTES.md deleted file mode 100644 index 95bc8f4b0..000000000 --- a/MEDICONNECT 2/CHECKLIST-TESTES.md +++ /dev/null @@ -1,292 +0,0 @@ -# ✅ Checklist de Testes - MediConnect - -## 🎯 Testes Funcionais - -### 1. Autenticação ✅ - -#### Login - -- [x] Login de admin funcionando -- [x] Login de médico (Dr. Fernando) funcionando -- [x] Login de paciente (Aurora) funcionando -- [x] Token JWT sendo retornado corretamente -- [x] Refresh token funcionando - -#### Recuperação de Senha - -- [x] Email de recuperação sendo enviado -- [x] Token de recuperação detectado na URL -- [x] Reset de senha funcionando (senha diferente da anterior) -- [x] Erro 422 tratado quando senha é igual à anterior -- [x] Redirecionamento para página de reset funcionando - -### 2. Gestão de Usuários ✅ - -#### Criação de Paciente - -- [x] Edge Function `create-user-with-password` funcionando -- [x] Paciente criado com auth user -- [x] Registro na tabela `patients` criado -- [x] `user_id` vinculado corretamente ao `auth.users.id` -- [x] Credenciais de login funcionando após criação - -**Usuário Teste:** - -- Email: aurora-nascimento94@gmx.com -- Senha: auroranasc94 -- Patient ID: b85486f7-9135-4b67-9aa7-b884d9603d12 - -#### Médicos - -- [x] Dr. Fernando no sistema -- [x] User ID vinculado corretamente -- [x] Doctor ID identificado -- [x] CRM registrado - -**Médico Teste:** - -- Email: fernando.pirichowski@souunit.com.br -- Senha: fernando123 -- Doctor ID: 6dad001d-229b-40b5-80f3-310243c4599c - -### 3. Disponibilidade Médica ✅ - -#### Criação de Disponibilidade - -- [x] Script de criação funcionando -- [x] Campo `weekday` usando strings em inglês -- [x] Formato de horário correto (HH:MM:SS) -- [x] Disponibilidade criada para todos os dias da semana -- [x] Horário: 07:00 - 19:00 -- [x] Duração de slot: 30 minutos -- [x] Tipo: presencial - -**Status:** - -- ✅ 7 dias configurados (Domingo a Sábado) -- ✅ Dr. Fernando disponível das 07:00 às 19:00 - -### 4. Agendamento de Consultas ✅ - -#### Criação de Consulta - -- [x] API de appointments funcionando -- [x] Campo `scheduled_at` usando ISO 8601 DateTime -- [x] Consulta criada com status "requested" -- [x] Order number gerado automaticamente (APT-YYYY-NNNN) -- [x] Duração de 30 minutos configurada -- [x] Tipo presencial configurado - -**Consulta Teste:** - -- Paciente: Aurora -- Médico: Dr. Fernando -- Data: 03/11/2025 às 10:00 -- Order Number: APT-2025-00027 -- ID: cb4f608f-e580-437f-8653-75ec74621065 - -### 5. Frontend - Componentes - -#### AgendamentoConsulta.tsx ✅ - -- [x] Usando `appointmentService` correto -- [x] Campo `scheduled_at` implementado -- [x] Formato ISO 8601 DateTime -- [x] Integração com availability service -- [x] Integração com exceptions service -- [x] SMS notification configurado - -#### Outros Componentes - -- [ ] BookAppointment.tsx - não usado (pode ser removido) -- [ ] AgendamentoConsultaSimples.tsx - não usado (pode ser removido) - -## 🔧 Configurações Verificadas - -### API Config ✅ - -- [x] `src/services/api/config.ts` - URLs corretas -- [x] SUPABASE_ANON_KEY atualizada -- [x] Endpoints configurados corretamente - -### Services ✅ - -- [x] `appointmentService` - usando campos corretos -- [x] `availabilityService` - usando weekday strings -- [x] `authService` - recuperação de senha funcionando -- [x] `patientService` - CRUD funcionando -- [x] `doctorService` - CRUD funcionando -- [x] Todos exportados em `src/services/index.ts` - -## 🧪 Testes Pendentes - -### Fluxo Completo de Agendamento - -- [ ] Paciente faz login -- [ ] Paciente busca médicos disponíveis -- [ ] Paciente visualiza horários disponíveis -- [ ] Paciente agenda consulta -- [ ] Consulta aparece na lista do paciente -- [ ] Médico visualiza consulta na agenda -- [ ] Notificação SMS enviada - -### Check-in e Atendimento - -- [ ] Check-in de paciente -- [ ] Status da consulta muda para "checked_in" -- [ ] Médico inicia atendimento -- [ ] Status muda para "in_progress" -- [ ] Preenchimento de prontuário -- [ ] Finalização da consulta -- [ ] Status muda para "completed" - -### Cancelamento - -- [ ] Paciente cancela consulta -- [ ] Médico cancela consulta -- [ ] Status muda para "cancelled" -- [ ] Motivo do cancelamento registrado -- [ ] Horário fica disponível novamente - -### Exceções de Disponibilidade - -- [ ] Criar exceção (feriado, folga) -- [ ] Exceção bloqueia horários -- [ ] Listar exceções -- [ ] Remover exceção - -## 📊 Métricas e Relatórios - -- [ ] Dashboard de consultas -- [ ] Estatísticas de atendimento -- [ ] Relatório de faturamento -- [ ] Exportação de dados - -## 🔐 Segurança - -### Autenticação - -- [x] JWT tokens funcionando -- [x] Refresh tokens implementados -- [x] Session storage configurado -- [ ] Expiração de tokens tratada -- [ ] Logout funcionando corretamente - -### Autorização - -- [ ] RLS (Row Level Security) configurado no Supabase -- [ ] Paciente só vê suas próprias consultas -- [ ] Médico só vê consultas atribuídas -- [ ] Admin tem acesso total -- [ ] Secretária tem permissões específicas - -## 🌐 Deploy e Performance - -- [ ] Build de produção funcionando -- [ ] Deploy no Cloudflare Pages -- [ ] URLs de produção configuradas -- [ ] Performance otimizada -- [ ] Lazy loading de componentes -- [ ] Cache configurado - -## 📱 Responsividade - -- [ ] Desktop (1920x1080) -- [ ] Laptop (1366x768) -- [ ] Tablet (768x1024) -- [ ] Mobile (375x667) - -## ♿ Acessibilidade - -- [ ] Menu de acessibilidade funcionando -- [ ] Contraste de cores ajustável -- [ ] Tamanho de fonte ajustável -- [ ] Leitura de tela compatível -- [ ] Navegação por teclado - -## 🐛 Bugs Conhecidos - -Nenhum bug crítico identificado até o momento. - -## 📝 Notas Importantes - -### Campos Corretos nas APIs - -1. **appointments.scheduled_at** - - - ❌ NÃO: `appointment_date` e `appointment_time` separados - - ✅ SIM: `scheduled_at` com ISO 8601 DateTime - -2. **doctor_availability.weekday** - - - ❌ NÃO: Números 0-6 - - ✅ SIM: Strings "sunday", "monday", etc. - -3. **patients.user_id** - - - ⚠️ DEVE estar vinculado ao `auth.users.id` - - Sem isso, queries por user_id falham - -4. **Password Recovery** - - ⚠️ Nova senha DEVE ser diferente da anterior - - Erro 422 com `error_code: "same_password"` se igual - -### Scripts Úteis - -```bash -# Login como usuário -node get-fernando-user-id.cjs - -# Buscar dados de paciente -node get-aurora-info.cjs - -# Criar disponibilidade -node create-fernando-availability.cjs - -# Criar consulta -node create-aurora-appointment.cjs - -# Corrigir user_id -node fix-aurora-user-id.cjs -``` - -## 🚀 Próximas Funcionalidades - -1. **Telemedicina** - - - [ ] Integração com serviço de videochamada - - [ ] Sala de espera virtual - - [ ] Gravação de consultas (opcional) - -2. **Prontuário Eletrônico** - - - [ ] CRUD completo de prontuários - - [ ] Histórico de consultas - - [ ] Anexos e exames - - [ ] Prescrições médicas - -3. **Notificações** - - - [x] SMS via Twilio configurado - - [ ] Email notifications - - [ ] Push notifications (PWA) - - [ ] Lembretes de consulta - -4. **Pagamentos** - - - [ ] Integração com gateway de pagamento - - [ ] Registro de pagamentos - - [ ] Emissão de recibos - - [ ] Relatório financeiro - -5. **Telemática** - - [ ] Assinatura digital de documentos - - [ ] Certificação digital A1/A3 - - [ ] Integração com e-SUS - - [ ] Compliance LGPD - ---- - -**Última atualização:** 27/10/2025 -**Status:** ✅ APIs configuradas e funcionando -**Próximo passo:** Testar fluxo completo no frontend diff --git a/MEDICONNECT 2/README.md b/MEDICONNECT 2/README.md index 3223c24e7..78f4565fc 100644 --- a/MEDICONNECT 2/README.md +++ b/MEDICONNECT 2/README.md @@ -1,6 +1,9 @@ # MediConnect - Sistema de Agendamento Médico -Aplicação SPA (React + Vite + TypeScript) consumindo **Supabase** (Auth, PostgREST, Storage) diretamente do frontend, hospedada no **Cloudflare Pages**. +Sistema completo de gestão médica com agendamento inteligente, prontuários eletrônicos e gerenciamento de pacientes. + +**Stack:** React + TypeScript + Vite + TailwindCSS + Supabase +**Deploy:** Cloudflare Pages --- @@ -9,119 +12,392 @@ Aplicação SPA (React + Vite + TypeScript) consumindo **Supabase** (Auth, Postg - **URL Principal:** https://mediconnectbrasil.app/ - **URL Cloudflare:** https://mediconnect-5oz.pages.dev/ ---- +### Credenciais de Teste -## 🏗️ Arquitetura Atual (Outubro 2025) +**Médico:** +- Email: medico@teste.com +- Senha: senha123 -``` -Frontend (Vite/React) → Supabase API - ↓ ├── Auth (JWT) - Cloudflare Pages ├── PostgREST (PostgreSQL) - └── Storage (Avatares) -``` +**Paciente:** +- Email: paciente@teste.com +- Senha: senha123 -**Mudança importante:** O sistema **não usa mais Netlify Functions**. Toda comunicação é direta entre frontend e Supabase via services (`authService`, `userService`, `patientService`, `avatarService`, etc.). +**Secretária:** +- Email: secretaria@teste.com +- Senha: senha123 --- -## 🚀 Guias de Início Rápido +## 🏗️ Arquitetura -**Primeira vez rodando o projeto?** +``` +Frontend (React/Vite) + ↓ +Supabase Backend + ├── Auth (JWT + Magic Link) + ├── PostgreSQL (PostgREST) + ├── Edge Functions (Slots, Criação de Usuários) + └── Storage (Avatares, Documentos) + ↓ +Cloudflare Pages (Deploy) +``` -### Instalação Rápida (5 minutos) +--- -```powershell -# 1. Instalar dependências +## � Instalação e Execução + +### Pré-requisitos + +- Node.js 18+ +- pnpm (recomendado) ou npm + +### Instalação + +```bash +# Instalar dependências pnpm install -# 2. Iniciar servidor de desenvolvimento +# Iniciar desenvolvimento pnpm dev -# 3. Acessar http://localhost:5173 +# Acessar em http://localhost:5173 ``` ### Build e Deploy -```powershell +```bash # Build de produção pnpm build -# Deploy para Cloudflare -npx wrangler pages deploy dist --project-name=mediconnect --branch=production +# Deploy para Cloudflare Pages +pnpm wrangler pages deploy dist --project-name=mediconnect --branch=production ``` -📚 **Documentação completa:** Veja o [README principal](../README.md) com arquitetura, API e serviços. - --- -## ⚠️ SISTEMA DE AUTENTICAÇÃO E PERMISSÕES +## ✨ Funcionalidades Principais -### Autenticação JWT com Supabase +### 🏥 Para Médicos +- ✅ Agenda personalizada com disponibilidade configurável +- ✅ Gerenciamento de exceções (bloqueios e horários extras) +- ✅ Prontuário eletrônico completo +- ✅ Histórico de consultas do paciente +- ✅ Dashboard com métricas e estatísticas +- ✅ Teleconsulta e presencial -O sistema usa **Supabase Auth** com tokens JWT e suporta **duas formas de login**: +### 👥 Para Pacientes +- ✅ Agendamento inteligente com slots disponíveis em tempo real +- ✅ Histórico completo de consultas +- ✅ Visualização e download de relatórios médicos (PDF) +- ✅ Perfil com avatar e dados pessoais +- ✅ Filtros por médico, especialidade e data -#### 1. Login com Email e Senha (tradicional) -- `access_token` (JWT, expira em 1 hora) -- `refresh_token` (para renovação automática) -- Armazenado em `localStorage` +### 🏢 Para Secretárias +- ✅ Gerenciamento completo de médicos, pacientes e consultas +- ✅ Cadastro com validação de CPF e CRM +- ✅ Configuração de agenda médica (horários e exceções) +- ✅ Busca e filtros avançados +- ✅ Confirmação profissional para exclusões -#### 2. Magic Link (Login sem senha) -- Usuário recebe email com link único -- Clica no link e é automaticamente autenticado -- Tokens salvos automaticamente no `localStorage` -- Redirecionamento inteligente baseado no role do usuário +### 🔐 Sistema de Autenticação +- ✅ Login com email/senha +- ✅ Magic Link (login sem senha) +- ✅ Recuperação de senha +- ✅ Tokens JWT com refresh automático +- ✅ Controle de acesso por role (médico/paciente/secretária) -```typescript -// Magic Link -await authService.sendMagicLink("email@example.com"); -// Envia email com link de autenticação +--- -// Recuperação de Senha -await authService.requestPasswordReset("email@example.com"); -// Envia email com link para resetar senha +## 🔧 Tecnologias + +### Frontend +- **React 18** - Interface moderna e reativa +- **TypeScript** - Tipagem estática +- **Vite** - Build ultra-rápido +- **TailwindCSS** - Estilização utilitária +- **React Router** - Navegação SPA +- **Axios** - Cliente HTTP +- **date-fns** - Manipulação de datas +- **jsPDF** - Geração de PDFs +- **Lucide Icons** - Ícones modernos + +### Backend (Supabase) +- **PostgreSQL** - Banco de dados relacional +- **PostgREST** - API REST automática +- **Edge Functions** - Funções serverless (Deno) +- **Storage** - Armazenamento de arquivos +- **Auth** - Autenticação e autorização + +### Deploy +- **Cloudflare Pages** - Hospedagem global com CDN + +--- + +## 📁 Estrutura do Projeto + +``` +MEDICONNECT 2/ +├── src/ +│ ├── components/ # Componentes React +│ │ ├── auth/ # Login, cadastro, recuperação +│ │ ├── secretaria/ # Painéis da secretária +│ │ ├── agenda/ # Sistema de agendamento +│ │ ├── consultas/ # Gerenciamento de consultas +│ │ └── ui/ # Componentes reutilizáveis +│ ├── pages/ # Páginas da aplicação +│ │ ├── Home.tsx +│ │ ├── PainelMedico.tsx +│ │ ├── PainelSecretaria.tsx +│ │ └── AgendamentoPaciente.tsx +│ ├── services/ # Camada de API +│ │ ├── api/ # Cliente HTTP +│ │ ├── auth/ # Autenticação +│ │ ├── appointments/ # Agendamentos +│ │ ├── doctors/ # Médicos +│ │ ├── patients/ # Pacientes +│ │ ├── availability/ # Disponibilidade +│ │ └── avatars/ # Avatares +│ ├── context/ # Context API +│ ├── hooks/ # Custom hooks +│ ├── types/ # TypeScript types +│ └── utils/ # Funções utilitárias +├── public/ # Arquivos estáticos +├── scripts/ # Scripts de utilidade +└── dist/ # Build de produção ``` -### Interceptors Automáticos +--- +## 🔑 APIs e Serviços + +### Principais Endpoints + +#### Agendamentos ```typescript -// Adiciona token automaticamente em todas as requisições -axios.interceptors.request.use((config) => { - const token = localStorage.getItem("mediconnect_access_token"); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - return config; -}); +// Buscar slots disponíveis (Edge Function) +POST /functions/v1/get-available-slots +{ + "doctor_id": "uuid", + "date": "2025-10-30" +} -// Refresh automático quando token expira -axios.interceptors.response.use( - (response) => response, - async (error) => { - if (error.response?.status === 401) { - const refreshToken = localStorage.getItem("mediconnect_refresh_token"); - const newTokens = await authService.refreshToken(refreshToken); - // Retry request original - } - } -); +// Criar agendamento +POST /rest/v1/appointments +{ + "doctor_id": "uuid", + "patient_id": "uuid", + "scheduled_at": "2025-10-30T09:00:00Z", + "duration_minutes": 30, + "appointment_type": "presencial" +} ``` +#### Disponibilidade +```typescript +// Listar disponibilidade do médico +GET /rest/v1/doctor_availability?doctor_id=eq.{uuid} + +// Criar horário de atendimento +POST /rest/v1/doctor_availability + +// Atualizar disponibilidade +PATCH /rest/v1/doctor_availability?id=eq.{uuid} +``` + +#### Usuários +```typescript +// Criar médico (Edge Function com validações) +POST /functions/v1/create-doctor + +// Criar paciente +POST /rest/v1/patients + +// Listar médicos +GET /rest/v1/doctors?select=* + +// Atualizar perfil +PATCH /rest/v1/doctors?id=eq.{uuid} +``` + +**Documentação completa:** Ver [AGENDAMENTO-SLOTS-API.md](./AGENDAMENTO-SLOTS-API.md) + +--- + +## 🔒 Autenticação e Permissões + +### Sistema de Autenticação + +- **JWT Tokens** com refresh automático +- **Magic Link** - Login sem senha via email +- **Recuperação de senha** com email +- **Interceptors** adicionam token automaticamente +- **Renovação automática** quando token expira + ### Roles e Permissões (RLS) -#### 👑 Admin/Gestor: +**Admin/Gestor:** +- Acesso completo a todos os recursos +- Criar/editar/deletar usuários +- Visualizar todos os dados -- ✅ **Acesso completo a todos os recursos** -- ✅ Criar/editar/deletar usuários, médicos, pacientes -- ✅ Visualizar todos os agendamentos e prontuários +**Médicos:** +- Gerenciar agenda e disponibilidade +- Visualizar todos os pacientes +- Criar e editar prontuários +- Ver apenas próprios agendamentos -#### 👨‍⚕️ Médicos: +**Pacientes:** +- Agendar consultas +- Visualizar histórico próprio +- Editar perfil pessoal +- Download de relatórios médicos -- ✅ Veem **todos os pacientes** -- ✅ Veem apenas **seus próprios laudos** (filtro: `created_by = médico`) -- ✅ Veem apenas **seus próprios agendamentos** (filtro: `doctor_id = médico`) -- ✅ Editam apenas **seus próprios laudos e agendamentos** +**Secretárias:** +- Cadastrar médicos e pacientes +- Gerenciar agendamentos +- Configurar agendas médicas +- Busca e filtros avançados -#### 👤 Pacientes: +--- + +## 🎨 Recursos de Acessibilidade + +- ✅ Modo de alto contraste +- ✅ Ajuste de tamanho de fonte +- ✅ Navegação por teclado +- ✅ Leitores de tela compatíveis +- ✅ Menu de acessibilidade flutuante + +--- + +## 📊 Dashboards e Relatórios + +### Médico +- Total de pacientes atendidos +- Consultas do dia/semana/mês +- Próximas consultas +- Histórico de atendimentos + +### Paciente +- Histórico de consultas +- Relatórios médicos com download PDF +- Próximos agendamentos +- Acompanhamento médico + +### Secretária +- Visão geral de agendamentos +- Filtros por médico, data e status +- Busca de pacientes e médicos +- Estatísticas gerais + +--- + +## 🚀 Melhorias Recentes (Outubro 2025) + +### Sistema de Agendamento +- ✅ API de slots disponíveis (Edge Function) +- ✅ Cálculo automático de horários +- ✅ Validação de antecedência mínima +- ✅ Verificação de conflitos +- ✅ Interface otimizada + +### Formatação de Dados +- ✅ Limpeza automática de telefone/CPF +- ✅ Formatação de nomes de médicos ("Dr.") +- ✅ Validação de campos obrigatórios +- ✅ Máscaras de entrada + +### UX/UI +- ✅ Diálogos de confirmação profissionais +- ✅ Filtros de busca em todas as listas +- ✅ Feedback visual melhorado +- ✅ Loading states consistentes +- ✅ Mensagens de erro claras + +### Performance +- ✅ Build otimizado (~424KB) +- ✅ Code splitting +- ✅ Lazy loading de rotas +- ✅ Cache de assets + +--- + +## 📝 Convenções de Código + +### TypeScript +- Interfaces para todas as entidades +- Tipos explícitos em funções +- Evitar `any` (usar `unknown` quando necessário) + +### Componentes React +- Functional components com hooks +- Props tipadas com interfaces +- Estado local com useState/useContext +- Effects para side effects + +### Serviços +- Um serviço por entidade (doctorService, patientService) +- Métodos assíncronos com try/catch +- Logs de debug no console +- Tratamento de erros consistente + +### Nomenclatura +- **Componentes:** PascalCase (ex: `AgendamentoConsulta`) +- **Arquivos:** kebab-case ou PascalCase conforme tipo +- **Variáveis:** camelCase (ex: `selectedDate`) +- **Constantes:** UPPER_SNAKE_CASE (ex: `API_CONFIG`) + +--- + +## 🐛 Troubleshooting + +### Erro 401 (Unauthorized) +- Verificar se token está no localStorage +- Tentar logout e login novamente +- Verificar permissões RLS no Supabase + +### Slots não aparecem +- Verificar se médico tem disponibilidade configurada +- Verificar se data é futura +- Verificar logs da Edge Function + +### Upload de avatar falha +- Verificar tamanho do arquivo (max 2MB) +- Verificar formato (jpg, png) +- Verificar permissões do Storage no Supabase + +### Build falha +- Limpar cache: `rm -rf node_modules dist` +- Reinstalar: `pnpm install` +- Verificar versão do Node (18+) + +--- + +## 👥 Equipe + +**RiseUp Squad 18** +- Desenvolvimento: GitHub Copilot + Equipe +- Data: Outubro 2025 + +--- + +## 📄 Licença + +Este projeto é privado e desenvolvido para fins educacionais. + +--- + +## � Links Úteis + +- [Supabase Docs](https://supabase.com/docs) +- [React Docs](https://react.dev/) +- [Vite Docs](https://vitejs.dev/) +- [TailwindCSS Docs](https://tailwindcss.com/) +- [Cloudflare Pages](https://pages.cloudflare.com/) + +--- + +**Última atualização:** 30 de Outubro de 2025 - ✅ Veem apenas **seus próprios dados** - ✅ Veem apenas **seus próprios laudos** (filtro: `patient_id = paciente`) @@ -190,6 +466,7 @@ await authService.refreshToken(refreshToken); ``` **Fluxo Magic Link:** + 1. Usuário solicita magic link na tela de login 2. `localStorage.setItem("magic_link_redirect", "/painel-medico")` salva contexto 3. Supabase envia email com link único diff --git a/MEDICONNECT 2/package.json b/MEDICONNECT 2/package.json index 98c1fb7c0..708b6bb25 100644 --- a/MEDICONNECT 2/package.json +++ b/MEDICONNECT 2/package.json @@ -15,6 +15,8 @@ "@supabase/supabase-js": "^2.76.1", "axios": "^1.12.2", "date-fns": "^2.30.0", + "html2canvas": "^1.4.1", + "jspdf": "^3.0.3", "lucide-react": "^0.540.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/MEDICONNECT 2/pnpm-lock.yaml b/MEDICONNECT 2/pnpm-lock.yaml index 4e685bc9f..ced41cc3c 100644 --- a/MEDICONNECT 2/pnpm-lock.yaml +++ b/MEDICONNECT 2/pnpm-lock.yaml @@ -22,6 +22,12 @@ importers: date-fns: specifier: ^2.30.0 version: 2.30.0 + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 + jspdf: + specifier: ^3.0.3 + version: 3.0.3 lucide-react: specifier: ^0.540.0 version: 0.540.0(react@18.3.1) @@ -819,12 +825,18 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + '@types/phoenix@1.6.6': resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + '@types/react-dom@18.3.7': resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} peerDependencies: @@ -836,6 +848,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -1060,6 +1075,10 @@ packages: bare-abort-controller: optional: true + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -1120,6 +1139,10 @@ packages: caniuse-lite@1.0.30001750: resolution: {integrity: sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ==} + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1200,6 +1223,9 @@ packages: resolution: {integrity: sha512-X8XDzyvYaA6msMyAM575CUoygY5b44QzLcGRKsK3MFmXcOvQa518dNPLsKYwkYsn72g3EiW+LE0ytd/FlqWmyw==} engines: {node: '>=18'} + core-js@3.46.0: + resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1220,6 +1246,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1307,6 +1336,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dompurify@3.3.0: + resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + dot-prop@9.0.0: resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} engines: {node: '>=18'} @@ -1488,6 +1520,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -1506,6 +1541,9 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -1659,6 +1697,10 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -1698,6 +1740,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1794,6 +1839,9 @@ packages: engines: {node: '>=6'} hasBin: true + jspdf@3.0.3: + resolution: {integrity: sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==} + junk@4.0.1: resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} engines: {node: '>=12.20'} @@ -2036,6 +2084,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2074,6 +2125,9 @@ packages: pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2178,6 +2232,9 @@ packages: quote-unquote@1.0.0: resolution: {integrity: sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==} + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + react-dom@18.3.1: resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: @@ -2244,6 +2301,9 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + remove-trailing-separator@1.1.0: resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} @@ -2275,6 +2335,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + rollup@4.52.4: resolution: {integrity: sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2343,6 +2407,10 @@ packages: stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + streamx@2.23.0: resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} @@ -2389,6 +2457,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + tailwindcss@3.4.18: resolution: {integrity: sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==} engines: {node: '>=14.0.0'} @@ -2407,6 +2479,9 @@ packages: text-hex@1.0.0: resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -2500,6 +2575,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + uuid@11.1.0: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true @@ -3301,10 +3379,15 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/pako@2.0.4': {} + '@types/phoenix@1.6.6': {} '@types/prop-types@15.7.15': {} + '@types/raf@3.4.3': + optional: true + '@types/react-dom@18.3.7(@types/react@18.3.26)': dependencies: '@types/react': 18.3.26 @@ -3316,6 +3399,9 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/ws@8.18.1': dependencies: '@types/node': 24.7.2 @@ -3613,6 +3699,8 @@ snapshots: bare-events@2.8.0: {} + base64-arraybuffer@1.0.2: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.8.16: {} @@ -3668,6 +3756,18 @@ snapshots: caniuse-lite@1.0.30001750: {} + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.28.4 + '@types/raf': 3.4.3 + core-js: 3.46.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -3749,6 +3849,9 @@ snapshots: graceful-fs: 4.2.11 p-event: 6.0.1 + core-js@3.46.0: + optional: true + core-util-is@1.0.3: {} crc-32@1.2.2: {} @@ -3768,6 +3871,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -3852,6 +3959,11 @@ snapshots: dlv@1.1.3: {} + dompurify@3.3.0: + optionalDependencies: + '@types/trusted-types': 2.0.7 + optional: true + dot-prop@9.0.0: dependencies: type-fest: 4.41.0 @@ -4100,6 +4212,12 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -4114,6 +4232,8 @@ snapshots: fecha@4.2.3: {} + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -4254,6 +4374,11 @@ snapshots: dependencies: lru-cache: 7.18.3 + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -4282,6 +4407,8 @@ snapshots: inherits@2.0.4: {} + iobuffer@5.4.0: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -4348,6 +4475,17 @@ snapshots: json5@2.2.3: {} + jspdf@3.0.3: + dependencies: + '@babel/runtime': 7.28.4 + fast-png: 6.4.0 + fflate: 0.8.2 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.46.0 + dompurify: 3.3.0 + html2canvas: 1.4.1 + junk@4.0.1: {} jwt-decode@4.0.0: {} @@ -4559,6 +4697,8 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@2.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -4588,6 +4728,9 @@ snapshots: pend@1.2.0: {} + performance-now@2.1.0: + optional: true + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -4682,6 +4825,11 @@ snapshots: quote-unquote@1.0.0: {} + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 @@ -4765,6 +4913,9 @@ snapshots: readdirp@4.1.2: {} + regenerator-runtime@0.13.11: + optional: true + remove-trailing-separator@1.1.0: {} require-directory@2.1.1: {} @@ -4789,6 +4940,9 @@ snapshots: reusify@1.1.0: {} + rgbcolor@1.0.1: + optional: true + rollup@4.52.4: dependencies: '@types/estree': 1.0.8 @@ -4868,6 +5022,9 @@ snapshots: stack-trace@0.0.10: {} + stackblur-canvas@2.7.0: + optional: true + streamx@2.23.0: dependencies: events-universal: 1.0.1 @@ -4925,6 +5082,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-pathdata@6.0.3: + optional: true + tailwindcss@3.4.18(yaml@2.8.1): dependencies: '@alloc/quick-lru': 5.2.0 @@ -4978,6 +5138,10 @@ snapshots: text-hex@1.0.0: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -5058,6 +5222,10 @@ snapshots: util-deprecate@1.0.2: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + uuid@11.1.0: {} validate-npm-package-license@3.0.4: diff --git a/MEDICONNECT 2/public/svante_paabo.jpg b/MEDICONNECT 2/public/svante_paabo.jpg new file mode 100644 index 000000000..90ce26aac Binary files /dev/null and b/MEDICONNECT 2/public/svante_paabo.jpg differ diff --git a/MEDICONNECT 2/src/components/AgendamentoConsulta.tsx b/MEDICONNECT 2/src/components/AgendamentoConsulta.tsx index e1bfe31f3..fbb4d6450 100644 --- a/MEDICONNECT 2/src/components/AgendamentoConsulta.tsx +++ b/MEDICONNECT 2/src/components/AgendamentoConsulta.tsx @@ -19,17 +19,11 @@ import { Clock, ChevronLeft, ChevronRight, - Stethoscope, AlertCircle, CheckCircle2, Search, } from "lucide-react"; -import { - availabilityService, - exceptionsService, - appointmentService, - smsService, -} from "../services"; +import { appointmentService } from "../services"; import { useAuth } from "../hooks/useAuth"; interface Medico { @@ -43,43 +37,6 @@ interface Medico { valorConsulta?: number; } -interface TimeSlot { - inicio: string; - fim: string; - ativo: boolean; -} - -interface DaySchedule { - ativo: boolean; - horarios: TimeSlot[]; -} - -interface Availability { - domingo: DaySchedule; - segunda: DaySchedule; - terca: DaySchedule; - quarta: DaySchedule; - quinta: DaySchedule; - sexta: DaySchedule; - sabado: DaySchedule; -} - -interface Exception { - id: string; - data: string; - motivo?: string; -} - -const dayOfWeekMap: { [key: number]: keyof Availability } = { - 0: "domingo", - 1: "segunda", - 2: "terca", - 3: "quarta", - 4: "quinta", - 5: "sexta", - 6: "sabado", -}; - interface AgendamentoConsultaProps { medicos: Medico[]; } @@ -99,8 +56,6 @@ export default function AgendamentoConsulta({ const [selectedSpecialty, setSelectedSpecialty] = useState("all"); const [currentMonth, setCurrentMonth] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(undefined); - const [availability, setAvailability] = useState(null); - const [exceptions, setExceptions] = useState([]); const [availableSlots, setAvailableSlots] = useState([]); const [selectedTime, setSelectedTime] = useState(""); const [appointmentType, setAppointmentType] = useState< @@ -132,110 +87,54 @@ export default function AgendamentoConsulta({ const specialties = Array.from(new Set(medicos.map((m) => m.especialidade))); - useEffect(() => { - if (selectedMedico) { - loadDoctorAvailability(); - loadDoctorExceptions(); - } - // eslint-disable-next-line - }, [selectedMedico]); + // Removemos as funções de availability e exceptions antigas + // A API de slots já considera tudo automaticamente - const loadDoctorAvailability = useCallback(async () => { - if (!selectedMedico) return; - try { - const response = await availabilityService.getAvailability( - selectedMedico.id - ); - if ( - response && - response.success && - response.data && - response.data.length > 0 - ) { - const avail = response.data[0]; - setAvailability({ - domingo: avail.domingo || { ativo: false, horarios: [] }, - segunda: avail.segunda || { ativo: false, horarios: [] }, - terca: avail.terca || { ativo: false, horarios: [] }, - quarta: avail.quarta || { ativo: false, horarios: [] }, - quinta: avail.quinta || { ativo: false, horarios: [] }, - sexta: avail.sexta || { ativo: false, horarios: [] }, - sabado: avail.sabado || { ativo: false, horarios: [] }, - }); - } else { - setAvailability(null); - } - } catch { - setAvailability(null); + const calculateAvailableSlots = useCallback(async () => { + if (!selectedDate || !selectedMedico) { + setAvailableSlots([]); + return; } - }, [selectedMedico]); - const loadDoctorExceptions = useCallback(async () => { - if (!selectedMedico) return; try { - const response = await exceptionService.listExceptions({ + const dateStr = format(selectedDate, "yyyy-MM-dd"); + + // Usa a Edge Function para calcular slots disponíveis + const response = await appointmentService.getAvailableSlots({ doctor_id: selectedMedico.id, + date: dateStr, }); - if (response && response.success && response.data) { - setExceptions(response.data as Exception[]); - } else { - setExceptions([]); - } - } catch { - setExceptions([]); - } - }, [selectedMedico]); - const calculateAvailableSlots = useCallback(() => { - if (!selectedDate || !availability) return; - const dateStr = format(selectedDate, "yyyy-MM-dd"); - const isBlocked = exceptions.some((exc) => exc.data === dateStr); - if (isBlocked) { + if (response && response.slots) { + // Filtra apenas os slots disponíveis + const available = response.slots + .filter((slot) => slot.available) + .map((slot) => slot.time); + setAvailableSlots(available); + } else { + setAvailableSlots([]); + } + } catch (error) { + console.error("[AgendamentoConsulta] Erro ao buscar slots:", error); setAvailableSlots([]); - return; } - const dayOfWeek = selectedDate.getDay(); - const dayKey = dayOfWeekMap[dayOfWeek]; - const daySchedule = availability[dayKey]; - if (!daySchedule || !daySchedule.ativo) { - setAvailableSlots([]); - return; - } - const slots = daySchedule.horarios - .filter((slot) => slot.ativo) - .map((slot) => slot.inicio); - setAvailableSlots(slots); - }, [selectedDate, availability, exceptions]); + }, [selectedDate, selectedMedico]); useEffect(() => { - if (selectedDate && availability && selectedMedico) { + if (selectedDate && selectedMedico) { calculateAvailableSlots(); } else { setAvailableSlots([]); } - }, [ - selectedDate, - availability, - exceptions, - calculateAvailableSlots, - selectedMedico, - ]); - - const isDateBlocked = (date: Date): boolean => { - const dateStr = format(date, "yyyy-MM-dd"); - return exceptions.some((exc) => exc.data === dateStr); - }; + }, [selectedDate, selectedMedico, calculateAvailableSlots]); + // Simplificado: a API de slots já considera disponibilidade e exceções const isDateAvailable = (date: Date): boolean => { - if (!availability) return false; + // Não permite datas passadas if (isBefore(date, startOfDay(new Date()))) return false; - if (isDateBlocked(date)) return false; - const dayOfWeek = date.getDay(); - const dayKey = dayOfWeekMap[dayOfWeek]; - const daySchedule = availability[dayKey]; - return ( - daySchedule?.ativo && daySchedule.horarios.some((slot) => slot.ativo) - ); + // Para simplificar, consideramos todos os dias futuros como possíveis + // A API fará a validação real quando buscar slots + return true; }; const generateCalendarDays = () => { @@ -271,34 +170,26 @@ export default function AgendamentoConsulta({ if (!selectedMedico || !selectedDate || !selectedTime || !user) return; try { setBookingError(""); - // Cria o agendamento na API real - const result = await consultasService.criar({ + + // Formata a data no formato ISO correto + const scheduledAt = format(selectedDate, "yyyy-MM-dd") + "T" + selectedTime + ":00Z"; + + // Cria o agendamento usando a API REST + const appointment = await appointmentService.create({ patient_id: user.id, doctor_id: selectedMedico.id, - scheduled_at: - format(selectedDate, "yyyy-MM-dd") + "T" + selectedTime + ":00.000Z", + scheduled_at: scheduledAt, duration_minutes: 30, - appointment_type: appointmentType, + appointment_type: appointmentType === "online" ? "telemedicina" : "presencial", chief_complaint: motivo, - patient_notes: "", - insurance_provider: "", }); - if (!result.success) { - setBookingError(result.error || "Erro ao agendar consulta"); - setShowConfirmDialog(false); - return; - } - // Envia SMS de confirmação (se telefone disponível) - if (user.telefone) { - await smsService.enviarConfirmacaoConsulta( - user.telefone, - user.nome || "Paciente", - selectedMedico.nome, - format(selectedDate, "dd/MM/yyyy") + " às " + selectedTime - ); - } + + console.log("[AgendamentoConsulta] Consulta criada:", appointment); + setBookingSuccess(true); setShowConfirmDialog(false); + + // Reset form após 3 segundos setTimeout(() => { setSelectedMedico(null); setSelectedDate(undefined); @@ -307,6 +198,7 @@ export default function AgendamentoConsulta({ setBookingSuccess(false); }, 3000); } catch (error) { + console.error("[AgendamentoConsulta] Erro ao agendar:", error); setBookingError( error instanceof Error ? error.message @@ -496,7 +388,6 @@ export default function AgendamentoConsulta({ const isTodayDate = isToday(day); const isAvailable = isCurrentMonth && isDateAvailable(day); - const isBlocked = isCurrentMonth && isDateBlocked(day); const isPast = isBefore(day, startOfDay(new Date())); return ( + + + + + )} ); } diff --git a/MEDICONNECT 2/src/components/secretaria/SecretaryPatientList.tsx b/MEDICONNECT 2/src/components/secretaria/SecretaryPatientList.tsx index 152985fcb..3e7847f7f 100644 --- a/MEDICONNECT 2/src/components/secretaria/SecretaryPatientList.tsx +++ b/MEDICONNECT 2/src/components/secretaria/SecretaryPatientList.tsx @@ -1,9 +1,10 @@ import { useState, useEffect } from "react"; import toast from "react-hot-toast"; import { Search, Plus, Eye, Calendar, Edit, Trash2, X } from "lucide-react"; -import { patientService, userService, type Patient } from "../../services"; +import { patientService, type Patient } from "../../services"; import PacienteForm, { type PacienteFormData } from "../pacientes/PacienteForm"; import { Avatar } from "../ui/Avatar"; +import { useAuth } from "../../hooks/useAuth"; const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"]; @@ -40,6 +41,7 @@ const buscarEnderecoViaCEP = async (cep: string) => { }; export function SecretaryPatientList() { + const { user } = useAuth(); const [patients, setPatients] = useState([]); const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); @@ -50,6 +52,8 @@ export function SecretaryPatientList() { // Modal states const [showModal, setShowModal] = useState(false); const [modalMode, setModalMode] = useState<"create" | "edit">("create"); + const [showDeleteDialog, setShowDeleteDialog] = useState(false); + const [patientToDelete, setPatientToDelete] = useState(null); const [formData, setFormData] = useState({ nome: "", social_name: "", @@ -85,6 +89,15 @@ export function SecretaryPatientList() { try { const data = await patientService.list(); console.log("✅ Pacientes carregados:", data); + // Log para verificar se temos user_id + if (Array.isArray(data) && data.length > 0) { + console.log("📋 Primeiro paciente (verificar user_id):", { + full_name: data[0].full_name, + user_id: data[0].user_id, + avatar_url: data[0].avatar_url, + email: data[0].email, + }); + } setPatients(Array.isArray(data) ? data : []); if (Array.isArray(data) && data.length === 0) { console.warn("⚠️ Nenhum paciente encontrado na API"); @@ -102,6 +115,28 @@ export function SecretaryPatientList() { loadPatients(); }, []); + // Função de filtro + const filteredPatients = patients.filter((patient) => { + // Filtro de busca por nome, CPF ou email + const searchLower = searchTerm.toLowerCase(); + const matchesSearch = + !searchTerm || + patient.full_name?.toLowerCase().includes(searchLower) || + patient.cpf?.includes(searchTerm) || + patient.email?.toLowerCase().includes(searchLower); + + // Filtro de aniversariantes do mês + const matchesBirthday = !showBirthdays || (() => { + if (!patient.birth_date) return false; + const birthDate = new Date(patient.birth_date); + const currentMonth = new Date().getMonth(); + const birthMonth = birthDate.getMonth(); + return currentMonth === birthMonth; + })(); + + return matchesSearch && matchesBirthday; + }); + const handleSearch = () => { loadPatients(); }; @@ -150,6 +185,7 @@ export function SecretaryPatientList() { setModalMode("edit"); setFormData({ id: patient.id, + user_id: patient.user_id, nome: patient.full_name || "", social_name: patient.social_name || "", cpf: patient.cpf || "", @@ -165,6 +201,7 @@ export function SecretaryPatientList() { convenio: "Particular", numeroCarteirinha: "", observacoes: "", + avatar_url: patient.avatar_url || undefined, endereco: { cep: patient.cep || "", rua: patient.street || "", @@ -213,18 +250,23 @@ export function SecretaryPatientList() { try { if (modalMode === "edit" && formData.id) { // Para edição, usa o endpoint antigo (PATCH /patients/:id) + // Remove formatação de telefone, CPF e CEP + const cleanPhone = formData.numeroTelefone.replace(/\D/g, ''); + const cleanCpf = formData.cpf.replace(/\D/g, ''); + const cleanCep = formData.endereco.cep ? formData.endereco.cep.replace(/\D/g, '') : null; + const patientData = { full_name: formData.nome, social_name: formData.social_name || null, - cpf: formData.cpf, + cpf: cleanCpf, sex: formData.sexo || null, birth_date: formData.dataNascimento || null, email: formData.email, - phone_mobile: formData.numeroTelefone, + phone_mobile: cleanPhone, blood_type: formData.tipo_sanguineo || null, height_m: formData.altura ? parseFloat(formData.altura) : null, weight_kg: formData.peso ? parseFloat(formData.peso) : null, - cep: formData.endereco.cep || null, + cep: cleanCep, street: formData.endereco.rua || null, number: formData.endereco.numero || null, complement: formData.endereco.complemento || null, @@ -235,26 +277,34 @@ export function SecretaryPatientList() { await patientService.update(formData.id, patientData); toast.success("Paciente atualizado com sucesso!"); } else { - // Para criação, usa o novo endpoint create-patient com validações completas + // Criar novo paciente usando a API REST direta + // Remove formatação de telefone e CPF + const cleanPhone = formData.numeroTelefone.replace(/\D/g, ''); + const cleanCpf = formData.cpf.replace(/\D/g, ''); + const cleanCep = formData.endereco.cep ? formData.endereco.cep.replace(/\D/g, '') : null; + const createData = { - email: formData.email, full_name: formData.nome, - cpf: formData.cpf, - phone_mobile: formData.numeroTelefone, - birth_date: formData.dataNascimento || undefined, - address: formData.endereco.rua - ? `${formData.endereco.rua}${ - formData.endereco.numero ? ", " + formData.endereco.numero : "" - }${ - formData.endereco.bairro ? " - " + formData.endereco.bairro : "" - }${ - formData.endereco.cidade ? " - " + formData.endereco.cidade : "" - }${ - formData.endereco.estado ? "/" + formData.endereco.estado : "" - }` - : undefined, + cpf: cleanCpf, + email: formData.email, + phone_mobile: cleanPhone, + birth_date: formData.dataNascimento || null, + social_name: formData.social_name || null, + sex: formData.sexo || null, + blood_type: formData.tipo_sanguineo || null, + weight_kg: formData.peso ? parseFloat(formData.peso) : null, + height_m: formData.altura ? parseFloat(formData.altura) : null, + street: formData.endereco.rua || null, + number: formData.endereco.numero || null, + complement: formData.endereco.complemento || null, + neighborhood: formData.endereco.bairro || null, + city: formData.endereco.cidade || null, + state: formData.endereco.estado || null, + cep: cleanCep, + created_by: user?.id || undefined, }; - await userService.createPatient(createData); + + await patientService.create(createData); toast.success("Paciente cadastrado com sucesso!"); } @@ -272,6 +322,34 @@ export function SecretaryPatientList() { setShowModal(false); }; + const handleDeleteClick = (patient: Patient) => { + setPatientToDelete(patient); + setShowDeleteDialog(true); + }; + + const handleConfirmDelete = async () => { + if (!patientToDelete?.id) return; + + setLoading(true); + try { + await patientService.delete(patientToDelete.id); + toast.success("Paciente deletado com sucesso!"); + setShowDeleteDialog(false); + setPatientToDelete(null); + loadPatients(); + } catch (error) { + console.error("Erro ao deletar paciente:", error); + toast.error("Erro ao deletar paciente"); + } finally { + setLoading(false); + } + }; + + const handleCancelDelete = () => { + setShowDeleteDialog(false); + setPatientToDelete(null); + }; + const getPatientColor = ( index: number ): "blue" | "green" | "purple" | "orange" | "pink" | "teal" => { @@ -394,17 +472,17 @@ export function SecretaryPatientList() { Carregando pacientes... - ) : patients.length === 0 ? ( + ) : filteredPatients.length === 0 ? ( - Nenhum paciente encontrado + {searchTerm ? "Nenhum paciente encontrado com esse termo" : "Nenhum paciente encontrado"} ) : ( - patients.map((patient, index) => ( + filteredPatients.map((patient, index) => ( + + + + + + + )} ); } diff --git a/MEDICONNECT 2/src/components/secretaria/SecretaryReportList.tsx b/MEDICONNECT 2/src/components/secretaria/SecretaryReportList.tsx index 64027ba77..b8c8a4c41 100644 --- a/MEDICONNECT 2/src/components/secretaria/SecretaryReportList.tsx +++ b/MEDICONNECT 2/src/components/secretaria/SecretaryReportList.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from "react"; import toast from "react-hot-toast"; -import { Search, FileText, Download, Plus } from "lucide-react"; +import { Search, FileText, Download, Plus, Eye, Edit2, X } from "lucide-react"; +import jsPDF from "jspdf"; +import html2canvas from "html2canvas"; import { reportService, type Report, @@ -12,15 +14,20 @@ export function SecretaryReportList() { const [reports, setReports] = useState([]); const [loading, setLoading] = useState(false); const [searchTerm, setSearchTerm] = useState(""); - const [typeFilter, setTypeFilter] = useState("Todos"); - const [periodFilter, setPeriodFilter] = useState("Todos"); + const [statusFilter, setStatusFilter] = useState(""); const [showCreateModal, setShowCreateModal] = useState(false); + const [showViewModal, setShowViewModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [selectedReport, setSelectedReport] = useState(null); const [patients, setPatients] = useState([]); const [formData, setFormData] = useState({ patient_id: "", exam: "", diagnosis: "", conclusion: "", + status: "draft" as "draft" | "completed" | "pending" | "cancelled", + cid_code: "", + requested_by: "", }); useEffect(() => { @@ -43,10 +50,32 @@ export function SecretaryReportList() { exam: "", diagnosis: "", conclusion: "", + status: "draft", + cid_code: "", + requested_by: "", }); setShowCreateModal(true); }; + const handleViewReport = (report: Report) => { + setSelectedReport(report); + setShowViewModal(true); + }; + + const handleOpenEditModal = (report: Report) => { + setSelectedReport(report); + setFormData({ + patient_id: report.patient_id, + exam: report.exam || "", + diagnosis: report.diagnosis || "", + conclusion: report.conclusion || "", + status: report.status || "draft", + cid_code: report.cid_code || "", + requested_by: report.requested_by || "", + }); + setShowEditModal(true); + }; + const handleCreateReport = async (e: React.FormEvent) => { e.preventDefault(); @@ -72,6 +101,142 @@ export function SecretaryReportList() { } }; + const handleUpdateReport = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!selectedReport?.id) { + toast.error("Relatório não identificado"); + return; + } + + try { + await reportService.update(selectedReport.id, { + patient_id: formData.patient_id, + exam: formData.exam || undefined, + diagnosis: formData.diagnosis || undefined, + conclusion: formData.conclusion || undefined, + status: formData.status, + cid_code: formData.cid_code || undefined, + requested_by: formData.requested_by || undefined, + }); + + toast.success("Relatório atualizado com sucesso!"); + setShowEditModal(false); + setSelectedReport(null); + loadReports(); + } catch (error) { + console.error("Erro ao atualizar relatório:", error); + toast.error("Erro ao atualizar relatório"); + } + }; + + const handleDownloadReport = async (report: Report) => { + try { + // Criar um elemento temporário para o relatório + const reportElement = document.createElement("div"); + reportElement.style.padding = "40px"; + reportElement.style.backgroundColor = "white"; + reportElement.style.width = "800px"; + reportElement.style.fontFamily = "Arial, sans-serif"; + + reportElement.innerHTML = ` +
+

Relatório Médico

+

${report.order_number || "—"}

+
+ +
+
+
+
+

STATUS

+

${ + report.status === "completed" + ? "✅ Concluído" + : report.status === "pending" + ? "⏳ Pendente" + : report.status === "draft" + ? "📝 Rascunho" + : "❌ Cancelado" + }

+
+
+

DATA

+

${formatDate(report.created_at)}

+
+
+
+
+ + ${report.exam ? ` +
+

EXAME REALIZADO

+

${report.exam}

+
+ ` : ""} + + ${report.cid_code ? ` +
+

CÓDIGO CID-10

+

${report.cid_code}

+
+ ` : ""} + + ${report.requested_by ? ` +
+

SOLICITADO POR

+

${report.requested_by}

+
+ ` : ""} + + ${report.diagnosis ? ` +
+

DIAGNÓSTICO

+

${report.diagnosis}

+
+ ` : ""} + + ${report.conclusion ? ` +
+

CONCLUSÃO

+

${report.conclusion}

+
+ ` : ""} + +
+

Documento gerado em ${new Date().toLocaleDateString("pt-BR", { day: "2-digit", month: "long", year: "numeric" })}

+
+ `; + + // Adicionar ao DOM temporariamente + document.body.appendChild(reportElement); + + // Capturar como imagem + const canvas = await html2canvas(reportElement, { + scale: 2, + backgroundColor: "#ffffff", + logging: false, + }); + + // Remover elemento temporário + document.body.removeChild(reportElement); + + // Criar PDF + const imgWidth = 210; // A4 width in mm + const imgHeight = (canvas.height * imgWidth) / canvas.width; + const pdf = new jsPDF("p", "mm", "a4"); + const imgData = canvas.toDataURL("image/png"); + + pdf.addImage(imgData, "PNG", 0, 0, imgWidth, imgHeight); + pdf.save(`relatorio-${report.order_number || "sem-numero"}.pdf`); + + toast.success("Relatório baixado com sucesso!"); + } catch (error) { + console.error("Erro ao gerar PDF:", error); + toast.error("Erro ao gerar PDF do relatório"); + } + }; + const loadReports = async () => { setLoading(true); try { @@ -96,8 +261,7 @@ export function SecretaryReportList() { const handleClear = () => { setSearchTerm(""); - setTypeFilter("Todos"); - setPeriodFilter("Todos"); + setStatusFilter(""); loadReports(); }; @@ -164,31 +328,17 @@ export function SecretaryReportList() {
- Tipo: + Status: -
-
- Período: -
@@ -284,18 +434,37 @@ export function SecretaryReportList() { {report.requested_by || "—"} - +
+ + + +
)) @@ -400,6 +569,287 @@ export function SecretaryReportList() { )} + + {/* Modal de Visualizar Relatório */} + {showViewModal && selectedReport && ( +
+
+
+

+ Visualizar Relatório +

+ +
+ +
+
+
+ +

+ {selectedReport.order_number || "—"} +

+
+
+ + + {selectedReport.status === "completed" + ? "Concluído" + : selectedReport.status === "pending" + ? "Pendente" + : selectedReport.status === "draft" + ? "Rascunho" + : "Cancelado"} + +
+
+ +
+ +

{selectedReport.exam || "—"}

+
+ +
+ +

{selectedReport.cid_code || "—"}

+
+ +
+ +

{selectedReport.requested_by || "—"}

+
+ +
+ +

+ {selectedReport.diagnosis || "—"} +

+
+ +
+ +

+ {selectedReport.conclusion || "—"} +

+
+ +
+
+ +

+ {formatDate(selectedReport.created_at)} +

+
+
+ +

+ {formatDate(selectedReport.updated_at)} +

+
+
+
+ +
+ + +
+
+
+ )} + + {/* Modal de Editar Relatório */} + {showEditModal && selectedReport && ( +
+
+
+

+ Editar Relatório +

+ +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + + setFormData({ ...formData, exam: e.target.value }) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" + placeholder="Nome do exame realizado" + /> +
+ +
+ + + setFormData({ ...formData, cid_code: e.target.value }) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" + placeholder="Ex: A00.0" + /> +
+ +
+ + + setFormData({ ...formData, requested_by: e.target.value }) + } + className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500" + placeholder="Nome do médico solicitante" + /> +
+ +
+ +