feat: Bienenpatenschaft mit Urkunde, Abo, Medaillen und 3D-Konto
Setzt Teilprojekte 1-5 der Spec 2026-10-16 um: - Urkunden-Engine (lib/certificate): ein Layout fuer PDF, Vorschau und 3D-Textur, Vorlage classic@1, eingebettete OFL-Schriften, Testmatrix - Einmal-Patenschaft ueber Traegerprodukte (Kategorie PATENSCHAFT), Paket wird im Checkout in Patenschaft (Regelsatz) und Honig aufgeteilt, Kontopflicht, rein digitale Bestellungen (FulfillmentMethod DIGITAL) - Ausstellung nach Zahlung idempotent, Wiederholung im App-Prozess (instrumentation + background-jobs), Ablage in CERTIFICATE_DIR - Verkaufsseite /patenschaft mit Konfigurator und Live-Vorschau, Warenkorb mit Schluessel je Position - Kundenkonto: 3D-Ansicht von Urkunde und Medaille, Downloads, ZIP, Uebertragung per befristetem Einmal-Link, Treuestufen bis VIP - Admin: Angebote, Patenschaften, Druck, Korrektur mit Revisionen, Abos, Erntefenster, Deckung, SEPA-Frist, Treuestufen - Abo ueber Stripe Billing: Lieferung erst nach bezahlten Monaten, SEPA-Neukunden-Frist und Rueckbuchungsschutz, Honigreservierung, Kuendigungsbutton, Monatsrechnungen, Stripe-Einrichtungsskript Startangebote werden inaktiv angelegt; Livebetrieb erst nach Freigabe. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
dcb578f062
commit
f227aeb8f9
269 files changed
+28054
-141
No files matched your search
@@ -0,0 +1,76 @@
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { BackLink } from "@/components/back-link";
|
||||
import { AdminBadge } from "@/components/admin-list";
|
||||
import { asStringArray } from "@/lib/json-array";
|
||||
import { getLocale } from "@/i18n/locale";
|
||||
import { getSponsorshipAdminStrings } from "@/i18n/sponsorship/admin";
|
||||
import { updateSponsorshipOfferAction } from "@/app/admin/actions";
|
||||
import { OfferForm } from "../../_components/offer-form";
|
||||
import { getHoneyProductOptions, getOfferOverview } from "../../_lib/offer-overview";
|
||||
|
||||
export default async function AdminSponsorshipOfferEditPage(props: PageProps<"/admin/patenschaften/angebote/[id]">) {
|
||||
const t = getSponsorshipAdminStrings(await getLocale());
|
||||
const tf = t.offerForm;
|
||||
const { id } = await props.params;
|
||||
const [row] = await getOfferOverview(id);
|
||||
if (!row) notFound();
|
||||
const { offer, isAvailable, honeyStock, activeSubscriptions } = row;
|
||||
const honeyOptions = await getHoneyProductOptions(offer.honeyProductId);
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<BackLink href="/admin/patenschaften/angebote" label={tf.backToOffers} />
|
||||
<h1 className="mb-6 text-2xl text-heading">{tf.editTitle(offer.name)}</h1>
|
||||
|
||||
{/* Live-Verfuegbarkeit: dieselbe Entscheidung wie auf der Verkaufsseite. */}
|
||||
<section className="mb-8 border border-ink/10 bg-background p-6">
|
||||
<h2 className="mb-3 text-xs tracking-wide-caps text-ink/65">{tf.availabilityHeading}</h2>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<AdminBadge tone={isAvailable ? "positive" : "pending"}>{isAvailable ? t.offers.available : t.offers.unavailable}</AdminBadge>
|
||||
<span className="text-sm text-ink/75">{isAvailable ? tf.availableNow : tf.unavailableNow}</span>
|
||||
</div>
|
||||
{honeyStock ? (
|
||||
<p className="mt-3 text-sm tabular-nums text-ink/75">{tf.stockLine(honeyStock.stock, honeyStock.reserved, honeyStock.free)}</p>
|
||||
) : null}
|
||||
{offer.kind === "ABO" ? (
|
||||
<p className="mt-1 text-sm tabular-nums text-ink/75">{tf.activeSubscriptions(activeSubscriptions, offer.maxActiveSubscriptions)}</p>
|
||||
) : null}
|
||||
{offer.kind === "ABO" && offer.cycle === "ERNTE" ? (
|
||||
<Link href={`/admin/patenschaften/angebote/${offer.id}/erntefenster`} className="link-draw mt-3 inline-block text-sm text-heading">
|
||||
{t.offers.harvestWindows} →
|
||||
</Link>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<OfferForm
|
||||
isNew={false}
|
||||
honeyOptions={honeyOptions}
|
||||
action={updateSponsorshipOfferAction}
|
||||
t={t}
|
||||
defaults={{
|
||||
id: offer.id,
|
||||
kind: offer.kind,
|
||||
name: offer.name,
|
||||
nameEn: offer.nameEn ?? "",
|
||||
slug: offer.slug,
|
||||
description: offer.description,
|
||||
descriptionEn: offer.descriptionEn ?? "",
|
||||
features: asStringArray(offer.features),
|
||||
featuresEn: asStringArray(offer.featuresEn),
|
||||
priceCents: offer.priceCents,
|
||||
honeyShareCentsPerJar: offer.honeyShareCentsPerJar,
|
||||
honeyShareCentsPerMonth: offer.honeyShareCentsPerMonth,
|
||||
isActive: offer.isActive,
|
||||
isPhysical: offer.isPhysical,
|
||||
sortOrder: offer.sortOrder,
|
||||
honeyProductId: offer.honeyProductId,
|
||||
honeyJars: offer.honeyJars,
|
||||
cycle: offer.cycle,
|
||||
maxActiveSubscriptions: offer.maxActiveSubscriptions,
|
||||
medalTier: offer.medalTier,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user