Files
vans-diy-bastelbedarf/src/data/rabatt.ts
T
qciganoandClaude Sonnet 5 e48700f982 Tier-Familien-Seiten verfeinert, Foto-Karussell, Partnercode auf Sale-Ausschluss
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]>
2026-08-02 23:41:27 +02:00

80 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}