// /opt/erp-system/app/billing/page.tsx 'use client'; import { useState, useEffect } from 'react'; import { FileText, CheckCircle, Clock, User as UserIcon, Printer, Search } from 'lucide-react'; import { jsPDF } from 'jspdf'; import autoTable from 'jspdf-autotable'; import { useToast } from '../components/ToastProvider'; export default function T2medBillingPage() { const [groups, setGroups] = useState([]); const [settings, setSettings] = useState(null); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const { toast } = useToast(); // T2med Split-Screen States const [activeCustomer, setActiveCustomer] = useState(null); const [draftEntries, setDraftEntries] = useState([]); useEffect(() => { fetchData(); }, []); const fetchData = async () => { setLoading(true); const [billingRes, settingsRes] = await Promise.all([ fetch('/api/billing'), fetch('/api/settings') ]); if (billingRes.ok && settingsRes.ok) { const entries = await billingRes.json(); const loadedSettings = await settingsRes.json(); setSettings(loadedSettings); // Gruppieren nach Kunde const groupedData = entries.reduce((acc: any, entry: any) => { const cid = entry.ticket.customerId; if (!acc[cid]) { acc[cid] = { customer: entry.ticket.customer, entries: [], totalMins: 0 }; } acc[cid].entries.push(entry); acc[cid].totalMins += entry.durationMins; return acc; }, {}); setGroups(Object.values(groupedData)); } setLoading(false); }; // Aktiviert den Kunden für die rechte Bildschirmhälfte const selectCustomer = (group: any) => { setActiveCustomer(group.customer); // Erstelle eine tiefe Kopie der Einträge für das Inline-Editing setDraftEntries(JSON.parse(JSON.stringify(group.entries))); }; // Inline-Editing im Rechnungsblatt const handleUpdateDraft = async (id: number, field: string, value: any) => { const updated = draftEntries.map(e => e.id === id ? { ...e, [field]: value } : e); setDraftEntries(updated); // Speichere die Änderung sofort im Hintergrund in der DB ab (T2med-Feeling) const entry = updated.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 }) }); // Aktualisiere die linke Liste leise mit den neuen Summen fetchData(); }; const markAsBilled = async () => { const entryIds = draftEntries.map(e => e.id); const res = await fetch('/api/billing', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entryIds }) }); if (res.ok) { setActiveCustomer(null); setDraftEntries([]); fetchData(); } else { toast('Fehler beim Abschließen.', 'error'); } }; const generatePDF = () => { if (!settings) return; const doc = new jsPDF(); const dateStr = new Date().toLocaleDateString('de-DE'); const yyyy = new Date().getFullYear(); const mm = String(new Date().getMonth() + 1).padStart(2, '0'); const seq = String(Date.now()).slice(-5); const invNo = `RE-${yyyy}${mm}-${seq}`; 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(activeCustomer.companyName || `${activeCustomer.firstName} ${activeCustomer.lastName}`, 14, 62); doc.setFont("helvetica", "normal"); doc.text(`${activeCustomer.address || ''}\n${activeCustomer.zipCode || ''} ${activeCustomer.city || ''}`, 14, 68); doc.text(`Datum: ${dateStr}`, 140, 62); doc.text(`Rechnungs-Nr: ${invNo}`, 140, 68); const tableData = draftEntries.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] }, styles: { cellPadding: 4, fontSize: 9 } }); const netTotal = draftEntries.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`); }; const filteredGroups = groups.filter(g => { const name = g.customer.companyName || `${g.customer.firstName} ${g.customer.lastName}`; return name.toLowerCase().includes(searchTerm.toLowerCase()); }); if (loading && groups.length === 0) return
Lade Abrechnungs-Modul...
; return (
{/* LINKE SPALTE: Kunden-Liste (T2med-Style) */}

Abrechnungslauf

setSearchTerm(e.target.value)} className="w-full pl-9 pr-3 py-2 bg-slate-100/80 border border-slate-200 rounded-xl text-sm focus:bg-white focus:border-indigo-400 focus:ring-4 focus:ring-indigo-500/10 outline-none transition-all" />
{filteredGroups.length === 0 ? (
Keine offenen Positionen.
) : ( filteredGroups.map((group, idx) => { const name = group.customer.companyName || `${group.customer.firstName} ${group.customer.lastName}`; const isActive = activeCustomer?.id === group.customer.id; return ( ); }) )}
{/* RECHTE SPALTE: Das aktive Rechnungsblatt */}
{!activeCustomer ? (

Bitte wähle links einen Kunden aus

) : (
{/* Action Bar (Oben rechts, sticky) */}
{/* Das "Papier"-Dokument */}

Rechnungsentwurf

{activeCustomer.companyName || `${activeCustomer.firstName} ${activeCustomer.lastName}`}

{activeCustomer.address || 'Keine Adresse hinterlegt'}

Stundensatz: {settings?.hourlyRate} €

MwSt: {settings?.taxRate} %

{/* T2med-Style Inline-Editing Tabelle */}
Datum
Leistung
Minuten
Betrag
{draftEntries.map((entry) => (
{new Date(entry.createdAt).toLocaleDateString('de-DE')}

Ticket #{entry.ticket.id}