Files
2026-05-20 18:58:23 +00:00

144 lines
5.9 KiB
TypeScript

// /opt/erp-system/app/billing/[customerId]/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ArrowLeft, Printer } from 'lucide-react';
import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable';
export default function InvoiceDraftPage() {
const params = useParams();
const router = useRouter();
const customerId = params.customerId;
const [entries, setEntries] = useState<any[]>([]);
const [customer, setCustomer] = useState<any>(null);
const [settings, setSettings] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (customerId) fetchData();
}, [customerId]);
const fetchData = async () => {
const [billingRes, settingsRes] = await Promise.all([
fetch('/api/billing'),
fetch('/api/settings')
]);
if (billingRes.ok && settingsRes.ok) {
const allEntries = await billingRes.json();
const filtered = allEntries.filter((e: any) => e.ticket.customerId === parseInt(customerId as string));
setEntries(filtered);
if (filtered.length > 0) setCustomer(filtered[0].ticket.customer);
setSettings(await settingsRes.json());
}
setLoading(false);
};
const handleUpdateEntry = async (id: number, field: string, value: any) => {
const updatedEntries = entries.map(e => e.id === id ? { ...e, [field]: value } : e);
setEntries(updatedEntries);
const entry = updatedEntries.find(e => e.id === id);
await fetch(`/api/time-entries/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description: entry.description, durationMins: entry.durationMins })
});
};
const generatePDF = () => {
const doc = new jsPDF();
const dateStr = new Date().toLocaleDateString('de-DE');
const invNo = `RE-${new Date().getFullYear()}-${Math.floor(1000 + Math.random() * 9000)}`;
doc.setFontSize(20);
doc.text(settings.companyName || "ERP SYSTEM", 14, 20);
doc.setFontSize(9);
doc.text(settings.companyInfo || "", 14, 30);
doc.setFontSize(11);
doc.text("Rechnung an:", 14, 55);
doc.setFont("helvetica", "bold");
doc.text(customer.companyName || `${customer.firstName} ${customer.lastName}`, 14, 62);
doc.setFont("helvetica", "normal");
doc.text(`${customer.address || ''}\n${customer.zipCode || ''} ${customer.city || ''}`, 14, 68);
doc.text(`Datum: ${dateStr}`, 140, 62);
doc.text(`Rechnungs-Nr: ${invNo}`, 140, 68);
const tableData = entries.map(e => [
new Date(e.createdAt).toLocaleDateString('de-DE'),
`${e.ticket.title}\n${e.description}`,
(e.durationMins / 60).toFixed(2) + " h",
settings.hourlyRate.toFixed(2) + " €",
((e.durationMins / 60) * settings.hourlyRate).toFixed(2) + " €"
]);
autoTable(doc, {
startY: 90,
head: [['Datum', 'Leistung', 'Menge', 'Einzelpreis', 'Gesamt']],
body: tableData,
theme: 'striped',
headStyles: { fillColor: [79, 70, 229] }
});
const netTotal = entries.reduce((sum, e) => sum + (e.durationMins / 60 * settings.hourlyRate), 0);
const tax = netTotal * (settings.taxRate / 100);
const grossTotal = netTotal + tax;
const finalY = (doc as any).lastAutoTable.finalY + 10;
doc.text(`Netto Gesamt:`, 140, finalY);
doc.text(`${netTotal.toFixed(2)}`, 180, finalY, { align: 'right' });
doc.text(`USt. ${settings.taxRate}%:`, 140, finalY + 6);
doc.text(`${tax.toFixed(2)}`, 180, finalY + 6, { align: 'right' });
doc.setFont("helvetica", "bold");
doc.text(`Rechnungsbetrag:`, 140, finalY + 14);
doc.text(`${grossTotal.toFixed(2)}`, 180, finalY + 14, { align: 'right' });
doc.save(`Rechnung_${invNo}.pdf`);
};
if (loading) return <div className="p-8">Lade Entwurf...</div>;
return (
<div className="max-w-5xl mx-auto space-y-6">
<div className="flex items-center justify-between">
<button onClick={() => router.back()} className="text-slate-500 hover:text-slate-800 flex items-center gap-2 transition">
<ArrowLeft className="w-4 h-4" /> Abbrechen
</button>
<button onClick={generatePDF} className="bg-indigo-600 text-white px-6 py-2.5 rounded-lg font-bold shadow-lg flex items-center gap-2 hover:bg-indigo-700 transition">
<Printer className="w-5 h-5" /> PDF erstellen
</button>
</div>
<div className="bg-white p-8 rounded-2xl shadow-sm border border-slate-200">
<h1 className="text-2xl font-bold text-slate-900 mb-6">Rechnung korrigieren</h1>
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-600 border-y border-slate-100">
<tr>
<th className="py-3 px-4 text-left">Beschreibung</th>
<th className="py-3 px-4 text-right">Minuten</th>
<th className="py-3 px-4 text-right">Betrag</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{entries.map((entry) => (
<tr key={entry.id}>
<td className="py-4 px-4">
<textarea className="w-full border-none p-0 focus:ring-0 text-slate-700 bg-transparent resize-none" rows={2} value={entry.description} onChange={(e) => handleUpdateEntry(entry.id, 'description', e.target.value)} />
</td>
<td className="py-4 px-4 text-right">
<input type="number" step="15" className="w-20 text-right border border-slate-200 rounded p-1" value={entry.durationMins} onChange={(e) => handleUpdateEntry(entry.id, 'durationMins', parseInt(e.target.value))} />
</td>
<td className="py-4 px-4 text-right font-mono">{(entry.durationMins / 60 * settings.hourlyRate).toFixed(2)} </td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}