Mehrere Feinschliff-Punkte auf den Tier-Familien-Seiten (z.B. "Hase"): - Abstand zwischen der Story-Box und der Kartenreihe verkleinert (vorher durch zwei separate Section-Umrandungen unnötig groß) — beide Bereiche teilen sich jetzt eine gemeinsame Section statt zwei gestapelte. - Platzhalter-Karten (Charaktere ohne Foto) bekommen jetzt exakt die gleiche Bildhöhe wie eine Karte mit echtem Foto (vorher griff nur die allgemeine Mindesthöhe, wirkte kleiner/anders proportioniert). - Neues optionales Feld `bannerName` an der Unterkategorie: überschreibt NUR die große Bannerüberschrift (Breadcrumb/Chips bleiben unverändert) — für "Hase" jetzt auf "HasiDog" gesetzt, den bekanntesten Charakter der Familie. - Unterkategorie-Seiten ganz ohne Produkte/Charaktere (z.B. Maschen- markierer, Holzknöpfe) zeigen jetzt schon 3 vorbereitete Platzhalter- Karten im selben Kartenstil statt nur einer nackten Textzeile — die Seite "sieht schon fertig aus", auch bevor echte Produkte da sind. Nebenbei einen Bug behoben: bei Unterkategorien ohne Charaktere, aber MIT echten Produkten, erschien vorher zusätzlich fälschlich der "noch keine Produkte"-Hinweis über der echten Produktreihe. Neu: Mini-Bildkarussell direkt auf jeder Produktkarte (Shop-Grid, Charakter-Kacheln, "Weitere Modelle", Sale-Seite, Startseite — überall wo ProductCard verwendet wird). Sobald ein Produkt mehrere Fotos hat (Admin-Bereich erlaubt das bereits über das Foto-Listenfeld), erscheinen dezente ‹/›-Pfeile plus Punkte-Indikator direkt auf der Karte, ganz ohne erst die Produktseite öffnen zu müssen. Neu: Partner-Rabattcodes können im Admin jetzt gezielt auf "gilt NICHT für Artikel, die schon im Sale sind" gestellt werden (giltAufSale: false) — der Rabatt berechnet sich dann nur noch auf den regulär bepreisten Teil des Warenkorbs, damit sich der Partner-Rabatt nicht zusätzlich auf einen bereits reduzierten Preis obendrauf summiert. Standardverhalten (Feld leer/true) bleibt wie bisher unverändert. Alle Änderungen lokal mit Test-Daten durchgespielt (mehrere Testfotos, Testprodukt im Sale + Partnercode mit/ohne giltAufSale, leere Unterkategorie-Seite) und vor dem Commit wieder zurückgesetzt. Co-Authored-By: Claude Sonnet 5 <[email protected]>
80 lines
3.9 KiB
TypeScript
80 lines
3.9 KiB
TypeScript
// Rabatt-Logik. Unterstützt "alle Art von Rabatten" (Wunsch vom 02.08.2026):
|
||
// 1. Klassischer Sale-Preis (bereits vorhanden: `preis`/`preisAlt`, manuell gesetzt).
|
||
// 2. Prozentualer oder fester Euro-Rabatt am Produkt, automatisch berechnet, optional
|
||
// zeitlich befristet (läuft am Stichtag von selbst wieder ab, ohne dass VanVan etwas
|
||
// manuell zurückstellen muss).
|
||
// 3. Mengenrabatt-Staffel (z.B. "ab 3 Stück 10% Rabatt") — wird im Warenkorb automatisch
|
||
// auf die Zeile angewendet.
|
||
// 4. Gutscheincodes (site-weit, siehe src/data/gutscheine.ts) — im Warenkorb/Checkout einlösbar.
|
||
|
||
import type { Product } from "./products";
|
||
|
||
/** Prüft, ob ein befristeter Rabatt heute noch gültig ist (Vergleich läuft im Browser zur
|
||
* echten aktuellen Uhrzeit, nicht nur zur Build-Zeit — der Rabatt läuft also wirklich
|
||
* automatisch ab, auch ohne neuen Deploy). */
|
||
export function istRabattAktiv(bis?: string): boolean {
|
||
if (!bis) return true;
|
||
const heute = new Date();
|
||
const ende = new Date(`${bis}T23:59:59`);
|
||
return heute <= ende;
|
||
}
|
||
|
||
/** Berechnet den effektiven Verkaufspreis eines Produkts unter Berücksichtigung von
|
||
* Prozent-/Betrag-Rabatt (falls aktiv). Reiner Sale-Preis über `preisAlt` bleibt unabhängig
|
||
* davon bestehen und wird hier NICHT verändert — beide Mechanismen sind unabhängig
|
||
* kombinierbar (z.B. Sale-Preis als Basis, zusätzlich zeitlich befristete Rabattaktion oben drauf). */
|
||
export function effektiverPreis(product: Product): number {
|
||
const basis = product.preis;
|
||
const r = product.rabatt;
|
||
if (!r || !istRabattAktiv(r.bis)) return basis;
|
||
if (typeof r.prozent === "number" && r.prozent > 0) {
|
||
return Math.max(0, basis * (1 - r.prozent / 100));
|
||
}
|
||
if (typeof r.betrag === "number" && r.betrag > 0) {
|
||
return Math.max(0, basis - r.betrag);
|
||
}
|
||
return basis;
|
||
}
|
||
|
||
/** Gibt an, ob gerade ein aktiver Rabatt greift (für Badge-Anzeige "-20%" o.ä.). */
|
||
export function hatAktivenRabatt(product: Product): boolean {
|
||
return effektiverPreis(product) < product.preis;
|
||
}
|
||
|
||
/** Ob ein Produkt gerade "im Sale" ist — zählt sowohl den klassischen, manuell gesetzten
|
||
* Sale-Preis (preisAlt) als auch einen automatisch berechneten, noch aktiven Prozent-/
|
||
* Betrags-Rabatt. Wird u.a. von Partner-Rabattcodes genutzt, die per Admin-Einstellung NUR auf
|
||
* regulär bepreiste Artikel gelten sollen (siehe Partnercode.giltAufSale in partnercodes.ts). */
|
||
export function istImSale(product: Product): boolean {
|
||
return Boolean(product.preisAlt) || hatAktivenRabatt(product);
|
||
}
|
||
|
||
/** Für die Badge-Anzeige: "-20%" oder "-5 €", je nachdem was hinterlegt ist. */
|
||
export function rabattLabel(product: Product): string | null {
|
||
const r = product.rabatt;
|
||
if (!r || !istRabattAktiv(r.bis)) return null;
|
||
if (typeof r.prozent === "number" && r.prozent > 0) return `-${r.prozent}%`;
|
||
if (typeof r.betrag === "number" && r.betrag > 0) return `-${r.betrag.toFixed(2).replace(".", ",")} €`;
|
||
return null;
|
||
}
|
||
|
||
/** Mengenrabatt: findet den besten (höchsten) Prozentsatz, der bei der gegebenen Stückzahl
|
||
* greift. Gibt 0 zurück, wenn kein Mengenrabatt hinterlegt ist oder die Menge zu klein ist. */
|
||
export function mengenrabattProzent(product: Product, menge: number): number {
|
||
const stufen = product.rabatt?.mengenrabatt ?? [];
|
||
let bester = 0;
|
||
for (const stufe of stufen) {
|
||
if (menge >= stufe.abMenge && stufe.prozent > bester) bester = stufe.prozent;
|
||
}
|
||
return bester;
|
||
}
|
||
|
||
/** Effektiver Zeilenpreis (Einzelpreis inkl. Rabatt × Menge, zusätzlich mit Mengenrabatt
|
||
* verrechnet) — das ist die Zahl, die im Warenkorb pro Position angezeigt/summiert wird. */
|
||
export function effektiverZeilenpreis(product: Product, menge: number): number {
|
||
const einzelpreis = effektiverPreis(product);
|
||
const mengenProzent = mengenrabattProzent(product, menge);
|
||
const zeile = einzelpreis * menge;
|
||
return mengenProzent > 0 ? zeile * (1 - mengenProzent / 100) : zeile;
|
||
}
|