fix: corrige exibição de nomes de médicos em laudos e relatórios
- Adiciona resolução de IDs para nomes nos laudos do painel do paciente - Implementa dropdown de médicos nos formulários de relatórios - Corrige API PATCH para retornar dados atualizados (header Prefer) - Adiciona fallback para buscar relatório após update - Limpa cache de nomes ao atualizar relatórios - Trata dados legados (nomes diretos vs UUIDs) - Exibe 'Médico não cadastrado' para IDs inexistentes
This commit is contained in:
parent
3443e46ca3
commit
3a3e4c1f55
390
api-testing-results.md
Normal file
390
api-testing-results.md
Normal file
@ -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 <access_token>",
|
||||||
|
"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
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={userCpf}
|
||||||
|
onChange={(e) => 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_
|
||||||
@ -68,7 +68,7 @@ export default function AgendamentoConsulta({
|
|||||||
const [bookingSuccess, setBookingSuccess] = useState(false);
|
const [bookingSuccess, setBookingSuccess] = useState(false);
|
||||||
const [bookingError, setBookingError] = useState("");
|
const [bookingError, setBookingError] = useState("");
|
||||||
const [showResultModal, setShowResultModal] = useState(false);
|
const [showResultModal, setShowResultModal] = useState(false);
|
||||||
const [resultType, setResultType] = useState<'success' | 'error'>('success');
|
const [resultType, setResultType] = useState<"success" | "error">("success");
|
||||||
const [availableDates, setAvailableDates] = useState<Set<string>>(new Set());
|
const [availableDates, setAvailableDates] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
// Removido o carregamento interno de médicos, pois agora vem por prop
|
// Removido o carregamento interno de médicos, pois agora vem por prop
|
||||||
@ -103,10 +103,13 @@ export default function AgendamentoConsulta({
|
|||||||
try {
|
try {
|
||||||
const { availabilityService } = await import("../services");
|
const { availabilityService } = await import("../services");
|
||||||
|
|
||||||
console.log("[AgendamentoConsulta] Buscando disponibilidades para médico:", {
|
console.log(
|
||||||
id: selectedMedico.id,
|
"[AgendamentoConsulta] Buscando disponibilidades para médico:",
|
||||||
nome: selectedMedico.nome
|
{
|
||||||
});
|
id: selectedMedico.id,
|
||||||
|
nome: selectedMedico.nome,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Busca todas as disponibilidades ativas do médico
|
// Busca todas as disponibilidades ativas do médico
|
||||||
const availabilities = await availabilityService.list({
|
const availabilities = await availabilityService.list({
|
||||||
@ -114,52 +117,62 @@ export default function AgendamentoConsulta({
|
|||||||
active: true,
|
active: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[AgendamentoConsulta] Disponibilidades retornadas da API:", {
|
console.log(
|
||||||
count: availabilities?.length || 0,
|
"[AgendamentoConsulta] Disponibilidades retornadas da API:",
|
||||||
data: availabilities
|
{
|
||||||
});
|
count: availabilities?.length || 0,
|
||||||
|
data: availabilities,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (!availabilities || availabilities.length === 0) {
|
if (!availabilities || availabilities.length === 0) {
|
||||||
console.warn("[AgendamentoConsulta] Nenhuma disponibilidade encontrada para o médico");
|
console.warn(
|
||||||
|
"[AgendamentoConsulta] Nenhuma disponibilidade encontrada para o médico"
|
||||||
|
);
|
||||||
setAvailableDates(new Set());
|
setAvailableDates(new Set());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mapeamento de string para número (formato da API)
|
// Mapeamento de string para número (formato da API)
|
||||||
const weekdayMap: Record<string, number> = {
|
const weekdayMap: Record<string, number> = {
|
||||||
"sunday": 0,
|
sunday: 0,
|
||||||
"monday": 1,
|
monday: 1,
|
||||||
"tuesday": 2,
|
tuesday: 2,
|
||||||
"wednesday": 3,
|
wednesday: 3,
|
||||||
"thursday": 4,
|
thursday: 4,
|
||||||
"friday": 5,
|
friday: 5,
|
||||||
"saturday": 6
|
saturday: 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mapeia os dias da semana que o médico atende (converte para número)
|
// Mapeia os dias da semana que o médico atende (converte para número)
|
||||||
const availableWeekdays = new Set<number>(
|
const availableWeekdays = new Set<number>(
|
||||||
availabilities.map((avail) => {
|
availabilities
|
||||||
// weekday pode vir como número ou string da API
|
.map((avail) => {
|
||||||
let weekdayNum: number;
|
// weekday pode vir como número ou string da API
|
||||||
|
let weekdayNum: number;
|
||||||
|
|
||||||
if (typeof avail.weekday === 'number') {
|
if (typeof avail.weekday === "number") {
|
||||||
weekdayNum = avail.weekday;
|
weekdayNum = avail.weekday;
|
||||||
} else if (typeof avail.weekday === 'string') {
|
} else if (typeof avail.weekday === "string") {
|
||||||
weekdayNum = weekdayMap[avail.weekday.toLowerCase()] ?? -1;
|
weekdayNum = weekdayMap[avail.weekday.toLowerCase()] ?? -1;
|
||||||
} else {
|
} else {
|
||||||
weekdayNum = -1;
|
weekdayNum = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("[AgendamentoConsulta] Convertendo weekday:", {
|
console.log("[AgendamentoConsulta] Convertendo weekday:", {
|
||||||
original: avail.weekday,
|
original: avail.weekday,
|
||||||
type: typeof avail.weekday,
|
type: typeof avail.weekday,
|
||||||
converted: weekdayNum
|
converted: weekdayNum,
|
||||||
});
|
});
|
||||||
return weekdayNum;
|
return weekdayNum;
|
||||||
}).filter(day => day >= 0 && day <= 6) // Remove valores inválidos
|
})
|
||||||
|
.filter((day) => day >= 0 && day <= 6) // Remove valores inválidos
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("[AgendamentoConsulta] Dias da semana disponíveis (números):", Array.from(availableWeekdays));
|
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
|
// Calcula todas as datas do mês atual e próximos 2 meses que têm disponibilidade
|
||||||
const today = startOfDay(new Date());
|
const today = startOfDay(new Date());
|
||||||
@ -178,13 +191,15 @@ export default function AgendamentoConsulta({
|
|||||||
console.log("[AgendamentoConsulta] Resumo do cálculo:", {
|
console.log("[AgendamentoConsulta] Resumo do cálculo:", {
|
||||||
weekdaysDisponiveis: Array.from(availableWeekdays),
|
weekdaysDisponiveis: Array.from(availableWeekdays),
|
||||||
datasCalculadas: availableDatesSet.size,
|
datasCalculadas: availableDatesSet.size,
|
||||||
primeiras5Datas: Array.from(availableDatesSet).slice(0, 5)
|
primeiras5Datas: Array.from(availableDatesSet).slice(0, 5),
|
||||||
});
|
});
|
||||||
|
|
||||||
setAvailableDates(availableDatesSet);
|
setAvailableDates(availableDatesSet);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AgendamentoConsulta] Erro ao carregar disponibilidades:", error);
|
console.error(
|
||||||
|
"[AgendamentoConsulta] Erro ao carregar disponibilidades:",
|
||||||
|
error
|
||||||
|
);
|
||||||
setAvailableDates(new Set());
|
setAvailableDates(new Set());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -221,10 +236,15 @@ export default function AgendamentoConsulta({
|
|||||||
active: true,
|
active: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[AgendamentoConsulta] Disponibilidades do médico:", availabilities);
|
console.log(
|
||||||
|
"[AgendamentoConsulta] Disponibilidades do médico:",
|
||||||
|
availabilities
|
||||||
|
);
|
||||||
|
|
||||||
if (!availabilities || availabilities.length === 0) {
|
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([]);
|
setAvailableSlots([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -241,17 +261,25 @@ export default function AgendamentoConsulta({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dayOfWeek = weekdayMap[selectedDate.getDay()];
|
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
|
// Filtra disponibilidades para o dia da semana
|
||||||
const dayAvailability = availabilities.filter(
|
const dayAvailability = availabilities.filter(
|
||||||
(avail) => avail.weekday === dayOfWeek && avail.active
|
(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) {
|
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([]);
|
setAvailableSlots([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -273,7 +301,9 @@ export default function AgendamentoConsulta({
|
|||||||
while (currentMinutes < endMinutes) {
|
while (currentMinutes < endMinutes) {
|
||||||
const hours = Math.floor(currentMinutes / 60);
|
const hours = Math.floor(currentMinutes / 60);
|
||||||
const minutes = 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);
|
allSlots.push(timeStr);
|
||||||
currentMinutes += slotMinutes;
|
currentMinutes += slotMinutes;
|
||||||
}
|
}
|
||||||
@ -284,7 +314,10 @@ export default function AgendamentoConsulta({
|
|||||||
doctor_id: selectedMedico.id,
|
doctor_id: selectedMedico.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[AgendamentoConsulta] Agendamentos existentes:", appointments);
|
console.log(
|
||||||
|
"[AgendamentoConsulta] Agendamentos existentes:",
|
||||||
|
appointments
|
||||||
|
);
|
||||||
|
|
||||||
// Filtra agendamentos para a data selecionada
|
// Filtra agendamentos para a data selecionada
|
||||||
const bookedSlots = appointments
|
const bookedSlots = appointments
|
||||||
@ -309,7 +342,10 @@ export default function AgendamentoConsulta({
|
|||||||
(slot) => !bookedSlots.includes(slot)
|
(slot) => !bookedSlots.includes(slot)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("[AgendamentoConsulta] Slots disponíveis calculados:", availableSlots);
|
console.log(
|
||||||
|
"[AgendamentoConsulta] Slots disponíveis calculados:",
|
||||||
|
availableSlots
|
||||||
|
);
|
||||||
|
|
||||||
setAvailableSlots(availableSlots);
|
setAvailableSlots(availableSlots);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -356,7 +392,10 @@ export default function AgendamentoConsulta({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Gera lista de anos (ano atual até +10 anos)
|
// Gera lista de anos (ano atual até +10 anos)
|
||||||
const availableYears = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() + i);
|
const availableYears = Array.from(
|
||||||
|
{ length: 11 },
|
||||||
|
(_, i) => new Date().getFullYear() + i
|
||||||
|
);
|
||||||
|
|
||||||
const handleSelectDoctor = (medico: Medico) => {
|
const handleSelectDoctor = (medico: Medico) => {
|
||||||
setSelectedMedico(medico);
|
setSelectedMedico(medico);
|
||||||
@ -369,8 +408,8 @@ export default function AgendamentoConsulta({
|
|||||||
// Scroll suave para a seção de detalhes
|
// Scroll suave para a seção de detalhes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
detailsRef.current?.scrollIntoView({
|
detailsRef.current?.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: "smooth",
|
||||||
block: 'start'
|
block: "start",
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
@ -393,32 +432,40 @@ export default function AgendamentoConsulta({
|
|||||||
doctor_id: selectedMedico.id,
|
doctor_id: selectedMedico.id,
|
||||||
scheduled_at: scheduledAt,
|
scheduled_at: scheduledAt,
|
||||||
duration_minutes: 30,
|
duration_minutes: 30,
|
||||||
appointment_type:
|
appointment_type: (appointmentType === "online"
|
||||||
(appointmentType === "online" ? "telemedicina" : "presencial") as "presencial" | "telemedicina",
|
? "telemedicina"
|
||||||
|
: "presencial") as "presencial" | "telemedicina",
|
||||||
chief_complaint: motivo,
|
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
|
// Cria o agendamento usando a API REST
|
||||||
const appointment = await appointmentService.create(appointmentData);
|
const appointment = await appointmentService.create(appointmentData);
|
||||||
|
|
||||||
console.log("[AgendamentoConsulta] Consulta criada com sucesso:", appointment);
|
console.log(
|
||||||
|
"[AgendamentoConsulta] Consulta criada com sucesso:",
|
||||||
|
appointment
|
||||||
|
);
|
||||||
|
|
||||||
// Mostra modal de sucesso
|
// Mostra modal de sucesso
|
||||||
setResultType('success');
|
setResultType("success");
|
||||||
setShowResultModal(true);
|
setShowResultModal(true);
|
||||||
setShowConfirmDialog(false);
|
setShowConfirmDialog(false);
|
||||||
setBookingSuccess(true);
|
setBookingSuccess(true);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error("[AgendamentoConsulta] Erro ao agendar:", error);
|
console.error("[AgendamentoConsulta] Erro ao agendar:", error);
|
||||||
|
|
||||||
const errorMessage = error instanceof Error
|
const errorMessage =
|
||||||
? error.message
|
error instanceof Error
|
||||||
: "Erro ao agendar consulta. Tente novamente.";
|
? error.message
|
||||||
|
: "Erro ao agendar consulta. Tente novamente.";
|
||||||
|
|
||||||
// Mostra modal de erro
|
// Mostra modal de erro
|
||||||
setResultType('error');
|
setResultType("error");
|
||||||
setShowResultModal(true);
|
setShowResultModal(true);
|
||||||
setBookingError(errorMessage);
|
setBookingError(errorMessage);
|
||||||
setShowConfirmDialog(false);
|
setShowConfirmDialog(false);
|
||||||
@ -435,13 +482,17 @@ export default function AgendamentoConsulta({
|
|||||||
<div className="flex flex-col items-center text-center space-y-4">
|
<div className="flex flex-col items-center text-center space-y-4">
|
||||||
{/* Ícone com Animação Giratória (1 volta) */}
|
{/* Ícone com Animação Giratória (1 volta) */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className={`absolute inset-0 rounded-full animate-pulse-ring ${
|
<div
|
||||||
resultType === 'success' ? 'bg-blue-100' : 'bg-red-100'
|
className={`absolute inset-0 rounded-full animate-pulse-ring ${
|
||||||
}`}></div>
|
resultType === "success" ? "bg-blue-100" : "bg-red-100"
|
||||||
<div className={`relative rounded-full p-4 sm:p-5 ${
|
}`}
|
||||||
resultType === 'success' ? 'bg-blue-500' : 'bg-red-500'
|
></div>
|
||||||
}`}>
|
<div
|
||||||
{resultType === 'success' ? (
|
className={`relative rounded-full p-4 sm:p-5 ${
|
||||||
|
resultType === "success" ? "bg-blue-500" : "bg-red-500"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{resultType === "success" ? (
|
||||||
<CheckCircle2 className="h-12 w-12 sm:h-16 sm:w-16 text-white animate-spin-once" />
|
<CheckCircle2 className="h-12 w-12 sm:h-16 sm:w-16 text-white animate-spin-once" />
|
||||||
) : (
|
) : (
|
||||||
<AlertCircle className="h-12 w-12 sm:h-16 sm:w-16 text-white animate-spin-once" />
|
<AlertCircle className="h-12 w-12 sm:h-16 sm:w-16 text-white animate-spin-once" />
|
||||||
@ -451,22 +502,29 @@ export default function AgendamentoConsulta({
|
|||||||
|
|
||||||
{/* Mensagem */}
|
{/* Mensagem */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className={`text-xl sm:text-2xl font-bold ${
|
<h3
|
||||||
resultType === 'success' ? 'text-blue-900' : 'text-red-900'
|
className={`text-xl sm:text-2xl font-bold ${
|
||||||
}`}>
|
resultType === "success" ? "text-blue-900" : "text-red-900"
|
||||||
{resultType === 'success' ? 'Consulta Agendada!' : 'Erro no Agendamento'}
|
}`}
|
||||||
|
>
|
||||||
|
{resultType === "success"
|
||||||
|
? "Consulta Agendada!"
|
||||||
|
: "Erro no Agendamento"}
|
||||||
</h3>
|
</h3>
|
||||||
{resultType === 'success' ? (
|
{resultType === "success" ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm sm:text-base text-gray-600">
|
<p className="text-sm sm:text-base text-gray-600">
|
||||||
Sua consulta foi agendada com sucesso. Você receberá uma confirmação por e-mail ou SMS.
|
Sua consulta foi agendada com sucesso. Você receberá uma
|
||||||
|
confirmação por e-mail ou SMS.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowResultModal(false);
|
setShowResultModal(false);
|
||||||
setBookingSuccess(false);
|
setBookingSuccess(false);
|
||||||
setBookingError('');
|
setBookingError("");
|
||||||
navigate('/acompanhamento', { state: { activeTab: 'consultas' } });
|
navigate("/acompanhamento", {
|
||||||
|
state: { activeTab: "consultas" },
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="text-blue-600 hover:text-blue-800 underline text-sm sm:text-base font-medium"
|
className="text-blue-600 hover:text-blue-800 underline text-sm sm:text-base font-medium"
|
||||||
>
|
>
|
||||||
@ -475,7 +533,8 @@ export default function AgendamentoConsulta({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm sm:text-base text-gray-600">
|
<p className="text-sm sm:text-base text-gray-600">
|
||||||
{bookingError || 'Não foi possível agendar a consulta. Tente novamente.'}
|
{bookingError ||
|
||||||
|
"Não foi possível agendar a consulta. Tente novamente."}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -485,9 +544,9 @@ export default function AgendamentoConsulta({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowResultModal(false);
|
setShowResultModal(false);
|
||||||
setBookingSuccess(false);
|
setBookingSuccess(false);
|
||||||
setBookingError('');
|
setBookingError("");
|
||||||
// Limpa o formulário se for sucesso
|
// Limpa o formulário se for sucesso
|
||||||
if (resultType === 'success') {
|
if (resultType === "success") {
|
||||||
setSelectedMedico(null);
|
setSelectedMedico(null);
|
||||||
setSelectedDate(undefined);
|
setSelectedDate(undefined);
|
||||||
setSelectedTime("");
|
setSelectedTime("");
|
||||||
@ -495,9 +554,9 @@ export default function AgendamentoConsulta({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`w-full font-semibold py-3 px-6 rounded-lg transition-colors ${
|
className={`w-full font-semibold py-3 px-6 rounded-lg transition-colors ${
|
||||||
resultType === 'success'
|
resultType === "success"
|
||||||
? 'bg-blue-500 hover:bg-blue-600 text-white'
|
? "bg-blue-500 hover:bg-blue-600 text-white"
|
||||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
: "bg-red-500 hover:bg-red-600 text-white"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
OK, Entendi!
|
OK, Entendi!
|
||||||
@ -531,7 +590,9 @@ export default function AgendamentoConsulta({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs sm:text-sm font-medium">Especialidade</label>
|
<label className="text-xs sm:text-sm font-medium">
|
||||||
|
Especialidade
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedSpecialty}
|
value={selectedSpecialty}
|
||||||
onChange={(e) => setSelectedSpecialty(e.target.value)}
|
onChange={(e) => setSelectedSpecialty(e.target.value)}
|
||||||
@ -552,7 +613,9 @@ export default function AgendamentoConsulta({
|
|||||||
<div
|
<div
|
||||||
key={medico.id}
|
key={medico.id}
|
||||||
className={`bg-white rounded-lg sm:rounded-xl border p-4 sm:p-6 flex flex-col sm:flex-row gap-3 sm:gap-4 items-start sm:items-center ${
|
className={`bg-white rounded-lg sm:rounded-xl border p-4 sm:p-6 flex flex-col sm:flex-row gap-3 sm:gap-4 items-start sm:items-center ${
|
||||||
selectedMedico?.id === medico.id ? "border-blue-500 bg-blue-50" : ""
|
selectedMedico?.id === medico.id
|
||||||
|
? "border-blue-500 bg-blue-50"
|
||||||
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="h-12 w-12 sm:h-16 sm:w-16 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-base sm:text-xl font-bold text-white flex-shrink-0">
|
<div className="h-12 w-12 sm:h-16 sm:w-16 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-base sm:text-xl font-bold text-white flex-shrink-0">
|
||||||
@ -565,17 +628,25 @@ export default function AgendamentoConsulta({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0 space-y-1.5 sm:space-y-2 w-full">
|
<div className="flex-1 min-w-0 space-y-1.5 sm:space-y-2 w-full">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm sm:text-base font-semibold truncate">{medico.nome}</h3>
|
<h3 className="text-sm sm:text-base font-semibold truncate">
|
||||||
<p className="text-xs sm:text-sm text-muted-foreground truncate">{medico.especialidade}</p>
|
{medico.nome}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs sm:text-sm text-muted-foreground truncate">
|
||||||
|
{medico.especialidade}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2 sm:gap-4 text-xs sm:text-sm text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-2 sm:gap-4 text-xs sm:text-sm text-muted-foreground">
|
||||||
<span className="truncate">CRM: {medico.crm}</span>
|
<span className="truncate">CRM: {medico.crm}</span>
|
||||||
{medico.valorConsulta ? (
|
{medico.valorConsulta ? (
|
||||||
<span className="whitespace-nowrap">R$ {medico.valorConsulta.toFixed(2)}</span>
|
<span className="whitespace-nowrap">
|
||||||
|
R$ {medico.valorConsulta.toFixed(2)}
|
||||||
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
||||||
<span className="text-xs sm:text-sm text-foreground truncate w-full sm:w-auto">{medico.email || "-"}</span>
|
<span className="text-xs sm:text-sm text-foreground truncate w-full sm:w-auto">
|
||||||
|
{medico.email || "-"}
|
||||||
|
</span>
|
||||||
<div className="flex gap-2 w-full sm:w-auto">
|
<div className="flex gap-2 w-full sm:w-auto">
|
||||||
<button
|
<button
|
||||||
className="flex-1 sm:flex-none px-3 py-1.5 sm:py-1 rounded-lg border text-xs sm:text-sm hover:bg-blue-50 transition-colors whitespace-nowrap"
|
className="flex-1 sm:flex-none px-3 py-1.5 sm:py-1 rounded-lg border text-xs sm:text-sm hover:bg-blue-50 transition-colors whitespace-nowrap"
|
||||||
@ -592,9 +663,14 @@ export default function AgendamentoConsulta({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{selectedMedico && (
|
{selectedMedico && (
|
||||||
<div ref={detailsRef} className="bg-white rounded-lg shadow p-4 sm:p-6 space-y-4 sm:space-y-6">
|
<div
|
||||||
|
ref={detailsRef}
|
||||||
|
className="bg-white rounded-lg shadow p-4 sm:p-6 space-y-4 sm:space-y-6"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg sm:text-xl font-semibold truncate">Detalhes do Agendamento</h2>
|
<h2 className="text-lg sm:text-xl font-semibold truncate">
|
||||||
|
Detalhes do Agendamento
|
||||||
|
</h2>
|
||||||
<p className="text-sm sm:text-base text-gray-600 truncate">
|
<p className="text-sm sm:text-base text-gray-600 truncate">
|
||||||
Consulta com {selectedMedico.nome} -{" "}
|
Consulta com {selectedMedico.nome} -{" "}
|
||||||
{selectedMedico.especialidade}
|
{selectedMedico.especialidade}
|
||||||
@ -610,7 +686,9 @@ export default function AgendamentoConsulta({
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<MapPin className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
|
<MapPin className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
|
||||||
<span className="text-sm sm:text-base font-medium">Presencial</span>
|
<span className="text-sm sm:text-base font-medium">
|
||||||
|
Presencial
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setAppointmentType("online")}
|
onClick={() => setAppointmentType("online")}
|
||||||
@ -627,7 +705,9 @@ export default function AgendamentoConsulta({
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div className="space-y-3 sm:space-y-4">
|
<div className="space-y-3 sm:space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-xs sm:text-sm font-medium">Selecione a Data</label>
|
<label className="text-xs sm:text-sm font-medium">
|
||||||
|
Selecione a Data
|
||||||
|
</label>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<div className="flex items-center justify-between gap-2 mb-3 sm:mb-4">
|
<div className="flex items-center justify-between gap-2 mb-3 sm:mb-4">
|
||||||
<button
|
<button
|
||||||
@ -641,7 +721,9 @@ export default function AgendamentoConsulta({
|
|||||||
<div className="flex items-center gap-2 flex-1 justify-center">
|
<div className="flex items-center gap-2 flex-1 justify-center">
|
||||||
<select
|
<select
|
||||||
value={currentMonth.getMonth()}
|
value={currentMonth.getMonth()}
|
||||||
onChange={(e) => handleMonthChange(Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
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"
|
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"
|
||||||
>
|
>
|
||||||
<option value={0}>Janeiro</option>
|
<option value={0}>Janeiro</option>
|
||||||
@ -660,7 +742,9 @@ export default function AgendamentoConsulta({
|
|||||||
|
|
||||||
<select
|
<select
|
||||||
value={currentMonth.getFullYear()}
|
value={currentMonth.getFullYear()}
|
||||||
onChange={(e) => handleYearChange(Number(e.target.value))}
|
onChange={(e) =>
|
||||||
|
handleYearChange(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"
|
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"
|
||||||
>
|
>
|
||||||
{availableYears.map((year) => (
|
{availableYears.map((year) => (
|
||||||
@ -681,16 +765,14 @@ export default function AgendamentoConsulta({
|
|||||||
</div>
|
</div>
|
||||||
<div className="border rounded-lg overflow-hidden">
|
<div className="border rounded-lg overflow-hidden">
|
||||||
<div className="grid grid-cols-7 bg-gray-50">
|
<div className="grid grid-cols-7 bg-gray-50">
|
||||||
{["D", "S", "T", "Q", "Q", "S", "S"].map(
|
{["D", "S", "T", "Q", "Q", "S", "S"].map((day, idx) => (
|
||||||
(day, idx) => (
|
<div
|
||||||
<div
|
key={idx}
|
||||||
key={idx}
|
className="text-center py-1.5 sm:py-2 text-xs sm:text-sm font-medium text-gray-600"
|
||||||
className="text-center py-1.5 sm:py-2 text-xs sm:text-sm font-medium text-gray-600"
|
>
|
||||||
>
|
{day}
|
||||||
{day}
|
</div>
|
||||||
</div>
|
))}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-7">
|
<div className="grid grid-cols-7">
|
||||||
{calendarDays.map((day, index) => {
|
{calendarDays.map((day, index) => {
|
||||||
@ -701,19 +783,31 @@ export default function AgendamentoConsulta({
|
|||||||
|
|
||||||
// Verifica se a data está no conjunto de datas disponíveis
|
// Verifica se a data está no conjunto de datas disponíveis
|
||||||
const dateStr = format(day, "yyyy-MM-dd");
|
const dateStr = format(day, "yyyy-MM-dd");
|
||||||
const isAvailable = isCurrentMonth && !isPast && availableDates.has(dateStr);
|
const isAvailable =
|
||||||
const isUnavailable = isCurrentMonth && !isPast && !availableDates.has(dateStr);
|
isCurrentMonth &&
|
||||||
|
!isPast &&
|
||||||
|
availableDates.has(dateStr);
|
||||||
|
const isUnavailable =
|
||||||
|
isCurrentMonth &&
|
||||||
|
!isPast &&
|
||||||
|
!availableDates.has(dateStr);
|
||||||
|
|
||||||
// Debug apenas para o primeiro dia do mês atual
|
// Debug apenas para o primeiro dia do mês atual
|
||||||
if (index === 0 && isCurrentMonth) {
|
if (index === 0 && isCurrentMonth) {
|
||||||
console.log("[AgendamentoConsulta] Debug calendário:", {
|
console.log(
|
||||||
totalDatasDisponiveis: availableDates.size,
|
"[AgendamentoConsulta] Debug calendário:",
|
||||||
primeiraData: dateStr,
|
{
|
||||||
diaDaSemana: day.getDay(),
|
totalDatasDisponiveis: availableDates.size,
|
||||||
isAvailable,
|
primeiraData: dateStr,
|
||||||
isUnavailable,
|
diaDaSemana: day.getDay(),
|
||||||
datas5Primeiras: Array.from(availableDates).slice(0, 5)
|
isAvailable,
|
||||||
});
|
isUnavailable,
|
||||||
|
datas5Primeiras: Array.from(availableDates).slice(
|
||||||
|
0,
|
||||||
|
5
|
||||||
|
),
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -748,9 +842,17 @@ export default function AgendamentoConsulta({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 sm:mt-3 space-y-0.5 sm:space-y-1 text-xs text-gray-600">
|
<div className="mt-2 sm:mt-3 space-y-0.5 sm:space-y-1 text-xs text-gray-600">
|
||||||
<p><span className="text-blue-600 font-semibold">●</span> Datas disponíveis</p>
|
<p>
|
||||||
<p><span className="text-red-600 font-semibold">●</span> Datas indisponíveis</p>
|
<span className="text-blue-600 font-semibold">●</span>{" "}
|
||||||
<p><span className="text-gray-400">●</span> Datas passadas</p>
|
Datas disponíveis
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-red-600 font-semibold">●</span>{" "}
|
||||||
|
Datas indisponíveis
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-gray-400">●</span> Datas passadas
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -846,7 +948,9 @@ export default function AgendamentoConsulta({
|
|||||||
{showConfirmDialog && (
|
{showConfirmDialog && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-4 sm:p-6 space-y-3 sm:space-y-4 max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-4 sm:p-6 space-y-3 sm:space-y-4 max-h-[90vh] overflow-y-auto">
|
||||||
<h3 className="text-lg sm:text-xl font-semibold">Confirmar Agendamento</h3>
|
<h3 className="text-lg sm:text-xl font-semibold">
|
||||||
|
Confirmar Agendamento
|
||||||
|
</h3>
|
||||||
<p className="text-sm sm:text-base text-gray-600">
|
<p className="text-sm sm:text-base text-gray-600">
|
||||||
Revise os detalhes da sua consulta antes de confirmar
|
Revise os detalhes da sua consulta antes de confirmar
|
||||||
</p>
|
</p>
|
||||||
@ -908,5 +1012,3 @@ export default function AgendamentoConsulta({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -171,7 +171,9 @@ const Chatbot: React.FC<ChatbotProps> = ({ className = "" }) => {
|
|||||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="font-medium hidden sm:inline">Precisa de ajuda?</span>
|
<span className="font-medium hidden sm:inline">
|
||||||
|
Precisa de ajuda?
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -279,7 +279,9 @@ const DisponibilidadeMedico: React.FC = () => {
|
|||||||
doctor_id: doctorId,
|
doctor_id: doctorId,
|
||||||
date: exceptionForm.date,
|
date: exceptionForm.date,
|
||||||
kind: exceptionForm.kind,
|
kind: exceptionForm.kind,
|
||||||
start_time: exceptionForm.wholeDayBlock ? null : exceptionForm.start_time,
|
start_time: exceptionForm.wholeDayBlock
|
||||||
|
? null
|
||||||
|
: exceptionForm.start_time,
|
||||||
end_time: exceptionForm.wholeDayBlock ? null : exceptionForm.end_time,
|
end_time: exceptionForm.wholeDayBlock ? null : exceptionForm.end_time,
|
||||||
reason: exceptionForm.reason || null,
|
reason: exceptionForm.reason || null,
|
||||||
created_by: user?.id || doctorId,
|
created_by: user?.id || doctorId,
|
||||||
@ -462,7 +464,10 @@ const DisponibilidadeMedico: React.FC = () => {
|
|||||||
type="date"
|
type="date"
|
||||||
value={exceptionForm.date}
|
value={exceptionForm.date}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setExceptionForm({ ...exceptionForm, date: e.target.value })
|
setExceptionForm({
|
||||||
|
...exceptionForm,
|
||||||
|
date: e.target.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
className="form-input w-full"
|
className="form-input w-full"
|
||||||
/>
|
/>
|
||||||
@ -476,7 +481,9 @@ const DisponibilidadeMedico: React.FC = () => {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setExceptionForm({
|
setExceptionForm({
|
||||||
...exceptionForm,
|
...exceptionForm,
|
||||||
kind: e.target.value as "bloqueio" | "disponibilidade_extra",
|
kind: e.target.value as
|
||||||
|
| "bloqueio"
|
||||||
|
| "disponibilidade_extra",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="form-input w-full"
|
className="form-input w-full"
|
||||||
@ -554,7 +561,10 @@ const DisponibilidadeMedico: React.FC = () => {
|
|||||||
<textarea
|
<textarea
|
||||||
value={exceptionForm.reason}
|
value={exceptionForm.reason}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setExceptionForm({ ...exceptionForm, reason: e.target.value })
|
setExceptionForm({
|
||||||
|
...exceptionForm,
|
||||||
|
reason: e.target.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
placeholder="Ex: Férias, Feriado, Plantão extra..."
|
placeholder="Ex: Férias, Feriado, Plantão extra..."
|
||||||
className="form-input w-full"
|
className="form-input w-full"
|
||||||
@ -617,7 +627,9 @@ const DisponibilidadeMedico: React.FC = () => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!exception.id) return;
|
if (!exception.id) return;
|
||||||
try {
|
try {
|
||||||
await availabilityService.deleteException(exception.id);
|
await availabilityService.deleteException(
|
||||||
|
exception.id
|
||||||
|
);
|
||||||
toast.success("Exceção removida");
|
toast.success("Exceção removida");
|
||||||
loadExceptions();
|
loadExceptions();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -681,7 +693,10 @@ const DisponibilidadeMedico: React.FC = () => {
|
|||||||
<select
|
<select
|
||||||
value={newSlot.slotMinutes}
|
value={newSlot.slotMinutes}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setNewSlot({ ...newSlot, slotMinutes: parseInt(e.target.value) })
|
setNewSlot({
|
||||||
|
...newSlot,
|
||||||
|
slotMinutes: parseInt(e.target.value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
className="form-input w-full"
|
className="form-input w-full"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -34,10 +34,15 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
|||||||
active: true,
|
active: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("📅 [AvailableSlotsPicker] Disponibilidades:", availabilities);
|
console.log(
|
||||||
|
"📅 [AvailableSlotsPicker] Disponibilidades:",
|
||||||
|
availabilities
|
||||||
|
);
|
||||||
|
|
||||||
if (!availabilities || availabilities.length === 0) {
|
if (!availabilities || availabilities.length === 0) {
|
||||||
console.warn("[AvailableSlotsPicker] Nenhuma disponibilidade configurada");
|
console.warn(
|
||||||
|
"[AvailableSlotsPicker] Nenhuma disponibilidade configurada"
|
||||||
|
);
|
||||||
setSlots([]);
|
setSlots([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@ -54,10 +59,15 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
|||||||
(avail) => avail.weekday === dayOfWeek && avail.active
|
(avail) => avail.weekday === dayOfWeek && avail.active
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("[AvailableSlotsPicker] Disponibilidades para o dia:", dayAvailability);
|
console.log(
|
||||||
|
"[AvailableSlotsPicker] Disponibilidades para o dia:",
|
||||||
|
dayAvailability
|
||||||
|
);
|
||||||
|
|
||||||
if (dayAvailability.length === 0) {
|
if (dayAvailability.length === 0) {
|
||||||
console.warn("[AvailableSlotsPicker] Médico não atende neste dia da semana");
|
console.warn(
|
||||||
|
"[AvailableSlotsPicker] Médico não atende neste dia da semana"
|
||||||
|
);
|
||||||
setSlots([]);
|
setSlots([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
@ -80,7 +90,9 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
|||||||
while (currentMinutes < endMinutes) {
|
while (currentMinutes < endMinutes) {
|
||||||
const hours = Math.floor(currentMinutes / 60);
|
const hours = Math.floor(currentMinutes / 60);
|
||||||
const minutes = 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);
|
allSlots.push(timeStr);
|
||||||
currentMinutes += slotMinutes;
|
currentMinutes += slotMinutes;
|
||||||
}
|
}
|
||||||
@ -91,7 +103,10 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
|||||||
doctor_id: doctorId,
|
doctor_id: doctorId,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[AvailableSlotsPicker] Agendamentos existentes:", appointments);
|
console.log(
|
||||||
|
"[AvailableSlotsPicker] Agendamentos existentes:",
|
||||||
|
appointments
|
||||||
|
);
|
||||||
|
|
||||||
// Filtra agendamentos para a data selecionada
|
// Filtra agendamentos para a data selecionada
|
||||||
const bookedSlots = (Array.isArray(appointments) ? appointments : [])
|
const bookedSlots = (Array.isArray(appointments) ? appointments : [])
|
||||||
@ -109,15 +124,22 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
|||||||
return format(aptDate, "HH:mm");
|
return format(aptDate, "HH:mm");
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[AvailableSlotsPicker] Horários já ocupados:", bookedSlots);
|
console.log(
|
||||||
|
"[AvailableSlotsPicker] Horários já ocupados:",
|
||||||
|
bookedSlots
|
||||||
|
);
|
||||||
|
|
||||||
// Remove slots já ocupados
|
// Remove slots já ocupados
|
||||||
const availableSlots = allSlots.filter((slot) => !bookedSlots.includes(slot));
|
const availableSlots = allSlots.filter(
|
||||||
|
(slot) => !bookedSlots.includes(slot)
|
||||||
|
);
|
||||||
|
|
||||||
console.log("✅ [AvailableSlotsPicker] Horários disponíveis:", availableSlots);
|
console.log(
|
||||||
|
"✅ [AvailableSlotsPicker] Horários disponíveis:",
|
||||||
|
availableSlots
|
||||||
|
);
|
||||||
setSlots(availableSlots);
|
setSlots(availableSlots);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ [AvailableSlotsPicker] Erro ao buscar slots:", error);
|
console.error("❌ [AvailableSlotsPicker] Erro ao buscar slots:", error);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@ -1,6 +1,16 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isBefore, startOfDay, addMonths, subMonths, getDay } from "date-fns";
|
import {
|
||||||
|
format,
|
||||||
|
startOfMonth,
|
||||||
|
endOfMonth,
|
||||||
|
eachDayOfInterval,
|
||||||
|
isBefore,
|
||||||
|
startOfDay,
|
||||||
|
addMonths,
|
||||||
|
subMonths,
|
||||||
|
getDay,
|
||||||
|
} from "date-fns";
|
||||||
import { ptBR } from "date-fns/locale";
|
import { ptBR } from "date-fns/locale";
|
||||||
import { availabilityService, appointmentService } from "../../services";
|
import { availabilityService, appointmentService } from "../../services";
|
||||||
import type { DoctorAvailability, DoctorException } from "../../services";
|
import type { DoctorAvailability, DoctorException } from "../../services";
|
||||||
@ -19,12 +29,20 @@ interface DayStatus {
|
|||||||
isPast: boolean; // Data já passou
|
isPast: boolean; // Data já passou
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: CalendarPickerProps) {
|
export function CalendarPicker({
|
||||||
|
doctorId,
|
||||||
|
selectedDate,
|
||||||
|
onSelectDate,
|
||||||
|
}: CalendarPickerProps) {
|
||||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>([]);
|
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
|
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [availableSlots, setAvailableSlots] = useState<Record<string, boolean>>({});
|
const [availableSlots, setAvailableSlots] = useState<Record<string, boolean>>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
// Carregar disponibilidades e exceções do médico
|
// Carregar disponibilidades e exceções do médico
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -72,7 +90,9 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
|||||||
// Buscar todos os agendamentos do médico uma vez só
|
// Buscar todos os agendamentos do médico uma vez só
|
||||||
let allAppointments: Array<{ scheduled_at: string; status: string }> = [];
|
let allAppointments: Array<{ scheduled_at: string; status: string }> = [];
|
||||||
try {
|
try {
|
||||||
const appointments = await appointmentService.list({ doctor_id: doctorId });
|
const appointments = await appointmentService.list({
|
||||||
|
doctor_id: doctorId,
|
||||||
|
});
|
||||||
allAppointments = Array.isArray(appointments) ? appointments : [];
|
allAppointments = Array.isArray(appointments) ? appointments : [];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[CalendarPicker] Erro ao buscar agendamentos:", error);
|
console.error("[CalendarPicker] Erro ao buscar agendamentos:", error);
|
||||||
@ -120,7 +140,9 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
|||||||
while (currentMinutes < endMinutes) {
|
while (currentMinutes < endMinutes) {
|
||||||
const hours = Math.floor(currentMinutes / 60);
|
const hours = Math.floor(currentMinutes / 60);
|
||||||
const minutes = 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);
|
allSlots.push(timeStr);
|
||||||
currentMinutes += slotMinutes;
|
currentMinutes += slotMinutes;
|
||||||
}
|
}
|
||||||
@ -143,11 +165,18 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Verifica se há pelo menos um slot disponível
|
// Verifica se há pelo menos um slot disponível
|
||||||
const availableSlots = allSlots.filter((slot) => !bookedSlots.includes(slot));
|
const availableSlots = allSlots.filter(
|
||||||
|
(slot) => !bookedSlots.includes(slot)
|
||||||
|
);
|
||||||
slotsMap[dateStr] = availableSlots.length > 0;
|
slotsMap[dateStr] = availableSlots.length > 0;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[CalendarPicker] Erro ao verificar slots para ${format(day, "yyyy-MM-dd")}:`, error);
|
console.error(
|
||||||
|
`[CalendarPicker] Erro ao verificar slots para ${format(
|
||||||
|
day,
|
||||||
|
"yyyy-MM-dd"
|
||||||
|
)}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
slotsMap[format(day, "yyyy-MM-dd")] = false;
|
slotsMap[format(day, "yyyy-MM-dd")] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,7 +214,8 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getDayClasses = (status: DayStatus, isSelected: boolean): string => {
|
const getDayClasses = (status: DayStatus, isSelected: boolean): string => {
|
||||||
const base = "w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors";
|
const base =
|
||||||
|
"w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors";
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
return `${base} bg-blue-600 text-white ring-2 ring-blue-400`;
|
return `${base} bg-blue-600 text-white ring-2 ring-blue-400`;
|
||||||
@ -241,7 +271,10 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
|||||||
<div className="grid grid-cols-7 gap-1">
|
<div className="grid grid-cols-7 gap-1">
|
||||||
{/* Cabeçalho dos dias da semana */}
|
{/* Cabeçalho dos dias da semana */}
|
||||||
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
|
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
|
||||||
<div key={day} className="text-center text-xs font-semibold text-gray-600 py-2">
|
<div
|
||||||
|
key={day}
|
||||||
|
className="text-center text-xs font-semibold text-gray-600 py-2"
|
||||||
|
>
|
||||||
{day}
|
{day}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -257,11 +290,18 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
|||||||
const classes = getDayClasses(status, isSelected);
|
const classes = getDayClasses(status, isSelected);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={format(day, "yyyy-MM-dd")} className="flex justify-center">
|
<div
|
||||||
|
key={format(day, "yyyy-MM-dd")}
|
||||||
|
className="flex justify-center"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleDayClick(day, status)}
|
onClick={() => handleDayClick(day, status)}
|
||||||
disabled={status.isPast || status.hasBlockException || (!status.hasAvailability && !status.available)}
|
disabled={
|
||||||
|
status.isPast ||
|
||||||
|
status.hasBlockException ||
|
||||||
|
(!status.hasAvailability && !status.available)
|
||||||
|
}
|
||||||
className={classes}
|
className={classes}
|
||||||
title={
|
title={
|
||||||
status.isPast
|
status.isPast
|
||||||
|
|||||||
@ -101,8 +101,11 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
|||||||
// Convert ISO to date and time
|
// Convert ISO to date and time
|
||||||
try {
|
try {
|
||||||
const d = new Date(editing.scheduled_at);
|
const d = new Date(editing.scheduled_at);
|
||||||
const dateStr = d.toISOString().split('T')[0]; // YYYY-MM-DD
|
const dateStr = d.toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
const timeStr = `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`;
|
const timeStr = `${d.getHours().toString().padStart(2, "0")}:${d
|
||||||
|
.getMinutes()
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
setSelectedDate(dateStr);
|
setSelectedDate(dateStr);
|
||||||
setSelectedTime(timeStr);
|
setSelectedTime(timeStr);
|
||||||
} catch {
|
} catch {
|
||||||
@ -170,9 +173,18 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
|||||||
if (editing) {
|
if (editing) {
|
||||||
const payload = {
|
const payload = {
|
||||||
scheduled_at: iso,
|
scheduled_at: iso,
|
||||||
appointment_type: (tipo || "presencial") as "presencial" | "telemedicina",
|
appointment_type: (tipo || "presencial") as
|
||||||
|
| "presencial"
|
||||||
|
| "telemedicina",
|
||||||
notes: observacoes || undefined,
|
notes: observacoes || undefined,
|
||||||
status: status as "requested" | "confirmed" | "checked_in" | "in_progress" | "completed" | "cancelled" | "no_show",
|
status: status as
|
||||||
|
| "requested"
|
||||||
|
| "confirmed"
|
||||||
|
| "checked_in"
|
||||||
|
| "in_progress"
|
||||||
|
| "completed"
|
||||||
|
| "cancelled"
|
||||||
|
| "no_show",
|
||||||
};
|
};
|
||||||
const updated = await appointmentService.update(editing.id, payload);
|
const updated = await appointmentService.update(editing.id, payload);
|
||||||
onSaved(updated);
|
onSaved(updated);
|
||||||
@ -181,7 +193,9 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
|||||||
patient_id: pacienteId,
|
patient_id: pacienteId,
|
||||||
doctor_id: medicoId,
|
doctor_id: medicoId,
|
||||||
scheduled_at: iso,
|
scheduled_at: iso,
|
||||||
appointment_type: (tipo || "presencial") as "presencial" | "telemedicina",
|
appointment_type: (tipo || "presencial") as
|
||||||
|
| "presencial"
|
||||||
|
| "telemedicina",
|
||||||
notes: observacoes || undefined,
|
notes: observacoes || undefined,
|
||||||
};
|
};
|
||||||
const created = await appointmentService.create(payload);
|
const created = await appointmentService.create(payload);
|
||||||
@ -279,7 +293,12 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
|||||||
{selectedDate && medicoId && (
|
{selectedDate && medicoId && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Horário * {selectedTime && <span className="text-blue-600 font-semibold">({selectedTime})</span>}
|
Horário *{" "}
|
||||||
|
{selectedTime && (
|
||||||
|
<span className="text-blue-600 font-semibold">
|
||||||
|
({selectedTime})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
<AvailableSlotsPicker
|
<AvailableSlotsPicker
|
||||||
doctorId={medicoId}
|
doctorId={medicoId}
|
||||||
|
|||||||
@ -28,9 +28,8 @@ export function SecretaryAppointmentList() {
|
|||||||
const [typeFilter, setTypeFilter] = useState("Todos");
|
const [typeFilter, setTypeFilter] = useState("Todos");
|
||||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||||
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
|
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
|
||||||
const [selectedAppointment, setSelectedAppointment] = useState<
|
const [selectedAppointment, setSelectedAppointment] =
|
||||||
AppointmentWithDetails | null
|
useState<AppointmentWithDetails | null>(null);
|
||||||
>(null);
|
|
||||||
const [patients, setPatients] = useState<Patient[]>([]);
|
const [patients, setPatients] = useState<Patient[]>([]);
|
||||||
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
||||||
const [formData, setFormData] = useState<any>({
|
const [formData, setFormData] = useState<any>({
|
||||||
@ -129,7 +128,7 @@ export function SecretaryAppointmentList() {
|
|||||||
Confirmada: "confirmed",
|
Confirmada: "confirmed",
|
||||||
Agendada: "requested",
|
Agendada: "requested",
|
||||||
Cancelada: "cancelled",
|
Cancelada: "cancelled",
|
||||||
"Concluída": "completed",
|
Concluída: "completed",
|
||||||
Concluida: "completed",
|
Concluida: "completed",
|
||||||
};
|
};
|
||||||
return map[label] || label.toLowerCase();
|
return map[label] || label.toLowerCase();
|
||||||
@ -148,10 +147,12 @@ export function SecretaryAppointmentList() {
|
|||||||
const typeValue = mapTypeFilterToValue(typeFilter);
|
const typeValue = mapTypeFilterToValue(typeFilter);
|
||||||
|
|
||||||
// Filtro de status
|
// Filtro de status
|
||||||
const matchesStatus = statusValue === null || appointment.status === statusValue;
|
const matchesStatus =
|
||||||
|
statusValue === null || appointment.status === statusValue;
|
||||||
|
|
||||||
// Filtro de tipo
|
// Filtro de tipo
|
||||||
const matchesType = typeValue === null || appointment.appointment_type === typeValue;
|
const matchesType =
|
||||||
|
typeValue === null || appointment.appointment_type === typeValue;
|
||||||
|
|
||||||
return matchesSearch && matchesStatus && matchesType;
|
return matchesSearch && matchesStatus && matchesType;
|
||||||
});
|
});
|
||||||
@ -194,7 +195,10 @@ export function SecretaryAppointmentList() {
|
|||||||
if (modalMode === "edit" && formData.id) {
|
if (modalMode === "edit" && formData.id) {
|
||||||
// Update only allowed fields per API types
|
// Update only allowed fields per API types
|
||||||
const updatePayload: any = {};
|
const updatePayload: any = {};
|
||||||
if (formData.scheduled_at) updatePayload.scheduled_at = new Date(formData.scheduled_at).toISOString();
|
if (formData.scheduled_at)
|
||||||
|
updatePayload.scheduled_at = new Date(
|
||||||
|
formData.scheduled_at
|
||||||
|
).toISOString();
|
||||||
if (formData.notes) updatePayload.notes = formData.notes;
|
if (formData.notes) updatePayload.notes = formData.notes;
|
||||||
await appointmentService.update(formData.id, updatePayload);
|
await appointmentService.update(formData.id, updatePayload);
|
||||||
toast.success("Consulta atualizada com sucesso!");
|
toast.success("Consulta atualizada com sucesso!");
|
||||||
@ -623,7 +627,12 @@ export function SecretaryAppointmentList() {
|
|||||||
{selectedDate && formData.doctor_id && (
|
{selectedDate && formData.doctor_id && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Horário * {selectedTime && <span className="text-blue-600 font-semibold">({selectedTime})</span>}
|
Horário *{" "}
|
||||||
|
{selectedTime && (
|
||||||
|
<span className="text-blue-600 font-semibold">
|
||||||
|
({selectedTime})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
<AvailableSlotsPicker
|
<AvailableSlotsPicker
|
||||||
doctorId={formData.doctor_id}
|
doctorId={formData.doctor_id}
|
||||||
@ -666,7 +675,9 @@ export function SecretaryAppointmentList() {
|
|||||||
type="submit"
|
type="submit"
|
||||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||||
>
|
>
|
||||||
{modalMode === "edit" ? "Salvar Alterações" : "Agendar Consulta"}
|
{modalMode === "edit"
|
||||||
|
? "Salvar Alterações"
|
||||||
|
: "Agendar Consulta"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -679,7 +690,9 @@ export function SecretaryAppointmentList() {
|
|||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
|
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-bold text-gray-900">Visualizar Consulta</h2>
|
<h2 className="text-2xl font-bold text-gray-900">
|
||||||
|
Visualizar Consulta
|
||||||
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedAppointment(null)}
|
onClick={() => setSelectedAppointment(null)}
|
||||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
@ -691,39 +704,73 @@ export function SecretaryAppointmentList() {
|
|||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-500 mb-1">Paciente</label>
|
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||||
<p className="text-gray-900 font-medium">{selectedAppointment.patient?.full_name || '—'}</p>
|
Paciente
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-900 font-medium">
|
||||||
|
{selectedAppointment.patient?.full_name || "—"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-500 mb-1">Médico</label>
|
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||||
<p className="text-gray-900">{selectedAppointment.doctor?.full_name || '—'}</p>
|
Médico
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-900">
|
||||||
|
{selectedAppointment.doctor?.full_name || "—"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-500 mb-1">Data</label>
|
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||||
<p className="text-gray-900">{selectedAppointment.scheduled_at ? formatDate(selectedAppointment.scheduled_at) : '—'}</p>
|
Data
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-900">
|
||||||
|
{selectedAppointment.scheduled_at
|
||||||
|
? formatDate(selectedAppointment.scheduled_at)
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-500 mb-1">Hora</label>
|
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||||
<p className="text-gray-900">{selectedAppointment.scheduled_at ? formatTime(selectedAppointment.scheduled_at) : '—'}</p>
|
Hora
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-900">
|
||||||
|
{selectedAppointment.scheduled_at
|
||||||
|
? formatTime(selectedAppointment.scheduled_at)
|
||||||
|
: "—"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-500 mb-1">Tipo</label>
|
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||||
<p className="text-gray-900">{selectedAppointment.appointment_type === 'telemedicina' ? 'Telemedicina' : 'Presencial'}</p>
|
Tipo
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-900">
|
||||||
|
{selectedAppointment.appointment_type === "telemedicina"
|
||||||
|
? "Telemedicina"
|
||||||
|
: "Presencial"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-500 mb-1">Status</label>
|
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||||
<div>{getStatusBadge(selectedAppointment.status || 'agendada')}</div>
|
Status
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
{getStatusBadge(selectedAppointment.status || "agendada")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-500 mb-1">Observações</label>
|
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||||
<p className="text-gray-900 whitespace-pre-wrap">{selectedAppointment.notes || '—'}</p>
|
Observações
|
||||||
|
</label>
|
||||||
|
<p className="text-gray-900 whitespace-pre-wrap">
|
||||||
|
{selectedAppointment.notes || "—"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-4">
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
@ -741,5 +788,3 @@ export function SecretaryAppointmentList() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -66,11 +66,13 @@ const formatCPF = (value: string): string => {
|
|||||||
const numbers = value.replace(/\D/g, "");
|
const numbers = value.replace(/\D/g, "");
|
||||||
if (numbers.length === 0) return "";
|
if (numbers.length === 0) return "";
|
||||||
if (numbers.length <= 3) return numbers;
|
if (numbers.length <= 3) return numbers;
|
||||||
if (numbers.length <= 6)
|
if (numbers.length <= 6) return `${numbers.slice(0, 3)}.${numbers.slice(3)}`;
|
||||||
return `${numbers.slice(0, 3)}.${numbers.slice(3)}`;
|
|
||||||
if (numbers.length <= 9)
|
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)}`;
|
||||||
return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(6, 9)}-${numbers.slice(9, 11)}`;
|
return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(
|
||||||
|
6,
|
||||||
|
9
|
||||||
|
)}-${numbers.slice(9, 11)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Função para formatar telefone: (XX) XXXXX-XXXX
|
// Função para formatar telefone: (XX) XXXXX-XXXX
|
||||||
@ -81,8 +83,13 @@ const formatPhone = (value: string): string => {
|
|||||||
if (numbers.length <= 7)
|
if (numbers.length <= 7)
|
||||||
return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`;
|
return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`;
|
||||||
if (numbers.length <= 11)
|
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(
|
||||||
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(7, 11)}`;
|
7
|
||||||
|
)}`;
|
||||||
|
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(
|
||||||
|
7,
|
||||||
|
11
|
||||||
|
)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SecretaryDoctorList({
|
export function SecretaryDoctorList({
|
||||||
@ -423,9 +430,14 @@ export function SecretaryDoctorList({
|
|||||||
if (onOpenSchedule) {
|
if (onOpenSchedule) {
|
||||||
onOpenSchedule(doctor.id);
|
onOpenSchedule(doctor.id);
|
||||||
} else {
|
} else {
|
||||||
sessionStorage.setItem("selectedDoctorForSchedule", doctor.id);
|
sessionStorage.setItem(
|
||||||
|
"selectedDoctorForSchedule",
|
||||||
|
doctor.id
|
||||||
|
);
|
||||||
// dispatch a custom event to inform parent (optional)
|
// dispatch a custom event to inform parent (optional)
|
||||||
window.dispatchEvent(new CustomEvent("open-doctor-schedule"));
|
window.dispatchEvent(
|
||||||
|
new CustomEvent("open-doctor-schedule")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
title="Gerenciar agenda"
|
title="Gerenciar agenda"
|
||||||
@ -501,7 +513,10 @@ export function SecretaryDoctorList({
|
|||||||
type="text"
|
type="text"
|
||||||
value={formData.cpf}
|
value={formData.cpf}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, cpf: formatCPF(e.target.value) })
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
cpf: formatCPF(e.target.value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
required
|
required
|
||||||
@ -649,7 +664,9 @@ export function SecretaryDoctorList({
|
|||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
<h2 className="text-xl font-semibold text-gray-900">Visualizar Médico</h2>
|
<h2 className="text-xl font-semibold text-gray-900">
|
||||||
|
Visualizar Médico
|
||||||
|
</h2>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowViewModal(false)}
|
onClick={() => setShowViewModal(false)}
|
||||||
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
||||||
@ -661,19 +678,23 @@ export function SecretaryDoctorList({
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">Nome</p>
|
<p className="text-sm text-gray-500">Nome</p>
|
||||||
<p className="text-gray-900 font-medium">{selectedDoctor.full_name}</p>
|
<p className="text-gray-900 font-medium">
|
||||||
|
{selectedDoctor.full_name}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">Especialidade</p>
|
<p className="text-sm text-gray-500">Especialidade</p>
|
||||||
<p className="text-gray-900">{selectedDoctor.specialty || '—'}</p>
|
<p className="text-gray-900">
|
||||||
|
{selectedDoctor.specialty || "—"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">CRM</p>
|
<p className="text-sm text-gray-500">CRM</p>
|
||||||
<p className="text-gray-900">{selectedDoctor.crm || '—'}</p>
|
<p className="text-gray-900">{selectedDoctor.crm || "—"}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-500">Email</p>
|
<p className="text-sm text-gray-500">Email</p>
|
||||||
<p className="text-gray-900">{selectedDoctor.email || '—'}</p>
|
<p className="text-gray-900">{selectedDoctor.email || "—"}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -691,5 +712,3 @@ export function SecretaryDoctorList({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -118,7 +118,10 @@ export function SecretaryDoctorSchedule() {
|
|||||||
const loadDoctorSchedule = useCallback(async () => {
|
const loadDoctorSchedule = useCallback(async () => {
|
||||||
if (!selectedDoctorId) return;
|
if (!selectedDoctorId) return;
|
||||||
|
|
||||||
console.log("[SecretaryDoctorSchedule] Carregando agenda do médico:", selectedDoctorId);
|
console.log(
|
||||||
|
"[SecretaryDoctorSchedule] Carregando agenda do médico:",
|
||||||
|
selectedDoctorId
|
||||||
|
);
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@ -154,7 +157,10 @@ export function SecretaryDoctorSchedule() {
|
|||||||
});
|
});
|
||||||
setExceptions(Array.isArray(exceptionsData) ? exceptionsData : []);
|
setExceptions(Array.isArray(exceptionsData) ? exceptionsData : []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[SecretaryDoctorSchedule] Erro ao carregar agenda:", error);
|
console.error(
|
||||||
|
"[SecretaryDoctorSchedule] Erro ao carregar agenda:",
|
||||||
|
error
|
||||||
|
);
|
||||||
toast.error("Erro ao carregar agenda do médico");
|
toast.error("Erro ao carregar agenda do médico");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -179,17 +185,17 @@ export function SecretaryDoctorSchedule() {
|
|||||||
|
|
||||||
for (let i = 0; i < 42; i++) {
|
for (let i = 0; i < 42; i++) {
|
||||||
const dayDate = new Date(currentDatePointer);
|
const dayDate = new Date(currentDatePointer);
|
||||||
const dayDateStr = dayDate.toISOString().split('T')[0];
|
const dayDateStr = dayDate.toISOString().split("T")[0];
|
||||||
|
|
||||||
// Filter appointments for this day
|
// Filter appointments for this day
|
||||||
const dayAppointments = appointments.filter(apt => {
|
const dayAppointments = appointments.filter((apt) => {
|
||||||
if (!apt.scheduled_at) return false;
|
if (!apt.scheduled_at) return false;
|
||||||
const aptDate = new Date(apt.scheduled_at).toISOString().split('T')[0];
|
const aptDate = new Date(apt.scheduled_at).toISOString().split("T")[0];
|
||||||
return aptDate === dayDateStr;
|
return aptDate === dayDateStr;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter exceptions for this day
|
// Filter exceptions for this day
|
||||||
const dayExceptions = exceptions.filter(exc => {
|
const dayExceptions = exceptions.filter((exc) => {
|
||||||
return exc.date === dayDateStr;
|
return exc.date === dayDateStr;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -294,16 +300,20 @@ export function SecretaryDoctorSchedule() {
|
|||||||
|
|
||||||
loadDoctorSchedule();
|
loadDoctorSchedule();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("[SecretaryDoctorSchedule] Erro ao adicionar disponibilidade:", {
|
console.error(
|
||||||
error,
|
"[SecretaryDoctorSchedule] Erro ao adicionar disponibilidade:",
|
||||||
message: error?.message,
|
{
|
||||||
response: error?.response?.data,
|
error,
|
||||||
});
|
message: error?.message,
|
||||||
|
response: error?.response?.data,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const errorMsg = error?.response?.data?.message ||
|
const errorMsg =
|
||||||
error?.response?.data?.hint ||
|
error?.response?.data?.message ||
|
||||||
error?.message ||
|
error?.response?.data?.hint ||
|
||||||
"Erro ao adicionar disponibilidade";
|
error?.message ||
|
||||||
|
"Erro ao adicionar disponibilidade";
|
||||||
toast.error(errorMsg);
|
toast.error(errorMsg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -330,8 +340,12 @@ export function SecretaryDoctorSchedule() {
|
|||||||
|
|
||||||
// Criar exceções para cada dia no intervalo
|
// Criar exceções para cada dia no intervalo
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) {
|
for (
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
let date = new Date(start);
|
||||||
|
date <= end;
|
||||||
|
date.setDate(date.getDate() + 1)
|
||||||
|
) {
|
||||||
|
const dateStr = date.toISOString().split("T")[0];
|
||||||
promises.push(
|
promises.push(
|
||||||
availabilityService.createException({
|
availabilityService.createException({
|
||||||
doctor_id: selectedDoctorId,
|
doctor_id: selectedDoctorId,
|
||||||
@ -347,8 +361,12 @@ export function SecretaryDoctorSchedule() {
|
|||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
const days = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
const days =
|
||||||
toast.success(`Exceção adicionada para ${days} dia${days > 1 ? 's' : ''} com sucesso`);
|
Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) +
|
||||||
|
1;
|
||||||
|
toast.success(
|
||||||
|
`Exceção adicionada para ${days} dia${days > 1 ? "s" : ""} com sucesso`
|
||||||
|
);
|
||||||
|
|
||||||
setShowExceptionDialog(false);
|
setShowExceptionDialog(false);
|
||||||
setExceptionType("férias");
|
setExceptionType("férias");
|
||||||
@ -362,9 +380,10 @@ export function SecretaryDoctorSchedule() {
|
|||||||
loadDoctorSchedule();
|
loadDoctorSchedule();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("Erro ao adicionar exceção:", error);
|
console.error("Erro ao adicionar exceção:", error);
|
||||||
const errorMsg = error?.response?.data?.message ||
|
const errorMsg =
|
||||||
error?.message ||
|
error?.response?.data?.message ||
|
||||||
"Erro ao adicionar exceção";
|
error?.message ||
|
||||||
|
"Erro ao adicionar exceção";
|
||||||
toast.error(errorMsg);
|
toast.error(errorMsg);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -397,27 +416,40 @@ export function SecretaryDoctorSchedule() {
|
|||||||
active: editActive,
|
active: editActive,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[SecretaryDoctorSchedule] Dados de atualização:", updateData);
|
console.log(
|
||||||
|
"[SecretaryDoctorSchedule] Dados de atualização:",
|
||||||
|
updateData
|
||||||
|
);
|
||||||
|
|
||||||
const result = await availabilityService.update(editingAvailability.id, updateData);
|
const result = await availabilityService.update(
|
||||||
|
editingAvailability.id,
|
||||||
|
updateData
|
||||||
|
);
|
||||||
|
|
||||||
console.log("[SecretaryDoctorSchedule] Resultado da atualização:", result);
|
console.log(
|
||||||
|
"[SecretaryDoctorSchedule] Resultado da atualização:",
|
||||||
|
result
|
||||||
|
);
|
||||||
|
|
||||||
toast.success("Disponibilidade atualizada com sucesso");
|
toast.success("Disponibilidade atualizada com sucesso");
|
||||||
setShowEditDialog(false);
|
setShowEditDialog(false);
|
||||||
setEditingAvailability(null);
|
setEditingAvailability(null);
|
||||||
loadDoctorSchedule();
|
loadDoctorSchedule();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("[SecretaryDoctorSchedule] Erro ao atualizar disponibilidade:", {
|
console.error(
|
||||||
error,
|
"[SecretaryDoctorSchedule] Erro ao atualizar disponibilidade:",
|
||||||
message: error?.message,
|
{
|
||||||
response: error?.response,
|
error,
|
||||||
data: error?.response?.data,
|
message: error?.message,
|
||||||
});
|
response: error?.response,
|
||||||
|
data: error?.response?.data,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const errorMessage = error?.response?.data?.message ||
|
const errorMessage =
|
||||||
error?.message ||
|
error?.response?.data?.message ||
|
||||||
"Erro ao atualizar disponibilidade";
|
error?.message ||
|
||||||
|
"Erro ao atualizar disponibilidade";
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -563,9 +595,10 @@ export function SecretaryDoctorSchedule() {
|
|||||||
|
|
||||||
{/* Exceções (bloqueios e disponibilidades extras) */}
|
{/* Exceções (bloqueios e disponibilidades extras) */}
|
||||||
{day.exceptions.map((exc, i) => {
|
{day.exceptions.map((exc, i) => {
|
||||||
const timeRange = exc.start_time && exc.end_time
|
const timeRange =
|
||||||
? `${exc.start_time} - ${exc.end_time}`
|
exc.start_time && exc.end_time
|
||||||
: "Dia inteiro";
|
? `${exc.start_time} - ${exc.end_time}`
|
||||||
|
: "Dia inteiro";
|
||||||
const tooltipText = exc.reason
|
const tooltipText = exc.reason
|
||||||
? `${timeRange} - ${exc.reason}`
|
? `${timeRange} - ${exc.reason}`
|
||||||
: timeRange;
|
: timeRange;
|
||||||
@ -588,11 +621,11 @@ export function SecretaryDoctorSchedule() {
|
|||||||
{/* Consultas agendadas */}
|
{/* Consultas agendadas */}
|
||||||
{day.appointments.map((apt, i) => {
|
{day.appointments.map((apt, i) => {
|
||||||
const time = apt.scheduled_at
|
const time = apt.scheduled_at
|
||||||
? new Date(apt.scheduled_at).toLocaleTimeString('pt-BR', {
|
? new Date(apt.scheduled_at).toLocaleTimeString("pt-BR", {
|
||||||
hour: '2-digit',
|
hour: "2-digit",
|
||||||
minute: '2-digit',
|
minute: "2-digit",
|
||||||
})
|
})
|
||||||
: '';
|
: "";
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`apt-${i}`}
|
key={`apt-${i}`}
|
||||||
@ -701,11 +734,11 @@ export function SecretaryDoctorSchedule() {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">
|
<p className="font-medium text-gray-900">
|
||||||
{new Date(exc.date).toLocaleDateString('pt-BR', {
|
{new Date(exc.date).toLocaleDateString("pt-BR", {
|
||||||
weekday: 'long',
|
weekday: "long",
|
||||||
year: 'numeric',
|
year: "numeric",
|
||||||
month: 'long',
|
month: "long",
|
||||||
day: 'numeric',
|
day: "numeric",
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
@ -725,7 +758,9 @@ export function SecretaryDoctorSchedule() {
|
|||||||
: "bg-purple-100 text-purple-700"
|
: "bg-purple-100 text-purple-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{exc.kind === "bloqueio" ? "Bloqueio" : "Disponibilidade Extra"}
|
{exc.kind === "bloqueio"
|
||||||
|
? "Bloqueio"
|
||||||
|
: "Disponibilidade Extra"}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@ -733,7 +768,7 @@ export function SecretaryDoctorSchedule() {
|
|||||||
window.confirm(
|
window.confirm(
|
||||||
`Tem certeza que deseja remover esta exceção?\n\nData: ${new Date(
|
`Tem certeza que deseja remover esta exceção?\n\nData: ${new Date(
|
||||||
exc.date
|
exc.date
|
||||||
).toLocaleDateString('pt-BR')}`
|
).toLocaleDateString("pt-BR")}`
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
@ -1082,5 +1117,3 @@ export function SecretaryDoctorSchedule() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,10 @@ export function SecretaryReportList() {
|
|||||||
const [showEditModal, setShowEditModal] = useState(false);
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
const [selectedReport, setSelectedReport] = useState<Report | null>(null);
|
const [selectedReport, setSelectedReport] = useState<Report | null>(null);
|
||||||
const [patients, setPatients] = useState<Patient[]>([]);
|
const [patients, setPatients] = useState<Patient[]>([]);
|
||||||
const [requestedByNames, setRequestedByNames] = useState<Record<string, string>>({});
|
const [doctors, setDoctors] = useState<any[]>([]);
|
||||||
|
const [requestedByNames, setRequestedByNames] = useState<
|
||||||
|
Record<string, string>
|
||||||
|
>({});
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
patient_id: "",
|
patient_id: "",
|
||||||
exam: "",
|
exam: "",
|
||||||
@ -35,12 +38,12 @@ export function SecretaryReportList() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadReports();
|
loadReports();
|
||||||
loadPatients();
|
loadPatients();
|
||||||
|
loadDoctors();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Recarrega automaticamente quando o filtro de status muda
|
// Recarrega automaticamente quando o filtro de status muda
|
||||||
// (evita depender do clique em Buscar)
|
// (evita depender do clique em Buscar)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
loadReports();
|
loadReports();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [statusFilter]);
|
}, [statusFilter]);
|
||||||
@ -54,6 +57,15 @@ export function SecretaryReportList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadDoctors = async () => {
|
||||||
|
try {
|
||||||
|
const data = await doctorService.list({});
|
||||||
|
setDoctors(Array.isArray(data) ? data : []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao carregar médicos:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOpenCreateModal = () => {
|
const handleOpenCreateModal = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
patient_id: "",
|
patient_id: "",
|
||||||
@ -120,7 +132,7 @@ export function SecretaryReportList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await reportService.update(selectedReport.id, {
|
const updatedReport = await reportService.update(selectedReport.id, {
|
||||||
patient_id: formData.patient_id,
|
patient_id: formData.patient_id,
|
||||||
exam: formData.exam || undefined,
|
exam: formData.exam || undefined,
|
||||||
diagnosis: formData.diagnosis || undefined,
|
diagnosis: formData.diagnosis || undefined,
|
||||||
@ -130,10 +142,19 @@ export function SecretaryReportList() {
|
|||||||
requested_by: formData.requested_by || undefined,
|
requested_by: formData.requested_by || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log("[SecretaryReportList] Relatório atualizado:", updatedReport);
|
||||||
|
console.log(
|
||||||
|
"[SecretaryReportList] Novo requested_by:",
|
||||||
|
formData.requested_by
|
||||||
|
);
|
||||||
|
|
||||||
toast.success("Relatório atualizado com sucesso!");
|
toast.success("Relatório atualizado com sucesso!");
|
||||||
setShowEditModal(false);
|
setShowEditModal(false);
|
||||||
setSelectedReport(null);
|
setSelectedReport(null);
|
||||||
loadReports();
|
|
||||||
|
// Limpar cache de nomes antes de recarregar
|
||||||
|
setRequestedByNames({});
|
||||||
|
await loadReports();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao atualizar relatório:", error);
|
console.error("Erro ao atualizar relatório:", error);
|
||||||
toast.error("Erro ao atualizar relatório");
|
toast.error("Erro ao atualizar relatório");
|
||||||
@ -278,25 +299,51 @@ export function SecretaryReportList() {
|
|||||||
const names: Record<string, string> = {};
|
const names: Record<string, string> = {};
|
||||||
|
|
||||||
// Buscar nomes únicos de requested_by
|
// Buscar nomes únicos de requested_by
|
||||||
const uniqueIds = [...new Set(reportsList.map(r => r.requested_by).filter(Boolean))];
|
const uniqueIds = [
|
||||||
|
...new Set(reportsList.map((r) => r.requested_by).filter(Boolean)),
|
||||||
|
];
|
||||||
|
|
||||||
for (const id of uniqueIds) {
|
try {
|
||||||
try {
|
// Buscar todos os médicos de uma vez
|
||||||
// Tentar buscar como médico primeiro
|
const doctors = await doctorService.list({});
|
||||||
const doctors = await doctorService.list({});
|
|
||||||
const doctor = doctors.find((d) => (d as any).user_id === id || d.id === id);
|
|
||||||
|
|
||||||
if (doctor) {
|
for (const id of uniqueIds) {
|
||||||
names[id!] = doctor.full_name || "Dr. " + (doctor.full_name || "Médico");
|
// Verificar se já é um nome (não é UUID)
|
||||||
} else {
|
const isUUID =
|
||||||
// Se não for médico, simplesmente pegar o texto que já está armazenado
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||||
// pois requested_by pode ser um nome direto
|
id!
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUUID) {
|
||||||
|
// Já é um nome direto (dados legados), manter como está
|
||||||
names[id!] = id!;
|
names[id!] = id!;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// É um UUID, procurar o médico
|
||||||
|
const doctor = doctors.find((d) => {
|
||||||
|
const doctorAny = d as any;
|
||||||
|
return doctorAny.user_id === id || d.id === id || doctorAny.id === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (doctor && doctor.full_name) {
|
||||||
|
// Formatar o nome com "Dr." se não tiver
|
||||||
|
const doctorName = doctor.full_name;
|
||||||
|
names[id!] = /^dr\.?\s/i.test(doctorName)
|
||||||
|
? doctorName
|
||||||
|
: `Dr. ${doctorName}`;
|
||||||
|
} else {
|
||||||
|
// UUID não encontrado - médico pode ter sido deletado
|
||||||
|
// Mostrar mensagem mais amigável
|
||||||
|
names[id!] = "Médico não cadastrado";
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error(`Erro ao buscar nome para ID ${id}:`, error);
|
|
||||||
names[id!] = id!; // Manter o valor original em caso de erro
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao buscar nomes dos médicos:", error);
|
||||||
|
// Em caso de erro, manter os IDs originais
|
||||||
|
uniqueIds.forEach((id) => {
|
||||||
|
names[id!] = id!;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setRequestedByNames(names);
|
setRequestedByNames(names);
|
||||||
@ -307,7 +354,15 @@ export function SecretaryReportList() {
|
|||||||
try {
|
try {
|
||||||
// Se um filtro de status estiver aplicado, encaminhar para o serviço
|
// Se um filtro de status estiver aplicado, encaminhar para o serviço
|
||||||
// Cast explícito para o tipo esperado pelo serviço (ReportStatus)
|
// Cast explícito para o tipo esperado pelo serviço (ReportStatus)
|
||||||
const filters = statusFilter ? { status: statusFilter as "draft" | "completed" | "pending" | "cancelled" } : undefined;
|
const filters = statusFilter
|
||||||
|
? {
|
||||||
|
status: statusFilter as
|
||||||
|
| "draft"
|
||||||
|
| "completed"
|
||||||
|
| "pending"
|
||||||
|
| "cancelled",
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
console.log("[SecretaryReportList] loadReports filters:", filters);
|
console.log("[SecretaryReportList] loadReports filters:", filters);
|
||||||
const data = await reportService.list(filters);
|
const data = await reportService.list(filters);
|
||||||
console.log("✅ Relatórios carregados:", data);
|
console.log("✅ Relatórios carregados:", data);
|
||||||
@ -518,7 +573,8 @@ export function SecretaryReportList() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 text-sm text-gray-700">
|
<td className="px-6 py-4 text-sm text-gray-700">
|
||||||
{report.requested_by
|
{report.requested_by
|
||||||
? (requestedByNames[report.requested_by] || report.requested_by)
|
? requestedByNames[report.requested_by] ||
|
||||||
|
report.requested_by
|
||||||
: "—"}
|
: "—"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4">
|
||||||
@ -609,6 +665,26 @@ export function SecretaryReportList() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Solicitado por
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.requested_by}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, requested_by: e.target.value })
|
||||||
|
}
|
||||||
|
className="form-input"
|
||||||
|
>
|
||||||
|
<option value="">Selecione um médico</option>
|
||||||
|
{doctors.map((doctor) => (
|
||||||
|
<option key={doctor.id} value={doctor.id}>
|
||||||
|
{doctor.full_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Diagnóstico
|
Diagnóstico
|
||||||
@ -732,7 +808,8 @@ export function SecretaryReportList() {
|
|||||||
</label>
|
</label>
|
||||||
<p className="text-gray-900">
|
<p className="text-gray-900">
|
||||||
{selectedReport.requested_by
|
{selectedReport.requested_by
|
||||||
? (requestedByNames[selectedReport.requested_by] || selectedReport.requested_by)
|
? requestedByNames[selectedReport.requested_by] ||
|
||||||
|
selectedReport.requested_by
|
||||||
: "—"}
|
: "—"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -889,15 +966,20 @@ export function SecretaryReportList() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Solicitado por
|
Solicitado por
|
||||||
</label>
|
</label>
|
||||||
<input
|
<select
|
||||||
type="text"
|
|
||||||
value={formData.requested_by}
|
value={formData.requested_by}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, requested_by: e.target.value })
|
setFormData({ ...formData, requested_by: e.target.value })
|
||||||
}
|
}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
placeholder="Nome do médico solicitante"
|
>
|
||||||
/>
|
<option value="">Selecione um médico</option>
|
||||||
|
{doctors.map((doctor) => (
|
||||||
|
<option key={doctor.id} value={doctor.id}>
|
||||||
|
{doctor.full_name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -954,5 +1036,3 @@ export function SecretaryReportList() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -87,6 +87,9 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
|
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
|
||||||
const [selectedLaudo, setSelectedLaudo] = useState<Report | null>(null);
|
const [selectedLaudo, setSelectedLaudo] = useState<Report | null>(null);
|
||||||
const [showLaudoModal, setShowLaudoModal] = useState(false);
|
const [showLaudoModal, setShowLaudoModal] = useState(false);
|
||||||
|
const [requestedByNames, setRequestedByNames] = useState<
|
||||||
|
Record<string, string>
|
||||||
|
>({});
|
||||||
|
|
||||||
const pacienteId = user?.id || "";
|
const pacienteId = user?.id || "";
|
||||||
const pacienteNome = user?.nome || "Paciente";
|
const pacienteNome = user?.nome || "Paciente";
|
||||||
@ -99,7 +102,10 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
|
|
||||||
// Detecta se veio de navegação com estado para abrir aba específica
|
// Detecta se veio de navegação com estado para abrir aba específica
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (location.state && (location.state as { activeTab?: string }).activeTab) {
|
if (
|
||||||
|
location.state &&
|
||||||
|
(location.state as { activeTab?: string }).activeTab
|
||||||
|
) {
|
||||||
const state = location.state as { activeTab: string };
|
const state = location.state as { activeTab: string };
|
||||||
setActiveTab(state.activeTab);
|
setActiveTab(state.activeTab);
|
||||||
// Limpa o estado após usar
|
// Limpa o estado após usar
|
||||||
@ -196,6 +202,32 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
fetchConsultas();
|
fetchConsultas();
|
||||||
}, [fetchConsultas]);
|
}, [fetchConsultas]);
|
||||||
|
|
||||||
|
// Função para carregar nomes dos médicos solicitantes
|
||||||
|
const loadRequestedByNames = useCallback(async (reports: Report[]) => {
|
||||||
|
const uniqueIds = [
|
||||||
|
...new Set(
|
||||||
|
reports.map((r) => r.requested_by).filter((id): id is string => !!id)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if (uniqueIds.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const doctors = await doctorService.list();
|
||||||
|
const nameMap: Record<string, string> = {};
|
||||||
|
|
||||||
|
uniqueIds.forEach((id) => {
|
||||||
|
const doctor = doctors.find((d) => d.id === id);
|
||||||
|
if (doctor && doctor.full_name) {
|
||||||
|
nameMap[id] = formatDoctorName(doctor.full_name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setRequestedByNames(nameMap);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao buscar nomes dos médicos:", error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Recarregar consultas quando mudar para a aba de consultas
|
// Recarregar consultas quando mudar para a aba de consultas
|
||||||
const fetchLaudos = useCallback(async () => {
|
const fetchLaudos = useCallback(async () => {
|
||||||
if (!pacienteId) return;
|
if (!pacienteId) return;
|
||||||
@ -203,6 +235,8 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const data = await reportService.list({ patient_id: pacienteId });
|
const data = await reportService.list({ patient_id: pacienteId });
|
||||||
setLaudos(data);
|
setLaudos(data);
|
||||||
|
// Carregar nomes dos médicos
|
||||||
|
await loadRequestedByNames(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao buscar laudos:", error);
|
console.error("Erro ao buscar laudos:", error);
|
||||||
toast.error("Erro ao carregar laudos");
|
toast.error("Erro ao carregar laudos");
|
||||||
@ -210,7 +244,7 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoadingLaudos(false);
|
setLoadingLaudos(false);
|
||||||
}
|
}
|
||||||
}, [pacienteId]);
|
}, [pacienteId, loadRequestedByNames]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === "appointments") {
|
if (activeTab === "appointments") {
|
||||||
@ -273,8 +307,12 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
const consultasPassadasDashboard = todasConsultasPassadas.slice(0, 3);
|
const consultasPassadasDashboard = todasConsultasPassadas.slice(0, 3);
|
||||||
|
|
||||||
// Para a página de consultas (com paginação)
|
// Para a página de consultas (com paginação)
|
||||||
const totalPaginasProximas = Math.ceil(todasConsultasProximas.length / consultasPorPagina);
|
const totalPaginasProximas = Math.ceil(
|
||||||
const totalPaginasPassadas = Math.ceil(todasConsultasPassadas.length / consultasPorPagina);
|
todasConsultasProximas.length / consultasPorPagina
|
||||||
|
);
|
||||||
|
const totalPaginasPassadas = Math.ceil(
|
||||||
|
todasConsultasPassadas.length / consultasPorPagina
|
||||||
|
);
|
||||||
|
|
||||||
const consultasProximas = todasConsultasProximas.slice(
|
const consultasProximas = todasConsultasProximas.slice(
|
||||||
(paginaProximas - 1) * consultasPorPagina,
|
(paginaProximas - 1) * consultasPorPagina,
|
||||||
@ -369,7 +407,9 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
<p className="font-medium text-gray-900 dark:text-white truncate text-sm sm:text-base">
|
<p className="font-medium text-gray-900 dark:text-white truncate text-sm sm:text-base">
|
||||||
{pacienteNome}
|
{pacienteNome}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs sm:text-sm text-gray-600 dark:text-gray-400">Paciente</p>
|
<p className="text-xs sm:text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Paciente
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -649,9 +689,13 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
{getMedicoEspecialidade(c.medicoId)}
|
{getMedicoEspecialidade(c.medicoId)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
{format(new Date(c.dataHora), "dd/MM/yyyy - HH:mm", {
|
{format(
|
||||||
locale: ptBR,
|
new Date(c.dataHora),
|
||||||
})}
|
"dd/MM/yyyy - HH:mm",
|
||||||
|
{
|
||||||
|
locale: ptBR,
|
||||||
|
}
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -662,7 +706,8 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
onClick={() => setActiveTab("appointments")}
|
onClick={() => setActiveTab("appointments")}
|
||||||
className="w-full mt-4 px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
className="w-full mt-4 px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
||||||
>
|
>
|
||||||
Ver mais consultas ({todasConsultasProximas.length - 3} restantes)
|
Ver mais consultas ({todasConsultasProximas.length - 3}{" "}
|
||||||
|
restantes)
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -698,10 +743,7 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
<User className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
<User className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
<span>Editar Perfil</span>
|
<span>Editar Perfil</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => navigate("/ajuda")} className="form-input">
|
||||||
onClick={() => navigate("/ajuda")}
|
|
||||||
className="form-input"
|
|
||||||
>
|
|
||||||
<HelpCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
<HelpCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||||
<span>Central de Ajuda</span>
|
<span>Central de Ajuda</span>
|
||||||
</button>
|
</button>
|
||||||
@ -781,11 +823,18 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
{totalPaginasProximas > 1 && (
|
{totalPaginasProximas > 1 && (
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
Mostrando {((paginaProximas - 1) * consultasPorPagina) + 1} a {Math.min(paginaProximas * consultasPorPagina, todasConsultasProximas.length)} de {todasConsultasProximas.length} consultas
|
Mostrando {(paginaProximas - 1) * consultasPorPagina + 1} a{" "}
|
||||||
|
{Math.min(
|
||||||
|
paginaProximas * consultasPorPagina,
|
||||||
|
todasConsultasProximas.length
|
||||||
|
)}{" "}
|
||||||
|
de {todasConsultasProximas.length} consultas
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPaginaProximas(Math.max(1, paginaProximas - 1))}
|
onClick={() =>
|
||||||
|
setPaginaProximas(Math.max(1, paginaProximas - 1))
|
||||||
|
}
|
||||||
disabled={paginaProximas === 1}
|
disabled={paginaProximas === 1}
|
||||||
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
|
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
|
||||||
>
|
>
|
||||||
@ -795,7 +844,11 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
Página {paginaProximas} de {totalPaginasProximas}
|
Página {paginaProximas} de {totalPaginasProximas}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPaginaProximas(Math.min(totalPaginasProximas, paginaProximas + 1))}
|
onClick={() =>
|
||||||
|
setPaginaProximas(
|
||||||
|
Math.min(totalPaginasProximas, paginaProximas + 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
disabled={paginaProximas === totalPaginasProximas}
|
disabled={paginaProximas === totalPaginasProximas}
|
||||||
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
|
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
|
||||||
>
|
>
|
||||||
@ -831,11 +884,18 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
{totalPaginasPassadas > 1 && (
|
{totalPaginasPassadas > 1 && (
|
||||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
Mostrando {((paginaPassadas - 1) * consultasPorPagina) + 1} a {Math.min(paginaPassadas * consultasPorPagina, todasConsultasPassadas.length)} de {todasConsultasPassadas.length} consultas
|
Mostrando {(paginaPassadas - 1) * consultasPorPagina + 1} a{" "}
|
||||||
|
{Math.min(
|
||||||
|
paginaPassadas * consultasPorPagina,
|
||||||
|
todasConsultasPassadas.length
|
||||||
|
)}{" "}
|
||||||
|
de {todasConsultasPassadas.length} consultas
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setPaginaPassadas(Math.max(1, paginaPassadas - 1))}
|
onClick={() =>
|
||||||
|
setPaginaPassadas(Math.max(1, paginaPassadas - 1))
|
||||||
|
}
|
||||||
disabled={paginaPassadas === 1}
|
disabled={paginaPassadas === 1}
|
||||||
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
|
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
|
||||||
>
|
>
|
||||||
@ -845,7 +905,11 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
Página {paginaPassadas} de {totalPaginasPassadas}
|
Página {paginaPassadas} de {totalPaginasPassadas}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPaginaPassadas(Math.min(totalPaginasPassadas, paginaPassadas + 1))}
|
onClick={() =>
|
||||||
|
setPaginaPassadas(
|
||||||
|
Math.min(totalPaginasPassadas, paginaPassadas + 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
disabled={paginaPassadas === totalPaginasPassadas}
|
disabled={paginaPassadas === totalPaginasPassadas}
|
||||||
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
|
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
|
||||||
>
|
>
|
||||||
@ -1097,7 +1161,9 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
<p className="font-medium text-gray-900 dark:text-white text-sm truncate">
|
<p className="font-medium text-gray-900 dark:text-white text-sm truncate">
|
||||||
{pacienteNome}
|
{pacienteNome}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-600 dark:text-gray-400">Paciente</p>
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
Paciente
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@ -1143,7 +1209,9 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto">
|
<main className="flex-1 overflow-y-auto">
|
||||||
<div className="container mx-auto p-4 sm:p-6 lg:p-8">{renderContent()}</div>
|
<div className="container mx-auto p-4 sm:p-6 lg:p-8">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Modal de Visualização do Laudo */}
|
{/* Modal de Visualização do Laudo */}
|
||||||
@ -1207,11 +1275,14 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
Data de Criação
|
Data de Criação
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900 dark:text-white">
|
<p className="text-sm text-gray-900 dark:text-white">
|
||||||
{new Date(selectedLaudo.created_at).toLocaleDateString("pt-BR", {
|
{new Date(selectedLaudo.created_at).toLocaleDateString(
|
||||||
day: "2-digit",
|
"pt-BR",
|
||||||
month: "long",
|
{
|
||||||
year: "numeric",
|
day: "2-digit",
|
||||||
})}
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{selectedLaudo.due_at && (
|
{selectedLaudo.due_at && (
|
||||||
@ -1220,11 +1291,14 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
Prazo de Entrega
|
Prazo de Entrega
|
||||||
</label>
|
</label>
|
||||||
<p className="text-sm text-gray-900 dark:text-white">
|
<p className="text-sm text-gray-900 dark:text-white">
|
||||||
{new Date(selectedLaudo.due_at).toLocaleDateString("pt-BR", {
|
{new Date(selectedLaudo.due_at).toLocaleDateString(
|
||||||
day: "2-digit",
|
"pt-BR",
|
||||||
month: "long",
|
{
|
||||||
year: "numeric",
|
day: "2-digit",
|
||||||
})}
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -1294,7 +1368,8 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||||
<p className="text-sm text-gray-900 dark:text-white">
|
<p className="text-sm text-gray-900 dark:text-white">
|
||||||
{selectedLaudo.requested_by}
|
{requestedByNames[selectedLaudo.requested_by] ||
|
||||||
|
selectedLaudo.requested_by}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1308,7 +1383,9 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 prose dark:prose-invert max-w-none"
|
className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 prose dark:prose-invert max-w-none"
|
||||||
dangerouslySetInnerHTML={{ __html: selectedLaudo.content_html }}
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: selectedLaudo.content_html,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -1334,5 +1411,3 @@ const AcompanhamentoPaciente: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default AcompanhamentoPaciente;
|
export default AcompanhamentoPaciente;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -247,7 +247,9 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
<h1 className="text-lg sm:text-xl lg:text-2xl font-bold truncate">
|
<h1 className="text-lg sm:text-xl lg:text-2xl font-bold truncate">
|
||||||
Bem-vindo(a), {pacienteLogado.nome}!
|
Bem-vindo(a), {pacienteLogado.nome}!
|
||||||
</h1>
|
</h1>
|
||||||
<p className="opacity-90 text-sm sm:text-base">Agende sua consulta médica</p>
|
<p className="opacity-90 text-sm sm:text-base">
|
||||||
|
Agende sua consulta médica
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
@ -286,215 +288,216 @@ const AgendamentoPaciente: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-lg sm:rounded-xl shadow border border-gray-200 p-4 sm:p-6">
|
<div className="bg-white rounded-lg sm:rounded-xl shadow border border-gray-200 p-4 sm:p-6">
|
||||||
{/* Etapa 1: Seleção de Médico */}
|
{/* Etapa 1: Seleção de Médico */}
|
||||||
{etapa === 1 && (
|
{etapa === 1 && (
|
||||||
<div className="space-y-4 sm:space-y-6">
|
<div className="space-y-4 sm:space-y-6">
|
||||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
|
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
|
||||||
<User className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
|
<User className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
|
||||||
Selecione o Médico
|
Selecione o Médico
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Médico/Especialidade
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={agendamento.medicoId}
|
|
||||||
onChange={(e) => handleMedicoChange(e.target.value)}
|
|
||||||
className="form-input text-sm sm:text-base"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Selecione um médico</option>
|
|
||||||
{medicos.map((medico) => (
|
|
||||||
<option key={medico._id} value={medico._id}>
|
|
||||||
{medico.nome} - {medico.especialidade} (R${" "}
|
|
||||||
{medico.valorConsulta})
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end pt-2">
|
|
||||||
<button
|
|
||||||
onClick={() => setEtapa(2)}
|
|
||||||
disabled={!agendamento.medicoId}
|
|
||||||
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto text-sm sm:text-base"
|
|
||||||
>
|
|
||||||
Próximo
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Etapa 2: Seleção de Data e Horário */}
|
|
||||||
{etapa === 2 && (
|
|
||||||
<div className="space-y-4 sm:space-y-6">
|
|
||||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
|
|
||||||
<Calendar className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
|
|
||||||
Selecione Data e Horário
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Data da Consulta
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={agendamento.data}
|
|
||||||
onChange={(e) => handleDataChange(e.target.value)}
|
|
||||||
className="form-input text-sm sm:text-base"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<option value="">Selecione uma data</option>
|
|
||||||
{proximosSeteDias().map((dia) => (
|
|
||||||
<option key={dia.valor} value={dia.valor}>
|
|
||||||
{dia.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{agendamento.data && agendamento.medicoId && (
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||||
Horários Disponíveis
|
Médico/Especialidade
|
||||||
</label>
|
</label>
|
||||||
<AvailableSlotsPicker
|
<select
|
||||||
doctorId={agendamento.medicoId}
|
value={agendamento.medicoId}
|
||||||
date={agendamento.data}
|
onChange={(e) => handleMedicoChange(e.target.value)}
|
||||||
onSelect={(t) =>
|
className="form-input text-sm sm:text-base"
|
||||||
setAgendamento((prev) => ({ ...prev, horario: t }))
|
required
|
||||||
|
>
|
||||||
|
<option value="">Selecione um médico</option>
|
||||||
|
{medicos.map((medico) => (
|
||||||
|
<option key={medico._id} value={medico._id}>
|
||||||
|
{medico.nome} - {medico.especialidade} (R${" "}
|
||||||
|
{medico.valorConsulta})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end pt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setEtapa(2)}
|
||||||
|
disabled={!agendamento.medicoId}
|
||||||
|
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
Próximo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Etapa 2: Seleção de Data e Horário */}
|
||||||
|
{etapa === 2 && (
|
||||||
|
<div className="space-y-4 sm:space-y-6">
|
||||||
|
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
|
||||||
|
<Calendar className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
|
||||||
|
Selecione Data e Horário
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Data da Consulta
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={agendamento.data}
|
||||||
|
onChange={(e) => handleDataChange(e.target.value)}
|
||||||
|
className="form-input text-sm sm:text-base"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Selecione uma data</option>
|
||||||
|
{proximosSeteDias().map((dia) => (
|
||||||
|
<option key={dia.valor} value={dia.valor}>
|
||||||
|
{dia.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{agendamento.data && agendamento.medicoId && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Horários Disponíveis
|
||||||
|
</label>
|
||||||
|
<AvailableSlotsPicker
|
||||||
|
doctorId={agendamento.medicoId}
|
||||||
|
date={agendamento.data}
|
||||||
|
onSelect={(t) =>
|
||||||
|
setAgendamento((prev) => ({ ...prev, horario: t }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setEtapa(1)}
|
||||||
|
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1"
|
||||||
|
>
|
||||||
|
Voltar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setEtapa(3)}
|
||||||
|
disabled={!agendamento.horario}
|
||||||
|
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2"
|
||||||
|
>
|
||||||
|
Próximo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Etapa 3: Informações Adicionais */}
|
||||||
|
{etapa === 3 && (
|
||||||
|
<div className="space-y-4 sm:space-y-6">
|
||||||
|
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
|
||||||
|
<FileText className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
|
||||||
|
Informações da Consulta
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Tipo de Consulta
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={agendamento.tipoConsulta}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAgendamento((prev) => ({
|
||||||
|
...prev,
|
||||||
|
tipoConsulta: e.target.value,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
className="form-input text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
<option value="primeira-vez">Primeira Consulta</option>
|
||||||
|
<option value="retorno">Retorno</option>
|
||||||
|
<option value="urgencia">Urgência</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Motivo da Consulta
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={agendamento.motivoConsulta}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAgendamento((prev) => ({
|
||||||
|
...prev,
|
||||||
|
motivoConsulta: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="form-input text-sm sm:text-base"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Descreva brevemente o motivo da consulta"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
|
<div>
|
||||||
<button
|
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||||
onClick={() => setEtapa(1)}
|
Observações (opcional)
|
||||||
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1"
|
</label>
|
||||||
>
|
<textarea
|
||||||
Voltar
|
value={agendamento.observacoes}
|
||||||
</button>
|
onChange={(e) =>
|
||||||
<button
|
setAgendamento((prev) => ({
|
||||||
onClick={() => setEtapa(3)}
|
...prev,
|
||||||
disabled={!agendamento.horario}
|
observacoes: e.target.value,
|
||||||
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2"
|
}))
|
||||||
>
|
}
|
||||||
Próximo
|
className="form-input text-sm sm:text-base"
|
||||||
</button>
|
rows={2}
|
||||||
</div>
|
placeholder="Informações adicionais relevantes"
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Etapa 3: Informações Adicionais */}
|
{/* Resumo do Agendamento */}
|
||||||
{etapa === 3 && (
|
<div className="bg-gray-50 rounded-lg p-3 sm:p-4 border border-gray-200">
|
||||||
<div className="space-y-4 sm:space-y-6">
|
<h3 className="text-sm sm:text-base font-semibold mb-2 sm:mb-3">
|
||||||
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
|
Resumo do Agendamento:
|
||||||
<FileText className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
|
</h3>
|
||||||
Informações da Consulta
|
<div className="space-y-1 sm:space-y-1.5 text-xs sm:text-sm">
|
||||||
</h2>
|
<p className="break-words">
|
||||||
|
<strong>Paciente:</strong> {pacienteLogado.nome}
|
||||||
|
</p>
|
||||||
|
<p className="break-words">
|
||||||
|
<strong>Médico:</strong> {medicoSelecionado?.nome}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Data:</strong>{" "}
|
||||||
|
{format(new Date(agendamento.data), "dd/MM/yyyy", {
|
||||||
|
locale: ptBR,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Horário:</strong> {agendamento.horario}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Valor:</strong> R${" "}
|
||||||
|
{medicoSelecionado?.valorConsulta}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
|
||||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
<button
|
||||||
Tipo de Consulta
|
onClick={() => setEtapa(2)}
|
||||||
</label>
|
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1"
|
||||||
<select
|
>
|
||||||
value={agendamento.tipoConsulta}
|
Voltar
|
||||||
onChange={(e) =>
|
</button>
|
||||||
setAgendamento((prev) => ({
|
<button
|
||||||
...prev,
|
onClick={confirmarAgendamento}
|
||||||
tipoConsulta: e.target.value,
|
disabled={loading}
|
||||||
}))
|
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2"
|
||||||
}
|
>
|
||||||
className="form-input text-sm sm:text-base"
|
{loading ? "Agendando..." : "Confirmar Agendamento"}
|
||||||
>
|
</button>
|
||||||
<option value="primeira-vez">Primeira Consulta</option>
|
|
||||||
<option value="retorno">Retorno</option>
|
|
||||||
<option value="urgencia">Urgência</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Motivo da Consulta
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={agendamento.motivoConsulta}
|
|
||||||
onChange={(e) =>
|
|
||||||
setAgendamento((prev) => ({
|
|
||||||
...prev,
|
|
||||||
motivoConsulta: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="form-input text-sm sm:text-base"
|
|
||||||
rows={3}
|
|
||||||
placeholder="Descreva brevemente o motivo da consulta"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Observações (opcional)
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={agendamento.observacoes}
|
|
||||||
onChange={(e) =>
|
|
||||||
setAgendamento((prev) => ({
|
|
||||||
...prev,
|
|
||||||
observacoes: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
className="form-input text-sm sm:text-base"
|
|
||||||
rows={2}
|
|
||||||
placeholder="Informações adicionais relevantes"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Resumo do Agendamento */}
|
|
||||||
<div className="bg-gray-50 rounded-lg p-3 sm:p-4 border border-gray-200">
|
|
||||||
<h3 className="text-sm sm:text-base font-semibold mb-2 sm:mb-3">
|
|
||||||
Resumo do Agendamento:
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-1 sm:space-y-1.5 text-xs sm:text-sm">
|
|
||||||
<p className="break-words">
|
|
||||||
<strong>Paciente:</strong> {pacienteLogado.nome}
|
|
||||||
</p>
|
|
||||||
<p className="break-words">
|
|
||||||
<strong>Médico:</strong> {medicoSelecionado?.nome}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Data:</strong>{" "}
|
|
||||||
{format(new Date(agendamento.data), "dd/MM/yyyy", {
|
|
||||||
locale: ptBR,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Horário:</strong> {agendamento.horario}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Valor:</strong> R$ {medicoSelecionado?.valorConsulta}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => setEtapa(2)}
|
|
||||||
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1"
|
|
||||||
>
|
|
||||||
Voltar
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={confirmarAgendamento}
|
|
||||||
disabled={loading}
|
|
||||||
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2"
|
|
||||||
>
|
|
||||||
{loading ? "Agendando..." : "Confirmar Agendamento"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -112,7 +112,10 @@ const Home: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 sm:space-y-8 px-4 sm:px-6 lg:px-8" id="main-content">
|
<div
|
||||||
|
className="space-y-6 sm:space-y-8 px-4 sm:px-6 lg:px-8"
|
||||||
|
id="main-content"
|
||||||
|
>
|
||||||
{/* Componente invisível que detecta tokens de recuperação e redireciona */}
|
{/* Componente invisível que detecta tokens de recuperação e redireciona */}
|
||||||
<RecoveryRedirect />
|
<RecoveryRedirect />
|
||||||
|
|
||||||
@ -257,9 +260,14 @@ const ActionCard: React.FC<ActionCardProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={`w-10 h-10 sm:w-12 sm:h-12 ${iconBgColor} rounded-lg flex items-center justify-center mb-3 sm:mb-4 group-hover:scale-110 transition-transform`}
|
className={`w-10 h-10 sm:w-12 sm:h-12 ${iconBgColor} rounded-lg flex items-center justify-center mb-3 sm:mb-4 group-hover:scale-110 transition-transform`}
|
||||||
>
|
>
|
||||||
<Icon className={`w-5 h-5 sm:w-6 sm:h-6 text-white`} aria-hidden="true" />
|
<Icon
|
||||||
|
className={`w-5 h-5 sm:w-6 sm:h-6 text-white`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base sm:text-lg font-semibold mb-2 text-gray-900">{title}</h3>
|
<h3 className="text-base sm:text-lg font-semibold mb-2 text-gray-900">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
<p className="text-xs sm:text-sm text-gray-600 mb-3 sm:mb-4 leading-relaxed">
|
<p className="text-xs sm:text-sm text-gray-600 mb-3 sm:mb-4 leading-relaxed">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -109,15 +109,20 @@ const ListaPacientes: React.FC = () => {
|
|||||||
|
|
||||||
<div className="space-y-1.5 sm:space-y-2">
|
<div className="space-y-1.5 sm:space-y-2">
|
||||||
<div className="text-xs sm:text-sm text-gray-700">
|
<div className="text-xs sm:text-sm text-gray-700">
|
||||||
<strong className="font-medium">CPF:</strong> {formatCPF(paciente.cpf)}
|
<strong className="font-medium">CPF:</strong>{" "}
|
||||||
|
{formatCPF(paciente.cpf)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-2 text-xs sm:text-sm text-gray-700">
|
<div className="flex items-start gap-2 text-xs sm:text-sm text-gray-700">
|
||||||
<Mail className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0 mt-0.5" />
|
<Mail className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0 mt-0.5" />
|
||||||
<span className="break-all">{formatEmail(paciente.email)}</span>
|
<span className="break-all">
|
||||||
|
{formatEmail(paciente.email)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-xs sm:text-sm text-gray-700">
|
<div className="flex items-center gap-2 text-xs sm:text-sm text-gray-700">
|
||||||
<Phone className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
<Phone className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
|
||||||
<span className="break-words">{formatPhone(paciente.phone_mobile)}</span>
|
<span className="break-words">
|
||||||
|
{formatPhone(paciente.phone_mobile)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs sm:text-sm text-gray-500 pt-1">
|
<div className="text-xs sm:text-sm text-gray-500 pt-1">
|
||||||
<strong className="font-medium">Nascimento:</strong>{" "}
|
<strong className="font-medium">Nascimento:</strong>{" "}
|
||||||
|
|||||||
@ -273,7 +273,9 @@ const PainelAdmin: React.FC = () => {
|
|||||||
const formattedCpf = formatCPF(userCpf);
|
const formattedCpf = formatCPF(userCpf);
|
||||||
|
|
||||||
// Formatar telefone celular se fornecido
|
// Formatar telefone celular se fornecido
|
||||||
const formattedPhoneMobile = userPhoneMobile ? formatPhone(userPhoneMobile) : "";
|
const formattedPhoneMobile = userPhoneMobile
|
||||||
|
? formatPhone(userPhoneMobile)
|
||||||
|
: "";
|
||||||
|
|
||||||
// Criar usuário com senha (método obrigatório com CPF)
|
// Criar usuário com senha (método obrigatório com CPF)
|
||||||
await userService.createUserWithPassword({
|
await userService.createUserWithPassword({
|
||||||
@ -299,10 +301,18 @@ const PainelAdmin: React.FC = () => {
|
|||||||
|
|
||||||
// Mostrar mensagem de erro detalhada
|
// Mostrar mensagem de erro detalhada
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
(error as { response?: { data?: { message?: string; error?: string } }; message?: string })
|
(
|
||||||
?.response?.data?.message ||
|
error as {
|
||||||
(error as { response?: { data?: { message?: string; error?: string } }; message?: string })
|
response?: { data?: { message?: string; error?: string } };
|
||||||
?.response?.data?.error ||
|
message?: string;
|
||||||
|
}
|
||||||
|
)?.response?.data?.message ||
|
||||||
|
(
|
||||||
|
error as {
|
||||||
|
response?: { data?: { message?: string; error?: string } };
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
)?.response?.data?.error ||
|
||||||
(error as { message?: string })?.message ||
|
(error as { message?: string })?.message ||
|
||||||
"Erro ao criar usuário";
|
"Erro ao criar usuário";
|
||||||
|
|
||||||
@ -699,7 +709,9 @@ const PainelAdmin: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Limpar telefone (remover formatação)
|
// Limpar telefone (remover formatação)
|
||||||
const phoneLimpo = medicoData.phone_mobile ? medicoData.phone_mobile.replace(/\D/g, "") : undefined;
|
const phoneLimpo = medicoData.phone_mobile
|
||||||
|
? medicoData.phone_mobile.replace(/\D/g, "")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
console.log("[PainelAdmin] Criando médico com API /create-doctor:", {
|
console.log("[PainelAdmin] Criando médico com API /create-doctor:", {
|
||||||
email: medicoData.email,
|
email: medicoData.email,
|
||||||
@ -841,20 +853,32 @@ const PainelAdmin: React.FC = () => {
|
|||||||
const formatCPF = (value: string): string => {
|
const formatCPF = (value: string): string => {
|
||||||
const numbers = value.replace(/\D/g, "");
|
const numbers = value.replace(/\D/g, "");
|
||||||
if (numbers.length <= 3) return numbers;
|
if (numbers.length <= 3) return numbers;
|
||||||
if (numbers.length <= 6) return `${numbers.slice(0, 3)}.${numbers.slice(3)}`;
|
if (numbers.length <= 6)
|
||||||
|
return `${numbers.slice(0, 3)}.${numbers.slice(3)}`;
|
||||||
if (numbers.length <= 9)
|
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(
|
||||||
return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(6, 9)}-${numbers.slice(9, 11)}`;
|
6
|
||||||
|
)}`;
|
||||||
|
return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(
|
||||||
|
6,
|
||||||
|
9
|
||||||
|
)}-${numbers.slice(9, 11)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Função para formatar telefone ((XX) XXXXX-XXXX)
|
// Função para formatar telefone ((XX) XXXXX-XXXX)
|
||||||
const formatPhone = (value: string): string => {
|
const formatPhone = (value: string): string => {
|
||||||
const numbers = value.replace(/\D/g, "");
|
const numbers = value.replace(/\D/g, "");
|
||||||
if (numbers.length <= 2) return numbers;
|
if (numbers.length <= 2) return numbers;
|
||||||
if (numbers.length <= 7) return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`;
|
if (numbers.length <= 7)
|
||||||
|
return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`;
|
||||||
if (numbers.length <= 11)
|
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(
|
||||||
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(7, 11)}`;
|
7
|
||||||
|
)}`;
|
||||||
|
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(
|
||||||
|
7,
|
||||||
|
11
|
||||||
|
)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Função para obter apenas números do CPF/telefone
|
// Função para obter apenas números do CPF/telefone
|
||||||
@ -1693,18 +1717,26 @@ const PainelAdmin: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{availableRoles.map((role) => (
|
{availableRoles.map((role) => (
|
||||||
<option key={role} value={role}>
|
<option key={role} value={role}>
|
||||||
{role === "paciente" ? "Paciente" :
|
{role === "paciente"
|
||||||
role === "medico" ? "Médico" :
|
? "Paciente"
|
||||||
role === "secretaria" ? "Secretária" :
|
: role === "medico"
|
||||||
role === "admin" ? "Administrador" :
|
? "Médico"
|
||||||
role === "gestor" ? "Gestor" : role}
|
: role === "secretaria"
|
||||||
|
? "Secretária"
|
||||||
|
: role === "admin"
|
||||||
|
? "Administrador"
|
||||||
|
: role === "gestor"
|
||||||
|
? "Gestor"
|
||||||
|
: role}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t pt-4">
|
<div className="border-t pt-4">
|
||||||
<h3 className="text-sm font-semibold mb-3">Campos Opcionais</h3>
|
<h3 className="text-sm font-semibold mb-3">
|
||||||
|
Campos Opcionais
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
@ -1715,7 +1747,10 @@ const PainelAdmin: React.FC = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
value={formUser.phone || ""}
|
value={formUser.phone || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormUser({ ...formUser, phone: formatPhone(e.target.value) })
|
setFormUser({
|
||||||
|
...formUser,
|
||||||
|
phone: formatPhone(e.target.value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
maxLength={15}
|
maxLength={15}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
@ -1730,7 +1765,9 @@ const PainelAdmin: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={userPhoneMobile}
|
value={userPhoneMobile}
|
||||||
onChange={(e) => setUserPhoneMobile(formatPhone(e.target.value))}
|
onChange={(e) =>
|
||||||
|
setUserPhoneMobile(formatPhone(e.target.value))
|
||||||
|
}
|
||||||
maxLength={15}
|
maxLength={15}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
placeholder="(00) 00000-0000"
|
placeholder="(00) 00000-0000"
|
||||||
@ -1894,7 +1931,10 @@ const PainelAdmin: React.FC = () => {
|
|||||||
required
|
required
|
||||||
value={formMedico.cpf}
|
value={formMedico.cpf}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormMedico({ ...formMedico, cpf: formatCPF(e.target.value) })
|
setFormMedico({
|
||||||
|
...formMedico,
|
||||||
|
cpf: formatCPF(e.target.value),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
maxLength={14}
|
maxLength={14}
|
||||||
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
|
||||||
@ -2229,5 +2269,3 @@ const PainelAdmin: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default PainelAdmin;
|
export default PainelAdmin;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -101,7 +101,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
|
|
||||||
// Estados para perfil do médico
|
// Estados para perfil do médico
|
||||||
const [isEditingProfile, setIsEditingProfile] = useState(false);
|
const [isEditingProfile, setIsEditingProfile] = useState(false);
|
||||||
const [profileTab, setProfileTab] = useState<"personal" | "professional" | "security">("personal");
|
const [profileTab, setProfileTab] = useState<
|
||||||
|
"personal" | "professional" | "security"
|
||||||
|
>("personal");
|
||||||
const [profileData, setProfileData] = useState({
|
const [profileData, setProfileData] = useState({
|
||||||
full_name: "",
|
full_name: "",
|
||||||
email: "",
|
email: "",
|
||||||
@ -1114,7 +1116,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
|
|
||||||
{/* Avatar Card */}
|
{/* Avatar Card */}
|
||||||
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
|
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
|
||||||
<h2 className="text-lg font-semibold mb-4 dark:text-white">Foto de Perfil</h2>
|
<h2 className="text-lg font-semibold mb-4 dark:text-white">
|
||||||
|
Foto de Perfil
|
||||||
|
</h2>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
userId={user?.id}
|
userId={user?.id}
|
||||||
@ -1193,7 +1197,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.full_name}
|
value={profileData.full_name}
|
||||||
onChange={(e) => handleProfileChange("full_name", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("full_name", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1206,7 +1212,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={profileData.email}
|
value={profileData.email}
|
||||||
onChange={(e) => handleProfileChange("email", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("email", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1219,7 +1227,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="tel"
|
type="tel"
|
||||||
value={profileData.phone}
|
value={profileData.phone}
|
||||||
onChange={(e) => handleProfileChange("phone", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("phone", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1244,7 +1254,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={profileData.birth_date}
|
value={profileData.birth_date}
|
||||||
onChange={(e) => handleProfileChange("birth_date", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("birth_date", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1256,7 +1268,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={profileData.sex}
|
value={profileData.sex}
|
||||||
onChange={(e) => handleProfileChange("sex", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("sex", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
>
|
>
|
||||||
@ -1270,7 +1284,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Endereço</h3>
|
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
|
||||||
|
Endereço
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
@ -1280,7 +1296,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.street}
|
value={profileData.street}
|
||||||
onChange={(e) => handleProfileChange("street", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("street", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1293,7 +1311,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.number}
|
value={profileData.number}
|
||||||
onChange={(e) => handleProfileChange("number", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("number", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1306,7 +1326,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.complement}
|
value={profileData.complement}
|
||||||
onChange={(e) => handleProfileChange("complement", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("complement", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1319,7 +1341,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.neighborhood}
|
value={profileData.neighborhood}
|
||||||
onChange={(e) => handleProfileChange("neighborhood", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("neighborhood", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1332,7 +1356,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.city}
|
value={profileData.city}
|
||||||
onChange={(e) => handleProfileChange("city", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("city", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1345,7 +1371,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.state}
|
value={profileData.state}
|
||||||
onChange={(e) => handleProfileChange("state", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("state", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
maxLength={2}
|
maxLength={2}
|
||||||
@ -1359,7 +1387,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.cep}
|
value={profileData.cep}
|
||||||
onChange={(e) => handleProfileChange("cep", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("cep", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1385,7 +1415,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.crm}
|
value={profileData.crm}
|
||||||
onChange={(e) => handleProfileChange("crm", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("crm", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1398,7 +1430,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={profileData.specialty}
|
value={profileData.specialty}
|
||||||
onChange={(e) => handleProfileChange("specialty", e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleProfileChange("specialty", e.target.value)
|
||||||
|
}
|
||||||
disabled={!isEditingProfile}
|
disabled={!isEditingProfile}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
/>
|
/>
|
||||||
@ -1468,7 +1502,9 @@ const PainelMedico: React.FC = () => {
|
|||||||
<div className="flex flex-col lg:flex-row min-h-screen bg-gray-50 dark:bg-slate-950">
|
<div className="flex flex-col lg:flex-row min-h-screen bg-gray-50 dark:bg-slate-950">
|
||||||
{renderSidebar()}
|
{renderSidebar()}
|
||||||
<main className="flex-1 overflow-y-auto">
|
<main className="flex-1 overflow-y-auto">
|
||||||
<div className="container mx-auto p-4 sm:p-6 lg:p-8">{renderContent()}</div>
|
<div className="container mx-auto p-4 sm:p-6 lg:p-8">
|
||||||
|
{renderContent()}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
@ -1595,5 +1631,3 @@ const PainelMedico: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default PainelMedico;
|
export default PainelMedico;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -81,9 +81,7 @@ export default function PainelSecretaria() {
|
|||||||
>
|
>
|
||||||
<Icon className="h-3.5 w-3.5 sm:h-4 sm:w-4 flex-shrink-0" />
|
<Icon className="h-3.5 w-3.5 sm:h-4 sm:w-4 flex-shrink-0" />
|
||||||
<span className="hidden sm:inline">{tab.label}</span>
|
<span className="hidden sm:inline">{tab.label}</span>
|
||||||
<span className="sm:hidden">
|
<span className="sm:hidden">{tab.label.split(" ")[0]}</span>
|
||||||
{tab.label.split(' ')[0]}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -97,7 +95,10 @@ export default function PainelSecretaria() {
|
|||||||
<SecretaryPatientList
|
<SecretaryPatientList
|
||||||
onOpenAppointment={(patientId: string) => {
|
onOpenAppointment={(patientId: string) => {
|
||||||
// store selected patient for appointment and switch to consultas tab
|
// store selected patient for appointment and switch to consultas tab
|
||||||
sessionStorage.setItem("selectedPatientForAppointment", patientId);
|
sessionStorage.setItem(
|
||||||
|
"selectedPatientForAppointment",
|
||||||
|
patientId
|
||||||
|
);
|
||||||
setActiveTab("consultas");
|
setActiveTab("consultas");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -118,5 +119,3 @@ export default function PainelSecretaria() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -220,7 +220,9 @@ export default function PerfilMedico() {
|
|||||||
|
|
||||||
{/* Avatar Card */}
|
{/* Avatar Card */}
|
||||||
<div className="bg-white rounded-lg shadow p-4 sm:p-6">
|
<div className="bg-white rounded-lg shadow p-4 sm:p-6">
|
||||||
<h2 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Foto de Perfil</h2>
|
<h2 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">
|
||||||
|
Foto de Perfil
|
||||||
|
</h2>
|
||||||
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6">
|
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6">
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
userId={user?.id}
|
userId={user?.id}
|
||||||
@ -235,7 +237,9 @@ export default function PerfilMedico() {
|
|||||||
<p className="font-medium text-gray-900 text-sm sm:text-base truncate">
|
<p className="font-medium text-gray-900 text-sm sm:text-base truncate">
|
||||||
{formData.full_name}
|
{formData.full_name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500 text-xs sm:text-sm truncate">{formData.specialty}</p>
|
<p className="text-gray-500 text-xs sm:text-sm truncate">
|
||||||
|
{formData.specialty}
|
||||||
|
</p>
|
||||||
<p className="text-xs sm:text-sm text-gray-500 truncate">
|
<p className="text-xs sm:text-sm text-gray-500 truncate">
|
||||||
CRM: {formData.crm} - {formData.crm_state}
|
CRM: {formData.crm} - {formData.crm_state}
|
||||||
</p>
|
</p>
|
||||||
@ -564,5 +568,3 @@ export default function PerfilMedico() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -267,7 +267,9 @@ export default function PerfilPaciente() {
|
|||||||
|
|
||||||
{/* Avatar Card */}
|
{/* Avatar Card */}
|
||||||
<div className="bg-white rounded-lg shadow p-4 sm:p-6">
|
<div className="bg-white rounded-lg shadow p-4 sm:p-6">
|
||||||
<h2 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Foto de Perfil</h2>
|
<h2 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">
|
||||||
|
Foto de Perfil
|
||||||
|
</h2>
|
||||||
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6">
|
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6">
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
userId={user?.id}
|
userId={user?.id}
|
||||||
@ -684,5 +686,3 @@ export default function PerfilPaciente() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -175,7 +175,12 @@ class ApiClient {
|
|||||||
url: string,
|
url: string,
|
||||||
config?: AxiosRequestConfig
|
config?: AxiosRequestConfig
|
||||||
): Promise<AxiosResponse<T>> {
|
): Promise<AxiosResponse<T>> {
|
||||||
console.log("[ApiClient] GET Request:", url, "Params:", JSON.stringify(config?.params));
|
console.log(
|
||||||
|
"[ApiClient] GET Request:",
|
||||||
|
url,
|
||||||
|
"Params:",
|
||||||
|
JSON.stringify(config?.params)
|
||||||
|
);
|
||||||
|
|
||||||
const response = await this.client.get<T>(url, config);
|
const response = await this.client.get<T>(url, config);
|
||||||
|
|
||||||
@ -183,9 +188,14 @@ class ApiClient {
|
|||||||
status: response.status,
|
status: response.status,
|
||||||
dataType: typeof response.data,
|
dataType: typeof response.data,
|
||||||
isArray: Array.isArray(response.data),
|
isArray: Array.isArray(response.data),
|
||||||
dataLength: Array.isArray(response.data) ? response.data.length : 'not array',
|
dataLength: Array.isArray(response.data)
|
||||||
|
? response.data.length
|
||||||
|
: "not array",
|
||||||
});
|
});
|
||||||
console.log("[ApiClient] Response Data:", JSON.stringify(response.data, null, 2));
|
console.log(
|
||||||
|
"[ApiClient] Response Data:",
|
||||||
|
JSON.stringify(response.data, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@ -255,7 +265,16 @@ class ApiClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.client.patch<T>(url, data, config);
|
// Adicionar header Prefer para Supabase retornar os dados atualizados
|
||||||
|
const configWithPrefer = {
|
||||||
|
...config,
|
||||||
|
headers: {
|
||||||
|
...config?.headers,
|
||||||
|
Prefer: "return=representation",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.client.patch<T>(url, data, configWithPrefer);
|
||||||
console.log("[ApiClient] PATCH Response:", {
|
console.log("[ApiClient] PATCH Response:", {
|
||||||
status: response.status,
|
status: response.status,
|
||||||
data: response.data,
|
data: response.data,
|
||||||
|
|||||||
@ -31,20 +31,29 @@ class AppointmentService {
|
|||||||
data
|
data
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("[AppointmentService] Resposta get-available-slots:", response.data);
|
console.log(
|
||||||
|
"[AppointmentService] Resposta get-available-slots:",
|
||||||
|
response.data
|
||||||
|
);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("[AppointmentService] ❌ Erro ao buscar slots:");
|
console.error("[AppointmentService] ❌ Erro ao buscar slots:");
|
||||||
console.error("[AppointmentService] Status:", error?.response?.status);
|
console.error("[AppointmentService] Status:", error?.response?.status);
|
||||||
console.error("[AppointmentService] Response Data:", JSON.stringify(error?.response?.data, null, 2));
|
console.error(
|
||||||
|
"[AppointmentService] Response Data:",
|
||||||
|
JSON.stringify(error?.response?.data, null, 2)
|
||||||
|
);
|
||||||
console.error("[AppointmentService] Message:", error?.message);
|
console.error("[AppointmentService] Message:", error?.message);
|
||||||
console.error("[AppointmentService] Input enviado:", JSON.stringify(data, null, 2));
|
console.error(
|
||||||
|
"[AppointmentService] Input enviado:",
|
||||||
|
JSON.stringify(data, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
error?.response?.data?.message ||
|
error?.response?.data?.message ||
|
||||||
error?.message ||
|
error?.message ||
|
||||||
"Erro ao buscar horários disponíveis"
|
"Erro ao buscar horários disponíveis"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,16 +67,24 @@ class AvailabilityService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
weekday: typeof item.weekday === 'string'
|
weekday:
|
||||||
? weekdayMap[item.weekday.toLowerCase()]
|
typeof item.weekday === "string"
|
||||||
: item.weekday,
|
? weekdayMap[item.weekday.toLowerCase()]
|
||||||
|
: item.weekday,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if (convertedData.length > 0) {
|
if (convertedData.length > 0) {
|
||||||
console.log("[AvailabilityService] ✅ Convertido:", convertedData.length, "registros");
|
console.log(
|
||||||
console.log("[AvailabilityService] Primeiro item convertido:", JSON.stringify(convertedData[0], null, 2));
|
"[AvailabilityService] ✅ Convertido:",
|
||||||
|
convertedData.length,
|
||||||
|
"registros"
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[AvailabilityService] Primeiro item convertido:",
|
||||||
|
JSON.stringify(convertedData[0], null, 2)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertedData;
|
return convertedData;
|
||||||
@ -129,7 +137,10 @@ class AvailabilityService {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("[AvailabilityService] Resposta da atualização:", response.data);
|
console.log(
|
||||||
|
"[AvailabilityService] Resposta da atualização:",
|
||||||
|
response.data
|
||||||
|
);
|
||||||
|
|
||||||
return Array.isArray(response.data) ? response.data[0] : response.data;
|
return Array.isArray(response.data) ? response.data[0] : response.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,14 +61,41 @@ class ReportService {
|
|||||||
* Nota: order_number não pode ser modificado
|
* Nota: order_number não pode ser modificado
|
||||||
*/
|
*/
|
||||||
async update(id: string, data: UpdateReportInput): Promise<Report> {
|
async update(id: string, data: UpdateReportInput): Promise<Report> {
|
||||||
const response = await apiClient.patch<Report[]>(
|
console.log("[ReportService] update() - id:", id, "data:", data);
|
||||||
|
|
||||||
|
const response = await apiClient.patch<Report | Report[]>(
|
||||||
`${this.basePath}?id=eq.${id}`,
|
`${this.basePath}?id=eq.${id}`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (response.data && response.data.length > 0) {
|
|
||||||
return response.data[0];
|
console.log("[ReportService] update() - response status:", response.status);
|
||||||
|
console.log("[ReportService] update() - response.data:", response.data);
|
||||||
|
console.log(
|
||||||
|
"[ReportService] update() - response type:",
|
||||||
|
typeof response.data,
|
||||||
|
"isArray:",
|
||||||
|
Array.isArray(response.data)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Supabase com Prefer: return=representation pode retornar array ou objeto
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
if (response.data.length > 0) {
|
||||||
|
return response.data[0];
|
||||||
|
}
|
||||||
|
// Array vazio - buscar o relatório atualizado
|
||||||
|
console.warn(
|
||||||
|
"[ReportService] update() - Array vazio, buscando relatório..."
|
||||||
|
);
|
||||||
|
return await this.getById(id);
|
||||||
|
} else if (response.data) {
|
||||||
|
return response.data as Report;
|
||||||
}
|
}
|
||||||
throw new Error("Relatório não encontrado");
|
|
||||||
|
// Última tentativa - buscar o relatório
|
||||||
|
console.warn(
|
||||||
|
"[ReportService] update() - Resposta vazia, buscando relatório..."
|
||||||
|
);
|
||||||
|
return await this.getById(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,12 +5,12 @@ export default {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontSize: {
|
fontSize: {
|
||||||
'xs': ['0.75rem', { lineHeight: '1rem' }],
|
xs: ["0.75rem", { lineHeight: "1rem" }],
|
||||||
'sm': ['0.875rem', { lineHeight: '1.25rem' }],
|
sm: ["0.875rem", { lineHeight: "1.25rem" }],
|
||||||
'base': ['1rem', { lineHeight: '1.5rem' }],
|
base: ["1rem", { lineHeight: "1.5rem" }],
|
||||||
'lg': ['1.125rem', { lineHeight: '1.75rem' }],
|
lg: ["1.125rem", { lineHeight: "1.75rem" }],
|
||||||
'xl': ['1.25rem', { lineHeight: '1.75rem' }],
|
xl: ["1.25rem", { lineHeight: "1.75rem" }],
|
||||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
"2xl": ["1.5rem", { lineHeight: "2rem" }],
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: "hsl(var(--border))",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user