I´d like to create a booking platform for a beauty shop (nail design). the customer shall be able to book a treatment. an admin backend is needed to manage articles and their durations.

This commit is contained in:
Quests Agent
2025-09-29 18:01:00 +02:00
parent a4ecf845bf
commit 63a402b3ad
9 changed files with 1068 additions and 27 deletions

View File

@@ -0,0 +1,225 @@
import { useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { queryClient } from "@/client/rpc-client";
export function BookingForm() {
const [selectedTreatment, setSelectedTreatment] = useState("");
const [customerName, setCustomerName] = useState("");
const [customerEmail, setCustomerEmail] = useState("");
const [customerPhone, setCustomerPhone] = useState("");
const [appointmentDate, setAppointmentDate] = useState("");
const [appointmentTime, setAppointmentTime] = useState("");
const [notes, setNotes] = useState("");
const { data: treatments } = useQuery(
queryClient.treatments.live.list.experimental_liveOptions()
);
const { data: allBookings } = useQuery(
queryClient.bookings.live.list.experimental_liveOptions()
);
const selectedDateBookings = allBookings?.filter(booking =>
booking.appointmentDate === appointmentDate
) || [];
const { mutate: createBooking, isPending } = useMutation(
queryClient.bookings.create.mutationOptions()
);
const selectedTreatmentData = treatments?.find(t => t.id === selectedTreatment);
// Generate available time slots (9 AM to 6 PM, 30-minute intervals)
const generateTimeSlots = () => {
const slots: string[] = [];
for (let hour = 9; hour < 18; hour++) {
for (let minute = 0; minute < 60; minute += 30) {
const time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
slots.push(time);
}
}
return slots;
};
const availableTimeSlots = generateTimeSlots().filter(time => {
if (!selectedDateBookings || !selectedTreatmentData) return true;
// Check if this time slot conflicts with existing bookings
const timeSlotStart = new Date(`${appointmentDate}T${time}:00`);
const timeSlotEnd = new Date(timeSlotStart.getTime() + selectedTreatmentData.duration * 60000);
return !selectedDateBookings.some(booking => {
if (booking.status === 'cancelled') return false;
const bookingStart = new Date(`${booking.appointmentDate}T${booking.appointmentTime}:00`);
const treatment = treatments?.find(t => t.id === booking.treatmentId);
if (!treatment) return false;
const bookingEnd = new Date(bookingStart.getTime() + treatment.duration * 60000);
return (timeSlotStart < bookingEnd && timeSlotEnd > bookingStart);
});
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!selectedTreatment || !customerName || !customerEmail || !customerPhone || !appointmentDate || !appointmentTime) {
alert("Please fill in all required fields");
return;
}
createBooking({
treatmentId: selectedTreatment,
customerName,
customerEmail,
customerPhone,
appointmentDate,
appointmentTime,
notes,
}, {
onSuccess: () => {
setSelectedTreatment("");
setCustomerName("");
setCustomerEmail("");
setCustomerPhone("");
setAppointmentDate("");
setAppointmentTime("");
setNotes("");
alert("Booking created successfully! We'll contact you to confirm your appointment.");
}
});
};
// Get minimum date (today)
const today = new Date().toISOString().split('T')[0];
return (
<div className="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Book Your Nail Treatment</h2>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Treatment Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Treatment *
</label>
<select
value={selectedTreatment}
onChange={(e) => setSelectedTreatment(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required
>
<option value="">Choose a treatment</option>
{treatments?.map((treatment) => (
<option key={treatment.id} value={treatment.id}>
{treatment.name} - ${(treatment.price / 100).toFixed(2)} ({treatment.duration} min)
</option>
))}
</select>
{selectedTreatmentData && (
<p className="mt-2 text-sm text-gray-600">{selectedTreatmentData.description}</p>
)}
</div>
{/* Customer Information */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name *
</label>
<input
type="text"
value={customerName}
onChange={(e) => setCustomerName(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email *
</label>
<input
type="email"
value={customerEmail}
onChange={(e) => setCustomerEmail(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Phone Number *
</label>
<input
type="tel"
value={customerPhone}
onChange={(e) => setCustomerPhone(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required
/>
</div>
{/* Date and Time Selection */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Preferred Date *
</label>
<input
type="date"
value={appointmentDate}
onChange={(e) => setAppointmentDate(e.target.value)}
min={today}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Preferred Time *
</label>
<select
value={appointmentTime}
onChange={(e) => setAppointmentTime(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
disabled={!appointmentDate || !selectedTreatment}
required
>
<option value="">Select time</option>
{availableTimeSlots.map((time) => (
<option key={time} value={time}>
{time}
</option>
))}
</select>
</div>
</div>
{/* Notes */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Additional Notes
</label>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-pink-500 focus:border-pink-500"
placeholder="Any special requests or information..."
/>
</div>
<button
type="submit"
disabled={isPending}
className="w-full bg-pink-600 text-white py-3 px-4 rounded-md hover:bg-pink-700 focus:ring-2 focus:ring-pink-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
>
{isPending ? "Booking..." : "Book Appointment"}
</button>
</form>
</div>
);
}