+
+ {formattedDate}
+
+ {course.timeFrom} – {course.timeTo}
+
+
+
+ {course.sport.length > 0 &&
{course.sport.join(", ")}
}
+
+
+
+ {levelConfig[course.level]?.label ?? "Jedes Niveau"}
+
+ {formattedPrice}
+
+
+
+ {isFull
+ ? "Ausgebucht"
+ : `${spotsLeft} ${spotsLeft === 1 ? "Platz" : "Plätze"} verfügbar`}
+
+
+ {/* Participant bar */}
+
+
0 ? Math.min(100, (safeCurrent / safeMax) * 100) : 0}%`,
+ }}
+ />
+
+
+ {course.description && (
+
+ )}
+
+
+
+ {isCheckingStatus ? (
+
+ ) : isBooked ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+const PublicCourseCard = ({ course }: PublicCourseCardProps) => {
+ const [dialogOpen, setDialogOpen] = useState(false);
+
+ const cardSafeCurrent = Math.max(0, course.currentParticipants);
+ const cardSafeMax = Math.max(0, course.maxParticipants);
+ const isFull = cardSafeCurrent >= cardSafeMax;
+ const currentLevel = levelConfig[course.level] ?? levelConfig.any;
+
+ const formattedDate = new Date(course.date).toLocaleDateString("de-DE", {
+ weekday: "long",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ });
+
+ const formattedPrice = course.price.toLocaleString("de-DE", {
+ style: "currency",
+ currency: "EUR",
+ });
+
+ const fillPercent = cardSafeMax > 0 ? Math.min(100, (cardSafeCurrent / cardSafeMax) * 100) : 0;
+
+ return (
+ <>
+
+
+
{formattedDate}
+
+ {course.timeFrom} – {course.timeTo}
+
+
+
+
{course.name}
+
+
{course.sport.join(", ")}
+
+
+
+ {currentLevel.label}
+
+ {formattedPrice}
+
+
+
+
+ {course.currentParticipants}/{course.maxParticipants} Teilnehmer
+
+
+
+
+
+ {isFull ? (
+
+ ) : (
+
+ )}
+
+
+
+ >
+ );
+};
+
+export default PublicCourseCard;
diff --git a/src/modules/courses/ui/views/business-public-view.tsx b/src/modules/courses/ui/views/business-public-view.tsx
new file mode 100644
index 0000000..ef49368
--- /dev/null
+++ b/src/modules/courses/ui/views/business-public-view.tsx
@@ -0,0 +1,197 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { CalendarIcon, X } from "lucide-react";
+import PublicCourseCard from "../components/PublicCourseCard";
+import { getPublishedCoursesForBusiness } from "../../actions/booking-actions";
+import { Calendar } from "@/src/components/ui/calendar";
+import { Popover, PopoverContent, PopoverTrigger } from "@/src/components/ui/popover";
+import { Button } from "@/src/components/ui/button";
+
+interface Business {
+ id: string;
+ name: string;
+ address: string;
+ email: string;
+ title: string;
+ slug: string;
+}
+
+interface PublicCourse {
+ id: string;
+ name: string;
+ sport: string[];
+ level: string;
+ date: Date;
+ timeFrom: string;
+ timeTo: string;
+ maxParticipants: number;
+ price: number;
+ currentParticipants: number;
+ description?: string;
+ room?: string;
+}
+
+interface BusinessPublicViewProps {
+ business: Business;
+ initialCourses: PublicCourse[];
+ initialWindow: { from: Date; to: Date };
+}
+
+const BusinessPublicView = ({
+ business,
+ initialCourses,
+ initialWindow,
+}: BusinessPublicViewProps) => {
+ const [loadedCourses, setLoadedCourses] = useState
(initialCourses);
+ const [loadedWindow, setLoadedWindow] = useState(initialWindow);
+ const [selectedDate, setSelectedDate] = useState(undefined);
+ const [calendarOpen, setCalendarOpen] = useState(false);
+ const [isPending, startTransition] = useTransition();
+
+ const handleDateSelect = (date: Date | undefined) => {
+ setCalendarOpen(false);
+ setSelectedDate(date);
+
+ if (!date) return;
+
+ const dayStart = new Date(date);
+ dayStart.setHours(0, 0, 0, 0);
+ const dayEnd = new Date(date);
+ dayEnd.setHours(23, 59, 59, 999);
+
+ // If within loaded window — filter client-side, no fetch needed
+ if (dayStart >= loadedWindow.from && dayEnd <= loadedWindow.to) {
+ return;
+ }
+
+ // Outside window — fetch ±15 day window around chosen date
+ const newFrom = new Date(date);
+ newFrom.setDate(newFrom.getDate() - 15);
+ newFrom.setHours(0, 0, 0, 0);
+ const newTo = new Date(date);
+ newTo.setDate(newTo.getDate() + 15);
+ newTo.setHours(23, 59, 59, 999);
+
+ startTransition(async () => {
+ const result = await getPublishedCoursesForBusiness(business.id, {
+ from: newFrom,
+ to: newTo,
+ });
+ if (result.success && result.courses) {
+ setLoadedCourses(result.courses as PublicCourse[]);
+ setLoadedWindow({ from: newFrom, to: newTo });
+ }
+ });
+ };
+
+ const handleReset = () => {
+ setSelectedDate(undefined);
+ if (
+ loadedWindow.from.getTime() !== initialWindow.from.getTime() ||
+ loadedWindow.to.getTime() !== initialWindow.to.getTime()
+ ) {
+ startTransition(async () => {
+ const result = await getPublishedCoursesForBusiness(business.id, initialWindow);
+ if (result.success && result.courses) {
+ setLoadedCourses(result.courses as PublicCourse[]);
+ setLoadedWindow(initialWindow);
+ }
+ });
+ }
+ };
+
+ const displayedCourses = selectedDate
+ ? loadedCourses.filter((c) => {
+ const d = new Date(c.date);
+ const dayStart = new Date(selectedDate);
+ dayStart.setHours(0, 0, 0, 0);
+ const dayEnd = new Date(selectedDate);
+ dayEnd.setHours(23, 59, 59, 999);
+ return d >= dayStart && d <= dayEnd;
+ })
+ : loadedCourses;
+
+ const formattedSelectedDate = selectedDate
+ ? selectedDate.toLocaleDateString("de-DE", { weekday: "long", day: "numeric", month: "long" })
+ : null;
+
+ return (
+
+ {/* Header */}
+
+
+
{business.name}
+ {business.title &&
{business.title}
}
+
+
+
+
+ {/* Courses Section */}
+
+
+
Unsere Kurse
+
+
+
+
+
+
+
+
+
+
+
+ {selectedDate && (
+
+ )}
+
+
+
+ {isPending ? (
+
+ ) : displayedCourses.length === 0 ? (
+
+ {selectedDate
+ ? "Für diesen Tag sind keine Kurse verfügbar."
+ : "Aktuell sind keine Kurse verfügbar."}
+
+ ) : (
+
+ {displayedCourses.map((course) => (
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+export default BusinessPublicView;
diff --git a/src/modules/courses/ui/views/course-create-view.tsx b/src/modules/courses/ui/views/course-create-view.tsx
index 8c688cf..441afcb 100644
--- a/src/modules/courses/ui/views/course-create-view.tsx
+++ b/src/modules/courses/ui/views/course-create-view.tsx
@@ -8,7 +8,7 @@ import RichTextEditor from "@/src/components/layout/RichTextEditor";
import { Button } from "@/src/components/ui/button";
import { Checkbox } from "@/src/components/ui/checkbox";
import { Label } from "@/src/components/ui/label";
-import { createCourse, updateCourse } from "../../actions/course-actions";
+import { createCourse, updateCourse, getMyBusinesses } from "../../actions/course-actions";
import { getMyRooms } from "@/src/modules/rooms/actions/room-actions";
import { getMyTrainers } from "@/src/modules/employee/actions/employee-actions";
import { toast } from "sonner";
@@ -80,6 +80,8 @@ const CourseCreateView = ({
const [isLoadingRooms, setIsLoadingRooms] = useState(true);
const [trainers, setTrainers] = useState>([]);
const [isLoadingTrainers, setIsLoadingTrainers] = useState(true);
+ const [selectedBusinessId, setSelectedBusinessId] = useState("");
+ const [businesses, setBusinesses] = useState>([]);
const {
deleteDialogOpen,
@@ -93,6 +95,21 @@ const CourseCreateView = ({
onSuccess: () => router.push("/courses"),
});
+ // Load businesses from database
+ useEffect(() => {
+ const loadBusinesses = async () => {
+ const result = await getMyBusinesses();
+ if (result.success) {
+ const opts = result.businesses.map((b) => ({ value: b.id, label: b.name }));
+ setBusinesses(opts);
+ if (opts.length === 1) setSelectedBusinessId(opts[0].value);
+ } else {
+ toast.error("Fehler beim Laden der Standorte. Bitte laden Sie die Seite neu.");
+ }
+ };
+ loadBusinesses();
+ }, []);
+
// Load rooms from database
useEffect(() => {
const loadRooms = async () => {
@@ -160,6 +177,7 @@ const CourseCreateView = ({
setIsStandingOrder(initialData.isStandingOrder || false);
setSelectedFrequency(initialData.frequency || "");
setSelectedWeekdays(initialData.weekdays || []);
+ setSelectedBusinessId(initialData.businessId ?? "");
}
}, [mode, initialData, customSports]);
@@ -244,6 +262,7 @@ const CourseCreateView = ({
isStandingOrder,
frequency: selectedFrequency || undefined,
weekdays: selectedWeekdays.length > 0 ? selectedWeekdays : undefined,
+ businessId: selectedBusinessId || undefined,
};
if (mode === "edit" && !courseId) {
@@ -298,6 +317,15 @@ const CourseCreateView = ({
)}