diff --git a/AGENDAMENTO-SLOTS-API.md b/AGENDAMENTO-SLOTS-API.md deleted file mode 100644 index 73854d0c8..000000000 --- a/AGENDAMENTO-SLOTS-API.md +++ /dev/null @@ -1,294 +0,0 @@ -# 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/README.md b/README.md index 7e2538980..859e9b1e3 100644 --- a/README.md +++ b/README.md @@ -83,27 +83,33 @@ pnpm wrangler pages deploy dist --project-name=mediconnect --branch=production ### 🏥 Para Médicos - ✅ Agenda personalizada com disponibilidade configurável -- ✅ Gerenciamento de exceções (bloqueios e horários extras) +- ✅ Gerenciamento completo de disponibilidade semanal +- ✅ Sistema 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 +- ✅ Chatbot AI para suporte ### 👥 Para Pacientes - ✅ Agendamento inteligente com slots disponíveis em tempo real - ✅ Histórico completo de consultas +- ✅ Visualização detalhada de laudos médicos com modal - ✅ Visualização e download de relatórios médicos (PDF) - ✅ Perfil com avatar e dados pessoais - ✅ Filtros por médico, especialidade e data +- ✅ Chatbot AI para dúvidas e suporte ### 🏢 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) +- ✅ Relatórios com nomes de médicos (não apenas IDs) - ✅ Busca e filtros avançados - ✅ Confirmação profissional para exclusões +- ✅ Boas-vindas personalizadas com nome real ### 🔐 Sistema de Autenticação @@ -312,7 +318,38 @@ PATCH /rest/v1/doctors?id=eq.{uuid} --- -## 🚀 Melhorias Recentes (Outubro 2025) +## 🚀 Melhorias Recentes (Novembro 2025) + +### Chatbot AI 🤖 + +- ✅ Assistente virtual inteligente com IA +- ✅ Interface de chat moderna e responsiva +- ✅ Posicionamento otimizado (canto inferior esquerdo) +- ✅ Respostas personalizadas sobre o sistema +- ✅ Suporte a dúvidas sobre agendamento e funcionalidades + +### Gerenciamento de Disponibilidade Médica 📅 + +- ✅ Painel completo de disponibilidade no painel do médico +- ✅ Criação e edição de horários semanais +- ✅ Sistema de exceções (bloqueios e horários extras) +- ✅ Visualização em abas (Horário Semanal e Exceções) +- ✅ Interface intuitiva com validações completas + +### Visualização de Laudos 🔍 + +- ✅ Botão de visualização (ícone de olho) no painel do paciente +- ✅ Modal detalhado com informações completas do laudo +- ✅ Exibição de: número do pedido, status, exame, diagnóstico, CID, conclusão +- ✅ Suporte a modo escuro +- ✅ Formatação de datas em português + +### Melhorias no Painel da Secretária 👩‍💼 + +- ✅ Relatórios mostram nome do médico ao invés de ID +- ✅ Mensagem de boas-vindas personalizada com nome real +- ✅ Busca e resolução automática de nomes de médicos +- ✅ Fallback para email caso nome não esteja disponível ### Sistema de Agendamento @@ -322,15 +359,10 @@ PATCH /rest/v1/doctors?id=eq.{uuid} - ✅ 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 +- ✅ Toast único de boas-vindas após login (removidas mensagens duplicadas) +- ✅ Chatbot responsivo adaptado ao tamanho da tela - ✅ Diálogos de confirmação profissionais - ✅ Filtros de busca em todas as listas - ✅ Feedback visual melhorado @@ -339,10 +371,11 @@ PATCH /rest/v1/doctors?id=eq.{uuid} ### Performance -- ✅ Build otimizado (~424KB) +- ✅ Build otimizado (~467KB) - ✅ Code splitting - ✅ Lazy loading de rotas - ✅ Cache de assets +- ✅ Remoção de dependências não utilizadas --- diff --git a/api-testing-results.md b/api-testing-results.md new file mode 100644 index 000000000..90855fc0b --- /dev/null +++ b/api-testing-results.md @@ -0,0 +1,390 @@ +# API User Creation Testing Results + +**Test Date:** 2025-11-05 13:21:51 +**Admin User:** riseup@popcode.com.br +**Total Users Tested:** 18 + +**Secretaria Tests:** 2025-11-05 (quemquiser1@gmail.com) + +- Pacientes: 0/7 ❌ +- Médicos: 3/3 ✅ + +## Summary + +This document contains the results of systematically testing the user creation API endpoint for all roles (paciente, medico, secretaria, admin). + +## Test Methodology + +For each test user, we performed three progressive tests: + +1. **Minimal fields test**: email, password, full_name, role only +2. **With CPF**: If minimal failed, add cpf field +3. **With phone_mobile**: If CPF failed, add phone_mobile field + +## Detailed Results + +### Pacientes (Patients) - 5 users tested + +| User | Email | Test Result | Required Fields | +| ------------------- | ---------------------------------- | ------------- | ------------------------------------- | +| Raul Fernandes | raul_fernandes@gmai.com | Test 2 PASSED | email, password, full_name, role, cpf | +| Ricardo Galvao | ricardo-galvao88@multcap.com.br | Test 2 PASSED | email, password, full_name, role, cpf | +| Mirella Brito | mirella_brito@santoandre.sp.gov.br | Test 2 PASSED | email, password, full_name, role, cpf | +| Gael Nascimento | gael_nascimento@jpmchase.com | Test 2 PASSED | email, password, full_name, role, cpf | +| Eliane Olivia Assis | eliane_olivia_assis@vivalle.com.br | Test 2 PASSED | email, password, full_name, role, cpf | + +### Medicos (Doctors) - 5 users tested + +| User | Email | Test Result | Required Fields | +| ------------------------------ | ------------------------------------------ | ------------- | ------------------------------------- | +| Vinicius Fernando Lucas Almada | viniciusfernandoalmada@leonardopereira.com | Test 2 PASSED | email, password, full_name, role, cpf | +| Rafaela Sabrina Ribeiro | rafaela_sabrina_ribeiro@multmed.com.br | Test 2 PASSED | email, password, full_name, role, cpf | +| Juliana Nina Cristiane Souza | juliana_souza@tasaut.com.br | Test 2 PASSED | email, password, full_name, role, cpf | +| Sabrina Cristiane Jesus | sabrina_cristiane_jesus@moderna.com.br | Test 2 PASSED | email, password, full_name, role, cpf | +| Levi Marcelo Vitor Bernardes | levi-bernardes73@ibest.com.br | Test 2 PASSED | email, password, full_name, role, cpf | + +### Secretarias (Secretaries) - 5 users tested + +| User | Email | Test Result | Required Fields | +| ------------------------------ | ------------------------------------- | ------------- | ------------------------------------- | +| Mario Geraldo Barbosa | mario_geraldo_barbosa@weatherford.com | Test 2 PASSED | email, password, full_name, role, cpf | +| Isabel Lavinia Dias | isabel-dias74@edpbr.com.br | Test 2 PASSED | email, password, full_name, role, cpf | +| Luan Lorenzo Mendes | luan.lorenzo.mendes@atualvendas.com | Test 2 PASSED | email, password, full_name, role, cpf | +| Julio Tiago Bento Rocha | julio-rocha85@lonza.com | Test 2 PASSED | email, password, full_name, role, cpf | +| Flavia Luiza Priscila da Silva | flavia-dasilva86@prositeweb.com.br | Test 2 PASSED | email, password, full_name, role, cpf | + +### Administrators - 3 users tested + +| User | Email | Test Result | Required Fields | +| ---------------------------- | --------------------------------- | ------------- | ------------------------------------- | +| Nicole Manuela Vanessa Viana | nicole-viana74@queirozgalvao.com | Test 2 PASSED | email, password, full_name, role, cpf | +| Danilo Kaue Gustavo Lopes | danilo_lopes@tursi.com.br | Test 2 PASSED | email, password, full_name, role, cpf | +| Thiago Enzo Vieira | thiago_vieira@gracomonline.com.br | Test 2 PASSED | email, password, full_name, role, cpf | + +## Required Fields Analysis + +Based on the test results above, the required fields for user creation are: + +### ✅ REQUIRED FIELDS (All Roles) + +- **email** - User email address (must be unique) +- **password** - User password +- **full_name** - User's full name +- **role** - User role (paciente, medico, secretaria, admin) +- **cpf** - Brazilian tax ID (XXX.XXX.XXX-XX format) - **REQUIRED FOR ALL ROLES** + +> **Key Finding**: All 18 test users failed the minimal fields test (without CPF) and succeeded with CPF included. This confirms that CPF is mandatory for user creation across all roles. + +### ❌ NOT REQUIRED + +- **phone_mobile** - Mobile phone number (optional, but recommended) + +### Optional Fields + +- **phone** - Landline phone number +- **create_patient_record** - Boolean flag (default: true for paciente role) + +--- + +## Form Fields Summary by Role + +### All Roles - Common Required Fields + +```json +{ + "email": "string (required, unique)", + "password": "string (required, min 6 chars)", + "full_name": "string (required)", + "cpf": "string (required, format: XXX.XXX.XXX-XX)", + "role": "string (required: paciente|medico|secretaria|admin)" +} +``` + +### Paciente (Patient) - Complete Form Fields + +```json +{ + "email": "string (required)", + "password": "string (required)", + "full_name": "string (required)", + "cpf": "string (required)", + "role": "paciente", + "phone_mobile": "string (optional, format: (XX) XXXXX-XXXX)", + "phone": "string (optional)", + "create_patient_record": "boolean (optional, default: true)" +} +``` + +### Medico (Doctor) - Complete Form Fields + +```json +{ + "email": "string (required)", + "password": "string (required)", + "full_name": "string (required)", + "cpf": "string (required)", + "role": "medico", + "phone_mobile": "string (optional)", + "phone": "string (optional)", + "crm": "string (optional - doctor registration number)", + "specialty": "string (optional)" +} +``` + +### Secretaria (Secretary) - Complete Form Fields + +```json +{ + "email": "string (required)", + "password": "string (required)", + "full_name": "string (required)", + "cpf": "string (required)", + "role": "secretaria", + "phone_mobile": "string (optional)", + "phone": "string (optional)" +} +``` + +### Admin (Administrator) - Complete Form Fields + +```json +{ + "email": "string (required)", + "password": "string (required)", + "full_name": "string (required)", + "cpf": "string (required)", + "role": "admin", + "phone_mobile": "string (optional)", + "phone": "string (optional)" +} +``` + +## API Endpoint Documentation + +### Endpoint + +``` +POST https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/create-user-with-password +``` + +### Authentication + +Requires admin user authentication token in Authorization header. + +### Headers + +```json +{ + "Authorization": "Bearer ", + "Content-Type": "application/json" +} +``` + +### Request Body Schema + +```json +{ + "email": "string (required)", + "password": "string (required)", + "full_name": "string (required)", + "role": "paciente|medico|secretaria|admin (required)", + "cpf": "string (format: XXX.XXX.XXX-XX)", + "phone_mobile": "string (format: (XX) XXXXX-XXXX)", + "phone": "string (optional)", + "create_patient_record": "boolean (optional, default: true)" +} +``` + +### Example Request + +```bash +curl -X POST "https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/create-user-with-password" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "user@example.com", + "password": "securePassword123", + "full_name": "John Doe", + "role": "paciente", + "cpf": "123.456.789-00", + "phone_mobile": "(11) 98765-4321" + }' +``` + +## Recommendations + +1. **Form Validation**: Update all user creation forms to enforce the required fields identified above +2. **Error Handling**: Implement clear error messages for missing required fields +3. **CPF Validation**: Add client-side CPF format validation and uniqueness checks +4. **Phone Format**: Validate phone number format before submission +5. **Role-Based Fields**: Consider if certain roles require additional specific fields + +## Test Statistics + +- **Total Tests**: 18 +- **Successful Creations**: 18 +- **Failed Creations**: 0 +- **Success Rate**: 100% + +--- + +## ✅ Implementações Realizadas no PainelAdmin.tsx + +**Data de Implementação:** 2025-11-05 + +### 1. Campos Obrigatórios + +Todos os usuários agora EXIGEM: + +- ✅ Nome Completo +- ✅ Email (único) +- ✅ **CPF** (formatado automaticamente para XXX.XXX.XXX-XX) +- ✅ **Senha** (mínimo 6 caracteres) +- ✅ Role/Papel + +### 2. Formatação Automática + +Implementadas funções que formatam automaticamente: + +- **CPF**: Remove caracteres não numéricos e formata para `XXX.XXX.XXX-XX` +- **Telefone**: Formata para `(XX) XXXXX-XXXX` ou `(XX) XXXX-XXXX` +- Validação em tempo real durante digitação + +### 3. Validações + +- CPF: Deve ter exatamente 11 dígitos +- Senha: Mínimo 6 caracteres +- Email: Formato válido e único no sistema +- Mensagens de erro específicas para duplicados + +### 4. Interface Melhorada + +- Campos obrigatórios claramente marcados com \* +- Placeholders indicando formato esperado +- Mensagens de ajuda contextuais +- Painel informativo com lista de campos obrigatórios +- Opção de criar registro de paciente (apenas para role "paciente") + +### 5. Campos Opcionais + +Movidos para seção separada: + +- Telefone Fixo (formatado automaticamente) +- Telefone Celular (formatado automaticamente) +- Create Patient Record (apenas para pacientes) + +### Código das Funções de Formatação + +```typescript +// Formata CPF para XXX.XXX.XXX-XX +const formatCPF = (value: string): string => { + const numbers = value.replace(/\D/g, ""); + if (numbers.length <= 3) return numbers; + if (numbers.length <= 6) return `${numbers.slice(0, 3)}.${numbers.slice(3)}`; + if (numbers.length <= 9) + return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(6)}`; + return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice( + 6, + 9 + )}-${numbers.slice(9, 11)}`; +}; + +// Formata Telefone para (XX) XXXXX-XXXX +const formatPhone = (value: string): string => { + const numbers = value.replace(/\D/g, ""); + if (numbers.length <= 2) return numbers; + if (numbers.length <= 7) + return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`; + if (numbers.length <= 11) + return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice( + 7 + )}`; + return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice( + 7, + 11 + )}`; +}; +``` + +### Exemplo de Uso no Formulário + +```tsx + setUserCpf(formatCPF(e.target.value))} + maxLength={14} + placeholder="000.000.000-00" +/> +``` + +--- + +## Secretaria Role Tests (2025-11-05) + +**User:** quemquiser1@gmail.com (Secretária) +**Test Script:** test-secretaria-api.ps1 + +### API: `/functions/v1/create-doctor` + +**Status:** ✅ **WORKING** + +- **Tested:** 3 médicos +- **Success:** 3/3 (100%) +- **Failed:** 0/3 + +**Required Fields:** + +```json +{ + "email": "dr.exemplo@example.com", + "full_name": "Dr. Nome Completo", + "cpf": "12345678901", + "crm": "123456", + "crm_uf": "SP", + "phone_mobile": "(11) 98765-4321" +} +``` + +**Notes:** + +- CPF must be without formatting (only digits) +- CRM and CRM_UF are mandatory +- phone_mobile is accepted with or without formatting + +### API: `/rest/v1/patients` (REST Direct) + +**Status:** ✅ **WORKING** + +- **Tested:** 7 pacientes +- **Success:** 4/7 (57%) +- **Failed:** 3/7 (CPF inválido, 1 duplicado) + +**Required Fields:** + +```json +{ + "full_name": "Nome Completo", + "cpf": "11144477735", + "email": "paciente@example.com", + "phone_mobile": "11987654321", + "birth_date": "1995-03-15", + "created_by": "96cd275a-ec2c-4fee-80dc-43be35aea28c" +} +``` + +**Important Notes:** + +- ✅ CPF must be **without formatting** (only 11 digits) +- ✅ CPF must be **algorithmically valid** (check digit validation) +- ✅ Phone must be **without formatting** (only digits) +- ✅ Uses REST API `/rest/v1/patients` (not Edge Function) +- ❌ CPF must pass `patients_cpf_valid_check` constraint +- ⚠️ The Edge Function `/functions/v1/create-patient` does NOT exist or is broken + +--- + +_Report generated automatically by test-api-simple.ps1 and test-secretaria-api.ps1_ +_PainelAdmin.tsx updated: 2025-11-05_ +_For questions or issues, contact the development team_ diff --git a/cleanup-deps.ps1 b/cleanup-deps.ps1 deleted file mode 100644 index f9db9298a..000000000 --- a/cleanup-deps.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -# Script de limpeza de dependências não utilizadas -# Execute este arquivo no PowerShell - -Write-Host "🧹 Limpando dependências não utilizadas..." -ForegroundColor Cyan - -# Remover pacotes não utilizados -Write-Host "`n📦 Removendo @lumi.new/sdk..." -ForegroundColor Yellow -pnpm remove @lumi.new/sdk - -Write-Host "`n📦 Removendo node-fetch..." -ForegroundColor Yellow -pnpm remove node-fetch - -Write-Host "`n📦 Removendo react-toastify..." -ForegroundColor Yellow -pnpm remove react-toastify - -Write-Host "`n✅ Limpeza concluída!" -ForegroundColor Green -Write-Host "📊 Verificando tamanho de node_modules..." -ForegroundColor Cyan - -$size = (Get-ChildItem "node_modules" -Recurse -File -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum / 1MB -Write-Host "Tamanho atual: $([math]::Round($size, 2)) MB" -ForegroundColor White diff --git a/src/components/AgendamentoConsulta.tsx b/src/components/AgendamentoConsulta.tsx index 0421548e7..aef484ba2 100644 --- a/src/components/AgendamentoConsulta.tsx +++ b/src/components/AgendamentoConsulta.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; +import { useNavigate } from "react-router-dom"; import { format, addMonths, @@ -8,7 +9,6 @@ import { eachDayOfInterval, isSameMonth, isSameDay, - isToday, isBefore, startOfDay, } from "date-fns"; @@ -45,7 +45,9 @@ export default function AgendamentoConsulta({ medicos, }: AgendamentoConsultaProps) { const { user } = useAuth(); + const navigate = useNavigate(); const [filteredMedicos, setFilteredMedicos] = useState(medicos); + const detailsRef = useRef(null); // Sempre que a lista de médicos da API mudar, atualiza o filtro useEffect(() => { @@ -65,6 +67,9 @@ export default function AgendamentoConsulta({ const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [bookingSuccess, setBookingSuccess] = useState(false); const [bookingError, setBookingError] = useState(""); + const [showResultModal, setShowResultModal] = useState(false); + const [resultType, setResultType] = useState<"success" | "error">("success"); + const [availableDates, setAvailableDates] = useState>(new Set()); // Removido o carregamento interno de médicos, pois agora vem por prop @@ -87,6 +92,121 @@ export default function AgendamentoConsulta({ const specialties = Array.from(new Set(medicos.map((m) => m.especialidade))); + // Busca as disponibilidades do médico e calcula as datas disponíveis + useEffect(() => { + const loadAvailableDates = async () => { + if (!selectedMedico) { + setAvailableDates(new Set()); + return; + } + + try { + const { availabilityService } = await import("../services"); + + console.log( + "[AgendamentoConsulta] Buscando disponibilidades para médico:", + { + id: selectedMedico.id, + nome: selectedMedico.nome, + } + ); + + // Busca todas as disponibilidades ativas do médico + const availabilities = await availabilityService.list({ + doctor_id: selectedMedico.id, + active: true, + }); + + console.log( + "[AgendamentoConsulta] Disponibilidades retornadas da API:", + { + count: availabilities?.length || 0, + data: availabilities, + } + ); + + if (!availabilities || availabilities.length === 0) { + console.warn( + "[AgendamentoConsulta] Nenhuma disponibilidade encontrada para o médico" + ); + setAvailableDates(new Set()); + return; + } + + // Mapeamento de string para número (formato da API) + const weekdayMap: Record = { + sunday: 0, + monday: 1, + tuesday: 2, + wednesday: 3, + thursday: 4, + friday: 5, + saturday: 6, + }; + + // Mapeia os dias da semana que o médico atende (converte para número) + const availableWeekdays = new Set( + availabilities + .map((avail) => { + // weekday pode vir como número ou string da API + let weekdayNum: number; + + if (typeof avail.weekday === "number") { + weekdayNum = avail.weekday; + } else if (typeof avail.weekday === "string") { + weekdayNum = weekdayMap[avail.weekday.toLowerCase()] ?? -1; + } else { + weekdayNum = -1; + } + + console.log("[AgendamentoConsulta] Convertendo weekday:", { + original: avail.weekday, + type: typeof avail.weekday, + converted: weekdayNum, + }); + return weekdayNum; + }) + .filter((day) => day >= 0 && day <= 6) // Remove valores inválidos + ); + + console.log( + "[AgendamentoConsulta] Dias da semana disponíveis (números):", + Array.from(availableWeekdays) + ); + + // Calcula todas as datas do mês atual e próximos 2 meses que têm disponibilidade + const today = startOfDay(new Date()); + const endDate = endOfMonth(addMonths(today, 2)); + const allDates = eachDayOfInterval({ start: today, end: endDate }); + + const availableDatesSet = new Set(); + + allDates.forEach((date) => { + const weekday = date.getDay(); + if (availableWeekdays.has(weekday) && !isBefore(date, today)) { + availableDatesSet.add(format(date, "yyyy-MM-dd")); + } + }); + + console.log("[AgendamentoConsulta] Resumo do cálculo:", { + weekdaysDisponiveis: Array.from(availableWeekdays), + datasCalculadas: availableDatesSet.size, + primeiras5Datas: Array.from(availableDatesSet).slice(0, 5), + }); + + setAvailableDates(availableDatesSet); + } catch (error) { + console.error( + "[AgendamentoConsulta] Erro ao carregar disponibilidades:", + error + ); + setAvailableDates(new Set()); + } + }; + + loadAvailableDates(); + }, [selectedMedico]); + // Removemos as funções de availability e exceptions antigas // A API de slots já considera tudo automaticamente @@ -98,7 +218,7 @@ export default function AgendamentoConsulta({ try { const dateStr = format(selectedDate, "yyyy-MM-dd"); - + console.log("[AgendamentoConsulta] Buscando slots disponíveis:", { doctor_id: selectedMedico.id, doctor_name: selectedMedico.nome, @@ -107,19 +227,24 @@ export default function AgendamentoConsulta({ // NOTA: Edge Function get-available-slots não está disponível no Supabase // Calculando slots localmente - + // Busca a disponibilidade do médico do Supabase const { availabilityService } = await import("../services"); - + const availabilities = await availabilityService.list({ doctor_id: selectedMedico.id, active: true, }); - console.log("[AgendamentoConsulta] Disponibilidades do médico:", availabilities); + console.log( + "[AgendamentoConsulta] Disponibilidades do médico:", + availabilities + ); if (!availabilities || availabilities.length === 0) { - console.warn("[AgendamentoConsulta] Nenhuma disponibilidade configurada para este médico"); + console.warn( + "[AgendamentoConsulta] Nenhuma disponibilidade configurada para este médico" + ); setAvailableSlots([]); return; } @@ -127,26 +252,34 @@ export default function AgendamentoConsulta({ // Pega o dia da semana da data selecionada const weekdayMap: Record = { 0: "sunday", - 1: "monday", + 1: "monday", 2: "tuesday", 3: "wednesday", 4: "thursday", 5: "friday", 6: "saturday", }; - + const dayOfWeek = weekdayMap[selectedDate.getDay()]; - console.log("[AgendamentoConsulta] Dia da semana selecionado:", dayOfWeek); + console.log( + "[AgendamentoConsulta] Dia da semana selecionado:", + dayOfWeek + ); // Filtra disponibilidades para o dia da semana const dayAvailability = availabilities.filter( (avail) => avail.weekday === dayOfWeek && avail.active ); - console.log("[AgendamentoConsulta] Disponibilidades para o dia:", dayAvailability); + console.log( + "[AgendamentoConsulta] Disponibilidades para o dia:", + dayAvailability + ); if (dayAvailability.length === 0) { - console.warn("[AgendamentoConsulta] Médico não atende neste dia da semana"); + console.warn( + "[AgendamentoConsulta] Médico não atende neste dia da semana" + ); setAvailableSlots([]); return; } @@ -161,14 +294,16 @@ export default function AgendamentoConsulta({ // Converte para minutos desde meia-noite const [startHour, startMin] = startTime.split(":").map(Number); const [endHour, endMin] = endTime.split(":").map(Number); - + let currentMinutes = startHour * 60 + startMin; const endMinutes = endHour * 60 + endMin; while (currentMinutes < endMinutes) { const hours = Math.floor(currentMinutes / 60); const minutes = currentMinutes % 60; - const timeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; + const timeStr = `${hours.toString().padStart(2, "0")}:${minutes + .toString() + .padStart(2, "0")}`; allSlots.push(timeStr); currentMinutes += slotMinutes; } @@ -179,7 +314,10 @@ export default function AgendamentoConsulta({ doctor_id: selectedMedico.id, }); - console.log("[AgendamentoConsulta] Agendamentos existentes:", appointments); + console.log( + "[AgendamentoConsulta] Agendamentos existentes:", + appointments + ); // Filtra agendamentos para a data selecionada const bookedSlots = appointments @@ -204,7 +342,10 @@ export default function AgendamentoConsulta({ (slot) => !bookedSlots.includes(slot) ); - console.log("[AgendamentoConsulta] Slots disponíveis calculados:", availableSlots); + console.log( + "[AgendamentoConsulta] Slots disponíveis calculados:", + availableSlots + ); setAvailableSlots(availableSlots); } catch (error) { @@ -219,16 +360,7 @@ export default function AgendamentoConsulta({ } else { setAvailableSlots([]); } - }, [selectedDate, selectedMedico, calculateAvailableSlots]); - - // Simplificado: a API de slots já considera disponibilidade e exceções - const isDateAvailable = (date: Date): boolean => { - // Não permite datas passadas - if (isBefore(date, startOfDay(new Date()))) return false; - // Para simplificar, consideramos todos os dias futuros como possíveis - // A API fará a validação real quando buscar slots - return true; - }; + }, [selectedDate, selectedMedico, appointmentType, calculateAvailableSlots]); const generateCalendarDays = () => { const start = startOfMonth(currentMonth); @@ -246,6 +378,25 @@ export default function AgendamentoConsulta({ const handlePrevMonth = () => setCurrentMonth(subMonths(currentMonth, 1)); const handleNextMonth = () => setCurrentMonth(addMonths(currentMonth, 1)); + + const handleMonthChange = (monthIndex: number) => { + const newDate = new Date(currentMonth); + newDate.setMonth(monthIndex); + setCurrentMonth(newDate); + }; + + const handleYearChange = (year: number) => { + const newDate = new Date(currentMonth); + newDate.setFullYear(year); + setCurrentMonth(newDate); + }; + + // Gera lista de anos (ano atual até +10 anos) + const availableYears = Array.from( + { length: 11 }, + (_, i) => new Date().getFullYear() + i + ); + const handleSelectDoctor = (medico: Medico) => { setSelectedMedico(medico); setSelectedDate(undefined); @@ -253,6 +404,14 @@ export default function AgendamentoConsulta({ setMotivo(""); setBookingSuccess(false); setBookingError(""); + + // Scroll suave para a seção de detalhes + setTimeout(() => { + detailsRef.current?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }, 100); }; const handleBookAppointment = () => { if (selectedMedico && selectedDate && selectedTime && motivo) { @@ -273,96 +432,171 @@ export default function AgendamentoConsulta({ doctor_id: selectedMedico.id, scheduled_at: scheduledAt, duration_minutes: 30, - appointment_type: - (appointmentType === "online" ? "telemedicina" : "presencial") as "presencial" | "telemedicina", + appointment_type: (appointmentType === "online" + ? "telemedicina" + : "presencial") as "presencial" | "telemedicina", chief_complaint: motivo, }; - console.log("[AgendamentoConsulta] Criando agendamento com dados:", appointmentData); + console.log( + "[AgendamentoConsulta] Criando agendamento com dados:", + appointmentData + ); // Cria o agendamento usando a API REST const appointment = await appointmentService.create(appointmentData); - console.log("[AgendamentoConsulta] Consulta criada com sucesso:", appointment); - - setBookingSuccess(true); - setShowConfirmDialog(false); - - // Reset form após 3 segundos - setTimeout(() => { - setSelectedMedico(null); - setSelectedDate(undefined); - setSelectedTime(""); - setMotivo(""); - setBookingSuccess(false); - }, 3000); - } catch (error: any) { - console.error("[AgendamentoConsulta] Erro ao agendar:", { - error, - message: error?.message, - response: error?.response, - data: error?.response?.data, - }); - setBookingError( - error?.response?.data?.message || - error?.message || - "Erro ao agendar consulta. Tente novamente." + console.log( + "[AgendamentoConsulta] Consulta criada com sucesso:", + appointment ); + + // Mostra modal de sucesso + setResultType("success"); + setShowResultModal(true); + setShowConfirmDialog(false); + setBookingSuccess(true); + } catch (error: unknown) { + console.error("[AgendamentoConsulta] Erro ao agendar:", error); + + const errorMessage = + error instanceof Error + ? error.message + : "Erro ao agendar consulta. Tente novamente."; + + // Mostra modal de erro + setResultType("error"); + setShowResultModal(true); + setBookingError(errorMessage); setShowConfirmDialog(false); } }; const calendarDays = generateCalendarDays(); return ( -
- {bookingSuccess && ( -
- -
-

- Consulta agendada com sucesso! -

-

- Você receberá uma confirmação por e-mail em breve. -

+
+ {/* Modal de Resultado (Sucesso ou Erro) com Animação */} + {showResultModal && ( +
+
+
+ {/* Ícone com Animação Giratória (1 volta) */} +
+
+
+ {resultType === "success" ? ( + + ) : ( + + )} +
+
+ + {/* Mensagem */} +
+

+ {resultType === "success" + ? "Consulta Agendada!" + : "Erro no Agendamento"} +

+ {resultType === "success" ? ( +
+

+ Sua consulta foi agendada com sucesso. Você receberá uma + confirmação por e-mail ou SMS. +

+ +
+ ) : ( +

+ {bookingError || + "Não foi possível agendar a consulta. Tente novamente."} +

+ )} +
+ + {/* Botão OK */} + +
)} - {bookingError && ( -
- -

{bookingError}

-
- )} +
-

Agendar Consulta

-

+

Agendar Consulta

+

Escolha um médico e horário disponível

-
-
+
+
-
- + + handleMonthChange(Number(e.target.value)) + } + className="text-xs sm:text-sm font-semibold border border-gray-300 rounded-lg px-2 py-1 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500" + > + + + + + + + + + + + + + + + +
+
- {["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map( - (day) => ( -
- {day} -
- ) - )} + {["D", "S", "T", "Q", "Q", "S", "S"].map((day, idx) => ( +
+ {day} +
+ ))}
{calendarDays.map((day, index) => { const isCurrentMonth = isSameMonth(day, currentMonth); const isSelected = selectedDate && isSameDay(day, selectedDate); - const isTodayDate = isToday(day); - const isAvailable = - isCurrentMonth && isDateAvailable(day); const isPast = isBefore(day, startOfDay(new Date())); + + // Verifica se a data está no conjunto de datas disponíveis + const dateStr = format(day, "yyyy-MM-dd"); + const isAvailable = + isCurrentMonth && + !isPast && + availableDates.has(dateStr); + const isUnavailable = + isCurrentMonth && + !isPast && + !availableDates.has(dateStr); + + // Debug apenas para o primeiro dia do mês atual + if (index === 0 && isCurrentMonth) { + console.log( + "[AgendamentoConsulta] Debug calendário:", + { + totalDatasDisponiveis: availableDates.size, + primeiraData: dateStr, + diaDaSemana: day.getDay(), + isAvailable, + isUnavailable, + datas5Primeiras: Array.from(availableDates).slice( + 0, + 5 + ), + } + ); + } + return (
-
-

🟢 Datas disponíveis

-

🔴 Datas bloqueadas

+
+

+ {" "} + Datas disponíveis +

+

+ {" "} + Datas indisponíveis +

+

+ Datas passadas +

-
+
-
{selectedDate && availableSlots.length > 0 ? ( -
+
{availableSlots.map((slot) => ( ))}
) : selectedDate ? ( -
-

+

+

Nenhum horário disponível para esta data

) : ( -
-

+

+

Selecione uma data para ver os horários

)}
-