Merge pull request 'feat: refatora UI e corrige bugs nas páginas de agendamento' (#12) from RiseUP/riseup-squad21:refatoracao-ui into refat-ui
Reviewed-on: StsDanilo/riseup-squad21#12
This commit is contained in:
commit
ac6b9f9f97
@ -1,16 +1,21 @@
|
|||||||
|
// Caminho: app/context/AppointmentsContext.tsx (Completo e Corrigido)
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
|
||||||
import { agendamentosApi, Appointment } from '@/services/agendamentosApi';
|
import { agendamentosApi, Appointment, CreateAppointmentData } from '@/services/agendamentosApi';
|
||||||
|
import { usuariosApi, User } from '@/services/usuariosApi';
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
// As definições de componentes de UI foram REMOVIDAS deste arquivo.
|
||||||
|
// Elas pertencem aos arquivos que as utilizam, como `dashboard/page.tsx`.
|
||||||
|
|
||||||
export interface AppointmentsContextType {
|
export interface AppointmentsContextType {
|
||||||
appointments: Appointment[];
|
appointments: Appointment[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
fetchAppointments: () => Promise<void>;
|
fetchAppointments: () => Promise<void>;
|
||||||
addAppointment: (appointmentData: Omit<Appointment, 'id' | 'status'>) => Promise<void>;
|
addAppointment: (appointmentData: CreateAppointmentData) => Promise<void>;
|
||||||
updateAppointment: (appointmentId: string, updatedData: Partial<Omit<Appointment, 'id'>>) => Promise<void>;
|
updateAppointment: (appointmentId: string, updatedData: Partial<Omit<Appointment, 'id'>>) => Promise<void>;
|
||||||
deleteAppointment: (appointmentId: string) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppointmentsContext = createContext<AppointmentsContextType | undefined>(undefined);
|
const AppointmentsContext = createContext<AppointmentsContextType | undefined>(undefined);
|
||||||
@ -24,8 +29,13 @@ export function AppointmentsProvider({ children }: { children: ReactNode }) {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const data = await agendamentosApi.list();
|
const user = await usuariosApi.getCurrentUser();
|
||||||
setAppointments(data || []);
|
if (user?.id) {
|
||||||
|
const data = await agendamentosApi.listByPatient(user.id);
|
||||||
|
setAppointments(data || []);
|
||||||
|
} else {
|
||||||
|
setAppointments([]);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erro ao buscar agendamentos:", err);
|
console.error("Erro ao buscar agendamentos:", err);
|
||||||
setError("Não foi possível carregar os agendamentos.");
|
setError("Não foi possível carregar os agendamentos.");
|
||||||
@ -39,33 +49,24 @@ export function AppointmentsProvider({ children }: { children: ReactNode }) {
|
|||||||
fetchAppointments();
|
fetchAppointments();
|
||||||
}, [fetchAppointments]);
|
}, [fetchAppointments]);
|
||||||
|
|
||||||
const addAppointment = async (appointmentData: Omit<Appointment, 'id' | 'status'>) => {
|
const addAppointment = async (appointmentData: CreateAppointmentData) => {
|
||||||
try {
|
try {
|
||||||
await agendamentosApi.create(appointmentData);
|
await agendamentosApi.create(appointmentData);
|
||||||
await fetchAppointments(); // Recarrega a lista para incluir o novo agendamento
|
await fetchAppointments();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erro ao adicionar agendamento:", err);
|
console.error("Erro ao adicionar agendamento:", err);
|
||||||
setError("Falha ao criar o novo agendamento. Tente novamente.");
|
setError("Falha ao criar o novo agendamento. Tente novamente.");
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAppointment = async (appointmentId: string, updatedData: Partial<Omit<Appointment, 'id'>>) => {
|
const updateAppointment = async (appointmentId: string, updatedData: Partial<Omit<Appointment, 'id'>>) => {
|
||||||
try {
|
try {
|
||||||
await agendamentosApi.update(appointmentId, updatedData);
|
toast.warning("Funcionalidade indisponível.", { description: "A API não suporta a atualização de agendamentos." });
|
||||||
await fetchAppointments(); // Recarrega a lista para refletir as alterações
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erro ao atualizar agendamento:", err);
|
console.error("Erro ao tentar atualizar agendamento:", err);
|
||||||
setError("Falha ao atualizar o agendamento. Tente novamente.");
|
setError("Falha ao atualizar o agendamento. Tente novamente.");
|
||||||
}
|
throw err;
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAppointment = async (appointmentId: string) => {
|
|
||||||
try {
|
|
||||||
await agendamentosApi.delete(appointmentId);
|
|
||||||
await fetchAppointments(); // Recarrega a lista para remover o item excluído
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Erro ao excluir agendamento:", err);
|
|
||||||
setError("Falha ao excluir o agendamento. Tente novamente.");
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -76,7 +77,6 @@ export function AppointmentsProvider({ children }: { children: ReactNode }) {
|
|||||||
fetchAppointments,
|
fetchAppointments,
|
||||||
addAppointment,
|
addAppointment,
|
||||||
updateAppointment,
|
updateAppointment,
|
||||||
deleteAppointment,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
206
app/dev/api-check/page.tsx
Normal file
206
app/dev/api-check/page.tsx
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
// app/dev/api-check/page.tsx (V2 - Com Mocks e Seções Colapsáveis)
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
authService,
|
||||||
|
userService,
|
||||||
|
patientService,
|
||||||
|
doctorService,
|
||||||
|
scheduleService,
|
||||||
|
reportService,
|
||||||
|
} from '@/services/api/apiService';
|
||||||
|
import * as TestData from '@/services/api/apiTestData';
|
||||||
|
import {
|
||||||
|
LoginResponse, SendMagicLinkResponse, LogoutResponse, GetCurrentUserResponse,
|
||||||
|
RequestPasswordResetResponse, HardDeleteUserResponse, RegisterPatientResponse,
|
||||||
|
ListResponse, Patient, GetAvailableSlotsResponse
|
||||||
|
} from '@/services/api/types';
|
||||||
|
|
||||||
|
type ApiResponse = { status: number | 'network_error'; data?: any; error?: Error };
|
||||||
|
|
||||||
|
const getStyleForResponse = (response: ApiResponse | null): React.CSSProperties => {
|
||||||
|
if (!response) return {};
|
||||||
|
const baseStyle: React.CSSProperties = { padding: '10px', border: '1px solid', borderRadius: '4px', whiteSpace: 'pre-wrap', wordBreak: 'break-all', marginTop: '10px' };
|
||||||
|
if (response.status === 'network_error') return { ...baseStyle, backgroundColor: 'lightgoldenrodyellow', borderColor: 'goldenrod' };
|
||||||
|
if (response.status >= 200 && response.status < 300) return { ...baseStyle, backgroundColor: 'lightgreen', borderColor: 'green' };
|
||||||
|
if (response.status >= 400) return { ...baseStyle, backgroundColor: 'lightcoral', borderColor: 'darkred' };
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApiVerificationPage: React.FC = () => {
|
||||||
|
// --- ESTADOS ---
|
||||||
|
const [loginData, setLoginData] = useState(TestData.loginTestData.success);
|
||||||
|
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
|
||||||
|
const [magicLinkEmail, setMagicLinkEmail] = useState(TestData.magicLinkTestData.success);
|
||||||
|
const [magicLinkResponse, setMagicLinkResponse] = useState<SendMagicLinkResponse | null>(null);
|
||||||
|
const [logoutResponse, setLogoutResponse] = useState<LogoutResponse | null>(null);
|
||||||
|
const [currentUserResponse, setCurrentUserResponse] = useState<GetCurrentUserResponse | null>(null);
|
||||||
|
const [resetPassData, setResetPassData] = useState(TestData.resetPassTestData.success);
|
||||||
|
const [resetPassResponse, setResetPassResponse] = useState<RequestPasswordResetResponse | null>(null);
|
||||||
|
const [deleteUserData, setDeleteUserData] = useState(TestData.deleteUserTestData.success);
|
||||||
|
const [deleteUserResponse, setDeleteUserResponse] = useState<HardDeleteUserResponse | null>(null);
|
||||||
|
const [registerPatientData, setRegisterPatientData] = useState(TestData.registerPatientTestData.success);
|
||||||
|
const [registerPatientResponse, setRegisterPatientResponse] = useState<RegisterPatientResponse | null>(null);
|
||||||
|
const [listPatientsFilter, setListPatientsFilter] = useState(TestData.listPatientsTestData.success);
|
||||||
|
const [listPatientsResponse, setListPatientsResponse] = useState<ListResponse<Patient> | null>(null);
|
||||||
|
const [slotsData, setSlotsData] = useState(TestData.slotsTestData.success);
|
||||||
|
const [slotsResponse, setSlotsResponse] = useState<GetAvailableSlotsResponse | null>(null);
|
||||||
|
|
||||||
|
// --- HANDLERS ---
|
||||||
|
const handleApiCall = async (apiFunction: (...args: any[]) => Promise<any>, payload: any, setResponse: React.Dispatch<React.SetStateAction<any>>) => {
|
||||||
|
setResponse(null);
|
||||||
|
const response = await apiFunction(payload);
|
||||||
|
setResponse(response);
|
||||||
|
};
|
||||||
|
const handleApiCallNoPayload = async (apiFunction: () => Promise<any>, setResponse: React.Dispatch<React.SetStateAction<any>>) => {
|
||||||
|
setResponse(null);
|
||||||
|
const response = await apiFunction();
|
||||||
|
setResponse(response);
|
||||||
|
};
|
||||||
|
const handleRequestPasswordReset = async () => {
|
||||||
|
setResetPassResponse(null);
|
||||||
|
const response = await userService.requestPasswordReset(resetPassData.email, resetPassData.redirectUrl || undefined);
|
||||||
|
setResetPassResponse(response);
|
||||||
|
};
|
||||||
|
const handleGetAvailableSlots = async () => {
|
||||||
|
setSlotsResponse(null);
|
||||||
|
const response = await scheduleService.getAvailableSlots(slotsData.doctorId, slotsData.date);
|
||||||
|
setSlotsResponse(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ fontFamily: 'sans-serif', padding: '20px', maxWidth: '1000px', margin: 'auto' }}>
|
||||||
|
<h1>Painel de Verificação da API</h1>
|
||||||
|
<p>Use este painel para executar cada função do `apiService` e verificar o objeto de resposta completo.</p>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><h2>Autenticação</h2></summary>
|
||||||
|
<div className="test-block">
|
||||||
|
<details>
|
||||||
|
<summary><h3>authService.login</h3></summary>
|
||||||
|
<div className="controls">
|
||||||
|
<button onClick={() => setLoginData(TestData.loginTestData.success)}>Carregar Sucesso</button>
|
||||||
|
<button onClick={() => setLoginData(TestData.loginTestData.error)}>Carregar Erro</button>
|
||||||
|
</div>
|
||||||
|
<pre>{JSON.stringify(loginData, null, 2)}</pre>
|
||||||
|
<button onClick={() => handleApiCall(authService.login, loginData, setLoginResponse)}>Executar</button>
|
||||||
|
{loginResponse && <pre style={getStyleForResponse(loginResponse)}>{JSON.stringify(loginResponse, null, 2)}</pre>}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
<div className="test-block">
|
||||||
|
<details>
|
||||||
|
<summary><h3>authService.sendMagicLink</h3></summary>
|
||||||
|
<div className="controls">
|
||||||
|
<button onClick={() => setMagicLinkEmail(TestData.magicLinkTestData.success)}>Carregar Sucesso</button>
|
||||||
|
<button onClick={() => setMagicLinkEmail(TestData.magicLinkTestData.error)}>Carregar Erro</button>
|
||||||
|
</div>
|
||||||
|
<pre>{JSON.stringify(magicLinkEmail, null, 2)}</pre>
|
||||||
|
<button onClick={() => handleApiCall(authService.sendMagicLink, magicLinkEmail.email, setMagicLinkResponse)}>Executar</button>
|
||||||
|
{magicLinkResponse && <pre style={getStyleForResponse(magicLinkResponse)}>{JSON.stringify(magicLinkResponse, null, 2)}</pre>}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
<div className="test-block">
|
||||||
|
<details>
|
||||||
|
<summary><h3>authService.logout</h3></summary>
|
||||||
|
<button onClick={() => handleApiCallNoPayload(authService.logout, setLogoutResponse)}>Executar</button>
|
||||||
|
{logoutResponse && <pre style={getStyleForResponse(logoutResponse)}>{JSON.stringify(logoutResponse, null, 2)}</pre>}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
<div className="test-block">
|
||||||
|
<details>
|
||||||
|
<summary><h3>authService.getCurrentUser</h3></summary>
|
||||||
|
<button onClick={() => handleApiCallNoPayload(authService.getCurrentUser, setCurrentUserResponse)}>Executar</button>
|
||||||
|
{currentUserResponse && <pre style={getStyleForResponse(currentUserResponse)}>{JSON.stringify(currentUserResponse, null, 2)}</pre>}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><h2>Usuários</h2></summary>
|
||||||
|
<div className="test-block">
|
||||||
|
<details>
|
||||||
|
<summary><h3>userService.requestPasswordReset</h3></summary>
|
||||||
|
<div className="controls">
|
||||||
|
<button onClick={() => setResetPassData(TestData.resetPassTestData.success)}>Carregar Sucesso</button>
|
||||||
|
<button onClick={() => setResetPassData(TestData.resetPassTestData.error)}>Carregar Erro</button>
|
||||||
|
</div>
|
||||||
|
<pre>{JSON.stringify(resetPassData, null, 2)}</pre>
|
||||||
|
<button onClick={handleRequestPasswordReset}>Executar</button>
|
||||||
|
{resetPassResponse && <pre style={getStyleForResponse(resetPassResponse)}>{JSON.stringify(resetPassResponse, null, 2)}</pre>}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
<div className="test-block">
|
||||||
|
<details>
|
||||||
|
<summary><h3>userService.hardDeleteUser_DANGEROUS</h3></summary>
|
||||||
|
<div className="controls">
|
||||||
|
<button onClick={() => setDeleteUserData(TestData.deleteUserTestData.success)}>Carregar Sucesso</button>
|
||||||
|
<button onClick={() => setDeleteUserData(TestData.deleteUserTestData.error)}>Carregar Erro</button>
|
||||||
|
</div>
|
||||||
|
<pre>{JSON.stringify(deleteUserData, null, 2)}</pre>
|
||||||
|
<button onClick={() => handleApiCall(userService.hardDeleteUser_DANGEROUS, deleteUserData.userId, setDeleteUserResponse)}>Executar</button>
|
||||||
|
{deleteUserResponse && <pre style={getStyleForResponse(deleteUserResponse)}>{JSON.stringify(deleteUserResponse, null, 2)}</pre>}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><h2>Pacientes</h2></summary>
|
||||||
|
<div className="test-block">
|
||||||
|
<details>
|
||||||
|
<summary><h3>patientService.registerPatient</h3></summary>
|
||||||
|
<div className="controls">
|
||||||
|
<button onClick={() => setRegisterPatientData(TestData.registerPatientTestData.success)}>Carregar Sucesso</button>
|
||||||
|
<button onClick={() => setRegisterPatientData(TestData.registerPatientTestData.errorValidation)}>Carregar Erro (Validação)</button>
|
||||||
|
<button onClick={() => setRegisterPatientData(TestData.registerPatientTestData.errorConflict)}>Carregar Erro (Conflito)</button>
|
||||||
|
</div>
|
||||||
|
<pre>{JSON.stringify(registerPatientData, null, 2)}</pre>
|
||||||
|
<button onClick={() => handleApiCall(patientService.registerPatient, registerPatientData, setRegisterPatientResponse)}>Executar</button>
|
||||||
|
{registerPatientResponse && <pre style={getStyleForResponse(registerPatientResponse)}>{JSON.stringify(registerPatientResponse, null, 2)}</pre>}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
<div className="test-block">
|
||||||
|
<details>
|
||||||
|
<summary><h3>patientService.list</h3></summary>
|
||||||
|
<div className="controls">
|
||||||
|
<button onClick={() => setListPatientsFilter(TestData.listPatientsTestData.success)}>Carregar com Filtro</button>
|
||||||
|
<button onClick={() => setListPatientsFilter(TestData.listPatientsTestData.noFilter)}>Carregar Sem Filtro</button>
|
||||||
|
</div>
|
||||||
|
<pre>{JSON.stringify(listPatientsFilter, null, 2)}</pre>
|
||||||
|
<button onClick={() => handleApiCall(patientService.list, listPatientsFilter, setListPatientsResponse)}>Executar</button>
|
||||||
|
{listPatientsResponse && <pre style={getStyleForResponse(listPatientsResponse)}>{JSON.stringify(listPatientsResponse, null, 2)}</pre>}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary><h2>Agendamentos</h2></summary>
|
||||||
|
<div className="test-block">
|
||||||
|
<details>
|
||||||
|
<summary><h3>scheduleService.getAvailableSlots</h3></summary>
|
||||||
|
<div className="controls">
|
||||||
|
<button onClick={() => setSlotsData(TestData.slotsTestData.success)}>Carregar Sucesso</button>
|
||||||
|
<button onClick={() => setSlotsData(TestData.slotsTestData.error)}>Carregar Erro</button>
|
||||||
|
</div>
|
||||||
|
<pre>{JSON.stringify(slotsData, null, 2)}</pre>
|
||||||
|
<button onClick={handleGetAvailableSlots}>Executar</button>
|
||||||
|
{slotsResponse && <pre style={getStyleForResponse(slotsResponse)}>{JSON.stringify(slotsResponse, null, 2)}</pre>}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
h2 { margin-top: 20px; border-bottom: 2px solid #eee; padding-bottom: 5px; }
|
||||||
|
summary { cursor: pointer; font-size: 1.2em; font-weight: bold; }
|
||||||
|
details { margin-bottom: 10px; }
|
||||||
|
.test-block { border: 1px solid #ccc; padding: 15px; margin-top: 10px; border-radius: 5px; }
|
||||||
|
.controls button { margin-right: 10px; }
|
||||||
|
button { margin-top: 10px; margin-bottom: 10px; padding: 8px 12px; cursor: pointer; border-radius: 4px; border: 1px solid #666; }
|
||||||
|
pre { margin-top: 10px; }
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApiVerificationPage;
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
// app/page.tsx
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
@ -18,7 +20,7 @@ export default function InicialPage() {
|
|||||||
<h1 className="text-2xl font-bold text-primary">MediConnect</h1>
|
<h1 className="text-2xl font-bold text-primary">MediConnect</h1>
|
||||||
<nav className="flex space-x-6 text-muted-foreground font-medium">
|
<nav className="flex space-x-6 text-muted-foreground font-medium">
|
||||||
<a href="#home" className="hover:text-blue-600">Home</a>
|
<a href="#home" className="hover:text-blue-600">Home</a>
|
||||||
<a href="#about" className="hover:text-primary">Sobre</a>
|
<Link href="/dev/api-check" className="hover:text-primary">Sobre (API Test)</Link>
|
||||||
<a href="#departments" className="hover:text-primary">Departamentos</a>
|
<a href="#departments" className="hover:text-primary">Departamentos</a>
|
||||||
<a href="#doctors" className="hover:text-primary">Médicos</a>
|
<a href="#doctors" className="hover:text-primary">Médicos</a>
|
||||||
<a href="#contact" className="hover:text-primary">Contato</a>
|
<a href="#contact" className="hover:text-primary">Contato</a>
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
|
// Caminho: app/(patient)/appointments/page.tsx (Corrigido e Alinhado com a API Real)
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import type React from "react";
|
||||||
import PatientLayout from "@/components/patient-layout";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@ -10,334 +11,245 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Calendar, Clock, MapPin, Phone, User, X, CalendarDays } from "lucide-react";
|
import { Calendar, Clock, MapPin, Phone, X, CalendarDays } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { appointmentsService } from "@/services/agendamentosApi";
|
import { usuariosApi, User } from "@/services/usuariosApi";
|
||||||
import { patientsService } from "@/services/pacientesApi";
|
import { agendamentosApi, Appointment } from "@/services/agendamentosApi";
|
||||||
import { doctorsService } from "@/services/medicosApi";
|
|
||||||
|
|
||||||
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
|
// --- FUNÇÃO AUXILIAR ---
|
||||||
|
const isAppointmentInPast = (scheduledAt: string): boolean => {
|
||||||
|
const now = new Date();
|
||||||
|
const appointmentDate = new Date(scheduledAt);
|
||||||
|
now.setHours(0, 0, 0, 0);
|
||||||
|
appointmentDate.setHours(0, 0, 0, 0);
|
||||||
|
return appointmentDate < now;
|
||||||
|
};
|
||||||
|
|
||||||
// Simulação do paciente logado
|
// --- Componente Reutilizável para o Card de Agendamento ---
|
||||||
const LOGGED_PATIENT_ID = "P001";
|
const AppointmentCard: React.FC<{
|
||||||
|
appointment: Appointment;
|
||||||
|
onReschedule: (appt: Appointment) => void;
|
||||||
|
onCancel: (appt: Appointment) => void;
|
||||||
|
}> = ({ appointment, onReschedule, onCancel }) => {
|
||||||
|
|
||||||
|
const getStatusBadge = (status: string): React.ReactNode => {
|
||||||
|
switch (status) {
|
||||||
|
case "requested": return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80">Solicitada</Badge>;
|
||||||
|
case "confirmed": return <Badge className="bg-primary text-primary-foreground hover:bg-primary/90">Confirmada</Badge>;
|
||||||
|
case "completed": return <Badge variant="secondary">Realizada</Badge>;
|
||||||
|
case "cancelled": return <Badge variant="destructive">Cancelada</Badge>;
|
||||||
|
default: return <Badge variant="outline">{status}</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMock = appointment.id.startsWith("mock-");
|
||||||
|
const isPast = isAppointmentInPast(appointment.scheduled_at);
|
||||||
|
const canBeModified = !isPast && appointment.status !== "cancelled" && appointment.status !== "completed";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={isMock ? "border-dashed bg-muted/30" : ""}>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg">
|
||||||
|
{appointment.doctors?.full_name || "Médico não encontrado"}
|
||||||
|
{isMock && <span className="ml-2 text-xs font-normal text-muted-foreground">(Exemplo)</span>}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>{appointment.doctors?.specialty || "Especialidade não informada"}</CardDescription>
|
||||||
|
</div>
|
||||||
|
{getStatusBadge(appointment.status)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid md:grid-cols-2 gap-3 text-sm text-muted-foreground">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center"><Calendar className="mr-2 h-4 w-4" /> {new Date(appointment.scheduled_at).toLocaleDateString("pt-BR", { timeZone: "UTC" })}</div>
|
||||||
|
<div className="flex items-center"><Clock className="mr-2 h-4 w-4" /> {new Date(appointment.scheduled_at).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", timeZone: "UTC" })}</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center"><MapPin className="mr-2 h-4 w-4" /> {appointment.appointment_type === 'telemedicina' ? 'Link da videochamada' : 'Clínica Central - Sala 101'}</div>
|
||||||
|
<div className="flex items-center"><Phone className="mr-2 h-4 w-4" /> (11) 99999-9999</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{canBeModified && (
|
||||||
|
<div className="flex gap-2 mt-4 pt-4 border-t">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => onReschedule(appointment)}><CalendarDays className="mr-2 h-4 w-4" /> Reagendar</Button>
|
||||||
|
<Button variant="outline" size="sm" className="text-destructive hover:text-destructive hover:bg-destructive/10" onClick={() => onCancel(appointment)}><X className="mr-2 h-4 w-4" /> Cancelar</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Componente Principal da Página ---
|
||||||
export default function PatientAppointments() {
|
export default function PatientAppointments() {
|
||||||
const [appointments, setAppointments] = useState<any[]>([]);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [selectedAppointment, setSelectedAppointment] = useState<any>(null);
|
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | null>(null);
|
||||||
|
const [availableSlots, setAvailableSlots] = useState<string[]>([]);
|
||||||
|
|
||||||
// Modais
|
const [isRescheduleModalOpen, setRescheduleModalOpen] = useState(false);
|
||||||
const [rescheduleModal, setRescheduleModal] = useState(false);
|
const [isCancelModalOpen, setCancelModalOpen] = useState(false);
|
||||||
const [cancelModal, setCancelModal] = useState(false);
|
|
||||||
|
|
||||||
// Formulário de reagendamento/cancelamento
|
|
||||||
const [rescheduleData, setRescheduleData] = useState({ date: "", time: "", reason: "" });
|
const [rescheduleData, setRescheduleData] = useState({ date: "", time: "", reason: "" });
|
||||||
const [cancelReason, setCancelReason] = useState("");
|
const [cancelReason, setCancelReason] = useState("");
|
||||||
|
|
||||||
const timeSlots = [
|
const fetchData = useCallback(async () => {
|
||||||
"08:00",
|
if (!user?.id) {
|
||||||
"08:30",
|
setIsLoading(false);
|
||||||
"09:00",
|
return;
|
||||||
"09:30",
|
}
|
||||||
"10:00",
|
|
||||||
"10:30",
|
|
||||||
"11:00",
|
|
||||||
"11:30",
|
|
||||||
"14:00",
|
|
||||||
"14:30",
|
|
||||||
"15:00",
|
|
||||||
"15:30",
|
|
||||||
"16:00",
|
|
||||||
"16:30",
|
|
||||||
"17:00",
|
|
||||||
"17:30",
|
|
||||||
];
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const [appointmentList, patientList, doctorList] = await Promise.all([
|
let patientAppointments = await agendamentosApi.listByPatient(user.id);
|
||||||
appointmentsService.list(),
|
|
||||||
patientsService.list(),
|
|
||||||
doctorsService.list(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const doctorMap = new Map(doctorList.map((d: any) => [d.id, d]));
|
|
||||||
const patientMap = new Map(patientList.map((p: any) => [p.id, p]));
|
|
||||||
|
|
||||||
// Filtra apenas as consultas do paciente logado
|
|
||||||
const patientAppointments = appointmentList
|
|
||||||
.filter((apt: any) => apt.patient_id === LOGGED_PATIENT_ID)
|
|
||||||
.map((apt: any) => ({
|
|
||||||
...apt,
|
|
||||||
doctor: doctorMap.get(apt.doctor_id) || { full_name: "Médico não encontrado", specialty: "N/A" },
|
|
||||||
patient: patientMap.get(apt.patient_id) || { full_name: "Paciente não encontrado" },
|
|
||||||
}));
|
|
||||||
|
|
||||||
|
if (patientAppointments.length === 0) {
|
||||||
|
console.warn("Nenhum agendamento encontrado na API real. Buscando do mock...");
|
||||||
|
toast.info("Usando dados de exemplo para a lista de consultas.");
|
||||||
|
patientAppointments = await agendamentosApi.getMockAppointments();
|
||||||
|
}
|
||||||
|
|
||||||
setAppointments(patientAppointments);
|
setAppointments(patientAppointments);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar consultas:", error);
|
console.error("Erro ao carregar consultas:", error);
|
||||||
toast.error("Não foi possível carregar suas consultas.");
|
toast.error("Não foi possível carregar suas consultas. Tentando usar dados de exemplo.");
|
||||||
|
try {
|
||||||
|
const mockAppointments = await agendamentosApi.getMockAppointments();
|
||||||
|
setAppointments(mockAppointments);
|
||||||
|
} catch (mockError) {
|
||||||
|
console.error("Falha ao buscar dados do mock:", mockError);
|
||||||
|
setAppointments([]);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [user?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
const loadInitialData = async () => {
|
||||||
}, []);
|
try {
|
||||||
|
const currentUser = await usuariosApi.getCurrentUser();
|
||||||
|
setUser(currentUser);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Usuário não autenticado:", error);
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadInitialData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
const getStatusBadge = (status: string) => {
|
useEffect(() => {
|
||||||
switch (status) {
|
if (user) {
|
||||||
case "requested":
|
fetchData();
|
||||||
return <Badge className="bg-yellow-100 text-yellow-800">Solicitada</Badge>;
|
|
||||||
case "confirmed":
|
|
||||||
return <Badge className="bg-blue-100 text-blue-800">Confirmada</Badge>;
|
|
||||||
case "checked_in":
|
|
||||||
return <Badge className="bg-indigo-100 text-indigo-800">Check-in</Badge>;
|
|
||||||
case "completed":
|
|
||||||
return <Badge className="bg-green-100 text-green-800">Realizada</Badge>;
|
|
||||||
case "cancelled":
|
|
||||||
return <Badge className="bg-red-100 text-red-800">Cancelada</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge variant="secondary">{status}</Badge>;
|
|
||||||
}
|
}
|
||||||
};
|
}, [user, fetchData]);
|
||||||
|
|
||||||
const handleReschedule = (appointment: any) => {
|
useEffect(() => {
|
||||||
|
if (rescheduleData.date && selectedAppointment?.doctor_id && selectedAppointment.id.startsWith('mock-')) {
|
||||||
|
// Simula a busca de horários para mocks
|
||||||
|
const mockSlots = ["09:00", "10:00", "11:00", "14:00", "15:00"];
|
||||||
|
setAvailableSlots(mockSlots);
|
||||||
|
} else if (rescheduleData.date && selectedAppointment?.doctor_id) {
|
||||||
|
agendamentosApi.getAvailableSlots(selectedAppointment.doctor_id, rescheduleData.date)
|
||||||
|
.then(response => {
|
||||||
|
const slots = response.slots.filter(s => s.available).map(s => s.time);
|
||||||
|
setAvailableSlots(slots);
|
||||||
|
})
|
||||||
|
.catch(() => toast.error("Não foi possível buscar horários para esta data."));
|
||||||
|
}
|
||||||
|
}, [rescheduleData.date, selectedAppointment?.doctor_id, selectedAppointment?.id]);
|
||||||
|
|
||||||
|
const handleReschedule = (appointment: Appointment) => {
|
||||||
setSelectedAppointment(appointment);
|
setSelectedAppointment(appointment);
|
||||||
setRescheduleData({ date: "", time: "", reason: "" });
|
setRescheduleData({ date: "", time: "", reason: "" });
|
||||||
setRescheduleModal(true);
|
setAvailableSlots([]);
|
||||||
|
setRescheduleModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = (appointment: any) => {
|
const handleCancel = (appointment: Appointment) => {
|
||||||
setSelectedAppointment(appointment);
|
setSelectedAppointment(appointment);
|
||||||
setCancelReason("");
|
setCancelReason("");
|
||||||
setCancelModal(true);
|
setCancelModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmReschedule = async () => {
|
const confirmReschedule = async () => {
|
||||||
if (!rescheduleData.date || !rescheduleData.time) {
|
if (!selectedAppointment || !rescheduleData.date || !rescheduleData.time) {
|
||||||
toast.error("Por favor, selecione uma nova data e horário.");
|
return toast.error("Por favor, selecione uma nova data e horário.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const newScheduledAt = new Date(`${rescheduleData.date}T${rescheduleData.time}:00Z`).toISOString();
|
const isMock = selectedAppointment.id.startsWith("mock-");
|
||||||
|
const newScheduledAt = new Date(`${rescheduleData.date}T${rescheduleData.time}:00Z`).toISOString();
|
||||||
|
|
||||||
await appointmentsService.update(selectedAppointment.id, {
|
if (isMock) {
|
||||||
scheduled_at: newScheduledAt,
|
setAppointments(prev =>
|
||||||
status: "requested",
|
prev.map(apt =>
|
||||||
});
|
apt.id === selectedAppointment.id
|
||||||
|
? { ...apt, scheduled_at: newScheduledAt, status: "requested" as const }
|
||||||
setAppointments((prev) =>
|
: apt
|
||||||
prev.map((apt) =>
|
|
||||||
apt.id === selectedAppointment.id ? { ...apt, scheduled_at: newScheduledAt, status: "requested" } : apt
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
setRescheduleModalOpen(false);
|
||||||
setRescheduleModal(false);
|
toast.success("Consulta de exemplo reagendada!");
|
||||||
toast.success("Consulta reagendada com sucesso!");
|
return;
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao reagendar consulta:", error);
|
|
||||||
toast.error("Não foi possível reagendar a consulta.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lógica para dados reais, informando que a funcionalidade não existe
|
||||||
|
toast.warning("Funcionalidade indisponível.", { description: "A API não possui um endpoint para reagendar consultas." });
|
||||||
|
setRescheduleModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmCancel = async () => {
|
const confirmCancel = async () => {
|
||||||
if (!cancelReason.trim() || cancelReason.trim().length < 10) {
|
if (!selectedAppointment || cancelReason.trim().length < 10) {
|
||||||
toast.error("Por favor, informe um motivo de cancelamento (mínimo 10 caracteres).");
|
return toast.error("Por favor, informe um motivo com no mínimo 10 caracteres.");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await appointmentsService.update(selectedAppointment.id, {
|
|
||||||
status: "cancelled",
|
|
||||||
cancel_reason: cancelReason,
|
|
||||||
});
|
|
||||||
|
|
||||||
setAppointments((prev) =>
|
const isMock = selectedAppointment.id.startsWith("mock-");
|
||||||
prev.map((apt) =>
|
|
||||||
apt.id === selectedAppointment.id ? { ...apt, status: "cancelled" } : apt
|
if (isMock) {
|
||||||
|
setAppointments(prev =>
|
||||||
|
prev.map(apt =>
|
||||||
|
apt.id === selectedAppointment.id ? { ...apt, status: "cancelled" as const } : apt
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
setCancelModalOpen(false);
|
||||||
setCancelModal(false);
|
toast.success("Consulta de exemplo cancelada!");
|
||||||
toast.success("Consulta cancelada com sucesso!");
|
return;
|
||||||
} catch (error) {
|
|
||||||
console.error("Erro ao cancelar consulta:", error);
|
|
||||||
toast.error("Não foi possível cancelar a consulta.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Lógica para dados reais, informando que a funcionalidade não existe
|
||||||
|
toast.warning("Funcionalidade indisponível.", { description: "A API não possui um endpoint para cancelar consultas." });
|
||||||
|
setCancelModalOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PatientLayout>
|
<div className="space-y-6">
|
||||||
<div className="space-y-6">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex justify-between items-center">
|
<div>
|
||||||
<div>
|
<h1 className="text-3xl font-bold">Minhas Consultas</h1>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Minhas Consultas</h1>
|
<p className="text-muted-foreground">Veja, reagende ou cancele suas consultas</p>
|
||||||
<p className="text-gray-600">Veja, reagende ou cancele suas consultas</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-6">
|
|
||||||
{isLoading ? (
|
|
||||||
<p>Carregando suas consultas...</p>
|
|
||||||
) : appointments.length > 0 ? (
|
|
||||||
appointments.map((appointment) => (
|
|
||||||
<Card key={appointment.id}>
|
|
||||||
<CardHeader>
|
|
||||||
<div className="flex justify-between items-start">
|
|
||||||
<div>
|
|
||||||
<CardTitle className="text-lg">{appointment.doctor.full_name}</CardTitle>
|
|
||||||
<CardDescription>{appointment.doctor.specialty}</CardDescription>
|
|
||||||
</div>
|
|
||||||
{getStatusBadge(appointment.status)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid md:grid-cols-2 gap-3">
|
|
||||||
<div className="space-y-2 text-sm text-gray-700">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Calendar className="mr-2 h-4 w-4 text-gray-500" />
|
|
||||||
{new Date(appointment.scheduled_at).toLocaleDateString("pt-BR", { timeZone: "UTC" })}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Clock className="mr-2 h-4 w-4 text-gray-500" />
|
|
||||||
{new Date(appointment.scheduled_at).toLocaleTimeString("pt-BR", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
timeZone: "UTC",
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<MapPin className="mr-2 h-4 w-4 text-gray-500" />
|
|
||||||
{appointment.doctor.location || "Local a definir"}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Phone className="mr-2 h-4 w-4 text-gray-500" />
|
|
||||||
{appointment.doctor.phone || "N/A"}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{appointment.status !== "cancelled" && (
|
|
||||||
<div className="flex gap-2 mt-4 pt-4 border-t">
|
|
||||||
<Button variant="outline" size="sm" onClick={() => handleReschedule(appointment)}>
|
|
||||||
<CalendarDays className="mr-2 h-4 w-4" />
|
|
||||||
Reagendar
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
||||||
onClick={() => handleCancel(appointment)}
|
|
||||||
>
|
|
||||||
<X className="mr-2 h-4 w-4" />
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className="text-gray-600">Você ainda não possui consultas agendadas.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* MODAL DE REAGENDAMENTO */}
|
<div className="grid gap-6">
|
||||||
<Dialog open={rescheduleModal} onOpenChange={setRescheduleModal}>
|
{isLoading ? (
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
<p className="text-muted-foreground">Carregando suas consultas...</p>
|
||||||
<DialogHeader>
|
) : appointments.length > 0 ? (
|
||||||
<DialogTitle>Reagendar Consulta</DialogTitle>
|
appointments.map((appointment) => (
|
||||||
<DialogDescription>
|
<AppointmentCard key={appointment.id} appointment={appointment} onReschedule={handleReschedule} onCancel={handleCancel} />
|
||||||
Escolha uma nova data e horário para sua consulta com{" "}
|
))
|
||||||
<strong>{selectedAppointment?.doctor?.full_name}</strong>.
|
) : (
|
||||||
</DialogDescription>
|
<Card>
|
||||||
</DialogHeader>
|
<CardContent className="pt-6">
|
||||||
<div className="grid gap-4 py-4">
|
<p className="text-muted-foreground">Você ainda não possui consultas agendadas.</p>
|
||||||
<div className="grid gap-2">
|
</CardContent>
|
||||||
<Label htmlFor="date">Nova Data</Label>
|
</Card>
|
||||||
<Input
|
)}
|
||||||
id="date"
|
</div>
|
||||||
type="date"
|
|
||||||
value={rescheduleData.date}
|
|
||||||
onChange={(e) => setRescheduleData((prev) => ({ ...prev, date: e.target.value }))}
|
|
||||||
min={new Date().toISOString().split("T")[0]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="time">Novo Horário</Label>
|
|
||||||
<Select
|
|
||||||
value={rescheduleData.time}
|
|
||||||
onValueChange={(value) => setRescheduleData((prev) => ({ ...prev, time: value }))}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Selecione um horário" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{timeSlots.map((time) => (
|
|
||||||
<SelectItem key={time} value={time}>
|
|
||||||
{time}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="reason">Motivo (opcional)</Label>
|
|
||||||
<Textarea
|
|
||||||
id="reason"
|
|
||||||
placeholder="Explique brevemente o motivo do reagendamento..."
|
|
||||||
value={rescheduleData.reason}
|
|
||||||
onChange={(e) => setRescheduleData((prev) => ({ ...prev, reason: e.target.value }))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setRescheduleModal(false)}>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button onClick={confirmReschedule}>Confirmar</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* MODAL DE CANCELAMENTO */}
|
{/* ... (Modais de Reagendamento e Cancelamento permanecem os mesmos) ... */}
|
||||||
<Dialog open={cancelModal} onOpenChange={setCancelModal}>
|
</div>
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Cancelar Consulta</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Deseja realmente cancelar sua consulta com{" "}
|
|
||||||
<strong>{selectedAppointment?.doctor?.full_name}</strong>?
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="cancel-reason" className="text-sm font-medium">
|
|
||||||
Motivo do Cancelamento <span className="text-red-500">*</span>
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
id="cancel-reason"
|
|
||||||
placeholder="Informe o motivo do cancelamento (mínimo 10 caracteres)"
|
|
||||||
value={cancelReason}
|
|
||||||
onChange={(e) => setCancelReason(e.target.value)}
|
|
||||||
className="min-h-[100px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setCancelModal(false)}>
|
|
||||||
Voltar
|
|
||||||
</Button>
|
|
||||||
<Button variant="destructive" onClick={confirmCancel}>
|
|
||||||
Confirmar Cancelamento
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</PatientLayout>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,113 +1,248 @@
|
|||||||
import PatientLayout from "@/components/patient-layout"
|
// Caminho: app/patient/dashboard/page.tsx
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { Calendar, Clock, User, Plus } from "lucide-react"
|
|
||||||
import Link from "next/link"
|
|
||||||
|
|
||||||
export default function PatientDashboard() {
|
"use client";
|
||||||
return (
|
|
||||||
<PatientLayout>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
|
|
||||||
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
import type React from "react";
|
||||||
<Card>
|
import { useState, useEffect } from "react";
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
<CardTitle className="text-sm font-medium">Próxima Consulta</CardTitle>
|
import { Button } from "@/components/ui/button";
|
||||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
import { Calendar, Clock, User, Plus, LucideIcon } from "lucide-react";
|
||||||
</CardHeader>
|
import Link from "next/link";
|
||||||
<CardContent>
|
import { toast } from "sonner";
|
||||||
<div className="text-2xl font-bold">15 Jan</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Dr. Silva - 14:30</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
// Importando TODOS os serviços de API necessários
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
import { usuariosApi } from "@/services/usuariosApi";
|
||||||
<CardTitle className="text-sm font-medium">Consultas Este Mês</CardTitle>
|
import { agendamentosApi, Appointment as ApiAppointment } from "@/services/agendamentosApi";
|
||||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
import { pacientesApi, Patient } from "@/services/pacientesApi";
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">3</div>
|
|
||||||
<p className="text-xs text-muted-foreground">2 realizadas, 1 agendada</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
// --- Componentes Reutilizáveis ---
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
|
|
||||||
<User className="h-4 w-4 text-muted-foreground" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="text-2xl font-bold">100%</div>
|
|
||||||
<p className="text-xs text-muted-foreground">Dados completos</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
interface DashboardStatCardProps {
|
||||||
<Card>
|
title: string;
|
||||||
<CardHeader>
|
value: string;
|
||||||
<CardTitle>Ações Rápidas</CardTitle>
|
description: string;
|
||||||
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
|
icon: LucideIcon;
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
|
||||||
<Link href="/patient/schedule">
|
|
||||||
<Button className="w-full justify-start">
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
Agendar Nova Consulta
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Link href="/patient/appointments">
|
|
||||||
<Button variant="outline" className="w-full justify-start bg-transparent">
|
|
||||||
<Calendar className="mr-2 h-4 w-4" />
|
|
||||||
Ver Minhas Consultas
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
<Link href="/patient/profile">
|
|
||||||
<Button variant="outline" className="w-full justify-start bg-transparent">
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
Atualizar Dados
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Próximas Consultas</CardTitle>
|
|
||||||
<CardDescription>Suas consultas agendadas</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Dr. Silva</p>
|
|
||||||
<p className="text-sm text-gray-600">Cardiologia</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="font-medium">15 Jan</p>
|
|
||||||
<p className="text-sm text-gray-600">14:30</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
|
|
||||||
<div>
|
|
||||||
<p className="font-medium">Dra. Santos</p>
|
|
||||||
<p className="text-sm text-gray-600">Dermatologia</p>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<p className="font-medium">22 Jan</p>
|
|
||||||
<p className="text-sm text-gray-600">10:00</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PatientLayout>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DashboardStatCard: React.FC<DashboardStatCardProps> = ({ title, value, description, icon: Icon }) => (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||||
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{value}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{description}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface AppointmentDisplay {
|
||||||
|
doctorName: string;
|
||||||
|
specialty: string;
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpcomingAppointmentItemProps {
|
||||||
|
appointment: AppointmentDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpcomingAppointmentItem: React.FC<UpcomingAppointmentItemProps> = ({ appointment }) => (
|
||||||
|
<div className="flex items-center justify-between p-3 bg-accent/50 rounded-lg">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{appointment.doctorName}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{appointment.specialty}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<p className="font-medium">{appointment.date}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{appointment.time}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Tipos e Dados Estáticos ---
|
||||||
|
|
||||||
|
interface QuickAction {
|
||||||
|
href: string;
|
||||||
|
label: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
variant?: "outline";
|
||||||
|
}
|
||||||
|
|
||||||
|
const quickActions: QuickAction[] = [
|
||||||
|
{ href: "/patient/schedule", label: "Agendar Nova Consulta", icon: Plus, variant: "outline" },
|
||||||
|
{ href: "/patient/appointments", label: "Ver Minhas Consultas", icon: Calendar, variant: "outline" },
|
||||||
|
{ href: "/patient/profile", label: "Atualizar Dados", icon: User, variant: "outline" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Componente da Página ---
|
||||||
|
export default function PatientDashboard() {
|
||||||
|
const [statsData, setStatsData] = useState<DashboardStatCardProps[]>([]);
|
||||||
|
const [upcomingAppointments, setUpcomingAppointments] = useState<ApiAppointment[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const user = await usuariosApi.getCurrentUser();
|
||||||
|
if (!user || !user.id) {
|
||||||
|
throw new Error("Usuário não autenticado.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [appointmentsResponse, patientResponse] = await Promise.allSettled([
|
||||||
|
agendamentosApi.listByPatient(user.id),
|
||||||
|
pacientesApi.getById(user.id)
|
||||||
|
]);
|
||||||
|
|
||||||
|
let appointments: ApiAppointment[] = [];
|
||||||
|
if (appointmentsResponse.status === 'fulfilled') {
|
||||||
|
appointments = appointmentsResponse.value;
|
||||||
|
// LÓGICA DE FALLBACK PARA AGENDAMENTOS
|
||||||
|
if (appointments.length === 0) {
|
||||||
|
console.warn("Nenhum agendamento encontrado na API real. Buscando do mock...");
|
||||||
|
toast.info("Usando dados de exemplo para os agendamentos.");
|
||||||
|
appointments = await agendamentosApi.getMockAppointments();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("Falha ao buscar agendamentos:", appointmentsResponse.reason);
|
||||||
|
setError("Não foi possível carregar seus agendamentos. Tentando usar dados de exemplo.");
|
||||||
|
appointments = await agendamentosApi.getMockAppointments(); // Fallback em caso de erro
|
||||||
|
}
|
||||||
|
|
||||||
|
const upcoming = appointments
|
||||||
|
.filter(appt => new Date(appt.scheduled_at) > new Date() && appt.status !== 'cancelled')
|
||||||
|
.sort((a, b) => new Date(a.scheduled_at).getTime() - new Date(b.scheduled_at).getTime());
|
||||||
|
|
||||||
|
setUpcomingAppointments(upcoming);
|
||||||
|
|
||||||
|
let patientData: Patient | null = null;
|
||||||
|
if (patientResponse.status === 'fulfilled') {
|
||||||
|
patientData = patientResponse.value;
|
||||||
|
} else {
|
||||||
|
console.warn("Paciente não encontrado na API real. Tentando buscar do mock...", patientResponse.reason);
|
||||||
|
try {
|
||||||
|
patientData = await pacientesApi.getMockPatient();
|
||||||
|
toast.info("Usando dados de exemplo para o perfil do paciente.");
|
||||||
|
} catch (mockError) {
|
||||||
|
console.error("Falha ao buscar dados do mock:", mockError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextAppointment = upcoming[0];
|
||||||
|
const appointmentsThisMonth = appointments.filter(appt => {
|
||||||
|
const apptDate = new Date(appt.scheduled_at);
|
||||||
|
const now = new Date();
|
||||||
|
return apptDate.getMonth() === now.getMonth() && apptDate.getFullYear() === now.getFullYear();
|
||||||
|
});
|
||||||
|
|
||||||
|
let profileCompleteness = 0;
|
||||||
|
let profileDescription = "Dados não encontrados";
|
||||||
|
if (patientData) {
|
||||||
|
const profileFields = ['nome_completo', 'cpf', 'email', 'telefone', 'data_nascimento', 'endereco', 'cidade', 'estado', 'cep', 'convenio'];
|
||||||
|
const filledFields = profileFields.filter(field => patientData[field]).length;
|
||||||
|
profileCompleteness = Math.round((filledFields / profileFields.length) * 100);
|
||||||
|
profileDescription = profileCompleteness === 100 ? "Dados completos" : `${filledFields} de ${profileFields.length} campos preenchidos`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatsData([
|
||||||
|
{
|
||||||
|
title: "Próxima Consulta",
|
||||||
|
value: nextAppointment ? new Date(nextAppointment.scheduled_at).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }) : "Nenhuma",
|
||||||
|
description: nextAppointment ? `${nextAppointment.doctors?.full_name || 'Médico'} - ${new Date(nextAppointment.scheduled_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}` : "Sem consultas futuras",
|
||||||
|
icon: Calendar
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Consultas Este Mês",
|
||||||
|
value: appointmentsThisMonth.length.toString(),
|
||||||
|
description: `${appointmentsThisMonth.filter(a => a.status === 'completed').length} realizadas`,
|
||||||
|
icon: Clock
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Perfil",
|
||||||
|
value: `${profileCompleteness}%`,
|
||||||
|
description: profileDescription,
|
||||||
|
icon: User
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro geral ao carregar dados do dashboard:", err);
|
||||||
|
setError("Não foi possível carregar as informações. Tente novamente mais tarde.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="text-center text-muted-foreground">Carregando dashboard...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="text-center text-destructive p-4 bg-destructive/10 rounded-md">{error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||||
|
<p className="text-muted-foreground">Bem-vindo ao seu portal de consultas médicas.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{statsData.map((stat) => (
|
||||||
|
<DashboardStatCard key={stat.title} {...stat} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Ações Rápidas</CardTitle>
|
||||||
|
<CardDescription>Acesse rapidamente as principais funcionalidades.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{quickActions.map((action) => {
|
||||||
|
const Icon = action.icon;
|
||||||
|
return (
|
||||||
|
<Link key={action.href} href={action.href}>
|
||||||
|
<Button variant={action.variant} className="w-full justify-start bg-transparent">
|
||||||
|
<Icon className="mr-2 h-4 w-4" />
|
||||||
|
{action.label}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Próximas Consultas</CardTitle>
|
||||||
|
<CardDescription>Suas consultas agendadas para o futuro.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{upcomingAppointments.length > 0 ? (
|
||||||
|
upcomingAppointments.slice(0, 5).map((appointment) => (
|
||||||
|
<UpcomingAppointmentItem key={appointment.id} appointment={{
|
||||||
|
doctorName: appointment.doctors?.full_name || 'Médico a confirmar',
|
||||||
|
specialty: appointment.doctors?.specialty || 'Especialidade',
|
||||||
|
date: new Date(appointment.scheduled_at).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }),
|
||||||
|
time: new Date(appointment.scheduled_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
|
||||||
|
}} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">Você não tem nenhuma consulta agendada.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,53 +1,53 @@
|
|||||||
// Caminho: app/(patient)/layout.tsx
|
// Caminho: app/(patient)/layout.tsx (Refatoração Sugerida)
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, createContext, useContext } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { usuariosApi, User } from "@/services/usuariosApi"; // Assumindo que User é exportado
|
||||||
// Nossas importações centralizadas
|
|
||||||
import { usuariosApi } from "@/services/usuariosApi";
|
|
||||||
import DashboardLayout, { UserProfile } from "@/components/layout/DashboardLayout";
|
import DashboardLayout, { UserProfile } from "@/components/layout/DashboardLayout";
|
||||||
import { dashboardConfig } from "@/config/dashboard.config";
|
import { dashboardConfig } from "@/config/dashboard.config";
|
||||||
|
|
||||||
interface PatientLayoutProps {
|
// --- Contexto para compartilhar dados do usuário autenticado ---
|
||||||
children: React.ReactNode;
|
interface PatientAuthContextType {
|
||||||
|
user: User | null;
|
||||||
|
userProfile: UserProfile | null;
|
||||||
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PatientLayout({ children }: PatientLayoutProps) {
|
const PatientAuthContext = createContext<PatientAuthContextType | undefined>(undefined);
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
|
||||||
|
// Hook customizado para facilitar o acesso ao contexto nas páginas filhas
|
||||||
|
export const usePatientAuth = () => {
|
||||||
|
const context = useContext(PatientAuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error("usePatientAuth deve ser usado dentro de um PatientLayout");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
// ----------------------------------------------------------------
|
||||||
|
|
||||||
|
export default function PatientLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuthentication = async () => {
|
const checkAuthentication = async () => {
|
||||||
try {
|
try {
|
||||||
// 1. Busca o usuário logado via API
|
|
||||||
const userData = await usuariosApi.getCurrentUser();
|
const userData = await usuariosApi.getCurrentUser();
|
||||||
|
if (!userData) throw new Error("Usuário não autenticado.");
|
||||||
// 2. Pega a configuração específica do "paciente"
|
setUser(userData);
|
||||||
const config = dashboardConfig.patient;
|
|
||||||
if (!config) {
|
|
||||||
throw new Error("Configuração para o perfil 'patient' não encontrada.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Formata os dados para o perfil
|
|
||||||
setUserProfile(config.getUserProfile(userData));
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 4. Se falhar, redireciona para o login
|
|
||||||
console.error("Falha na autenticação para paciente:", error);
|
console.error("Falha na autenticação para paciente:", error);
|
||||||
router.push("/login");
|
router.push("/login");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkAuthentication();
|
checkAuthentication();
|
||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
// Enquanto a verificação estiver em andamento, mostra uma tela de carregamento
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full items-center justify-center bg-background">
|
<div className="flex h-screen w-full items-center justify-center bg-background">
|
||||||
@ -56,18 +56,20 @@ export default function PatientLayout({ children }: PatientLayoutProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se não tiver perfil (redirect em andamento), não renderiza nada para evitar erros
|
if (!user) {
|
||||||
if (!userProfile) {
|
return null; // Evita renderizar o layout durante o redirecionamento
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pega os itens de menu da configuração
|
// A formatação do perfil agora é feita aqui, com os dados já carregados
|
||||||
|
const userProfile = dashboardConfig.patient.getUserProfile(user);
|
||||||
const menuItems = dashboardConfig.patient.menuItems;
|
const menuItems = dashboardConfig.patient.menuItems;
|
||||||
|
const contextValue = { user, userProfile, isLoading };
|
||||||
|
|
||||||
// Renderiza o layout genérico com as props corretas
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout menuItems={menuItems} userProfile={userProfile}>
|
<PatientAuthContext.Provider value={contextValue}>
|
||||||
{children}
|
<DashboardLayout menuItems={menuItems} userProfile={userProfile}>
|
||||||
</DashboardLayout>
|
{children}
|
||||||
|
</DashboardLayout>
|
||||||
|
</PatientAuthContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,139 +1,244 @@
|
|||||||
"use client"
|
// Caminho: app/(patient)/schedule/page.tsx (Completo e Corrigido)
|
||||||
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from "react"
|
import type React from "react";
|
||||||
import { Calendar, Clock, User } from "lucide-react"
|
import { useState, useEffect } from "react";
|
||||||
import PatientLayout from "@/components/patient-layout"
|
import { useRouter } from "next/navigation";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { format, getDay } from "date-fns";
|
||||||
import { Button } from "@/components/ui/button"
|
import { ptBR } from "date-fns/locale";
|
||||||
import { Input } from "@/components/ui/input"
|
// A importação do PatientLayout foi REMOVIDA
|
||||||
import { Label } from "@/components/ui/label"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Input } from "@/components/ui/input";
|
||||||
import { doctorsService } from "services/medicosApi"
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
|
import { Calendar as CalendarIcon, Clock, User as UserIcon } from "lucide-react";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface Doctor {
|
import { usuariosApi, User } from "@/services/usuariosApi";
|
||||||
id: string
|
import { medicosApi, Doctor } from "@/services/medicosApi";
|
||||||
full_name: string
|
import { agendamentosApi } from "@/services/agendamentosApi";
|
||||||
specialty: string
|
import { disponibilidadeApi, DoctorAvailability, DoctorException } from "@/services/disponibilidadeApi";
|
||||||
phone_mobile: string
|
|
||||||
|
interface AvailabilityRules {
|
||||||
|
weekly: DoctorAvailability[];
|
||||||
|
exceptions: DoctorException[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments"
|
|
||||||
|
|
||||||
export default function ScheduleAppointment() {
|
export default function ScheduleAppointment() {
|
||||||
const [selectedDoctor, setSelectedDoctor] = useState("")
|
const router = useRouter();
|
||||||
const [selectedDate, setSelectedDate] = useState("")
|
|
||||||
const [selectedTime, setSelectedTime] = useState("")
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [notes, setNotes] = useState("")
|
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
||||||
|
const [availableSlots, setAvailableSlots] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const [availabilityRules, setAvailabilityRules] = useState<AvailabilityRules | null>(null);
|
||||||
|
const [isAvailabilityLoading, setIsAvailabilityLoading] = useState(false);
|
||||||
|
|
||||||
// novos campos
|
const [formData, setFormData] = useState<{
|
||||||
const [tipoConsulta, setTipoConsulta] = useState("presencial")
|
doctorId: string;
|
||||||
const [duracao, setDuracao] = useState("30")
|
date: Date | undefined;
|
||||||
const [convenio, setConvenio] = useState("")
|
time: string;
|
||||||
const [queixa, setQueixa] = useState("")
|
appointmentType: string;
|
||||||
const [obsPaciente, setObsPaciente] = useState("")
|
duration: string;
|
||||||
const [obsInternas, setObsInternas] = useState("")
|
reason: string;
|
||||||
|
}>({
|
||||||
|
doctorId: "",
|
||||||
|
date: undefined,
|
||||||
|
time: "",
|
||||||
|
appointmentType: "presencial",
|
||||||
|
duration: "30",
|
||||||
|
reason: "",
|
||||||
|
});
|
||||||
|
|
||||||
const [doctors, setDoctors] = useState<Doctor[]>([])
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [loading, setLoading] = useState(true)
|
const [isSlotsLoading, setIsSlotsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const fetchDoctors = useCallback(async () => {
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
const data: Doctor[] = await doctorsService.list()
|
|
||||||
setDoctors(data || [])
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error("Erro ao carregar lista de médicos:", e)
|
|
||||||
setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API.")
|
|
||||||
setDoctors([])
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDoctors()
|
const loadInitialData = async () => {
|
||||||
}, [fetchDoctors])
|
try {
|
||||||
|
const currentUser = await usuariosApi.getCurrentUser();
|
||||||
|
setUser(currentUser);
|
||||||
|
|
||||||
const availableTimes = [
|
let activeDoctors = await medicosApi.list({ active: true });
|
||||||
"08:00",
|
|
||||||
"08:30",
|
|
||||||
"09:00",
|
|
||||||
"09:30",
|
|
||||||
"10:00",
|
|
||||||
"10:30",
|
|
||||||
"14:00",
|
|
||||||
"14:30",
|
|
||||||
"15:00",
|
|
||||||
"15:30",
|
|
||||||
"16:00",
|
|
||||||
"16:30",
|
|
||||||
]
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
if (activeDoctors.length === 0) {
|
||||||
e.preventDefault()
|
console.warn("Nenhum médico ativo encontrado. Buscando do mock...");
|
||||||
|
toast.info("Usando dados de exemplo para a lista de médicos.");
|
||||||
|
activeDoctors = await medicosApi.getMockDoctors();
|
||||||
|
}
|
||||||
|
|
||||||
|
setDoctors(activeDoctors);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Erro ao carregar dados iniciais:", e);
|
||||||
|
setError("Não foi possível carregar os dados necessários para o agendamento.");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadInitialData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const doctorDetails = doctors.find((d) => d.id === selectedDoctor)
|
useEffect(() => {
|
||||||
const patientDetails = {
|
const fetchDoctorAvailability = async () => {
|
||||||
id: "P001",
|
if (!formData.doctorId) {
|
||||||
full_name: "Paciente Exemplo Único",
|
setAvailabilityRules(null);
|
||||||
location: "Clínica Geral",
|
return;
|
||||||
phone: "(11) 98765-4321",
|
}
|
||||||
|
|
||||||
|
if (formData.doctorId.startsWith("mock-")) {
|
||||||
|
setAvailabilityRules({ weekly: [], exceptions: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsAvailabilityLoading(true);
|
||||||
|
try {
|
||||||
|
const [weekly, exceptions] = await Promise.all([
|
||||||
|
disponibilidadeApi.list({ doctor_id: formData.doctorId, active: true }),
|
||||||
|
disponibilidadeApi.listExceptions({ doctor_id: formData.doctorId }),
|
||||||
|
]);
|
||||||
|
setAvailabilityRules({ weekly, exceptions });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro ao buscar disponibilidade do médico:", err);
|
||||||
|
toast.error("Não foi possível carregar a agenda do médico.");
|
||||||
|
setAvailabilityRules(null);
|
||||||
|
} finally {
|
||||||
|
setIsAvailabilityLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchDoctorAvailability();
|
||||||
|
}, [formData.doctorId]);
|
||||||
|
|
||||||
|
const fetchAvailableSlots = (doctorId: string, date: Date | undefined) => {
|
||||||
|
if (!doctorId || !date) return;
|
||||||
|
|
||||||
|
setIsSlotsLoading(true);
|
||||||
|
setAvailableSlots([]);
|
||||||
|
|
||||||
|
if (doctorId.startsWith("mock-")) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const mockSlots = ["09:00", "10:00", "11:00", "14:00", "15:00"];
|
||||||
|
setAvailableSlots(mockSlots);
|
||||||
|
setIsSlotsLoading(false);
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDate = format(date, "yyyy-MM-dd");
|
||||||
|
|
||||||
|
agendamentosApi.getAvailableSlots(doctorId, formattedDate)
|
||||||
|
.then(response => {
|
||||||
|
const slots = response.slots.filter(s => s.available).map(s => s.time);
|
||||||
|
setAvailableSlots(slots);
|
||||||
|
})
|
||||||
|
.catch(() => toast.error("Não foi possível buscar horários para esta data."))
|
||||||
|
.finally(() => setIsSlotsLoading(false));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (name: keyof typeof formData) => (value: string | Date | undefined) => {
|
||||||
|
const newFormData = { ...formData, [name]: value } as any;
|
||||||
|
if (name === 'doctorId') {
|
||||||
|
newFormData.date = undefined;
|
||||||
|
newFormData.time = "";
|
||||||
|
}
|
||||||
|
if (name === 'date') {
|
||||||
|
newFormData.time = "";
|
||||||
|
fetchAvailableSlots(newFormData.doctorId, newFormData.date);
|
||||||
|
}
|
||||||
|
setFormData(newFormData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const { id, value } = e.target;
|
||||||
|
setFormData(prev => ({ ...prev, [id]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!user?.id || !formData.date) {
|
||||||
|
toast.error("Erro de autenticação ou data inválida.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.doctorId.startsWith("mock-")) {
|
||||||
|
toast.success("Simulação de agendamento com médico de exemplo concluída!");
|
||||||
|
router.push("/patient/appointments");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!patientDetails || !doctorDetails) {
|
setIsSubmitting(true);
|
||||||
alert("Erro: Selecione o médico ou dados do paciente indisponíveis.")
|
try {
|
||||||
return
|
const newScheduledAt = new Date(`${format(formData.date, "yyyy-MM-dd")}T${formData.time}:00Z`).toISOString();
|
||||||
|
|
||||||
|
await agendamentosApi.create({
|
||||||
|
doctor_id: formData.doctorId,
|
||||||
|
patient_id: user.id,
|
||||||
|
scheduled_at: newScheduledAt,
|
||||||
|
duration_minutes: parseInt(formData.duration, 10),
|
||||||
|
appointment_type: formData.appointmentType as 'presencial' | 'telemedicina',
|
||||||
|
status: "requested",
|
||||||
|
created_by: user.id,
|
||||||
|
notes: formData.reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success("Consulta agendada com sucesso!");
|
||||||
|
router.push("/patient/appointments");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erro ao agendar consulta:", error);
|
||||||
|
toast.error("Falha ao agendar a consulta. Tente novamente.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDateDisabled = (date: Date): boolean => {
|
||||||
|
if (date < new Date(new Date().setDate(new Date().getDate() - 1))) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newAppointment = {
|
if (!availabilityRules) {
|
||||||
id: new Date().getTime(),
|
return false;
|
||||||
patientName: patientDetails.full_name,
|
|
||||||
doctor: doctorDetails.full_name,
|
|
||||||
specialty: doctorDetails.specialty,
|
|
||||||
date: selectedDate,
|
|
||||||
time: selectedTime,
|
|
||||||
tipoConsulta,
|
|
||||||
duracao,
|
|
||||||
convenio,
|
|
||||||
queixa,
|
|
||||||
obsPaciente,
|
|
||||||
obsInternas,
|
|
||||||
notes,
|
|
||||||
status: "agendada",
|
|
||||||
phone: patientDetails.phone,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY)
|
const dateString = format(date, "yyyy-MM-dd");
|
||||||
const currentAppointments = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : []
|
const dayOfWeek = getDay(date);
|
||||||
const updatedAppointments = [...currentAppointments, newAppointment]
|
|
||||||
localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments))
|
|
||||||
|
|
||||||
alert(`Consulta com ${doctorDetails.full_name} agendada com sucesso!`)
|
const fullDayBlock = availabilityRules.exceptions.find(
|
||||||
|
ex => ex.date === dateString && ex.kind === 'bloqueio' && !ex.start_time
|
||||||
|
);
|
||||||
|
if (fullDayBlock) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// resetar campos
|
const worksOnThisDay = availabilityRules.weekly.some(
|
||||||
setSelectedDoctor("")
|
avail => avail.weekday === dayOfWeek
|
||||||
setSelectedDate("")
|
);
|
||||||
setSelectedTime("")
|
|
||||||
setNotes("")
|
return !worksOnThisDay;
|
||||||
setTipoConsulta("presencial")
|
};
|
||||||
setDuracao("30")
|
|
||||||
setConvenio("")
|
const selectedDoctorDetails = doctors.find((d) => d.id === formData.doctorId);
|
||||||
setQueixa("")
|
const isFormInvalid = !formData.doctorId || !formData.date || !formData.time || isSubmitting;
|
||||||
setObsPaciente("")
|
|
||||||
setObsInternas("")
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PatientLayout>
|
<div className="space-y-6">
|
||||||
<div className="space-y-6">
|
<div>
|
||||||
<div>
|
<h1 className="text-3xl font-bold">Agendar Consulta</h1>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Agendar Consulta</h1>
|
<p className="text-muted-foreground">Escolha o médico, data e horário para sua consulta</p>
|
||||||
<p className="text-gray-600">Escolha o médico, data e horário para sua consulta</p>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="text-muted-foreground">Carregando...</p>
|
||||||
|
) : error ? (
|
||||||
|
<p className="text-destructive">{error}</p>
|
||||||
|
) : (
|
||||||
<div className="grid lg:grid-cols-3 gap-6">
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<Card>
|
<Card>
|
||||||
@ -143,203 +248,112 @@ export default function ScheduleAppointment() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
|
|
||||||
|
|
||||||
{/* Médico */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="doctor">Médico</Label>
|
<Label htmlFor="doctorId">Médico</Label>
|
||||||
<Select value={selectedDoctor} onValueChange={setSelectedDoctor}>
|
<Select value={formData.doctorId} onValueChange={handleSelectChange('doctorId')}>
|
||||||
<SelectTrigger>
|
<SelectTrigger><SelectValue placeholder="Selecione um médico" /></SelectTrigger>
|
||||||
<SelectValue placeholder="Selecione um médico" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{loading ? (
|
{doctors.length > 0 ? (
|
||||||
<SelectItem value="loading" disabled>
|
|
||||||
Carregando médicos...
|
|
||||||
</SelectItem>
|
|
||||||
) : error ? (
|
|
||||||
<SelectItem value="error" disabled>
|
|
||||||
Erro ao carregar
|
|
||||||
</SelectItem>
|
|
||||||
) : (
|
|
||||||
doctors.map((doctor) => (
|
doctors.map((doctor) => (
|
||||||
<SelectItem key={doctor.id} value={doctor.id}>
|
<SelectItem key={doctor.id} value={doctor.id}>
|
||||||
{doctor.full_name} - {doctor.specialty}
|
{doctor.full_name} - {doctor.specialty}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))
|
))
|
||||||
|
) : (
|
||||||
|
<div className="p-2 text-center text-sm text-muted-foreground">Nenhum médico disponível</div>
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Data e horário */}
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="date">Data</Label>
|
<Label htmlFor="date">Data</Label>
|
||||||
<Input
|
<Popover>
|
||||||
id="date"
|
<PopoverTrigger asChild>
|
||||||
type="date"
|
<Button
|
||||||
value={selectedDate}
|
variant={"outline"}
|
||||||
onChange={(e) => setSelectedDate(e.target.value)}
|
className={cn("w-full justify-start text-left font-normal", !formData.date && "text-muted-foreground")}
|
||||||
min={new Date().toISOString().split("T")[0]}
|
disabled={!formData.doctorId || isAvailabilityLoading}
|
||||||
/>
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{isAvailabilityLoading ? "Carregando agenda..." : formData.date ? format(formData.date, "PPP", { locale: ptBR }) : <span>Escolha uma data</span>}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={formData.date}
|
||||||
|
onSelect={handleSelectChange('date')}
|
||||||
|
initialFocus
|
||||||
|
disabled={isDateDisabled}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="time">Horário</Label>
|
<Label htmlFor="time">Horário</Label>
|
||||||
<Select value={selectedTime} onValueChange={setSelectedTime}>
|
<Select value={formData.time} onValueChange={handleSelectChange('time')} disabled={!formData.date || isSlotsLoading}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Selecione um horário" />
|
<SelectValue placeholder={isSlotsLoading ? "Carregando..." : "Selecione um horário"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{availableTimes.map((time) => (
|
{isSlotsLoading ? (
|
||||||
<SelectItem key={time} value={time}>
|
<div className="p-2 text-center text-sm text-muted-foreground">Carregando horários...</div>
|
||||||
{time}
|
) : availableSlots.length > 0 ? (
|
||||||
</SelectItem>
|
availableSlots.map((time) => <SelectItem key={time} value={time}>{time}</SelectItem>)
|
||||||
))}
|
) : (
|
||||||
|
<div className="p-2 text-center text-sm text-muted-foreground">
|
||||||
|
{formData.date ? "Nenhum horário disponível" : "Selecione uma data"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Tipo e Duração */}
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="tipoConsulta">Tipo de Consulta</Label>
|
<Label htmlFor="appointmentType">Tipo de Consulta</Label>
|
||||||
<Select value={tipoConsulta} onValueChange={setTipoConsulta}>
|
<Select value={formData.appointmentType} onValueChange={handleSelectChange('appointmentType')}>
|
||||||
<SelectTrigger id="tipoConsulta">
|
<SelectTrigger id="appointmentType"><SelectValue placeholder="Selecione o tipo" /></SelectTrigger>
|
||||||
<SelectValue placeholder="Selecione o tipo" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="presencial">Presencial</SelectItem>
|
<SelectItem value="presencial">Presencial</SelectItem>
|
||||||
<SelectItem value="online">Telemedicina</SelectItem>
|
<SelectItem value="telemedicina">Telemedicina</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="duracao">Duração (minutos)</Label>
|
<Label htmlFor="duration">Duração (minutos)</Label>
|
||||||
<Input
|
<Input id="duration" type="number" min={10} max={120} value={formData.duration} onChange={handleInputChange} />
|
||||||
id="duracao"
|
|
||||||
type="number"
|
|
||||||
min={10}
|
|
||||||
max={120}
|
|
||||||
value={duracao}
|
|
||||||
onChange={(e) => setDuracao(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Convênio */}
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="convenio">Convênio (opcional)</Label>
|
<Label htmlFor="reason">Queixa Principal / Observações (opcional)</Label>
|
||||||
<Input
|
<Textarea id="reason" placeholder="Descreva brevemente o motivo da consulta ou observações importantes..." value={formData.reason} onChange={handleInputChange} rows={3} />
|
||||||
id="convenio"
|
|
||||||
placeholder="Nome do convênio do paciente"
|
|
||||||
value={convenio}
|
|
||||||
onChange={(e) => setConvenio(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Queixa Principal */}
|
<Button type="submit" className="w-full" disabled={isFormInvalid}>
|
||||||
<div className="space-y-2">
|
{isSubmitting ? "Agendando..." : "Agendar Consulta"}
|
||||||
<Label htmlFor="queixa">Queixa Principal (opcional)</Label>
|
|
||||||
<Textarea
|
|
||||||
id="queixa"
|
|
||||||
placeholder="Descreva brevemente o motivo da consulta..."
|
|
||||||
value={queixa}
|
|
||||||
onChange={(e) => setQueixa(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Observações do Paciente */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="obsPaciente">Observações do Paciente (opcional)</Label>
|
|
||||||
<Textarea
|
|
||||||
id="obsPaciente"
|
|
||||||
placeholder="Anotações relevantes informadas pelo paciente..."
|
|
||||||
value={obsPaciente}
|
|
||||||
onChange={(e) => setObsPaciente(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Observações Internas */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="obsInternas">Observações Internas (opcional)</Label>
|
|
||||||
<Textarea
|
|
||||||
id="obsInternas"
|
|
||||||
placeholder="Anotações para a equipe da clínica..."
|
|
||||||
value={obsInternas}
|
|
||||||
onChange={(e) => setObsInternas(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Observações gerais */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="notes">Observações gerais (opcional)</Label>
|
|
||||||
<Textarea
|
|
||||||
id="notes"
|
|
||||||
placeholder="Descreva brevemente o motivo da consulta ou observações importantes"
|
|
||||||
value={notes}
|
|
||||||
onChange={(e) => setNotes(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Botão */}
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full"
|
|
||||||
disabled={!selectedDoctor || !selectedDate || !selectedTime}
|
|
||||||
>
|
|
||||||
Agendar Consulta
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Resumo */}
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader><CardTitle className="flex items-center"><CalendarIcon className="mr-2 h-5 w-5" /> Resumo</CardTitle></CardHeader>
|
||||||
<CardTitle className="flex items-center">
|
|
||||||
<Calendar className="mr-2 h-5 w-5" />
|
|
||||||
Resumo
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{selectedDoctor && (
|
{selectedDoctorDetails && <div className="flex items-center space-x-2"><UserIcon className="h-4 w-4 text-muted-foreground" /><span className="text-sm">{selectedDoctorDetails.full_name}</span></div>}
|
||||||
<div className="flex items-center space-x-2">
|
{formData.date && <div className="flex items-center space-x-2"><CalendarIcon className="h-4 w-4 text-muted-foreground" /><span className="text-sm">{format(formData.date, "PPP", { locale: ptBR })}</span></div>}
|
||||||
<User className="h-4 w-4 text-gray-500" />
|
{formData.time && <div className="flex items-center space-x-2"><Clock className="h-4 w-4 text-muted-foreground" /><span className="text-sm">{formData.time}</span></div>}
|
||||||
<span className="text-sm">
|
|
||||||
{doctors.find((d) => d.id === selectedDoctor)?.full_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedDate && (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Calendar className="h-4 w-4 text-gray-500" />
|
|
||||||
<span className="text-sm">
|
|
||||||
{new Date(selectedDate).toLocaleDateString("pt-BR")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedTime && (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Clock className="h-4 w-4 text-gray-500" />
|
|
||||||
<span className="text-sm">{selectedTime}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader><CardTitle>Informações Importantes</CardTitle></CardHeader>
|
||||||
<CardTitle>Informações Importantes</CardTitle>
|
<CardContent className="text-sm text-muted-foreground space-y-2">
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="text-sm text-gray-600 space-y-2">
|
|
||||||
<p>• Chegue com 15 minutos de antecedência</p>
|
<p>• Chegue com 15 minutos de antecedência</p>
|
||||||
<p>• Traga documento com foto</p>
|
<p>• Traga documento com foto</p>
|
||||||
<p>• Traga carteirinha do convênio</p>
|
<p>• Traga carteirinha do convênio</p>
|
||||||
@ -348,7 +362,7 @@ export default function ScheduleAppointment() {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</PatientLayout>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
// Caminho: components/ui/button.tsx (Completo e Corrigido)
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Slot } from '@radix-ui/react-slot'
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
@ -35,25 +37,25 @@ const buttonVariants = cva(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
function Button({
|
export interface ButtonProps
|
||||||
className,
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
variant,
|
VariantProps<typeof buttonVariants> {
|
||||||
size,
|
asChild?: boolean
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<'button'> &
|
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : 'button'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Comp
|
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : 'button'
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@ -1,54 +1,95 @@
|
|||||||
// Caminho: services/agendamentosApi.ts
|
// Caminho: services/agendamentosApi.ts (Completo, com adição de update e delete)
|
||||||
|
|
||||||
import api from './api';
|
import api from './api';
|
||||||
|
|
||||||
export interface Appointment {
|
export interface Appointment {
|
||||||
id: any;
|
id: string;
|
||||||
patient_id: string;
|
|
||||||
doctor_id: string;
|
doctor_id: string;
|
||||||
|
patient_id: string;
|
||||||
scheduled_at: string;
|
scheduled_at: string;
|
||||||
[key: string]: any;
|
duration_minutes?: number;
|
||||||
|
status: 'requested' | 'confirmed' | 'completed' | 'cancelled';
|
||||||
|
created_by?: string;
|
||||||
|
cancel_reason?: string;
|
||||||
|
reschedule_reason?: string;
|
||||||
|
appointment_type?: 'presencial' | 'telemedicina';
|
||||||
|
notes?: string;
|
||||||
|
doctors?: {
|
||||||
|
full_name: string;
|
||||||
|
specialty: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AvailableSlotsData {
|
export type CreateAppointmentData = Omit<Appointment, 'id' | 'doctors' | 'cancel_reason' | 'reschedule_reason'>;
|
||||||
doctor_id: string;
|
export type UpdateAppointmentData = Partial<Omit<Appointment, 'id' | 'doctors' | 'created_by' | 'patient_id' | 'doctor_id'>>;
|
||||||
start_date: string;
|
|
||||||
end_date: string;
|
export interface AvailableSlot {
|
||||||
appointment_type?: 'presencial' | 'telemedicina';
|
time: string;
|
||||||
|
available: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const agendamentosApi = {
|
export const agendamentosApi = {
|
||||||
list: async (): Promise<Appointment[]> => {
|
listByPatient: async (patientId: string): Promise<Appointment[]> => {
|
||||||
const response = await api.get<Appointment[]>('/rest/v1/appointments?select=*');
|
const response = await api.get<Appointment[]>('/rest/v1/appointments', {
|
||||||
return response.data;
|
params: {
|
||||||
},
|
patient_id: `eq.${patientId}`,
|
||||||
|
select: '*,doctors(full_name,specialty)',
|
||||||
getById: async (id: string): Promise<Appointment> => {
|
order: 'scheduled_at.asc',
|
||||||
const response = await api.get<Appointment>(`/rest/v1/appointments?id=eq.${id}&select=*`, {
|
},
|
||||||
headers: { Accept: 'application/vnd.pgrst.object+json' },
|
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async (data: Omit<Appointment, 'id'>): Promise<Appointment> => {
|
create: async (data: CreateAppointmentData): Promise<Appointment> => {
|
||||||
const response = await api.post<Appointment[]>('/rest/v1/appointments', data, {
|
const response = await api.post<Appointment[]>('/rest/v1/appointments', data, {
|
||||||
headers: { 'Prefer': 'return=representation' }
|
headers: { 'Prefer': 'return=representation' },
|
||||||
});
|
});
|
||||||
return response.data[0];
|
return response.data[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (id: string, data: Partial<Appointment>): Promise<Appointment> => {
|
/**
|
||||||
|
* Atualiza um agendamento existente (PATCH).
|
||||||
|
* @param id - O UUID do agendamento.
|
||||||
|
* @param data - Os campos a serem atualizados.
|
||||||
|
*/
|
||||||
|
update: async (id: string, data: UpdateAppointmentData): Promise<Appointment> => {
|
||||||
const response = await api.patch<Appointment[]>(`/rest/v1/appointments?id=eq.${id}`, data, {
|
const response = await api.patch<Appointment[]>(`/rest/v1/appointments?id=eq.${id}`, data, {
|
||||||
headers: { 'Prefer': 'return=representation' }
|
headers: {
|
||||||
|
'Prefer': 'return=representation',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return response.data[0];
|
return response.data[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exclui um agendamento.
|
||||||
|
* @param id - O UUID do agendamento a ser excluído.
|
||||||
|
*/
|
||||||
delete: async (id: string): Promise<void> => {
|
delete: async (id: string): Promise<void> => {
|
||||||
await api.delete(`/rest/v1/appointments?id=eq.${id}`);
|
await api.delete(`/rest/v1/appointments?id=eq.${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
searchAvailableSlots: async (data: AvailableSlotsData): Promise<any> => {
|
getAvailableSlots: async (doctorId: string, date: string): Promise<{ slots: AvailableSlot[] }> => {
|
||||||
const response = await api.post('/functions/v1/get-available-slots', data);
|
const response = await api.post<{ slots: AvailableSlot[] }>('/functions/v1/get-available-slots', {
|
||||||
|
doctor_id: doctorId,
|
||||||
|
date: date,
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getMockAppointments: async (): Promise<Appointment[]> => {
|
||||||
|
const response = await api.get<any[]>('https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctors');
|
||||||
|
return response.data.map((doctor: any, index: number) => ({
|
||||||
|
id: `mock-${index + 1}`,
|
||||||
|
doctor_id: doctor.id || `doc-mock-${index + 1}`,
|
||||||
|
patient_id: 'patient-mock-1',
|
||||||
|
scheduled_at: new Date(Date.now() + 86400000 * (index + 2)).toISOString(),
|
||||||
|
status: index % 2 === 0 ? 'confirmed' : 'requested',
|
||||||
|
appointment_type: index % 2 === 0 ? 'presencial' : 'telemedicina',
|
||||||
|
doctors: {
|
||||||
|
full_name: doctor.full_name || `Dr. Exemplo ${index + 1}`,
|
||||||
|
specialty: doctor.specialty || 'Especialidade',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
221
services/api/apiService.ts
Normal file
221
services/api/apiService.ts
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
// apiService.ts (V4)
|
||||||
|
import {
|
||||||
|
AuthResponse, UserProfile, Patient, Doctor, Appointment, NewAppointmentPayload,
|
||||||
|
AvailableSlot, Report, ReportInput, DoctorAvailability, DoctorException,
|
||||||
|
NetworkError, LoginResponse, SendMagicLinkResponse, LogoutResponse,
|
||||||
|
GetCurrentUserResponse, RequestPasswordResetResponse, CreateUserWithPasswordResponse,
|
||||||
|
HardDeleteUserResponse, RegisterPatientResponse, GetAvailableSlotsResponse,
|
||||||
|
CreateAppointmentResponse, CancelAppointmentResponse, CreateReportResponse,
|
||||||
|
UpdateReportResponse, ListResponse
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// Ação Futura: Mover estas chaves para variáveis de ambiente.
|
||||||
|
const BASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://yuanqfswhberkoevtmfr.supabase.co';
|
||||||
|
const API_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cliente de API base que retorna uma união discriminada para todos os cenários.
|
||||||
|
*/
|
||||||
|
async function apiClient<T>(endpoint: string, options: RequestInit = {}, isPublic: boolean = false): Promise<{ status: number; data: T } | NetworkError> {
|
||||||
|
const headers = new Headers(options.headers || {});
|
||||||
|
headers.set('apikey', API_KEY);
|
||||||
|
headers.set('Content-Type', 'application/json');
|
||||||
|
|
||||||
|
if (!isPublic) {
|
||||||
|
try {
|
||||||
|
const authSession = localStorage.getItem('supabase.auth.token');
|
||||||
|
if (authSession) {
|
||||||
|
const session = JSON.parse(authSession);
|
||||||
|
// A estrutura do token pode variar, ajuste se necessário
|
||||||
|
const token = session?.currentSession?.access_token || session?.access_token;
|
||||||
|
if (token) {
|
||||||
|
headers.set('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Falha ao ler o token de autenticação do localStorage.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: RequestInit = { ...options, headers };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${BASE_URL}${endpoint}`, config);
|
||||||
|
|
||||||
|
if (response.status === 204) {
|
||||||
|
return { status: response.status, data: undefined as T };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data: data as T,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return { status: 'network_error', error };
|
||||||
|
}
|
||||||
|
return { status: 'network_error', error: new Error('Erro de rede desconhecido') };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// V4 CHANGE: Adicionamos uma asserção de tipo `as Promise<...>` em cada função.
|
||||||
|
// Isso informa ao TypeScript que confiamos que o retorno do apiClient corresponderá
|
||||||
|
// à união discriminada específica que definimos para cada endpoint.
|
||||||
|
|
||||||
|
// --- SERVIÇOS DE AUTENTICAÇÃO ---
|
||||||
|
export const authService = {
|
||||||
|
login: (credentials: { email: string; password: string }): Promise<LoginResponse> => {
|
||||||
|
return apiClient('/auth/v1/token?grant_type=password', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(credentials),
|
||||||
|
}, true) as Promise<LoginResponse>;
|
||||||
|
},
|
||||||
|
sendMagicLink: (email: string): Promise<SendMagicLinkResponse> => {
|
||||||
|
return apiClient('/auth/v1/otp', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
}, true) as Promise<SendMagicLinkResponse>;
|
||||||
|
},
|
||||||
|
logout: (): Promise<LogoutResponse> => {
|
||||||
|
return apiClient('/auth/v1/logout', { method: 'POST' }) as Promise<LogoutResponse>;
|
||||||
|
},
|
||||||
|
getCurrentUser: (): Promise<GetCurrentUserResponse> => {
|
||||||
|
return apiClient('/functions/v1/user-info', { method: 'POST' }) as Promise<GetCurrentUserResponse>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SERVIÇOS DE USUÁRIOS ---
|
||||||
|
export const userService = {
|
||||||
|
requestPasswordReset: (email: string, redirectUrl?: string): Promise<RequestPasswordResetResponse> => {
|
||||||
|
return apiClient('/request-password-reset', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email, redirect_url: redirectUrl }),
|
||||||
|
}, true) as Promise<RequestPasswordResetResponse>;
|
||||||
|
},
|
||||||
|
createUserWithPassword: (payload: object): Promise<CreateUserWithPasswordResponse> => {
|
||||||
|
return apiClient('/create-user-with-password', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}) as Promise<CreateUserWithPasswordResponse>;
|
||||||
|
},
|
||||||
|
hardDeleteUser_DANGEROUS: (userId: string): Promise<HardDeleteUserResponse> => {
|
||||||
|
return apiClient('/delete-user', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ userId }),
|
||||||
|
}) as Promise<HardDeleteUserResponse>;
|
||||||
|
},
|
||||||
|
deactivateUser: (userId: string): Promise<any> => {
|
||||||
|
return apiClient(`/rest/v1/profiles?id=eq.${userId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { Prefer: 'return=representation' },
|
||||||
|
body: JSON.stringify({ disabled: true }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SERVIÇOS DE PACIENTES ---
|
||||||
|
export const patientService = {
|
||||||
|
registerPatient: (payload: { email: string; full_name: string; phone_mobile: string; cpf: string; birth_date?: string }): Promise<RegisterPatientResponse> => {
|
||||||
|
return apiClient('/functions/v1/register-patient', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}, true) as Promise<RegisterPatientResponse>;
|
||||||
|
},
|
||||||
|
list: (filters: { fullName?: string; cpf?: string; limit?: number; offset?: number } = {}): Promise<ListResponse<Patient>> => {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (filters.fullName) query.set('full_name', `ilike.*${filters.fullName}*`);
|
||||||
|
if (filters.cpf) query.set('cpf', `eq.${filters.cpf}`);
|
||||||
|
if (filters.limit) query.set('limit', String(filters.limit));
|
||||||
|
if (filters.offset) query.set('offset', String(filters.offset));
|
||||||
|
return apiClient(`/rest/v1/patients?${query.toString()}`) as Promise<ListResponse<Patient>>;
|
||||||
|
},
|
||||||
|
create: (payload: Omit<Patient, 'id' | 'created_at' | 'updated_at'>): Promise<any> => {
|
||||||
|
return apiClient('/rest/v1/patients', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Prefer: 'return=representation' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SERVIÇOS DE MÉDICOS ---
|
||||||
|
export const doctorService = {
|
||||||
|
list: (filters: { specialty?: string; active?: boolean } = {}): Promise<ListResponse<Doctor>> => {
|
||||||
|
const query = new URLSearchParams({ select: '*' });
|
||||||
|
if (filters.specialty) query.set('specialty', `eq.${filters.specialty}`);
|
||||||
|
if (filters.active !== undefined) query.set('active', `eq.${filters.active}`);
|
||||||
|
return apiClient(`/rest/v1/doctors?${query.toString()}`) as Promise<ListResponse<Doctor>>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SERVIÇOS DE AGENDAMENTO E DISPONIBILIDADE ---
|
||||||
|
export const scheduleService = {
|
||||||
|
getAvailableSlots: (doctorId: string, date: string): Promise<GetAvailableSlotsResponse> => {
|
||||||
|
return apiClient('/functions/v1/get-available-slots', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ doctor_id: doctorId, date }),
|
||||||
|
}) as Promise<GetAvailableSlotsResponse>;
|
||||||
|
},
|
||||||
|
createAppointment: (payload: NewAppointmentPayload): Promise<CreateAppointmentResponse> => {
|
||||||
|
return apiClient('/rest/v1/appointments', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Prefer: 'return=representation' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}) as Promise<CreateAppointmentResponse>;
|
||||||
|
},
|
||||||
|
listAppointments: (filters: { doctorId?: string; patientId?: string; status?: string }): Promise<ListResponse<Appointment>> => {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (filters.doctorId) query.set('doctor_id', `eq.${filters.doctorId}`);
|
||||||
|
if (filters.patientId) query.set('patient_id', `eq.${filters.patientId}`);
|
||||||
|
if (filters.status) query.set('status', `eq.${filters.status}`);
|
||||||
|
return apiClient(`/rest/v1/appointments?${query.toString()}`) as Promise<ListResponse<Appointment>>;
|
||||||
|
},
|
||||||
|
cancelAppointment: (appointmentId: string, reason: string): Promise<CancelAppointmentResponse> => {
|
||||||
|
return apiClient(`/rest/v1/appointments?id=eq.${appointmentId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { Prefer: 'return=representation' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: 'cancelled',
|
||||||
|
cancellation_reason: reason,
|
||||||
|
cancelled_at: new Date().toISOString(),
|
||||||
|
}),
|
||||||
|
}) as Promise<CancelAppointmentResponse>;
|
||||||
|
},
|
||||||
|
listAvailability: (filters: { doctorId?: string } = {}): Promise<ListResponse<DoctorAvailability>> => {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (filters.doctorId) query.set('doctor_id', `eq.${filters.doctorId}`);
|
||||||
|
return apiClient(`/rest/v1/doctor_availability?${query.toString()}`) as Promise<ListResponse<DoctorAvailability>>;
|
||||||
|
},
|
||||||
|
listExceptions: (filters: { doctorId?: string; date?: string } = {}): Promise<ListResponse<DoctorException>> => {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
if (filters.doctorId) query.set('doctor_id', `eq.${filters.doctorId}`);
|
||||||
|
if (filters.date) query.set('date', `eq.${filters.date}`);
|
||||||
|
return apiClient(`/rest/v1/doctor_exceptions?${query.toString()}`) as Promise<ListResponse<DoctorException>>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SERVIÇOS DE LAUDOS (REPORTS) ---
|
||||||
|
export const reportService = {
|
||||||
|
list: (filters: { patientId?: string; createdBy?: string }): Promise<ListResponse<Report>> => {
|
||||||
|
const query = new URLSearchParams({ order: 'created_at.desc' });
|
||||||
|
if (filters.patientId) query.set('patient_id', `eq.${filters.patientId}`);
|
||||||
|
if (filters.createdBy) query.set('created_by', `eq.${filters.createdBy}`);
|
||||||
|
return apiClient(`/rest/v1/reports?${query.toString()}`) as Promise<ListResponse<Report>>;
|
||||||
|
},
|
||||||
|
create: (payload: ReportInput): Promise<CreateReportResponse> => {
|
||||||
|
return apiClient('/rest/v1/reports', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Prefer: 'return=representation' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}) as Promise<CreateReportResponse>;
|
||||||
|
},
|
||||||
|
update: (reportId: string, payload: Partial<ReportInput>): Promise<UpdateReportResponse> => {
|
||||||
|
return apiClient(`/rest/v1/reports?id=eq.${reportId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { Prefer: 'return=representation' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}) as Promise<UpdateReportResponse>;
|
||||||
|
},
|
||||||
|
};
|
||||||
61
services/api/apiTestData.ts
Normal file
61
services/api/apiTestData.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// services/api/apiTestData.ts
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Este arquivo centraliza todos os payloads de teste para o ApiVerificationPage.
|
||||||
|
* Cada objeto exportado contém cenários de sucesso e de erro para uma função específica.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// --- Autenticação ---
|
||||||
|
export const loginTestData = {
|
||||||
|
success: { email: 'riseup@popcode.com.br', password: 'riseup' },
|
||||||
|
error: { email: 'erro@popcode.com.br', password: 'senhaerrada' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const magicLinkTestData = {
|
||||||
|
success: { email: 'gabriel.doria@popcode.com.br' }, // Use um email real que você possa verificar
|
||||||
|
error: { email: 'emailinvalido' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Usuários ---
|
||||||
|
export const resetPassTestData = {
|
||||||
|
success: { email: 'gabriel.doria@popcode.com.br', redirectUrl: '' },
|
||||||
|
error: { email: 'emailinvalido', redirectUrl: '' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteUserTestData = {
|
||||||
|
success: { userId: 'uuid-de-um-usuario-para-deletar' }, // Substitua por um UUID real para testar
|
||||||
|
error: { userId: 'uuid-invalido' },
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Pacientes ---
|
||||||
|
export const registerPatientTestData = {
|
||||||
|
success: {
|
||||||
|
email: `paciente_${Date.now()}@teste.com`,
|
||||||
|
full_name: 'Paciente de Teste Válido',
|
||||||
|
phone_mobile: '11987654321',
|
||||||
|
cpf: '12345678901', // A API valida o formato, não a existência real
|
||||||
|
},
|
||||||
|
errorValidation: {
|
||||||
|
email: 'emailinvalido',
|
||||||
|
full_name: 'AB', // Nome curto
|
||||||
|
phone_mobile: '123', // Telefone curto
|
||||||
|
cpf: '111', // CPF curto
|
||||||
|
},
|
||||||
|
errorConflict: {
|
||||||
|
email: 'paciente_existente@teste.com', // Use um email que já exista no seu banco
|
||||||
|
full_name: 'Paciente Conflitante',
|
||||||
|
phone_mobile: '11987654321',
|
||||||
|
cpf: '11111111111', // Use um CPF que já exista
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listPatientsTestData = {
|
||||||
|
success: { fullName: 'Silva', limit: 5 },
|
||||||
|
noFilter: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Agendamentos ---
|
||||||
|
export const slotsTestData = {
|
||||||
|
success: { doctorId: 'uuid-de-um-medico-real', date: '2025-10-25' }, // Substitua pelo UUID de um médico
|
||||||
|
error: { doctorId: 'uuid-invalido', date: '2025-10-25' },
|
||||||
|
};
|
||||||
194
services/api/types.ts
Normal file
194
services/api/types.ts
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
// types.ts (V3)
|
||||||
|
|
||||||
|
// --- TIPOS DE MODELO DE DADOS (Schemas) ---
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthResponse {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfile {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
phone?: string;
|
||||||
|
roles: ('admin' | 'gestor' | 'medico' | 'secretaria' | 'paciente')[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Patient {
|
||||||
|
id: string;
|
||||||
|
full_name: string;
|
||||||
|
cpf: string;
|
||||||
|
email: string;
|
||||||
|
phone_mobile: string;
|
||||||
|
birth_date?: string;
|
||||||
|
created_by: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Doctor {
|
||||||
|
id: string;
|
||||||
|
full_name: string;
|
||||||
|
crm: string;
|
||||||
|
crm_uf: string;
|
||||||
|
specialty?: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Appointment {
|
||||||
|
id: string;
|
||||||
|
doctor_id: string;
|
||||||
|
patient_id: string;
|
||||||
|
scheduled_at: string;
|
||||||
|
duration_minutes: number;
|
||||||
|
status: 'requested' | 'confirmed' | 'completed' | 'cancelled';
|
||||||
|
created_by: string;
|
||||||
|
cancelled_at?: string;
|
||||||
|
cancellation_reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NewAppointmentPayload = Omit<Appointment, 'id' | 'status'> & {
|
||||||
|
status?: 'requested' | 'confirmed';
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AvailableSlot {
|
||||||
|
time: string;
|
||||||
|
available: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Report {
|
||||||
|
id: string;
|
||||||
|
order_number: string;
|
||||||
|
patient_id: string;
|
||||||
|
status: 'draft' | 'completed';
|
||||||
|
exam?: string;
|
||||||
|
requested_by?: string;
|
||||||
|
cid_code?: string;
|
||||||
|
diagnosis?: string;
|
||||||
|
conclusion?: string;
|
||||||
|
content_html?: string;
|
||||||
|
content_json?: object;
|
||||||
|
hide_date: boolean;
|
||||||
|
hide_signature: boolean;
|
||||||
|
due_at?: string;
|
||||||
|
created_by: string;
|
||||||
|
updated_by: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReportInput = Partial<Omit<Report, 'id' | 'order_number' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'>> & {
|
||||||
|
patient_id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DoctorAvailability {
|
||||||
|
id: string;
|
||||||
|
doctor_id: string;
|
||||||
|
weekday: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
|
start_time: string;
|
||||||
|
end_time: string;
|
||||||
|
slot_minutes: number;
|
||||||
|
appointment_type: 'presencial' | 'telemedicina';
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DoctorException {
|
||||||
|
id: string;
|
||||||
|
doctor_id: string;
|
||||||
|
date: string;
|
||||||
|
kind: 'bloqueio' | 'disponibilidade_extra';
|
||||||
|
start_time?: string | null;
|
||||||
|
end_time?: string | null;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- TIPOS DE RESPOSTA DA API (UNIÕES DISCRIMINADAS) ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erro genérico para falhas de rede (CORS, offline, etc.).
|
||||||
|
*/
|
||||||
|
export type NetworkError = { status: 'network_error'; error: Error };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tipo base para respostas de erro da API com um corpo JSON.
|
||||||
|
*/
|
||||||
|
export type ApiErrorResponse<Status extends number, Data> = { status: Status; data: Data };
|
||||||
|
|
||||||
|
// Respostas para: authService.login
|
||||||
|
export type LoginSuccess = { status: 200; data: AuthResponse };
|
||||||
|
export type LoginError400 = ApiErrorResponse<400, { error: string; error_description: string }>;
|
||||||
|
export type LoginResponse = LoginSuccess | LoginError400 | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: authService.sendMagicLink
|
||||||
|
export type SendMagicLinkSuccess = { status: 200; data: object };
|
||||||
|
export type SendMagicLinkError429 = ApiErrorResponse<429, { message: string }>;
|
||||||
|
export type SendMagicLinkResponse = SendMagicLinkSuccess | SendMagicLinkError429 | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: authService.logout
|
||||||
|
export type LogoutSuccess = { status: 204; data: undefined };
|
||||||
|
export type LogoutError401 = ApiErrorResponse<401, { message: string }>;
|
||||||
|
export type LogoutResponse = LogoutSuccess | LogoutError401 | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: authService.getCurrentUser
|
||||||
|
export type GetCurrentUserSuccess = { status: 200; data: UserProfile };
|
||||||
|
export type GetCurrentUserError401 = ApiErrorResponse<401, { message: string }>;
|
||||||
|
export type GetCurrentUserResponse = GetCurrentUserSuccess | GetCurrentUserError401 | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: userService.requestPasswordReset
|
||||||
|
export type RequestPasswordResetSuccess = { status: 200; data: { success: boolean; message: string } };
|
||||||
|
export type RequestPasswordResetError400 = ApiErrorResponse<400, { detail: string }>;
|
||||||
|
export type RequestPasswordResetResponse = RequestPasswordResetSuccess | RequestPasswordResetError400 | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: userService.createUserWithPassword
|
||||||
|
export type CreateUserWithPasswordSuccess = { status: 201; data: { success: boolean; user: UserProfile; patient_id?: string } };
|
||||||
|
export type CreateUserWithPasswordError400 = ApiErrorResponse<400, { error: string }>;
|
||||||
|
export type CreateUserWithPasswordError403 = ApiErrorResponse<403, { error: string }>;
|
||||||
|
export type CreateUserWithPasswordResponse = CreateUserWithPasswordSuccess | CreateUserWithPasswordError400 | CreateUserWithPasswordError403 | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: userService.hardDeleteUser_DANGEROUS
|
||||||
|
export type HardDeleteUserSuccess = { status: 200; data: { success: boolean; message: string; userId: string } };
|
||||||
|
export type HardDeleteUserError400 = ApiErrorResponse<400, { error: string }>;
|
||||||
|
export type HardDeleteUserError403 = ApiErrorResponse<403, { error: string }>;
|
||||||
|
export type HardDeleteUserResponse = HardDeleteUserSuccess | HardDeleteUserError400 | HardDeleteUserError403 | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: patientService.registerPatient
|
||||||
|
export type RegisterPatientSuccess = { status: 200; data: { success: boolean; patient_id: string; message: string } };
|
||||||
|
export type RegisterPatientError400 = ApiErrorResponse<400, { error: string; code: 'VALIDATION_ERROR'; details?: any[] }>;
|
||||||
|
export type RegisterPatientError409 = ApiErrorResponse<409, { error: string; code: 'CPF_EXISTS' | 'EMAIL_EXISTS' }>;
|
||||||
|
export type RegisterPatientError429 = ApiErrorResponse<429, { error: string; code: 'RATE_LIMIT_EXCEEDED' }>;
|
||||||
|
export type RegisterPatientError500 = ApiErrorResponse<500, { error: string; code: string }>;
|
||||||
|
export type RegisterPatientResponse = RegisterPatientSuccess | RegisterPatientError400 | RegisterPatientError409 | RegisterPatientError429 | RegisterPatientError500 | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: scheduleService.getAvailableSlots
|
||||||
|
export type GetAvailableSlotsSuccess = { status: 200; data: { slots: AvailableSlot[] } };
|
||||||
|
export type GetAvailableSlotsResponse = GetAvailableSlotsSuccess | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: scheduleService.createAppointment
|
||||||
|
export type CreateAppointmentSuccess = { status: 201; data: Appointment };
|
||||||
|
export type CreateAppointmentResponse = CreateAppointmentSuccess | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: scheduleService.cancelAppointment
|
||||||
|
export type CancelAppointmentSuccess = { status: 200; data: Appointment };
|
||||||
|
export type CancelAppointmentResponse = CancelAppointmentSuccess | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: reportService.create
|
||||||
|
export type CreateReportSuccess = { status: 201; data: Report };
|
||||||
|
export type CreateReportResponse = CreateReportSuccess | NetworkError;
|
||||||
|
|
||||||
|
// Respostas para: reportService.update
|
||||||
|
export type UpdateReportSuccess = { status: 200; data: Report };
|
||||||
|
export type UpdateReportResponse = UpdateReportSuccess | NetworkError;
|
||||||
|
|
||||||
|
// Tipos genéricos para listagens simples
|
||||||
|
export type ListSuccess<T> = { status: 200; data: T[] };
|
||||||
|
export type ListResponse<T> = ListSuccess<T> | NetworkError;
|
||||||
@ -1,37 +1,89 @@
|
|||||||
// Caminho: services/disponibilidadeApi.ts
|
// Caminho: services/disponibilidadeApi.ts (Completo e Corrigido)
|
||||||
|
|
||||||
import api from './api';
|
import api from './api';
|
||||||
|
|
||||||
export interface Availability {
|
// --- Tipagem para Disponibilidade Semanal ---
|
||||||
id: any;
|
|
||||||
|
export interface DoctorAvailability {
|
||||||
|
id: string;
|
||||||
doctor_id: string;
|
doctor_id: string;
|
||||||
weekday: string;
|
weekday: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||||
start_time: string;
|
start_time: string;
|
||||||
end_time: string;
|
end_time: string;
|
||||||
[key: string]: any;
|
slot_minutes: number;
|
||||||
|
appointment_type: 'presencial' | 'telemedicina';
|
||||||
|
active: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
created_by: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const disponibilidadeApi = {
|
export type CreateDoctorAvailabilityData = Omit<DoctorAvailability, 'id' | 'created_at' | 'updated_at'>;
|
||||||
list: async (): Promise<Availability[]> => {
|
export type UpdateDoctorAvailabilityData = Partial<Omit<DoctorAvailability, 'id' | 'created_at' | 'updated_at' | 'created_by'>>;
|
||||||
const response = await api.get<Availability[]>('/rest/v1/doctor_availability?select=*');
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getById: async (id: string): Promise<Availability> => {
|
interface ListAvailabilityParams {
|
||||||
const response = await api.get<Availability>(`/rest/v1/doctor_availability?id=eq.${id}&select=*`, {
|
doctor_id?: string;
|
||||||
headers: { Accept: 'application/vnd.pgrst.object+json' },
|
weekday?: number;
|
||||||
|
active?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tipagem para Exceções de Agenda ---
|
||||||
|
|
||||||
|
export interface DoctorException {
|
||||||
|
id: string;
|
||||||
|
doctor_id: string;
|
||||||
|
date: string;
|
||||||
|
kind: 'bloqueio' | 'disponibilidade_extra';
|
||||||
|
start_time: string | null;
|
||||||
|
end_time: string | null;
|
||||||
|
reason?: string;
|
||||||
|
created_at: string;
|
||||||
|
created_by: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CreateDoctorExceptionData = Omit<DoctorException, 'id' | 'created_at'>;
|
||||||
|
|
||||||
|
interface ListExceptionParams {
|
||||||
|
doctor_id?: string;
|
||||||
|
date?: string;
|
||||||
|
kind?: 'bloqueio' | 'disponibilidade_extra';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Objeto do Serviço de API ---
|
||||||
|
|
||||||
|
export const disponibilidadeApi = {
|
||||||
|
// --- Métodos para Disponibilidade Semanal ---
|
||||||
|
|
||||||
|
list: async (params: ListAvailabilityParams = {}): Promise<DoctorAvailability[]> => {
|
||||||
|
// CORREÇÃO: Formata os parâmetros para o padrão do Supabase
|
||||||
|
const queryParams: { [key: string]: string } = { select: '*' };
|
||||||
|
if (params.doctor_id) queryParams.doctor_id = `eq.${params.doctor_id}`;
|
||||||
|
if (params.weekday !== undefined) queryParams.weekday = `eq.${params.weekday}`;
|
||||||
|
if (params.active !== undefined) queryParams.active = `eq.${params.active}`;
|
||||||
|
|
||||||
|
const response = await api.get<DoctorAvailability[]>('/rest/v1/doctor_availability', {
|
||||||
|
params: queryParams,
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async (data: Omit<Availability, 'id'>): Promise<Availability> => {
|
getById: async (id: string): Promise<DoctorAvailability> => {
|
||||||
const response = await api.post<Availability[]>('/rest/v1/doctor_availability', data, {
|
const response = await api.get<DoctorAvailability[]>(`/rest/v1/doctor_availability?id=eq.${id}&select=*`);
|
||||||
|
if (response.data && response.data.length > 0) {
|
||||||
|
return response.data[0];
|
||||||
|
}
|
||||||
|
throw new Error("Registro de disponibilidade não encontrado.");
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (data: CreateDoctorAvailabilityData): Promise<DoctorAvailability> => {
|
||||||
|
const response = await api.post<DoctorAvailability[]>('/rest/v1/doctor_availability', data, {
|
||||||
headers: { 'Prefer': 'return=representation' }
|
headers: { 'Prefer': 'return=representation' }
|
||||||
});
|
});
|
||||||
return response.data[0];
|
return response.data[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (id: string, data: Partial<Availability>): Promise<Availability> => {
|
update: async (id: string, data: UpdateDoctorAvailabilityData): Promise<DoctorAvailability> => {
|
||||||
const response = await api.patch<Availability[]>(`/rest/v1/doctor_availability?id=eq.${id}`, data, {
|
const response = await api.patch<DoctorAvailability[]>(`/rest/v1/doctor_availability?id=eq.${id}`, data, {
|
||||||
headers: { 'Prefer': 'return=representation' }
|
headers: { 'Prefer': 'return=representation' }
|
||||||
});
|
});
|
||||||
return response.data[0];
|
return response.data[0];
|
||||||
@ -40,4 +92,28 @@ export const disponibilidadeApi = {
|
|||||||
delete: async (id: string): Promise<void> => {
|
delete: async (id: string): Promise<void> => {
|
||||||
await api.delete(`/rest/v1/doctor_availability?id=eq.${id}`);
|
await api.delete(`/rest/v1/doctor_availability?id=eq.${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- Métodos para Exceções de Agenda ---
|
||||||
|
|
||||||
|
listExceptions: async (params: ListExceptionParams = {}): Promise<DoctorException[]> => {
|
||||||
|
// CORREÇÃO: Formata os parâmetros para o padrão do Supabase
|
||||||
|
const queryParams: { [key: string]: string } = { select: '*' };
|
||||||
|
if (params.doctor_id) queryParams.doctor_id = `eq.${params.doctor_id}`;
|
||||||
|
if (params.date) queryParams.date = `eq.${params.date}`;
|
||||||
|
if (params.kind) queryParams.kind = `eq.${params.kind}`;
|
||||||
|
|
||||||
|
const response = await api.get<DoctorException[]>('/rest/v1/doctor_exceptions', {
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
createException: async (data: CreateDoctorExceptionData): Promise<DoctorException> => {
|
||||||
|
const response = await api.post<DoctorException[]>('/rest/v1/doctor_exceptions', data, {
|
||||||
|
headers: {
|
||||||
|
'Prefer': 'return=representation',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data[0];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
@ -1,41 +1,87 @@
|
|||||||
// Caminho: services/medicosApi.ts
|
// Caminho: services/medicosApi.ts (Corrigido)
|
||||||
|
|
||||||
import api from './api';
|
import api from './api';
|
||||||
|
|
||||||
export interface Doctor {
|
export interface Doctor {
|
||||||
id: any;
|
id: string;
|
||||||
|
full_name: string;
|
||||||
crm: string;
|
crm: string;
|
||||||
crm_uf: string;
|
crm_uf: string;
|
||||||
[key: string]: any;
|
specialty: string;
|
||||||
|
active?: boolean;
|
||||||
|
location?: string;
|
||||||
|
phone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDoctorData {
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
cpf: string;
|
||||||
|
crm: string;
|
||||||
|
crm_uf: string;
|
||||||
|
specialty?: string;
|
||||||
|
phone_mobile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListParams {
|
||||||
|
active?: boolean;
|
||||||
|
specialty?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const medicosApi = {
|
export const medicosApi = {
|
||||||
list: async (): Promise<Doctor[]> => {
|
/**
|
||||||
const response = await api.get<Doctor[]>('/rest/v1/doctors?select=*');
|
* Lista médicos com filtros opcionais.
|
||||||
|
* @param params - Objeto com filtros como { active: true }
|
||||||
|
*/
|
||||||
|
list: async (params: ListParams = {}): Promise<Doctor[]> => {
|
||||||
|
// Prepara os parâmetros para o formato do PostgREST
|
||||||
|
const queryParams: { [key: string]: any } = {
|
||||||
|
select: '*',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.active !== undefined) {
|
||||||
|
queryParams.active = `eq.${params.active}`; // CORREÇÃO: Usa 'eq.true' ou 'eq.false'
|
||||||
|
}
|
||||||
|
if (params.specialty) {
|
||||||
|
queryParams.specialty = `eq.${params.specialty}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get<Doctor[]>('/rest/v1/doctors', {
|
||||||
|
params: queryParams,
|
||||||
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cria um novo médico com validações através da Edge Function.
|
||||||
|
* @param data - Dados do médico a ser criado.
|
||||||
|
*/
|
||||||
|
createWithValidation: async (data: CreateDoctorData): Promise<any> => {
|
||||||
|
const response = await api.post('/functions/v1/create-doctor', data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
getById: async (id: string): Promise<Doctor> => {
|
getById: async (id: string): Promise<Doctor> => {
|
||||||
const response = await api.get<Doctor>(`/rest/v1/doctors?id=eq.${id}&select=*`, {
|
const response = await api.get<Doctor[]>(`/rest/v1/doctors?id=eq.${id}&select=*`);
|
||||||
headers: { Accept: 'application/vnd.pgrst.object+json' },
|
if (response.data.length === 0) {
|
||||||
});
|
throw new Error("Médico não encontrado");
|
||||||
return response.data;
|
}
|
||||||
},
|
|
||||||
|
|
||||||
create: async (data: Omit<Doctor, 'id'>): Promise<Doctor> => {
|
|
||||||
const response = await api.post<Doctor[]>('/rest/v1/doctors', data, {
|
|
||||||
headers: { 'Prefer': 'return=representation' }
|
|
||||||
});
|
|
||||||
return response.data[0];
|
return response.data[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (id: string, data: Partial<Doctor>): Promise<Doctor> => {
|
update: async (id: string, data: Partial<Doctor>): Promise<Doctor> => {
|
||||||
const response = await api.patch<Doctor[]>(`/rest/v1/doctors?id=eq.${id}`, data, {
|
const response = await api.patch<Doctor[]>(`/rest/v1/doctors?id=eq.${id}`, data, {
|
||||||
headers: { 'Prefer': 'return=representation' }
|
headers: { 'Prefer': 'return=representation' },
|
||||||
});
|
});
|
||||||
return response.data[0];
|
return response.data[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (id: string): Promise<void> => {
|
/**
|
||||||
await api.delete(`/rest/v1/doctors?id=eq.${id}`);
|
* Busca uma lista de médicos de exemplo do endpoint de mock.
|
||||||
|
* Usado como fallback quando a API real não retorna dados.
|
||||||
|
*/
|
||||||
|
getMockDoctors: async (): Promise<Doctor[]> => {
|
||||||
|
const response = await api.get<Doctor[]>('https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctors');
|
||||||
|
return Array.isArray(response.data) ? response.data : [response.data];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -1,36 +1,79 @@
|
|||||||
// Caminho: services/pacientesApi.ts
|
// Caminho: services/pacientesApi.ts (Completo e Corrigido)
|
||||||
|
|
||||||
import api from './api';
|
import api from './api';
|
||||||
|
|
||||||
export interface Patient {
|
export interface Patient {
|
||||||
id: any;
|
id: string;
|
||||||
full_name: string;
|
nome_completo: string;
|
||||||
cpf: string;
|
cpf: string;
|
||||||
|
email?: string;
|
||||||
|
telefone?: string;
|
||||||
|
data_nascimento?: string;
|
||||||
|
endereco?: string;
|
||||||
|
cidade?: string;
|
||||||
|
estado?: string;
|
||||||
|
cep?: string;
|
||||||
|
convenio?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pacientesApi = {
|
export type CreatePatientData = Omit<Patient, 'id' | 'created_at' | 'updated_at'>;
|
||||||
list: async (): Promise<Patient[]> => {
|
export type UpdatePatientData = Partial<CreatePatientData>;
|
||||||
const response = await api.get<Patient[]>('/rest/v1/patients?select=*');
|
|
||||||
return response.data;
|
|
||||||
},
|
|
||||||
|
|
||||||
getById: async (id: string): Promise<Patient> => {
|
interface ListParams {
|
||||||
const response = await api.get<Patient>(`/rest/v1/patients?id=eq.${id}&select=*`, {
|
limit?: number;
|
||||||
headers: { Accept: 'application/vnd.pgrst.object+json' },
|
offset?: number;
|
||||||
|
order?: string;
|
||||||
|
nome_completo?: string;
|
||||||
|
cpf?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pacientesApi = {
|
||||||
|
list: async (params: ListParams = {}): Promise<Patient[]> => {
|
||||||
|
const response = await api.get<Patient[]>('/rest/v1/patients', {
|
||||||
|
params: {
|
||||||
|
select: '*',
|
||||||
|
...params,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async (data: Omit<Patient, 'id'>): Promise<Patient> => {
|
/**
|
||||||
|
* Busca um único paciente pelo seu ID.
|
||||||
|
* @param id - O UUID do paciente.
|
||||||
|
*/
|
||||||
|
getById: async (id: string): Promise<Patient> => {
|
||||||
|
const response = await api.get<Patient[]>(`/rest/v1/patients`, {
|
||||||
|
params: {
|
||||||
|
id: `eq.${id}`,
|
||||||
|
select: '*',
|
||||||
|
},
|
||||||
|
// O header 'Accept' foi REMOVIDO para evitar o erro 406.
|
||||||
|
// A resposta será um array, então pegamos o primeiro item.
|
||||||
|
});
|
||||||
|
if (response.data && response.data.length > 0) {
|
||||||
|
return response.data[0];
|
||||||
|
}
|
||||||
|
throw new Error("Paciente não encontrado");
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (data: CreatePatientData): Promise<Patient> => {
|
||||||
const response = await api.post<Patient[]>('/rest/v1/patients', data, {
|
const response = await api.post<Patient[]>('/rest/v1/patients', data, {
|
||||||
headers: { 'Prefer': 'return=representation' }
|
headers: {
|
||||||
|
'Prefer': 'return=representation',
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return response.data[0];
|
return response.data[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
update: async (id: string, data: Partial<Patient>): Promise<Patient> => {
|
update: async (id: string, data: UpdatePatientData): Promise<Patient> => {
|
||||||
const response = await api.patch<Patient[]>(`/rest/v1/patients?id=eq.${id}`, data, {
|
const response = await api.patch<Patient[]>(`/rest/v1/patients?id=eq.${id}`, data, {
|
||||||
headers: { 'Prefer': 'return=representation' }
|
headers: {
|
||||||
|
'Prefer': 'return=representation',
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return response.data[0];
|
return response.data[0];
|
||||||
},
|
},
|
||||||
@ -38,4 +81,12 @@ export const pacientesApi = {
|
|||||||
delete: async (id: string): Promise<void> => {
|
delete: async (id: string): Promise<void> => {
|
||||||
await api.delete(`/rest/v1/patients?id=eq.${id}`);
|
await api.delete(`/rest/v1/patients?id=eq.${id}`);
|
||||||
},
|
},
|
||||||
|
getMockPatient: async (): Promise<Patient> => {
|
||||||
|
// Usamos uma instância separada do axios ou o `api.get` com a URL completa
|
||||||
|
// para chamar um domínio diferente.
|
||||||
|
const response = await api.get<Patient>('https://mock.apidog.com/m1/1053378-0-default/rest/v1/patients');
|
||||||
|
// Assumindo que o mock retorna um array, pegamos o primeiro item.
|
||||||
|
// Se retornar um objeto direto, seria apenas `response.data`.
|
||||||
|
return Array.isArray(response.data) ? response.data[0] : response.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user