Kuma/src/modules/zahlungen/ui/components/zahlungen-table.tsx
2026-10-01 23:28:32 +02:00

210 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/src/components/ui/table";
import { Button } from "@/src/components/ui/button";
import {
Calendar,
User,
Dumbbell,
Building2,
Wallet,
Tag,
Download,
CreditCard,
Banknote,
} from "lucide-react";
import ZahlungStatusBadge from "./zahlungen-status-badge";
import type { ZahlungRow } from "../../actions/zahlungen-actions";
function formatPrice(cents: number | null): string {
if (cents === null) return "–";
return new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(cents / 100);
}
function formatDate(iso: string): string {
// Explizite Zeitzone, damit Server-Prerendering und Browser-Hydration
// dieselbe Uhrzeit zugrunde legen - sonst kann derselbe ISO-Zeitstempel je
// nach Zeitzone auf unterschiedliche Kalendertage fallen (Hydration-Mismatch).
return new Date(iso).toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
timeZone: "Europe/Berlin",
});
}
interface ZahlungenTableProps {
zahlungen: ZahlungRow[];
showBusinessColumn: boolean;
/** Studios ohne Steuerangaben: dort gibt es keinen Rechnungs-Download. */
invoiceBlockedBusinessIds?: string[];
hasMore: boolean;
loadingMore: boolean;
onLoadMore: () => void;
}
const ZahlungenTable = ({
zahlungen,
showBusinessColumn,
invoiceBlockedBusinessIds = [],
hasMore,
loadingMore,
onLoadMore,
}: ZahlungenTableProps) => {
const columnCount = showBusinessColumn ? 9 : 8;
return (
<div className="rounded-xl overflow-hidden border border-white/40 bg-white/60 backdrop-blur-xl shadow-sm supports-backdrop-filter:bg-white/50">
<Table>
<TableHeader>
<TableRow className="bg-white/30 hover:bg-white/30 border-b-0">
<TableHead className="font-semibold text-gray-700 pt-4 pb-3">
<span className="flex items-center gap-2">
<Calendar size={14} className="text-gray-400" />
Datum
</span>
</TableHead>
<TableHead className="font-semibold text-gray-700 pt-4 pb-3">
<span className="flex items-center gap-2">
<User size={14} className="text-gray-400" />
Kunde
</span>
</TableHead>
<TableHead className="hidden md:table-cell font-semibold text-gray-700 pt-4 pb-3">
<span className="flex items-center gap-2">
<Dumbbell size={14} className="text-gray-400" />
Kurs
</span>
</TableHead>
{showBusinessColumn && (
<TableHead className="hidden lg:table-cell font-semibold text-gray-700 pt-4 pb-3">
<span className="flex items-center gap-2">
<Building2 size={14} className="text-gray-400" />
Business
</span>
</TableHead>
)}
<TableHead className="font-semibold text-gray-700 pt-4 pb-3">
<span className="flex items-center gap-2">
<Wallet size={14} className="text-gray-400" />
Betrag
</span>
</TableHead>
<TableHead className="hidden lg:table-cell font-semibold text-gray-700 pt-4 pb-3">
<span className="flex items-center gap-2">
<CreditCard size={14} className="text-gray-400" />
Stripe-Gebühr
</span>
</TableHead>
<TableHead className="font-semibold text-gray-700 pt-4 pb-3">
<span className="flex items-center gap-2">
<Banknote size={14} className="text-gray-400" />
Auszahlung
</span>
</TableHead>
<TableHead className="font-semibold text-gray-700 pt-4 pb-3">
<span className="flex items-center gap-2">
<Tag size={14} className="text-gray-400" />
Status
</span>
</TableHead>
<TableHead className="w-12" />
</TableRow>
<tr aria-hidden>
<td colSpan={columnCount} className="p-0">
<hr className="border-none bg-linear-to-r from-transparent via-gray-300/70 to-transparent" />
</td>
</tr>
</TableHeader>
<TableBody>
{zahlungen.length === 0 ? (
<TableRow>
<TableCell colSpan={columnCount} className="text-center py-12 text-muted-foreground">
Keine Zahlungen gefunden.
</TableCell>
</TableRow>
) : (
zahlungen.map((z) => (
<TableRow
key={z.id}
className="border-b border-white/30 transition-colors duration-150 hover:bg-white/70"
>
<TableCell className="text-muted-foreground">{formatDate(z.createdAt)}</TableCell>
<TableCell className="font-medium">
<span className="flex flex-col">
<span className="flex items-center gap-2">
{z.customerName}
{z.isGuest && (
<span className="text-xs font-semibold px-2 py-0.5 rounded-full bg-gray-100 text-gray-500">
Gast
</span>
)}
</span>
<span className="text-xs text-muted-foreground">{z.customerEmail}</span>
</span>
</TableCell>
<TableCell className="hidden md:table-cell text-muted-foreground">
{z.courseName}
</TableCell>
{showBusinessColumn && (
<TableCell className="hidden lg:table-cell text-muted-foreground">
{z.businessName}
</TableCell>
)}
<TableCell>{formatPrice(z.amountPaid)}</TableCell>
<TableCell className="hidden lg:table-cell text-muted-foreground">
{z.stripeFeeCents === null ? (
// Stripe rechnet die Gebühr erst nach der Zahlung ab -
// meist Sekunden, bei SEPA-Lastschrift auch Tage.
<span title="Wird eingetragen, sobald Stripe die Zahlung abgerechnet hat.">
steht noch aus
</span>
) : z.stripeFeeCents > 0 ? (
`− ${formatPrice(z.stripeFeeCents)}`
) : (
formatPrice(0)
)}
</TableCell>
<TableCell className="font-medium">{formatPrice(z.payoutCents)}</TableCell>
<TableCell>
<ZahlungStatusBadge status={z.paymentStatus} />
</TableCell>
<TableCell>
{(z.paymentStatus === "paid" || z.paymentStatus === "partially_refunded") &&
!invoiceBlockedBusinessIds.includes(z.businessId) && (
<a href={`/api/zahlungen/${z.id}/invoice`} download>
<Button
variant="ghost"
size="icon"
className="rounded-full hover:bg-white/60"
>
<Download size={16} />
</Button>
</a>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
{hasMore && (
<div className="flex justify-center p-4 border-t border-white/30">
<Button variant="outline" onClick={onLoadMore} disabled={loadingMore}>
{loadingMore ? "Lädt…" : "Mehr laden"}
</Button>
</div>
)}
</div>
);
};
export default ZahlungenTable;