- Produktseite: Mengen-Stepper und manuelle Eingabe sind jetzt auf den tatsächlichen Lagerbestand begrenzt (zählt bereits im Warenkorb liegende Menge desselben Produkts mit). Ist das Maximum schon erreicht, wird der "In den Warenkorb"-Button deaktiviert und zeigt "Maximale verfügbare Menge ist bereits im Warenkorb"; bei wenig Rest- bestand (≤5) erscheint stattdessen "Nur noch X auf Lager". - Warenkorb-Seite: der "+"-Button bei der Mengenauswahl ist ebenfalls auf den Lagerbestand begrenzt. - Als zusätzliche Absicherung greift das Limit jetzt auch direkt in addToCart()/updateQuantity() selbst (cart.ts) — unabhängig davon, welche Oberfläche die Funktion aufruft, kann so nie mehr Menge als Bestand ins localStorage gelangen. Verifiziert per Puppeteer an einem Produkt mit Bestand 2: 6× auf "+" klicken bleibt bei 2, manuelles Eintippen von 999 wird auf 2 geklemmt, nach dem Hinzufügen ist der Button deaktiviert, der "+"-Button im Warenkorb erhöht nicht weiter — keine Konsolenfehler.
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
// Rein clientseitiger Warenkorb (Phase 1, kein Backend). Speichert im localStorage.
|
|
// TODO Phase 2: durch echten Cloudflare-Worker-Warenkorb/Checkout ersetzen.
|
|
|
|
import { getProduct } from "../data/products";
|
|
import { effektiverZeilenpreis } from "../data/rabatt";
|
|
import { findeGutschein, gutscheinRabatt, type Gutschein } from "../data/gutscheine";
|
|
|
|
export interface CartItem {
|
|
slug: string;
|
|
name: string;
|
|
preis: number;
|
|
menge: number;
|
|
// Ob dieser Artikel gerade gekauft werden soll oder nur "im Warenkorb liegen bleibt" für
|
|
// später. Fehlt das Feld (ältere, bereits gespeicherte Warenkörbe), gilt es als ausgewählt —
|
|
// siehe istAusgewaehlt().
|
|
ausgewaehlt?: boolean;
|
|
}
|
|
|
|
/** true, sofern nicht explizit auf false gesetzt — hält ältere localStorage-Warenkörbe ohne
|
|
* dieses Feld weiterhin vollständig "ausgewählt" (kein Verhaltensbruch für Bestandsbesucher). */
|
|
export function istAusgewaehlt(item: CartItem): boolean {
|
|
return item.ausgewaehlt !== false;
|
|
}
|
|
|
|
const KEY = "vandiy_cart_v1";
|
|
|
|
export function getCart(): CartItem[] {
|
|
if (typeof localStorage === "undefined") return [];
|
|
try {
|
|
return JSON.parse(localStorage.getItem(KEY) || "[]");
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function saveCart(items: CartItem[]) {
|
|
localStorage.setItem(KEY, JSON.stringify(items));
|
|
window.dispatchEvent(new CustomEvent("cart:changed", { detail: items }));
|
|
}
|
|
|
|
/** Nie mehr als tatsächlich auf Lager ist — greift unabhängig davon, welche Oberfläche
|
|
* addToCart()/updateQuantity() aufruft, damit diese Regel garantiert eingehalten wird und
|
|
* nicht nur an einer einzelnen Stelle in der UI. */
|
|
function maxBestand(slug: string): number {
|
|
const produkt = getProduct(slug);
|
|
return produkt ? produkt.bestand : Infinity;
|
|
}
|
|
|
|
export function addToCart(item: Omit<CartItem, "menge">, menge = 1) {
|
|
const cart = getCart();
|
|
const max = maxBestand(item.slug);
|
|
const existing = cart.find((i) => i.slug === item.slug);
|
|
if (existing) {
|
|
existing.menge = Math.min(existing.menge + menge, max);
|
|
} else {
|
|
cart.push({ ...item, menge: Math.min(Math.max(1, menge), max) });
|
|
}
|
|
saveCart(cart);
|
|
}
|
|
|
|
export function updateQuantity(slug: string, menge: number) {
|
|
let cart = getCart();
|
|
menge = Math.min(menge, maxBestand(slug));
|
|
if (menge <= 0) {
|
|
cart = cart.filter((i) => i.slug !== slug);
|
|
} else {
|
|
const item = cart.find((i) => i.slug === slug);
|
|
if (item) item.menge = menge;
|
|
}
|
|
saveCart(cart);
|
|
}
|
|
|
|
export function removeFromCart(slug: string) {
|
|
saveCart(getCart().filter((i) => i.slug !== slug));
|
|
}
|
|
|
|
/** Legt fest, ob ein Artikel jetzt gekauft werden soll (true) oder nur im Warenkorb liegen
|
|
* bleibt, ohne mitgekauft zu werden (false) — z.B. wenn man sich noch nicht sicher ist. */
|
|
export function setAusgewaehlt(slug: string, ausgewaehlt: boolean) {
|
|
const cart = getCart();
|
|
const item = cart.find((i) => i.slug === slug);
|
|
if (item) item.ausgewaehlt = ausgewaehlt;
|
|
saveCart(cart);
|
|
}
|
|
|
|
export function cartCount(): number {
|
|
return getCart().reduce((sum, i) => sum + i.menge, 0);
|
|
}
|
|
|
|
// Zwischensumme inkl. Produkt-Rabatt (Prozent/Betrag) UND Mengenrabatt-Staffel — schaut sich
|
|
// dafür live die aktuellen Produktdaten an (nicht den bei addToCart gespeicherten Preis), damit
|
|
// ein nachträglich geänderter/abgelaufener Rabatt sich sofort korrekt auswirkt. Zählt NUR
|
|
// Artikel, die gerade zum Kauf ausgewählt sind — abgewählte Artikel bleiben im Warenkorb liegen,
|
|
// fließen aber nicht in Summe/Checkout ein.
|
|
export function cartTotal(): number {
|
|
return getCart()
|
|
.filter(istAusgewaehlt)
|
|
.reduce((sum, i) => {
|
|
const produkt = getProduct(i.slug);
|
|
return sum + (produkt ? effektiverZeilenpreis(produkt, i.menge) : i.menge * i.preis);
|
|
}, 0);
|
|
}
|
|
|
|
const COUPON_KEY = "vandiy_coupon_v1";
|
|
|
|
export function getCouponCode(): string {
|
|
if (typeof localStorage === "undefined") return "";
|
|
return localStorage.getItem(COUPON_KEY) || "";
|
|
}
|
|
|
|
export function setCouponCode(code: string) {
|
|
localStorage.setItem(COUPON_KEY, code.trim());
|
|
window.dispatchEvent(new CustomEvent("cart:changed", { detail: getCart() }));
|
|
}
|
|
|
|
export function clearCoupon() {
|
|
localStorage.removeItem(COUPON_KEY);
|
|
window.dispatchEvent(new CustomEvent("cart:changed", { detail: getCart() }));
|
|
}
|
|
|
|
/** Der aktuell eingelöste Gutschein, falls der gespeicherte Code (noch) gültig ist. */
|
|
export function appliedCoupon(): Gutschein | undefined {
|
|
const code = getCouponCode();
|
|
if (!code) return undefined;
|
|
return findeGutschein(code);
|
|
}
|
|
|
|
/** Rabatt-Betrag in Euro, den der eingelöste Gutschein auf die gegebene Zwischensumme gibt. */
|
|
export function couponDiscount(subtotal: number): number {
|
|
const g = appliedCoupon();
|
|
return g ? gutscheinRabatt(g, subtotal) : 0;
|
|
}
|