583 lines
25 KiB
Plaintext
583 lines
25 KiB
Plaintext
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { FiCalendar, FiMapPin, FiUser, FiMail, FiPhone, FiCamera, FiCheck, FiAlertCircle } from 'react-icons/fi';
|
|
|
|
interface Location {
|
|
id: string;
|
|
name: string;
|
|
slug: string;
|
|
city: string;
|
|
}
|
|
|
|
interface PriceConfig {
|
|
model: string;
|
|
basePrice: number;
|
|
pricePerKm: number;
|
|
includedKm: number;
|
|
}
|
|
|
|
const photoboxModels = [
|
|
{ value: 'VINTAGE_SMILE', label: 'Vintage Smile', description: 'Klassische Vintage-Box mit Sofortdruck' },
|
|
{ value: 'VINTAGE_PHOTOS', label: 'Vintage Photos', description: 'Vintage-Box mit erweiterten Funktionen' },
|
|
{ value: 'NOSTALGIE', label: 'Nostalgie', description: 'Nostalgische Fotobox im Retro-Design' },
|
|
{ value: 'MAGIC_MIRROR', label: 'Magic Mirror', description: 'Interaktiver Fotospiegel' },
|
|
];
|
|
|
|
export default function BookingFormPage() {
|
|
const [step, setStep] = useState(1);
|
|
const [locations, setLocations] = useState<Location[]>([]);
|
|
const [priceConfigs, setPriceConfigs] = useState<PriceConfig[]>([]);
|
|
const [availability, setAvailability] = useState<any>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
const [success, setSuccess] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const [formData, setFormData] = useState({
|
|
locationSlug: '',
|
|
model: '',
|
|
customerName: '',
|
|
customerEmail: '',
|
|
customerPhone: '',
|
|
customerAddress: '',
|
|
customerCity: '',
|
|
customerZip: '',
|
|
invoiceType: 'PRIVATE',
|
|
companyName: '',
|
|
eventDate: '',
|
|
eventAddress: '',
|
|
eventCity: '',
|
|
eventZip: '',
|
|
eventLocation: '',
|
|
setupTimeStart: '',
|
|
setupTimeLatest: '',
|
|
dismantleTimeEarliest: '',
|
|
dismantleTimeLatest: '',
|
|
notes: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetch('/api/locations')
|
|
.then(res => res.json())
|
|
.then(data => setLocations(data.locations || []));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (formData.locationSlug) {
|
|
fetch(`/api/prices?location=${formData.locationSlug}`)
|
|
.then(res => res.json())
|
|
.then(data => setPriceConfigs(data.prices || []));
|
|
}
|
|
}, [formData.locationSlug]);
|
|
|
|
useEffect(() => {
|
|
if (formData.locationSlug && formData.model && formData.eventDate) {
|
|
checkAvailability();
|
|
}
|
|
}, [formData.locationSlug, formData.model, formData.eventDate]);
|
|
|
|
const checkAvailability = async () => {
|
|
try {
|
|
const res = await fetch(
|
|
`/api/availability?location=${formData.locationSlug}&model=${formData.model}&date=${formData.eventDate}`
|
|
);
|
|
const data = await res.json();
|
|
setAvailability(data);
|
|
} catch (err) {
|
|
console.error('Availability check failed:', err);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError('');
|
|
|
|
try {
|
|
const res = await fetch('/api/bookings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(formData),
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.error || 'Buchung fehlgeschlagen');
|
|
}
|
|
|
|
setSuccess(true);
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getPrice = () => {
|
|
const config = priceConfigs.find(p => p.model === formData.model);
|
|
return config ? config.basePrice : 0;
|
|
};
|
|
|
|
if (success) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-green-100 flex items-center justify-center p-4">
|
|
<div className="max-w-md w-full bg-white rounded-2xl shadow-2xl p-8 text-center">
|
|
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<FiCheck className="text-3xl text-green-600" />
|
|
</div>
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-2">Anfrage erfolgreich!</h2>
|
|
<p className="text-gray-600 mb-6">
|
|
Vielen Dank für Ihre Buchungsanfrage. Wir melden uns in Kürze bei Ihnen mit allen Details.
|
|
</p>
|
|
<a
|
|
href="/booking"
|
|
className="inline-block px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
|
|
>
|
|
Weitere Buchung
|
|
</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-red-50 to-red-100 py-12 px-4">
|
|
<div className="max-w-4xl mx-auto">
|
|
<div className="bg-white rounded-2xl shadow-2xl overflow-hidden">
|
|
<div className="bg-red-600 text-white p-6">
|
|
<h1 className="text-3xl font-bold">Fotobox buchen</h1>
|
|
<p className="text-red-100 mt-2">Save the Moment - Ihr Event, unvergesslich</p>
|
|
</div>
|
|
|
|
<div className="p-8">
|
|
<div className="flex items-center justify-between mb-8">
|
|
{[1, 2, 3].map((s) => (
|
|
<div key={s} className="flex items-center">
|
|
<div
|
|
className={`w-10 h-10 rounded-full flex items-center justify-center font-bold ${
|
|
step >= s ? 'bg-red-600 text-white' : 'bg-gray-200 text-gray-500'
|
|
}`}
|
|
>
|
|
{s}
|
|
</div>
|
|
{s < 3 && <div className={`h-1 w-20 mx-2 ${step > s ? 'bg-red-600' : 'bg-gray-200'}`} />}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
{step === 1 && (
|
|
<div className="space-y-6">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">Standort & Fotobox wählen</h2>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
<FiMapPin className="inline mr-2" />
|
|
Standort
|
|
</label>
|
|
<select
|
|
value={formData.locationSlug}
|
|
onChange={(e) => setFormData({ ...formData, locationSlug: e.target.value })}
|
|
required
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
>
|
|
<option value="">Bitte wählen...</option>
|
|
{locations.map((loc) => (
|
|
<option key={loc.id} value={loc.slug}>
|
|
{loc.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
<FiCamera className="inline mr-2" />
|
|
Fotobox-Modell
|
|
</label>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{photoboxModels.map((model) => {
|
|
const price = priceConfigs.find(p => p.model === model.value);
|
|
return (
|
|
<label
|
|
key={model.value}
|
|
className={`border-2 rounded-lg p-4 cursor-pointer transition-all ${
|
|
formData.model === model.value
|
|
? 'border-red-600 bg-red-50'
|
|
: 'border-gray-200 hover:border-red-300'
|
|
}`}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="model"
|
|
value={model.value}
|
|
checked={formData.model === model.value}
|
|
onChange={(e) => setFormData({ ...formData, model: e.target.value })}
|
|
className="sr-only"
|
|
/>
|
|
<div className="font-semibold text-gray-900">{model.label}</div>
|
|
<div className="text-sm text-gray-600 mt-1">{model.description}</div>
|
|
{price && (
|
|
<div className="text-red-600 font-bold mt-2">
|
|
ab {price.basePrice}€
|
|
</div>
|
|
)}
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
<FiCalendar className="inline mr-2" />
|
|
Event-Datum
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={formData.eventDate}
|
|
onChange={(e) => setFormData({ ...formData, eventDate: e.target.value })}
|
|
required
|
|
min={new Date().toISOString().split('T')[0]}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
{availability && (
|
|
<div className={`mt-2 p-3 rounded-lg ${
|
|
availability.available
|
|
? availability.isLastOne
|
|
? 'bg-yellow-50 text-yellow-800 border border-yellow-200'
|
|
: 'bg-green-50 text-green-800 border border-green-200'
|
|
: 'bg-red-50 text-red-800 border border-red-200'
|
|
}`}>
|
|
<FiAlertCircle className="inline mr-2" />
|
|
{availability.message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setStep(2)}
|
|
disabled={!formData.locationSlug || !formData.model || !formData.eventDate || (availability && !availability.available)}
|
|
className="w-full bg-red-600 text-white py-3 rounded-lg font-semibold hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Weiter
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{step === 2 && (
|
|
<div className="space-y-6">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">Ihre Kontaktdaten</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Rechnungsart
|
|
</label>
|
|
<div className="flex gap-4">
|
|
<label className="flex-1">
|
|
<input
|
|
type="radio"
|
|
name="invoiceType"
|
|
value="PRIVATE"
|
|
checked={formData.invoiceType === 'PRIVATE'}
|
|
onChange={(e) => setFormData({ ...formData, invoiceType: e.target.value as any })}
|
|
className="mr-2"
|
|
/>
|
|
Privat
|
|
</label>
|
|
<label className="flex-1">
|
|
<input
|
|
type="radio"
|
|
name="invoiceType"
|
|
value="BUSINESS"
|
|
checked={formData.invoiceType === 'BUSINESS'}
|
|
onChange={(e) => setFormData({ ...formData, invoiceType: e.target.value as any })}
|
|
className="mr-2"
|
|
/>
|
|
Geschäftlich
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{formData.invoiceType === 'BUSINESS' && (
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Firmenname
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.companyName}
|
|
onChange={(e) => setFormData({ ...formData, companyName: e.target.value })}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
<FiUser className="inline mr-2" />
|
|
Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.customerName}
|
|
onChange={(e) => setFormData({ ...formData, customerName: e.target.value })}
|
|
required
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
<FiMail className="inline mr-2" />
|
|
E-Mail
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={formData.customerEmail}
|
|
onChange={(e) => setFormData({ ...formData, customerEmail: e.target.value })}
|
|
required
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
<FiPhone className="inline mr-2" />
|
|
Telefon
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={formData.customerPhone}
|
|
onChange={(e) => setFormData({ ...formData, customerPhone: e.target.value })}
|
|
required
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Adresse (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.customerAddress}
|
|
onChange={(e) => setFormData({ ...formData, customerAddress: e.target.value })}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
PLZ (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.customerZip}
|
|
onChange={(e) => setFormData({ ...formData, customerZip: e.target.value })}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Stadt (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.customerCity}
|
|
onChange={(e) => setFormData({ ...formData, customerCity: e.target.value })}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => setStep(1)}
|
|
className="flex-1 bg-gray-200 text-gray-800 py-3 rounded-lg font-semibold hover:bg-gray-300 transition-colors"
|
|
>
|
|
Zurück
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setStep(3)}
|
|
disabled={!formData.customerName || !formData.customerEmail || !formData.customerPhone}
|
|
className="flex-1 bg-red-600 text-white py-3 rounded-lg font-semibold hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Weiter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{step === 3 && (
|
|
<div className="space-y-6">
|
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">Event-Details</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Event-Adresse
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.eventAddress}
|
|
onChange={(e) => setFormData({ ...formData, eventAddress: e.target.value })}
|
|
required
|
|
placeholder="Straße und Hausnummer"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
PLZ
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.eventZip}
|
|
onChange={(e) => setFormData({ ...formData, eventZip: e.target.value })}
|
|
required
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Stadt
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.eventCity}
|
|
onChange={(e) => setFormData({ ...formData, eventCity: e.target.value })}
|
|
required
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Location-Name (optional)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.eventLocation}
|
|
onChange={(e) => setFormData({ ...formData, eventLocation: e.target.value })}
|
|
placeholder="z.B. Hotel Maritim, Villa Rosengarten"
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Aufbau möglich ab
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={formData.setupTimeStart}
|
|
onChange={(e) => setFormData({ ...formData, setupTimeStart: e.target.value })}
|
|
required
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Aufbau spätestens bis
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={formData.setupTimeLatest}
|
|
onChange={(e) => setFormData({ ...formData, setupTimeLatest: e.target.value })}
|
|
required
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Abbau frühestens ab (optional)
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={formData.dismantleTimeEarliest}
|
|
onChange={(e) => setFormData({ ...formData, dismantleTimeEarliest: e.target.value })}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Abbau spätestens bis (optional)
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={formData.dismantleTimeLatest}
|
|
onChange={(e) => setFormData({ ...formData, dismantleTimeLatest: e.target.value })}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Besondere Wünsche / Anmerkungen (optional)
|
|
</label>
|
|
<textarea
|
|
value={formData.notes}
|
|
onChange={(e) => setFormData({ ...formData, notes: e.target.value })}
|
|
rows={4}
|
|
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="bg-gray-50 p-4 rounded-lg">
|
|
<h3 className="font-semibold text-gray-900 mb-2">Zusammenfassung</h3>
|
|
<div className="space-y-1 text-sm text-gray-600">
|
|
<p>Modell: {photoboxModels.find(m => m.value === formData.model)?.label}</p>
|
|
<p>Datum: {new Date(formData.eventDate).toLocaleDateString('de-DE')}</p>
|
|
<p>Standort: {locations.find(l => l.slug === formData.locationSlug)?.name}</p>
|
|
<p className="text-lg font-bold text-gray-900 mt-2">
|
|
Basispreis: {getPrice()}€ *
|
|
</p>
|
|
<p className="text-xs text-gray-500">* zzgl. Anfahrtskosten</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => setStep(2)}
|
|
className="flex-1 bg-gray-200 text-gray-800 py-3 rounded-lg font-semibold hover:bg-gray-300 transition-colors"
|
|
>
|
|
Zurück
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="flex-1 bg-red-600 text-white py-3 rounded-lg font-semibold hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
{loading ? 'Wird gesendet...' : 'Verbindlich anfragen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|