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 [bookingError, setBookingError] = useState("");
|
||||
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());
|
||||
|
||||
// Removido o carregamento interno de médicos, pois agora vem por prop
|
||||
@ -102,64 +102,77 @@ export default function AgendamentoConsulta({
|
||||
|
||||
try {
|
||||
const { availabilityService } = await import("../services");
|
||||
|
||||
console.log("[AgendamentoConsulta] Buscando disponibilidades para médico:", {
|
||||
id: selectedMedico.id,
|
||||
nome: selectedMedico.nome
|
||||
});
|
||||
|
||||
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Buscando disponibilidades para médico:",
|
||||
{
|
||||
id: selectedMedico.id,
|
||||
nome: selectedMedico.nome,
|
||||
}
|
||||
);
|
||||
|
||||
// Busca todas as disponibilidades ativas do médico
|
||||
const availabilities = await availabilityService.list({
|
||||
doctor_id: selectedMedico.id,
|
||||
active: true,
|
||||
});
|
||||
|
||||
console.log("[AgendamentoConsulta] Disponibilidades retornadas da API:", {
|
||||
count: availabilities?.length || 0,
|
||||
data: availabilities
|
||||
});
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Disponibilidades retornadas da API:",
|
||||
{
|
||||
count: availabilities?.length || 0,
|
||||
data: availabilities,
|
||||
}
|
||||
);
|
||||
|
||||
if (!availabilities || availabilities.length === 0) {
|
||||
console.warn("[AgendamentoConsulta] Nenhuma disponibilidade encontrada para o médico");
|
||||
console.warn(
|
||||
"[AgendamentoConsulta] Nenhuma disponibilidade encontrada para o médico"
|
||||
);
|
||||
setAvailableDates(new Set());
|
||||
return;
|
||||
}
|
||||
|
||||
// Mapeamento de string para número (formato da API)
|
||||
const weekdayMap: Record<string, number> = {
|
||||
"sunday": 0,
|
||||
"monday": 1,
|
||||
"tuesday": 2,
|
||||
"wednesday": 3,
|
||||
"thursday": 4,
|
||||
"friday": 5,
|
||||
"saturday": 6
|
||||
sunday: 0,
|
||||
monday: 1,
|
||||
tuesday: 2,
|
||||
wednesday: 3,
|
||||
thursday: 4,
|
||||
friday: 5,
|
||||
saturday: 6,
|
||||
};
|
||||
|
||||
// Mapeia os dias da semana que o médico atende (converte para número)
|
||||
const availableWeekdays = new Set<number>(
|
||||
availabilities.map((avail) => {
|
||||
// weekday pode vir como número ou string da API
|
||||
let weekdayNum: number;
|
||||
|
||||
if (typeof avail.weekday === 'number') {
|
||||
weekdayNum = avail.weekday;
|
||||
} else if (typeof avail.weekday === 'string') {
|
||||
weekdayNum = weekdayMap[avail.weekday.toLowerCase()] ?? -1;
|
||||
} else {
|
||||
weekdayNum = -1;
|
||||
}
|
||||
|
||||
console.log("[AgendamentoConsulta] Convertendo weekday:", {
|
||||
original: avail.weekday,
|
||||
type: typeof avail.weekday,
|
||||
converted: weekdayNum
|
||||
});
|
||||
return weekdayNum;
|
||||
}).filter(day => day >= 0 && day <= 6) // Remove valores inválidos
|
||||
availabilities
|
||||
.map((avail) => {
|
||||
// weekday pode vir como número ou string da API
|
||||
let weekdayNum: number;
|
||||
|
||||
if (typeof avail.weekday === "number") {
|
||||
weekdayNum = avail.weekday;
|
||||
} else if (typeof avail.weekday === "string") {
|
||||
weekdayNum = weekdayMap[avail.weekday.toLowerCase()] ?? -1;
|
||||
} else {
|
||||
weekdayNum = -1;
|
||||
}
|
||||
|
||||
console.log("[AgendamentoConsulta] Convertendo weekday:", {
|
||||
original: avail.weekday,
|
||||
type: typeof avail.weekday,
|
||||
converted: weekdayNum,
|
||||
});
|
||||
return weekdayNum;
|
||||
})
|
||||
.filter((day) => day >= 0 && day <= 6) // Remove valores inválidos
|
||||
);
|
||||
|
||||
console.log("[AgendamentoConsulta] Dias da semana disponíveis (números):", Array.from(availableWeekdays));
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Dias da semana disponíveis (números):",
|
||||
Array.from(availableWeekdays)
|
||||
);
|
||||
|
||||
// Calcula todas as datas do mês atual e próximos 2 meses que têm disponibilidade
|
||||
const today = startOfDay(new Date());
|
||||
@ -167,7 +180,7 @@ export default function AgendamentoConsulta({
|
||||
const allDates = eachDayOfInterval({ start: today, end: endDate });
|
||||
|
||||
const availableDatesSet = new Set<string>();
|
||||
|
||||
|
||||
allDates.forEach((date) => {
|
||||
const weekday = date.getDay();
|
||||
if (availableWeekdays.has(weekday) && !isBefore(date, today)) {
|
||||
@ -178,13 +191,15 @@ export default function AgendamentoConsulta({
|
||||
console.log("[AgendamentoConsulta] Resumo do cálculo:", {
|
||||
weekdaysDisponiveis: Array.from(availableWeekdays),
|
||||
datasCalculadas: availableDatesSet.size,
|
||||
primeiras5Datas: Array.from(availableDatesSet).slice(0, 5)
|
||||
primeiras5Datas: Array.from(availableDatesSet).slice(0, 5),
|
||||
});
|
||||
|
||||
setAvailableDates(availableDatesSet);
|
||||
|
||||
setAvailableDates(availableDatesSet);
|
||||
} catch (error) {
|
||||
console.error("[AgendamentoConsulta] Erro ao carregar disponibilidades:", error);
|
||||
console.error(
|
||||
"[AgendamentoConsulta] Erro ao carregar disponibilidades:",
|
||||
error
|
||||
);
|
||||
setAvailableDates(new Set());
|
||||
}
|
||||
};
|
||||
@ -203,7 +218,7 @@ export default function AgendamentoConsulta({
|
||||
|
||||
try {
|
||||
const dateStr = format(selectedDate, "yyyy-MM-dd");
|
||||
|
||||
|
||||
console.log("[AgendamentoConsulta] Buscando slots disponíveis:", {
|
||||
doctor_id: selectedMedico.id,
|
||||
doctor_name: selectedMedico.nome,
|
||||
@ -212,19 +227,24 @@ export default function AgendamentoConsulta({
|
||||
|
||||
// NOTA: Edge Function get-available-slots não está disponível no Supabase
|
||||
// Calculando slots localmente
|
||||
|
||||
|
||||
// Busca a disponibilidade do médico do Supabase
|
||||
const { availabilityService } = await import("../services");
|
||||
|
||||
|
||||
const availabilities = await availabilityService.list({
|
||||
doctor_id: selectedMedico.id,
|
||||
active: true,
|
||||
});
|
||||
|
||||
console.log("[AgendamentoConsulta] Disponibilidades do médico:", availabilities);
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Disponibilidades do médico:",
|
||||
availabilities
|
||||
);
|
||||
|
||||
if (!availabilities || availabilities.length === 0) {
|
||||
console.warn("[AgendamentoConsulta] Nenhuma disponibilidade configurada para este médico");
|
||||
console.warn(
|
||||
"[AgendamentoConsulta] Nenhuma disponibilidade configurada para este médico"
|
||||
);
|
||||
setAvailableSlots([]);
|
||||
return;
|
||||
}
|
||||
@ -232,26 +252,34 @@ export default function AgendamentoConsulta({
|
||||
// Pega o dia da semana da data selecionada
|
||||
const weekdayMap: Record<number, string> = {
|
||||
0: "sunday",
|
||||
1: "monday",
|
||||
1: "monday",
|
||||
2: "tuesday",
|
||||
3: "wednesday",
|
||||
4: "thursday",
|
||||
5: "friday",
|
||||
6: "saturday",
|
||||
};
|
||||
|
||||
|
||||
const dayOfWeek = weekdayMap[selectedDate.getDay()];
|
||||
console.log("[AgendamentoConsulta] Dia da semana selecionado:", dayOfWeek);
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Dia da semana selecionado:",
|
||||
dayOfWeek
|
||||
);
|
||||
|
||||
// Filtra disponibilidades para o dia da semana
|
||||
const dayAvailability = availabilities.filter(
|
||||
(avail) => avail.weekday === dayOfWeek && avail.active
|
||||
);
|
||||
|
||||
console.log("[AgendamentoConsulta] Disponibilidades para o dia:", dayAvailability);
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Disponibilidades para o dia:",
|
||||
dayAvailability
|
||||
);
|
||||
|
||||
if (dayAvailability.length === 0) {
|
||||
console.warn("[AgendamentoConsulta] Médico não atende neste dia da semana");
|
||||
console.warn(
|
||||
"[AgendamentoConsulta] Médico não atende neste dia da semana"
|
||||
);
|
||||
setAvailableSlots([]);
|
||||
return;
|
||||
}
|
||||
@ -266,14 +294,16 @@ export default function AgendamentoConsulta({
|
||||
// Converte para minutos desde meia-noite
|
||||
const [startHour, startMin] = startTime.split(":").map(Number);
|
||||
const [endHour, endMin] = endTime.split(":").map(Number);
|
||||
|
||||
|
||||
let currentMinutes = startHour * 60 + startMin;
|
||||
const endMinutes = endHour * 60 + endMin;
|
||||
|
||||
while (currentMinutes < endMinutes) {
|
||||
const hours = Math.floor(currentMinutes / 60);
|
||||
const minutes = currentMinutes % 60;
|
||||
const timeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
||||
const timeStr = `${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
allSlots.push(timeStr);
|
||||
currentMinutes += slotMinutes;
|
||||
}
|
||||
@ -284,7 +314,10 @@ export default function AgendamentoConsulta({
|
||||
doctor_id: selectedMedico.id,
|
||||
});
|
||||
|
||||
console.log("[AgendamentoConsulta] Agendamentos existentes:", appointments);
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Agendamentos existentes:",
|
||||
appointments
|
||||
);
|
||||
|
||||
// Filtra agendamentos para a data selecionada
|
||||
const bookedSlots = appointments
|
||||
@ -309,7 +342,10 @@ export default function AgendamentoConsulta({
|
||||
(slot) => !bookedSlots.includes(slot)
|
||||
);
|
||||
|
||||
console.log("[AgendamentoConsulta] Slots disponíveis calculados:", availableSlots);
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Slots disponíveis calculados:",
|
||||
availableSlots
|
||||
);
|
||||
|
||||
setAvailableSlots(availableSlots);
|
||||
} catch (error) {
|
||||
@ -342,13 +378,13 @@ export default function AgendamentoConsulta({
|
||||
|
||||
const handlePrevMonth = () => setCurrentMonth(subMonths(currentMonth, 1));
|
||||
const handleNextMonth = () => setCurrentMonth(addMonths(currentMonth, 1));
|
||||
|
||||
|
||||
const handleMonthChange = (monthIndex: number) => {
|
||||
const newDate = new Date(currentMonth);
|
||||
newDate.setMonth(monthIndex);
|
||||
setCurrentMonth(newDate);
|
||||
};
|
||||
|
||||
|
||||
const handleYearChange = (year: number) => {
|
||||
const newDate = new Date(currentMonth);
|
||||
newDate.setFullYear(year);
|
||||
@ -356,8 +392,11 @@ export default function AgendamentoConsulta({
|
||||
};
|
||||
|
||||
// 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) => {
|
||||
setSelectedMedico(medico);
|
||||
setSelectedDate(undefined);
|
||||
@ -365,12 +404,12 @@ export default function AgendamentoConsulta({
|
||||
setMotivo("");
|
||||
setBookingSuccess(false);
|
||||
setBookingError("");
|
||||
|
||||
|
||||
// Scroll suave para a seção de detalhes
|
||||
setTimeout(() => {
|
||||
detailsRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
detailsRef.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
@ -393,32 +432,40 @@ export default function AgendamentoConsulta({
|
||||
doctor_id: selectedMedico.id,
|
||||
scheduled_at: scheduledAt,
|
||||
duration_minutes: 30,
|
||||
appointment_type:
|
||||
(appointmentType === "online" ? "telemedicina" : "presencial") as "presencial" | "telemedicina",
|
||||
appointment_type: (appointmentType === "online"
|
||||
? "telemedicina"
|
||||
: "presencial") as "presencial" | "telemedicina",
|
||||
chief_complaint: motivo,
|
||||
};
|
||||
|
||||
console.log("[AgendamentoConsulta] Criando agendamento com dados:", appointmentData);
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Criando agendamento com dados:",
|
||||
appointmentData
|
||||
);
|
||||
|
||||
// Cria o agendamento usando a API REST
|
||||
const appointment = await appointmentService.create(appointmentData);
|
||||
|
||||
console.log("[AgendamentoConsulta] Consulta criada com sucesso:", appointment);
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Consulta criada com sucesso:",
|
||||
appointment
|
||||
);
|
||||
|
||||
// Mostra modal de sucesso
|
||||
setResultType('success');
|
||||
setResultType("success");
|
||||
setShowResultModal(true);
|
||||
setShowConfirmDialog(false);
|
||||
setBookingSuccess(true);
|
||||
} catch (error: unknown) {
|
||||
console.error("[AgendamentoConsulta] Erro ao agendar:", error);
|
||||
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: "Erro ao agendar consulta. Tente novamente.";
|
||||
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Erro ao agendar consulta. Tente novamente.";
|
||||
|
||||
// Mostra modal de erro
|
||||
setResultType('error');
|
||||
setResultType("error");
|
||||
setShowResultModal(true);
|
||||
setBookingError(errorMessage);
|
||||
setShowConfirmDialog(false);
|
||||
@ -435,38 +482,49 @@ export default function AgendamentoConsulta({
|
||||
<div className="flex flex-col items-center text-center space-y-4">
|
||||
{/* Ícone com Animação Giratória (1 volta) */}
|
||||
<div className="relative">
|
||||
<div className={`absolute inset-0 rounded-full animate-pulse-ring ${
|
||||
resultType === 'success' ? 'bg-blue-100' : 'bg-red-100'
|
||||
}`}></div>
|
||||
<div className={`relative rounded-full p-4 sm:p-5 ${
|
||||
resultType === 'success' ? 'bg-blue-500' : 'bg-red-500'
|
||||
}`}>
|
||||
{resultType === 'success' ? (
|
||||
<div
|
||||
className={`absolute inset-0 rounded-full animate-pulse-ring ${
|
||||
resultType === "success" ? "bg-blue-100" : "bg-red-100"
|
||||
}`}
|
||||
></div>
|
||||
<div
|
||||
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" />
|
||||
) : (
|
||||
<AlertCircle className="h-12 w-12 sm:h-16 sm:w-16 text-white animate-spin-once" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Mensagem */}
|
||||
<div className="space-y-2">
|
||||
<h3 className={`text-xl sm:text-2xl font-bold ${
|
||||
resultType === 'success' ? 'text-blue-900' : 'text-red-900'
|
||||
}`}>
|
||||
{resultType === 'success' ? 'Consulta Agendada!' : 'Erro no Agendamento'}
|
||||
<h3
|
||||
className={`text-xl sm:text-2xl font-bold ${
|
||||
resultType === "success" ? "text-blue-900" : "text-red-900"
|
||||
}`}
|
||||
>
|
||||
{resultType === "success"
|
||||
? "Consulta Agendada!"
|
||||
: "Erro no Agendamento"}
|
||||
</h3>
|
||||
{resultType === 'success' ? (
|
||||
{resultType === "success" ? (
|
||||
<div className="space-y-2">
|
||||
<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>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowResultModal(false);
|
||||
setBookingSuccess(false);
|
||||
setBookingError('');
|
||||
navigate('/acompanhamento', { state: { activeTab: 'consultas' } });
|
||||
setBookingError("");
|
||||
navigate("/acompanhamento", {
|
||||
state: { activeTab: "consultas" },
|
||||
});
|
||||
}}
|
||||
className="text-blue-600 hover:text-blue-800 underline text-sm sm:text-base font-medium"
|
||||
>
|
||||
@ -475,19 +533,20 @@ export default function AgendamentoConsulta({
|
||||
</div>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Botão OK */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowResultModal(false);
|
||||
setBookingSuccess(false);
|
||||
setBookingError('');
|
||||
setBookingError("");
|
||||
// Limpa o formulário se for sucesso
|
||||
if (resultType === 'success') {
|
||||
if (resultType === "success") {
|
||||
setSelectedMedico(null);
|
||||
setSelectedDate(undefined);
|
||||
setSelectedTime("");
|
||||
@ -495,9 +554,9 @@ export default function AgendamentoConsulta({
|
||||
}
|
||||
}}
|
||||
className={`w-full font-semibold py-3 px-6 rounded-lg transition-colors ${
|
||||
resultType === 'success'
|
||||
? 'bg-blue-500 hover:bg-blue-600 text-white'
|
||||
: 'bg-red-500 hover:bg-red-600 text-white'
|
||||
resultType === "success"
|
||||
? "bg-blue-500 hover:bg-blue-600 text-white"
|
||||
: "bg-red-500 hover:bg-red-600 text-white"
|
||||
}`}
|
||||
>
|
||||
OK, Entendi!
|
||||
@ -506,7 +565,7 @@ export default function AgendamentoConsulta({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
<div>
|
||||
<h1 className="text-xl sm:text-2xl font-bold">Agendar Consulta</h1>
|
||||
<p className="text-sm sm:text-base text-muted-foreground">
|
||||
@ -531,7 +590,9 @@ export default function AgendamentoConsulta({
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
value={selectedSpecialty}
|
||||
onChange={(e) => setSelectedSpecialty(e.target.value)}
|
||||
@ -552,7 +613,9 @@ export default function AgendamentoConsulta({
|
||||
<div
|
||||
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 ${
|
||||
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">
|
||||
@ -565,17 +628,25 @@ export default function AgendamentoConsulta({
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 space-y-1.5 sm:space-y-2 w-full">
|
||||
<div>
|
||||
<h3 className="text-sm sm:text-base font-semibold truncate">{medico.nome}</h3>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground truncate">{medico.especialidade}</p>
|
||||
<h3 className="text-sm sm:text-base font-semibold truncate">
|
||||
{medico.nome}
|
||||
</h3>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground truncate">
|
||||
{medico.especialidade}
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
{medico.valorConsulta ? (
|
||||
<span className="whitespace-nowrap">R$ {medico.valorConsulta.toFixed(2)}</span>
|
||||
<span className="whitespace-nowrap">
|
||||
R$ {medico.valorConsulta.toFixed(2)}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<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">
|
||||
<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"
|
||||
@ -592,9 +663,14 @@ export default function AgendamentoConsulta({
|
||||
))}
|
||||
</div>
|
||||
{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>
|
||||
<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">
|
||||
Consulta com {selectedMedico.nome} -{" "}
|
||||
{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" />
|
||||
<span className="text-sm sm:text-base font-medium">Presencial</span>
|
||||
<span className="text-sm sm:text-base font-medium">
|
||||
Presencial
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
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="space-y-3 sm:space-y-4">
|
||||
<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="flex items-center justify-between gap-2 mb-3 sm:mb-4">
|
||||
<button
|
||||
@ -637,11 +717,13 @@ export default function AgendamentoConsulta({
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 sm:h-5 sm:w-5" />
|
||||
</button>
|
||||
|
||||
|
||||
<div className="flex items-center gap-2 flex-1 justify-center">
|
||||
<select
|
||||
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"
|
||||
>
|
||||
<option value={0}>Janeiro</option>
|
||||
@ -657,10 +739,12 @@ export default function AgendamentoConsulta({
|
||||
<option value={10}>Novembro</option>
|
||||
<option value={11}>Dezembro</option>
|
||||
</select>
|
||||
|
||||
|
||||
<select
|
||||
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"
|
||||
>
|
||||
{availableYears.map((year) => (
|
||||
@ -670,7 +754,7 @@ export default function AgendamentoConsulta({
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<button
|
||||
onClick={handleNextMonth}
|
||||
className="p-1.5 sm:p-2 hover:bg-gray-100 rounded-lg flex-shrink-0"
|
||||
@ -681,16 +765,14 @@ export default function AgendamentoConsulta({
|
||||
</div>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<div className="grid grid-cols-7 bg-gray-50">
|
||||
{["D", "S", "T", "Q", "Q", "S", "S"].map(
|
||||
(day, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="text-center py-1.5 sm:py-2 text-xs sm:text-sm font-medium text-gray-600"
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{["D", "S", "T", "Q", "Q", "S", "S"].map((day, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="text-center py-1.5 sm:py-2 text-xs sm:text-sm font-medium text-gray-600"
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-7">
|
||||
{calendarDays.map((day, index) => {
|
||||
@ -698,24 +780,36 @@ export default function AgendamentoConsulta({
|
||||
const isSelected =
|
||||
selectedDate && isSameDay(day, selectedDate);
|
||||
const isPast = isBefore(day, startOfDay(new Date()));
|
||||
|
||||
|
||||
// Verifica se a data está no conjunto de datas disponíveis
|
||||
const dateStr = format(day, "yyyy-MM-dd");
|
||||
const isAvailable = isCurrentMonth && !isPast && availableDates.has(dateStr);
|
||||
const isUnavailable = isCurrentMonth && !isPast && !availableDates.has(dateStr);
|
||||
|
||||
const isAvailable =
|
||||
isCurrentMonth &&
|
||||
!isPast &&
|
||||
availableDates.has(dateStr);
|
||||
const isUnavailable =
|
||||
isCurrentMonth &&
|
||||
!isPast &&
|
||||
!availableDates.has(dateStr);
|
||||
|
||||
// Debug apenas para o primeiro dia do mês atual
|
||||
if (index === 0 && isCurrentMonth) {
|
||||
console.log("[AgendamentoConsulta] Debug calendário:", {
|
||||
totalDatasDisponiveis: availableDates.size,
|
||||
primeiraData: dateStr,
|
||||
diaDaSemana: day.getDay(),
|
||||
isAvailable,
|
||||
isUnavailable,
|
||||
datas5Primeiras: Array.from(availableDates).slice(0, 5)
|
||||
});
|
||||
console.log(
|
||||
"[AgendamentoConsulta] Debug calendário:",
|
||||
{
|
||||
totalDatasDisponiveis: availableDates.size,
|
||||
primeiraData: dateStr,
|
||||
diaDaSemana: day.getDay(),
|
||||
isAvailable,
|
||||
isUnavailable,
|
||||
datas5Primeiras: Array.from(availableDates).slice(
|
||||
0,
|
||||
5
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
@ -748,9 +842,17 @@ export default function AgendamentoConsulta({
|
||||
</div>
|
||||
</div>
|
||||
<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><span className="text-red-600 font-semibold">●</span> Datas indisponíveis</p>
|
||||
<p><span className="text-gray-400">●</span> Datas passadas</p>
|
||||
<p>
|
||||
<span className="text-blue-600 font-semibold">●</span>{" "}
|
||||
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>
|
||||
@ -846,7 +948,9 @@ export default function AgendamentoConsulta({
|
||||
{showConfirmDialog && (
|
||||
<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">
|
||||
<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">
|
||||
Revise os detalhes da sua consulta antes de confirmar
|
||||
</p>
|
||||
@ -908,5 +1012,3 @@ export default function AgendamentoConsulta({
|
||||
</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"
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-medium hidden sm:inline">Precisa de ajuda?</span>
|
||||
<span className="font-medium hidden sm:inline">
|
||||
Precisa de ajuda?
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
@ -279,7 +279,9 @@ const DisponibilidadeMedico: React.FC = () => {
|
||||
doctor_id: doctorId,
|
||||
date: exceptionForm.date,
|
||||
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,
|
||||
reason: exceptionForm.reason || null,
|
||||
created_by: user?.id || doctorId,
|
||||
@ -462,7 +464,10 @@ const DisponibilidadeMedico: React.FC = () => {
|
||||
type="date"
|
||||
value={exceptionForm.date}
|
||||
onChange={(e) =>
|
||||
setExceptionForm({ ...exceptionForm, date: e.target.value })
|
||||
setExceptionForm({
|
||||
...exceptionForm,
|
||||
date: e.target.value,
|
||||
})
|
||||
}
|
||||
className="form-input w-full"
|
||||
/>
|
||||
@ -476,7 +481,9 @@ const DisponibilidadeMedico: React.FC = () => {
|
||||
onChange={(e) =>
|
||||
setExceptionForm({
|
||||
...exceptionForm,
|
||||
kind: e.target.value as "bloqueio" | "disponibilidade_extra",
|
||||
kind: e.target.value as
|
||||
| "bloqueio"
|
||||
| "disponibilidade_extra",
|
||||
})
|
||||
}
|
||||
className="form-input w-full"
|
||||
@ -554,7 +561,10 @@ const DisponibilidadeMedico: React.FC = () => {
|
||||
<textarea
|
||||
value={exceptionForm.reason}
|
||||
onChange={(e) =>
|
||||
setExceptionForm({ ...exceptionForm, reason: e.target.value })
|
||||
setExceptionForm({
|
||||
...exceptionForm,
|
||||
reason: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="Ex: Férias, Feriado, Plantão extra..."
|
||||
className="form-input w-full"
|
||||
@ -617,7 +627,9 @@ const DisponibilidadeMedico: React.FC = () => {
|
||||
onClick={async () => {
|
||||
if (!exception.id) return;
|
||||
try {
|
||||
await availabilityService.deleteException(exception.id);
|
||||
await availabilityService.deleteException(
|
||||
exception.id
|
||||
);
|
||||
toast.success("Exceção removida");
|
||||
loadExceptions();
|
||||
} catch (error) {
|
||||
@ -681,7 +693,10 @@ const DisponibilidadeMedico: React.FC = () => {
|
||||
<select
|
||||
value={newSlot.slotMinutes}
|
||||
onChange={(e) =>
|
||||
setNewSlot({ ...newSlot, slotMinutes: parseInt(e.target.value) })
|
||||
setNewSlot({
|
||||
...newSlot,
|
||||
slotMinutes: parseInt(e.target.value),
|
||||
})
|
||||
}
|
||||
className="form-input w-full"
|
||||
>
|
||||
|
||||
@ -34,10 +34,15 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
||||
active: true,
|
||||
});
|
||||
|
||||
console.log("📅 [AvailableSlotsPicker] Disponibilidades:", availabilities);
|
||||
console.log(
|
||||
"📅 [AvailableSlotsPicker] Disponibilidades:",
|
||||
availabilities
|
||||
);
|
||||
|
||||
if (!availabilities || availabilities.length === 0) {
|
||||
console.warn("[AvailableSlotsPicker] Nenhuma disponibilidade configurada");
|
||||
console.warn(
|
||||
"[AvailableSlotsPicker] Nenhuma disponibilidade configurada"
|
||||
);
|
||||
setSlots([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
@ -54,10 +59,15 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
||||
(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) {
|
||||
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([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
@ -80,7 +90,9 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
||||
while (currentMinutes < endMinutes) {
|
||||
const hours = Math.floor(currentMinutes / 60);
|
||||
const minutes = currentMinutes % 60;
|
||||
const timeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
||||
const timeStr = `${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
allSlots.push(timeStr);
|
||||
currentMinutes += slotMinutes;
|
||||
}
|
||||
@ -91,7 +103,10 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
||||
doctor_id: doctorId,
|
||||
});
|
||||
|
||||
console.log("[AvailableSlotsPicker] Agendamentos existentes:", appointments);
|
||||
console.log(
|
||||
"[AvailableSlotsPicker] Agendamentos existentes:",
|
||||
appointments
|
||||
);
|
||||
|
||||
// Filtra agendamentos para a data selecionada
|
||||
const bookedSlots = (Array.isArray(appointments) ? appointments : [])
|
||||
@ -109,15 +124,22 @@ const AvailableSlotsPicker: React.FC<Props> = ({
|
||||
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
|
||||
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);
|
||||
setLoading(false);
|
||||
|
||||
} catch (error) {
|
||||
console.error("❌ [AvailableSlotsPicker] Erro ao buscar slots:", error);
|
||||
setLoading(false);
|
||||
|
||||
@ -1,6 +1,16 @@
|
||||
import { useState, useEffect } from "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 { availabilityService, appointmentService } from "../../services";
|
||||
import type { DoctorAvailability, DoctorException } from "../../services";
|
||||
@ -19,17 +29,25 @@ interface DayStatus {
|
||||
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 [availabilities, setAvailabilities] = useState<DoctorAvailability[]>([]);
|
||||
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>(
|
||||
[]
|
||||
);
|
||||
const [exceptions, setExceptions] = useState<DoctorException[]>([]);
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!doctorId) return;
|
||||
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@ -37,7 +55,7 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
||||
availabilityService.list({ doctor_id: doctorId, active: true }),
|
||||
availabilityService.listExceptions({ doctor_id: doctorId }),
|
||||
]);
|
||||
|
||||
|
||||
setAvailabilities(Array.isArray(availData) ? availData : []);
|
||||
setExceptions(Array.isArray(exceptData) ? exceptData : []);
|
||||
} catch (error) {
|
||||
@ -72,7 +90,9 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
||||
// Buscar todos os agendamentos do médico uma vez só
|
||||
let allAppointments: Array<{ scheduled_at: string; status: string }> = [];
|
||||
try {
|
||||
const appointments = await appointmentService.list({ doctor_id: doctorId });
|
||||
const appointments = await appointmentService.list({
|
||||
doctor_id: doctorId,
|
||||
});
|
||||
allAppointments = Array.isArray(appointments) ? appointments : [];
|
||||
} catch (error) {
|
||||
console.error("[CalendarPicker] Erro ao buscar agendamentos:", error);
|
||||
@ -120,7 +140,9 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
||||
while (currentMinutes < endMinutes) {
|
||||
const hours = Math.floor(currentMinutes / 60);
|
||||
const minutes = currentMinutes % 60;
|
||||
const timeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
||||
const timeStr = `${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
allSlots.push(timeStr);
|
||||
currentMinutes += slotMinutes;
|
||||
}
|
||||
@ -143,11 +165,18 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
@ -185,7 +214,8 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
||||
};
|
||||
|
||||
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) {
|
||||
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">
|
||||
{/* Cabeçalho dos dias da semana */}
|
||||
{["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}
|
||||
</div>
|
||||
))}
|
||||
@ -257,11 +290,18 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
|
||||
const classes = getDayClasses(status, isSelected);
|
||||
|
||||
return (
|
||||
<div key={format(day, "yyyy-MM-dd")} className="flex justify-center">
|
||||
<div
|
||||
key={format(day, "yyyy-MM-dd")}
|
||||
className="flex justify-center"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleDayClick(day, status)}
|
||||
disabled={status.isPast || status.hasBlockException || (!status.hasAvailability && !status.available)}
|
||||
disabled={
|
||||
status.isPast ||
|
||||
status.hasBlockException ||
|
||||
(!status.hasAvailability && !status.available)
|
||||
}
|
||||
className={classes}
|
||||
title={
|
||||
status.isPast
|
||||
|
||||
@ -101,8 +101,11 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
||||
// Convert ISO to date and time
|
||||
try {
|
||||
const d = new Date(editing.scheduled_at);
|
||||
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 dateStr = d.toISOString().split("T")[0]; // YYYY-MM-DD
|
||||
const timeStr = `${d.getHours().toString().padStart(2, "0")}:${d
|
||||
.getMinutes()
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
setSelectedDate(dateStr);
|
||||
setSelectedTime(timeStr);
|
||||
} catch {
|
||||
@ -170,9 +173,18 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
||||
if (editing) {
|
||||
const payload = {
|
||||
scheduled_at: iso,
|
||||
appointment_type: (tipo || "presencial") as "presencial" | "telemedicina",
|
||||
appointment_type: (tipo || "presencial") as
|
||||
| "presencial"
|
||||
| "telemedicina",
|
||||
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);
|
||||
onSaved(updated);
|
||||
@ -181,7 +193,9 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
||||
patient_id: pacienteId,
|
||||
doctor_id: medicoId,
|
||||
scheduled_at: iso,
|
||||
appointment_type: (tipo || "presencial") as "presencial" | "telemedicina",
|
||||
appointment_type: (tipo || "presencial") as
|
||||
| "presencial"
|
||||
| "telemedicina",
|
||||
notes: observacoes || undefined,
|
||||
};
|
||||
const created = await appointmentService.create(payload);
|
||||
@ -279,7 +293,12 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
|
||||
{selectedDate && medicoId && (
|
||||
<div>
|
||||
<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>
|
||||
<AvailableSlotsPicker
|
||||
doctorId={medicoId}
|
||||
|
||||
@ -28,9 +28,8 @@ export function SecretaryAppointmentList() {
|
||||
const [typeFilter, setTypeFilter] = useState("Todos");
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
|
||||
const [selectedAppointment, setSelectedAppointment] = useState<
|
||||
AppointmentWithDetails | null
|
||||
>(null);
|
||||
const [selectedAppointment, setSelectedAppointment] =
|
||||
useState<AppointmentWithDetails | null>(null);
|
||||
const [patients, setPatients] = useState<Patient[]>([]);
|
||||
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
||||
const [formData, setFormData] = useState<any>({
|
||||
@ -129,7 +128,7 @@ export function SecretaryAppointmentList() {
|
||||
Confirmada: "confirmed",
|
||||
Agendada: "requested",
|
||||
Cancelada: "cancelled",
|
||||
"Concluída": "completed",
|
||||
Concluída: "completed",
|
||||
Concluida: "completed",
|
||||
};
|
||||
return map[label] || label.toLowerCase();
|
||||
@ -148,10 +147,12 @@ export function SecretaryAppointmentList() {
|
||||
const typeValue = mapTypeFilterToValue(typeFilter);
|
||||
|
||||
// Filtro de status
|
||||
const matchesStatus = statusValue === null || appointment.status === statusValue;
|
||||
const matchesStatus =
|
||||
statusValue === null || appointment.status === statusValue;
|
||||
|
||||
// Filtro de tipo
|
||||
const matchesType = typeValue === null || appointment.appointment_type === typeValue;
|
||||
const matchesType =
|
||||
typeValue === null || appointment.appointment_type === typeValue;
|
||||
|
||||
return matchesSearch && matchesStatus && matchesType;
|
||||
});
|
||||
@ -194,7 +195,10 @@ export function SecretaryAppointmentList() {
|
||||
if (modalMode === "edit" && formData.id) {
|
||||
// Update only allowed fields per API types
|
||||
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;
|
||||
await appointmentService.update(formData.id, updatePayload);
|
||||
toast.success("Consulta atualizada com sucesso!");
|
||||
@ -623,7 +627,12 @@ export function SecretaryAppointmentList() {
|
||||
{selectedDate && formData.doctor_id && (
|
||||
<div>
|
||||
<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>
|
||||
<AvailableSlotsPicker
|
||||
doctorId={formData.doctor_id}
|
||||
@ -666,7 +675,9 @@ export function SecretaryAppointmentList() {
|
||||
type="submit"
|
||||
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>
|
||||
</div>
|
||||
</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="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">
|
||||
<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
|
||||
onClick={() => setSelectedAppointment(null)}
|
||||
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="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">Paciente</label>
|
||||
<p className="text-gray-900 font-medium">{selectedAppointment.patient?.full_name || '—'}</p>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||
Paciente
|
||||
</label>
|
||||
<p className="text-gray-900 font-medium">
|
||||
{selectedAppointment.patient?.full_name || "—"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">Médico</label>
|
||||
<p className="text-gray-900">{selectedAppointment.doctor?.full_name || '—'}</p>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||
Médico
|
||||
</label>
|
||||
<p className="text-gray-900">
|
||||
{selectedAppointment.doctor?.full_name || "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">Data</label>
|
||||
<p className="text-gray-900">{selectedAppointment.scheduled_at ? formatDate(selectedAppointment.scheduled_at) : '—'}</p>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||
Data
|
||||
</label>
|
||||
<p className="text-gray-900">
|
||||
{selectedAppointment.scheduled_at
|
||||
? formatDate(selectedAppointment.scheduled_at)
|
||||
: "—"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">Hora</label>
|
||||
<p className="text-gray-900">{selectedAppointment.scheduled_at ? formatTime(selectedAppointment.scheduled_at) : '—'}</p>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||
Hora
|
||||
</label>
|
||||
<p className="text-gray-900">
|
||||
{selectedAppointment.scheduled_at
|
||||
? formatTime(selectedAppointment.scheduled_at)
|
||||
: "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">Tipo</label>
|
||||
<p className="text-gray-900">{selectedAppointment.appointment_type === 'telemedicina' ? 'Telemedicina' : 'Presencial'}</p>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||
Tipo
|
||||
</label>
|
||||
<p className="text-gray-900">
|
||||
{selectedAppointment.appointment_type === "telemedicina"
|
||||
? "Telemedicina"
|
||||
: "Presencial"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">Status</label>
|
||||
<div>{getStatusBadge(selectedAppointment.status || 'agendada')}</div>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||
Status
|
||||
</label>
|
||||
<div>
|
||||
{getStatusBadge(selectedAppointment.status || "agendada")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">Observações</label>
|
||||
<p className="text-gray-900 whitespace-pre-wrap">{selectedAppointment.notes || '—'}</p>
|
||||
<label className="block text-sm font-medium text-gray-500 mb-1">
|
||||
Observações
|
||||
</label>
|
||||
<p className="text-gray-900 whitespace-pre-wrap">
|
||||
{selectedAppointment.notes || "—"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
@ -741,5 +788,3 @@ export function SecretaryAppointmentList() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -66,11 +66,13 @@ const formatCPF = (value: string): string => {
|
||||
const numbers = value.replace(/\D/g, "");
|
||||
if (numbers.length === 0) return "";
|
||||
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)
|
||||
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
|
||||
@ -81,8 +83,13 @@ const formatPhone = (value: string): string => {
|
||||
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)}`;
|
||||
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
|
||||
)}`;
|
||||
};
|
||||
|
||||
export function SecretaryDoctorList({
|
||||
@ -423,9 +430,14 @@ export function SecretaryDoctorList({
|
||||
if (onOpenSchedule) {
|
||||
onOpenSchedule(doctor.id);
|
||||
} else {
|
||||
sessionStorage.setItem("selectedDoctorForSchedule", doctor.id);
|
||||
sessionStorage.setItem(
|
||||
"selectedDoctorForSchedule",
|
||||
doctor.id
|
||||
);
|
||||
// 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"
|
||||
@ -501,7 +513,10 @@ export function SecretaryDoctorList({
|
||||
type="text"
|
||||
value={formData.cpf}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, cpf: formatCPF(e.target.value) })
|
||||
setFormData({
|
||||
...formData,
|
||||
cpf: formatCPF(e.target.value),
|
||||
})
|
||||
}
|
||||
className="form-input"
|
||||
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="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">
|
||||
<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
|
||||
onClick={() => setShowViewModal(false)}
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@ -691,5 +712,3 @@ export function SecretaryDoctorList({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ const weekdayToText = (weekday: Weekday | undefined | null): string => {
|
||||
5: "Sexta-feira",
|
||||
6: "Sábado",
|
||||
};
|
||||
|
||||
|
||||
return weekdayMap[weekday] || "Desconhecido";
|
||||
};
|
||||
|
||||
@ -118,20 +118,23 @@ export function SecretaryDoctorSchedule() {
|
||||
const loadDoctorSchedule = useCallback(async () => {
|
||||
if (!selectedDoctorId) return;
|
||||
|
||||
console.log("[SecretaryDoctorSchedule] Carregando agenda do médico:", selectedDoctorId);
|
||||
|
||||
console.log(
|
||||
"[SecretaryDoctorSchedule] Carregando agenda do médico:",
|
||||
selectedDoctorId
|
||||
);
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// Load availabilities
|
||||
const availData = await availabilityService.list({
|
||||
doctor_id: selectedDoctorId,
|
||||
});
|
||||
|
||||
|
||||
console.log("[SecretaryDoctorSchedule] Disponibilidades recebidas:", {
|
||||
count: availData?.length || 0,
|
||||
data: availData,
|
||||
});
|
||||
|
||||
|
||||
setAvailabilities(Array.isArray(availData) ? availData : []);
|
||||
|
||||
// Load appointments for the doctor
|
||||
@ -154,7 +157,10 @@ export function SecretaryDoctorSchedule() {
|
||||
});
|
||||
setExceptions(Array.isArray(exceptionsData) ? exceptionsData : []);
|
||||
} 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");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -179,17 +185,17 @@ export function SecretaryDoctorSchedule() {
|
||||
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const dayDate = new Date(currentDatePointer);
|
||||
const dayDateStr = dayDate.toISOString().split('T')[0];
|
||||
|
||||
const dayDateStr = dayDate.toISOString().split("T")[0];
|
||||
|
||||
// Filter appointments for this day
|
||||
const dayAppointments = appointments.filter(apt => {
|
||||
const dayAppointments = appointments.filter((apt) => {
|
||||
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;
|
||||
});
|
||||
|
||||
// Filter exceptions for this day
|
||||
const dayExceptions = exceptions.filter(exc => {
|
||||
const dayExceptions = exceptions.filter((exc) => {
|
||||
return exc.date === dayDateStr;
|
||||
});
|
||||
|
||||
@ -273,7 +279,7 @@ export function SecretaryDoctorSchedule() {
|
||||
appointment_type: "presencial" as const,
|
||||
active: true,
|
||||
};
|
||||
|
||||
|
||||
console.log("[SecretaryDoctorSchedule] Payload para criação:", payload);
|
||||
return availabilityService.create(payload);
|
||||
});
|
||||
@ -285,25 +291,29 @@ export function SecretaryDoctorSchedule() {
|
||||
selectedWeekdays.length > 1 ? "s" : ""
|
||||
} com sucesso`
|
||||
);
|
||||
|
||||
|
||||
setShowAvailabilityDialog(false);
|
||||
setSelectedWeekdays([]);
|
||||
setStartTime("08:00");
|
||||
setEndTime("18:00");
|
||||
setDuration(30);
|
||||
|
||||
|
||||
loadDoctorSchedule();
|
||||
} catch (error: any) {
|
||||
console.error("[SecretaryDoctorSchedule] Erro ao adicionar disponibilidade:", {
|
||||
error,
|
||||
message: error?.message,
|
||||
response: error?.response?.data,
|
||||
});
|
||||
|
||||
const errorMsg = error?.response?.data?.message ||
|
||||
error?.response?.data?.hint ||
|
||||
error?.message ||
|
||||
"Erro ao adicionar disponibilidade";
|
||||
console.error(
|
||||
"[SecretaryDoctorSchedule] Erro ao adicionar disponibilidade:",
|
||||
{
|
||||
error,
|
||||
message: error?.message,
|
||||
response: error?.response?.data,
|
||||
}
|
||||
);
|
||||
|
||||
const errorMsg =
|
||||
error?.response?.data?.message ||
|
||||
error?.response?.data?.hint ||
|
||||
error?.message ||
|
||||
"Erro ao adicionar disponibilidade";
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
};
|
||||
@ -327,11 +337,15 @@ export function SecretaryDoctorSchedule() {
|
||||
try {
|
||||
const start = new Date(exceptionStartDate);
|
||||
const end = new Date(exceptionEndDate);
|
||||
|
||||
|
||||
// Criar exceções para cada dia no intervalo
|
||||
const promises = [];
|
||||
for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) {
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
for (
|
||||
let date = new Date(start);
|
||||
date <= end;
|
||||
date.setDate(date.getDate() + 1)
|
||||
) {
|
||||
const dateStr = date.toISOString().split("T")[0];
|
||||
promises.push(
|
||||
availabilityService.createException({
|
||||
doctor_id: selectedDoctorId,
|
||||
@ -346,10 +360,14 @@ export function SecretaryDoctorSchedule() {
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const days = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1;
|
||||
toast.success(`Exceção adicionada para ${days} dia${days > 1 ? 's' : ''} com sucesso`);
|
||||
|
||||
|
||||
const days =
|
||||
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);
|
||||
setExceptionType("férias");
|
||||
setExceptionStartDate("");
|
||||
@ -358,13 +376,14 @@ export function SecretaryDoctorSchedule() {
|
||||
setExceptionIsFullDay(true);
|
||||
setExceptionStartTime("08:00");
|
||||
setExceptionEndTime("18:00");
|
||||
|
||||
|
||||
loadDoctorSchedule();
|
||||
} catch (error: any) {
|
||||
console.error("Erro ao adicionar exceção:", error);
|
||||
const errorMsg = error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Erro ao adicionar exceção";
|
||||
const errorMsg =
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Erro ao adicionar exceção";
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
};
|
||||
@ -397,27 +416,40 @@ export function SecretaryDoctorSchedule() {
|
||||
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");
|
||||
setShowEditDialog(false);
|
||||
setEditingAvailability(null);
|
||||
loadDoctorSchedule();
|
||||
} catch (error: any) {
|
||||
console.error("[SecretaryDoctorSchedule] Erro ao atualizar disponibilidade:", {
|
||||
error,
|
||||
message: error?.message,
|
||||
response: error?.response,
|
||||
data: error?.response?.data,
|
||||
});
|
||||
|
||||
const errorMessage = error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Erro ao atualizar disponibilidade";
|
||||
console.error(
|
||||
"[SecretaryDoctorSchedule] Erro ao atualizar disponibilidade:",
|
||||
{
|
||||
error,
|
||||
message: error?.message,
|
||||
response: error?.response,
|
||||
data: error?.response?.data,
|
||||
}
|
||||
);
|
||||
|
||||
const errorMessage =
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Erro ao atualizar disponibilidade";
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
@ -560,16 +592,17 @@ export function SecretaryDoctorSchedule() {
|
||||
<div className="text-sm text-gray-700 mb-1 font-medium">
|
||||
{day.date.getDate()}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Exceções (bloqueios e disponibilidades extras) */}
|
||||
{day.exceptions.map((exc, i) => {
|
||||
const timeRange = exc.start_time && exc.end_time
|
||||
? `${exc.start_time} - ${exc.end_time}`
|
||||
: "Dia inteiro";
|
||||
const tooltipText = exc.reason
|
||||
const timeRange =
|
||||
exc.start_time && exc.end_time
|
||||
? `${exc.start_time} - ${exc.end_time}`
|
||||
: "Dia inteiro";
|
||||
const tooltipText = exc.reason
|
||||
? `${timeRange} - ${exc.reason}`
|
||||
: timeRange;
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`exc-${i}`}
|
||||
@ -587,12 +620,12 @@ export function SecretaryDoctorSchedule() {
|
||||
|
||||
{/* Consultas agendadas */}
|
||||
{day.appointments.map((apt, i) => {
|
||||
const time = apt.scheduled_at
|
||||
? new Date(apt.scheduled_at).toLocaleTimeString('pt-BR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
const time = apt.scheduled_at
|
||||
? new Date(apt.scheduled_at).toLocaleTimeString("pt-BR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
: '';
|
||||
: "";
|
||||
return (
|
||||
<div
|
||||
key={`apt-${i}`}
|
||||
@ -701,11 +734,11 @@ export function SecretaryDoctorSchedule() {
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium text-gray-900">
|
||||
{new Date(exc.date).toLocaleDateString('pt-BR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
{new Date(exc.date).toLocaleDateString("pt-BR", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
@ -725,7 +758,9 @@ export function SecretaryDoctorSchedule() {
|
||||
: "bg-purple-100 text-purple-700"
|
||||
}`}
|
||||
>
|
||||
{exc.kind === "bloqueio" ? "Bloqueio" : "Disponibilidade Extra"}
|
||||
{exc.kind === "bloqueio"
|
||||
? "Bloqueio"
|
||||
: "Disponibilidade Extra"}
|
||||
</span>
|
||||
<button
|
||||
onClick={async () => {
|
||||
@ -733,7 +768,7 @@ export function SecretaryDoctorSchedule() {
|
||||
window.confirm(
|
||||
`Tem certeza que deseja remover esta exceção?\n\nData: ${new Date(
|
||||
exc.date
|
||||
).toLocaleDateString('pt-BR')}`
|
||||
).toLocaleDateString("pt-BR")}`
|
||||
)
|
||||
) {
|
||||
try {
|
||||
@ -1082,5 +1117,3 @@ export function SecretaryDoctorSchedule() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -21,7 +21,10 @@ export function SecretaryReportList() {
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [selectedReport, setSelectedReport] = useState<Report | null>(null);
|
||||
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({
|
||||
patient_id: "",
|
||||
exam: "",
|
||||
@ -35,12 +38,12 @@ export function SecretaryReportList() {
|
||||
useEffect(() => {
|
||||
loadReports();
|
||||
loadPatients();
|
||||
loadDoctors();
|
||||
}, []);
|
||||
|
||||
// Recarrega automaticamente quando o filtro de status muda
|
||||
// (evita depender do clique em Buscar)
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
loadReports();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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 = () => {
|
||||
setFormData({
|
||||
patient_id: "",
|
||||
@ -120,7 +132,7 @@ export function SecretaryReportList() {
|
||||
}
|
||||
|
||||
try {
|
||||
await reportService.update(selectedReport.id, {
|
||||
const updatedReport = await reportService.update(selectedReport.id, {
|
||||
patient_id: formData.patient_id,
|
||||
exam: formData.exam || undefined,
|
||||
diagnosis: formData.diagnosis || undefined,
|
||||
@ -130,10 +142,19 @@ export function SecretaryReportList() {
|
||||
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!");
|
||||
setShowEditModal(false);
|
||||
setSelectedReport(null);
|
||||
loadReports();
|
||||
|
||||
// Limpar cache de nomes antes de recarregar
|
||||
setRequestedByNames({});
|
||||
await loadReports();
|
||||
} catch (error) {
|
||||
console.error("Erro ao atualizar relatório:", error);
|
||||
toast.error("Erro ao atualizar relatório");
|
||||
@ -276,29 +297,55 @@ export function SecretaryReportList() {
|
||||
|
||||
const loadRequestedByNames = async (reportsList: Report[]) => {
|
||||
const names: Record<string, string> = {};
|
||||
|
||||
|
||||
// Buscar nomes únicos de requested_by
|
||||
const uniqueIds = [...new Set(reportsList.map(r => r.requested_by).filter(Boolean))];
|
||||
|
||||
for (const id of uniqueIds) {
|
||||
try {
|
||||
// Tentar buscar como médico primeiro
|
||||
const doctors = await doctorService.list({});
|
||||
const doctor = doctors.find((d) => (d as any).user_id === id || d.id === id);
|
||||
|
||||
if (doctor) {
|
||||
names[id!] = doctor.full_name || "Dr. " + (doctor.full_name || "Médico");
|
||||
} else {
|
||||
// Se não for médico, simplesmente pegar o texto que já está armazenado
|
||||
// pois requested_by pode ser um nome direto
|
||||
const uniqueIds = [
|
||||
...new Set(reportsList.map((r) => r.requested_by).filter(Boolean)),
|
||||
];
|
||||
|
||||
try {
|
||||
// Buscar todos os médicos de uma vez
|
||||
const doctors = await doctorService.list({});
|
||||
|
||||
for (const id of uniqueIds) {
|
||||
// Verificar se já é um nome (não é UUID)
|
||||
const isUUID =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
||||
id!
|
||||
);
|
||||
|
||||
if (!isUUID) {
|
||||
// Já é um nome direto (dados legados), manter como está
|
||||
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);
|
||||
};
|
||||
|
||||
@ -307,7 +354,15 @@ export function SecretaryReportList() {
|
||||
try {
|
||||
// Se um filtro de status estiver aplicado, encaminhar para o serviço
|
||||
// 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);
|
||||
const data = await reportService.list(filters);
|
||||
console.log("✅ Relatórios carregados:", data);
|
||||
@ -323,12 +378,12 @@ export function SecretaryReportList() {
|
||||
});
|
||||
}
|
||||
setReports(reportsList);
|
||||
|
||||
|
||||
// Carregar nomes dos solicitantes
|
||||
if (reportsList.length > 0) {
|
||||
await loadRequestedByNames(reportsList);
|
||||
}
|
||||
|
||||
|
||||
if (Array.isArray(data) && data.length === 0) {
|
||||
console.warn("⚠️ Nenhum relatório encontrado na API");
|
||||
}
|
||||
@ -517,8 +572,9 @@ export function SecretaryReportList() {
|
||||
{formatDate(report.created_at)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-700">
|
||||
{report.requested_by
|
||||
? (requestedByNames[report.requested_by] || report.requested_by)
|
||||
{report.requested_by
|
||||
? requestedByNames[report.requested_by] ||
|
||||
report.requested_by
|
||||
: "—"}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
@ -609,6 +665,26 @@ export function SecretaryReportList() {
|
||||
/>
|
||||
</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>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Diagnóstico
|
||||
@ -731,8 +807,9 @@ export function SecretaryReportList() {
|
||||
Solicitado por
|
||||
</label>
|
||||
<p className="text-gray-900">
|
||||
{selectedReport.requested_by
|
||||
? (requestedByNames[selectedReport.requested_by] || selectedReport.requested_by)
|
||||
{selectedReport.requested_by
|
||||
? requestedByNames[selectedReport.requested_by] ||
|
||||
selectedReport.requested_by
|
||||
: "—"}
|
||||
</p>
|
||||
</div>
|
||||
@ -889,15 +966,20 @@ export function SecretaryReportList() {
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Solicitado por
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
<select
|
||||
value={formData.requested_by}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, requested_by: e.target.value })
|
||||
}
|
||||
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>
|
||||
@ -954,5 +1036,3 @@ export function SecretaryReportList() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -8,12 +8,12 @@
|
||||
body {
|
||||
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* Garantir que o texto nunca fique muito grande */
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 640px) {
|
||||
html {
|
||||
font-size: 14px;
|
||||
|
||||
@ -87,6 +87,9 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
|
||||
const [selectedLaudo, setSelectedLaudo] = useState<Report | null>(null);
|
||||
const [showLaudoModal, setShowLaudoModal] = useState(false);
|
||||
const [requestedByNames, setRequestedByNames] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
|
||||
const pacienteId = user?.id || "";
|
||||
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
|
||||
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 };
|
||||
setActiveTab(state.activeTab);
|
||||
// Limpa o estado após usar
|
||||
@ -196,6 +202,32 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
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
|
||||
const fetchLaudos = useCallback(async () => {
|
||||
if (!pacienteId) return;
|
||||
@ -203,6 +235,8 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
try {
|
||||
const data = await reportService.list({ patient_id: pacienteId });
|
||||
setLaudos(data);
|
||||
// Carregar nomes dos médicos
|
||||
await loadRequestedByNames(data);
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar laudos:", error);
|
||||
toast.error("Erro ao carregar laudos");
|
||||
@ -210,7 +244,7 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
} finally {
|
||||
setLoadingLaudos(false);
|
||||
}
|
||||
}, [pacienteId]);
|
||||
}, [pacienteId, loadRequestedByNames]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === "appointments") {
|
||||
@ -273,8 +307,12 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
const consultasPassadasDashboard = todasConsultasPassadas.slice(0, 3);
|
||||
|
||||
// Para a página de consultas (com paginação)
|
||||
const totalPaginasProximas = Math.ceil(todasConsultasProximas.length / consultasPorPagina);
|
||||
const totalPaginasPassadas = Math.ceil(todasConsultasPassadas.length / consultasPorPagina);
|
||||
const totalPaginasProximas = Math.ceil(
|
||||
todasConsultasProximas.length / consultasPorPagina
|
||||
);
|
||||
const totalPaginasPassadas = Math.ceil(
|
||||
todasConsultasPassadas.length / consultasPorPagina
|
||||
);
|
||||
|
||||
const consultasProximas = todasConsultasProximas.slice(
|
||||
(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">
|
||||
{pacienteNome}
|
||||
</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>
|
||||
@ -649,9 +689,13 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
{getMedicoEspecialidade(c.medicoId)}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{format(new Date(c.dataHora), "dd/MM/yyyy - HH:mm", {
|
||||
locale: ptBR,
|
||||
})}
|
||||
{format(
|
||||
new Date(c.dataHora),
|
||||
"dd/MM/yyyy - HH:mm",
|
||||
{
|
||||
locale: ptBR,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -662,7 +706,8 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
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"
|
||||
>
|
||||
Ver mais consultas ({todasConsultasProximas.length - 3} restantes)
|
||||
Ver mais consultas ({todasConsultasProximas.length - 3}{" "}
|
||||
restantes)
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
@ -698,10 +743,7 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
<User className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<span>Editar Perfil</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate("/ajuda")}
|
||||
className="form-input"
|
||||
>
|
||||
<button onClick={() => navigate("/ajuda")} className="form-input">
|
||||
<HelpCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
|
||||
<span>Central de Ajuda</span>
|
||||
</button>
|
||||
@ -776,16 +818,23 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
<div className="space-y-4">
|
||||
{consultasProximas.map((c) => renderAppointmentCard(c))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Paginação Próximas Consultas */}
|
||||
{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="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 className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPaginaProximas(Math.max(1, paginaProximas - 1))}
|
||||
onClick={() =>
|
||||
setPaginaProximas(Math.max(1, 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"
|
||||
>
|
||||
@ -795,7 +844,11 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
Página {paginaProximas} de {totalPaginasProximas}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPaginaProximas(Math.min(totalPaginasProximas, paginaProximas + 1))}
|
||||
onClick={() =>
|
||||
setPaginaProximas(
|
||||
Math.min(totalPaginasProximas, paginaProximas + 1)
|
||||
)
|
||||
}
|
||||
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"
|
||||
>
|
||||
@ -826,16 +879,23 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
<div className="space-y-4">
|
||||
{consultasPassadas.map((c) => renderAppointmentCard(c, true))}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Paginação Consultas Passadas */}
|
||||
{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="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 className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPaginaPassadas(Math.max(1, paginaPassadas - 1))}
|
||||
onClick={() =>
|
||||
setPaginaPassadas(Math.max(1, 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"
|
||||
>
|
||||
@ -845,7 +905,11 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
Página {paginaPassadas} de {totalPaginasPassadas}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPaginaPassadas(Math.min(totalPaginasPassadas, paginaPassadas + 1))}
|
||||
onClick={() =>
|
||||
setPaginaPassadas(
|
||||
Math.min(totalPaginasPassadas, paginaPassadas + 1)
|
||||
)
|
||||
}
|
||||
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"
|
||||
>
|
||||
@ -1079,7 +1143,7 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row min-h-screen bg-gray-50 dark:bg-slate-950">
|
||||
{renderSidebar()}
|
||||
|
||||
|
||||
{/* Mobile Header */}
|
||||
<div className="lg:hidden bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-700 p-4 sticky top-0 z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -1097,7 +1161,9 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
<p className="font-medium text-gray-900 dark:text-white text-sm truncate">
|
||||
{pacienteNome}
|
||||
</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>
|
||||
<button
|
||||
@ -1110,7 +1176,7 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
<LogOut className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Mobile Nav */}
|
||||
<div className="mt-3 flex gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide">
|
||||
{menuItems.map((item) => {
|
||||
@ -1143,7 +1209,9 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
{/* Modal de Visualização do Laudo */}
|
||||
@ -1207,11 +1275,14 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
Data de Criação
|
||||
</label>
|
||||
<p className="text-sm text-gray-900 dark:text-white">
|
||||
{new Date(selectedLaudo.created_at).toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
{new Date(selectedLaudo.created_at).toLocaleDateString(
|
||||
"pt-BR",
|
||||
{
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{selectedLaudo.due_at && (
|
||||
@ -1220,11 +1291,14 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
Prazo de Entrega
|
||||
</label>
|
||||
<p className="text-sm text-gray-900 dark:text-white">
|
||||
{new Date(selectedLaudo.due_at).toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
{new Date(selectedLaudo.due_at).toLocaleDateString(
|
||||
"pt-BR",
|
||||
{
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@ -1294,7 +1368,8 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
</label>
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<p className="text-sm text-gray-900 dark:text-white">
|
||||
{selectedLaudo.requested_by}
|
||||
{requestedByNames[selectedLaudo.requested_by] ||
|
||||
selectedLaudo.requested_by}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -1308,7 +1383,9 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
</label>
|
||||
<div
|
||||
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>
|
||||
)}
|
||||
@ -1334,5 +1411,3 @@ const AcompanhamentoPaciente: React.FC = () => {
|
||||
};
|
||||
|
||||
export default AcompanhamentoPaciente;
|
||||
|
||||
|
||||
|
||||
@ -225,8 +225,8 @@ const AgendamentoPaciente: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={resetarAgendamento}
|
||||
<button
|
||||
onClick={resetarAgendamento}
|
||||
className="btn-primary w-full sm:w-auto text-sm sm:text-base"
|
||||
>
|
||||
Fazer Novo Agendamento
|
||||
@ -247,7 +247,9 @@ const AgendamentoPaciente: React.FC = () => {
|
||||
<h1 className="text-lg sm:text-xl lg:text-2xl font-bold truncate">
|
||||
Bem-vindo(a), {pacienteLogado.nome}!
|
||||
</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>
|
||||
<button
|
||||
onClick={logout}
|
||||
@ -286,215 +288,216 @@ const AgendamentoPaciente: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<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 && (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
<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" />
|
||||
Selecione o Médico
|
||||
</h2>
|
||||
{/* Etapa 1: Seleção de Médico */}
|
||||
{etapa === 1 && (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
<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" />
|
||||
Selecione o Médico
|
||||
</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>
|
||||
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
|
||||
Horários Disponíveis
|
||||
Médico/Especialidade
|
||||
</label>
|
||||
<AvailableSlotsPicker
|
||||
doctorId={agendamento.medicoId}
|
||||
date={agendamento.data}
|
||||
onSelect={(t) =>
|
||||
setAgendamento((prev) => ({ ...prev, horario: t }))
|
||||
<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>
|
||||
<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 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>
|
||||
)}
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{/* 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>
|
||||
<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>
|
||||
<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 className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
|
||||
<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 className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
|
||||
<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>
|
||||
);
|
||||
|
||||
@ -112,7 +112,10 @@ const Home: React.FC = () => {
|
||||
};
|
||||
|
||||
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 */}
|
||||
<RecoveryRedirect />
|
||||
|
||||
@ -257,9 +260,14 @@ const ActionCard: React.FC<ActionCardProps> = ({
|
||||
<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`}
|
||||
>
|
||||
<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>
|
||||
<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">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
@ -64,25 +64,25 @@ const ListaPacientes: React.FC = () => {
|
||||
<Users className="w-5 h-5 sm:w-6 sm:h-6 text-blue-600 flex-shrink-0" />{" "}
|
||||
Pacientes Cadastrados
|
||||
</h2>
|
||||
|
||||
|
||||
{loading && (
|
||||
<div className="text-sm sm:text-base text-gray-500 text-center py-8">
|
||||
Carregando pacientes...
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{!loading && error && (
|
||||
<div className="text-sm sm:text-base text-red-600 bg-red-50 border border-red-200 p-3 sm:p-4 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{!loading && !error && pacientes.length === 0 && (
|
||||
<div className="text-sm sm:text-base text-gray-500 text-center py-8">
|
||||
Nenhum paciente cadastrado.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{!loading && !error && pacientes.length > 0 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 sm:gap-5 lg:gap-6">
|
||||
{pacientes.map((paciente, idx) => (
|
||||
@ -106,18 +106,23 @@ const ListaPacientes: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<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 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" />
|
||||
<span className="break-all">{formatEmail(paciente.email)}</span>
|
||||
<span className="break-all">
|
||||
{formatEmail(paciente.email)}
|
||||
</span>
|
||||
</div>
|
||||
<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" />
|
||||
<span className="break-words">{formatPhone(paciente.phone_mobile)}</span>
|
||||
<span className="break-words">
|
||||
{formatPhone(paciente.phone_mobile)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs sm:text-sm text-gray-500 pt-1">
|
||||
<strong className="font-medium">Nascimento:</strong>{" "}
|
||||
|
||||
@ -24,7 +24,7 @@ const ListaSecretarias: React.FC = () => {
|
||||
<UserPlus className="w-5 h-5 sm:w-6 sm:h-6 text-green-600 flex-shrink-0" />{" "}
|
||||
Secretárias Cadastradas
|
||||
</h2>
|
||||
|
||||
|
||||
{secretarias.length === 0 ? (
|
||||
<div className="text-sm sm:text-base text-gray-500 text-center py-8">
|
||||
Nenhuma secretária cadastrada.
|
||||
@ -45,7 +45,7 @@ const ListaSecretarias: React.FC = () => {
|
||||
{sec.nome}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="space-y-1.5 sm:space-y-2">
|
||||
<div className="text-xs sm:text-sm text-gray-700">
|
||||
<strong className="font-medium">CPF:</strong> {sec.cpf}
|
||||
|
||||
@ -273,7 +273,9 @@ const PainelAdmin: React.FC = () => {
|
||||
const formattedCpf = formatCPF(userCpf);
|
||||
|
||||
// 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)
|
||||
await userService.createUserWithPassword({
|
||||
@ -299,10 +301,18 @@ const PainelAdmin: React.FC = () => {
|
||||
|
||||
// Mostrar mensagem de erro detalhada
|
||||
const errorMessage =
|
||||
(error as { response?: { data?: { message?: string; error?: string } }; message?: string })
|
||||
?.response?.data?.message ||
|
||||
(error as { response?: { data?: { message?: string; error?: string } }; message?: string })
|
||||
?.response?.data?.error ||
|
||||
(
|
||||
error as {
|
||||
response?: { data?: { message?: string; error?: string } };
|
||||
message?: string;
|
||||
}
|
||||
)?.response?.data?.message ||
|
||||
(
|
||||
error as {
|
||||
response?: { data?: { message?: string; error?: string } };
|
||||
message?: string;
|
||||
}
|
||||
)?.response?.data?.error ||
|
||||
(error as { message?: string })?.message ||
|
||||
"Erro ao criar usuário";
|
||||
|
||||
@ -699,7 +709,9 @@ const PainelAdmin: React.FC = () => {
|
||||
}
|
||||
|
||||
// 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:", {
|
||||
email: medicoData.email,
|
||||
@ -841,20 +853,32 @@ const PainelAdmin: React.FC = () => {
|
||||
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 <= 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)}`;
|
||||
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)}`;
|
||||
};
|
||||
|
||||
// Função para formatar telefone ((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 <= 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)}`;
|
||||
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
|
||||
)}`;
|
||||
};
|
||||
|
||||
// Função para obter apenas números do CPF/telefone
|
||||
@ -1693,19 +1717,27 @@ const PainelAdmin: React.FC = () => {
|
||||
>
|
||||
{availableRoles.map((role) => (
|
||||
<option key={role} value={role}>
|
||||
{role === "paciente" ? "Paciente" :
|
||||
role === "medico" ? "Médico" :
|
||||
role === "secretaria" ? "Secretária" :
|
||||
role === "admin" ? "Administrador" :
|
||||
role === "gestor" ? "Gestor" : role}
|
||||
{role === "paciente"
|
||||
? "Paciente"
|
||||
: role === "medico"
|
||||
? "Médico"
|
||||
: role === "secretaria"
|
||||
? "Secretária"
|
||||
: role === "admin"
|
||||
? "Administrador"
|
||||
: role === "gestor"
|
||||
? "Gestor"
|
||||
: role}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<label className="block text-sm font-medium mb-1">
|
||||
@ -1715,7 +1747,10 @@ const PainelAdmin: React.FC = () => {
|
||||
type="text"
|
||||
value={formUser.phone || ""}
|
||||
onChange={(e) =>
|
||||
setFormUser({ ...formUser, phone: formatPhone(e.target.value) })
|
||||
setFormUser({
|
||||
...formUser,
|
||||
phone: formatPhone(e.target.value),
|
||||
})
|
||||
}
|
||||
maxLength={15}
|
||||
className="form-input"
|
||||
@ -1730,7 +1765,9 @@ const PainelAdmin: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={userPhoneMobile}
|
||||
onChange={(e) => setUserPhoneMobile(formatPhone(e.target.value))}
|
||||
onChange={(e) =>
|
||||
setUserPhoneMobile(formatPhone(e.target.value))
|
||||
}
|
||||
maxLength={15}
|
||||
className="form-input"
|
||||
placeholder="(00) 00000-0000"
|
||||
@ -1894,7 +1931,10 @@ const PainelAdmin: React.FC = () => {
|
||||
required
|
||||
value={formMedico.cpf}
|
||||
onChange={(e) =>
|
||||
setFormMedico({ ...formMedico, cpf: formatCPF(e.target.value) })
|
||||
setFormMedico({
|
||||
...formMedico,
|
||||
cpf: formatCPF(e.target.value),
|
||||
})
|
||||
}
|
||||
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"
|
||||
@ -2229,5 +2269,3 @@ const PainelAdmin: React.FC = () => {
|
||||
};
|
||||
|
||||
export default PainelAdmin;
|
||||
|
||||
|
||||
|
||||
@ -101,7 +101,9 @@ const PainelMedico: React.FC = () => {
|
||||
|
||||
// Estados para perfil do médico
|
||||
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({
|
||||
full_name: "",
|
||||
email: "",
|
||||
@ -1020,7 +1022,7 @@ const PainelMedico: React.FC = () => {
|
||||
// Carregar dados do perfil do médico
|
||||
const loadDoctorProfile = useCallback(async () => {
|
||||
if (!doctorId) return;
|
||||
|
||||
|
||||
try {
|
||||
const doctor = await doctorService.getById(doctorId);
|
||||
setProfileData({
|
||||
@ -1114,7 +1116,9 @@ const PainelMedico: React.FC = () => {
|
||||
|
||||
{/* Avatar Card */}
|
||||
<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">
|
||||
<AvatarUpload
|
||||
userId={user?.id}
|
||||
@ -1193,7 +1197,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.full_name}
|
||||
onChange={(e) => handleProfileChange("full_name", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("full_name", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1206,7 +1212,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="email"
|
||||
value={profileData.email}
|
||||
onChange={(e) => handleProfileChange("email", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("email", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1219,7 +1227,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="tel"
|
||||
value={profileData.phone}
|
||||
onChange={(e) => handleProfileChange("phone", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("phone", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1244,7 +1254,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="date"
|
||||
value={profileData.birth_date}
|
||||
onChange={(e) => handleProfileChange("birth_date", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("birth_date", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1256,7 +1268,9 @@ const PainelMedico: React.FC = () => {
|
||||
</label>
|
||||
<select
|
||||
value={profileData.sex}
|
||||
onChange={(e) => handleProfileChange("sex", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("sex", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
>
|
||||
@ -1270,7 +1284,9 @@ const PainelMedico: React.FC = () => {
|
||||
</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="md:col-span-2">
|
||||
@ -1280,7 +1296,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.street}
|
||||
onChange={(e) => handleProfileChange("street", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("street", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1293,7 +1311,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.number}
|
||||
onChange={(e) => handleProfileChange("number", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("number", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1306,7 +1326,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.complement}
|
||||
onChange={(e) => handleProfileChange("complement", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("complement", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1319,7 +1341,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.neighborhood}
|
||||
onChange={(e) => handleProfileChange("neighborhood", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("neighborhood", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1332,7 +1356,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.city}
|
||||
onChange={(e) => handleProfileChange("city", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("city", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1345,7 +1371,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.state}
|
||||
onChange={(e) => handleProfileChange("state", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("state", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
maxLength={2}
|
||||
@ -1359,7 +1387,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.cep}
|
||||
onChange={(e) => handleProfileChange("cep", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("cep", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1385,7 +1415,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.crm}
|
||||
onChange={(e) => handleProfileChange("crm", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("crm", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
className="form-input"
|
||||
/>
|
||||
@ -1398,7 +1430,9 @@ const PainelMedico: React.FC = () => {
|
||||
<input
|
||||
type="text"
|
||||
value={profileData.specialty}
|
||||
onChange={(e) => handleProfileChange("specialty", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleProfileChange("specialty", e.target.value)
|
||||
}
|
||||
disabled={!isEditingProfile}
|
||||
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">
|
||||
{renderSidebar()}
|
||||
<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>
|
||||
|
||||
{/* Modals */}
|
||||
@ -1595,5 +1631,3 @@ const PainelMedico: React.FC = () => {
|
||||
};
|
||||
|
||||
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" />
|
||||
<span className="hidden sm:inline">{tab.label}</span>
|
||||
<span className="sm:hidden">
|
||||
{tab.label.split(' ')[0]}
|
||||
</span>
|
||||
<span className="sm:hidden">{tab.label.split(" ")[0]}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@ -97,7 +95,10 @@ export default function PainelSecretaria() {
|
||||
<SecretaryPatientList
|
||||
onOpenAppointment={(patientId: string) => {
|
||||
// store selected patient for appointment and switch to consultas tab
|
||||
sessionStorage.setItem("selectedPatientForAppointment", patientId);
|
||||
sessionStorage.setItem(
|
||||
"selectedPatientForAppointment",
|
||||
patientId
|
||||
);
|
||||
setActiveTab("consultas");
|
||||
}}
|
||||
/>
|
||||
@ -118,5 +119,3 @@ export default function PainelSecretaria() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -220,7 +220,9 @@ export default function PerfilMedico() {
|
||||
|
||||
{/* Avatar Card */}
|
||||
<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">
|
||||
<AvatarUpload
|
||||
userId={user?.id}
|
||||
@ -235,7 +237,9 @@ export default function PerfilMedico() {
|
||||
<p className="font-medium text-gray-900 text-sm sm:text-base truncate">
|
||||
{formData.full_name}
|
||||
</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">
|
||||
CRM: {formData.crm} - {formData.crm_state}
|
||||
</p>
|
||||
@ -564,5 +568,3 @@ export default function PerfilMedico() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -267,7 +267,9 @@ export default function PerfilPaciente() {
|
||||
|
||||
{/* Avatar Card */}
|
||||
<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">
|
||||
<AvatarUpload
|
||||
userId={user?.id}
|
||||
@ -684,5 +686,3 @@ export default function PerfilPaciente() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -175,18 +175,28 @@ class ApiClient {
|
||||
url: string,
|
||||
config?: AxiosRequestConfig
|
||||
): 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);
|
||||
|
||||
|
||||
console.log("[ApiClient] GET Response:", {
|
||||
status: response.status,
|
||||
dataType: typeof 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;
|
||||
}
|
||||
|
||||
@ -201,7 +211,7 @@ class ApiClient {
|
||||
data,
|
||||
config,
|
||||
});
|
||||
|
||||
|
||||
try {
|
||||
const response = await this.client.post<T>(url, data, config);
|
||||
console.log("[ApiClient] POST Response:", {
|
||||
@ -253,9 +263,18 @@ class ApiClient {
|
||||
data,
|
||||
config,
|
||||
});
|
||||
|
||||
|
||||
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:", {
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
|
||||
@ -24,27 +24,36 @@ class AppointmentService {
|
||||
): Promise<GetAvailableSlotsResponse> {
|
||||
try {
|
||||
console.log("[AppointmentService] Chamando get-available-slots:", data);
|
||||
|
||||
|
||||
// Usa callFunction para chamar a Edge Function
|
||||
const response = await apiClient.callFunction<GetAvailableSlotsResponse>(
|
||||
"get-available-slots",
|
||||
data
|
||||
);
|
||||
|
||||
console.log("[AppointmentService] Resposta get-available-slots:", response.data);
|
||||
|
||||
|
||||
console.log(
|
||||
"[AppointmentService] Resposta get-available-slots:",
|
||||
response.data
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
console.error("[AppointmentService] ❌ Erro ao buscar slots:");
|
||||
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] Input enviado:", JSON.stringify(data, null, 2));
|
||||
|
||||
console.error(
|
||||
"[AppointmentService] Input enviado:",
|
||||
JSON.stringify(data, null, 2)
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Erro ao buscar horários disponíveis"
|
||||
error?.response?.data?.message ||
|
||||
error?.message ||
|
||||
"Erro ao buscar horários disponíveis"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,12 +46,12 @@ class AvailabilityService {
|
||||
const response = await apiClient.get<any[]>(this.basePath, {
|
||||
params,
|
||||
});
|
||||
|
||||
|
||||
console.log("[AvailabilityService] Resposta:", {
|
||||
count: response.data?.length || 0,
|
||||
isArray: Array.isArray(response.data),
|
||||
});
|
||||
|
||||
|
||||
// Converter weekday de string para número (compatibilidade com banco antigo)
|
||||
const convertedData: DoctorAvailability[] = Array.isArray(response.data)
|
||||
? response.data.map((item) => {
|
||||
@ -64,21 +64,29 @@ class AvailabilityService {
|
||||
friday: 5,
|
||||
saturday: 6,
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
...item,
|
||||
weekday: typeof item.weekday === 'string'
|
||||
? weekdayMap[item.weekday.toLowerCase()]
|
||||
: item.weekday,
|
||||
weekday:
|
||||
typeof item.weekday === "string"
|
||||
? weekdayMap[item.weekday.toLowerCase()]
|
||||
: item.weekday,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
|
||||
if (convertedData.length > 0) {
|
||||
console.log("[AvailabilityService] ✅ Convertido:", convertedData.length, "registros");
|
||||
console.log("[AvailabilityService] Primeiro item convertido:", JSON.stringify(convertedData[0], null, 2));
|
||||
console.log(
|
||||
"[AvailabilityService] ✅ Convertido:",
|
||||
convertedData.length,
|
||||
"registros"
|
||||
);
|
||||
console.log(
|
||||
"[AvailabilityService] Primeiro item convertido:",
|
||||
JSON.stringify(convertedData[0], null, 2)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return convertedData;
|
||||
}
|
||||
|
||||
@ -100,9 +108,9 @@ class AvailabilityService {
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
console.log("[AvailabilityService] Resposta da criação:", response.data);
|
||||
|
||||
|
||||
return Array.isArray(response.data) ? response.data[0] : response.data;
|
||||
}
|
||||
|
||||
@ -129,8 +137,11 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@ -61,14 +61,41 @@ class ReportService {
|
||||
* Nota: order_number não pode ser modificado
|
||||
*/
|
||||
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}`,
|
||||
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: {
|
||||
extend: {
|
||||
fontSize: {
|
||||
'xs': ['0.75rem', { lineHeight: '1rem' }],
|
||||
'sm': ['0.875rem', { lineHeight: '1.25rem' }],
|
||||
'base': ['1rem', { lineHeight: '1.5rem' }],
|
||||
'lg': ['1.125rem', { lineHeight: '1.75rem' }],
|
||||
'xl': ['1.25rem', { lineHeight: '1.75rem' }],
|
||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||
xs: ["0.75rem", { lineHeight: "1rem" }],
|
||||
sm: ["0.875rem", { lineHeight: "1.25rem" }],
|
||||
base: ["1rem", { lineHeight: "1.5rem" }],
|
||||
lg: ["1.125rem", { lineHeight: "1.75rem" }],
|
||||
xl: ["1.25rem", { lineHeight: "1.75rem" }],
|
||||
"2xl": ["1.5rem", { lineHeight: "2rem" }],
|
||||
},
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user