350 lines
12 KiB
TypeScript
350 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import {
|
|
ChevronLeft,
|
|
ChevronRight,
|
|
Plus,
|
|
Clock,
|
|
User,
|
|
Calendar as CalendarIcon,
|
|
} from "lucide-react";
|
|
|
|
interface Appointment {
|
|
id: string;
|
|
patient: string;
|
|
time: string;
|
|
duration: number;
|
|
type: "consulta" | "exame" | "retorno";
|
|
status: "confirmed" | "pending" | "absent";
|
|
professional: string;
|
|
notes: string;
|
|
}
|
|
|
|
interface Professional {
|
|
id: string;
|
|
name: string;
|
|
specialty: string;
|
|
}
|
|
|
|
interface AgendaCalendarProperties {
|
|
professionals: Professional[];
|
|
appointments: Appointment[];
|
|
onAddAppointment: () => void;
|
|
onEditAppointment: (appointment: Appointment) => void;
|
|
}
|
|
|
|
export default function AgendaCalendar({
|
|
professionals,
|
|
appointments,
|
|
onAddAppointment,
|
|
onEditAppointment,
|
|
}: AgendaCalendarProperties) {
|
|
const [view, setView] = useState<"day" | "week" | "month">("week");
|
|
const [selectedProfessional, setSelectedProfessional] = useState("all");
|
|
const [currentDate, setCurrentDate] = useState(new Date());
|
|
|
|
const timeSlots = Array.from({ length: 11 }, (_, index) => {
|
|
const hour = index + 8; // Das 8h às 18h
|
|
return [
|
|
`${hour.toString().padStart(2, "0")}:00`,
|
|
`${hour.toString().padStart(2, "0")}:30`,
|
|
];
|
|
}).flat();
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case "confirmed":
|
|
return "bg-green-100 border-green-500 text-green-800";
|
|
case "pending":
|
|
return "bg-yellow-100 border-yellow-500 text-yellow-800";
|
|
case "absent":
|
|
return "bg-red-100 border-red-500 text-red-800";
|
|
default:
|
|
return "bg-gray-100 border-gray-500 text-gray-800";
|
|
}
|
|
};
|
|
|
|
const getTypeIcon = (type: string) => {
|
|
switch (type) {
|
|
case "consulta":
|
|
return "🩺";
|
|
case "exame":
|
|
return "📋";
|
|
case "retorno":
|
|
return "↩️";
|
|
default:
|
|
return "📅";
|
|
}
|
|
};
|
|
|
|
const formatDate = (date: Date) => {
|
|
return date.toLocaleDateString("pt-BR", {
|
|
weekday: "long",
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
});
|
|
};
|
|
|
|
const navigateDate = (direction: "prev" | "next") => {
|
|
const newDate = new Date(currentDate);
|
|
if (view === "day") {
|
|
newDate.setDate(newDate.getDate() + (direction === "next" ? 1 : -1));
|
|
} else if (view === "week") {
|
|
newDate.setDate(newDate.getDate() + (direction === "next" ? 7 : -7));
|
|
} else {
|
|
newDate.setMonth(newDate.getMonth() + (direction === "next" ? 1 : -1));
|
|
}
|
|
setCurrentDate(newDate);
|
|
};
|
|
|
|
const goToToday = () => {
|
|
setCurrentDate(new Date());
|
|
};
|
|
|
|
const filteredAppointments =
|
|
selectedProfessional === "all"
|
|
? appointments
|
|
: appointments.filter((app) => app.professional === selectedProfessional);
|
|
|
|
return (
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="p-4 border-b border-gray-200">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
<h2 className="text-xl font-semibold text-gray-900 mb-4 sm:mb-0">
|
|
Agenda
|
|
</h2>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<select
|
|
value={selectedProfessional}
|
|
onChange={(e) => setSelectedProfessional(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
|
>
|
|
<option value="all">Todos os profissionais</option>
|
|
{professionals.map((prof) => (
|
|
<option key={prof.id} value={prof.id}>
|
|
{prof.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
|
|
<div className="inline-flex rounded-md shadow-sm">
|
|
<button
|
|
type="button"
|
|
onClick={() => setView("day")}
|
|
className={`px-3 py-2 text-sm font-medium rounded-l-md ${
|
|
view === "day"
|
|
? "bg-blue-100 text-blue-700 border border-blue-300"
|
|
: "bg-white text-gray-700 border border-gray-300"
|
|
}`}
|
|
>
|
|
Dia
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setView("week")}
|
|
className={`px-3 py-2 text-sm font-medium -ml-px ${
|
|
view === "week"
|
|
? "bg-blue-100 text-blue-700 border border-blue-300"
|
|
: "bg-white text-gray-700 border border-gray-300"
|
|
}`}
|
|
>
|
|
Semana
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setView("month")}
|
|
className={`px-3 py-2 text-sm font-medium -ml-px rounded-r-md ${
|
|
view === "month"
|
|
? "bg-blue-100 text-blue-700 border border-blue-300"
|
|
: "bg-white text-gray-700 border border-gray-300"
|
|
}`}
|
|
>
|
|
Mês
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
onClick={onAddAppointment}
|
|
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Novo Agendamento
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-4 border-b border-gray-200">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<button
|
|
onClick={() => navigateDate("prev")}
|
|
className="p-1 rounded-md hover:bg-gray-100"
|
|
>
|
|
<ChevronLeft className="h-5 w-5 text-gray-600" />
|
|
</button>
|
|
<h3 className="text-lg font-medium text-gray-900">
|
|
{formatDate(currentDate)}
|
|
</h3>
|
|
<button
|
|
onClick={() => navigateDate("next")}
|
|
className="p-1 rounded-md hover:bg-gray-100"
|
|
>
|
|
<ChevronRight className="h-5 w-5 text-gray-600" />
|
|
</button>
|
|
<button
|
|
onClick={goToToday}
|
|
className="ml-4 px-3 py-1 text-sm border border-gray-300 rounded-md hover:bg-gray-100"
|
|
>
|
|
Hoje
|
|
</button>
|
|
</div>
|
|
<div className="text-sm text-gray-500">
|
|
Atalhos: 'C' para calendário, 'F' para fila de espera
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{}
|
|
{view !== "month" && (
|
|
<div className="overflow-auto">
|
|
<div className="min-w-full">
|
|
<div className="flex">
|
|
<div className="w-20 flex-shrink-0 border-r border-gray-200">
|
|
<div className="h-12 border-b border-gray-200 flex items-center justify-center text-sm font-medium text-gray-500">
|
|
Hora
|
|
</div>
|
|
{timeSlots.map((time) => (
|
|
<div
|
|
key={time}
|
|
className="h-16 border-b border-gray-200 flex items-center justify-center text-sm text-gray-500"
|
|
>
|
|
{time}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<div className="h-12 border-b border-gray-200 flex items-center justify-center text-sm font-medium text-gray-500">
|
|
{currentDate.toLocaleDateString("pt-BR", { weekday: "long" })}
|
|
</div>
|
|
<div className="relative">
|
|
{timeSlots.map((time) => (
|
|
<div
|
|
key={time}
|
|
className="h-16 border-b border-gray-200"
|
|
></div>
|
|
))}
|
|
|
|
{filteredAppointments.map((app) => {
|
|
const [date, timeString] = app.time.split("T");
|
|
const [hours, minutes] = timeString.split(":");
|
|
const hour = parseInt(hours);
|
|
const minute = parseInt(minutes);
|
|
|
|
return (
|
|
<div
|
|
key={app.id}
|
|
className={`absolute left-1 right-1 border-l-4 rounded p-2 shadow-sm cursor-pointer ${getStatusColor(app.status)}`}
|
|
style={{
|
|
top: `${(hour - 8) * 64 + (minute / 60) * 64 + 48}px`,
|
|
height: `${(app.duration / 60) * 64}px`,
|
|
}}
|
|
onClick={() => onEditAppointment(app)}
|
|
>
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<div className="font-medium flex items-center">
|
|
<User className="h-3 w-3 mr-1" />
|
|
{app.patient}
|
|
</div>
|
|
<div className="text-xs flex items-center mt-1">
|
|
<Clock className="h-3 w-3 mr-1" />
|
|
{hours}:{minutes} - {app.type}{" "}
|
|
{getTypeIcon(app.type)}
|
|
</div>
|
|
<div className="text-xs mt-1">
|
|
{
|
|
professionals.find(
|
|
(p) => p.id === app.professional,
|
|
)?.name
|
|
}
|
|
</div>
|
|
</div>
|
|
<div className="text-xs capitalize">
|
|
{app.status === "confirmed"
|
|
? "confirmado"
|
|
: app.status === "pending"
|
|
? "pendente"
|
|
: "ausente"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{}
|
|
{view === "month" && (
|
|
<div className="p-4">
|
|
<div className="space-y-4">
|
|
{filteredAppointments.map((app) => {
|
|
const [date, timeString] = app.time.split("T");
|
|
const [hours, minutes] = timeString.split(":");
|
|
|
|
return (
|
|
<div
|
|
key={app.id}
|
|
className={`border-l-4 p-4 rounded-lg shadow-sm ${getStatusColor(app.status)}`}
|
|
>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-center">
|
|
<div className="flex items-center">
|
|
<User className="h-4 w-4 mr-2" />
|
|
<span className="font-medium">{app.patient}</span>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<Clock className="h-4 w-4 mr-2" />
|
|
<span>
|
|
{hours}:{minutes} - {app.type} {getTypeIcon(app.type)}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center">
|
|
<span className="text-sm">
|
|
{
|
|
professionals.find((p) => p.id === app.professional)
|
|
?.name
|
|
}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{app.notes && (
|
|
<div className="mt-2 text-sm text-gray-600">
|
|
{app.notes}
|
|
</div>
|
|
)}
|
|
<div className="mt-2 flex justify-end">
|
|
<button
|
|
onClick={() => onEditAppointment(app)}
|
|
className="text-blue-600 hover:text-blue-800 text-sm"
|
|
>
|
|
Editar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|