forked from RiseUP/riseup-squad21
adicionando calendário para as consultas
This commit is contained in:
parent
4af7c35f73
commit
452d4147dd
@ -5,60 +5,94 @@ import { useState, useEffect } from "react";
|
||||
import DoctorLayout from "@/components/doctor-layout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Clock, Calendar, MapPin, Phone, User, X, RefreshCw } from "lucide-react";
|
||||
import { Clock, Calendar as CalendarIcon, MapPin, Phone, User, X, RefreshCw } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// IMPORTAR O COMPONENTE CALENDÁRIO DA SHADCN
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { format } from "date-fns"; // Usaremos o date-fns para formatação e comparação de datas
|
||||
|
||||
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
|
||||
|
||||
// --- TIPAGEM DA CONSULTA SALVA NO LOCALSTORAGE ---
|
||||
// Reflete a estrutura salva pelo secretarypage.tsx
|
||||
interface LocalStorageAppointment {
|
||||
id: number; // ID único simples (timestamp)
|
||||
id: number;
|
||||
patientName: string;
|
||||
doctor: string; // Nome completo do médico (para filtrar)
|
||||
doctor: string;
|
||||
specialty: string;
|
||||
date: string; // Data no formato YYYY-MM-DD
|
||||
time: string; // Hora no formato HH:MM
|
||||
date: string; // Data no formato YYYY-MM-DD
|
||||
time: string; // Hora no formato HH:MM
|
||||
status: "agendada" | "confirmada" | "cancelada" | "realizada";
|
||||
location: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
// --- SIMULAÇÃO DO MÉDICO LOGADO ---
|
||||
// **IMPORTANTE**: Em um ambiente real, este valor viria do seu sistema de autenticação.
|
||||
// Use um nome que corresponda a um médico que você cadastrou e usou para agendar.
|
||||
const LOGGED_IN_DOCTOR_NAME = "Dr. João Silva"; // <--- AJUSTE ESTE NOME PARA TESTAR
|
||||
const LOGGED_IN_DOCTOR_NAME = "Dr. João Santos";
|
||||
|
||||
// Função auxiliar para comparar se duas datas (Date objects) são o mesmo dia
|
||||
const isSameDay = (date1: Date, date2: Date) => {
|
||||
return date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate();
|
||||
};
|
||||
|
||||
// --- COMPONENTE PRINCIPAL ---
|
||||
|
||||
export default function DoctorAppointmentsPage() {
|
||||
const [appointments, setAppointments] = useState<LocalStorageAppointment[]>([]);
|
||||
const [allAppointments, setAllAppointments] = useState<LocalStorageAppointment[]>([]);
|
||||
const [filteredAppointments, setFilteredAppointments] = useState<LocalStorageAppointment[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// NOVO ESTADO 1: Armazena os dias com consultas (para o calendário)
|
||||
const [bookedDays, setBookedDays] = useState<Date[]>([]);
|
||||
|
||||
// NOVO ESTADO 2: Armazena a data selecionada no calendário
|
||||
const [selectedCalendarDate, setSelectedCalendarDate] = useState<Date | undefined>(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
loadAppointments();
|
||||
}, []);
|
||||
|
||||
// Efeito para filtrar a lista sempre que o calendário ou a lista completa for atualizada
|
||||
useEffect(() => {
|
||||
if (selectedCalendarDate) {
|
||||
const dateString = format(selectedCalendarDate, 'yyyy-MM-dd');
|
||||
|
||||
// Filtra a lista completa de agendamentos pela data selecionada
|
||||
const todayAppointments = allAppointments
|
||||
.filter(app => app.date === dateString)
|
||||
.sort((a, b) => a.time.localeCompare(b.time)); // Ordena por hora
|
||||
|
||||
setFilteredAppointments(todayAppointments);
|
||||
} else {
|
||||
// Se nenhuma data estiver selecionada (ou se for limpa), mostra todos (ou os de hoje)
|
||||
const todayDateString = format(new Date(), 'yyyy-MM-dd');
|
||||
const todayAppointments = allAppointments
|
||||
.filter(app => app.date === todayDateString)
|
||||
.sort((a, b) => a.time.localeCompare(b.time));
|
||||
|
||||
setFilteredAppointments(todayAppointments);
|
||||
}
|
||||
}, [allAppointments, selectedCalendarDate]);
|
||||
|
||||
const loadAppointments = () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY);
|
||||
const allAppointments: LocalStorageAppointment[] = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : [];
|
||||
const allAppts: LocalStorageAppointment[] = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : [];
|
||||
|
||||
// 1. FILTRAGEM CRÍTICA: Apenas as consultas para o médico logado
|
||||
const filteredAppointments = allAppointments.filter(
|
||||
(app) => app.doctor === LOGGED_IN_DOCTOR_NAME
|
||||
);
|
||||
// ***** NENHUM FILTRO POR MÉDICO AQUI (Como solicitado) *****
|
||||
const appointmentsToShow = allAppts;
|
||||
|
||||
// 2. Ordena por Data e Hora
|
||||
filteredAppointments.sort((a, b) => {
|
||||
const dateTimeA = new Date(`${a.date}T${a.time}:00`);
|
||||
const dateTimeB = new Date(`${b.date}T${b.time}:00`);
|
||||
return dateTimeA.getTime() - dateTimeB.getTime();
|
||||
});
|
||||
// 1. EXTRAI E PREPARA AS DATAS PARA O CALENDÁRIO
|
||||
const uniqueBookedDates = Array.from(new Set(appointmentsToShow.map(app => app.date)));
|
||||
|
||||
// Converte YYYY-MM-DD para objetos Date, garantindo que o tempo seja meia-noite (00:00:00)
|
||||
const dateObjects = uniqueBookedDates.map(dateString => new Date(dateString + 'T00:00:00'));
|
||||
|
||||
setAppointments(filteredAppointments);
|
||||
setAllAppointments(appointmentsToShow);
|
||||
setBookedDays(dateObjects);
|
||||
toast.success("Agenda atualizada com sucesso!");
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar a agenda do LocalStorage:", error);
|
||||
@ -68,8 +102,8 @@ export default function DoctorAppointmentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Função utilitária para mapear o status para a cor da Badge
|
||||
const getStatusVariant = (status: LocalStorageAppointment['status']) => {
|
||||
// ... (código mantido)
|
||||
switch (status) {
|
||||
case "confirmada":
|
||||
case "agendada":
|
||||
@ -84,117 +118,153 @@ export default function DoctorAppointmentsPage() {
|
||||
};
|
||||
|
||||
const handleCancel = (id: number) => {
|
||||
// Lógica para CANCELAR a consulta no LocalStorage
|
||||
// ... (código mantido para cancelamento)
|
||||
const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY);
|
||||
const allAppointments: LocalStorageAppointment[] = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : [];
|
||||
const allAppts: LocalStorageAppointment[] = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : [];
|
||||
|
||||
const updatedAppointments = allAppointments.map(app =>
|
||||
const updatedAppointments = allAppts.map(app =>
|
||||
app.id === id ? { ...app, status: "cancelada" as const } : app
|
||||
);
|
||||
|
||||
localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments));
|
||||
loadAppointments(); // Recarrega a lista filtrada
|
||||
loadAppointments();
|
||||
toast.info(`Consulta cancelada com sucesso.`);
|
||||
};
|
||||
|
||||
const handleReSchedule = (id: number) => {
|
||||
// Aqui você navegaria para a tela de agendamento passando o ID para pré-preencher
|
||||
toast.info(`Reagendamento da Consulta ID: ${id}. Navegar para a página de agendamento.`);
|
||||
};
|
||||
|
||||
const displayDate = selectedCalendarDate ?
|
||||
new Date(selectedCalendarDate).toLocaleDateString("pt-BR", {weekday: 'long', day: '2-digit', month: 'long'}) :
|
||||
"Selecione uma data";
|
||||
|
||||
|
||||
return (
|
||||
<DoctorLayout>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Minhas Consultas</h1>
|
||||
<p className="text-gray-600">Agenda atual ({LOGGED_IN_DOCTOR_NAME}) e histórico de atendimentos</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Agenda Médica Centralizada</h1>
|
||||
<p className="text-gray-600">Todas as consultas do sistema são exibidas aqui ({LOGGED_IN_DOCTOR_NAME})</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-xl font-semibold">Consultas para: {displayDate}</h2>
|
||||
<Button onClick={loadAppointments} disabled={isLoading} variant="outline" size="sm">
|
||||
<RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||
Atualizar Agenda
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{isLoading ? (
|
||||
<p className="text-center text-lg text-gray-500">Carregando a agenda...</p>
|
||||
) : appointments.length === 0 ? (
|
||||
<p className="text-center text-lg text-gray-500">Nenhuma consulta agendada para você (Médico: {LOGGED_IN_DOCTOR_NAME}).</p>
|
||||
) : (
|
||||
appointments.map((appointment) => {
|
||||
// Formatação de data e hora
|
||||
const showActions = appointment.status === "agendada" || appointment.status === "confirmada";
|
||||
{/* NOVO LAYOUT DE DUAS COLUNAS */}
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
|
||||
{/* COLUNA 1: CALENDÁRIO */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center">
|
||||
<CalendarIcon className="mr-2 h-5 w-5" />
|
||||
Calendário
|
||||
</CardTitle>
|
||||
<p className="text-sm text-gray-500">Dias em azul possuem agendamentos.</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex justify-center p-2">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedCalendarDate}
|
||||
onSelect={setSelectedCalendarDate}
|
||||
initialFocus
|
||||
// A CHAVE DO HIGHLIGHT: Passa o array de datas agendadas
|
||||
modifiers={{ booked: bookedDays }}
|
||||
// Define o estilo CSS para o modificador 'booked'
|
||||
modifiersClassNames={{
|
||||
booked: "bg-blue-600 text-white aria-selected:!bg-blue-700 hover:!bg-blue-700/90"
|
||||
}}
|
||||
className="rounded-md border p-2"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
return (
|
||||
<Card key={appointment.id} className="shadow-lg">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
{/* NOME DO PACIENTE */}
|
||||
<CardTitle className="text-xl font-semibold flex items-center">
|
||||
<User className="mr-2 h-5 w-5 text-blue-600" />
|
||||
{appointment.patientName}
|
||||
</CardTitle>
|
||||
{/* STATUS DA CONSULTA */}
|
||||
<Badge variant={getStatusVariant(appointment.status)} className="uppercase">
|
||||
{appointment.status}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
{/* COLUNA 2: LISTA DE CONSULTAS FILTRADAS */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
{isLoading ? (
|
||||
<p className="text-center text-lg text-gray-500">Carregando a agenda...</p>
|
||||
) : filteredAppointments.length === 0 ? (
|
||||
<p className="text-center text-lg text-gray-500">Nenhuma consulta encontrada para a data selecionada.</p>
|
||||
) : (
|
||||
filteredAppointments.map((appointment) => {
|
||||
const showActions = appointment.status === "agendada" || appointment.status === "confirmada";
|
||||
|
||||
<CardContent className="grid md:grid-cols-3 gap-4 pt-4">
|
||||
{/* COLUNA 1: Data e Hora */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<Calendar className="mr-2 h-4 w-4 text-gray-500" />
|
||||
{new Date(appointment.date).toLocaleDateString("pt-BR", { timeZone: "UTC" })}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<Clock className="mr-2 h-4 w-4 text-gray-500" />
|
||||
{appointment.time}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<Card key={appointment.id} className="shadow-lg">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-xl font-semibold flex items-center">
|
||||
<User className="mr-2 h-5 w-5 text-blue-600" />
|
||||
{appointment.patientName}
|
||||
</CardTitle>
|
||||
<Badge variant={getStatusVariant(appointment.status)} className="uppercase">
|
||||
{appointment.status}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
{/* COLUNA 2: Local e Contato */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<MapPin className="mr-2 h-4 w-4 text-gray-500" />
|
||||
{appointment.location}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<Phone className="mr-2 h-4 w-4 text-gray-500" />
|
||||
{/* Note: O telefone do paciente não está salvo no LocalStorage no seu código atual, usando um valor fixo */}
|
||||
{(appointment.phone || "(11) 9XXXX-YYYY")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* COLUNA 3: Ações (Botões) */}
|
||||
<div className="flex flex-col justify-center items-end">
|
||||
{showActions && (
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleReSchedule(appointment.id)}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Reagendar
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleCancel(appointment.id)}
|
||||
>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Cancelar
|
||||
</Button>
|
||||
<CardContent className="grid md:grid-cols-3 gap-4 pt-4">
|
||||
{/* Detalhes e Ações... (mantidos) */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<User className="mr-2 h-4 w-4 text-gray-500" />
|
||||
<span className="font-semibold">Médico:</span> {appointment.doctor}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<CalendarIcon className="mr-2 h-4 w-4 text-gray-500" />
|
||||
{new Date(appointment.date).toLocaleDateString("pt-BR", { timeZone: "UTC" })}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<Clock className="mr-2 h-4 w-4 text-gray-500" />
|
||||
{appointment.time}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<MapPin className="mr-2 h-4 w-4 text-gray-500" />
|
||||
{appointment.location}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<Phone className="mr-2 h-4 w-4 text-gray-500" />
|
||||
{appointment.phone || "N/A"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col justify-center items-end">
|
||||
{showActions && (
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleReSchedule(appointment.id)}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Reagendar
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleCancel(appointment.id)}
|
||||
>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DoctorLayout>
|
||||
|
||||
@ -122,24 +122,4 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-picker::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.color-picker::-webkit-color-swatch {
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
}
|
||||
@ -1,135 +1,96 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import type React from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import type React from "react"
|
||||
import { useState } from "react"
|
||||
// Importações de componentes omitidas para brevidade, mas estão no código original
|
||||
import PatientLayout from "@/components/patient-layout"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Calendar, Clock, User } from "lucide-react"
|
||||
|
||||
// [SINCRONIZAÇÃO 1] - Importando a lista de 'appointments' para a validação de conflito
|
||||
import { useAppointments } from "../../context/AppointmentsContext";
|
||||
// Chave do LocalStorage, a mesma usada em secretarypage.tsx
|
||||
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
|
||||
|
||||
// Componentes de UI e Layout
|
||||
import PatientLayout from "@/components/patient-layout";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Calendar, Clock, User } from "lucide-react";
|
||||
import { doctorsService } from "services/doctorsApi.mjs";
|
||||
export default function ScheduleAppointment() {
|
||||
const [selectedDoctor, setSelectedDoctor] = useState("")
|
||||
const [selectedDate, setSelectedDate] = useState("")
|
||||
const [selectedTime, setSelectedTime] = useState("")
|
||||
const [notes, setNotes] = useState("")
|
||||
|
||||
// Interface para o estado local do formulário (sem alterações)
|
||||
interface AppointmentFormState {
|
||||
id: string;
|
||||
date: string;
|
||||
time: string;
|
||||
observations: string;
|
||||
}
|
||||
const doctors = [
|
||||
{ id: "1", name: "Dr. João Silva", specialty: "Cardiologia" },
|
||||
{ id: "2", name: "Dra. Maria Santos", specialty: "Dermatologia" },
|
||||
{ id: "3", name: "Dr. Pedro Costa", specialty: "Ortopedia" },
|
||||
{ id: "4", name: "Dra. Ana Lima", specialty: "Ginecologia" },
|
||||
]
|
||||
|
||||
interface Doctor {
|
||||
id: string;
|
||||
full_name: string;
|
||||
specialty: string;
|
||||
phone_mobile: string;
|
||||
|
||||
}
|
||||
|
||||
// --- DADOS MOCKADOS (ALTERAÇÃO 1: Adicionando location e phone) ---
|
||||
const doctors = [
|
||||
{ id: "1", name: "Dr. João Silva", specialty: "Cardiologia", location: "Consultório A - 2º andar", phone: "(11) 3333-4444" },
|
||||
{ id: "2", name: "Dra. Maria Santos", specialty: "Dermatologia", location: "Consultório B - 1º andar", phone: "(11) 3333-5555" },
|
||||
{ id: "3", name: "Dr. Pedro Costa", specialty: "Ortopedia", location: "Consultório C - 3º andar", phone: "(11) 3333-6666" },
|
||||
];
|
||||
const availableTimes = ["09:00", "09:30", "10:00", "10:30", "14:00", "14:30", "15:00"];
|
||||
// -------------------------------------------------------------
|
||||
|
||||
export default function ScheduleAppointmentPage() {
|
||||
const router = useRouter();
|
||||
const [doctors, setDoctors] = useState<Doctor[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// [SINCRONIZAÇÃO 1 - continuação] - Obtendo a lista de agendamentos existentes
|
||||
const { addAppointment, appointments } = useAppointments();
|
||||
|
||||
const [formData, setFormData] = useState<AppointmentFormState>({
|
||||
id: "",
|
||||
date: "",
|
||||
time: "",
|
||||
observations: "",
|
||||
});
|
||||
|
||||
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(() => {
|
||||
fetchDoctors();
|
||||
}, [fetchDoctors]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prevState => ({ ...prevState, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSelectChange = (name: keyof AppointmentFormState, value: string) => {
|
||||
setFormData(prevState => ({ ...prevState, [name]: value }));
|
||||
};
|
||||
const availableTimes = [
|
||||
"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) => {
|
||||
e.preventDefault();
|
||||
if (!formData.id || !formData.date || !formData.time) {
|
||||
toast.error("Por favor, preencha os campos de médico, data e horário.");
|
||||
e.preventDefault()
|
||||
|
||||
const doctorDetails = doctors.find((d) => d.id === selectedDoctor)
|
||||
|
||||
// --- SIMULAÇÃO DO PACIENTE LOGADO ---
|
||||
// Você só tem um usuário para cada role. Vamos simular um paciente:
|
||||
const patientDetails = {
|
||||
id: "P001",
|
||||
full_name: "Paciente Exemplo Único", // Este nome aparecerá na agenda do médico
|
||||
location: "Clínica Geral",
|
||||
phone: "(11) 98765-4321"
|
||||
};
|
||||
|
||||
if (!patientDetails || !doctorDetails) {
|
||||
alert("Erro: Selecione o médico ou dados do paciente indisponíveis.");
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedDoctor = doctors.find(doc => doc.id === formData.id);
|
||||
if (!selectedDoctor) return;
|
||||
const newAppointment = {
|
||||
id: new Date().getTime(), // ID único simples
|
||||
patientName: patientDetails.full_name,
|
||||
doctor: doctorDetails.name, // Nome completo do médico (necessário para a listagem)
|
||||
specialty: doctorDetails.specialty,
|
||||
date: selectedDate,
|
||||
time: selectedTime,
|
||||
status: "agendada",
|
||||
phone: patientDetails.phone,
|
||||
};
|
||||
|
||||
// Validação de conflito (sem alterações, já estava correta)
|
||||
const isConflict = appointments.some(
|
||||
(apt) =>
|
||||
apt.doctorName === selectedDoctor.full_name &&
|
||||
apt.date === formData.date &&
|
||||
apt.time === formData.time
|
||||
);
|
||||
// 1. Carrega agendamentos existentes
|
||||
const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY);
|
||||
const currentAppointments = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : [];
|
||||
|
||||
// 2. Adiciona o novo agendamento
|
||||
const updatedAppointments = [...currentAppointments, newAppointment];
|
||||
|
||||
if (isConflict) {
|
||||
toast.error("Este horário já está ocupado para o médico selecionado.");
|
||||
return;
|
||||
}
|
||||
// 3. Salva a lista atualizada no LocalStorage
|
||||
localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments));
|
||||
|
||||
// [ALTERAÇÃO 2] - Utilizando os dados do médico selecionado para location e phone
|
||||
// e removendo os placeholders.
|
||||
addAppointment({
|
||||
doctorName: selectedDoctor.full_name,
|
||||
specialty: selectedDoctor.specialty,
|
||||
date: formData.date,
|
||||
time: formData.time,
|
||||
observations: formData.observations,
|
||||
phone: selectedDoctor.phone_mobile,
|
||||
location: ""
|
||||
});
|
||||
|
||||
toast.success("Consulta agendada com sucesso!");
|
||||
router.push('/patient/appointments');
|
||||
};
|
||||
|
||||
// Validação de data passada (sem alterações, já estava correta)
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
alert(`Consulta com ${doctorDetails.name} agendada com sucesso!`);
|
||||
|
||||
// Limpar o formulário após o sucesso (opcional)
|
||||
setSelectedDoctor("");
|
||||
setSelectedDate("");
|
||||
setSelectedTime("");
|
||||
setNotes("");
|
||||
}
|
||||
|
||||
return (
|
||||
<PatientLayout>
|
||||
@ -139,7 +100,7 @@ export default function ScheduleAppointmentPage() {
|
||||
<p className="text-gray-600">Escolha o médico, data e horário para sua consulta</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@ -150,41 +111,35 @@ export default function ScheduleAppointmentPage() {
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="doctor">Médico</Label>
|
||||
<Select
|
||||
value={formData.id}
|
||||
onValueChange={(value) => handleSelectChange('id', value)}
|
||||
>
|
||||
<Select value={selectedDoctor} onValueChange={setSelectedDoctor}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Seleione um médico" />
|
||||
<SelectValue placeholder="Selecione um médico" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{doctors.map((doctor) => (
|
||||
<SelectItem key={doctor.id} value={doctor.id}>
|
||||
{doctor.full_name} - {doctor.specialty}
|
||||
{doctor.name} - {doctor.specialty}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="date">Data</Label>
|
||||
<Input
|
||||
id="date"
|
||||
name="date"
|
||||
type="date"
|
||||
value={formData.date}
|
||||
onChange={handleChange}
|
||||
min={today}
|
||||
value={selectedDate}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
min={new Date().toISOString().split("T")[0]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="time">Horário</Label>
|
||||
<Select
|
||||
value={formData.time}
|
||||
onValueChange={(value) => handleSelectChange('time', value)}
|
||||
>
|
||||
<Select value={selectedTime} onValueChange={setSelectedTime}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione um horário" />
|
||||
</SelectTrigger>
|
||||
@ -200,18 +155,17 @@ export default function ScheduleAppointmentPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="observations">Observações (opcional)</Label>
|
||||
<Label htmlFor="notes">Observações (opcional)</Label>
|
||||
<Textarea
|
||||
id="observations"
|
||||
name="observations"
|
||||
id="notes"
|
||||
placeholder="Descreva brevemente o motivo da consulta ou observações importantes"
|
||||
value={formData.observations}
|
||||
onChange={handleChange}
|
||||
rows={4}
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="w-full bg-gray-600 hover:bg-gray-700 text-white text-base py-6">
|
||||
<Button type="submit" className="w-full" disabled={!selectedDoctor || !selectedDate || !selectedTime}>
|
||||
Agendar Consulta
|
||||
</Button>
|
||||
</form>
|
||||
@ -222,30 +176,30 @@ export default function ScheduleAppointmentPage() {
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center text-base">
|
||||
<CardTitle className="flex items-center">
|
||||
<Calendar className="mr-2 h-5 w-5" />
|
||||
Resumo
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm">
|
||||
{formData.id ? (
|
||||
<div className="flex items-center">
|
||||
<User className="mr-2 h-4 w-4 text-gray-500" />
|
||||
<span>{doctors.find((d) => d.id === formData.id)?.full_name}</span>
|
||||
</div>
|
||||
) : <p className="text-gray-500">Preencha o formulário...</p>}
|
||||
|
||||
{formData.date && (
|
||||
<div className="flex items-center">
|
||||
<Calendar className="mr-2 h-4 w-4 text-gray-500" />
|
||||
<span>{new Date(formData.date).toLocaleDateString("pt-BR", { timeZone: 'UTC' })}</span>
|
||||
<CardContent className="space-y-4">
|
||||
{selectedDoctor && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<User className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm">{doctors.find((d) => d.id === selectedDoctor)?.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.time && (
|
||||
<div className="flex items-center">
|
||||
<Clock className="mr-2 h-4 w-4 text-gray-500" />
|
||||
<span>{formData.time}</span>
|
||||
{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>
|
||||
@ -253,20 +207,18 @@ export default function ScheduleAppointmentPage() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Informações Importantes</CardTitle>
|
||||
<CardTitle>Informações Importantes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="space-y-2 text-sm text-gray-600 list-disc list-inside">
|
||||
<li>Chegue com 15 minutos de antecedência</li>
|
||||
<li>Traga documento com foto</li>
|
||||
<li>Traga carteirinha do convênio</li>
|
||||
<li>Traga exames anteriores, se houver</li>
|
||||
</ul>
|
||||
<CardContent className="text-sm text-gray-600 space-y-2">
|
||||
<p>• Chegue com 15 minutos de antecedência</p>
|
||||
<p>• Traga documento com foto</p>
|
||||
<p>• Traga carteirinha do convênio</p>
|
||||
<p>• Traga exames anteriores, se houver</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PatientLayout>
|
||||
);
|
||||
)
|
||||
}
|
||||
14
package-lock.json
generated
14
package-lock.json
generated
@ -46,7 +46,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"geist": "^1.3.1",
|
||||
"input-otp": "1.4.1",
|
||||
@ -54,13 +54,13 @@
|
||||
"next": "14.2.16",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^18",
|
||||
"react-day-picker": "9.8.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "latest",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.9",
|
||||
"zod": "3.25.67"
|
||||
@ -2499,14 +2499,14 @@
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
|
||||
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@ -2517,7 +2517,7 @@
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
@ -3579,7 +3579,6 @@
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -4126,7 +4125,6 @@
|
||||
"version": "4.1.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tailwindcss-animate": {
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"geist": "^1.3.1",
|
||||
"input-otp": "1.4.1",
|
||||
@ -55,13 +55,13 @@
|
||||
"next": "14.2.16",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^18",
|
||||
"react-day-picker": "9.8.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "latest",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.9",
|
||||
"zod": "3.25.67"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user