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:
guisilvagomes 2025-11-05 18:25:13 -03:00
parent 3443e46ca3
commit 3a3e4c1f55
27 changed files with 1679 additions and 682 deletions

390
api-testing-results.md Normal file
View 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_

View File

@ -68,7 +68,7 @@ export default function AgendamentoConsulta({
const [bookingSuccess, setBookingSuccess] = useState(false); const [bookingSuccess, setBookingSuccess] = useState(false);
const [bookingError, setBookingError] = useState(""); const [bookingError, setBookingError] = useState("");
const [showResultModal, setShowResultModal] = useState(false); const [showResultModal, setShowResultModal] = useState(false);
const [resultType, setResultType] = useState<'success' | 'error'>('success'); const [resultType, setResultType] = useState<"success" | "error">("success");
const [availableDates, setAvailableDates] = useState<Set<string>>(new Set()); const [availableDates, setAvailableDates] = useState<Set<string>>(new Set());
// Removido o carregamento interno de médicos, pois agora vem por prop // Removido o carregamento interno de médicos, pois agora vem por prop
@ -102,64 +102,77 @@ export default function AgendamentoConsulta({
try { try {
const { availabilityService } = await import("../services"); const { availabilityService } = await import("../services");
console.log("[AgendamentoConsulta] Buscando disponibilidades para médico:", { console.log(
id: selectedMedico.id, "[AgendamentoConsulta] Buscando disponibilidades para médico:",
nome: selectedMedico.nome {
}); id: selectedMedico.id,
nome: selectedMedico.nome,
}
);
// Busca todas as disponibilidades ativas do médico // Busca todas as disponibilidades ativas do médico
const availabilities = await availabilityService.list({ const availabilities = await availabilityService.list({
doctor_id: selectedMedico.id, doctor_id: selectedMedico.id,
active: true, active: true,
}); });
console.log("[AgendamentoConsulta] Disponibilidades retornadas da API:", { console.log(
count: availabilities?.length || 0, "[AgendamentoConsulta] Disponibilidades retornadas da API:",
data: availabilities {
}); count: availabilities?.length || 0,
data: availabilities,
}
);
if (!availabilities || availabilities.length === 0) { if (!availabilities || availabilities.length === 0) {
console.warn("[AgendamentoConsulta] Nenhuma disponibilidade encontrada para o médico"); console.warn(
"[AgendamentoConsulta] Nenhuma disponibilidade encontrada para o médico"
);
setAvailableDates(new Set()); setAvailableDates(new Set());
return; return;
} }
// Mapeamento de string para número (formato da API) // Mapeamento de string para número (formato da API)
const weekdayMap: Record<string, number> = { const weekdayMap: Record<string, number> = {
"sunday": 0, sunday: 0,
"monday": 1, monday: 1,
"tuesday": 2, tuesday: 2,
"wednesday": 3, wednesday: 3,
"thursday": 4, thursday: 4,
"friday": 5, friday: 5,
"saturday": 6 saturday: 6,
}; };
// Mapeia os dias da semana que o médico atende (converte para número) // Mapeia os dias da semana que o médico atende (converte para número)
const availableWeekdays = new Set<number>( const availableWeekdays = new Set<number>(
availabilities.map((avail) => { availabilities
// weekday pode vir como número ou string da API .map((avail) => {
let weekdayNum: number; // weekday pode vir como número ou string da API
let weekdayNum: number;
if (typeof avail.weekday === 'number') {
weekdayNum = avail.weekday; if (typeof avail.weekday === "number") {
} else if (typeof avail.weekday === 'string') { weekdayNum = avail.weekday;
weekdayNum = weekdayMap[avail.weekday.toLowerCase()] ?? -1; } else if (typeof avail.weekday === "string") {
} else { weekdayNum = weekdayMap[avail.weekday.toLowerCase()] ?? -1;
weekdayNum = -1; } else {
} weekdayNum = -1;
}
console.log("[AgendamentoConsulta] Convertendo weekday:", {
original: avail.weekday, console.log("[AgendamentoConsulta] Convertendo weekday:", {
type: typeof avail.weekday, original: avail.weekday,
converted: weekdayNum type: typeof avail.weekday,
}); converted: weekdayNum,
return weekdayNum; });
}).filter(day => day >= 0 && day <= 6) // Remove valores inválidos 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 // Calcula todas as datas do mês atual e próximos 2 meses que têm disponibilidade
const today = startOfDay(new Date()); const today = startOfDay(new Date());
@ -167,7 +180,7 @@ export default function AgendamentoConsulta({
const allDates = eachDayOfInterval({ start: today, end: endDate }); const allDates = eachDayOfInterval({ start: today, end: endDate });
const availableDatesSet = new Set<string>(); const availableDatesSet = new Set<string>();
allDates.forEach((date) => { allDates.forEach((date) => {
const weekday = date.getDay(); const weekday = date.getDay();
if (availableWeekdays.has(weekday) && !isBefore(date, today)) { if (availableWeekdays.has(weekday) && !isBefore(date, today)) {
@ -178,13 +191,15 @@ export default function AgendamentoConsulta({
console.log("[AgendamentoConsulta] Resumo do cálculo:", { console.log("[AgendamentoConsulta] Resumo do cálculo:", {
weekdaysDisponiveis: Array.from(availableWeekdays), weekdaysDisponiveis: Array.from(availableWeekdays),
datasCalculadas: availableDatesSet.size, datasCalculadas: availableDatesSet.size,
primeiras5Datas: Array.from(availableDatesSet).slice(0, 5) primeiras5Datas: Array.from(availableDatesSet).slice(0, 5),
}); });
setAvailableDates(availableDatesSet);
setAvailableDates(availableDatesSet);
} catch (error) { } catch (error) {
console.error("[AgendamentoConsulta] Erro ao carregar disponibilidades:", error); console.error(
"[AgendamentoConsulta] Erro ao carregar disponibilidades:",
error
);
setAvailableDates(new Set()); setAvailableDates(new Set());
} }
}; };
@ -203,7 +218,7 @@ export default function AgendamentoConsulta({
try { try {
const dateStr = format(selectedDate, "yyyy-MM-dd"); const dateStr = format(selectedDate, "yyyy-MM-dd");
console.log("[AgendamentoConsulta] Buscando slots disponíveis:", { console.log("[AgendamentoConsulta] Buscando slots disponíveis:", {
doctor_id: selectedMedico.id, doctor_id: selectedMedico.id,
doctor_name: selectedMedico.nome, 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 // NOTA: Edge Function get-available-slots não está disponível no Supabase
// Calculando slots localmente // Calculando slots localmente
// Busca a disponibilidade do médico do Supabase // Busca a disponibilidade do médico do Supabase
const { availabilityService } = await import("../services"); const { availabilityService } = await import("../services");
const availabilities = await availabilityService.list({ const availabilities = await availabilityService.list({
doctor_id: selectedMedico.id, doctor_id: selectedMedico.id,
active: true, active: true,
}); });
console.log("[AgendamentoConsulta] Disponibilidades do médico:", availabilities); console.log(
"[AgendamentoConsulta] Disponibilidades do médico:",
availabilities
);
if (!availabilities || availabilities.length === 0) { if (!availabilities || availabilities.length === 0) {
console.warn("[AgendamentoConsulta] Nenhuma disponibilidade configurada para este médico"); console.warn(
"[AgendamentoConsulta] Nenhuma disponibilidade configurada para este médico"
);
setAvailableSlots([]); setAvailableSlots([]);
return; return;
} }
@ -232,26 +252,34 @@ export default function AgendamentoConsulta({
// Pega o dia da semana da data selecionada // Pega o dia da semana da data selecionada
const weekdayMap: Record<number, string> = { const weekdayMap: Record<number, string> = {
0: "sunday", 0: "sunday",
1: "monday", 1: "monday",
2: "tuesday", 2: "tuesday",
3: "wednesday", 3: "wednesday",
4: "thursday", 4: "thursday",
5: "friday", 5: "friday",
6: "saturday", 6: "saturday",
}; };
const dayOfWeek = weekdayMap[selectedDate.getDay()]; const dayOfWeek = weekdayMap[selectedDate.getDay()];
console.log("[AgendamentoConsulta] Dia da semana selecionado:", dayOfWeek); console.log(
"[AgendamentoConsulta] Dia da semana selecionado:",
dayOfWeek
);
// Filtra disponibilidades para o dia da semana // Filtra disponibilidades para o dia da semana
const dayAvailability = availabilities.filter( const dayAvailability = availabilities.filter(
(avail) => avail.weekday === dayOfWeek && avail.active (avail) => avail.weekday === dayOfWeek && avail.active
); );
console.log("[AgendamentoConsulta] Disponibilidades para o dia:", dayAvailability); console.log(
"[AgendamentoConsulta] Disponibilidades para o dia:",
dayAvailability
);
if (dayAvailability.length === 0) { if (dayAvailability.length === 0) {
console.warn("[AgendamentoConsulta] Médico não atende neste dia da semana"); console.warn(
"[AgendamentoConsulta] Médico não atende neste dia da semana"
);
setAvailableSlots([]); setAvailableSlots([]);
return; return;
} }
@ -266,14 +294,16 @@ export default function AgendamentoConsulta({
// Converte para minutos desde meia-noite // Converte para minutos desde meia-noite
const [startHour, startMin] = startTime.split(":").map(Number); const [startHour, startMin] = startTime.split(":").map(Number);
const [endHour, endMin] = endTime.split(":").map(Number); const [endHour, endMin] = endTime.split(":").map(Number);
let currentMinutes = startHour * 60 + startMin; let currentMinutes = startHour * 60 + startMin;
const endMinutes = endHour * 60 + endMin; const endMinutes = endHour * 60 + endMin;
while (currentMinutes < endMinutes) { while (currentMinutes < endMinutes) {
const hours = Math.floor(currentMinutes / 60); const hours = Math.floor(currentMinutes / 60);
const minutes = currentMinutes % 60; const minutes = currentMinutes % 60;
const timeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; const timeStr = `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}`;
allSlots.push(timeStr); allSlots.push(timeStr);
currentMinutes += slotMinutes; currentMinutes += slotMinutes;
} }
@ -284,7 +314,10 @@ export default function AgendamentoConsulta({
doctor_id: selectedMedico.id, doctor_id: selectedMedico.id,
}); });
console.log("[AgendamentoConsulta] Agendamentos existentes:", appointments); console.log(
"[AgendamentoConsulta] Agendamentos existentes:",
appointments
);
// Filtra agendamentos para a data selecionada // Filtra agendamentos para a data selecionada
const bookedSlots = appointments const bookedSlots = appointments
@ -309,7 +342,10 @@ export default function AgendamentoConsulta({
(slot) => !bookedSlots.includes(slot) (slot) => !bookedSlots.includes(slot)
); );
console.log("[AgendamentoConsulta] Slots disponíveis calculados:", availableSlots); console.log(
"[AgendamentoConsulta] Slots disponíveis calculados:",
availableSlots
);
setAvailableSlots(availableSlots); setAvailableSlots(availableSlots);
} catch (error) { } catch (error) {
@ -342,13 +378,13 @@ export default function AgendamentoConsulta({
const handlePrevMonth = () => setCurrentMonth(subMonths(currentMonth, 1)); const handlePrevMonth = () => setCurrentMonth(subMonths(currentMonth, 1));
const handleNextMonth = () => setCurrentMonth(addMonths(currentMonth, 1)); const handleNextMonth = () => setCurrentMonth(addMonths(currentMonth, 1));
const handleMonthChange = (monthIndex: number) => { const handleMonthChange = (monthIndex: number) => {
const newDate = new Date(currentMonth); const newDate = new Date(currentMonth);
newDate.setMonth(monthIndex); newDate.setMonth(monthIndex);
setCurrentMonth(newDate); setCurrentMonth(newDate);
}; };
const handleYearChange = (year: number) => { const handleYearChange = (year: number) => {
const newDate = new Date(currentMonth); const newDate = new Date(currentMonth);
newDate.setFullYear(year); newDate.setFullYear(year);
@ -356,8 +392,11 @@ export default function AgendamentoConsulta({
}; };
// Gera lista de anos (ano atual até +10 anos) // Gera lista de anos (ano atual até +10 anos)
const availableYears = Array.from({ length: 11 }, (_, i) => new Date().getFullYear() + i); const availableYears = Array.from(
{ length: 11 },
(_, i) => new Date().getFullYear() + i
);
const handleSelectDoctor = (medico: Medico) => { const handleSelectDoctor = (medico: Medico) => {
setSelectedMedico(medico); setSelectedMedico(medico);
setSelectedDate(undefined); setSelectedDate(undefined);
@ -365,12 +404,12 @@ export default function AgendamentoConsulta({
setMotivo(""); setMotivo("");
setBookingSuccess(false); setBookingSuccess(false);
setBookingError(""); setBookingError("");
// Scroll suave para a seção de detalhes // Scroll suave para a seção de detalhes
setTimeout(() => { setTimeout(() => {
detailsRef.current?.scrollIntoView({ detailsRef.current?.scrollIntoView({
behavior: 'smooth', behavior: "smooth",
block: 'start' block: "start",
}); });
}, 100); }, 100);
}; };
@ -393,32 +432,40 @@ export default function AgendamentoConsulta({
doctor_id: selectedMedico.id, doctor_id: selectedMedico.id,
scheduled_at: scheduledAt, scheduled_at: scheduledAt,
duration_minutes: 30, duration_minutes: 30,
appointment_type: appointment_type: (appointmentType === "online"
(appointmentType === "online" ? "telemedicina" : "presencial") as "presencial" | "telemedicina", ? "telemedicina"
: "presencial") as "presencial" | "telemedicina",
chief_complaint: motivo, chief_complaint: motivo,
}; };
console.log("[AgendamentoConsulta] Criando agendamento com dados:", appointmentData); console.log(
"[AgendamentoConsulta] Criando agendamento com dados:",
appointmentData
);
// Cria o agendamento usando a API REST // Cria o agendamento usando a API REST
const appointment = await appointmentService.create(appointmentData); const appointment = await appointmentService.create(appointmentData);
console.log("[AgendamentoConsulta] Consulta criada com sucesso:", appointment); console.log(
"[AgendamentoConsulta] Consulta criada com sucesso:",
appointment
);
// Mostra modal de sucesso // Mostra modal de sucesso
setResultType('success'); setResultType("success");
setShowResultModal(true); setShowResultModal(true);
setShowConfirmDialog(false); setShowConfirmDialog(false);
setBookingSuccess(true); setBookingSuccess(true);
} catch (error: unknown) { } catch (error: unknown) {
console.error("[AgendamentoConsulta] Erro ao agendar:", error); console.error("[AgendamentoConsulta] Erro ao agendar:", error);
const errorMessage = error instanceof Error const errorMessage =
? error.message error instanceof Error
: "Erro ao agendar consulta. Tente novamente."; ? error.message
: "Erro ao agendar consulta. Tente novamente.";
// Mostra modal de erro // Mostra modal de erro
setResultType('error'); setResultType("error");
setShowResultModal(true); setShowResultModal(true);
setBookingError(errorMessage); setBookingError(errorMessage);
setShowConfirmDialog(false); setShowConfirmDialog(false);
@ -435,38 +482,49 @@ export default function AgendamentoConsulta({
<div className="flex flex-col items-center text-center space-y-4"> <div className="flex flex-col items-center text-center space-y-4">
{/* Ícone com Animação Giratória (1 volta) */} {/* Ícone com Animação Giratória (1 volta) */}
<div className="relative"> <div className="relative">
<div className={`absolute inset-0 rounded-full animate-pulse-ring ${ <div
resultType === 'success' ? 'bg-blue-100' : 'bg-red-100' className={`absolute inset-0 rounded-full animate-pulse-ring ${
}`}></div> resultType === "success" ? "bg-blue-100" : "bg-red-100"
<div className={`relative rounded-full p-4 sm:p-5 ${ }`}
resultType === 'success' ? 'bg-blue-500' : 'bg-red-500' ></div>
}`}> <div
{resultType === 'success' ? ( className={`relative rounded-full p-4 sm:p-5 ${
resultType === "success" ? "bg-blue-500" : "bg-red-500"
}`}
>
{resultType === "success" ? (
<CheckCircle2 className="h-12 w-12 sm:h-16 sm:w-16 text-white animate-spin-once" /> <CheckCircle2 className="h-12 w-12 sm:h-16 sm:w-16 text-white animate-spin-once" />
) : ( ) : (
<AlertCircle className="h-12 w-12 sm:h-16 sm:w-16 text-white animate-spin-once" /> <AlertCircle className="h-12 w-12 sm:h-16 sm:w-16 text-white animate-spin-once" />
)} )}
</div> </div>
</div> </div>
{/* Mensagem */} {/* Mensagem */}
<div className="space-y-2"> <div className="space-y-2">
<h3 className={`text-xl sm:text-2xl font-bold ${ <h3
resultType === 'success' ? 'text-blue-900' : 'text-red-900' className={`text-xl sm:text-2xl font-bold ${
}`}> resultType === "success" ? "text-blue-900" : "text-red-900"
{resultType === 'success' ? 'Consulta Agendada!' : 'Erro no Agendamento'} }`}
>
{resultType === "success"
? "Consulta Agendada!"
: "Erro no Agendamento"}
</h3> </h3>
{resultType === 'success' ? ( {resultType === "success" ? (
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm sm:text-base text-gray-600"> <p className="text-sm sm:text-base text-gray-600">
Sua consulta foi agendada com sucesso. Você receberá uma confirmação por e-mail ou SMS. Sua consulta foi agendada com sucesso. Você receberá uma
confirmação por e-mail ou SMS.
</p> </p>
<button <button
onClick={() => { onClick={() => {
setShowResultModal(false); setShowResultModal(false);
setBookingSuccess(false); setBookingSuccess(false);
setBookingError(''); setBookingError("");
navigate('/acompanhamento', { state: { activeTab: 'consultas' } }); navigate("/acompanhamento", {
state: { activeTab: "consultas" },
});
}} }}
className="text-blue-600 hover:text-blue-800 underline text-sm sm:text-base font-medium" className="text-blue-600 hover:text-blue-800 underline text-sm sm:text-base font-medium"
> >
@ -475,19 +533,20 @@ export default function AgendamentoConsulta({
</div> </div>
) : ( ) : (
<p className="text-sm sm:text-base text-gray-600"> <p className="text-sm sm:text-base text-gray-600">
{bookingError || 'Não foi possível agendar a consulta. Tente novamente.'} {bookingError ||
"Não foi possível agendar a consulta. Tente novamente."}
</p> </p>
)} )}
</div> </div>
{/* Botão OK */} {/* Botão OK */}
<button <button
onClick={() => { onClick={() => {
setShowResultModal(false); setShowResultModal(false);
setBookingSuccess(false); setBookingSuccess(false);
setBookingError(''); setBookingError("");
// Limpa o formulário se for sucesso // Limpa o formulário se for sucesso
if (resultType === 'success') { if (resultType === "success") {
setSelectedMedico(null); setSelectedMedico(null);
setSelectedDate(undefined); setSelectedDate(undefined);
setSelectedTime(""); setSelectedTime("");
@ -495,9 +554,9 @@ export default function AgendamentoConsulta({
} }
}} }}
className={`w-full font-semibold py-3 px-6 rounded-lg transition-colors ${ className={`w-full font-semibold py-3 px-6 rounded-lg transition-colors ${
resultType === 'success' resultType === "success"
? 'bg-blue-500 hover:bg-blue-600 text-white' ? "bg-blue-500 hover:bg-blue-600 text-white"
: 'bg-red-500 hover:bg-red-600 text-white' : "bg-red-500 hover:bg-red-600 text-white"
}`} }`}
> >
OK, Entendi! OK, Entendi!
@ -506,7 +565,7 @@ export default function AgendamentoConsulta({
</div> </div>
</div> </div>
)} )}
<div> <div>
<h1 className="text-xl sm:text-2xl font-bold">Agendar Consulta</h1> <h1 className="text-xl sm:text-2xl font-bold">Agendar Consulta</h1>
<p className="text-sm sm:text-base text-muted-foreground"> <p className="text-sm sm:text-base text-muted-foreground">
@ -531,7 +590,9 @@ export default function AgendamentoConsulta({
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs sm:text-sm font-medium">Especialidade</label> <label className="text-xs sm:text-sm font-medium">
Especialidade
</label>
<select <select
value={selectedSpecialty} value={selectedSpecialty}
onChange={(e) => setSelectedSpecialty(e.target.value)} onChange={(e) => setSelectedSpecialty(e.target.value)}
@ -552,7 +613,9 @@ export default function AgendamentoConsulta({
<div <div
key={medico.id} key={medico.id}
className={`bg-white rounded-lg sm:rounded-xl border p-4 sm:p-6 flex flex-col sm:flex-row gap-3 sm:gap-4 items-start sm:items-center ${ className={`bg-white rounded-lg sm:rounded-xl border p-4 sm:p-6 flex flex-col sm:flex-row gap-3 sm:gap-4 items-start sm:items-center ${
selectedMedico?.id === medico.id ? "border-blue-500 bg-blue-50" : "" selectedMedico?.id === medico.id
? "border-blue-500 bg-blue-50"
: ""
}`} }`}
> >
<div className="h-12 w-12 sm:h-16 sm:w-16 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-base sm:text-xl font-bold text-white flex-shrink-0"> <div className="h-12 w-12 sm:h-16 sm:w-16 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-base sm:text-xl font-bold text-white flex-shrink-0">
@ -565,17 +628,25 @@ export default function AgendamentoConsulta({
</div> </div>
<div className="flex-1 min-w-0 space-y-1.5 sm:space-y-2 w-full"> <div className="flex-1 min-w-0 space-y-1.5 sm:space-y-2 w-full">
<div> <div>
<h3 className="text-sm sm:text-base font-semibold truncate">{medico.nome}</h3> <h3 className="text-sm sm:text-base font-semibold truncate">
<p className="text-xs sm:text-sm text-muted-foreground truncate">{medico.especialidade}</p> {medico.nome}
</h3>
<p className="text-xs sm:text-sm text-muted-foreground truncate">
{medico.especialidade}
</p>
</div> </div>
<div className="flex flex-wrap items-center gap-2 sm:gap-4 text-xs sm:text-sm text-muted-foreground"> <div className="flex flex-wrap items-center gap-2 sm:gap-4 text-xs sm:text-sm text-muted-foreground">
<span className="truncate">CRM: {medico.crm}</span> <span className="truncate">CRM: {medico.crm}</span>
{medico.valorConsulta ? ( {medico.valorConsulta ? (
<span className="whitespace-nowrap">R$ {medico.valorConsulta.toFixed(2)}</span> <span className="whitespace-nowrap">
R$ {medico.valorConsulta.toFixed(2)}
</span>
) : null} ) : null}
</div> </div>
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
<span className="text-xs sm:text-sm text-foreground truncate w-full sm:w-auto">{medico.email || "-"}</span> <span className="text-xs sm:text-sm text-foreground truncate w-full sm:w-auto">
{medico.email || "-"}
</span>
<div className="flex gap-2 w-full sm:w-auto"> <div className="flex gap-2 w-full sm:w-auto">
<button <button
className="flex-1 sm:flex-none px-3 py-1.5 sm:py-1 rounded-lg border text-xs sm:text-sm hover:bg-blue-50 transition-colors whitespace-nowrap" className="flex-1 sm:flex-none px-3 py-1.5 sm:py-1 rounded-lg border text-xs sm:text-sm hover:bg-blue-50 transition-colors whitespace-nowrap"
@ -592,9 +663,14 @@ export default function AgendamentoConsulta({
))} ))}
</div> </div>
{selectedMedico && ( {selectedMedico && (
<div ref={detailsRef} className="bg-white rounded-lg shadow p-4 sm:p-6 space-y-4 sm:space-y-6"> <div
ref={detailsRef}
className="bg-white rounded-lg shadow p-4 sm:p-6 space-y-4 sm:space-y-6"
>
<div> <div>
<h2 className="text-lg sm:text-xl font-semibold truncate">Detalhes do Agendamento</h2> <h2 className="text-lg sm:text-xl font-semibold truncate">
Detalhes do Agendamento
</h2>
<p className="text-sm sm:text-base text-gray-600 truncate"> <p className="text-sm sm:text-base text-gray-600 truncate">
Consulta com {selectedMedico.nome} -{" "} Consulta com {selectedMedico.nome} -{" "}
{selectedMedico.especialidade} {selectedMedico.especialidade}
@ -610,7 +686,9 @@ export default function AgendamentoConsulta({
}`} }`}
> >
<MapPin className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" /> <MapPin className="h-4 w-4 sm:h-5 sm:w-5 flex-shrink-0" />
<span className="text-sm sm:text-base font-medium">Presencial</span> <span className="text-sm sm:text-base font-medium">
Presencial
</span>
</button> </button>
<button <button
onClick={() => setAppointmentType("online")} onClick={() => setAppointmentType("online")}
@ -627,7 +705,9 @@ export default function AgendamentoConsulta({
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-3 sm:space-y-4"> <div className="space-y-3 sm:space-y-4">
<div> <div>
<label className="text-xs sm:text-sm font-medium">Selecione a Data</label> <label className="text-xs sm:text-sm font-medium">
Selecione a Data
</label>
<div className="mt-2"> <div className="mt-2">
<div className="flex items-center justify-between gap-2 mb-3 sm:mb-4"> <div className="flex items-center justify-between gap-2 mb-3 sm:mb-4">
<button <button
@ -637,11 +717,13 @@ export default function AgendamentoConsulta({
> >
<ChevronLeft className="h-4 w-4 sm:h-5 sm:w-5" /> <ChevronLeft className="h-4 w-4 sm:h-5 sm:w-5" />
</button> </button>
<div className="flex items-center gap-2 flex-1 justify-center"> <div className="flex items-center gap-2 flex-1 justify-center">
<select <select
value={currentMonth.getMonth()} value={currentMonth.getMonth()}
onChange={(e) => handleMonthChange(Number(e.target.value))} onChange={(e) =>
handleMonthChange(Number(e.target.value))
}
className="text-xs sm:text-sm font-semibold border border-gray-300 rounded-lg px-2 py-1 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500" className="text-xs sm:text-sm font-semibold border border-gray-300 rounded-lg px-2 py-1 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
<option value={0}>Janeiro</option> <option value={0}>Janeiro</option>
@ -657,10 +739,12 @@ export default function AgendamentoConsulta({
<option value={10}>Novembro</option> <option value={10}>Novembro</option>
<option value={11}>Dezembro</option> <option value={11}>Dezembro</option>
</select> </select>
<select <select
value={currentMonth.getFullYear()} value={currentMonth.getFullYear()}
onChange={(e) => handleYearChange(Number(e.target.value))} onChange={(e) =>
handleYearChange(Number(e.target.value))
}
className="text-xs sm:text-sm font-semibold border border-gray-300 rounded-lg px-2 py-1 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500" className="text-xs sm:text-sm font-semibold border border-gray-300 rounded-lg px-2 py-1 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
> >
{availableYears.map((year) => ( {availableYears.map((year) => (
@ -670,7 +754,7 @@ export default function AgendamentoConsulta({
))} ))}
</select> </select>
</div> </div>
<button <button
onClick={handleNextMonth} onClick={handleNextMonth}
className="p-1.5 sm:p-2 hover:bg-gray-100 rounded-lg flex-shrink-0" 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>
<div className="border rounded-lg overflow-hidden"> <div className="border rounded-lg overflow-hidden">
<div className="grid grid-cols-7 bg-gray-50"> <div className="grid grid-cols-7 bg-gray-50">
{["D", "S", "T", "Q", "Q", "S", "S"].map( {["D", "S", "T", "Q", "Q", "S", "S"].map((day, idx) => (
(day, idx) => ( <div
<div key={idx}
key={idx} className="text-center py-1.5 sm:py-2 text-xs sm:text-sm font-medium text-gray-600"
className="text-center py-1.5 sm:py-2 text-xs sm:text-sm font-medium text-gray-600" >
> {day}
{day} </div>
</div> ))}
)
)}
</div> </div>
<div className="grid grid-cols-7"> <div className="grid grid-cols-7">
{calendarDays.map((day, index) => { {calendarDays.map((day, index) => {
@ -698,24 +780,36 @@ export default function AgendamentoConsulta({
const isSelected = const isSelected =
selectedDate && isSameDay(day, selectedDate); selectedDate && isSameDay(day, selectedDate);
const isPast = isBefore(day, startOfDay(new Date())); const isPast = isBefore(day, startOfDay(new Date()));
// Verifica se a data está no conjunto de datas disponíveis // Verifica se a data está no conjunto de datas disponíveis
const dateStr = format(day, "yyyy-MM-dd"); const dateStr = format(day, "yyyy-MM-dd");
const isAvailable = isCurrentMonth && !isPast && availableDates.has(dateStr); const isAvailable =
const isUnavailable = isCurrentMonth && !isPast && !availableDates.has(dateStr); isCurrentMonth &&
!isPast &&
availableDates.has(dateStr);
const isUnavailable =
isCurrentMonth &&
!isPast &&
!availableDates.has(dateStr);
// Debug apenas para o primeiro dia do mês atual // Debug apenas para o primeiro dia do mês atual
if (index === 0 && isCurrentMonth) { if (index === 0 && isCurrentMonth) {
console.log("[AgendamentoConsulta] Debug calendário:", { console.log(
totalDatasDisponiveis: availableDates.size, "[AgendamentoConsulta] Debug calendário:",
primeiraData: dateStr, {
diaDaSemana: day.getDay(), totalDatasDisponiveis: availableDates.size,
isAvailable, primeiraData: dateStr,
isUnavailable, diaDaSemana: day.getDay(),
datas5Primeiras: Array.from(availableDates).slice(0, 5) isAvailable,
}); isUnavailable,
datas5Primeiras: Array.from(availableDates).slice(
0,
5
),
}
);
} }
return ( return (
<button <button
key={index} key={index}
@ -748,9 +842,17 @@ export default function AgendamentoConsulta({
</div> </div>
</div> </div>
<div className="mt-2 sm:mt-3 space-y-0.5 sm:space-y-1 text-xs text-gray-600"> <div className="mt-2 sm:mt-3 space-y-0.5 sm:space-y-1 text-xs text-gray-600">
<p><span className="text-blue-600 font-semibold"></span> Datas disponíveis</p> <p>
<p><span className="text-red-600 font-semibold"></span> Datas indisponíveis</p> <span className="text-blue-600 font-semibold"></span>{" "}
<p><span className="text-gray-400"></span> Datas passadas</p> Datas disponíveis
</p>
<p>
<span className="text-red-600 font-semibold"></span>{" "}
Datas indisponíveis
</p>
<p>
<span className="text-gray-400"></span> Datas passadas
</p>
</div> </div>
</div> </div>
</div> </div>
@ -846,7 +948,9 @@ export default function AgendamentoConsulta({
{showConfirmDialog && ( {showConfirmDialog && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-4 sm:p-6 space-y-3 sm:space-y-4 max-h-[90vh] overflow-y-auto"> <div className="bg-white rounded-lg shadow-xl max-w-md w-full p-4 sm:p-6 space-y-3 sm:space-y-4 max-h-[90vh] overflow-y-auto">
<h3 className="text-lg sm:text-xl font-semibold">Confirmar Agendamento</h3> <h3 className="text-lg sm:text-xl font-semibold">
Confirmar Agendamento
</h3>
<p className="text-sm sm:text-base text-gray-600"> <p className="text-sm sm:text-base text-gray-600">
Revise os detalhes da sua consulta antes de confirmar Revise os detalhes da sua consulta antes de confirmar
</p> </p>
@ -908,5 +1012,3 @@ export default function AgendamentoConsulta({
</div> </div>
); );
} }

View File

@ -171,7 +171,9 @@ const Chatbot: React.FC<ChatbotProps> = ({ className = "" }) => {
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/> />
</svg> </svg>
<span className="font-medium hidden sm:inline">Precisa de ajuda?</span> <span className="font-medium hidden sm:inline">
Precisa de ajuda?
</span>
</button> </button>
)} )}

View File

@ -279,7 +279,9 @@ const DisponibilidadeMedico: React.FC = () => {
doctor_id: doctorId, doctor_id: doctorId,
date: exceptionForm.date, date: exceptionForm.date,
kind: exceptionForm.kind, kind: exceptionForm.kind,
start_time: exceptionForm.wholeDayBlock ? null : exceptionForm.start_time, start_time: exceptionForm.wholeDayBlock
? null
: exceptionForm.start_time,
end_time: exceptionForm.wholeDayBlock ? null : exceptionForm.end_time, end_time: exceptionForm.wholeDayBlock ? null : exceptionForm.end_time,
reason: exceptionForm.reason || null, reason: exceptionForm.reason || null,
created_by: user?.id || doctorId, created_by: user?.id || doctorId,
@ -462,7 +464,10 @@ const DisponibilidadeMedico: React.FC = () => {
type="date" type="date"
value={exceptionForm.date} value={exceptionForm.date}
onChange={(e) => onChange={(e) =>
setExceptionForm({ ...exceptionForm, date: e.target.value }) setExceptionForm({
...exceptionForm,
date: e.target.value,
})
} }
className="form-input w-full" className="form-input w-full"
/> />
@ -476,7 +481,9 @@ const DisponibilidadeMedico: React.FC = () => {
onChange={(e) => onChange={(e) =>
setExceptionForm({ setExceptionForm({
...exceptionForm, ...exceptionForm,
kind: e.target.value as "bloqueio" | "disponibilidade_extra", kind: e.target.value as
| "bloqueio"
| "disponibilidade_extra",
}) })
} }
className="form-input w-full" className="form-input w-full"
@ -554,7 +561,10 @@ const DisponibilidadeMedico: React.FC = () => {
<textarea <textarea
value={exceptionForm.reason} value={exceptionForm.reason}
onChange={(e) => onChange={(e) =>
setExceptionForm({ ...exceptionForm, reason: e.target.value }) setExceptionForm({
...exceptionForm,
reason: e.target.value,
})
} }
placeholder="Ex: Férias, Feriado, Plantão extra..." placeholder="Ex: Férias, Feriado, Plantão extra..."
className="form-input w-full" className="form-input w-full"
@ -617,7 +627,9 @@ const DisponibilidadeMedico: React.FC = () => {
onClick={async () => { onClick={async () => {
if (!exception.id) return; if (!exception.id) return;
try { try {
await availabilityService.deleteException(exception.id); await availabilityService.deleteException(
exception.id
);
toast.success("Exceção removida"); toast.success("Exceção removida");
loadExceptions(); loadExceptions();
} catch (error) { } catch (error) {
@ -681,7 +693,10 @@ const DisponibilidadeMedico: React.FC = () => {
<select <select
value={newSlot.slotMinutes} value={newSlot.slotMinutes}
onChange={(e) => onChange={(e) =>
setNewSlot({ ...newSlot, slotMinutes: parseInt(e.target.value) }) setNewSlot({
...newSlot,
slotMinutes: parseInt(e.target.value),
})
} }
className="form-input w-full" className="form-input w-full"
> >

View File

@ -34,10 +34,15 @@ const AvailableSlotsPicker: React.FC<Props> = ({
active: true, active: true,
}); });
console.log("📅 [AvailableSlotsPicker] Disponibilidades:", availabilities); console.log(
"📅 [AvailableSlotsPicker] Disponibilidades:",
availabilities
);
if (!availabilities || availabilities.length === 0) { if (!availabilities || availabilities.length === 0) {
console.warn("[AvailableSlotsPicker] Nenhuma disponibilidade configurada"); console.warn(
"[AvailableSlotsPicker] Nenhuma disponibilidade configurada"
);
setSlots([]); setSlots([]);
setLoading(false); setLoading(false);
return; return;
@ -54,10 +59,15 @@ const AvailableSlotsPicker: React.FC<Props> = ({
(avail) => avail.weekday === dayOfWeek && avail.active (avail) => avail.weekday === dayOfWeek && avail.active
); );
console.log("[AvailableSlotsPicker] Disponibilidades para o dia:", dayAvailability); console.log(
"[AvailableSlotsPicker] Disponibilidades para o dia:",
dayAvailability
);
if (dayAvailability.length === 0) { if (dayAvailability.length === 0) {
console.warn("[AvailableSlotsPicker] Médico não atende neste dia da semana"); console.warn(
"[AvailableSlotsPicker] Médico não atende neste dia da semana"
);
setSlots([]); setSlots([]);
setLoading(false); setLoading(false);
return; return;
@ -80,7 +90,9 @@ const AvailableSlotsPicker: React.FC<Props> = ({
while (currentMinutes < endMinutes) { while (currentMinutes < endMinutes) {
const hours = Math.floor(currentMinutes / 60); const hours = Math.floor(currentMinutes / 60);
const minutes = currentMinutes % 60; const minutes = currentMinutes % 60;
const timeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; const timeStr = `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}`;
allSlots.push(timeStr); allSlots.push(timeStr);
currentMinutes += slotMinutes; currentMinutes += slotMinutes;
} }
@ -91,7 +103,10 @@ const AvailableSlotsPicker: React.FC<Props> = ({
doctor_id: doctorId, doctor_id: doctorId,
}); });
console.log("[AvailableSlotsPicker] Agendamentos existentes:", appointments); console.log(
"[AvailableSlotsPicker] Agendamentos existentes:",
appointments
);
// Filtra agendamentos para a data selecionada // Filtra agendamentos para a data selecionada
const bookedSlots = (Array.isArray(appointments) ? appointments : []) const bookedSlots = (Array.isArray(appointments) ? appointments : [])
@ -109,15 +124,22 @@ const AvailableSlotsPicker: React.FC<Props> = ({
return format(aptDate, "HH:mm"); return format(aptDate, "HH:mm");
}); });
console.log("[AvailableSlotsPicker] Horários já ocupados:", bookedSlots); console.log(
"[AvailableSlotsPicker] Horários já ocupados:",
bookedSlots
);
// Remove slots já ocupados // Remove slots já ocupados
const availableSlots = allSlots.filter((slot) => !bookedSlots.includes(slot)); const availableSlots = allSlots.filter(
(slot) => !bookedSlots.includes(slot)
);
console.log("✅ [AvailableSlotsPicker] Horários disponíveis:", availableSlots); console.log(
"✅ [AvailableSlotsPicker] Horários disponíveis:",
availableSlots
);
setSlots(availableSlots); setSlots(availableSlots);
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
console.error("❌ [AvailableSlotsPicker] Erro ao buscar slots:", error); console.error("❌ [AvailableSlotsPicker] Erro ao buscar slots:", error);
setLoading(false); setLoading(false);

View File

@ -1,6 +1,16 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { ChevronLeft, ChevronRight } from "lucide-react"; import { ChevronLeft, ChevronRight } from "lucide-react";
import { format, startOfMonth, endOfMonth, eachDayOfInterval, isBefore, startOfDay, addMonths, subMonths, getDay } from "date-fns"; import {
format,
startOfMonth,
endOfMonth,
eachDayOfInterval,
isBefore,
startOfDay,
addMonths,
subMonths,
getDay,
} from "date-fns";
import { ptBR } from "date-fns/locale"; import { ptBR } from "date-fns/locale";
import { availabilityService, appointmentService } from "../../services"; import { availabilityService, appointmentService } from "../../services";
import type { DoctorAvailability, DoctorException } from "../../services"; import type { DoctorAvailability, DoctorException } from "../../services";
@ -19,17 +29,25 @@ interface DayStatus {
isPast: boolean; // Data já passou isPast: boolean; // Data já passou
} }
export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: CalendarPickerProps) { export function CalendarPicker({
doctorId,
selectedDate,
onSelectDate,
}: CalendarPickerProps) {
const [currentMonth, setCurrentMonth] = useState(new Date()); const [currentMonth, setCurrentMonth] = useState(new Date());
const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>([]); const [availabilities, setAvailabilities] = useState<DoctorAvailability[]>(
[]
);
const [exceptions, setExceptions] = useState<DoctorException[]>([]); const [exceptions, setExceptions] = useState<DoctorException[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [availableSlots, setAvailableSlots] = useState<Record<string, boolean>>({}); const [availableSlots, setAvailableSlots] = useState<Record<string, boolean>>(
{}
);
// Carregar disponibilidades e exceções do médico // Carregar disponibilidades e exceções do médico
useEffect(() => { useEffect(() => {
if (!doctorId) return; if (!doctorId) return;
const loadData = async () => { const loadData = async () => {
setLoading(true); setLoading(true);
try { try {
@ -37,7 +55,7 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
availabilityService.list({ doctor_id: doctorId, active: true }), availabilityService.list({ doctor_id: doctorId, active: true }),
availabilityService.listExceptions({ doctor_id: doctorId }), availabilityService.listExceptions({ doctor_id: doctorId }),
]); ]);
setAvailabilities(Array.isArray(availData) ? availData : []); setAvailabilities(Array.isArray(availData) ? availData : []);
setExceptions(Array.isArray(exceptData) ? exceptData : []); setExceptions(Array.isArray(exceptData) ? exceptData : []);
} catch (error) { } catch (error) {
@ -72,7 +90,9 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
// Buscar todos os agendamentos do médico uma vez só // Buscar todos os agendamentos do médico uma vez só
let allAppointments: Array<{ scheduled_at: string; status: string }> = []; let allAppointments: Array<{ scheduled_at: string; status: string }> = [];
try { try {
const appointments = await appointmentService.list({ doctor_id: doctorId }); const appointments = await appointmentService.list({
doctor_id: doctorId,
});
allAppointments = Array.isArray(appointments) ? appointments : []; allAppointments = Array.isArray(appointments) ? appointments : [];
} catch (error) { } catch (error) {
console.error("[CalendarPicker] Erro ao buscar agendamentos:", error); console.error("[CalendarPicker] Erro ao buscar agendamentos:", error);
@ -120,7 +140,9 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
while (currentMinutes < endMinutes) { while (currentMinutes < endMinutes) {
const hours = Math.floor(currentMinutes / 60); const hours = Math.floor(currentMinutes / 60);
const minutes = currentMinutes % 60; const minutes = currentMinutes % 60;
const timeStr = `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; const timeStr = `${hours.toString().padStart(2, "0")}:${minutes
.toString()
.padStart(2, "0")}`;
allSlots.push(timeStr); allSlots.push(timeStr);
currentMinutes += slotMinutes; currentMinutes += slotMinutes;
} }
@ -143,11 +165,18 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
}); });
// Verifica se há pelo menos um slot disponível // Verifica se há pelo menos um slot disponível
const availableSlots = allSlots.filter((slot) => !bookedSlots.includes(slot)); const availableSlots = allSlots.filter(
(slot) => !bookedSlots.includes(slot)
);
slotsMap[dateStr] = availableSlots.length > 0; slotsMap[dateStr] = availableSlots.length > 0;
} catch (error) { } catch (error) {
console.error(`[CalendarPicker] Erro ao verificar slots para ${format(day, "yyyy-MM-dd")}:`, error); console.error(
`[CalendarPicker] Erro ao verificar slots para ${format(
day,
"yyyy-MM-dd"
)}:`,
error
);
slotsMap[format(day, "yyyy-MM-dd")] = false; slotsMap[format(day, "yyyy-MM-dd")] = false;
} }
} }
@ -185,7 +214,8 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
}; };
const getDayClasses = (status: DayStatus, isSelected: boolean): string => { const getDayClasses = (status: DayStatus, isSelected: boolean): string => {
const base = "w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors"; const base =
"w-10 h-10 rounded-full flex items-center justify-center text-sm font-medium transition-colors";
if (isSelected) { if (isSelected) {
return `${base} bg-blue-600 text-white ring-2 ring-blue-400`; return `${base} bg-blue-600 text-white ring-2 ring-blue-400`;
@ -241,7 +271,10 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
<div className="grid grid-cols-7 gap-1"> <div className="grid grid-cols-7 gap-1">
{/* Cabeçalho dos dias da semana */} {/* Cabeçalho dos dias da semana */}
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => ( {["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
<div key={day} className="text-center text-xs font-semibold text-gray-600 py-2"> <div
key={day}
className="text-center text-xs font-semibold text-gray-600 py-2"
>
{day} {day}
</div> </div>
))} ))}
@ -257,11 +290,18 @@ export function CalendarPicker({ doctorId, selectedDate, onSelectDate }: Calenda
const classes = getDayClasses(status, isSelected); const classes = getDayClasses(status, isSelected);
return ( return (
<div key={format(day, "yyyy-MM-dd")} className="flex justify-center"> <div
key={format(day, "yyyy-MM-dd")}
className="flex justify-center"
>
<button <button
type="button" type="button"
onClick={() => handleDayClick(day, status)} onClick={() => handleDayClick(day, status)}
disabled={status.isPast || status.hasBlockException || (!status.hasAvailability && !status.available)} disabled={
status.isPast ||
status.hasBlockException ||
(!status.hasAvailability && !status.available)
}
className={classes} className={classes}
title={ title={
status.isPast status.isPast

View File

@ -101,8 +101,11 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
// Convert ISO to date and time // Convert ISO to date and time
try { try {
const d = new Date(editing.scheduled_at); const d = new Date(editing.scheduled_at);
const dateStr = d.toISOString().split('T')[0]; // YYYY-MM-DD const dateStr = d.toISOString().split("T")[0]; // YYYY-MM-DD
const timeStr = `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; const timeStr = `${d.getHours().toString().padStart(2, "0")}:${d
.getMinutes()
.toString()
.padStart(2, "0")}`;
setSelectedDate(dateStr); setSelectedDate(dateStr);
setSelectedTime(timeStr); setSelectedTime(timeStr);
} catch { } catch {
@ -170,9 +173,18 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
if (editing) { if (editing) {
const payload = { const payload = {
scheduled_at: iso, scheduled_at: iso,
appointment_type: (tipo || "presencial") as "presencial" | "telemedicina", appointment_type: (tipo || "presencial") as
| "presencial"
| "telemedicina",
notes: observacoes || undefined, notes: observacoes || undefined,
status: status as "requested" | "confirmed" | "checked_in" | "in_progress" | "completed" | "cancelled" | "no_show", status: status as
| "requested"
| "confirmed"
| "checked_in"
| "in_progress"
| "completed"
| "cancelled"
| "no_show",
}; };
const updated = await appointmentService.update(editing.id, payload); const updated = await appointmentService.update(editing.id, payload);
onSaved(updated); onSaved(updated);
@ -181,7 +193,9 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
patient_id: pacienteId, patient_id: pacienteId,
doctor_id: medicoId, doctor_id: medicoId,
scheduled_at: iso, scheduled_at: iso,
appointment_type: (tipo || "presencial") as "presencial" | "telemedicina", appointment_type: (tipo || "presencial") as
| "presencial"
| "telemedicina",
notes: observacoes || undefined, notes: observacoes || undefined,
}; };
const created = await appointmentService.create(payload); const created = await appointmentService.create(payload);
@ -279,7 +293,12 @@ const ConsultaModal: React.FC<ConsultaModalProps> = ({
{selectedDate && medicoId && ( {selectedDate && medicoId && (
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Horário * {selectedTime && <span className="text-blue-600 font-semibold">({selectedTime})</span>} Horário *{" "}
{selectedTime && (
<span className="text-blue-600 font-semibold">
({selectedTime})
</span>
)}
</label> </label>
<AvailableSlotsPicker <AvailableSlotsPicker
doctorId={medicoId} doctorId={medicoId}

View File

@ -28,9 +28,8 @@ export function SecretaryAppointmentList() {
const [typeFilter, setTypeFilter] = useState("Todos"); const [typeFilter, setTypeFilter] = useState("Todos");
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [modalMode, setModalMode] = useState<"create" | "edit">("create"); const [modalMode, setModalMode] = useState<"create" | "edit">("create");
const [selectedAppointment, setSelectedAppointment] = useState< const [selectedAppointment, setSelectedAppointment] =
AppointmentWithDetails | null useState<AppointmentWithDetails | null>(null);
>(null);
const [patients, setPatients] = useState<Patient[]>([]); const [patients, setPatients] = useState<Patient[]>([]);
const [doctors, setDoctors] = useState<Doctor[]>([]); const [doctors, setDoctors] = useState<Doctor[]>([]);
const [formData, setFormData] = useState<any>({ const [formData, setFormData] = useState<any>({
@ -129,7 +128,7 @@ export function SecretaryAppointmentList() {
Confirmada: "confirmed", Confirmada: "confirmed",
Agendada: "requested", Agendada: "requested",
Cancelada: "cancelled", Cancelada: "cancelled",
"Concluída": "completed", Concluída: "completed",
Concluida: "completed", Concluida: "completed",
}; };
return map[label] || label.toLowerCase(); return map[label] || label.toLowerCase();
@ -148,10 +147,12 @@ export function SecretaryAppointmentList() {
const typeValue = mapTypeFilterToValue(typeFilter); const typeValue = mapTypeFilterToValue(typeFilter);
// Filtro de status // Filtro de status
const matchesStatus = statusValue === null || appointment.status === statusValue; const matchesStatus =
statusValue === null || appointment.status === statusValue;
// Filtro de tipo // Filtro de tipo
const matchesType = typeValue === null || appointment.appointment_type === typeValue; const matchesType =
typeValue === null || appointment.appointment_type === typeValue;
return matchesSearch && matchesStatus && matchesType; return matchesSearch && matchesStatus && matchesType;
}); });
@ -194,7 +195,10 @@ export function SecretaryAppointmentList() {
if (modalMode === "edit" && formData.id) { if (modalMode === "edit" && formData.id) {
// Update only allowed fields per API types // Update only allowed fields per API types
const updatePayload: any = {}; const updatePayload: any = {};
if (formData.scheduled_at) updatePayload.scheduled_at = new Date(formData.scheduled_at).toISOString(); if (formData.scheduled_at)
updatePayload.scheduled_at = new Date(
formData.scheduled_at
).toISOString();
if (formData.notes) updatePayload.notes = formData.notes; if (formData.notes) updatePayload.notes = formData.notes;
await appointmentService.update(formData.id, updatePayload); await appointmentService.update(formData.id, updatePayload);
toast.success("Consulta atualizada com sucesso!"); toast.success("Consulta atualizada com sucesso!");
@ -623,7 +627,12 @@ export function SecretaryAppointmentList() {
{selectedDate && formData.doctor_id && ( {selectedDate && formData.doctor_id && (
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Horário * {selectedTime && <span className="text-blue-600 font-semibold">({selectedTime})</span>} Horário *{" "}
{selectedTime && (
<span className="text-blue-600 font-semibold">
({selectedTime})
</span>
)}
</label> </label>
<AvailableSlotsPicker <AvailableSlotsPicker
doctorId={formData.doctor_id} doctorId={formData.doctor_id}
@ -666,7 +675,9 @@ export function SecretaryAppointmentList() {
type="submit" type="submit"
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors" className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
> >
{modalMode === "edit" ? "Salvar Alterações" : "Agendar Consulta"} {modalMode === "edit"
? "Salvar Alterações"
: "Agendar Consulta"}
</button> </button>
</div> </div>
</form> </form>
@ -679,7 +690,9 @@ export function SecretaryAppointmentList() {
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto"> <div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200 flex items-center justify-between"> <div className="p-6 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-2xl font-bold text-gray-900">Visualizar Consulta</h2> <h2 className="text-2xl font-bold text-gray-900">
Visualizar Consulta
</h2>
<button <button
onClick={() => setSelectedAppointment(null)} onClick={() => setSelectedAppointment(null)}
className="p-2 hover:bg-gray-100 rounded-lg transition-colors" className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
@ -691,39 +704,73 @@ export function SecretaryAppointmentList() {
<div className="p-6 space-y-4"> <div className="p-6 space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-500 mb-1">Paciente</label> <label className="block text-sm font-medium text-gray-500 mb-1">
<p className="text-gray-900 font-medium">{selectedAppointment.patient?.full_name || '—'}</p> Paciente
</label>
<p className="text-gray-900 font-medium">
{selectedAppointment.patient?.full_name || "—"}
</p>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-500 mb-1">Médico</label> <label className="block text-sm font-medium text-gray-500 mb-1">
<p className="text-gray-900">{selectedAppointment.doctor?.full_name || '—'}</p> Médico
</label>
<p className="text-gray-900">
{selectedAppointment.doctor?.full_name || "—"}
</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-sm font-medium text-gray-500 mb-1">Data</label> <label className="block text-sm font-medium text-gray-500 mb-1">
<p className="text-gray-900">{selectedAppointment.scheduled_at ? formatDate(selectedAppointment.scheduled_at) : '—'}</p> Data
</label>
<p className="text-gray-900">
{selectedAppointment.scheduled_at
? formatDate(selectedAppointment.scheduled_at)
: "—"}
</p>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-500 mb-1">Hora</label> <label className="block text-sm font-medium text-gray-500 mb-1">
<p className="text-gray-900">{selectedAppointment.scheduled_at ? formatTime(selectedAppointment.scheduled_at) : '—'}</p> Hora
</label>
<p className="text-gray-900">
{selectedAppointment.scheduled_at
? formatTime(selectedAppointment.scheduled_at)
: "—"}
</p>
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-500 mb-1">Tipo</label> <label className="block text-sm font-medium text-gray-500 mb-1">
<p className="text-gray-900">{selectedAppointment.appointment_type === 'telemedicina' ? 'Telemedicina' : 'Presencial'}</p> Tipo
</label>
<p className="text-gray-900">
{selectedAppointment.appointment_type === "telemedicina"
? "Telemedicina"
: "Presencial"}
</p>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-500 mb-1">Status</label> <label className="block text-sm font-medium text-gray-500 mb-1">
<div>{getStatusBadge(selectedAppointment.status || 'agendada')}</div> Status
</label>
<div>
{getStatusBadge(selectedAppointment.status || "agendada")}
</div>
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-500 mb-1">Observações</label> <label className="block text-sm font-medium text-gray-500 mb-1">
<p className="text-gray-900 whitespace-pre-wrap">{selectedAppointment.notes || '—'}</p> Observações
</label>
<p className="text-gray-900 whitespace-pre-wrap">
{selectedAppointment.notes || "—"}
</p>
</div> </div>
<div className="flex justify-end gap-3 pt-4"> <div className="flex justify-end gap-3 pt-4">
@ -741,5 +788,3 @@ export function SecretaryAppointmentList() {
</div> </div>
); );
} }

View File

@ -66,11 +66,13 @@ const formatCPF = (value: string): string => {
const numbers = value.replace(/\D/g, ""); const numbers = value.replace(/\D/g, "");
if (numbers.length === 0) return ""; if (numbers.length === 0) return "";
if (numbers.length <= 3) return numbers; if (numbers.length <= 3) return numbers;
if (numbers.length <= 6) if (numbers.length <= 6) return `${numbers.slice(0, 3)}.${numbers.slice(3)}`;
return `${numbers.slice(0, 3)}.${numbers.slice(3)}`;
if (numbers.length <= 9) if (numbers.length <= 9)
return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(6)}`; return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(6)}`;
return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(6, 9)}-${numbers.slice(9, 11)}`; return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(
6,
9
)}-${numbers.slice(9, 11)}`;
}; };
// Função para formatar telefone: (XX) XXXXX-XXXX // Função para formatar telefone: (XX) XXXXX-XXXX
@ -81,8 +83,13 @@ const formatPhone = (value: string): string => {
if (numbers.length <= 7) if (numbers.length <= 7)
return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`; return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`;
if (numbers.length <= 11) if (numbers.length <= 11)
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(7)}`; return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(7, 11)}`; 7
)}`;
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(
7,
11
)}`;
}; };
export function SecretaryDoctorList({ export function SecretaryDoctorList({
@ -423,9 +430,14 @@ export function SecretaryDoctorList({
if (onOpenSchedule) { if (onOpenSchedule) {
onOpenSchedule(doctor.id); onOpenSchedule(doctor.id);
} else { } else {
sessionStorage.setItem("selectedDoctorForSchedule", doctor.id); sessionStorage.setItem(
"selectedDoctorForSchedule",
doctor.id
);
// dispatch a custom event to inform parent (optional) // dispatch a custom event to inform parent (optional)
window.dispatchEvent(new CustomEvent("open-doctor-schedule")); window.dispatchEvent(
new CustomEvent("open-doctor-schedule")
);
} }
}} }}
title="Gerenciar agenda" title="Gerenciar agenda"
@ -501,7 +513,10 @@ export function SecretaryDoctorList({
type="text" type="text"
value={formData.cpf} value={formData.cpf}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, cpf: formatCPF(e.target.value) }) setFormData({
...formData,
cpf: formatCPF(e.target.value),
})
} }
className="form-input" className="form-input"
required required
@ -649,7 +664,9 @@ export function SecretaryDoctorList({
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col"> <div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
<div className="flex items-center justify-between p-6 border-b border-gray-200"> <div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">Visualizar Médico</h2> <h2 className="text-xl font-semibold text-gray-900">
Visualizar Médico
</h2>
<button <button
onClick={() => setShowViewModal(false)} onClick={() => setShowViewModal(false)}
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors" className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
@ -661,19 +678,23 @@ export function SecretaryDoctorList({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<p className="text-sm text-gray-500">Nome</p> <p className="text-sm text-gray-500">Nome</p>
<p className="text-gray-900 font-medium">{selectedDoctor.full_name}</p> <p className="text-gray-900 font-medium">
{selectedDoctor.full_name}
</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Especialidade</p> <p className="text-sm text-gray-500">Especialidade</p>
<p className="text-gray-900">{selectedDoctor.specialty || '—'}</p> <p className="text-gray-900">
{selectedDoctor.specialty || "—"}
</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">CRM</p> <p className="text-sm text-gray-500">CRM</p>
<p className="text-gray-900">{selectedDoctor.crm || '—'}</p> <p className="text-gray-900">{selectedDoctor.crm || "—"}</p>
</div> </div>
<div> <div>
<p className="text-sm text-gray-500">Email</p> <p className="text-sm text-gray-500">Email</p>
<p className="text-gray-900">{selectedDoctor.email || '—'}</p> <p className="text-gray-900">{selectedDoctor.email || "—"}</p>
</div> </div>
</div> </div>
</div> </div>
@ -691,5 +712,3 @@ export function SecretaryDoctorList({
</div> </div>
); );
} }

View File

@ -34,7 +34,7 @@ const weekdayToText = (weekday: Weekday | undefined | null): string => {
5: "Sexta-feira", 5: "Sexta-feira",
6: "Sábado", 6: "Sábado",
}; };
return weekdayMap[weekday] || "Desconhecido"; return weekdayMap[weekday] || "Desconhecido";
}; };
@ -118,20 +118,23 @@ export function SecretaryDoctorSchedule() {
const loadDoctorSchedule = useCallback(async () => { const loadDoctorSchedule = useCallback(async () => {
if (!selectedDoctorId) return; if (!selectedDoctorId) return;
console.log("[SecretaryDoctorSchedule] Carregando agenda do médico:", selectedDoctorId); console.log(
"[SecretaryDoctorSchedule] Carregando agenda do médico:",
selectedDoctorId
);
setLoading(true); setLoading(true);
try { try {
// Load availabilities // Load availabilities
const availData = await availabilityService.list({ const availData = await availabilityService.list({
doctor_id: selectedDoctorId, doctor_id: selectedDoctorId,
}); });
console.log("[SecretaryDoctorSchedule] Disponibilidades recebidas:", { console.log("[SecretaryDoctorSchedule] Disponibilidades recebidas:", {
count: availData?.length || 0, count: availData?.length || 0,
data: availData, data: availData,
}); });
setAvailabilities(Array.isArray(availData) ? availData : []); setAvailabilities(Array.isArray(availData) ? availData : []);
// Load appointments for the doctor // Load appointments for the doctor
@ -154,7 +157,10 @@ export function SecretaryDoctorSchedule() {
}); });
setExceptions(Array.isArray(exceptionsData) ? exceptionsData : []); setExceptions(Array.isArray(exceptionsData) ? exceptionsData : []);
} catch (error) { } catch (error) {
console.error("[SecretaryDoctorSchedule] Erro ao carregar agenda:", error); console.error(
"[SecretaryDoctorSchedule] Erro ao carregar agenda:",
error
);
toast.error("Erro ao carregar agenda do médico"); toast.error("Erro ao carregar agenda do médico");
} finally { } finally {
setLoading(false); setLoading(false);
@ -179,17 +185,17 @@ export function SecretaryDoctorSchedule() {
for (let i = 0; i < 42; i++) { for (let i = 0; i < 42; i++) {
const dayDate = new Date(currentDatePointer); const dayDate = new Date(currentDatePointer);
const dayDateStr = dayDate.toISOString().split('T')[0]; const dayDateStr = dayDate.toISOString().split("T")[0];
// Filter appointments for this day // Filter appointments for this day
const dayAppointments = appointments.filter(apt => { const dayAppointments = appointments.filter((apt) => {
if (!apt.scheduled_at) return false; if (!apt.scheduled_at) return false;
const aptDate = new Date(apt.scheduled_at).toISOString().split('T')[0]; const aptDate = new Date(apt.scheduled_at).toISOString().split("T")[0];
return aptDate === dayDateStr; return aptDate === dayDateStr;
}); });
// Filter exceptions for this day // Filter exceptions for this day
const dayExceptions = exceptions.filter(exc => { const dayExceptions = exceptions.filter((exc) => {
return exc.date === dayDateStr; return exc.date === dayDateStr;
}); });
@ -273,7 +279,7 @@ export function SecretaryDoctorSchedule() {
appointment_type: "presencial" as const, appointment_type: "presencial" as const,
active: true, active: true,
}; };
console.log("[SecretaryDoctorSchedule] Payload para criação:", payload); console.log("[SecretaryDoctorSchedule] Payload para criação:", payload);
return availabilityService.create(payload); return availabilityService.create(payload);
}); });
@ -285,25 +291,29 @@ export function SecretaryDoctorSchedule() {
selectedWeekdays.length > 1 ? "s" : "" selectedWeekdays.length > 1 ? "s" : ""
} com sucesso` } com sucesso`
); );
setShowAvailabilityDialog(false); setShowAvailabilityDialog(false);
setSelectedWeekdays([]); setSelectedWeekdays([]);
setStartTime("08:00"); setStartTime("08:00");
setEndTime("18:00"); setEndTime("18:00");
setDuration(30); setDuration(30);
loadDoctorSchedule(); loadDoctorSchedule();
} catch (error: any) { } catch (error: any) {
console.error("[SecretaryDoctorSchedule] Erro ao adicionar disponibilidade:", { console.error(
error, "[SecretaryDoctorSchedule] Erro ao adicionar disponibilidade:",
message: error?.message, {
response: error?.response?.data, error,
}); message: error?.message,
response: error?.response?.data,
const errorMsg = error?.response?.data?.message || }
error?.response?.data?.hint || );
error?.message ||
"Erro ao adicionar disponibilidade"; const errorMsg =
error?.response?.data?.message ||
error?.response?.data?.hint ||
error?.message ||
"Erro ao adicionar disponibilidade";
toast.error(errorMsg); toast.error(errorMsg);
} }
}; };
@ -327,11 +337,15 @@ export function SecretaryDoctorSchedule() {
try { try {
const start = new Date(exceptionStartDate); const start = new Date(exceptionStartDate);
const end = new Date(exceptionEndDate); const end = new Date(exceptionEndDate);
// Criar exceções para cada dia no intervalo // Criar exceções para cada dia no intervalo
const promises = []; const promises = [];
for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) { for (
const dateStr = date.toISOString().split('T')[0]; let date = new Date(start);
date <= end;
date.setDate(date.getDate() + 1)
) {
const dateStr = date.toISOString().split("T")[0];
promises.push( promises.push(
availabilityService.createException({ availabilityService.createException({
doctor_id: selectedDoctorId, doctor_id: selectedDoctorId,
@ -346,10 +360,14 @@ export function SecretaryDoctorSchedule() {
} }
await Promise.all(promises); await Promise.all(promises);
const days = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) + 1; const days =
toast.success(`Exceção adicionada para ${days} dia${days > 1 ? 's' : ''} com sucesso`); Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)) +
1;
toast.success(
`Exceção adicionada para ${days} dia${days > 1 ? "s" : ""} com sucesso`
);
setShowExceptionDialog(false); setShowExceptionDialog(false);
setExceptionType("férias"); setExceptionType("férias");
setExceptionStartDate(""); setExceptionStartDate("");
@ -358,13 +376,14 @@ export function SecretaryDoctorSchedule() {
setExceptionIsFullDay(true); setExceptionIsFullDay(true);
setExceptionStartTime("08:00"); setExceptionStartTime("08:00");
setExceptionEndTime("18:00"); setExceptionEndTime("18:00");
loadDoctorSchedule(); loadDoctorSchedule();
} catch (error: any) { } catch (error: any) {
console.error("Erro ao adicionar exceção:", error); console.error("Erro ao adicionar exceção:", error);
const errorMsg = error?.response?.data?.message || const errorMsg =
error?.message || error?.response?.data?.message ||
"Erro ao adicionar exceção"; error?.message ||
"Erro ao adicionar exceção";
toast.error(errorMsg); toast.error(errorMsg);
} }
}; };
@ -397,27 +416,40 @@ export function SecretaryDoctorSchedule() {
active: editActive, active: editActive,
}; };
console.log("[SecretaryDoctorSchedule] Dados de atualização:", updateData); console.log(
"[SecretaryDoctorSchedule] Dados de atualização:",
updateData
);
const result = await availabilityService.update(editingAvailability.id, updateData); const result = await availabilityService.update(
editingAvailability.id,
updateData
);
console.log("[SecretaryDoctorSchedule] Resultado da atualização:", result); console.log(
"[SecretaryDoctorSchedule] Resultado da atualização:",
result
);
toast.success("Disponibilidade atualizada com sucesso"); toast.success("Disponibilidade atualizada com sucesso");
setShowEditDialog(false); setShowEditDialog(false);
setEditingAvailability(null); setEditingAvailability(null);
loadDoctorSchedule(); loadDoctorSchedule();
} catch (error: any) { } catch (error: any) {
console.error("[SecretaryDoctorSchedule] Erro ao atualizar disponibilidade:", { console.error(
error, "[SecretaryDoctorSchedule] Erro ao atualizar disponibilidade:",
message: error?.message, {
response: error?.response, error,
data: error?.response?.data, message: error?.message,
}); response: error?.response,
data: error?.response?.data,
const errorMessage = error?.response?.data?.message || }
error?.message || );
"Erro ao atualizar disponibilidade";
const errorMessage =
error?.response?.data?.message ||
error?.message ||
"Erro ao atualizar disponibilidade";
toast.error(errorMessage); toast.error(errorMessage);
} }
}; };
@ -560,16 +592,17 @@ export function SecretaryDoctorSchedule() {
<div className="text-sm text-gray-700 mb-1 font-medium"> <div className="text-sm text-gray-700 mb-1 font-medium">
{day.date.getDate()} {day.date.getDate()}
</div> </div>
{/* Exceções (bloqueios e disponibilidades extras) */} {/* Exceções (bloqueios e disponibilidades extras) */}
{day.exceptions.map((exc, i) => { {day.exceptions.map((exc, i) => {
const timeRange = exc.start_time && exc.end_time const timeRange =
? `${exc.start_time} - ${exc.end_time}` exc.start_time && exc.end_time
: "Dia inteiro"; ? `${exc.start_time} - ${exc.end_time}`
const tooltipText = exc.reason : "Dia inteiro";
const tooltipText = exc.reason
? `${timeRange} - ${exc.reason}` ? `${timeRange} - ${exc.reason}`
: timeRange; : timeRange;
return ( return (
<div <div
key={`exc-${i}`} key={`exc-${i}`}
@ -587,12 +620,12 @@ export function SecretaryDoctorSchedule() {
{/* Consultas agendadas */} {/* Consultas agendadas */}
{day.appointments.map((apt, i) => { {day.appointments.map((apt, i) => {
const time = apt.scheduled_at const time = apt.scheduled_at
? new Date(apt.scheduled_at).toLocaleTimeString('pt-BR', { ? new Date(apt.scheduled_at).toLocaleTimeString("pt-BR", {
hour: '2-digit', hour: "2-digit",
minute: '2-digit', minute: "2-digit",
}) })
: ''; : "";
return ( return (
<div <div
key={`apt-${i}`} key={`apt-${i}`}
@ -701,11 +734,11 @@ export function SecretaryDoctorSchedule() {
> >
<div> <div>
<p className="font-medium text-gray-900"> <p className="font-medium text-gray-900">
{new Date(exc.date).toLocaleDateString('pt-BR', { {new Date(exc.date).toLocaleDateString("pt-BR", {
weekday: 'long', weekday: "long",
year: 'numeric', year: "numeric",
month: 'long', month: "long",
day: 'numeric', day: "numeric",
})} })}
</p> </p>
<p className="text-sm text-gray-600"> <p className="text-sm text-gray-600">
@ -725,7 +758,9 @@ export function SecretaryDoctorSchedule() {
: "bg-purple-100 text-purple-700" : "bg-purple-100 text-purple-700"
}`} }`}
> >
{exc.kind === "bloqueio" ? "Bloqueio" : "Disponibilidade Extra"} {exc.kind === "bloqueio"
? "Bloqueio"
: "Disponibilidade Extra"}
</span> </span>
<button <button
onClick={async () => { onClick={async () => {
@ -733,7 +768,7 @@ export function SecretaryDoctorSchedule() {
window.confirm( window.confirm(
`Tem certeza que deseja remover esta exceção?\n\nData: ${new Date( `Tem certeza que deseja remover esta exceção?\n\nData: ${new Date(
exc.date exc.date
).toLocaleDateString('pt-BR')}` ).toLocaleDateString("pt-BR")}`
) )
) { ) {
try { try {
@ -1082,5 +1117,3 @@ export function SecretaryDoctorSchedule() {
</div> </div>
); );
} }

View File

@ -21,7 +21,10 @@ export function SecretaryReportList() {
const [showEditModal, setShowEditModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false);
const [selectedReport, setSelectedReport] = useState<Report | null>(null); const [selectedReport, setSelectedReport] = useState<Report | null>(null);
const [patients, setPatients] = useState<Patient[]>([]); const [patients, setPatients] = useState<Patient[]>([]);
const [requestedByNames, setRequestedByNames] = useState<Record<string, string>>({}); const [doctors, setDoctors] = useState<any[]>([]);
const [requestedByNames, setRequestedByNames] = useState<
Record<string, string>
>({});
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
patient_id: "", patient_id: "",
exam: "", exam: "",
@ -35,12 +38,12 @@ export function SecretaryReportList() {
useEffect(() => { useEffect(() => {
loadReports(); loadReports();
loadPatients(); loadPatients();
loadDoctors();
}, []); }, []);
// Recarrega automaticamente quando o filtro de status muda // Recarrega automaticamente quando o filtro de status muda
// (evita depender do clique em Buscar) // (evita depender do clique em Buscar)
useEffect(() => { useEffect(() => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
loadReports(); loadReports();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [statusFilter]); }, [statusFilter]);
@ -54,6 +57,15 @@ export function SecretaryReportList() {
} }
}; };
const loadDoctors = async () => {
try {
const data = await doctorService.list({});
setDoctors(Array.isArray(data) ? data : []);
} catch (error) {
console.error("Erro ao carregar médicos:", error);
}
};
const handleOpenCreateModal = () => { const handleOpenCreateModal = () => {
setFormData({ setFormData({
patient_id: "", patient_id: "",
@ -120,7 +132,7 @@ export function SecretaryReportList() {
} }
try { try {
await reportService.update(selectedReport.id, { const updatedReport = await reportService.update(selectedReport.id, {
patient_id: formData.patient_id, patient_id: formData.patient_id,
exam: formData.exam || undefined, exam: formData.exam || undefined,
diagnosis: formData.diagnosis || undefined, diagnosis: formData.diagnosis || undefined,
@ -130,10 +142,19 @@ export function SecretaryReportList() {
requested_by: formData.requested_by || undefined, requested_by: formData.requested_by || undefined,
}); });
console.log("[SecretaryReportList] Relatório atualizado:", updatedReport);
console.log(
"[SecretaryReportList] Novo requested_by:",
formData.requested_by
);
toast.success("Relatório atualizado com sucesso!"); toast.success("Relatório atualizado com sucesso!");
setShowEditModal(false); setShowEditModal(false);
setSelectedReport(null); setSelectedReport(null);
loadReports();
// Limpar cache de nomes antes de recarregar
setRequestedByNames({});
await loadReports();
} catch (error) { } catch (error) {
console.error("Erro ao atualizar relatório:", error); console.error("Erro ao atualizar relatório:", error);
toast.error("Erro ao atualizar relatório"); toast.error("Erro ao atualizar relatório");
@ -276,29 +297,55 @@ export function SecretaryReportList() {
const loadRequestedByNames = async (reportsList: Report[]) => { const loadRequestedByNames = async (reportsList: Report[]) => {
const names: Record<string, string> = {}; const names: Record<string, string> = {};
// Buscar nomes únicos de requested_by // Buscar nomes únicos de requested_by
const uniqueIds = [...new Set(reportsList.map(r => r.requested_by).filter(Boolean))]; const uniqueIds = [
...new Set(reportsList.map((r) => r.requested_by).filter(Boolean)),
for (const id of uniqueIds) { ];
try {
// Tentar buscar como médico primeiro try {
const doctors = await doctorService.list({}); // Buscar todos os médicos de uma vez
const doctor = doctors.find((d) => (d as any).user_id === id || d.id === id); const doctors = await doctorService.list({});
if (doctor) { for (const id of uniqueIds) {
names[id!] = doctor.full_name || "Dr. " + (doctor.full_name || "Médico"); // Verificar se já é um nome (não é UUID)
} else { const isUUID =
// Se não for médico, simplesmente pegar o texto que já está armazenado /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
// pois requested_by pode ser um nome direto id!
);
if (!isUUID) {
// Já é um nome direto (dados legados), manter como está
names[id!] = id!; names[id!] = id!;
continue;
}
// É um UUID, procurar o médico
const doctor = doctors.find((d) => {
const doctorAny = d as any;
return doctorAny.user_id === id || d.id === id || doctorAny.id === id;
});
if (doctor && doctor.full_name) {
// Formatar o nome com "Dr." se não tiver
const doctorName = doctor.full_name;
names[id!] = /^dr\.?\s/i.test(doctorName)
? doctorName
: `Dr. ${doctorName}`;
} else {
// UUID não encontrado - médico pode ter sido deletado
// Mostrar mensagem mais amigável
names[id!] = "Médico não cadastrado";
} }
} catch (error) {
console.error(`Erro ao buscar nome para ID ${id}:`, error);
names[id!] = id!; // Manter o valor original em caso de erro
} }
} catch (error) {
console.error("Erro ao buscar nomes dos médicos:", error);
// Em caso de erro, manter os IDs originais
uniqueIds.forEach((id) => {
names[id!] = id!;
});
} }
setRequestedByNames(names); setRequestedByNames(names);
}; };
@ -307,7 +354,15 @@ export function SecretaryReportList() {
try { try {
// Se um filtro de status estiver aplicado, encaminhar para o serviço // Se um filtro de status estiver aplicado, encaminhar para o serviço
// Cast explícito para o tipo esperado pelo serviço (ReportStatus) // Cast explícito para o tipo esperado pelo serviço (ReportStatus)
const filters = statusFilter ? { status: statusFilter as "draft" | "completed" | "pending" | "cancelled" } : undefined; const filters = statusFilter
? {
status: statusFilter as
| "draft"
| "completed"
| "pending"
| "cancelled",
}
: undefined;
console.log("[SecretaryReportList] loadReports filters:", filters); console.log("[SecretaryReportList] loadReports filters:", filters);
const data = await reportService.list(filters); const data = await reportService.list(filters);
console.log("✅ Relatórios carregados:", data); console.log("✅ Relatórios carregados:", data);
@ -323,12 +378,12 @@ export function SecretaryReportList() {
}); });
} }
setReports(reportsList); setReports(reportsList);
// Carregar nomes dos solicitantes // Carregar nomes dos solicitantes
if (reportsList.length > 0) { if (reportsList.length > 0) {
await loadRequestedByNames(reportsList); await loadRequestedByNames(reportsList);
} }
if (Array.isArray(data) && data.length === 0) { if (Array.isArray(data) && data.length === 0) {
console.warn("⚠️ Nenhum relatório encontrado na API"); console.warn("⚠️ Nenhum relatório encontrado na API");
} }
@ -517,8 +572,9 @@ export function SecretaryReportList() {
{formatDate(report.created_at)} {formatDate(report.created_at)}
</td> </td>
<td className="px-6 py-4 text-sm text-gray-700"> <td className="px-6 py-4 text-sm text-gray-700">
{report.requested_by {report.requested_by
? (requestedByNames[report.requested_by] || report.requested_by) ? requestedByNames[report.requested_by] ||
report.requested_by
: "—"} : "—"}
</td> </td>
<td className="px-6 py-4"> <td className="px-6 py-4">
@ -609,6 +665,26 @@ export function SecretaryReportList() {
/> />
</div> </div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Solicitado por
</label>
<select
value={formData.requested_by}
onChange={(e) =>
setFormData({ ...formData, requested_by: e.target.value })
}
className="form-input"
>
<option value="">Selecione um médico</option>
{doctors.map((doctor) => (
<option key={doctor.id} value={doctor.id}>
{doctor.full_name}
</option>
))}
</select>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Diagnóstico Diagnóstico
@ -731,8 +807,9 @@ export function SecretaryReportList() {
Solicitado por Solicitado por
</label> </label>
<p className="text-gray-900"> <p className="text-gray-900">
{selectedReport.requested_by {selectedReport.requested_by
? (requestedByNames[selectedReport.requested_by] || selectedReport.requested_by) ? requestedByNames[selectedReport.requested_by] ||
selectedReport.requested_by
: "—"} : "—"}
</p> </p>
</div> </div>
@ -889,15 +966,20 @@ export function SecretaryReportList() {
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Solicitado por Solicitado por
</label> </label>
<input <select
type="text"
value={formData.requested_by} value={formData.requested_by}
onChange={(e) => onChange={(e) =>
setFormData({ ...formData, requested_by: e.target.value }) setFormData({ ...formData, requested_by: e.target.value })
} }
className="form-input" className="form-input"
placeholder="Nome do médico solicitante" >
/> <option value="">Selecione um médico</option>
{doctors.map((doctor) => (
<option key={doctor.id} value={doctor.id}>
{doctor.full_name}
</option>
))}
</select>
</div> </div>
<div> <div>
@ -954,5 +1036,3 @@ export function SecretaryReportList() {
</div> </div>
); );
} }

View File

@ -8,12 +8,12 @@
body { body {
font-family: "Inter", system-ui, -apple-system, sans-serif; font-family: "Inter", system-ui, -apple-system, sans-serif;
} }
/* Garantir que o texto nunca fique muito grande */ /* Garantir que o texto nunca fique muito grande */
html { html {
font-size: 16px; font-size: 16px;
} }
@media (max-width: 640px) { @media (max-width: 640px) {
html { html {
font-size: 14px; font-size: 14px;

View File

@ -87,6 +87,9 @@ const AcompanhamentoPaciente: React.FC = () => {
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined); const [avatarUrl, setAvatarUrl] = useState<string | undefined>(undefined);
const [selectedLaudo, setSelectedLaudo] = useState<Report | null>(null); const [selectedLaudo, setSelectedLaudo] = useState<Report | null>(null);
const [showLaudoModal, setShowLaudoModal] = useState(false); const [showLaudoModal, setShowLaudoModal] = useState(false);
const [requestedByNames, setRequestedByNames] = useState<
Record<string, string>
>({});
const pacienteId = user?.id || ""; const pacienteId = user?.id || "";
const pacienteNome = user?.nome || "Paciente"; const pacienteNome = user?.nome || "Paciente";
@ -99,7 +102,10 @@ const AcompanhamentoPaciente: React.FC = () => {
// Detecta se veio de navegação com estado para abrir aba específica // Detecta se veio de navegação com estado para abrir aba específica
useEffect(() => { useEffect(() => {
if (location.state && (location.state as { activeTab?: string }).activeTab) { if (
location.state &&
(location.state as { activeTab?: string }).activeTab
) {
const state = location.state as { activeTab: string }; const state = location.state as { activeTab: string };
setActiveTab(state.activeTab); setActiveTab(state.activeTab);
// Limpa o estado após usar // Limpa o estado após usar
@ -196,6 +202,32 @@ const AcompanhamentoPaciente: React.FC = () => {
fetchConsultas(); fetchConsultas();
}, [fetchConsultas]); }, [fetchConsultas]);
// Função para carregar nomes dos médicos solicitantes
const loadRequestedByNames = useCallback(async (reports: Report[]) => {
const uniqueIds = [
...new Set(
reports.map((r) => r.requested_by).filter((id): id is string => !!id)
),
];
if (uniqueIds.length === 0) return;
try {
const doctors = await doctorService.list();
const nameMap: Record<string, string> = {};
uniqueIds.forEach((id) => {
const doctor = doctors.find((d) => d.id === id);
if (doctor && doctor.full_name) {
nameMap[id] = formatDoctorName(doctor.full_name);
}
});
setRequestedByNames(nameMap);
} catch (error) {
console.error("Erro ao buscar nomes dos médicos:", error);
}
}, []);
// Recarregar consultas quando mudar para a aba de consultas // Recarregar consultas quando mudar para a aba de consultas
const fetchLaudos = useCallback(async () => { const fetchLaudos = useCallback(async () => {
if (!pacienteId) return; if (!pacienteId) return;
@ -203,6 +235,8 @@ const AcompanhamentoPaciente: React.FC = () => {
try { try {
const data = await reportService.list({ patient_id: pacienteId }); const data = await reportService.list({ patient_id: pacienteId });
setLaudos(data); setLaudos(data);
// Carregar nomes dos médicos
await loadRequestedByNames(data);
} catch (error) { } catch (error) {
console.error("Erro ao buscar laudos:", error); console.error("Erro ao buscar laudos:", error);
toast.error("Erro ao carregar laudos"); toast.error("Erro ao carregar laudos");
@ -210,7 +244,7 @@ const AcompanhamentoPaciente: React.FC = () => {
} finally { } finally {
setLoadingLaudos(false); setLoadingLaudos(false);
} }
}, [pacienteId]); }, [pacienteId, loadRequestedByNames]);
useEffect(() => { useEffect(() => {
if (activeTab === "appointments") { if (activeTab === "appointments") {
@ -273,8 +307,12 @@ const AcompanhamentoPaciente: React.FC = () => {
const consultasPassadasDashboard = todasConsultasPassadas.slice(0, 3); const consultasPassadasDashboard = todasConsultasPassadas.slice(0, 3);
// Para a página de consultas (com paginação) // Para a página de consultas (com paginação)
const totalPaginasProximas = Math.ceil(todasConsultasProximas.length / consultasPorPagina); const totalPaginasProximas = Math.ceil(
const totalPaginasPassadas = Math.ceil(todasConsultasPassadas.length / consultasPorPagina); todasConsultasProximas.length / consultasPorPagina
);
const totalPaginasPassadas = Math.ceil(
todasConsultasPassadas.length / consultasPorPagina
);
const consultasProximas = todasConsultasProximas.slice( const consultasProximas = todasConsultasProximas.slice(
(paginaProximas - 1) * consultasPorPagina, (paginaProximas - 1) * consultasPorPagina,
@ -369,7 +407,9 @@ const AcompanhamentoPaciente: React.FC = () => {
<p className="font-medium text-gray-900 dark:text-white truncate text-sm sm:text-base"> <p className="font-medium text-gray-900 dark:text-white truncate text-sm sm:text-base">
{pacienteNome} {pacienteNome}
</p> </p>
<p className="text-xs sm:text-sm text-gray-600 dark:text-gray-400">Paciente</p> <p className="text-xs sm:text-sm text-gray-600 dark:text-gray-400">
Paciente
</p>
</div> </div>
</div> </div>
</div> </div>
@ -649,9 +689,13 @@ const AcompanhamentoPaciente: React.FC = () => {
{getMedicoEspecialidade(c.medicoId)} {getMedicoEspecialidade(c.medicoId)}
</p> </p>
<p className="text-sm text-gray-600 dark:text-gray-400"> <p className="text-sm text-gray-600 dark:text-gray-400">
{format(new Date(c.dataHora), "dd/MM/yyyy - HH:mm", { {format(
locale: ptBR, new Date(c.dataHora),
})} "dd/MM/yyyy - HH:mm",
{
locale: ptBR,
}
)}
</p> </p>
</div> </div>
</div> </div>
@ -662,7 +706,8 @@ const AcompanhamentoPaciente: React.FC = () => {
onClick={() => setActiveTab("appointments")} onClick={() => setActiveTab("appointments")}
className="w-full mt-4 px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500" className="w-full mt-4 px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
> >
Ver mais consultas ({todasConsultasProximas.length - 3} restantes) Ver mais consultas ({todasConsultasProximas.length - 3}{" "}
restantes)
</button> </button>
)} )}
</> </>
@ -698,10 +743,7 @@ const AcompanhamentoPaciente: React.FC = () => {
<User className="h-5 w-5 text-blue-600 dark:text-blue-400" /> <User className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<span>Editar Perfil</span> <span>Editar Perfil</span>
</button> </button>
<button <button onClick={() => navigate("/ajuda")} className="form-input">
onClick={() => navigate("/ajuda")}
className="form-input"
>
<HelpCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" /> <HelpCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<span>Central de Ajuda</span> <span>Central de Ajuda</span>
</button> </button>
@ -776,16 +818,23 @@ const AcompanhamentoPaciente: React.FC = () => {
<div className="space-y-4"> <div className="space-y-4">
{consultasProximas.map((c) => renderAppointmentCard(c))} {consultasProximas.map((c) => renderAppointmentCard(c))}
</div> </div>
{/* Paginação Próximas Consultas */} {/* Paginação Próximas Consultas */}
{totalPaginasProximas > 1 && ( {totalPaginasProximas > 1 && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700"> <div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
<div className="text-sm text-gray-600 dark:text-gray-400"> <div className="text-sm text-gray-600 dark:text-gray-400">
Mostrando {((paginaProximas - 1) * consultasPorPagina) + 1} a {Math.min(paginaProximas * consultasPorPagina, todasConsultasProximas.length)} de {todasConsultasProximas.length} consultas Mostrando {(paginaProximas - 1) * consultasPorPagina + 1} a{" "}
{Math.min(
paginaProximas * consultasPorPagina,
todasConsultasProximas.length
)}{" "}
de {todasConsultasProximas.length} consultas
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setPaginaProximas(Math.max(1, paginaProximas - 1))} onClick={() =>
setPaginaProximas(Math.max(1, paginaProximas - 1))
}
disabled={paginaProximas === 1} disabled={paginaProximas === 1}
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium" className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
> >
@ -795,7 +844,11 @@ const AcompanhamentoPaciente: React.FC = () => {
Página {paginaProximas} de {totalPaginasProximas} Página {paginaProximas} de {totalPaginasProximas}
</span> </span>
<button <button
onClick={() => setPaginaProximas(Math.min(totalPaginasProximas, paginaProximas + 1))} onClick={() =>
setPaginaProximas(
Math.min(totalPaginasProximas, paginaProximas + 1)
)
}
disabled={paginaProximas === totalPaginasProximas} disabled={paginaProximas === totalPaginasProximas}
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium" className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
> >
@ -826,16 +879,23 @@ const AcompanhamentoPaciente: React.FC = () => {
<div className="space-y-4"> <div className="space-y-4">
{consultasPassadas.map((c) => renderAppointmentCard(c, true))} {consultasPassadas.map((c) => renderAppointmentCard(c, true))}
</div> </div>
{/* Paginação Consultas Passadas */} {/* Paginação Consultas Passadas */}
{totalPaginasPassadas > 1 && ( {totalPaginasPassadas > 1 && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700"> <div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-6 pt-4 border-t border-gray-200 dark:border-slate-700">
<div className="text-sm text-gray-600 dark:text-gray-400"> <div className="text-sm text-gray-600 dark:text-gray-400">
Mostrando {((paginaPassadas - 1) * consultasPorPagina) + 1} a {Math.min(paginaPassadas * consultasPorPagina, todasConsultasPassadas.length)} de {todasConsultasPassadas.length} consultas Mostrando {(paginaPassadas - 1) * consultasPorPagina + 1} a{" "}
{Math.min(
paginaPassadas * consultasPorPagina,
todasConsultasPassadas.length
)}{" "}
de {todasConsultasPassadas.length} consultas
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setPaginaPassadas(Math.max(1, paginaPassadas - 1))} onClick={() =>
setPaginaPassadas(Math.max(1, paginaPassadas - 1))
}
disabled={paginaPassadas === 1} disabled={paginaPassadas === 1}
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium" className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
> >
@ -845,7 +905,11 @@ const AcompanhamentoPaciente: React.FC = () => {
Página {paginaPassadas} de {totalPaginasPassadas} Página {paginaPassadas} de {totalPaginasPassadas}
</span> </span>
<button <button
onClick={() => setPaginaPassadas(Math.min(totalPaginasPassadas, paginaPassadas + 1))} onClick={() =>
setPaginaPassadas(
Math.min(totalPaginasPassadas, paginaPassadas + 1)
)
}
disabled={paginaPassadas === totalPaginasPassadas} disabled={paginaPassadas === totalPaginasPassadas}
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium" className="px-4 py-2 rounded-lg border border-gray-300 dark:border-slate-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 font-medium"
> >
@ -1079,7 +1143,7 @@ const AcompanhamentoPaciente: React.FC = () => {
return ( return (
<div className="flex flex-col lg:flex-row min-h-screen bg-gray-50 dark:bg-slate-950"> <div className="flex flex-col lg:flex-row min-h-screen bg-gray-50 dark:bg-slate-950">
{renderSidebar()} {renderSidebar()}
{/* Mobile Header */} {/* 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="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"> <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"> <p className="font-medium text-gray-900 dark:text-white text-sm truncate">
{pacienteNome} {pacienteNome}
</p> </p>
<p className="text-xs text-gray-600 dark:text-gray-400">Paciente</p> <p className="text-xs text-gray-600 dark:text-gray-400">
Paciente
</p>
</div> </div>
</div> </div>
<button <button
@ -1110,7 +1176,7 @@ const AcompanhamentoPaciente: React.FC = () => {
<LogOut className="h-5 w-5" /> <LogOut className="h-5 w-5" />
</button> </button>
</div> </div>
{/* Mobile Nav */} {/* Mobile Nav */}
<div className="mt-3 flex gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide"> <div className="mt-3 flex gap-2 overflow-x-auto pb-2 -mx-4 px-4 scrollbar-hide">
{menuItems.map((item) => { {menuItems.map((item) => {
@ -1143,7 +1209,9 @@ const AcompanhamentoPaciente: React.FC = () => {
</div> </div>
<main className="flex-1 overflow-y-auto"> <main className="flex-1 overflow-y-auto">
<div className="container mx-auto p-4 sm:p-6 lg:p-8">{renderContent()}</div> <div className="container mx-auto p-4 sm:p-6 lg:p-8">
{renderContent()}
</div>
</main> </main>
{/* Modal de Visualização do Laudo */} {/* Modal de Visualização do Laudo */}
@ -1207,11 +1275,14 @@ const AcompanhamentoPaciente: React.FC = () => {
Data de Criação Data de Criação
</label> </label>
<p className="text-sm text-gray-900 dark:text-white"> <p className="text-sm text-gray-900 dark:text-white">
{new Date(selectedLaudo.created_at).toLocaleDateString("pt-BR", { {new Date(selectedLaudo.created_at).toLocaleDateString(
day: "2-digit", "pt-BR",
month: "long", {
year: "numeric", day: "2-digit",
})} month: "long",
year: "numeric",
}
)}
</p> </p>
</div> </div>
{selectedLaudo.due_at && ( {selectedLaudo.due_at && (
@ -1220,11 +1291,14 @@ const AcompanhamentoPaciente: React.FC = () => {
Prazo de Entrega Prazo de Entrega
</label> </label>
<p className="text-sm text-gray-900 dark:text-white"> <p className="text-sm text-gray-900 dark:text-white">
{new Date(selectedLaudo.due_at).toLocaleDateString("pt-BR", { {new Date(selectedLaudo.due_at).toLocaleDateString(
day: "2-digit", "pt-BR",
month: "long", {
year: "numeric", day: "2-digit",
})} month: "long",
year: "numeric",
}
)}
</p> </p>
</div> </div>
)} )}
@ -1294,7 +1368,8 @@ const AcompanhamentoPaciente: React.FC = () => {
</label> </label>
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4"> <div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
<p className="text-sm text-gray-900 dark:text-white"> <p className="text-sm text-gray-900 dark:text-white">
{selectedLaudo.requested_by} {requestedByNames[selectedLaudo.requested_by] ||
selectedLaudo.requested_by}
</p> </p>
</div> </div>
</div> </div>
@ -1308,7 +1383,9 @@ const AcompanhamentoPaciente: React.FC = () => {
</label> </label>
<div <div
className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 prose dark:prose-invert max-w-none" className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 prose dark:prose-invert max-w-none"
dangerouslySetInnerHTML={{ __html: selectedLaudo.content_html }} dangerouslySetInnerHTML={{
__html: selectedLaudo.content_html,
}}
/> />
</div> </div>
)} )}
@ -1334,5 +1411,3 @@ const AcompanhamentoPaciente: React.FC = () => {
}; };
export default AcompanhamentoPaciente; export default AcompanhamentoPaciente;

View File

@ -225,8 +225,8 @@ const AgendamentoPaciente: React.FC = () => {
)} )}
</div> </div>
</div> </div>
<button <button
onClick={resetarAgendamento} onClick={resetarAgendamento}
className="btn-primary w-full sm:w-auto text-sm sm:text-base" className="btn-primary w-full sm:w-auto text-sm sm:text-base"
> >
Fazer Novo Agendamento Fazer Novo Agendamento
@ -247,7 +247,9 @@ const AgendamentoPaciente: React.FC = () => {
<h1 className="text-lg sm:text-xl lg:text-2xl font-bold truncate"> <h1 className="text-lg sm:text-xl lg:text-2xl font-bold truncate">
Bem-vindo(a), {pacienteLogado.nome}! Bem-vindo(a), {pacienteLogado.nome}!
</h1> </h1>
<p className="opacity-90 text-sm sm:text-base">Agende sua consulta médica</p> <p className="opacity-90 text-sm sm:text-base">
Agende sua consulta médica
</p>
</div> </div>
<button <button
onClick={logout} onClick={logout}
@ -286,215 +288,216 @@ const AgendamentoPaciente: React.FC = () => {
</div> </div>
<div className="bg-white rounded-lg sm:rounded-xl shadow border border-gray-200 p-4 sm:p-6"> <div className="bg-white rounded-lg sm:rounded-xl shadow border border-gray-200 p-4 sm:p-6">
{/* Etapa 1: Seleção de Médico */} {/* Etapa 1: Seleção de Médico */}
{etapa === 1 && ( {etapa === 1 && (
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
<h2 className="text-lg sm:text-xl font-semibold flex items-center"> <h2 className="text-lg sm:text-xl font-semibold flex items-center">
<User className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" /> <User className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
Selecione o Médico Selecione o Médico
</h2> </h2>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Médico/Especialidade
</label>
<select
value={agendamento.medicoId}
onChange={(e) => handleMedicoChange(e.target.value)}
className="form-input text-sm sm:text-base"
required
>
<option value="">Selecione um médico</option>
{medicos.map((medico) => (
<option key={medico._id} value={medico._id}>
{medico.nome} - {medico.especialidade} (R${" "}
{medico.valorConsulta})
</option>
))}
</select>
</div>
<div className="flex justify-end pt-2">
<button
onClick={() => setEtapa(2)}
disabled={!agendamento.medicoId}
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto text-sm sm:text-base"
>
Próximo
</button>
</div>
</div>
)}
{/* Etapa 2: Seleção de Data e Horário */}
{etapa === 2 && (
<div className="space-y-4 sm:space-y-6">
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
<Calendar className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
Selecione Data e Horário
</h2>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Data da Consulta
</label>
<select
value={agendamento.data}
onChange={(e) => handleDataChange(e.target.value)}
className="form-input text-sm sm:text-base"
required
>
<option value="">Selecione uma data</option>
{proximosSeteDias().map((dia) => (
<option key={dia.valor} value={dia.valor}>
{dia.label}
</option>
))}
</select>
</div>
{agendamento.data && agendamento.medicoId && (
<div> <div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2"> <label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Horários Disponíveis Médico/Especialidade
</label> </label>
<AvailableSlotsPicker <select
doctorId={agendamento.medicoId} value={agendamento.medicoId}
date={agendamento.data} onChange={(e) => handleMedicoChange(e.target.value)}
onSelect={(t) => className="form-input text-sm sm:text-base"
setAgendamento((prev) => ({ ...prev, horario: t })) required
>
<option value="">Selecione um médico</option>
{medicos.map((medico) => (
<option key={medico._id} value={medico._id}>
{medico.nome} - {medico.especialidade} (R${" "}
{medico.valorConsulta})
</option>
))}
</select>
</div>
<div className="flex justify-end pt-2">
<button
onClick={() => setEtapa(2)}
disabled={!agendamento.medicoId}
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed w-full sm:w-auto text-sm sm:text-base"
>
Próximo
</button>
</div>
</div>
)}
{/* Etapa 2: Seleção de Data e Horário */}
{etapa === 2 && (
<div className="space-y-4 sm:space-y-6">
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
<Calendar className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
Selecione Data e Horário
</h2>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Data da Consulta
</label>
<select
value={agendamento.data}
onChange={(e) => handleDataChange(e.target.value)}
className="form-input text-sm sm:text-base"
required
>
<option value="">Selecione uma data</option>
{proximosSeteDias().map((dia) => (
<option key={dia.valor} value={dia.valor}>
{dia.label}
</option>
))}
</select>
</div>
{agendamento.data && agendamento.medicoId && (
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Horários Disponíveis
</label>
<AvailableSlotsPicker
doctorId={agendamento.medicoId}
date={agendamento.data}
onSelect={(t) =>
setAgendamento((prev) => ({ ...prev, horario: t }))
}
/>
</div>
)}
<div className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
<button
onClick={() => setEtapa(1)}
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1"
>
Voltar
</button>
<button
onClick={() => setEtapa(3)}
disabled={!agendamento.horario}
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2"
>
Próximo
</button>
</div>
</div>
)}
{/* Etapa 3: Informações Adicionais */}
{etapa === 3 && (
<div className="space-y-4 sm:space-y-6">
<h2 className="text-lg sm:text-xl font-semibold flex items-center">
<FileText className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" />
Informações da Consulta
</h2>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Tipo de Consulta
</label>
<select
value={agendamento.tipoConsulta}
onChange={(e) =>
setAgendamento((prev) => ({
...prev,
tipoConsulta: e.target.value,
}))
} }
className="form-input text-sm sm:text-base"
>
<option value="primeira-vez">Primeira Consulta</option>
<option value="retorno">Retorno</option>
<option value="urgencia">Urgência</option>
</select>
</div>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Motivo da Consulta
</label>
<textarea
value={agendamento.motivoConsulta}
onChange={(e) =>
setAgendamento((prev) => ({
...prev,
motivoConsulta: e.target.value,
}))
}
className="form-input text-sm sm:text-base"
rows={3}
placeholder="Descreva brevemente o motivo da consulta"
/> />
</div> </div>
)}
<div className="flex flex-col sm:flex-row justify-between gap-3 pt-2"> <div>
<button <label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
onClick={() => setEtapa(1)} Observações (opcional)
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1" </label>
> <textarea
Voltar value={agendamento.observacoes}
</button> onChange={(e) =>
<button setAgendamento((prev) => ({
onClick={() => setEtapa(3)} ...prev,
disabled={!agendamento.horario} observacoes: e.target.value,
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2" }))
> }
Próximo className="form-input text-sm sm:text-base"
</button> rows={2}
</div> placeholder="Informações adicionais relevantes"
</div> />
)} </div>
{/* Etapa 3: Informações Adicionais */} {/* Resumo do Agendamento */}
{etapa === 3 && ( <div className="bg-gray-50 rounded-lg p-3 sm:p-4 border border-gray-200">
<div className="space-y-4 sm:space-y-6"> <h3 className="text-sm sm:text-base font-semibold mb-2 sm:mb-3">
<h2 className="text-lg sm:text-xl font-semibold flex items-center"> Resumo do Agendamento:
<FileText className="w-4 h-4 sm:w-5 sm:h-5 mr-2 flex-shrink-0" /> </h3>
Informações da Consulta <div className="space-y-1 sm:space-y-1.5 text-xs sm:text-sm">
</h2> <p className="break-words">
<strong>Paciente:</strong> {pacienteLogado.nome}
</p>
<p className="break-words">
<strong>Médico:</strong> {medicoSelecionado?.nome}
</p>
<p>
<strong>Data:</strong>{" "}
{format(new Date(agendamento.data), "dd/MM/yyyy", {
locale: ptBR,
})}
</p>
<p>
<strong>Horário:</strong> {agendamento.horario}
</p>
<p>
<strong>Valor:</strong> R${" "}
{medicoSelecionado?.valorConsulta}
</p>
</div>
</div>
<div> <div className="flex flex-col sm:flex-row justify-between gap-3 pt-2">
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2"> <button
Tipo de Consulta onClick={() => setEtapa(2)}
</label> className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1"
<select >
value={agendamento.tipoConsulta} Voltar
onChange={(e) => </button>
setAgendamento((prev) => ({ <button
...prev, onClick={confirmarAgendamento}
tipoConsulta: e.target.value, disabled={loading}
})) className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2"
} >
className="form-input text-sm sm:text-base" {loading ? "Agendando..." : "Confirmar Agendamento"}
> </button>
<option value="primeira-vez">Primeira Consulta</option>
<option value="retorno">Retorno</option>
<option value="urgencia">Urgência</option>
</select>
</div>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Motivo da Consulta
</label>
<textarea
value={agendamento.motivoConsulta}
onChange={(e) =>
setAgendamento((prev) => ({
...prev,
motivoConsulta: e.target.value,
}))
}
className="form-input text-sm sm:text-base"
rows={3}
placeholder="Descreva brevemente o motivo da consulta"
/>
</div>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-2">
Observações (opcional)
</label>
<textarea
value={agendamento.observacoes}
onChange={(e) =>
setAgendamento((prev) => ({
...prev,
observacoes: e.target.value,
}))
}
className="form-input text-sm sm:text-base"
rows={2}
placeholder="Informações adicionais relevantes"
/>
</div>
{/* Resumo do Agendamento */}
<div className="bg-gray-50 rounded-lg p-3 sm:p-4 border border-gray-200">
<h3 className="text-sm sm:text-base font-semibold mb-2 sm:mb-3">
Resumo do Agendamento:
</h3>
<div className="space-y-1 sm:space-y-1.5 text-xs sm:text-sm">
<p className="break-words">
<strong>Paciente:</strong> {pacienteLogado.nome}
</p>
<p className="break-words">
<strong>Médico:</strong> {medicoSelecionado?.nome}
</p>
<p>
<strong>Data:</strong>{" "}
{format(new Date(agendamento.data), "dd/MM/yyyy", {
locale: ptBR,
})}
</p>
<p>
<strong>Horário:</strong> {agendamento.horario}
</p>
<p>
<strong>Valor:</strong> R$ {medicoSelecionado?.valorConsulta}
</p>
</div> </div>
</div> </div>
)}
<div className="flex flex-col sm:flex-row justify-between gap-3 pt-2"> </div>
<button
onClick={() => setEtapa(2)}
className="btn-secondary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-gray-300 w-full sm:w-auto text-sm sm:text-base order-2 sm:order-1"
>
Voltar
</button>
<button
onClick={confirmarAgendamento}
disabled={loading}
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500 w-full sm:w-auto text-sm sm:text-base order-1 sm:order-2"
>
{loading ? "Agendando..." : "Confirmar Agendamento"}
</button>
</div>
</div>
)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -112,7 +112,10 @@ const Home: React.FC = () => {
}; };
return ( return (
<div className="space-y-6 sm:space-y-8 px-4 sm:px-6 lg:px-8" id="main-content"> <div
className="space-y-6 sm:space-y-8 px-4 sm:px-6 lg:px-8"
id="main-content"
>
{/* Componente invisível que detecta tokens de recuperação e redireciona */} {/* Componente invisível que detecta tokens de recuperação e redireciona */}
<RecoveryRedirect /> <RecoveryRedirect />
@ -257,9 +260,14 @@ const ActionCard: React.FC<ActionCardProps> = ({
<div <div
className={`w-10 h-10 sm:w-12 sm:h-12 ${iconBgColor} rounded-lg flex items-center justify-center mb-3 sm:mb-4 group-hover:scale-110 transition-transform`} className={`w-10 h-10 sm:w-12 sm:h-12 ${iconBgColor} rounded-lg flex items-center justify-center mb-3 sm:mb-4 group-hover:scale-110 transition-transform`}
> >
<Icon className={`w-5 h-5 sm:w-6 sm:h-6 text-white`} aria-hidden="true" /> <Icon
className={`w-5 h-5 sm:w-6 sm:h-6 text-white`}
aria-hidden="true"
/>
</div> </div>
<h3 className="text-base sm:text-lg font-semibold mb-2 text-gray-900">{title}</h3> <h3 className="text-base sm:text-lg font-semibold mb-2 text-gray-900">
{title}
</h3>
<p className="text-xs sm:text-sm text-gray-600 mb-3 sm:mb-4 leading-relaxed"> <p className="text-xs sm:text-sm text-gray-600 mb-3 sm:mb-4 leading-relaxed">
{description} {description}
</p> </p>

View File

@ -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" />{" "} <Users className="w-5 h-5 sm:w-6 sm:h-6 text-blue-600 flex-shrink-0" />{" "}
Pacientes Cadastrados Pacientes Cadastrados
</h2> </h2>
{loading && ( {loading && (
<div className="text-sm sm:text-base text-gray-500 text-center py-8"> <div className="text-sm sm:text-base text-gray-500 text-center py-8">
Carregando pacientes... Carregando pacientes...
</div> </div>
)} )}
{!loading && error && ( {!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"> <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} {error}
</div> </div>
)} )}
{!loading && !error && pacientes.length === 0 && ( {!loading && !error && pacientes.length === 0 && (
<div className="text-sm sm:text-base text-gray-500 text-center py-8"> <div className="text-sm sm:text-base text-gray-500 text-center py-8">
Nenhum paciente cadastrado. Nenhum paciente cadastrado.
</div> </div>
)} )}
{!loading && !error && pacientes.length > 0 && ( {!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"> <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) => ( {pacientes.map((paciente, idx) => (
@ -106,18 +106,23 @@ const ListaPacientes: React.FC = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="space-y-1.5 sm:space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<div className="text-xs sm:text-sm text-gray-700"> <div className="text-xs sm:text-sm text-gray-700">
<strong className="font-medium">CPF:</strong> {formatCPF(paciente.cpf)} <strong className="font-medium">CPF:</strong>{" "}
{formatCPF(paciente.cpf)}
</div> </div>
<div className="flex items-start gap-2 text-xs sm:text-sm text-gray-700"> <div className="flex items-start gap-2 text-xs sm:text-sm text-gray-700">
<Mail className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0 mt-0.5" /> <Mail className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0 mt-0.5" />
<span className="break-all">{formatEmail(paciente.email)}</span> <span className="break-all">
{formatEmail(paciente.email)}
</span>
</div> </div>
<div className="flex items-center gap-2 text-xs sm:text-sm text-gray-700"> <div className="flex items-center gap-2 text-xs sm:text-sm text-gray-700">
<Phone className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" /> <Phone className="w-3.5 h-3.5 sm:w-4 sm:h-4 flex-shrink-0" />
<span className="break-words">{formatPhone(paciente.phone_mobile)}</span> <span className="break-words">
{formatPhone(paciente.phone_mobile)}
</span>
</div> </div>
<div className="text-xs sm:text-sm text-gray-500 pt-1"> <div className="text-xs sm:text-sm text-gray-500 pt-1">
<strong className="font-medium">Nascimento:</strong>{" "} <strong className="font-medium">Nascimento:</strong>{" "}

View File

@ -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" />{" "} <UserPlus className="w-5 h-5 sm:w-6 sm:h-6 text-green-600 flex-shrink-0" />{" "}
Secretárias Cadastradas Secretárias Cadastradas
</h2> </h2>
{secretarias.length === 0 ? ( {secretarias.length === 0 ? (
<div className="text-sm sm:text-base text-gray-500 text-center py-8"> <div className="text-sm sm:text-base text-gray-500 text-center py-8">
Nenhuma secretária cadastrada. Nenhuma secretária cadastrada.
@ -45,7 +45,7 @@ const ListaSecretarias: React.FC = () => {
{sec.nome} {sec.nome}
</span> </span>
</div> </div>
<div className="space-y-1.5 sm:space-y-2"> <div className="space-y-1.5 sm:space-y-2">
<div className="text-xs sm:text-sm text-gray-700"> <div className="text-xs sm:text-sm text-gray-700">
<strong className="font-medium">CPF:</strong> {sec.cpf} <strong className="font-medium">CPF:</strong> {sec.cpf}

View File

@ -273,7 +273,9 @@ const PainelAdmin: React.FC = () => {
const formattedCpf = formatCPF(userCpf); const formattedCpf = formatCPF(userCpf);
// Formatar telefone celular se fornecido // Formatar telefone celular se fornecido
const formattedPhoneMobile = userPhoneMobile ? formatPhone(userPhoneMobile) : ""; const formattedPhoneMobile = userPhoneMobile
? formatPhone(userPhoneMobile)
: "";
// Criar usuário com senha (método obrigatório com CPF) // Criar usuário com senha (método obrigatório com CPF)
await userService.createUserWithPassword({ await userService.createUserWithPassword({
@ -299,10 +301,18 @@ const PainelAdmin: React.FC = () => {
// Mostrar mensagem de erro detalhada // Mostrar mensagem de erro detalhada
const errorMessage = const errorMessage =
(error as { response?: { data?: { message?: string; error?: string } }; message?: string }) (
?.response?.data?.message || error as {
(error as { response?: { data?: { message?: string; error?: string } }; message?: string }) response?: { data?: { message?: string; error?: string } };
?.response?.data?.error || message?: string;
}
)?.response?.data?.message ||
(
error as {
response?: { data?: { message?: string; error?: string } };
message?: string;
}
)?.response?.data?.error ||
(error as { message?: string })?.message || (error as { message?: string })?.message ||
"Erro ao criar usuário"; "Erro ao criar usuário";
@ -699,7 +709,9 @@ const PainelAdmin: React.FC = () => {
} }
// Limpar telefone (remover formatação) // Limpar telefone (remover formatação)
const phoneLimpo = medicoData.phone_mobile ? medicoData.phone_mobile.replace(/\D/g, "") : undefined; const phoneLimpo = medicoData.phone_mobile
? medicoData.phone_mobile.replace(/\D/g, "")
: undefined;
console.log("[PainelAdmin] Criando médico com API /create-doctor:", { console.log("[PainelAdmin] Criando médico com API /create-doctor:", {
email: medicoData.email, email: medicoData.email,
@ -841,20 +853,32 @@ const PainelAdmin: React.FC = () => {
const formatCPF = (value: string): string => { const formatCPF = (value: string): string => {
const numbers = value.replace(/\D/g, ""); const numbers = value.replace(/\D/g, "");
if (numbers.length <= 3) return numbers; if (numbers.length <= 3) return numbers;
if (numbers.length <= 6) return `${numbers.slice(0, 3)}.${numbers.slice(3)}`; if (numbers.length <= 6)
return `${numbers.slice(0, 3)}.${numbers.slice(3)}`;
if (numbers.length <= 9) if (numbers.length <= 9)
return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(6)}`; return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(
return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(6, 9)}-${numbers.slice(9, 11)}`; 6
)}`;
return `${numbers.slice(0, 3)}.${numbers.slice(3, 6)}.${numbers.slice(
6,
9
)}-${numbers.slice(9, 11)}`;
}; };
// Função para formatar telefone ((XX) XXXXX-XXXX) // Função para formatar telefone ((XX) XXXXX-XXXX)
const formatPhone = (value: string): string => { const formatPhone = (value: string): string => {
const numbers = value.replace(/\D/g, ""); const numbers = value.replace(/\D/g, "");
if (numbers.length <= 2) return numbers; if (numbers.length <= 2) return numbers;
if (numbers.length <= 7) return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`; if (numbers.length <= 7)
return `(${numbers.slice(0, 2)}) ${numbers.slice(2)}`;
if (numbers.length <= 11) if (numbers.length <= 11)
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(7)}`; return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(7, 11)}`; 7
)}`;
return `(${numbers.slice(0, 2)}) ${numbers.slice(2, 7)}-${numbers.slice(
7,
11
)}`;
}; };
// Função para obter apenas números do CPF/telefone // Função para obter apenas números do CPF/telefone
@ -1693,19 +1717,27 @@ const PainelAdmin: React.FC = () => {
> >
{availableRoles.map((role) => ( {availableRoles.map((role) => (
<option key={role} value={role}> <option key={role} value={role}>
{role === "paciente" ? "Paciente" : {role === "paciente"
role === "medico" ? "Médico" : ? "Paciente"
role === "secretaria" ? "Secretária" : : role === "medico"
role === "admin" ? "Administrador" : ? "Médico"
role === "gestor" ? "Gestor" : role} : role === "secretaria"
? "Secretária"
: role === "admin"
? "Administrador"
: role === "gestor"
? "Gestor"
: role}
</option> </option>
))} ))}
</select> </select>
</div> </div>
<div className="border-t pt-4"> <div className="border-t pt-4">
<h3 className="text-sm font-semibold mb-3">Campos Opcionais</h3> <h3 className="text-sm font-semibold mb-3">
Campos Opcionais
</h3>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<label className="block text-sm font-medium mb-1"> <label className="block text-sm font-medium mb-1">
@ -1715,7 +1747,10 @@ const PainelAdmin: React.FC = () => {
type="text" type="text"
value={formUser.phone || ""} value={formUser.phone || ""}
onChange={(e) => onChange={(e) =>
setFormUser({ ...formUser, phone: formatPhone(e.target.value) }) setFormUser({
...formUser,
phone: formatPhone(e.target.value),
})
} }
maxLength={15} maxLength={15}
className="form-input" className="form-input"
@ -1730,7 +1765,9 @@ const PainelAdmin: React.FC = () => {
<input <input
type="text" type="text"
value={userPhoneMobile} value={userPhoneMobile}
onChange={(e) => setUserPhoneMobile(formatPhone(e.target.value))} onChange={(e) =>
setUserPhoneMobile(formatPhone(e.target.value))
}
maxLength={15} maxLength={15}
className="form-input" className="form-input"
placeholder="(00) 00000-0000" placeholder="(00) 00000-0000"
@ -1894,7 +1931,10 @@ const PainelAdmin: React.FC = () => {
required required
value={formMedico.cpf} value={formMedico.cpf}
onChange={(e) => onChange={(e) =>
setFormMedico({ ...formMedico, cpf: formatCPF(e.target.value) }) setFormMedico({
...formMedico,
cpf: formatCPF(e.target.value),
})
} }
maxLength={14} maxLength={14}
className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40" className="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-purple-600 focus:border-purple-600/40"
@ -2229,5 +2269,3 @@ const PainelAdmin: React.FC = () => {
}; };
export default PainelAdmin; export default PainelAdmin;

View File

@ -101,7 +101,9 @@ const PainelMedico: React.FC = () => {
// Estados para perfil do médico // Estados para perfil do médico
const [isEditingProfile, setIsEditingProfile] = useState(false); const [isEditingProfile, setIsEditingProfile] = useState(false);
const [profileTab, setProfileTab] = useState<"personal" | "professional" | "security">("personal"); const [profileTab, setProfileTab] = useState<
"personal" | "professional" | "security"
>("personal");
const [profileData, setProfileData] = useState({ const [profileData, setProfileData] = useState({
full_name: "", full_name: "",
email: "", email: "",
@ -1020,7 +1022,7 @@ const PainelMedico: React.FC = () => {
// Carregar dados do perfil do médico // Carregar dados do perfil do médico
const loadDoctorProfile = useCallback(async () => { const loadDoctorProfile = useCallback(async () => {
if (!doctorId) return; if (!doctorId) return;
try { try {
const doctor = await doctorService.getById(doctorId); const doctor = await doctorService.getById(doctorId);
setProfileData({ setProfileData({
@ -1114,7 +1116,9 @@ const PainelMedico: React.FC = () => {
{/* Avatar Card */} {/* Avatar Card */}
<div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6"> <div className="bg-white dark:bg-slate-900 rounded-lg border border-gray-200 dark:border-slate-700 p-6">
<h2 className="text-lg font-semibold mb-4 dark:text-white">Foto de Perfil</h2> <h2 className="text-lg font-semibold mb-4 dark:text-white">
Foto de Perfil
</h2>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<AvatarUpload <AvatarUpload
userId={user?.id} userId={user?.id}
@ -1193,7 +1197,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.full_name} value={profileData.full_name}
onChange={(e) => handleProfileChange("full_name", e.target.value)} onChange={(e) =>
handleProfileChange("full_name", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1206,7 +1212,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="email" type="email"
value={profileData.email} value={profileData.email}
onChange={(e) => handleProfileChange("email", e.target.value)} onChange={(e) =>
handleProfileChange("email", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1219,7 +1227,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="tel" type="tel"
value={profileData.phone} value={profileData.phone}
onChange={(e) => handleProfileChange("phone", e.target.value)} onChange={(e) =>
handleProfileChange("phone", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1244,7 +1254,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="date" type="date"
value={profileData.birth_date} value={profileData.birth_date}
onChange={(e) => handleProfileChange("birth_date", e.target.value)} onChange={(e) =>
handleProfileChange("birth_date", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1256,7 +1268,9 @@ const PainelMedico: React.FC = () => {
</label> </label>
<select <select
value={profileData.sex} value={profileData.sex}
onChange={(e) => handleProfileChange("sex", e.target.value)} onChange={(e) =>
handleProfileChange("sex", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
> >
@ -1270,7 +1284,9 @@ const PainelMedico: React.FC = () => {
</div> </div>
<div> <div>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Endereço</h3> <h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">
Endereço
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2"> <div className="md:col-span-2">
@ -1280,7 +1296,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.street} value={profileData.street}
onChange={(e) => handleProfileChange("street", e.target.value)} onChange={(e) =>
handleProfileChange("street", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1293,7 +1311,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.number} value={profileData.number}
onChange={(e) => handleProfileChange("number", e.target.value)} onChange={(e) =>
handleProfileChange("number", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1306,7 +1326,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.complement} value={profileData.complement}
onChange={(e) => handleProfileChange("complement", e.target.value)} onChange={(e) =>
handleProfileChange("complement", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1319,7 +1341,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.neighborhood} value={profileData.neighborhood}
onChange={(e) => handleProfileChange("neighborhood", e.target.value)} onChange={(e) =>
handleProfileChange("neighborhood", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1332,7 +1356,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.city} value={profileData.city}
onChange={(e) => handleProfileChange("city", e.target.value)} onChange={(e) =>
handleProfileChange("city", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1345,7 +1371,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.state} value={profileData.state}
onChange={(e) => handleProfileChange("state", e.target.value)} onChange={(e) =>
handleProfileChange("state", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
maxLength={2} maxLength={2}
@ -1359,7 +1387,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.cep} value={profileData.cep}
onChange={(e) => handleProfileChange("cep", e.target.value)} onChange={(e) =>
handleProfileChange("cep", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1385,7 +1415,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.crm} value={profileData.crm}
onChange={(e) => handleProfileChange("crm", e.target.value)} onChange={(e) =>
handleProfileChange("crm", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1398,7 +1430,9 @@ const PainelMedico: React.FC = () => {
<input <input
type="text" type="text"
value={profileData.specialty} value={profileData.specialty}
onChange={(e) => handleProfileChange("specialty", e.target.value)} onChange={(e) =>
handleProfileChange("specialty", e.target.value)
}
disabled={!isEditingProfile} disabled={!isEditingProfile}
className="form-input" className="form-input"
/> />
@ -1468,7 +1502,9 @@ const PainelMedico: React.FC = () => {
<div className="flex flex-col lg:flex-row min-h-screen bg-gray-50 dark:bg-slate-950"> <div className="flex flex-col lg:flex-row min-h-screen bg-gray-50 dark:bg-slate-950">
{renderSidebar()} {renderSidebar()}
<main className="flex-1 overflow-y-auto"> <main className="flex-1 overflow-y-auto">
<div className="container mx-auto p-4 sm:p-6 lg:p-8">{renderContent()}</div> <div className="container mx-auto p-4 sm:p-6 lg:p-8">
{renderContent()}
</div>
</main> </main>
{/* Modals */} {/* Modals */}
@ -1595,5 +1631,3 @@ const PainelMedico: React.FC = () => {
}; };
export default PainelMedico; export default PainelMedico;

View File

@ -81,9 +81,7 @@ export default function PainelSecretaria() {
> >
<Icon className="h-3.5 w-3.5 sm:h-4 sm:w-4 flex-shrink-0" /> <Icon className="h-3.5 w-3.5 sm:h-4 sm:w-4 flex-shrink-0" />
<span className="hidden sm:inline">{tab.label}</span> <span className="hidden sm:inline">{tab.label}</span>
<span className="sm:hidden"> <span className="sm:hidden">{tab.label.split(" ")[0]}</span>
{tab.label.split(' ')[0]}
</span>
</button> </button>
); );
})} })}
@ -97,7 +95,10 @@ export default function PainelSecretaria() {
<SecretaryPatientList <SecretaryPatientList
onOpenAppointment={(patientId: string) => { onOpenAppointment={(patientId: string) => {
// store selected patient for appointment and switch to consultas tab // store selected patient for appointment and switch to consultas tab
sessionStorage.setItem("selectedPatientForAppointment", patientId); sessionStorage.setItem(
"selectedPatientForAppointment",
patientId
);
setActiveTab("consultas"); setActiveTab("consultas");
}} }}
/> />
@ -118,5 +119,3 @@ export default function PainelSecretaria() {
</div> </div>
); );
} }

View File

@ -220,7 +220,9 @@ export default function PerfilMedico() {
{/* Avatar Card */} {/* Avatar Card */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6"> <div className="bg-white rounded-lg shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Foto de Perfil</h2> <h2 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">
Foto de Perfil
</h2>
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6"> <div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6">
<AvatarUpload <AvatarUpload
userId={user?.id} userId={user?.id}
@ -235,7 +237,9 @@ export default function PerfilMedico() {
<p className="font-medium text-gray-900 text-sm sm:text-base truncate"> <p className="font-medium text-gray-900 text-sm sm:text-base truncate">
{formData.full_name} {formData.full_name}
</p> </p>
<p className="text-gray-500 text-xs sm:text-sm truncate">{formData.specialty}</p> <p className="text-gray-500 text-xs sm:text-sm truncate">
{formData.specialty}
</p>
<p className="text-xs sm:text-sm text-gray-500 truncate"> <p className="text-xs sm:text-sm text-gray-500 truncate">
CRM: {formData.crm} - {formData.crm_state} CRM: {formData.crm} - {formData.crm_state}
</p> </p>
@ -564,5 +568,3 @@ export default function PerfilMedico() {
</div> </div>
); );
} }

View File

@ -267,7 +267,9 @@ export default function PerfilPaciente() {
{/* Avatar Card */} {/* Avatar Card */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6"> <div className="bg-white rounded-lg shadow p-4 sm:p-6">
<h2 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">Foto de Perfil</h2> <h2 className="text-base sm:text-lg font-semibold mb-3 sm:mb-4">
Foto de Perfil
</h2>
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6"> <div className="flex flex-col sm:flex-row items-center sm:items-start gap-4 sm:gap-6">
<AvatarUpload <AvatarUpload
userId={user?.id} userId={user?.id}
@ -684,5 +686,3 @@ export default function PerfilPaciente() {
</div> </div>
); );
} }

View File

@ -175,18 +175,28 @@ class ApiClient {
url: string, url: string,
config?: AxiosRequestConfig config?: AxiosRequestConfig
): Promise<AxiosResponse<T>> { ): Promise<AxiosResponse<T>> {
console.log("[ApiClient] GET Request:", url, "Params:", JSON.stringify(config?.params)); console.log(
"[ApiClient] GET Request:",
url,
"Params:",
JSON.stringify(config?.params)
);
const response = await this.client.get<T>(url, config); const response = await this.client.get<T>(url, config);
console.log("[ApiClient] GET Response:", { console.log("[ApiClient] GET Response:", {
status: response.status, status: response.status,
dataType: typeof response.data, dataType: typeof response.data,
isArray: Array.isArray(response.data), isArray: Array.isArray(response.data),
dataLength: Array.isArray(response.data) ? response.data.length : 'not array', dataLength: Array.isArray(response.data)
? response.data.length
: "not array",
}); });
console.log("[ApiClient] Response Data:", JSON.stringify(response.data, null, 2)); console.log(
"[ApiClient] Response Data:",
JSON.stringify(response.data, null, 2)
);
return response; return response;
} }
@ -201,7 +211,7 @@ class ApiClient {
data, data,
config, config,
}); });
try { try {
const response = await this.client.post<T>(url, data, config); const response = await this.client.post<T>(url, data, config);
console.log("[ApiClient] POST Response:", { console.log("[ApiClient] POST Response:", {
@ -253,9 +263,18 @@ class ApiClient {
data, data,
config, config,
}); });
try { try {
const response = await this.client.patch<T>(url, data, config); // Adicionar header Prefer para Supabase retornar os dados atualizados
const configWithPrefer = {
...config,
headers: {
...config?.headers,
Prefer: "return=representation",
},
};
const response = await this.client.patch<T>(url, data, configWithPrefer);
console.log("[ApiClient] PATCH Response:", { console.log("[ApiClient] PATCH Response:", {
status: response.status, status: response.status,
data: response.data, data: response.data,

View File

@ -24,27 +24,36 @@ class AppointmentService {
): Promise<GetAvailableSlotsResponse> { ): Promise<GetAvailableSlotsResponse> {
try { try {
console.log("[AppointmentService] Chamando get-available-slots:", data); console.log("[AppointmentService] Chamando get-available-slots:", data);
// Usa callFunction para chamar a Edge Function // Usa callFunction para chamar a Edge Function
const response = await apiClient.callFunction<GetAvailableSlotsResponse>( const response = await apiClient.callFunction<GetAvailableSlotsResponse>(
"get-available-slots", "get-available-slots",
data data
); );
console.log("[AppointmentService] Resposta get-available-slots:", response.data); console.log(
"[AppointmentService] Resposta get-available-slots:",
response.data
);
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
console.error("[AppointmentService] ❌ Erro ao buscar slots:"); console.error("[AppointmentService] ❌ Erro ao buscar slots:");
console.error("[AppointmentService] Status:", error?.response?.status); console.error("[AppointmentService] Status:", error?.response?.status);
console.error("[AppointmentService] Response Data:", JSON.stringify(error?.response?.data, null, 2)); console.error(
"[AppointmentService] Response Data:",
JSON.stringify(error?.response?.data, null, 2)
);
console.error("[AppointmentService] Message:", error?.message); console.error("[AppointmentService] Message:", error?.message);
console.error("[AppointmentService] Input enviado:", JSON.stringify(data, null, 2)); console.error(
"[AppointmentService] Input enviado:",
JSON.stringify(data, null, 2)
);
throw new Error( throw new Error(
error?.response?.data?.message || error?.response?.data?.message ||
error?.message || error?.message ||
"Erro ao buscar horários disponíveis" "Erro ao buscar horários disponíveis"
); );
} }
} }

View File

@ -46,12 +46,12 @@ class AvailabilityService {
const response = await apiClient.get<any[]>(this.basePath, { const response = await apiClient.get<any[]>(this.basePath, {
params, params,
}); });
console.log("[AvailabilityService] Resposta:", { console.log("[AvailabilityService] Resposta:", {
count: response.data?.length || 0, count: response.data?.length || 0,
isArray: Array.isArray(response.data), isArray: Array.isArray(response.data),
}); });
// Converter weekday de string para número (compatibilidade com banco antigo) // Converter weekday de string para número (compatibilidade com banco antigo)
const convertedData: DoctorAvailability[] = Array.isArray(response.data) const convertedData: DoctorAvailability[] = Array.isArray(response.data)
? response.data.map((item) => { ? response.data.map((item) => {
@ -64,21 +64,29 @@ class AvailabilityService {
friday: 5, friday: 5,
saturday: 6, saturday: 6,
}; };
return { return {
...item, ...item,
weekday: typeof item.weekday === 'string' weekday:
? weekdayMap[item.weekday.toLowerCase()] typeof item.weekday === "string"
: item.weekday, ? weekdayMap[item.weekday.toLowerCase()]
: item.weekday,
}; };
}) })
: []; : [];
if (convertedData.length > 0) { if (convertedData.length > 0) {
console.log("[AvailabilityService] ✅ Convertido:", convertedData.length, "registros"); console.log(
console.log("[AvailabilityService] Primeiro item convertido:", JSON.stringify(convertedData[0], null, 2)); "[AvailabilityService] ✅ Convertido:",
convertedData.length,
"registros"
);
console.log(
"[AvailabilityService] Primeiro item convertido:",
JSON.stringify(convertedData[0], null, 2)
);
} }
return convertedData; return convertedData;
} }
@ -100,9 +108,9 @@ class AvailabilityService {
}, },
} }
); );
console.log("[AvailabilityService] Resposta da criação:", response.data); console.log("[AvailabilityService] Resposta da criação:", response.data);
return Array.isArray(response.data) ? response.data[0] : 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; return Array.isArray(response.data) ? response.data[0] : response.data;
} }

View File

@ -61,14 +61,41 @@ class ReportService {
* Nota: order_number não pode ser modificado * Nota: order_number não pode ser modificado
*/ */
async update(id: string, data: UpdateReportInput): Promise<Report> { async update(id: string, data: UpdateReportInput): Promise<Report> {
const response = await apiClient.patch<Report[]>( console.log("[ReportService] update() - id:", id, "data:", data);
const response = await apiClient.patch<Report | Report[]>(
`${this.basePath}?id=eq.${id}`, `${this.basePath}?id=eq.${id}`,
data data
); );
if (response.data && response.data.length > 0) {
return response.data[0]; console.log("[ReportService] update() - response status:", response.status);
console.log("[ReportService] update() - response.data:", response.data);
console.log(
"[ReportService] update() - response type:",
typeof response.data,
"isArray:",
Array.isArray(response.data)
);
// Supabase com Prefer: return=representation pode retornar array ou objeto
if (Array.isArray(response.data)) {
if (response.data.length > 0) {
return response.data[0];
}
// Array vazio - buscar o relatório atualizado
console.warn(
"[ReportService] update() - Array vazio, buscando relatório..."
);
return await this.getById(id);
} else if (response.data) {
return response.data as Report;
} }
throw new Error("Relatório não encontrado");
// Última tentativa - buscar o relatório
console.warn(
"[ReportService] update() - Resposta vazia, buscando relatório..."
);
return await this.getById(id);
} }
} }

View File

@ -5,12 +5,12 @@ export default {
theme: { theme: {
extend: { extend: {
fontSize: { fontSize: {
'xs': ['0.75rem', { lineHeight: '1rem' }], xs: ["0.75rem", { lineHeight: "1rem" }],
'sm': ['0.875rem', { lineHeight: '1.25rem' }], sm: ["0.875rem", { lineHeight: "1.25rem" }],
'base': ['1rem', { lineHeight: '1.5rem' }], base: ["1rem", { lineHeight: "1.5rem" }],
'lg': ['1.125rem', { lineHeight: '1.75rem' }], lg: ["1.125rem", { lineHeight: "1.75rem" }],
'xl': ['1.25rem', { lineHeight: '1.75rem' }], xl: ["1.25rem", { lineHeight: "1.75rem" }],
'2xl': ['1.5rem', { lineHeight: '2rem' }], "2xl": ["1.5rem", { lineHeight: "2rem" }],
}, },
colors: { colors: {
border: "hsl(var(--border))", border: "hsl(var(--border))",