Initial commit - ERP System
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// /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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user