- Vintage Modell hinzugefuegt - Equipment Multi-Select (Neue Buchung + Bearbeitung) - Kundenadresse in Formularen - Bearbeiten-Seite fuer Buchungen - Abbau-Zeiten in Formularen und Uebersicht - Vertrag PDF nur bei Privatkunden - LexOffice Kontakt-Erstellung Fix (BUSINESS) - Zurueck-Pfeil auf Touren-Seite
521 lines
19 KiB
TypeScript
521 lines
19 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { FiCalendar, FiMapPin } from "react-icons/fi";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
|
|
interface NewBookingFormProps {
|
|
locations: any[];
|
|
user: any;
|
|
}
|
|
|
|
export default function NewBookingForm({
|
|
locations,
|
|
user,
|
|
}: NewBookingFormProps) {
|
|
const router = useRouter();
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState("");
|
|
const [equipmentList, setEquipmentList] = useState<any[]>([]);
|
|
const [selectedEquipment, setSelectedEquipment] = useState<string[]>([]);
|
|
|
|
useEffect(() => {
|
|
fetch("/api/equipment")
|
|
.then((res) => res.json())
|
|
.then((data) => setEquipmentList(data.equipment || []))
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
const toggleEquipment = (id: string) => {
|
|
setSelectedEquipment((prev) =>
|
|
prev.includes(id) ? prev.filter((e) => e !== id) : [...prev, id]
|
|
);
|
|
};
|
|
|
|
const [formData, setFormData] = useState({
|
|
locationId: "",
|
|
model: "VINTAGE_SMILE",
|
|
customerName: "",
|
|
customerEmail: "",
|
|
customerPhone: "",
|
|
customerAddress: "",
|
|
customerCity: "",
|
|
customerZip: "",
|
|
invoiceType: "PRIVATE",
|
|
companyName: "",
|
|
eventDate: "",
|
|
eventAddress: "",
|
|
eventCity: "",
|
|
eventZip: "",
|
|
eventLocation: "",
|
|
setupTimeStart: "",
|
|
setupTimeLatest: "",
|
|
dismantleTimeEarliest: "",
|
|
dismantleTimeLatest: "",
|
|
calculatedPrice: 0,
|
|
notes: "",
|
|
});
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError("");
|
|
|
|
try {
|
|
const res = await fetch("/api/bookings/create", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ ...formData, equipmentIds: selectedEquipment }),
|
|
});
|
|
|
|
const data = await res.json();
|
|
|
|
if (!res.ok) {
|
|
throw new Error(data.error || "Fehler beim Erstellen");
|
|
}
|
|
|
|
alert("Buchung erfolgreich erstellt!");
|
|
router.push(`/dashboard/bookings/${data.booking.id}`);
|
|
} catch (err: any) {
|
|
setError(err.message);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-4xl mx-auto">
|
|
<div className="mb-8">
|
|
<Link
|
|
href="/dashboard/bookings"
|
|
className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors"
|
|
>
|
|
← Zurück zu Buchungen
|
|
</Link>
|
|
<h2 className="text-3xl font-bold text-white">
|
|
Neue Buchung erstellen
|
|
</h2>
|
|
<p className="text-gray-400 mt-1">Manuelle Buchung anlegen</p>
|
|
</div>
|
|
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl shadow-sm p-6 space-y-6"
|
|
>
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-white mb-4">
|
|
Standort & Fotobox
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Standort
|
|
</label>
|
|
<select
|
|
value={formData.locationId}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, locationId: e.target.value })
|
|
}
|
|
required
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white 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.id}>
|
|
{loc.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Modell
|
|
</label>
|
|
<select
|
|
value={formData.model}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, model: e.target.value })
|
|
}
|
|
required
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
>
|
|
<option value="VINTAGE">Vintage</option>
|
|
<option value="VINTAGE_SMILE">Vintage Smile</option>
|
|
<option value="VINTAGE_PHOTOS">Vintage Photos</option>
|
|
<option value="NOSTALGIE">Nostalgie</option>
|
|
<option value="MAGIC_MIRROR">Magic Mirror</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{equipmentList.length > 0 && (
|
|
<div className="mt-4">
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Zusatzausstattung
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{equipmentList.map((eq) => (
|
|
<label
|
|
key={eq.id}
|
|
className={`flex items-center gap-2 p-3 rounded-lg border cursor-pointer transition-colors ${
|
|
selectedEquipment.includes(eq.id)
|
|
? "bg-red-500/10 border-red-500/50 text-white"
|
|
: "bg-gray-700/50 border-gray-600 text-gray-300 hover:border-gray-500"
|
|
}`}
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
checked={selectedEquipment.includes(eq.id)}
|
|
onChange={() => toggleEquipment(eq.id)}
|
|
className="accent-red-500"
|
|
/>
|
|
{eq.name}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-white mb-4">Kundendaten</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Rechnungsart
|
|
</label>
|
|
<div className="flex gap-4">
|
|
<label className="flex items-center text-white">
|
|
<input
|
|
type="radio"
|
|
value="PRIVATE"
|
|
checked={formData.invoiceType === "PRIVATE"}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, invoiceType: e.target.value })
|
|
}
|
|
className="mr-2"
|
|
/>
|
|
Privat
|
|
</label>
|
|
<label className="flex items-center text-white">
|
|
<input
|
|
type="radio"
|
|
value="BUSINESS"
|
|
checked={formData.invoiceType === "BUSINESS"}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, invoiceType: e.target.value })
|
|
}
|
|
className="mr-2"
|
|
/>
|
|
Geschäftlich
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{formData.invoiceType === "BUSINESS" && (
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Firmenname
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.companyName}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, companyName: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Name
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.customerName}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, customerName: e.target.value })
|
|
}
|
|
required
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-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-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Telefon
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={formData.customerPhone}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, customerPhone: e.target.value })
|
|
}
|
|
required
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Adresse
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.customerAddress}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, customerAddress: e.target.value })
|
|
}
|
|
placeholder="Straße und Hausnummer"
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
PLZ
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.customerZip}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, customerZip: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Stadt
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.customerCity}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, customerCity: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-white mb-4">
|
|
Event-Details
|
|
</h3>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-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-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Preis (€)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={formData.calculatedPrice}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
calculatedPrice: parseFloat(e.target.value),
|
|
})
|
|
}
|
|
required
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-300 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-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 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-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 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-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-300 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"
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent placeholder-gray-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Aufbau ab
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={formData.setupTimeStart}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, setupTimeStart: e.target.value })
|
|
}
|
|
required
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Aufbau spätestens
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={formData.setupTimeLatest}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, setupTimeLatest: e.target.value })
|
|
}
|
|
required
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Abbau ab
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={formData.dismantleTimeEarliest}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, dismantleTimeEarliest: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Abbau spätestens
|
|
</label>
|
|
<input
|
|
type="datetime-local"
|
|
value={formData.dismantleTimeLatest}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, dismantleTimeLatest: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Notizen (optional)
|
|
</label>
|
|
<textarea
|
|
value={formData.notes}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, notes: e.target.value })
|
|
}
|
|
rows={4}
|
|
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 text-white rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-red-500/20 border border-red-500/50 text-red-400 px-4 py-3 rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-4 pt-4 border-t border-gray-700">
|
|
<Link
|
|
href="/dashboard/bookings"
|
|
className="flex-1 px-4 py-3 bg-gray-700 text-gray-300 rounded-lg font-semibold text-center hover:bg-gray-600 transition-colors"
|
|
>
|
|
Abbrechen
|
|
</Link>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="flex-1 px-4 py-3 bg-gradient-to-r from-red-600 to-pink-600 text-white rounded-lg font-semibold hover:from-red-700 hover:to-pink-700 transition-all shadow-lg disabled:opacity-50"
|
|
>
|
|
{loading ? "Wird erstellt..." : "Buchung erstellen"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|