"use client"; import * as React from "react"; import { format, startOfWeek, endOfWeek, eachDayOfInterval, isSameDay, subWeeks, addWeeks, subMonths, addMonths } from "date-fns"; import { Calendar } from "~/components/ui/calendar"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetFooter, } from "~/components/ui/sheet"; import { Button } from "~/components/ui/button"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { NumberInput } from "~/components/ui/number-input"; import { Plus, Trash2, Clock, Calendar as CalendarIcon, ChevronLeft, ChevronRight } from "lucide-react"; import { cn } from "~/lib/utils"; interface InvoiceItem { id: string; date: Date; description: string; hours: number; rate: number; amount: number; } interface InvoiceCalendarViewProps { items: InvoiceItem[]; onUpdateItem: ( index: number, field: string, value: string | number | Date ) => void; onAddItem: (date?: Date) => void; onRemoveItem: (index: number) => void; className?: string; defaultHourlyRate: number | null; } export function InvoiceCalendarView({ items, onUpdateItem, onAddItem, onRemoveItem, className, defaultHourlyRate: _defaultHourlyRate, }: InvoiceCalendarViewProps) { const [date, setDate] = React.useState(undefined); // Start unselected const [viewDate, setViewDate] = React.useState(new Date()); // Controls the view (month/week) const [view, setView] = React.useState<"month" | "week">("month"); const [sheetOpen, setSheetOpen] = React.useState(false); // Derived state for selected date items - solves cursor jumping const selectedDateItems = React.useMemo(() => { if (!date) return []; return items .map((item, index) => ({ item, index })) .filter((wrapper) => { const itemDate = new Date(wrapper.item.date); return isSameDay(itemDate, date); }); }, [items, date]); // Helper to get items for any date (for calendar view) const getItemsForDate = React.useCallback((targetDate: Date) => { return items .map((item, index) => ({ item, index })) .filter((wrapper) => { const itemDate = new Date(wrapper.item.date); return isSameDay(itemDate, targetDate); }); }, [items]); const handleSelectDate = (newDate: Date | undefined) => { if (!newDate) return; setDate(newDate); setSheetOpen(true); }; const handleAddNewItem = () => { if (date) { onAddItem(date); } }; // Week View Logic - Uses viewDate const currentWeekStart = startOfWeek(viewDate); const currentWeekEnd = endOfWeek(viewDate); const weekDays = eachDayOfInterval({ start: currentWeekStart, end: currentWeekEnd }); const handleCloseSheet = (isOpen: boolean) => { setSheetOpen(isOpen); if (!isOpen) { setDate(undefined); } }; return (
{/* Navigation Controls */}
{view === "week" ? ( <> {`${format(currentWeekStart, "MMM d")} - ${format(currentWeekEnd, "MMM d")}`} ) : ( <> {format(viewDate, "MMMM yyyy")} )}
{/* View Switcher */}
{view === "month" ? ( "", // Clear default caption text to prevent duplication }} components={{ DayButton: (props) => { const { day, modifiers, className, ...buttonProps } = props; const DayDate = day.date; const dayItems = getItemsForDate(DayDate); // const totalHours = dayItems.reduce((acc, curr) => acc + curr.item.hours, 0); // Unused now return ( ); } }} /> ) : (
{weekDays.map((day) => { const isSelected = date && isSameDay(day, date); const isToday = isSameDay(day, new Date()); const dayItems = getItemsForDate(day); const totalHours = dayItems.reduce((acc, curr) => acc + curr.item.hours, 0); return ( ); })}
)}
{/* Sheet for Day Details */}
{date ? format(date, "EEEE, MMMM do") : "Details"}
{date && selectedDateItems.length === 0 ? (

No hours logged

There are no time entries recorded for this day yet.

) : (
{selectedDateItems.map(({ item, index }) => (
{/* Description */}
onUpdateItem(index, "description", e.target.value)} placeholder="Describe the work performed..." className="w-full text-sm font-medium" />
{/* Controls Row */}
{/* Hours */} onUpdateItem(index, "hours", v)} step={0.25} min={0} width="auto" className="h-9 flex-1 min-w-[100px] font-mono" suffix="h" /> {/* Rate */} onUpdateItem(index, "rate", v)} prefix="$" min={0} step={1} width="auto" className="h-9 flex-1 min-w-[100px] font-mono" /> {/* Amount */}
${(item.hours * item.rate).toFixed(2)}
{/* Actions */}
))}
)}
); }