Files
Atlas/components/BookingAutomationPanel.tsx
Julia Wehden a2c95c70e7 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
2026-03-19 16:21:55 +01:00

449 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}