- 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
449 lines
18 KiB
TypeScript
449 lines
18 KiB
TypeScript
'use client';
|
||
|
||
import { useState } from 'react';
|
||
import { FiMail, FiCalendar, FiFileText, FiCheckCircle, FiAlertCircle, FiPlay, FiCheck, FiDownload, FiClock } from 'react-icons/fi';
|
||
|
||
interface BookingAutomationPanelProps {
|
||
booking: {
|
||
id: string;
|
||
bookingNumber: string;
|
||
status: string;
|
||
contractSigned: boolean;
|
||
contractSignedAt: string | null;
|
||
contractGenerated: boolean;
|
||
contractSentAt: string | null;
|
||
calendarSynced: boolean;
|
||
calendarSyncedAt: string | null;
|
||
lexofficeContactId: string | null;
|
||
lexofficeOfferId: string | null;
|
||
lexofficeConfirmationId: string | null;
|
||
lexofficeInvoiceId: string | null;
|
||
};
|
||
invoiceType?: string;
|
||
}
|
||
|
||
export default function BookingAutomationPanel({ booking, invoiceType }: BookingAutomationPanelProps) {
|
||
const [isRunning, setIsRunning] = useState(false);
|
||
const [isConfirming, setIsConfirming] = useState(false);
|
||
const [result, setResult] = useState<any>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
const runAutomation = async () => {
|
||
setIsRunning(true);
|
||
setError(null);
|
||
setResult(null);
|
||
|
||
try {
|
||
const response = await fetch('/api/admin/test-automation', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ bookingId: booking.id }),
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
setResult(data);
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 2000);
|
||
} else {
|
||
setError(data.error || 'Fehler bei der Automation');
|
||
}
|
||
} catch (err: any) {
|
||
setError(err.message || 'Netzwerkfehler');
|
||
} finally {
|
||
setIsRunning(false);
|
||
}
|
||
};
|
||
|
||
const confirmBooking = async () => {
|
||
setIsConfirming(true);
|
||
setError(null);
|
||
|
||
try {
|
||
const response = await fetch(`/api/bookings/${booking.id}/confirm`, {
|
||
method: 'POST',
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
alert('✅ Buchung erfolgreich bestätigt!');
|
||
window.location.reload();
|
||
} else {
|
||
setError(data.error || 'Fehler bei der Bestätigung');
|
||
}
|
||
} catch (err: any) {
|
||
setError(err.message || 'Netzwerkfehler');
|
||
} finally {
|
||
setIsConfirming(false);
|
||
}
|
||
};
|
||
|
||
const hasRunAutomation = Boolean(booking.lexofficeOfferId);
|
||
const canConfirm = booking.contractSigned && booking.status !== 'CONFIRMED';
|
||
|
||
const downloadPDF = async (type: 'quotation' | 'contract' | 'confirmation') => {
|
||
try {
|
||
const response = await fetch(`/api/bookings/${booking.id}/${type}-pdf`);
|
||
|
||
if (!response.ok) {
|
||
const data = await response.json();
|
||
alert(`Fehler: ${data.error}`);
|
||
return;
|
||
}
|
||
|
||
const blob = await response.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
|
||
let filename = '';
|
||
if (type === 'quotation') filename = `Angebot_${booking.bookingNumber}.pdf`;
|
||
if (type === 'contract') filename = `Mietvertrag_${booking.bookingNumber}.pdf`;
|
||
if (type === 'confirmation') filename = `Auftragsbestaetigung_${booking.bookingNumber}.pdf`;
|
||
|
||
a.download = filename;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
window.URL.revokeObjectURL(url);
|
||
document.body.removeChild(a);
|
||
} catch (err: any) {
|
||
alert(`Fehler beim Download: ${err.message}`);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="bg-gradient-to-br from-gray-800 to-gray-900 border border-gray-700 rounded-xl p-6 shadow-lg">
|
||
<h3 className="text-xl font-bold text-white mb-6 flex items-center gap-2">
|
||
<FiPlay className="text-pink-500" />
|
||
Automation & Status
|
||
</h3>
|
||
|
||
{/* Status Grid */}
|
||
<div className={`grid grid-cols-2 ${invoiceType !== 'BUSINESS' ? 'md:grid-cols-4' : 'md:grid-cols-3'} gap-4 mb-6`}>
|
||
{/* Contract Generated - nur bei Privatkunden */}
|
||
{invoiceType !== 'BUSINESS' && (
|
||
<div className={`p-4 rounded-lg border ${booking.contractGenerated ? 'bg-green-500/10 border-green-500/50' : 'bg-gray-700/50 border-gray-600'}`}>
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<FiFileText className={booking.contractGenerated ? 'text-green-400' : 'text-gray-500'} />
|
||
<span className={`text-sm font-medium ${booking.contractGenerated ? 'text-green-400' : 'text-gray-400'}`}>
|
||
Vertrag PDF
|
||
</span>
|
||
</div>
|
||
{booking.contractGenerated ? (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-1 text-xs text-green-400">
|
||
<FiCheckCircle size={12} />
|
||
Generiert
|
||
</div>
|
||
<button
|
||
onClick={() => downloadPDF('contract')}
|
||
className="w-full flex items-center justify-center gap-1 px-2 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded transition-colors"
|
||
>
|
||
<FiDownload size={12} />
|
||
Download
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="text-xs text-gray-500">Nicht generiert</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* LexOffice Created */}
|
||
<div className={`p-4 rounded-lg border ${booking.lexofficeOfferId ? 'bg-green-500/10 border-green-500/50' : 'bg-gray-700/50 border-gray-600'}`}>
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<FiFileText className={booking.lexofficeOfferId ? 'text-green-400' : 'text-gray-500'} />
|
||
<span className={`text-sm font-medium ${booking.lexofficeOfferId ? 'text-green-400' : 'text-gray-400'}`}>
|
||
LexOffice
|
||
</span>
|
||
</div>
|
||
{booking.lexofficeOfferId ? (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-1 text-xs text-green-400">
|
||
<FiCheckCircle size={12} />
|
||
Angebot erstellt
|
||
</div>
|
||
<div className="text-xs text-yellow-400 bg-yellow-500/10 border border-yellow-500/30 rounded px-2 py-1">
|
||
⚠️ Angebot wurde als Entwurf erstellt.<br/>
|
||
Bitte in LexOffice suchen und freigeben, um PDF herunterzuladen.
|
||
</div>
|
||
<button
|
||
onClick={() => downloadPDF('quotation')}
|
||
className="w-full flex items-center justify-center gap-1 px-2 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded transition-colors"
|
||
>
|
||
<FiDownload size={12} />
|
||
PDF Download
|
||
</button>
|
||
<a
|
||
href="https://app.lexoffice.de/#!sales/quotations"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="w-full flex items-center justify-center gap-1 px-2 py-1 bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 text-xs rounded transition-colors"
|
||
>
|
||
<FiFileText size={12} />
|
||
Angebot in LexOffice suchen
|
||
</a>
|
||
</div>
|
||
) : (
|
||
<div className="text-xs text-gray-500">Nicht erstellt</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Email Sent */}
|
||
<div className={`p-4 rounded-lg border ${booking.contractSentAt ? 'bg-green-500/10 border-green-500/50' : 'bg-gray-700/50 border-gray-600'}`}>
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<FiMail className={booking.contractSentAt ? 'text-green-400' : 'text-gray-500'} />
|
||
<span className={`text-sm font-medium ${booking.contractSentAt ? 'text-green-400' : 'text-gray-400'}`}>
|
||
E-Mail
|
||
</span>
|
||
</div>
|
||
{booking.contractSentAt ? (
|
||
<div className="flex items-center gap-1 text-xs text-green-400">
|
||
<FiCheckCircle size={12} />
|
||
Versendet
|
||
</div>
|
||
) : (
|
||
<div className="text-xs text-gray-500">Nicht versendet</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Calendar Synced */}
|
||
<div className={`p-4 rounded-lg border ${booking.calendarSynced ? 'bg-green-500/10 border-green-500/50' : 'bg-gray-700/50 border-gray-600'}`}>
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<FiCalendar className={booking.calendarSynced ? 'text-green-400' : 'text-gray-500'} />
|
||
<span className={`text-sm font-medium ${booking.calendarSynced ? 'text-green-400' : 'text-gray-400'}`}>
|
||
Kalender
|
||
</span>
|
||
</div>
|
||
{booking.calendarSynced ? (
|
||
<div className="flex items-center gap-1 text-xs text-green-400">
|
||
<FiCheckCircle size={12} />
|
||
Synchronisiert
|
||
</div>
|
||
) : (
|
||
<div className="text-xs text-gray-500">Nicht synchronisiert</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* LexOffice Workflow Timeline */}
|
||
<div className="mb-6 bg-gray-800/50 border border-gray-700 rounded-lg p-6">
|
||
<h4 className="text-sm font-bold text-gray-300 mb-4">LexOffice Workflow</h4>
|
||
<div className="flex items-center justify-between">
|
||
{/* Step 1: Angebot */}
|
||
<div className="flex flex-col items-center flex-1">
|
||
<div className={`w-16 h-16 rounded-full flex items-center justify-center mb-2 ${
|
||
booking.lexofficeOfferId
|
||
? 'bg-green-500/20 border-2 border-green-500'
|
||
: 'bg-gray-700 border-2 border-gray-600'
|
||
}`}>
|
||
{booking.lexofficeOfferId ? (
|
||
<FiCheckCircle className="text-green-400 text-2xl" />
|
||
) : (
|
||
<FiClock className="text-gray-400 text-2xl" />
|
||
)}
|
||
</div>
|
||
<span className={`text-xs font-medium mb-1 ${booking.lexofficeOfferId ? 'text-green-400' : 'text-gray-400'}`}>
|
||
Angebot
|
||
</span>
|
||
{booking.lexofficeOfferId && (
|
||
<div className="flex flex-col gap-1">
|
||
<button
|
||
onClick={() => downloadPDF('quotation')}
|
||
className="flex items-center gap-1 px-2 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded transition-colors"
|
||
>
|
||
<FiDownload size={10} />
|
||
PDF
|
||
</button>
|
||
<a
|
||
href="https://app.lexoffice.de/#!sales/quotations"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="flex items-center gap-1 px-2 py-1 bg-blue-600/20 hover:bg-blue-600/30 text-blue-400 text-xs rounded transition-colors justify-center"
|
||
>
|
||
<FiFileText size={10} />
|
||
Angebot in LexOffice suchen
|
||
</a>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Connector Line */}
|
||
<div className={`h-0.5 flex-1 mx-2 ${booking.lexofficeOfferId ? 'bg-green-500' : 'bg-gray-600'}`}></div>
|
||
|
||
{/* Step 2: Auftragsbestätigung */}
|
||
<div className="flex flex-col items-center flex-1">
|
||
<div className={`w-16 h-16 rounded-full flex items-center justify-center mb-2 ${
|
||
booking.lexofficeConfirmationId
|
||
? 'bg-green-500/20 border-2 border-green-500'
|
||
: booking.lexofficeOfferId
|
||
? 'bg-yellow-500/20 border-2 border-yellow-500'
|
||
: 'bg-gray-700 border-2 border-gray-600'
|
||
}`}>
|
||
{booking.lexofficeConfirmationId ? (
|
||
<FiCheckCircle className="text-green-400 text-2xl" />
|
||
) : booking.lexofficeOfferId ? (
|
||
<FiClock className="text-yellow-400 text-2xl" />
|
||
) : (
|
||
<FiClock className="text-gray-400 text-2xl" />
|
||
)}
|
||
</div>
|
||
<span className={`text-xs font-medium mb-1 ${
|
||
booking.lexofficeConfirmationId
|
||
? 'text-green-400'
|
||
: booking.lexofficeOfferId
|
||
? 'text-yellow-400'
|
||
: 'text-gray-400'
|
||
}`}>
|
||
Auftragsbestätigung
|
||
</span>
|
||
{booking.lexofficeConfirmationId && (
|
||
<button
|
||
onClick={() => downloadPDF('confirmation')}
|
||
className="mt-1 flex items-center gap-1 px-2 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded transition-colors"
|
||
>
|
||
<FiDownload size={10} />
|
||
PDF
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Connector Line */}
|
||
<div className={`h-0.5 flex-1 mx-2 ${booking.lexofficeConfirmationId ? 'bg-green-500' : 'bg-gray-600'}`}></div>
|
||
|
||
{/* Step 3: Rechnung */}
|
||
<div className="flex flex-col items-center flex-1">
|
||
<div className={`w-16 h-16 rounded-full flex items-center justify-center mb-2 ${
|
||
booking.lexofficeInvoiceId
|
||
? 'bg-green-500/20 border-2 border-green-500'
|
||
: booking.lexofficeConfirmationId
|
||
? 'bg-yellow-500/20 border-2 border-yellow-500'
|
||
: 'bg-gray-700 border-2 border-gray-600'
|
||
}`}>
|
||
{booking.lexofficeInvoiceId ? (
|
||
<FiCheckCircle className="text-green-400 text-2xl" />
|
||
) : booking.lexofficeConfirmationId ? (
|
||
<FiClock className="text-yellow-400 text-2xl" />
|
||
) : (
|
||
<FiClock className="text-gray-400 text-2xl" />
|
||
)}
|
||
</div>
|
||
<span className={`text-xs font-medium mb-1 ${
|
||
booking.lexofficeInvoiceId
|
||
? 'text-green-400'
|
||
: booking.lexofficeConfirmationId
|
||
? 'text-yellow-400'
|
||
: 'text-gray-400'
|
||
}`}>
|
||
Rechnung
|
||
</span>
|
||
{booking.lexofficeInvoiceId && (
|
||
<button
|
||
className="mt-1 flex items-center gap-1 px-2 py-1 bg-green-600/20 hover:bg-green-600/30 text-green-400 text-xs rounded transition-colors"
|
||
>
|
||
<FiDownload size={10} />
|
||
PDF
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="space-y-4">
|
||
{/* Run Automation Button */}
|
||
{!hasRunAutomation && (
|
||
<button
|
||
onClick={runAutomation}
|
||
disabled={isRunning}
|
||
className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-gradient-to-r from-pink-600 to-red-600 text-white rounded-lg hover:from-pink-700 hover:to-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg"
|
||
>
|
||
{isRunning ? (
|
||
<>
|
||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||
Automation läuft...
|
||
</>
|
||
) : (
|
||
<>
|
||
<FiPlay />
|
||
Automation starten
|
||
</>
|
||
)}
|
||
</button>
|
||
)}
|
||
|
||
{/* Confirm Booking Button */}
|
||
{canConfirm && (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2 text-sm text-green-400 bg-green-500/10 border border-green-500/50 rounded-lg p-3">
|
||
<FiCheckCircle />
|
||
<span>Vertrag wurde unterschrieben am {new Date(booking.contractSignedAt!).toLocaleDateString('de-DE')}</span>
|
||
</div>
|
||
<button
|
||
onClick={confirmBooking}
|
||
disabled={isConfirming}
|
||
className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-gradient-to-r from-green-600 to-emerald-600 text-white rounded-lg hover:from-green-700 hover:to-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-lg"
|
||
>
|
||
{isConfirming ? (
|
||
<>
|
||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||
Bestätige Buchung...
|
||
</>
|
||
) : (
|
||
<>
|
||
<FiCheck />
|
||
Buchung bestätigen (RESERVED → CONFIRMED)
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{booking.status === 'CONFIRMED' && (
|
||
<div className="flex items-center gap-2 text-sm text-blue-400 bg-blue-500/10 border border-blue-500/50 rounded-lg p-3">
|
||
<FiCheckCircle />
|
||
<span>Buchung ist bestätigt</span>
|
||
{booking.lexofficeConfirmationId && (
|
||
<span className="ml-auto text-xs text-gray-400">
|
||
LexOffice AB: {booking.lexofficeConfirmationId.slice(0, 8)}...
|
||
</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Result Display */}
|
||
{result && (
|
||
<div className="mt-6 p-4 bg-green-500/10 border border-green-500/50 rounded-lg">
|
||
<h4 className="text-sm font-bold text-green-400 mb-2">✅ Automation erfolgreich!</h4>
|
||
<div className="space-y-1 text-xs text-gray-300">
|
||
<div>Contract PDF: {result.contractGenerated ? '✅' : '❌'}</div>
|
||
<div>LexOffice: {result.lexofficeCreated ? '✅' : '❌'}</div>
|
||
<div>E-Mail: {result.emailSent ? '✅' : '❌'}</div>
|
||
<div>Kalender: {result.calendarSynced ? '✅' : '❌'}</div>
|
||
{result.errors?.length > 0 && (
|
||
<div className="mt-2 text-red-400">
|
||
Fehler: {result.errors.join(', ')}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error Display */}
|
||
{error && (
|
||
<div className="mt-6 p-4 bg-red-500/10 border border-red-500/50 rounded-lg flex items-start gap-2">
|
||
<FiAlertCircle className="text-red-400 flex-shrink-0 mt-0.5" />
|
||
<div>
|
||
<h4 className="text-sm font-bold text-red-400 mb-1">Fehler</h4>
|
||
<p className="text-xs text-gray-300">{error}</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|