feat: Equipment-System, Buchungsbearbeitung, Kundenadresse, LexOffice-Fix
- 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
This commit is contained in:
311
app/dashboard/bookings/[id]/edit/page.tsx
Normal file
311
app/dashboard/bookings/[id]/edit/page.tsx
Normal file
@@ -0,0 +1,311 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function EditBookingPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const bookingId = params.id as string;
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [equipmentList, setEquipmentList] = useState<any[]>([]);
|
||||
const [selectedEquipment, setSelectedEquipment] = useState<string[]>([]);
|
||||
const [formData, setFormData] = useState({
|
||||
customerName: "",
|
||||
customerEmail: "",
|
||||
customerPhone: "",
|
||||
customerAddress: "",
|
||||
customerCity: "",
|
||||
customerZip: "",
|
||||
companyName: "",
|
||||
invoiceType: "PRIVATE",
|
||||
model: "",
|
||||
eventDate: "",
|
||||
eventAddress: "",
|
||||
eventCity: "",
|
||||
eventZip: "",
|
||||
eventLocation: "",
|
||||
setupTimeStart: "",
|
||||
setupTimeLatest: "",
|
||||
dismantleTimeEarliest: "",
|
||||
dismantleTimeLatest: "",
|
||||
calculatedPrice: 0,
|
||||
notes: "",
|
||||
withPrintFlat: false,
|
||||
});
|
||||
|
||||
const toggleEquipment = (id: string) => {
|
||||
setSelectedEquipment((prev) =>
|
||||
prev.includes(id) ? prev.filter((e) => e !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([
|
||||
fetch(`/api/bookings/${bookingId}`).then((r) => r.json()),
|
||||
fetch("/api/equipment").then((r) => r.json()),
|
||||
])
|
||||
.then(([bookingData, eqData]) => {
|
||||
const b = bookingData.booking || bookingData;
|
||||
setFormData({
|
||||
customerName: b.customerName || "",
|
||||
customerEmail: b.customerEmail || "",
|
||||
customerPhone: b.customerPhone || "",
|
||||
customerAddress: b.customerAddress || "",
|
||||
customerCity: b.customerCity || "",
|
||||
customerZip: b.customerZip || "",
|
||||
companyName: b.companyName || "",
|
||||
invoiceType: b.invoiceType || "PRIVATE",
|
||||
model: b.model || b.photobox?.model || "",
|
||||
eventDate: b.eventDate ? new Date(b.eventDate).toISOString().split("T")[0] : "",
|
||||
eventAddress: b.eventAddress || "",
|
||||
eventCity: b.eventCity || "",
|
||||
eventZip: b.eventZip || "",
|
||||
eventLocation: b.eventLocation || "",
|
||||
setupTimeStart: b.setupTimeStart ? new Date(b.setupTimeStart).toISOString().slice(0, 16) : "",
|
||||
setupTimeLatest: b.setupTimeLatest ? new Date(b.setupTimeLatest).toISOString().slice(0, 16) : "",
|
||||
dismantleTimeEarliest: b.dismantleTimeEarliest ? new Date(b.dismantleTimeEarliest).toISOString().slice(0, 16) : "",
|
||||
dismantleTimeLatest: b.dismantleTimeLatest ? new Date(b.dismantleTimeLatest).toISOString().slice(0, 16) : "",
|
||||
calculatedPrice: b.calculatedPrice || 0,
|
||||
notes: b.notes || "",
|
||||
withPrintFlat: b.withPrintFlat || false,
|
||||
});
|
||||
setEquipmentList(eqData.equipment || []);
|
||||
if (b.bookingEquipment) {
|
||||
setSelectedEquipment(b.bookingEquipment.map((be: any) => be.equipmentId));
|
||||
}
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Buchung konnte nicht geladen werden");
|
||||
setLoading(false);
|
||||
});
|
||||
}, [bookingId]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/bookings/${bookingId}`, {
|
||||
method: "PATCH",
|
||||
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 Speichern");
|
||||
}
|
||||
|
||||
router.push(`/dashboard/bookings/${bookingId}`);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 flex items-center justify-center">
|
||||
<div className="text-gray-400">Laden...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const inputClass = "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";
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
href={`/dashboard/bookings/${bookingId}`}
|
||||
className="text-sm text-gray-400 hover:text-gray-300 mb-2 inline-block transition-colors"
|
||||
>
|
||||
← Zurück zur Buchung
|
||||
</Link>
|
||||
<h2 className="text-3xl font-bold text-white">Buchung bearbeiten</h2>
|
||||
</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">Fotobox & Ausstattung</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">Modell</label>
|
||||
<select
|
||||
value={formData.model}
|
||||
onChange={(e) => setFormData({ ...formData, model: e.target.value })}
|
||||
className={inputClass}
|
||||
>
|
||||
<option value="">Bitte wählen...</option>
|
||||
<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={inputClass} />
|
||||
</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={inputClass} />
|
||||
</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={inputClass} />
|
||||
</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={inputClass} />
|
||||
</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={inputClass} />
|
||||
</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={inputClass} />
|
||||
</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={inputClass} />
|
||||
</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 className={inputClass} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Preis (EUR)</label>
|
||||
<input type="number" step="0.01" value={formData.calculatedPrice} onChange={(e) => setFormData({ ...formData, calculatedPrice: parseFloat(e.target.value) })} required className={inputClass} />
|
||||
</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 className={inputClass} />
|
||||
</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={inputClass} />
|
||||
</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={inputClass} />
|
||||
</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 })} className={inputClass} />
|
||||
</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 })} className={inputClass} />
|
||||
</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 })} className={inputClass} />
|
||||
</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={inputClass} />
|
||||
</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={inputClass} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="flex items-center text-white gap-2">
|
||||
<input type="checkbox" checked={formData.withPrintFlat} onChange={(e) => setFormData({ ...formData, withPrintFlat: e.target.checked })} />
|
||||
Druckflatrate
|
||||
</label>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">Notizen</label>
|
||||
<textarea value={formData.notes} onChange={(e) => setFormData({ ...formData, notes: e.target.value })} rows={4} className={inputClass} />
|
||||
</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/${bookingId}`} 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={saving} 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">
|
||||
{saving ? "Wird gespeichert..." : "Änderungen speichern"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user