Rabatt-System, größerer Mengen-Stepper, Zahlungsart zuerst, echte HasiDog-Inhalte

- Neues Rabatt-System: Prozent-/Betrag-Rabatt mit optionalem Ablaufdatum,
  Mengenrabatt-Staffel pro Produkt, sowie site-weite Gutscheincodes.
  Wird live im Warenkorb/Checkout berechnet (src/data/rabatt.ts,
  src/data/gutscheine.ts, cart.ts) und ist im Adminbereich direkt unter dem
  Preis bearbeitbar (config.yml: "Rabatte" + neue Collection
  "Gutscheincodes"). Effektiver Preis + Rabatt-Badge auf Produktkarten und
  Produktseite sichtbar.
- Mengen-Stepper auf der Produktseite vergrößert (bereits vorbereitet,
  jetzt mit Build/Test verifiziert).
- Zahlungsart im Checkout auf Platz 1, mit markenfarbigen, fertig
  gestalteten Buttons (PayPal/Klarna/Banküberweisung).
- Unterkategorie-Seiten zeigen jetzt VanVans eigene Produktbeschreibung
  und das echte Fotos des Produkts statt Platzhaltertext, mit kleinem
  Link zur HasiDog-Universum-Seite statt großem Button.

Verifiziert per Puppeteer-Funktionstest gegen den echten Production-Build
(Rabatt-Berechnung, Mengenrabatt, Gutschein-Einlösung, Checkout-Summary,
keine Konsolenfehler).
This commit is contained in:
qcigano
2026-08-02 18:59:11 +02:00
parent 0f93ab73c3
commit 6f850181f1
25 changed files with 1098 additions and 194 deletions
+36
View File
@@ -0,0 +1,36 @@
// Gutscheincodes — site-weit gültig, von VanVan im Adminbereich unter "🎟️ Gutscheincodes"
// verwaltet. Werden im Warenkorb/Checkout eingelöst und auf den Gesamt-Warenwert angewendet.
// @ts-ignore -- JSON-Import, zur Build-Zeit von Astro/Vite aufgelöst
import gutscheineRaw from "../content/gutscheine.json";
export interface Gutschein {
code: string;
prozent?: number;
betrag?: number;
bis?: string; // ISO-Datum, optional
}
const gutscheine = (gutscheineRaw as { codes: Gutschein[] }).codes ?? [];
function istGueltig(bis?: string): boolean {
if (!bis) return true;
return new Date() <= new Date(`${bis}T23:59:59`);
}
/** Sucht einen Gutschein anhand des eingegebenen Codes (Groß-/Kleinschreibung egal). */
export function findeGutschein(code: string): Gutschein | undefined {
const normalisiert = code.trim().toUpperCase();
return gutscheine.find((g) => g.code.toUpperCase() === normalisiert && istGueltig(g.bis));
}
/** Berechnet den Rabatt-Betrag in Euro für einen gegebenen Warenwert. */
export function gutscheinRabatt(gutschein: Gutschein, warenwert: number): number {
if (typeof gutschein.prozent === "number" && gutschein.prozent > 0) {
return Math.min(warenwert, warenwert * (gutschein.prozent / 100));
}
if (typeof gutschein.betrag === "number" && gutschein.betrag > 0) {
return Math.min(warenwert, gutschein.betrag);
}
return 0;
}
+13
View File
@@ -34,6 +34,15 @@ export interface Product {
// Versandkosten-Staffel (siehe src/data/versand.ts). Optional, damit Bestandsprodukte ohne
// diese Angabe nicht kaputtgehen — dann greift automatisch die Staffel als Rückfalllösung.
versand?: { de?: number; at?: number; ch?: number; lu?: number };
// Rabatte — mehrere Arten gleichzeitig möglich (siehe src/data/rabatt.ts für die Logik):
// 1. Prozent/Betrag-Rabatt, optional zeitlich befristet (läuft automatisch ab).
// 2. Mengenrabatt-Staffel (ab X Stück Y% Rabatt), wird im Warenkorb automatisch angewendet.
rabatt?: {
prozent?: number; // z.B. 20 = 20% Rabatt auf den Preis
betrag?: number; // Alternative zu prozent: fester Euro-Betrag Rabatt
bis?: string; // ISO-Datum, z.B. "2026-12-31" — Rabatt gilt nur bis einschließlich diesem Tag
mengenrabatt?: { abMenge: number; prozent: number }[];
};
}
const modules = import.meta.glob("/src/content/products/*.json", { eager: true }) as Record<
@@ -53,6 +62,10 @@ export function productsByCategory(kategorie: string): Product[] {
return products.filter((p) => p.kategorie === kategorie);
}
export function productsByUnterkategorie(kategorie: string, unterkategorie: string): Product[] {
return products.filter((p) => p.kategorie === kategorie && p.unterkategorie === unterkategorie);
}
export function saleProducts(): Product[] {
return products.filter((p) => p.badges.includes("sale") || p.preisAlt);
}
+71
View File
@@ -0,0 +1,71 @@
// 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;
}
/** 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;
}