mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 21:41:17 +00:00
added price input
This commit is contained in:
parent
9fc4496b95
commit
6773173f49
8 changed files with 82 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 || [],
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export type Course = {
|
|||
room: string;
|
||||
description: string;
|
||||
maxParticipants: number;
|
||||
price: number;
|
||||
isStandingOrder: boolean;
|
||||
frequency: string | null;
|
||||
weekdays: string[];
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue