booking form validation and data types

This commit is contained in:
Asya Vee
2025-08-26 18:06:44 +04:00
parent 07ff37f038
commit 47baf8dfe2
11 changed files with 661 additions and 279 deletions

Binary file not shown.

View File

@@ -8,7 +8,7 @@ export default function Home() {
</main> </main>
<footer className="bg-white text-black py-8 mt-12"> <footer className="bg-white text-black py-8 mt-12">
<div className="max-w-2xl mx-auto px-4 text-center"> <div className="max-w-2xl mx-auto px-4 text-center">
<p className="text-gray-600 mb-2">Questions? Get in touch</p> <p className="text-gray-600 mb-2">Get in touch</p>
<a <a
href="mailto:vitrify@asyavee.me" href="mailto:vitrify@asyavee.me"
className="text-blue-600 hover:text-blue-800 transition-colors font-medium" className="text-blue-600 hover:text-blue-800 transition-colors font-medium"

View File

@@ -1,16 +1,34 @@
'use client' 'use client'
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { Calendar, Clock, User, Mail, Users } from 'lucide-react'; import { useForm } from 'react-hook-form';
import BookingTypeSelector from './BookingTypeSelector';
import DateSelector from './DateSelector';
import TimeSlotSelector from './TimeSlotSelector';
import CustomerDetails from './CustomerDetails';
import BookingSummary from './BookingSummary';
interface FormData {
customerName: string;
customerEmail: string;
participantsCount: number;
}
const BookingInterface = () => { const BookingInterface = () => {
const [selectedDate, setSelectedDate] = useState(''); const [selectedDate, setSelectedDate] = useState('');
const [selectedTimeSlot, setSelectedTimeSlot] = useState(''); const [selectedTimeSlot, setSelectedTimeSlot] = useState('');
const [selectedBookingType, setSelectedBookingType] = useState(''); const [selectedBookingType, setSelectedBookingType] = useState('');
const [customerName, setCustomerName] = useState('');
const [customerEmail, setCustomerEmail] = useState('');
const [participantsCount, setParticipantsCount] = useState(1); const [participantsCount, setParticipantsCount] = useState(1);
const { register, handleSubmit, formState: { errors, isValid }, setValue, trigger } = useForm<FormData>({
mode: 'onChange',
defaultValues: {
customerName: '',
customerEmail: '',
participantsCount: 1
}
});
// Mock data - will be replaced with API calls // Mock data - will be replaced with API calls
const bookingTypes = [ const bookingTypes = [
{ id: '1', name: 'wheel_rental', display_name: 'Wheel Rental', requires_payment: false, price_per_person: 0 }, { id: '1', name: 'wheel_rental', display_name: 'Wheel Rental', requires_payment: false, price_per_person: 0 },
@@ -30,35 +48,44 @@ const BookingInterface = () => {
const selectedBookingTypeData = bookingTypes.find(bt => bt.id === selectedBookingType); const selectedBookingTypeData = bookingTypes.find(bt => bt.id === selectedBookingType);
const handleSubmit = () => { useEffect(() => {
console.log({ setValue('participantsCount', participantsCount);
selectedBookingType, trigger('participantsCount');
selectedDate, }, [participantsCount, setValue, trigger]);
selectedTimeSlot,
customerName, const onSubmit = (data: FormData) => {
customerEmail, const selectedTimeSlotData = timeSlots.find(slot => slot.id === selectedTimeSlot);
participantsCount
}); const formData = {
bookingTypeId: selectedBookingType,
customerName: data.customerName,
customerEmail: data.customerEmail,
startTime: selectedDate && selectedTimeSlotData
? `${selectedDate}T${selectedTimeSlotData.start_time}:00`
: '',
endTime: selectedDate && selectedTimeSlotData
? `${selectedDate}T${selectedTimeSlotData.end_time}:00`
: '',
participantsCount: data.participantsCount,
};
console.log(formData);
}; };
const generateCalendarDays = () => { const handleParticipantsCountChange = (count: number) => {
const today = new Date(); setParticipantsCount(count);
const days = [];
for (let i = 0; i < 14; i++) {
const date = new Date(today);
date.setDate(today.getDate() + i);
days.push({
date: date.toISOString().split('T')[0],
day: date.getDate(),
month: date.toLocaleDateString('en', { month: 'short' }),
dayName: date.toLocaleDateString('en', { weekday: 'short' })
});
}
return days;
}; };
const calendarDays = generateCalendarDays(); const handleBookingTypeChange = (typeId: string) => {
setSelectedBookingType(typeId);
setSelectedDate('');
setSelectedTimeSlot('');
};
const handleDateChange = (date: string) => {
setSelectedDate(date);
setSelectedTimeSlot('');
};
return ( return (
<div className="min-h-screen bg-gray-50 p-4"> <div className="min-h-screen bg-gray-50 p-4">
@@ -71,201 +98,53 @@ const BookingInterface = () => {
<div className="space-y-8"> <div className="space-y-8">
{/* Booking Type Selection */} {/* Booking Type Selection */}
<div className="bg-white rounded-xl p-6 shadow-sm"> <BookingTypeSelector
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2"> bookingTypes={bookingTypes}
<Calendar className="w-5 h-5" /> selectedBookingType={selectedBookingType}
Select Experience onBookingTypeChange={handleBookingTypeChange}
</h2> />
<div className="grid gap-3">
{bookingTypes.map((type) => (
<label key={type.id} className="cursor-pointer">
<input
type="radio"
name="bookingType"
value={type.id}
checked={selectedBookingType === type.id}
onChange={(e) => setSelectedBookingType(e.target.value)}
className="sr-only"
/>
<div className={`p-4 rounded-lg border-2 transition-all ${
selectedBookingType === type.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}>
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium text-gray-900">{type.display_name}</h3>
{type.requires_payment && (
<p className="text-sm text-gray-600 mt-1">
${type.price_per_person} per person
</p>
)}
</div>
{type.requires_payment && (
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
Payment Required
</span>
)}
</div>
</div>
</label>
))}
</div>
</div>
{/* Date Selection */} {/* Date Selection */}
{selectedBookingType && ( {selectedBookingType && (
<div className="bg-white rounded-xl p-6 shadow-sm"> <DateSelector
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2"> selectedDate={selectedDate}
<Calendar className="w-5 h-5" /> onDateChange={handleDateChange}
Choose Date />
</h2>
<div className="grid grid-cols-7 gap-2 md:gap-3">
{calendarDays.map((day) => (
<button
key={day.date}
type="button"
onClick={() => setSelectedDate(day.date)}
className={`p-3 rounded-lg text-center transition-all ${
selectedDate === day.date
? 'bg-blue-500 text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-700'
}`}
>
<div className="text-xs font-medium">{day.dayName}</div>
<div className="text-sm font-semibold">{day.day}</div>
<div className="text-xs">{day.month}</div>
</button>
))}
</div>
</div>
)} )}
{/* Time Slot Selection */} {/* Time Slot Selection */}
{selectedDate && ( {selectedDate && (
<div className="bg-white rounded-xl p-6 shadow-sm"> <TimeSlotSelector
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2"> timeSlots={timeSlots}
<Clock className="w-5 h-5" /> selectedTimeSlot={selectedTimeSlot}
Available Times onTimeSlotChange={setSelectedTimeSlot}
</h2> />
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{timeSlots.map((slot) => (
<button
key={slot.id}
type="button"
disabled={!slot.available}
onClick={() => setSelectedTimeSlot(slot.id)}
className={`p-3 rounded-lg text-center transition-all ${
!slot.available
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: selectedTimeSlot === slot.id
? 'bg-blue-500 text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-700'
}`}
>
<div className="font-medium">
{slot.start_time} - {slot.end_time}
</div>
</button>
))}
</div>
</div>
)} )}
{/* Customer Details */} {/* Customer Details */}
{selectedTimeSlot && ( {selectedTimeSlot && (
<div className="bg-white rounded-xl p-6 shadow-sm"> <form onSubmit={handleSubmit(onSubmit)}>
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2"> <CustomerDetails
<User className="w-5 h-5" /> register={register}
Your Details errors={errors}
</h2> participantsCount={participantsCount}
onParticipantsCountChange={handleParticipantsCountChange}
/>
<div className="space-y-4"> {/* Summary & Submit */}
<div> {isValid && (
<label className="block text-sm font-medium text-gray-700 mb-2"> <div className="mt-8">
Full Name * <BookingSummary
</label> selectedBookingTypeData={selectedBookingTypeData}
<input selectedDate={selectedDate}
type="text" selectedTimeSlot={selectedTimeSlot}
value={customerName} timeSlots={timeSlots}
onChange={(e) => setCustomerName(e.target.value)} participantsCount={participantsCount}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" onSubmit={handleSubmit(onSubmit)}
placeholder="Enter your full name"
/> />
</div> </div>
)}
<div> </form>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address *
</label>
<input
type="email"
value={customerEmail}
onChange={(e) => setCustomerEmail(e.target.value)}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter your email"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2 flex items-center gap-2">
<Users className="w-4 h-4" />
Number of Participants
</label>
<select
value={participantsCount}
onChange={(e) => setParticipantsCount(parseInt(e.target.value))}
className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
{[1, 2, 3, 4, 5, 6, 7, 8].map(num => (
<option key={num} value={num}>{num} participant{num > 1 ? 's' : ''}</option>
))}
</select>
</div>
</div>
</div>
)}
{/* Summary & Submit */}
{selectedTimeSlot && customerName && customerEmail && (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Booking Summary</h2>
<div className="space-y-3 mb-6">
<div className="flex justify-between">
<span className="text-gray-600">Experience:</span>
<span className="font-medium">{selectedBookingTypeData?.display_name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Date:</span>
<span className="font-medium">{selectedDate}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Time:</span>
<span className="font-medium">
{timeSlots.find(s => s.id === selectedTimeSlot)?.start_time} -
{timeSlots.find(s => s.id === selectedTimeSlot)?.end_time}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Participants:</span>
<span className="font-medium">{participantsCount}</span>
</div>
{selectedBookingTypeData?.requires_payment && (
<div className="flex justify-between text-lg font-semibold border-t pt-3">
<span>Total:</span>
<span>${selectedBookingTypeData.price_per_person * participantsCount}</span>
</div>
)}
</div>
<button
onClick={handleSubmit}
className="w-full bg-blue-600 text-white py-4 px-6 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
{selectedBookingTypeData?.requires_payment ? 'Continue to Payment' : 'Confirm Booking'}
</button>
</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -0,0 +1,78 @@
import React from 'react';
interface BookingType {
id: string;
name: string;
display_name: string;
requires_payment: boolean;
price_per_person: number;
}
interface TimeSlot {
id: string;
start_time: string;
end_time: string;
available: boolean;
}
interface BookingSummaryProps {
selectedBookingTypeData: BookingType | undefined;
selectedDate: string;
selectedTimeSlot: string;
timeSlots: TimeSlot[];
participantsCount: number;
onSubmit: () => void;
}
const BookingSummary: React.FC<BookingSummaryProps> = ({
selectedBookingTypeData,
selectedDate,
selectedTimeSlot,
timeSlots,
participantsCount,
onSubmit,
}) => {
const selectedTimeSlotData = timeSlots.find(s => s.id === selectedTimeSlot);
return (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Booking Summary</h2>
<div className="space-y-3 mb-6">
<div className="flex justify-between">
<span className="text-gray-600">Experience:</span>
<span className="font-medium">{selectedBookingTypeData?.display_name}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Date:</span>
<span className="font-medium">{selectedDate}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Time:</span>
<span className="font-medium">
{selectedTimeSlotData?.start_time} - {selectedTimeSlotData?.end_time}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Participants:</span>
<span className="font-medium">{participantsCount}</span>
</div>
{selectedBookingTypeData?.requires_payment && (
<div className="flex justify-between text-lg font-semibold border-t pt-3">
<span>Total:</span>
<span>{selectedBookingTypeData.price_per_person * participantsCount}</span>
</div>
)}
</div>
<button
onClick={onSubmit}
className="w-full bg-blue-600 text-white py-4 px-6 rounded-lg font-semibold hover:bg-blue-700 transition-colors"
>
{selectedBookingTypeData?.requires_payment ? 'Continue to Payment' : 'Confirm Booking'}
</button>
</div>
);
};
export default BookingSummary;

View File

@@ -0,0 +1,68 @@
import React from 'react';
import { Calendar } from 'lucide-react';
interface BookingType {
id: string;
name: string;
display_name: string;
requires_payment: boolean;
price_per_person: number;
}
interface BookingTypeSelectorProps {
bookingTypes: BookingType[];
selectedBookingType: string;
onBookingTypeChange: (typeId: string) => void;
}
const BookingTypeSelector: React.FC<BookingTypeSelectorProps> = ({
bookingTypes,
selectedBookingType,
onBookingTypeChange,
}) => {
return (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Calendar className="w-5 h-5" />
Select Experience
</h2>
<div className="grid gap-3">
{bookingTypes.map((type) => (
<label key={type.id} className="cursor-pointer">
<input
type="radio"
name="bookingType"
value={type.id}
checked={selectedBookingType === type.id}
onChange={(e) => onBookingTypeChange(e.target.value)}
className="sr-only"
/>
<div className={`p-4 rounded-lg border-2 transition-all ${
selectedBookingType === type.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}>
<div className="flex justify-between items-start">
<div>
<h3 className="font-medium text-gray-900">{type.display_name}</h3>
{type.requires_payment && (
<p className="text-sm text-gray-600 mt-1">
${type.price_per_person} per person
</p>
)}
</div>
{type.requires_payment && (
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full">
Payment Required
</span>
)}
</div>
</div>
</label>
))}
</div>
</div>
);
};
export default BookingTypeSelector;

View File

@@ -0,0 +1,116 @@
import React from 'react';
import { User, Users } from 'lucide-react';
import { UseFormRegister, FieldErrors } from 'react-hook-form';
interface FormData {
customerName: string;
customerEmail: string;
participantsCount: number;
}
interface CustomerDetailsProps {
register: UseFormRegister<FormData>;
errors: FieldErrors<FormData>;
participantsCount: number;
onParticipantsCountChange: (count: number) => void;
}
const CustomerDetails: React.FC<CustomerDetailsProps> = ({
register,
errors,
participantsCount,
onParticipantsCountChange,
}) => {
return (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2">
<User className="w-5 h-5" />
Your Details
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Full Name *
</label>
<input
type="text"
{...register('customerName', {
required: 'Full name is required',
minLength: { value: 2, message: 'Name must be at least 2 characters' }
})}
className={`w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.customerName ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="Enter your full name"
/>
{errors.customerName && (
<p className="text-red-500 text-sm mt-1">{errors.customerName.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Email Address *
</label>
<input
type="email"
{...register('customerEmail', {
required: 'Email address is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Please enter a valid email address'
}
})}
className={`w-full p-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${
errors.customerEmail ? 'border-red-300' : 'border-gray-300'
}`}
placeholder="Enter your email"
/>
{errors.customerEmail && (
<p className="text-red-500 text-sm mt-1">{errors.customerEmail.message}</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2 flex items-center gap-2">
<Users className="w-4 h-4" />
Number of Participants
</label>
<div className="flex items-center justify-center bg-gray-100 rounded-lg p-1 max-w-xs">
<button
type="button"
onClick={() => onParticipantsCountChange(Math.max(1, participantsCount - 1))}
className="w-12 h-12 flex items-center justify-center rounded-lg bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-800 transition-colors"
disabled={participantsCount <= 1}
>
<span className="text-xl font-medium"></span>
</button>
<div className="flex-1 text-center px-4">
<span className="text-xl font-semibold text-gray-900">{participantsCount}</span>
<div className="text-xs text-gray-500">participant{participantsCount > 1 ? 's' : ''}</div>
</div>
<button
type="button"
onClick={() => onParticipantsCountChange(Math.min(8, participantsCount + 1))}
className="w-12 h-12 flex items-center justify-center rounded-lg bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-800 transition-colors"
disabled={participantsCount >= 8}
>
<span className="text-xl font-medium">+</span>
</button>
</div>
<input
type="hidden"
{...register('participantsCount', {
min: { value: 1, message: 'At least 1 participant required' },
max: { value: 8, message: 'Maximum 8 participants allowed' }
})}
value={participantsCount}
/>
</div>
</div>
</div>
);
};
export default CustomerDetails;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { Calendar } from 'lucide-react';
interface CalendarDay {
date: string;
day: number;
month: string;
dayName: string;
}
interface DateSelectorProps {
selectedDate: string;
onDateChange: (date: string) => void;
}
const DateSelector: React.FC<DateSelectorProps> = ({
selectedDate,
onDateChange,
}) => {
const generateCalendarDays = (): CalendarDay[] => {
const today = new Date();
const days = [];
for (let i = 0; i < 14; i++) {
const date = new Date(today);
date.setDate(today.getDate() + i);
days.push({
date: date.toISOString().split('T')[0],
day: date.getDate(),
month: date.toLocaleDateString('en', { month: 'short' }),
dayName: date.toLocaleDateString('en', { weekday: 'short' })
});
}
return days;
};
const calendarDays = generateCalendarDays();
return (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Calendar className="w-5 h-5" />
Choose Date
</h2>
<div className="grid grid-cols-7 gap-2 md:gap-3">
{calendarDays.map((day) => (
<button
key={day.date}
type="button"
onClick={() => onDateChange(day.date)}
className={`p-3 rounded-lg text-center transition-all ${
selectedDate === day.date
? 'bg-blue-500 text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-700'
}`}
>
<div className="text-xs font-medium">{day.dayName}</div>
<div className="text-sm font-semibold">{day.day}</div>
<div className="text-xs">{day.month}</div>
</button>
))}
</div>
</div>
);
};
export default DateSelector;

View File

@@ -0,0 +1,53 @@
import React from 'react';
import { Clock } from 'lucide-react';
interface TimeSlot {
id: string;
start_time: string;
end_time: string;
available: boolean;
}
interface TimeSlotSelectorProps {
timeSlots: TimeSlot[];
selectedTimeSlot: string;
onTimeSlotChange: (slotId: string) => void;
}
const TimeSlotSelector: React.FC<TimeSlotSelectorProps> = ({
timeSlots,
selectedTimeSlot,
onTimeSlotChange,
}) => {
return (
<div className="bg-white rounded-xl p-6 shadow-sm">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center gap-2">
<Clock className="w-5 h-5" />
Available Times
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{timeSlots.map((slot) => (
<button
key={slot.id}
type="button"
disabled={!slot.available}
onClick={() => onTimeSlotChange(slot.id)}
className={`p-3 rounded-lg text-center transition-all ${
!slot.available
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
: selectedTimeSlot === slot.id
? 'bg-blue-500 text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-700'
}`}
>
<div className="font-medium">
{slot.start_time} - {slot.end_time}
</div>
</button>
))}
</div>
</div>
);
};
export default TimeSlotSelector;

View File

@@ -19,6 +19,7 @@
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hook-form": "^7.62.0",
"tailwindcss": "^4.1.12" "tailwindcss": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,79 +1,186 @@
// types/booking.ts // Base types for PocketBase records
export interface BaseRecord {
export interface BookingType { id: string;
id: string; created: string;
name: string; updated: string;
display_name: string;
requires_payment: boolean;
default_duration: number; // in minutes
description?: string;
resources: string; // relation to resources collection
created: string;
updated: string;
} }
export interface Resource { // Resource entity
id: string; export interface Resource extends BaseRecord {
type: 'wheel' | 'workstation'; name: string;
capacity: number; type: 'wheel' | 'workstation';
is_active: boolean; capacity: number;
created: string; is_active: boolean;
updated: string;
} }
export interface TimeSlot { // Booking Type entity
id: string; export interface BookingType extends BaseRecord {
booking_types: string[]; // relation to booking types name: string;
start_time: string; display_name: string;
end_time: string; description?: string;
is_reccuring: boolean; requires_payment?: boolean;
max_capacity: number; base_duration: number; // minutes, min 30
is_active: boolean; min_duration: number;
recurrence_pattern?: { price_per_person: number;
type: string; resources: string; // relation to Resource
days: number[]; min_capacity: number;
end_date: string; max_capacity: number;
}; is_active: boolean;
created: string;
updated: string;
} }
export interface Booking { // Time Slot entity
id: string; export interface TimeSlot extends BaseRecord {
booking_type: string; // relation to booking type booking_types: string[]; // relation to BookingType (many-to-many)
customer_name: string; start_time: string;
customer_email: string; end_time: string;
internal_notes?: string; is_active: boolean;
start_time: string; is_reccuring?: boolean;
end_time: string; recurrence_pattern?: RecurrencePattern;
participants_count: number; max_capacity: number;
status: 'confirmed' | 'cancelled' | 'completed';
payment_status: 'not_required' | 'paid' | 'refunded' | 'partially_refunded' | 'pending';
payment_required: boolean;
cancellation_token: string;
created: string;
updated: string;
} }
// Recurrence pattern structure
export interface RecurrencePattern {
type: 'daily' | 'weekly' | 'monthly';
interval: number;
days_of_week?: number[]; // 0-6, Sunday = 0
end_date?: string;
occurrences?: number;
}
// Booking entity
export interface Booking extends BaseRecord {
booking_type: string; // relation to BookingType
customer_name: string;
customer_email: string;
internal_notes?: string;
start_time: string;
end_time: string;
participants_count: number;
status: 'confirmed' | 'cancelled' | 'completed';
payment_status: 'not_required' | 'paid' | 'refunded' | 'partially_refunded' | 'pending';
payment_required: boolean;
cancellation_token: string;
use_subscription?: string; // relation to subscription (future feature)
}
// Frontend form types
export interface BookingFormData { export interface BookingFormData {
bookingTypeId: string; bookingTypeId: string;
customerName: string; customerName: string;
customerEmail: string; customerEmail: string;
participantsCount: number; startTime: string;
startTime: Date; endTime: string;
endTime: Date; participantsCount: number;
internalNotes?: string;
} }
// For the booking flow state // API response types
export interface BookingFlowState { export interface BookingResponse {
step: 'booking-type' | 'date-time' | 'customer-details' | 'confirmation'; success: boolean;
selectedBookingType?: BookingType; booking?: Booking;
selectedDate?: Date; error?: string;
selectedTimeSlot?: string; // time slot ID or time string cancellationUrl?: string;
customerDetails?: {
name: string;
email: string;
participantsCount: number;
notes?: string;
};
} }
export interface AvailabilityResponse {
success: boolean;
availableSlots?: TimeSlot[];
error?: string;
}
// Expanded types with relations
export interface BookingWithRelations extends Omit<Booking, 'booking_type'> {
expand?: {
booking_type?: BookingType;
};
}
export interface BookingTypeWithRelations extends Omit<BookingType, 'resources'> {
expand?: {
resources?: Resource;
};
}
export interface TimeSlotWithRelations extends Omit<TimeSlot, 'booking_types'> {
expand?: {
booking_types?: BookingType[];
};
}
// Utility types for API operations
export interface CreateBookingData {
booking_type: string;
customer_name: string;
customer_email: string;
start_time: string;
end_time: string;
participants_count: number;
internal_notes?: string;
status: 'confirmed';
payment_status: 'not_required' | 'pending';
payment_required: boolean;
}
export interface UpdateBookingData {
status?: 'confirmed' | 'cancelled' | 'completed';
payment_status?: 'not_required' | 'paid' | 'refunded' | 'partially_refunded' | 'pending';
internal_notes?: string;
}
// Filter and query types
export interface BookingFilters {
status?: string;
payment_status?: string;
booking_type?: string;
date_from?: string;
date_to?: string;
customer_email?: string;
}
export interface TimeSlotFilters {
booking_type?: string;
date_from?: string;
date_to?: string;
is_active?: boolean;
}
// Error types
export interface BookingError {
code: string;
message: string;
field?: string;
}
// Validation types
export interface BookingValidation {
isValid: boolean;
errors: BookingError[];
}
// Constants
export const BOOKING_TYPES = {
WHEEL_RENTAL: 'wheel_rental',
HAND_BUILDING: 'hand_building_coworking',
PERSONAL_WORKSHOP: 'personal_workshop',
GROUP_WORKSHOP: 'group_workshop',
} as const;
export const BOOKING_STATUS = {
CONFIRMED: 'confirmed',
CANCELLED: 'cancelled',
COMPLETED: 'completed',
} as const;
export const PAYMENT_STATUS = {
NOT_REQUIRED: 'not_required',
PAID: 'paid',
REFUNDED: 'refunded',
PARTIALLY_REFUNDED: 'partially_refunded',
PENDING: 'pending',
} as const;
export const RESOURCE_TYPES = {
WHEEL: 'wheel',
WORKSTATION: 'workstation',
} as const;

13
pnpm-lock.yaml generated
View File

@@ -83,6 +83,9 @@ importers:
react-dom: react-dom:
specifier: ^19.1.0 specifier: ^19.1.0
version: 19.1.0(react@19.1.0) version: 19.1.0(react@19.1.0)
react-hook-form:
specifier: ^7.62.0
version: 7.62.0(react@19.1.0)
tailwindcss: tailwindcss:
specifier: ^4.1.12 specifier: ^4.1.12
version: 4.1.12 version: 4.1.12
@@ -1437,6 +1440,12 @@ packages:
peerDependencies: peerDependencies:
react: ^19.1.0 react: ^19.1.0
react-hook-form@7.62.0:
resolution: {integrity: sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19
react-is@16.13.1: react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@@ -3063,6 +3072,10 @@ snapshots:
react: 19.1.0 react: 19.1.0
scheduler: 0.26.0 scheduler: 0.26.0
react-hook-form@7.62.0(react@19.1.0):
dependencies:
react: 19.1.0
react-is@16.13.1: {} react-is@16.13.1: {}
react@19.1.0: {} react@19.1.0: {}