Initial commit - SaveTheMoment Atlas Basis-Setup
This commit is contained in:
325
components/ContractSigningForm.tsx
Normal file
325
components/ContractSigningForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user