Files
Epicnabbo-Catalogus-Updated…/src/app/admin/transactions/page.tsx
T

198 lines
5.5 KiB
TypeScript

import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import type { Prisma } from "@/generated/prisma/client";
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { type TransactionRow, TransactionsTable } from "./transactions-table";
const PAID_STATUS = ["completed", "paid", "success", "approved"] as const;
const FAILED_STATUS = [
"failed",
"denied",
"refunded",
"cancelled",
"canceled",
] as const;
const PENDING_STATUS = ["pending", "created", "processing"] as const;
function statusKind(
status: string | null | undefined,
): TransactionRow["statusKind"] {
if (!status) return "neutral";
const s = status.toLowerCase();
if ((PAID_STATUS as readonly string[]).includes(s)) return "ok";
if ((FAILED_STATUS as readonly string[]).includes(s)) return "danger";
return "neutral";
}
function formatAmount(amount: number, currency: string): string {
const n = Number(amount);
const value = Number.isFinite(n) ? n.toFixed(2) : "0.00";
return `${value} ${currency}`;
}
const SORT_MAP: Record<
string,
Prisma.WebsitePaypalTransactionsOrderByWithRelationInput
> = {
id: { id: "desc" },
amount: { amount: "desc" },
date: { createdAt: "desc" },
status: { status: "desc" },
package: { description: "desc" },
payer: { userId: "desc" },
};
export default async function AdminTransactions({
searchParams,
}: {
searchParams: Promise<Record<string, string>>;
}) {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.SHOP_VIEW, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.transactions");
const raw = await searchParams;
const parsed = parseListParams(new URLSearchParams(raw));
const statusFilter = raw.filter_status ?? "";
const where: Prisma.WebsitePaypalTransactionsWhereInput = {};
if (statusFilter === "paid") {
where.status = { in: [...PAID_STATUS] };
} else if (statusFilter === "failed") {
where.status = { in: [...FAILED_STATUS] };
} else if (statusFilter === "pending") {
where.status = { in: [...PENDING_STATUS] };
}
if (parsed.search.trim()) {
const q = parsed.search.trim();
const asId = Number(q);
let userIds: number[] = [];
try {
const users = await prisma.user.findMany({
where: { username: { contains: q } },
select: { id: true },
take: 50,
});
userIds = users.map((u) => u.id);
} catch {
userIds = [];
}
where.AND = [
...(where.status ? [{ status: where.status }] : []),
{
OR: [
{ transactionId: { contains: q } },
{ description: { contains: q } },
...(Number.isFinite(asId) && asId > 0 ? [{ userId: asId }] : []),
...(userIds.length ? [{ userId: { in: userIds } }] : []),
],
},
];
delete where.status;
}
let total = 0;
try {
total = await prisma.websitePaypalTransactions.count({ where });
} catch {
total = 0;
}
const pagination = calcPagination(total, parsed.page, parsed.perPage);
const sortKey = parsed.sort ?? "id";
const baseOrder = SORT_MAP[sortKey] ?? { id: "desc" as const };
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
const orderBy = {
[orderField]: parsed.order,
} as Prisma.WebsitePaypalTransactionsOrderByWithRelationInput;
let transactions: Awaited<
ReturnType<typeof prisma.websitePaypalTransactions.findMany>
> = [];
try {
transactions = await prisma.websitePaypalTransactions.findMany({
where,
orderBy,
skip: pagination.offset,
take: pagination.perPage,
});
} catch {
transactions = [];
}
const userIds = [...new Set(transactions.map((x) => x.userId))];
let names = new Map<number, string>();
if (userIds.length > 0) {
try {
const users = await prisma.user.findMany({
where: { id: { in: userIds } },
select: { id: true, username: true },
});
names = new Map(users.map((u) => [u.id, u.username]));
} catch {
names = new Map();
}
}
const rows: TransactionRow[] = transactions.map((x) => ({
id: Number(x.id),
payer: names.get(x.userId) ?? `#${x.userId}`,
payerId: x.userId,
amount: formatAmount(x.amount, x.currency),
package: x.description ?? "—",
status: x.status ?? "",
statusKind: statusKind(x.status),
transactionId: x.transactionId,
date: formatDate(x.createdAt),
}));
const pagePaid = rows.filter((r) => r.statusKind === "ok").length;
const pageFailed = rows.filter((r) => r.statusKind === "danger").length;
return (
<main>
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6">
<StatusCard label={t("totalTransactions")} value={total} icon="🧾" />
<StatusCard
label={t("pagePaid")}
value={pagePaid}
state={pagePaid > 0 ? "ok" : "neutral"}
icon="💳"
/>
<StatusCard
label={t("pageFailed")}
value={pageFailed}
state={pageFailed > 0 ? "danger" : "neutral"}
icon="⚠️"
/>
</div>
<section className="mt-6">
<h2 className="admin-section-title">{t("title")}</h2>
{total === 0 && !parsed.search && !statusFilter ? (
<div className="admin-empty">{t("noTransactions")}</div>
) : (
<TransactionsTable
data={{
rows,
total,
page: pagination.page,
perPage: pagination.perPage,
lastPage: pagination.lastPage,
}}
/>
)}
</section>
</main>
);
}