mirror of
https://github.com/Since-3/Kuma.git
synced 2026-10-12 20:51:18 +00:00
210 lines
7.8 KiB
TypeScript
210 lines
7.8 KiB
TypeScript
"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;
|