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 DoctorLayout from "@/components/doctor-layout";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { Badge } from "@/components/ui/badge";
|
||||||
import { toast } from "sonner";
|
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";
|
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
|
||||||
|
|
||||||
// --- TIPAGEM DA CONSULTA SALVA NO LOCALSTORAGE ---
|
// --- TIPAGEM DA CONSULTA SALVA NO LOCALSTORAGE ---
|
||||||
// Reflete a estrutura salva pelo secretarypage.tsx
|
|
||||||
interface LocalStorageAppointment {
|
interface LocalStorageAppointment {
|
||||||
id: number; // ID único simples (timestamp)
|
id: number;
|
||||||
patientName: string;
|
patientName: string;
|
||||||
doctor: string; // Nome completo do médico (para filtrar)
|
doctor: string;
|
||||||
specialty: string;
|
specialty: string;
|
||||||
date: string; // Data no formato YYYY-MM-DD
|
date: string; // Data no formato YYYY-MM-DD
|
||||||
time: string; // Hora no formato HH:MM
|
time: string; // Hora no formato HH:MM
|
||||||
status: "agendada" | "confirmada" | "cancelada" | "realizada";
|
status: "agendada" | "confirmada" | "cancelada" | "realizada";
|
||||||
location: string;
|
location: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SIMULAÇÃO DO MÉDICO LOGADO ---
|
const LOGGED_IN_DOCTOR_NAME = "Dr. João Santos";
|
||||||
// **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.
|
// Função auxiliar para comparar se duas datas (Date objects) são o mesmo dia
|
||||||
const LOGGED_IN_DOCTOR_NAME = "Dr. João Silva"; // <--- AJUSTE ESTE NOME PARA TESTAR
|
const isSameDay = (date1: Date, date2: Date) => {
|
||||||
|
return date1.getFullYear() === date2.getFullYear() &&
|
||||||
|
date1.getMonth() === date2.getMonth() &&
|
||||||
|
date1.getDate() === date2.getDate();
|
||||||
|
};
|
||||||
|
|
||||||
// --- COMPONENTE PRINCIPAL ---
|
// --- COMPONENTE PRINCIPAL ---
|
||||||
|
|
||||||
export default function DoctorAppointmentsPage() {
|
export default function DoctorAppointmentsPage() {
|
||||||
const [appointments, setAppointments] = useState<LocalStorageAppointment[]>([]);
|
const [allAppointments, setAllAppointments] = useState<LocalStorageAppointment[]>([]);
|
||||||
|
const [filteredAppointments, setFilteredAppointments] = useState<LocalStorageAppointment[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
loadAppointments();
|
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 = () => {
|
const loadAppointments = () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY);
|
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
|
// ***** NENHUM FILTRO POR MÉDICO AQUI (Como solicitado) *****
|
||||||
const filteredAppointments = allAppointments.filter(
|
const appointmentsToShow = allAppts;
|
||||||
(app) => app.doctor === LOGGED_IN_DOCTOR_NAME
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Ordena por Data e Hora
|
// 1. EXTRAI E PREPARA AS DATAS PARA O CALENDÁRIO
|
||||||
filteredAppointments.sort((a, b) => {
|
const uniqueBookedDates = Array.from(new Set(appointmentsToShow.map(app => app.date)));
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
||||||
setAppointments(filteredAppointments);
|
// 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'));
|
||||||
|
|
||||||
|
setAllAppointments(appointmentsToShow);
|
||||||
|
setBookedDays(dateObjects);
|
||||||
toast.success("Agenda atualizada com sucesso!");
|
toast.success("Agenda atualizada com sucesso!");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar a agenda do LocalStorage:", 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']) => {
|
const getStatusVariant = (status: LocalStorageAppointment['status']) => {
|
||||||
|
// ... (código mantido)
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "confirmada":
|
case "confirmada":
|
||||||
case "agendada":
|
case "agendada":
|
||||||
@ -84,117 +118,153 @@ export default function DoctorAppointmentsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = (id: number) => {
|
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 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
|
app.id === id ? { ...app, status: "cancelada" as const } : app
|
||||||
);
|
);
|
||||||
|
|
||||||
localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments));
|
localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments));
|
||||||
loadAppointments(); // Recarrega a lista filtrada
|
loadAppointments();
|
||||||
toast.info(`Consulta cancelada com sucesso.`);
|
toast.info(`Consulta cancelada com sucesso.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReSchedule = (id: number) => {
|
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.`);
|
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 (
|
return (
|
||||||
<DoctorLayout>
|
<DoctorLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Minhas Consultas</h1>
|
<h1 className="text-3xl font-bold text-gray-900">Agenda Médica Centralizada</h1>
|
||||||
<p className="text-gray-600">Agenda atual ({LOGGED_IN_DOCTOR_NAME}) e histórico de atendimentos</p>
|
<p className="text-gray-600">Todas as consultas do sistema são exibidas aqui ({LOGGED_IN_DOCTOR_NAME})</p>
|
||||||
</div>
|
</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">
|
<Button onClick={loadAppointments} disabled={isLoading} variant="outline" size="sm">
|
||||||
<RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
<RefreshCw className={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||||
Atualizar Agenda
|
Atualizar Agenda
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* NOVO LAYOUT DE DUAS COLUNAS */}
|
||||||
{isLoading ? (
|
<div className="grid lg:grid-cols-3 gap-6">
|
||||||
<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";
|
|
||||||
|
|
||||||
return (
|
{/* COLUNA 1: CALENDÁRIO */}
|
||||||
<Card key={appointment.id} className="shadow-lg">
|
<div className="lg:col-span-1">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<Card>
|
||||||
{/* NOME DO PACIENTE */}
|
<CardHeader>
|
||||||
<CardTitle className="text-xl font-semibold flex items-center">
|
<CardTitle className="flex items-center">
|
||||||
<User className="mr-2 h-5 w-5 text-blue-600" />
|
<CalendarIcon className="mr-2 h-5 w-5" />
|
||||||
{appointment.patientName}
|
Calendário
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
{/* STATUS DA CONSULTA */}
|
<p className="text-sm text-gray-500">Dias em azul possuem agendamentos.</p>
|
||||||
<Badge variant={getStatusVariant(appointment.status)} className="uppercase">
|
</CardHeader>
|
||||||
{appointment.status}
|
<CardContent className="flex justify-center p-2">
|
||||||
</Badge>
|
<Calendar
|
||||||
</CardHeader>
|
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>
|
||||||
|
|
||||||
<CardContent className="grid md:grid-cols-3 gap-4 pt-4">
|
{/* COLUNA 2: LISTA DE CONSULTAS FILTRADAS */}
|
||||||
{/* COLUNA 1: Data e Hora */}
|
<div className="lg:col-span-2 space-y-4">
|
||||||
<div className="space-y-3">
|
{isLoading ? (
|
||||||
<div className="flex items-center text-sm text-gray-700">
|
<p className="text-center text-lg text-gray-500">Carregando a agenda...</p>
|
||||||
<Calendar className="mr-2 h-4 w-4 text-gray-500" />
|
) : filteredAppointments.length === 0 ? (
|
||||||
{new Date(appointment.date).toLocaleDateString("pt-BR", { timeZone: "UTC" })}
|
<p className="text-center text-lg text-gray-500">Nenhuma consulta encontrada para a data selecionada.</p>
|
||||||
</div>
|
) : (
|
||||||
<div className="flex items-center text-sm text-gray-700">
|
filteredAppointments.map((appointment) => {
|
||||||
<Clock className="mr-2 h-4 w-4 text-gray-500" />
|
const showActions = appointment.status === "agendada" || appointment.status === "confirmada";
|
||||||
{appointment.time}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* COLUNA 2: Local e Contato */}
|
return (
|
||||||
<div className="space-y-3">
|
<Card key={appointment.id} className="shadow-lg">
|
||||||
<div className="flex items-center text-sm text-gray-700">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<MapPin className="mr-2 h-4 w-4 text-gray-500" />
|
<CardTitle className="text-xl font-semibold flex items-center">
|
||||||
{appointment.location}
|
<User className="mr-2 h-5 w-5 text-blue-600" />
|
||||||
</div>
|
{appointment.patientName}
|
||||||
<div className="flex items-center text-sm text-gray-700">
|
</CardTitle>
|
||||||
<Phone className="mr-2 h-4 w-4 text-gray-500" />
|
<Badge variant={getStatusVariant(appointment.status)} className="uppercase">
|
||||||
{/* Note: O telefone do paciente não está salvo no LocalStorage no seu código atual, usando um valor fixo */}
|
{appointment.status}
|
||||||
{(appointment.phone || "(11) 9XXXX-YYYY")}
|
</Badge>
|
||||||
</div>
|
</CardHeader>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* COLUNA 3: Ações (Botões) */}
|
<CardContent className="grid md:grid-cols-3 gap-4 pt-4">
|
||||||
<div className="flex flex-col justify-center items-end">
|
{/* Detalhes e Ações... (mantidos) */}
|
||||||
{showActions && (
|
<div className="space-y-3">
|
||||||
<div className="flex space-x-2">
|
<div className="flex items-center text-sm text-gray-700">
|
||||||
<Button
|
<User className="mr-2 h-4 w-4 text-gray-500" />
|
||||||
variant="outline"
|
<span className="font-semibold">Médico:</span> {appointment.doctor}
|
||||||
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>
|
||||||
)}
|
<div className="flex items-center text-sm text-gray-700">
|
||||||
</div>
|
<CalendarIcon className="mr-2 h-4 w-4 text-gray-500" />
|
||||||
</CardContent>
|
{new Date(appointment.date).toLocaleDateString("pt-BR", { timeZone: "UTC" })}
|
||||||
</Card>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</DoctorLayout>
|
</DoctorLayout>
|
||||||
|
|||||||
@ -123,23 +123,3 @@
|
|||||||
@apply bg-background text-foreground;
|
@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 type React from "react"
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState } from "react"
|
||||||
import { useRouter } from "next/navigation";
|
// Importações de componentes omitidas para brevidade, mas estão no código original
|
||||||
import { toast } from "sonner";
|
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
|
// Chave do LocalStorage, a mesma usada em secretarypage.tsx
|
||||||
import { useAppointments } from "../../context/AppointmentsContext";
|
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
|
||||||
|
|
||||||
// Componentes de UI e Layout
|
export default function ScheduleAppointment() {
|
||||||
import PatientLayout from "@/components/patient-layout";
|
const [selectedDoctor, setSelectedDoctor] = useState("")
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
const [selectedDate, setSelectedDate] = useState("")
|
||||||
import { Button } from "@/components/ui/button";
|
const [selectedTime, setSelectedTime] = useState("")
|
||||||
import { Input } from "@/components/ui/input";
|
const [notes, setNotes] = useState("")
|
||||||
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";
|
|
||||||
|
|
||||||
// Interface para o estado local do formulário (sem alterações)
|
const doctors = [
|
||||||
interface AppointmentFormState {
|
{ id: "1", name: "Dr. João Silva", specialty: "Cardiologia" },
|
||||||
id: string;
|
{ id: "2", name: "Dra. Maria Santos", specialty: "Dermatologia" },
|
||||||
date: string;
|
{ id: "3", name: "Dr. Pedro Costa", specialty: "Ortopedia" },
|
||||||
time: string;
|
{ id: "4", name: "Dra. Ana Lima", specialty: "Ginecologia" },
|
||||||
observations: string;
|
]
|
||||||
}
|
|
||||||
|
|
||||||
interface Doctor {
|
const availableTimes = [
|
||||||
id: string;
|
"08:00",
|
||||||
full_name: string;
|
"08:30",
|
||||||
specialty: string;
|
"09:00",
|
||||||
phone_mobile: string;
|
"09:30",
|
||||||
|
"10:00",
|
||||||
}
|
"10:30",
|
||||||
|
"14:00",
|
||||||
// --- DADOS MOCKADOS (ALTERAÇÃO 1: Adicionando location e phone) ---
|
"14:30",
|
||||||
const doctors = [
|
"15:00",
|
||||||
{ id: "1", name: "Dr. João Silva", specialty: "Cardiologia", location: "Consultório A - 2º andar", phone: "(11) 3333-4444" },
|
"15:30",
|
||||||
{ id: "2", name: "Dra. Maria Santos", specialty: "Dermatologia", location: "Consultório B - 1º andar", phone: "(11) 3333-5555" },
|
"16:00",
|
||||||
{ id: "3", name: "Dr. Pedro Costa", specialty: "Ortopedia", location: "Consultório C - 3º andar", phone: "(11) 3333-6666" },
|
"16:30",
|
||||||
];
|
]
|
||||||
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 handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
if (!formData.id || !formData.date || !formData.time) {
|
|
||||||
toast.error("Por favor, preencha os campos de médico, data e horário.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedDoctor = doctors.find(doc => doc.id === formData.id);
|
const newAppointment = {
|
||||||
if (!selectedDoctor) return;
|
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)
|
// 1. Carrega agendamentos existentes
|
||||||
const isConflict = appointments.some(
|
const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY);
|
||||||
(apt) =>
|
const currentAppointments = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : [];
|
||||||
apt.doctorName === selectedDoctor.full_name &&
|
|
||||||
apt.date === formData.date &&
|
|
||||||
apt.time === formData.time
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isConflict) {
|
// 2. Adiciona o novo agendamento
|
||||||
toast.error("Este horário já está ocupado para o médico selecionado.");
|
const updatedAppointments = [...currentAppointments, newAppointment];
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// [ALTERAÇÃO 2] - Utilizando os dados do médico selecionado para location e phone
|
// 3. Salva a lista atualizada no LocalStorage
|
||||||
// e removendo os placeholders.
|
localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments));
|
||||||
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!");
|
alert(`Consulta com ${doctorDetails.name} agendada com sucesso!`);
|
||||||
router.push('/patient/appointments');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validação de data passada (sem alterações, já estava correta)
|
// Limpar o formulário após o sucesso (opcional)
|
||||||
const today = new Date().toISOString().split('T')[0];
|
setSelectedDoctor("");
|
||||||
|
setSelectedDate("");
|
||||||
|
setSelectedTime("");
|
||||||
|
setNotes("");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PatientLayout>
|
<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>
|
<p className="text-gray-600">Escolha o médico, data e horário para sua consulta</p>
|
||||||
</div>
|
</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">
|
<div className="lg:col-span-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@ -150,41 +111,35 @@ export default function ScheduleAppointmentPage() {
|
|||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="doctor">Médico</Label>
|
<Label htmlFor="doctor">Médico</Label>
|
||||||
<Select
|
<Select value={selectedDoctor} onValueChange={setSelectedDoctor}>
|
||||||
value={formData.id}
|
|
||||||
onValueChange={(value) => handleSelectChange('id', value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Seleione um médico" />
|
<SelectValue placeholder="Selecione um médico" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{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.name} - {doctor.specialty}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="date">Data</Label>
|
<Label htmlFor="date">Data</Label>
|
||||||
<Input
|
<Input
|
||||||
id="date"
|
id="date"
|
||||||
name="date"
|
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.date}
|
value={selectedDate}
|
||||||
onChange={handleChange}
|
onChange={(e) => setSelectedDate(e.target.value)}
|
||||||
min={today}
|
min={new Date().toISOString().split("T")[0]}
|
||||||
/>
|
/>
|
||||||
</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
|
<Select value={selectedTime} onValueChange={setSelectedTime}>
|
||||||
value={formData.time}
|
|
||||||
onValueChange={(value) => handleSelectChange('time', value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Selecione um horário" />
|
<SelectValue placeholder="Selecione um horário" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@ -200,18 +155,17 @@ export default function ScheduleAppointmentPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="observations">Observações (opcional)</Label>
|
<Label htmlFor="notes">Observações (opcional)</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="observations"
|
id="notes"
|
||||||
name="observations"
|
|
||||||
placeholder="Descreva brevemente o motivo da consulta ou observações importantes"
|
placeholder="Descreva brevemente o motivo da consulta ou observações importantes"
|
||||||
value={formData.observations}
|
value={notes}
|
||||||
onChange={handleChange}
|
onChange={(e) => setNotes(e.target.value)}
|
||||||
rows={4}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
Agendar Consulta
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@ -222,30 +176,30 @@ export default function ScheduleAppointmentPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center text-base">
|
<CardTitle className="flex items-center">
|
||||||
<Calendar className="mr-2 h-5 w-5" />
|
<Calendar className="mr-2 h-5 w-5" />
|
||||||
Resumo
|
Resumo
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 text-sm">
|
<CardContent className="space-y-4">
|
||||||
{formData.id ? (
|
{selectedDoctor && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center space-x-2">
|
||||||
<User className="mr-2 h-4 w-4 text-gray-500" />
|
<User className="h-4 w-4 text-gray-500" />
|
||||||
<span>{doctors.find((d) => d.id === formData.id)?.full_name}</span>
|
<span className="text-sm">{doctors.find((d) => d.id === selectedDoctor)?.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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{formData.time && (
|
{selectedDate && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center space-x-2">
|
||||||
<Clock className="mr-2 h-4 w-4 text-gray-500" />
|
<Calendar className="h-4 w-4 text-gray-500" />
|
||||||
<span>{formData.time}</span>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -253,20 +207,18 @@ export default function ScheduleAppointmentPage() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Informações Importantes</CardTitle>
|
<CardTitle>Informações Importantes</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="text-sm text-gray-600 space-y-2">
|
||||||
<ul className="space-y-2 text-sm text-gray-600 list-disc list-inside">
|
<p>• Chegue com 15 minutos de antecedência</p>
|
||||||
<li>Chegue com 15 minutos de antecedência</li>
|
<p>• Traga documento com foto</p>
|
||||||
<li>Traga documento com foto</li>
|
<p>• Traga carteirinha do convênio</p>
|
||||||
<li>Traga carteirinha do convênio</li>
|
<p>• Traga exames anteriores, se houver</p>
|
||||||
<li>Traga exames anteriores, se houver</li>
|
|
||||||
</ul>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PatientLayout>
|
</PatientLayout>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
14
package-lock.json
generated
14
package-lock.json
generated
@ -46,7 +46,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "8.5.1",
|
"embla-carousel-react": "8.5.1",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
@ -54,13 +54,13 @@
|
|||||||
"next": "14.2.16",
|
"next": "14.2.16",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-day-picker": "9.8.0",
|
"react-day-picker": "^9.8.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.60.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"sonner": "latest",
|
"sonner": "latest",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.9",
|
"vaul": "^0.9.9",
|
||||||
"zod": "3.25.67"
|
"zod": "3.25.67"
|
||||||
@ -2499,14 +2499,14 @@
|
|||||||
"version": "15.7.15",
|
"version": "15.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.24",
|
"version": "18.3.24",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
|
||||||
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
|
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
@ -2517,7 +2517,7 @@
|
|||||||
"version": "18.3.7",
|
"version": "18.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
@ -3579,7 +3579,6 @@
|
|||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -4126,7 +4125,6 @@
|
|||||||
"version": "4.1.13",
|
"version": "4.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||||
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss-animate": {
|
"node_modules/tailwindcss-animate": {
|
||||||
|
|||||||
@ -47,7 +47,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"embla-carousel-react": "8.5.1",
|
"embla-carousel-react": "8.5.1",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"input-otp": "1.4.1",
|
"input-otp": "1.4.1",
|
||||||
@ -55,13 +55,13 @@
|
|||||||
"next": "14.2.16",
|
"next": "14.2.16",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-day-picker": "9.8.0",
|
"react-day-picker": "^9.8.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
"react-hook-form": "^7.60.0",
|
"react-hook-form": "^7.60.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "2.15.4",
|
"recharts": "2.15.4",
|
||||||
"sonner": "latest",
|
"sonner": "latest",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.9",
|
"vaul": "^0.9.9",
|
||||||
"zod": "3.25.67"
|
"zod": "3.25.67"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user