feat: Wabenhain Honig-Shop Anwendung
Vollstaendige Next.js/Prisma-E-Commerce-Anwendung fuer eine Imkerei: Shop, Warenkorb, Stripe-Checkout, Kundenkonto mit 2FA, Admin-Bereich (Produkte, Ernten, Chargen, Bestellungen, Gutscheine, admin-editierbare Website-Inhalte), i18n (DE/EN), Rate-Limiting/CSP/Audit-Log-Haertung, PostgreSQL- und MySQL-Unterstuetzung ueber austauschbare Prisma- Treiber-Adapter, Unit-/Komponenten-/E2E-Tests.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import Link from "next/link";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { BackLink } from "@/components/back-link";
|
||||
import { formatDate, formatPrice } from "@/lib/format";
|
||||
import { insensitiveContains } from "@/lib/prisma-search";
|
||||
import { updateReturnRequestStatusAction, refundReturnRequestAction } from "./actions";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
|
||||
export default async function AdminRetourenPage(props: PageProps<"/admin/retouren">) {
|
||||
const t = getDictionary(await getLocale());
|
||||
const statusLabels: Record<string, string> = {
|
||||
ANGEFRAGT: t.adminReturnsPage.statusRequested,
|
||||
ANGENOMMEN: t.adminReturnsPage.statusAccepted,
|
||||
ABGELEHNT: t.adminReturnsPage.statusRejected,
|
||||
ERSTATTET: t.adminReturnsPage.statusRefunded,
|
||||
};
|
||||
const { q } = await props.searchParams;
|
||||
const query = typeof q === "string" ? q.trim() : "";
|
||||
|
||||
const returnRequests = await prisma.returnRequest.findMany({
|
||||
where: query
|
||||
? {
|
||||
order: {
|
||||
OR: [
|
||||
{ orderNumber: insensitiveContains(query) },
|
||||
{ firstName: insensitiveContains(query) },
|
||||
{ lastName: insensitiveContains(query) },
|
||||
{ email: insensitiveContains(query) },
|
||||
],
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 100,
|
||||
include: {
|
||||
order: { include: { items: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BackLink href="/admin" label={t.adminReturnsPage.backToDashboard} />
|
||||
<h1 className="text-2xl text-forest mb-8">{t.adminReturnsPage.title}</h1>
|
||||
|
||||
<form method="GET" className="flex flex-wrap items-end gap-3 mb-6">
|
||||
<label className="text-sm">
|
||||
<span className="block text-ink/65 text-xs mb-1">{t.adminReturnsPage.searchLabel}</span>
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
defaultValue={query}
|
||||
placeholder={t.adminReturnsPage.searchPlaceholder}
|
||||
className="border border-ink/20 bg-background px-3 py-2 text-sm w-64"
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" className="bg-forest text-cream px-4 py-2 text-sm">
|
||||
{t.adminReturnsPage.filterButton}
|
||||
</button>
|
||||
{query ? (
|
||||
<Link href="/admin/retouren" className="text-sm text-ink/65 underline">
|
||||
{t.adminReturnsPage.resetFilter}
|
||||
</Link>
|
||||
) : null}
|
||||
</form>
|
||||
|
||||
<div className="space-y-4">
|
||||
{returnRequests.map((request) => (
|
||||
<div key={request.id} className="bg-background border border-ink/10 p-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-forest">{request.order.orderNumber}</p>
|
||||
<p className="text-sm text-ink/70">
|
||||
{request.order.firstName} {request.order.lastName} · {request.order.email}
|
||||
</p>
|
||||
<p className="text-xs text-ink/65 mt-1">{formatDate(request.createdAt)}</p>
|
||||
</div>
|
||||
<p className="text-sm">{statusLabels[request.status] ?? request.status}</p>
|
||||
</div>
|
||||
|
||||
<ul className="mt-4 text-sm text-ink/70 space-y-1">
|
||||
{request.order.items.map((item) => (
|
||||
<li key={item.id}>
|
||||
{item.quantity} × {item.productName}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="mt-2 text-forest">{formatPrice(request.order.totalCents, request.order.currency)}</p>
|
||||
|
||||
<p className="mt-4 text-sm text-ink/70 border-t border-ink/10 pt-4">{request.reason}</p>
|
||||
|
||||
{request.status === "ANGEFRAGT" ? (
|
||||
<div className="mt-4 flex gap-4">
|
||||
<form action={updateReturnRequestStatusAction}>
|
||||
<input type="hidden" name="id" value={request.id} />
|
||||
<input type="hidden" name="status" value="ANGENOMMEN" />
|
||||
<button type="submit" className="text-xs text-forest underline">
|
||||
{t.adminReturnsPage.accept}
|
||||
</button>
|
||||
</form>
|
||||
<form action={updateReturnRequestStatusAction}>
|
||||
<input type="hidden" name="id" value={request.id} />
|
||||
<input type="hidden" name="status" value="ABGELEHNT" />
|
||||
<button type="submit" className="text-xs text-red-700 underline">
|
||||
{t.adminReturnsPage.reject}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : null}
|
||||
{request.status === "ANGENOMMEN" ? (
|
||||
<form action={refundReturnRequestAction} className="mt-4">
|
||||
<input type="hidden" name="id" value={request.id} />
|
||||
<button type="submit" className="text-xs text-forest underline">
|
||||
{t.adminReturnsPage.refundNow}
|
||||
</button>
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
{returnRequests.length === 0 ? (
|
||||
<p className="text-ink/65">
|
||||
{query ? t.adminReturnsPage.noResultsFiltered : t.adminReturnsPage.noRequestsYet}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user