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:
1 parent
5c76fcc266
commit
c7927f2883
240 files changed
+26096
-137
No files matched your search
@@ -0,0 +1,113 @@
|
||||
import type { Metadata } from "next";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { Container } from "@/components/ui";
|
||||
import { BackLink } from "@/components/back-link";
|
||||
import { getCurrentCustomer } from "@/lib/auth";
|
||||
import { getInvoiceForCustomer } from "@/lib/account-data";
|
||||
import { formatPrice, formatDate } from "@/lib/format";
|
||||
import { getCompanyInfo } from "@/lib/company-info";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getDictionary } from "@/i18n/get-dictionary";
|
||||
|
||||
export const metadata: Metadata = { title: "Rechnung" };
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function InvoicePage(props: PageProps<"/rechnung/[invoiceNumber]">) {
|
||||
const t = getDictionary(await getLocale());
|
||||
const { invoiceNumber } = await props.params;
|
||||
const customer = await getCurrentCustomer();
|
||||
if (!customer) redirect(`/konto?error=${encodeURIComponent(t.invoicePage.loginRequiredError)}`);
|
||||
|
||||
const invoice = await getInvoiceForCustomer(invoiceNumber, customer.id);
|
||||
if (!invoice) notFound();
|
||||
|
||||
const { order } = invoice;
|
||||
const company = await getCompanyInfo();
|
||||
|
||||
return (
|
||||
<div className="py-16 print:py-0">
|
||||
<Container className="max-w-2xl">
|
||||
<div className="print:hidden flex items-center justify-between">
|
||||
<BackLink href="/konto/bestellungen" label={t.invoicePage.backToOrders} />
|
||||
<a
|
||||
href={`/rechnung/${invoice.invoiceNumber}/pdf`}
|
||||
className="mb-6 text-sm text-forest underline"
|
||||
>
|
||||
{t.invoicePage.downloadPdf}
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="text-2xl tracking-[0.2em] font-serif text-forest">{company.name.toUpperCase()}</p>
|
||||
<p className="mt-2 text-sm text-ink/70">
|
||||
{company.address.street}
|
||||
<br />
|
||||
{company.address.zip} {company.address.city}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-sm text-ink/70">
|
||||
<p className="text-forest">{t.invoicePage.invoiceLabel(invoice.invoiceNumber)}</p>
|
||||
<p>{t.invoicePage.orderLabel(order.orderNumber)}</p>
|
||||
<p>{formatDate(invoice.issuedAt)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 text-sm text-ink/75">
|
||||
<p>{order.firstName} {order.lastName}</p>
|
||||
<p>{order.street}</p>
|
||||
<p>{order.zip} {order.city}</p>
|
||||
</div>
|
||||
|
||||
<table className="mt-10 w-full text-sm">
|
||||
<thead className="text-left text-ink/65 border-b border-ink/10">
|
||||
<tr>
|
||||
<th className="py-2">{t.invoicePage.itemHeader}</th>
|
||||
<th className="py-2 text-right">{t.invoicePage.quantityHeader}</th>
|
||||
<th className="py-2 text-right">{t.invoicePage.priceHeader}</th>
|
||||
<th className="py-2 text-right">{t.invoicePage.totalHeader}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-ink/10">
|
||||
{order.items.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td className="py-2">{item.productName} ({item.weightGrams} g)</td>
|
||||
<td className="py-2 text-right">{item.quantity}</td>
|
||||
<td className="py-2 text-right">{formatPrice(item.unitPriceCents)}</td>
|
||||
<td className="py-2 text-right">{formatPrice(item.totalCents)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="mt-6 flex justify-end">
|
||||
<div className="w-56 space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink/70">{t.invoicePage.subtotalLabel}</span>
|
||||
<span>{formatPrice(order.subtotalCents, order.currency)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink/70">{t.invoicePage.shippingLabel}</span>
|
||||
<span>
|
||||
{order.shippingCents === 0 ? t.invoicePage.freeShipping : formatPrice(order.shippingCents, order.currency)}
|
||||
</span>
|
||||
</div>
|
||||
{order.discountCents > 0 ? (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-ink/70">
|
||||
{t.invoicePage.discountLabel}{order.discountCode ? ` (${order.discountCode.code})` : ""}
|
||||
</span>
|
||||
<span>−{formatPrice(order.discountCents, order.currency)}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex justify-between text-forest pt-2 border-t border-ink/10">
|
||||
<span>{t.invoicePage.totalLabel}</span>
|
||||
<span>{formatPrice(invoice.totalCents, order.currency)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-16 text-xs text-ink/65">{t.invoicePage.legalNote}</p>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user