added price input

This commit is contained in:
Rajpreet0 2026-01-15 18:48:06 +01:00
parent 9fc4496b95
commit 6773173f49
8 changed files with 82 additions and 2 deletions

View file

@ -111,6 +111,7 @@ model Course {
room String
description String // Rich text HTML content
maxParticipants Int // Maximum number of participants
price Float @default(10.0) // Preis in Euro
isStandingOrder Boolean @default(false)
frequency String? // daily, weekly, biweekly, monthly, custom
weekdays String[] // For custom frequency

View file

@ -11,6 +11,8 @@ interface InputProps {
isLabel?: boolean;
placeholder?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onBlur?: (e: React.FocusEvent<HTMLInputElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
value?: string;
error?: string;
max?: string;
@ -23,6 +25,8 @@ const InputComponent: React.FC<InputProps> = ({
placeholder,
isLabel,
onChange,
onBlur,
onFocus,
value,
max,
error,
@ -45,6 +49,8 @@ const InputComponent: React.FC<InputProps> = ({
id={id}
placeholder={placeholder}
onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
value={value}
max={max}
className="h-[50px] border-blue rounded-xl w-full"

View file

@ -70,6 +70,7 @@ export async function createCourse(data: CourseFormData, status: "draft" | "publ
room: validatedData.room,
description: validatedData.description,
maxParticipants: validatedData.maxParticipants,
price: validatedData.price,
isStandingOrder: validatedData.isStandingOrder,
frequency: validatedData.frequency || null,
weekdays: validatedData.weekdays || [],
@ -346,6 +347,7 @@ export async function updateCourse(
room: validatedData.room,
description: validatedData.description,
maxParticipants: validatedData.maxParticipants,
price: validatedData.price,
isStandingOrder: validatedData.isStandingOrder,
frequency: validatedData.frequency || null,
weekdays: validatedData.weekdays || [],

View file

@ -49,6 +49,12 @@ export const courseSchema = z
.min(1, "Mindestens 1 Teilnehmer erforderlich")
.max(100, "Maximal 100 Teilnehmer erlaubt"),
// Preis in Euro (0-9999.99)
price: z
.number()
.min(0, "Preis muss eine gültige positive Zahl sein")
.max(9999.99, "Preis darf maximal 9.999,99 € betragen"),
// Gibt an, ob der Kurs ein Dauerauftrag ist (regelmäßig wiederkehrend)
isStandingOrder: z.boolean(),

View file

@ -9,6 +9,7 @@ export type Course = {
room: string;
description: string;
maxParticipants: number;
price: number;
isStandingOrder: boolean;
frequency: string | null;
weekdays: string[];

View file

@ -22,6 +22,7 @@ interface CourseListItemProps {
timeTo: string; // Uhrzeit am Ende des Kurses
currentParticipants: number; // Aktuelle Anzahl der Teilnehmer
maxParticipants: number; // Maximale Anzahl der Teilnehmer
price: number; // Preis in Euro
trainers: string[];
onEdit?: () => void; // Callback-Funktion beim Klick auf Bearbeiten-Button
onDelete?: () => void; // Callback-Funktion beim Klick auf Löschen-Button
@ -42,6 +43,7 @@ const CourseListItem: React.FC<CourseListItemProps> = ({
timeTo,
currentParticipants,
maxParticipants,
price,
trainers,
onEdit,
onDelete,
@ -108,8 +110,13 @@ const CourseListItem: React.FC<CourseListItemProps> = ({
{currentParticipants}/{maxParticipants} Teilnehmer
</span>
{/*TODO: Price */}
<h2 className="text-lg">15 €</h2>
{/* Price */}
<h2 className="text-lg font-semibold">
{price.toLocaleString("de-DE", {
style: "currency",
currency: "EUR",
})}
</h2>
</div>
{/* Participant Bar */}

View file

@ -72,6 +72,9 @@ const CourseCreateView = ({
const [isStandingOrder, setIsStandingOrder] = useState(false);
const [selectedFrequency, setSelectedFrequency] = useState("");
const [selectedWeekdays, setSelectedWeekdays] = useState<string[]>([]);
const [price, setPrice] = useState("");
const [priceDisplay, setPriceDisplay] = useState("");
const [isPriceFocused, setIsPriceFocused] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [rooms, setRooms] = useState<Array<{ value: string; label: string }>>([]);
@ -119,6 +122,7 @@ const CourseCreateView = ({
setSelectedRoom(initialData.room || "");
setDescription(initialData.description || "");
setMaxParticipants(initialData.maxParticipants?.toString() || "");
setPrice(initialData.price?.toString() || "");
setIsStandingOrder(initialData.isStandingOrder || false);
setSelectedFrequency(initialData.frequency || "");
setSelectedWeekdays(initialData.weekdays || []);
@ -151,6 +155,15 @@ const CourseCreateView = ({
newErrors.maxParticipants = "Maximal 100 Teilnehmer erlaubt";
}
const priceValue = parseFloat(price);
if (!price.trim()) {
newErrors.price = "Preis ist erforderlich";
} else if (isNaN(priceValue) || priceValue < 0) {
newErrors.price = "Preis muss eine gültige positive Zahl sein";
} else if (priceValue > 9999.99) {
newErrors.price = "Preis darf maximal 9.999,99 € betragen";
}
if (isStandingOrder) {
if (!selectedFrequency)
newErrors.frequency = "Häufigkeit ist erforderlich bei Daueraufträgen";
@ -182,6 +195,7 @@ const CourseCreateView = ({
room: selectedRoom,
description,
maxParticipants: parseInt(maxParticipants),
price: parseFloat(price),
isStandingOrder,
frequency: selectedFrequency || undefined,
weekdays: selectedWeekdays.length > 0 ? selectedWeekdays : undefined,
@ -284,6 +298,47 @@ const CourseCreateView = ({
{errors.timeTo && <p className="text-red-500 text-sm mt-1">{errors.timeTo}</p>}
</div>
<div>
<InputComponent
isLabel
label="Preis (€)"
type="text"
id="course-create-price"
value={
isPriceFocused
? priceDisplay
: price
? parseFloat(price).toLocaleString("de-DE", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})
: ""
}
onChange={(e) => {
const value = e.target.value;
// Remove all non-numeric characters except comma and dot
const cleanValue = value.replace(/[^\d,\.]/g, "");
// Replace comma with dot for internal storage
const normalizedValue = cleanValue.replace(",", ".");
setPriceDisplay(value);
// Allow empty string, numbers with optional decimal point and max 2 decimal places
if (normalizedValue === "" || /^\d*\.?\d{0,2}$/.test(normalizedValue)) {
setPrice(normalizedValue);
}
}}
onFocus={() => {
setIsPriceFocused(true);
setPriceDisplay(price.replace(".", ","));
}}
onBlur={() => {
setIsPriceFocused(false);
}}
placeholder="0,00"
/>
{errors.price && <p className="text-red-500 text-sm mt-1">{errors.price}</p>}
</div>
<MultiSelectDropdown
label="Trainer"
selected={selectedTrainers}

View file

@ -21,6 +21,7 @@ type Course = {
room: string;
description: string;
maxParticipants: number;
price: number;
isStandingOrder: boolean;
frequency: string | null;
weekdays: string[];
@ -433,6 +434,7 @@ const CourseListView = () => {
timeTo={course.timeTo}
currentParticipants={course._count?.bookings || 0}
maxParticipants={course.maxParticipants}
price={course.price}
trainers={course.trainers}
status={course.status}
isPast={isPastCourse(course.date)}