diff --git a/ApiCore-Frontend/src/app/(app)/calendar/page.tsx b/ApiCore-Frontend/src/app/(app)/calendar/page.tsx index 7e1df7d..00db797 100644 --- a/ApiCore-Frontend/src/app/(app)/calendar/page.tsx +++ b/ApiCore-Frontend/src/app/(app)/calendar/page.tsx @@ -20,6 +20,8 @@ import { CalendarEvent, CalendarEventType } from "@/lib/calendar/types"; import { fetchApiaries } from "@/lib/apiaries/api"; import { Button } from "@/components/ui/button"; import { PageHeader } from "@/components/page-header"; +import { EmptyState } from "@/components/empty-state"; +import { ErrorState, LoadingState } from "@/components/query-state"; import { Select, SelectContent, @@ -27,25 +29,17 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { cn } from "@/lib/utils"; const WEEKDAYS = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]; -const EVENT_ICON: Record = { - TASK: ListTodo, - INSPECTION: ClipboardCheck, - TREATMENT: Syringe, - FEEDING: Droplet, - HARVEST: Package, - QUEEN_ASSIGNMENT: Crown, -}; - -const EVENT_TONE: Record = { - TASK: "bg-secondary/15 text-secondary", - INSPECTION: "bg-primary/15 text-primary", - TREATMENT: "bg-destructive/15 text-destructive", - FEEDING: "bg-warning/20 text-warning-foreground", - HARVEST: "bg-primary/15 text-primary", - QUEEN_ASSIGNMENT: "bg-secondary/15 text-secondary", +const EVENT_META: Record = { + TASK: { icon: ListTodo, label: "Aufgabe", tone: "bg-secondary/15" }, + INSPECTION: { icon: ClipboardCheck, label: "Durchsicht", tone: "bg-primary/15" }, + TREATMENT: { icon: Syringe, label: "Behandlung", tone: "bg-destructive/15" }, + FEEDING: { icon: Droplet, label: "Fütterung", tone: "bg-warning/20" }, + HARVEST: { icon: Package, label: "Ernte", tone: "bg-primary/15" }, + QUEEN_ASSIGNMENT: { icon: Crown, label: "Königin", tone: "bg-secondary/15" }, }; function eventHref(event: CalendarEvent): string | null { @@ -56,8 +50,11 @@ function eventHref(event: CalendarEvent): string | null { return null; } +/** Local calendar day "2026-10-01" (not UTC - late evening entries stay on their day). */ function toDateKey(date: Date): string { - return date.toISOString().slice(0, 10); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${date.getFullYear()}-${month}-${day}`; } function startOfMonth(date: Date): Date { @@ -71,9 +68,69 @@ function startOfGrid(monthStart: Date): Date { return gridStart; } +function dayHeading(date: Date): string { + return date.toLocaleDateString("de-DE", { weekday: "long", day: "numeric", month: "long" }); +} + +function EventRow({ event }: { event: CalendarEvent }) { + const meta = EVENT_META[event.type]; + const Icon = meta.icon; + const href = eventHref(event); + const content = ( + + + {meta.label} + {event.title} + + ); + return href ? ( + + {content} + + ) : ( + content + ); +} + +function DayEvents({ + date, + events, + isToday, +}: { + date: Date; + events: CalendarEvent[]; + isToday: boolean; +}) { + return ( +
+

+ {dayHeading(date)} + {isToday ? ( + + Heute + + ) : null} +

+ {events.length === 0 ? ( +

Nichts eingetragen.

+ ) : ( +
    + {events.map((event) => ( +
  • + +
  • + ))} +
+ )} +
+ ); +} + export default function CalendarPage() { const [month, setMonth] = useState(() => startOfMonth(new Date())); const [apiaryId, setApiaryId] = useState(""); + const [selectedKey, setSelectedKey] = useState(() => toDateKey(new Date())); + const todayKey = toDateKey(new Date()); const monthStart = startOfMonth(month); const gridStart = startOfGrid(monthStart); @@ -87,7 +144,7 @@ export default function CalendarPage() { queryKey: ["apiaries"], queryFn: () => fetchApiaries(), }); - const { data: events, isLoading, isError } = useQuery({ + const eventsQuery = useQuery({ queryKey: ["calendar", monthStart.toISOString(), apiaryId], queryFn: () => fetchCalendarEvents({ @@ -99,129 +156,197 @@ export default function CalendarPage() { const eventsByDay = useMemo(() => { const map = new Map(); - for (const event of events ?? []) { - const key = event.date.slice(0, 10); + for (const event of eventsQuery.data ?? []) { + const key = toDateKey(new Date(event.date)); const bucket = map.get(key) ?? []; bucket.push(event); map.set(key, bucket); } return map; - }, [events]); + }, [eventsQuery.data]); + + const daysOfMonthWithEvents = gridDays.filter( + (day) => + day.getMonth() === month.getMonth() && (eventsByDay.get(toDateKey(day))?.length ?? 0) > 0, + ); + const selectedDay = gridDays.find((day) => toDateKey(day) === selectedKey); + + function goToMonth(offset: number) { + setMonth(new Date(month.getFullYear(), month.getMonth() + offset, 1)); + } + + function goToToday() { + setMonth(startOfMonth(new Date())); + setSelectedKey(todayKey); + } + + const monthLabel = month.toLocaleDateString("de-DE", { month: "long", year: "numeric" }); return (
- - - - - } /> - {isLoading ?

Lädt…

: null} - {isError ? ( -

- Kalender konnte nicht geladen werden. +

+ +

+ {monthLabel}

+ + + +
+ + {eventsQuery.isLoading ? : null} + {eventsQuery.isError ? ( + void eventsQuery.refetch()} + isRetrying={eventsQuery.isFetching} + /> ) : null} -
- {WEEKDAYS.map((weekday) => ( -
- {weekday} + {eventsQuery.data ? ( + <> + {/* Phones: a plain list of the days that have something on. */} +
+ {daysOfMonthWithEvents.length === 0 ? ( + + ) : ( + daysOfMonthWithEvents.map((day) => ( + + )) + )}
- ))} - {gridDays.map((day) => { - const key = toDateKey(day); - const dayEvents = eventsByDay.get(key) ?? []; - const isCurrentMonth = day.getMonth() === month.getMonth(); - const isToday = key === toDateKey(new Date()); - return ( -
- - {day.getDate()} - -
- {dayEvents.slice(0, 3).map((event) => { - const Icon = EVENT_ICON[event.type]; - const href = eventHref(event); - const content = ( + + {/* Wider screens: month grid; choosing a day lists all its entries below. */} +
+
+ {WEEKDAYS.map((weekday) => ( +
+ {weekday} +
+ ))} + {gridDays.map((day) => { + const key = toDateKey(day); + const dayEvents = eventsByDay.get(key) ?? []; + const isCurrentMonth = day.getMonth() === month.getMonth(); + const isToday = key === todayKey; + const isSelected = key === selectedKey; + return ( +
+ + {dayEvents.slice(0, 3).map((event) => { + const meta = EVENT_META[event.type]; + const Icon = meta.icon; + return ( + + + {event.title} + + ); + })} + {dayEvents.length > 3 ? ( + + +{dayEvents.length - 3} weitere + + ) : null} + + + ); + })}
- ); - })} -
+ {selectedDay ? ( + + ) : null} +
+ + ) : null}
); }