Files
frontend/components/organisms/shift-schedule/MonthlyCalendar.tsx
T
2026-05-14 02:44:43 +00:00

165 lines
5.4 KiB
TypeScript

"use client";
import { useShift } from "@/lib/shift-context";
import { useMemo } from "react";
import type { MonthlyCalendarProps } from "./ShiftSchedule.types";
const DAY_HEADERS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
function formatDateISO(d: Date): string {
const y = d.getFullYear();
const m = (d.getMonth() + 1).toString().padStart(2, "0");
const day = d.getDate().toString().padStart(2, "0");
return `${y}-${m}-${day}`;
}
function isToday(d: Date): boolean {
const today = new Date(2026, 3, 10);
return (
d.getDate() === today.getDate() &&
d.getMonth() === today.getMonth() &&
d.getFullYear() === today.getFullYear()
);
}
export default function MonthlyCalendar({
onShiftClick,
onDateSelect,
}: MonthlyCalendarProps) {
const { currentDate, shifts } = useShift();
const calendarDays = useMemo(() => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
// Monday = 0, Sunday = 6
let startOffset = firstDay.getDay() - 1;
if (startOffset < 0) startOffset = 6;
const days: (Date | null)[] = [];
// Fill leading empty cells
for (let i = 0; i < startOffset; i++) {
days.push(null);
}
// Fill actual days
for (let d = 1; d <= lastDay.getDate(); d++) {
days.push(new Date(year, month, d));
}
// Fill trailing empty cells to complete the grid
while (days.length % 7 !== 0) {
days.push(null);
}
return days;
}, [currentDate]);
const getShiftSummary = (date: Date) => {
const dateStr = formatDateISO(date);
const dayShifts = shifts.filter((s) => s.date === dateStr);
// const available = dayShifts.filter((s) => s.status === "available").length;
// const registered = dayShifts.filter(
// (s) => s.status === "registered",
// ).length;
// const leave = dayShifts.filter((s) => s.status === "approved_leave").length;
// const absent = dayShifts.filter((s) => s.status === "absent").length;
return { total: dayShifts.length };
};
return (
<div className="overflow-hidden rounded-xl border border-(--color-border-light)">
{/* Day headers */}
<div className="grid grid-cols-7 border-b border-(--color-border-light) bg-gray-50">
{DAY_HEADERS.map((day) => (
<div
key={day}
className="px-2 py-2.5 text-center text-xs font-semibold text-(--color-text-muted) uppercase"
>
{day}
</div>
))}
</div>
{/* Calendar grid */}
<div className="grid grid-cols-7">
{calendarDays.map((date, i) => {
if (!date) {
return (
<div
key={`empty-${i}`}
className="min-h-25 border-r border-b border-(--color-border-light) bg-gray-50/30"
/>
);
}
const summary = getShiftSummary(date);
const today = isToday(date);
return (
<button
type="button"
key={i}
onClick={() => onDateSelect?.(formatDateISO(date))}
className={`min-h-25 cursor-pointer border-r border-b border-(--color-border-light) bg-transparent p-2 text-left transition hover:bg-gray-50 ${
today ? "bg-(--color-primary)/5" : ""
}`}
>
<span
className={`inline-flex h-7 w-7 items-center justify-center rounded-full text-sm ${
today
? "bg-(--color-primary) font-bold text-white"
: "text-foreground font-medium"
}`}
>
{date.getDate()}
</span>
{/* {summary.total > 0 && (
<div className="mt-2 space-y-1">
{summary.available > 0 && (
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-blue-400"></span>
<span className="text-[10px] text-blue-600">
{summary.available} available
</span>
</div>
)}
{summary.registered > 0 && (
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-blue-700"></span>
<span className="text-[10px] text-blue-800">
{summary.registered} registered
</span>
</div>
)}
{summary.leave > 0 && (
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-purple-400"></span>
<span className="text-[10px] text-purple-600">
{summary.leave} on leave
</span>
</div>
)}
{summary.absent > 0 && (
<div className="flex items-center gap-1.5">
<span className="h-2 w-2 rounded-full bg-red-400"></span>
<span className="text-[10px] text-red-600">
{summary.absent} absent
</span>
</div>
)}
</div>
)} */}
</button>
);
})}
</div>
</div>
);
}