162 lines
4.9 KiB
TypeScript
162 lines
4.9 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo } from "react";
|
|
import { cn } from "@/shared/lib/utils";
|
|
import type { DeskReservation } from "../types";
|
|
import { DESKS } from "../types";
|
|
import {
|
|
getMonday,
|
|
toDateKey,
|
|
formatDateShort,
|
|
getReservationsForDate,
|
|
isDateBookable,
|
|
} from "../services/reservation-service";
|
|
|
|
interface DeskCalendarProps {
|
|
/** The week anchor date */
|
|
weekStart: Date;
|
|
selectedDate: string;
|
|
reservations: DeskReservation[];
|
|
onSelectDate: (dateKey: string) => void;
|
|
onPrevWeek: () => void;
|
|
onNextWeek: () => void;
|
|
canGoPrev: boolean;
|
|
canGoNext: boolean;
|
|
}
|
|
|
|
export function DeskCalendar({
|
|
weekStart,
|
|
selectedDate,
|
|
reservations,
|
|
onSelectDate,
|
|
onPrevWeek,
|
|
onNextWeek,
|
|
canGoPrev,
|
|
canGoNext,
|
|
}: DeskCalendarProps) {
|
|
const monday = useMemo(() => getMonday(weekStart), [weekStart]);
|
|
|
|
const weekDays = useMemo(() => {
|
|
const days: string[] = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
const d = new Date(monday);
|
|
d.setDate(monday.getDate() + i);
|
|
days.push(toDateKey(d));
|
|
}
|
|
return days;
|
|
}, [monday]);
|
|
|
|
const todayKey = toDateKey(new Date());
|
|
|
|
return (
|
|
<div className="flex flex-col gap-2">
|
|
{/* Week navigation */}
|
|
<div className="flex items-center justify-between">
|
|
<button
|
|
type="button"
|
|
onClick={onPrevWeek}
|
|
disabled={!canGoPrev}
|
|
className={cn(
|
|
"rounded-md px-2 py-1 text-xs font-medium transition-colors",
|
|
canGoPrev
|
|
? "text-muted-foreground hover:text-foreground hover:bg-muted/60"
|
|
: "text-muted-foreground/30 cursor-not-allowed",
|
|
)}
|
|
>
|
|
← Săpt. anterioară
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={onNextWeek}
|
|
disabled={!canGoNext}
|
|
className={cn(
|
|
"rounded-md px-2 py-1 text-xs font-medium transition-colors",
|
|
canGoNext
|
|
? "text-muted-foreground hover:text-foreground hover:bg-muted/60"
|
|
: "text-muted-foreground/30 cursor-not-allowed",
|
|
)}
|
|
>
|
|
Săpt. următoare →
|
|
</button>
|
|
</div>
|
|
|
|
{/* Day cells */}
|
|
<div className="grid grid-cols-5 gap-1.5">
|
|
{weekDays.map((dateKey) => {
|
|
const dayReservations = getReservationsForDate(dateKey, reservations);
|
|
const bookedCount = dayReservations.length;
|
|
const totalDesks = DESKS.length;
|
|
const isSelected = dateKey === selectedDate;
|
|
const isToday = dateKey === todayKey;
|
|
const bookable = isDateBookable(dateKey);
|
|
const isPast = dateKey < todayKey;
|
|
const hasNoBookings = bookedCount === 0 && !isPast;
|
|
|
|
return (
|
|
<button
|
|
key={dateKey}
|
|
type="button"
|
|
onClick={() => onSelectDate(dateKey)}
|
|
disabled={!bookable && !isPast}
|
|
className={cn(
|
|
"relative flex flex-col items-center gap-0.5 rounded-lg border px-2 py-2 text-center transition-all",
|
|
isSelected
|
|
? "border-primary/50 bg-primary/8 ring-1 ring-primary/20"
|
|
: "border-border/40 hover:border-border/70 hover:bg-muted/30",
|
|
isPast && !isSelected && "opacity-50",
|
|
!bookable && !isPast && "opacity-40 cursor-not-allowed",
|
|
)}
|
|
>
|
|
{/* Today dot */}
|
|
{isToday && (
|
|
<div className="absolute top-1 right-1 h-1.5 w-1.5 rounded-full bg-primary" />
|
|
)}
|
|
|
|
{/* Day name + date */}
|
|
<span
|
|
className={cn(
|
|
"text-[11px] font-medium leading-tight",
|
|
isSelected ? "text-primary" : "text-muted-foreground",
|
|
)}
|
|
>
|
|
{formatDateShort(dateKey)}
|
|
</span>
|
|
|
|
{/* Occupancy indicator */}
|
|
<div className="flex gap-0.5 mt-0.5">
|
|
{Array.from({ length: totalDesks }).map((_, i) => (
|
|
<div
|
|
key={i}
|
|
className={cn(
|
|
"h-1 w-3 rounded-full transition-colors",
|
|
i < bookedCount
|
|
? "bg-primary/60"
|
|
: hasNoBookings
|
|
? "bg-amber-400/40"
|
|
: "bg-muted-foreground/15",
|
|
)}
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{/* Count label */}
|
|
<span
|
|
className={cn(
|
|
"text-[10px] leading-tight",
|
|
bookedCount === totalDesks
|
|
? "text-primary/70"
|
|
: hasNoBookings
|
|
? "text-amber-500/70"
|
|
: "text-muted-foreground/60",
|
|
)}
|
|
>
|
|
{bookedCount}/{totalDesks}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|