Initial commit - SaveTheMoment Atlas Basis-Setup

This commit is contained in:
Dennis Forte
2025-11-12 20:21:32 +01:00
commit 0b6e429329
167 changed files with 30843 additions and 0 deletions

View File

@@ -0,0 +1,325 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import SignaturePad from './SignaturePad';
import { formatDate } from '@/lib/date-utils';
import { FiUpload, FiDownload } from 'react-icons/fi';
interface ContractSigningFormProps {
booking: any;
location: any;
photobox: any;
token: string;
}
export default function ContractSigningForm({ booking, location, photobox, token }: ContractSigningFormProps) {
const router = useRouter();
const [signatureData, setSignatureData] = useState<string | null>(null);
const [acceptTerms, setAcceptTerms] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [showSignature, setShowSignature] = useState(false);
const [showUpload, setShowUpload] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [uploadProgress, setUploadProgress] = useState(false);
const handleSubmit = async () => {
if (!signatureData || !acceptTerms) return;
setSubmitting(true);
try {
const res = await fetch('/api/contract/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token,
signatureData,
name: booking.customerName,
email: booking.customerEmail,
}),
});
if (res.ok) {
router.push(`/contract/success`);
} else {
alert('Fehler beim Unterschreiben des Vertrags');
setSubmitting(false);
}
} catch (error) {
alert('Fehler beim Unterschreiben des Vertrags');
setSubmitting(false);
}
};
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files[0]) {
const file = e.target.files[0];
if (file.type === 'application/pdf') {
setSelectedFile(file);
} else {
alert('Bitte wählen Sie eine PDF-Datei aus');
}
}
};
const handleUpload = async () => {
if (!selectedFile || !acceptTerms) return;
setUploadProgress(true);
try {
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('token', token);
const res = await fetch('/api/contract/upload', {
method: 'POST',
body: formData,
});
if (res.ok) {
router.push(`/contract/success`);
} else {
alert('Fehler beim Hochladen des Vertrags');
setUploadProgress(false);
}
} catch (error) {
alert('Fehler beim Hochladen des Vertrags');
setUploadProgress(false);
}
};
const handleDownloadContract = async () => {
try {
const decoded = Buffer.from(token, 'base64url').toString();
const bookingId = decoded.split('-')[0];
const res = await fetch(`/api/bookings/${bookingId}/contract`);
if (res.ok) {
const blob = await res.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `Vertrag-${booking.bookingNumber}.pdf`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} else {
alert('Fehler beim Herunterladen des Vertrags');
}
} catch (error) {
alert('Fehler beim Herunterladen des Vertrags');
}
};
return (
<div className="max-w-4xl mx-auto bg-white rounded-xl shadow-lg overflow-hidden">
{/* Header */}
<div className="bg-red-600 text-white p-6">
<h1 className="text-2xl font-bold">SaveTheMoment Atlas</h1>
<p className="text-red-100">Mietvertrag für Fotobox</p>
</div>
{/* Contract Details */}
<div className="p-8 space-y-6">
<div>
<h2 className="text-xl font-bold text-gray-900 mb-4">Vertragsdaten</h2>
<div className="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-lg">
<div>
<p className="text-sm text-gray-600">Buchungsnummer</p>
<p className="font-semibold">{booking.bookingNumber}</p>
</div>
<div>
<p className="text-sm text-gray-600">Event-Datum</p>
<p className="font-semibold">{formatDate(booking.eventDate)}</p>
</div>
<div>
<p className="text-sm text-gray-600">Location</p>
<p className="font-semibold">{booking.eventLocation || 'Nicht angegeben'}</p>
</div>
<div>
<p className="text-sm text-gray-600">Gesamtpreis</p>
<p className="font-semibold">{booking.calculatedPrice?.toFixed(2) || '0.00'} </p>
</div>
</div>
</div>
<div>
<h3 className="font-semibold text-gray-900 mb-2">Kundendaten</h3>
<div className="bg-gray-50 p-4 rounded-lg space-y-2">
<p><span className="text-gray-600">Name:</span> {booking.customerName}</p>
<p><span className="text-gray-600">E-Mail:</span> {booking.customerEmail}</p>
<p><span className="text-gray-600">Telefon:</span> {booking.customerPhone}</p>
</div>
</div>
<div>
<h3 className="font-semibold text-gray-900 mb-2">Vertragsbedingungen</h3>
<div className="bg-gray-50 p-4 rounded-lg space-y-2 text-sm">
<p>1. Der Vermieter verpflichtet sich, die Fotobox am Veranstaltungsort aufzubauen und nach der Veranstaltung wieder abzubauen.</p>
<p>2. Die Mietdauer beginnt mit dem Aufbau und endet mit dem Abbau der Fotobox.</p>
<p>3. Der Mieter haftet für Schäden, die während der Mietzeit an der Fotobox entstehen.</p>
<p>4. Der vereinbarte Mietpreis ist spätestens 7 Tage vor der Veranstaltung fällig.</p>
<p>5. Bei Stornierung weniger als 14 Tage vor der Veranstaltung wird eine Bearbeitungsgebühr von 50% erhoben.</p>
</div>
</div>
{/* AGB Checkbox */}
<div className="border-t pt-6">
<label className="flex items-start gap-3 cursor-pointer">
<input
type="checkbox"
checked={acceptTerms}
onChange={(e) => setAcceptTerms(e.target.checked)}
className="w-5 h-5 text-red-600 rounded mt-1"
/>
<span className="text-sm text-gray-700">
Ich habe die Vertragsbedingungen gelesen und akzeptiere diese. Mir ist bewusst, dass dieser Vertrag rechtsverbindlich ist.
</span>
</label>
</div>
{/* Signature Section */}
{!showSignature && !showUpload ? (
<div className="border-t pt-6 space-y-4">
<button
onClick={() => setShowSignature(true)}
disabled={!acceptTerms}
className="w-full px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:bg-gray-400 disabled:cursor-not-allowed font-semibold"
>
Online unterschreiben
</button>
<div className="text-center text-gray-500 font-medium">
oder
</div>
<div className="space-y-3">
<button
onClick={handleDownloadContract}
className="w-full px-6 py-3 bg-white border-2 border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors font-semibold flex items-center justify-center gap-2"
>
<FiDownload />
Vertrag herunterladen
</button>
<button
onClick={() => setShowUpload(true)}
disabled={!acceptTerms}
className="w-full px-6 py-3 bg-white border-2 border-red-600 text-red-600 rounded-lg hover:bg-red-50 transition-colors disabled:border-gray-400 disabled:text-gray-400 disabled:cursor-not-allowed font-semibold flex items-center justify-center gap-2"
>
<FiUpload />
Unterschriebenen Vertrag hochladen
</button>
</div>
<p className="text-sm text-gray-600 text-center mt-3">
Laden Sie den Vertrag herunter, unterschreiben Sie ihn und laden Sie ihn dann wieder hoch
</p>
</div>
) : showSignature ? (
<div className="border-t pt-6">
<h3 className="font-semibold text-gray-900 mb-4">Unterschrift</h3>
{!signatureData ? (
<SignaturePad
onSave={(data) => setSignatureData(data)}
onCancel={() => setShowSignature(false)}
/>
) : (
<div className="space-y-4">
<div className="border-2 border-green-500 rounded-lg p-4 bg-green-50">
<p className="text-green-800 font-semibold mb-2"> Unterschrift erfasst</p>
<img src={signatureData} alt="Unterschrift" className="max-h-32 border-t border-gray-300 pt-2" />
</div>
<div className="flex gap-3">
<button
onClick={() => setSignatureData(null)}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300"
>
Neue Unterschrift
</button>
<button
onClick={handleSubmit}
disabled={submitting}
className="flex-1 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:bg-gray-400 font-semibold"
>
{submitting ? 'Wird übermittelt...' : 'Vertrag jetzt unterschreiben'}
</button>
</div>
</div>
)}
</div>
) : (
<div className="border-t pt-6">
<h3 className="font-semibold text-gray-900 mb-4">Unterschriebenen Vertrag hochladen</h3>
{!selectedFile ? (
<div className="space-y-4">
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
<FiUpload className="w-12 h-12 mx-auto text-gray-400 mb-4" />
<p className="text-gray-600 mb-4">
Wählen Sie die unterschriebene PDF-Datei aus
</p>
<input
type="file"
accept="application/pdf"
onChange={handleFileSelect}
className="hidden"
id="contract-upload"
/>
<label
htmlFor="contract-upload"
className="inline-block px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors cursor-pointer font-semibold"
>
Datei auswählen
</label>
</div>
<button
onClick={() => setShowUpload(false)}
className="w-full px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300"
>
Zurück
</button>
</div>
) : (
<div className="space-y-4">
<div className="border-2 border-green-500 rounded-lg p-4 bg-green-50">
<p className="text-green-800 font-semibold mb-2"> Datei ausgewählt</p>
<p className="text-gray-700">{selectedFile.name}</p>
<p className="text-sm text-gray-600 mt-1">
Größe: {(selectedFile.size / 1024).toFixed(2)} KB
</p>
</div>
<div className="flex gap-3">
<button
onClick={() => setSelectedFile(null)}
className="px-4 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300"
>
Andere Datei wählen
</button>
<button
onClick={handleUpload}
disabled={uploadProgress}
className="flex-1 px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:bg-gray-400 font-semibold"
>
{uploadProgress ? 'Wird hochgeladen...' : 'Jetzt hochladen'}
</button>
</div>
</div>
)}
</div>
)}
</div>
{/* Footer */}
<div className="bg-gray-50 px-8 py-4 text-sm text-gray-600 border-t">
<p>
SaveTheMoment Atlas {location.name} {location.contactEmail} {location.contactPhone}
</p>
<p className="mt-1">
Dieser Vertrag wird elektronisch gespeichert. Sie erhalten eine Kopie per E-Mail.
</p>
</div>
</div>
);
}