import { Link, useLoaderData, useNavigate, useRevalidator } from "react-router"; import { requireUser } from "@/session.server"; import prisma from "@/lib/prisma"; import { useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { InvoiceStatusBadge } from "@/components/invoice/invoice-status-badge"; import { formatCurrency, formatDate } from "@/lib/tax"; import { ChevronLeft, Download, CheckCircle, Send, Trash2 } from "lucide-react"; import { InvoiceStatus } from "@prisma/client"; export async function loader({ request, params, }: { request: Request; params: { id: string; invoiceId: string }; }) { const user = await requireUser(request); const { id, invoiceId } = params; const invoice = await prisma.invoice.findFirst({ where: { id: invoiceId, companyId: id, company: { userId: user.id } }, include: { items: { orderBy: { position: "asc" } }, customer: true, company: true, }, }); if (!invoice) throw new Response("Not Found", { status: 404 }); return { invoice: { ...invoice, netTotal: Number(invoice.netTotal), taxTotal: Number(invoice.taxTotal), grossTotal: Number(invoice.grossTotal), issueDate: invoice.issueDate.toISOString(), dueDate: invoice.dueDate.toISOString(), deliveryDate: invoice.deliveryDate?.toISOString() ?? null, items: invoice.items.map((item) => ({ ...item, quantity: Number(item.quantity), unitPrice: Number(item.unitPrice), taxRate: Number(item.taxRate), netAmount: Number(item.netAmount), taxAmount: Number(item.taxAmount), grossAmount: Number(item.grossAmount), })), }, }; } export default function InvoiceDetailPage() { const { invoice } = useLoaderData(); const navigate = useNavigate(); const { revalidate } = useRevalidator(); const [loading, setLoading] = useState(false); const id = invoice.companyId; const taxGroups = invoice.items.reduce( (acc, item) => { const rate = item.taxRate; if (!acc[rate]) acc[rate] = { net: 0, tax: 0 }; acc[rate].net += item.netAmount; acc[rate].tax += item.taxAmount; return acc; }, {} as Record ); async function updateStatus(status: InvoiceStatus) { setLoading(true); await fetch(`/api/invoices/${invoice.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status }), }); setLoading(false); revalidate(); } async function handleDelete() { if (!confirm("Rechnung wirklich löschen?")) return; await fetch(`/api/invoices/${invoice.id}`, { method: "DELETE" }); navigate(`/companies/${id}/invoices`); } async function downloadPdf() { const res = await fetch(`/api/invoices/${invoice.id}/pdf`); if (!res.ok) return; const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `rechnung-${invoice.number}.pdf`; a.click(); URL.revokeObjectURL(url); } return (
Zurück zu Rechnungen

{invoice.number}

{invoice.customer.name} · {formatDate(invoice.issueDate)}

{/* Invoice Actions */}
{invoice.status === "DRAFT" && ( )} {invoice.status === "SENT" && ( )} {(invoice.status === "DRAFT" || invoice.status === "CANCELLED") && ( )}

Absender

{invoice.company.name}

{invoice.company.legalForm &&

{invoice.company.legalForm}

}

{invoice.company.address}

{invoice.company.zip} {invoice.company.city}

{invoice.company.taxId && (

St.-Nr.: {invoice.company.taxId}

)} {invoice.company.vatId && (

USt-IdNr.: {invoice.company.vatId}

)}

Rechnungsempfänger

{invoice.customer.name}

{invoice.customer.address}

{invoice.customer.zip} {invoice.customer.city}

{invoice.customer.vatId && (

USt-IdNr.: {invoice.customer.vatId}

)}

Rechnungsnummer

{invoice.number}

Rechnungsdatum

{formatDate(invoice.issueDate)}

{invoice.deliveryDate && (

Leistungsdatum

{formatDate(invoice.deliveryDate)}

)}

Fällig am

{formatDate(invoice.dueDate)}

{invoice.items.map((item) => ( ))}
# Beschreibung Menge EP (netto) MwSt. Betrag (brutto)
{item.position} {item.description} {item.quantity} {item.unit && {item.unit}} {formatCurrency(item.unitPrice)} {item.taxRate}% {formatCurrency(item.grossAmount)}
Nettobetrag {formatCurrency(invoice.netTotal)}
{Object.entries(taxGroups).map(([rate, { net, tax }]) => (
MwSt. {rate}% auf {formatCurrency(net)} {formatCurrency(tax)}
))}
Gesamtbetrag (brutto) {formatCurrency(invoice.grossTotal)}
{invoice.notes && (

Hinweise

{invoice.notes}

)} {invoice.company.bankIban && (

Bankverbindung: {invoice.company.bankName && `${invoice.company.bankName} · `} IBAN: {invoice.company.bankIban} {invoice.company.bankBic && ` · BIC: ${invoice.company.bankBic}`}

)}
Zusammenfassung

Netto

{formatCurrency(invoice.netTotal)}

MwSt.

{formatCurrency(invoice.taxTotal)}

Brutto

{formatCurrency(invoice.grossTotal)}

); }