diff --git a/public/admin/config.yml b/public/admin/config.yml index 4a4fffa..8292a55 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -92,6 +92,16 @@ collections: - { label: Bestseller, value: bestseller } - { label: "Im Sale", value: sale } - { label: Handgemacht, value: handgemacht } + - label: "🚚 Eigene Versandkosten (nur für Einzelkauf dieses Produkts)" + name: versand + widget: object + required: false + hint: "Gilt NUR, wenn jemand genau 1 Stück von genau diesem Produkt kauft. Bei mehreren Produkten/Stück im Warenkorb gilt automatisch die allgemeine Versandkosten-Staffel (siehe eigener Bereich 'Versandkosten-Staffel' im Menü links). Leer lassen = Staffel gilt auch beim Einzelkauf." + fields: + - { label: "Deutschland (€)", name: de, widget: number, value_type: float, step: 0.01, required: false } + - { label: "Österreich (€)", name: at, widget: number, value_type: float, step: 0.01, required: false } + - { label: "Schweiz (€)", name: ch, widget: number, value_type: float, step: 0.01, required: false } + - { label: "Luxemburg (€)", name: lu, widget: number, value_type: float, step: 0.01, required: false } - name: categories label: Kategorien @@ -147,3 +157,44 @@ collections: - { label: Englisch, name: en, widget: string } - { label: "Schweizerdeutsch", name: ch, widget: string } - { label: Französisch, name: fr, widget: string } + + - name: versandkosten + label: "🚚 Versandkosten-Staffel" + files: + - name: staffel + label: "Versandkosten-Staffel nach Warenwert" + file: src/content/versandstaffel.json + description: > + Gilt automatisch im Warenkorb/an der Kasse, sobald jemand mehrere Produkte oder + mehrere Stück kauft (bei genau 1 Stück von 1 Produkt gilt stattdessen der eigene + Versandpreis am Produkt, falls dort einer eingetragen ist). Ab 75 € Warenwert ist der + Versand immer kostenlos, unabhängig von dieser Tabelle. + fields: + - label: Deutschland + name: de + widget: list + summary: "ab {{fields.abWarenwert}} € → {{fields.kosten}} €" + fields: + - { label: "Ab Warenwert (€)", name: abWarenwert, widget: number, value_type: float, step: 0.01 } + - { label: "Versandkosten (€)", name: kosten, widget: number, value_type: float, step: 0.01 } + - label: Österreich + name: at + widget: list + summary: "ab {{fields.abWarenwert}} € → {{fields.kosten}} €" + fields: + - { label: "Ab Warenwert (€)", name: abWarenwert, widget: number, value_type: float, step: 0.01 } + - { label: "Versandkosten (€)", name: kosten, widget: number, value_type: float, step: 0.01 } + - label: Schweiz + name: ch + widget: list + summary: "ab {{fields.abWarenwert}} € → {{fields.kosten}} €" + fields: + - { label: "Ab Warenwert (€)", name: abWarenwert, widget: number, value_type: float, step: 0.01 } + - { label: "Versandkosten (€)", name: kosten, widget: number, value_type: float, step: 0.01 } + - label: Luxemburg + name: lu + widget: list + summary: "ab {{fields.abWarenwert}} € → {{fields.kosten}} €" + fields: + - { label: "Ab Warenwert (€)", name: abWarenwert, widget: number, value_type: float, step: 0.01 } + - { label: "Versandkosten (€)", name: kosten, widget: number, value_type: float, step: 0.01 } diff --git a/src/content/versandstaffel.json b/src/content/versandstaffel.json new file mode 100644 index 0000000..da3cbfa --- /dev/null +++ b/src/content/versandstaffel.json @@ -0,0 +1,22 @@ +{ + "de": [ + { "abWarenwert": 0, "kosten": 4.95 }, + { "abWarenwert": 25, "kosten": 3.95 }, + { "abWarenwert": 50, "kosten": 2.95 } + ], + "at": [ + { "abWarenwert": 0, "kosten": 6.95 }, + { "abWarenwert": 25, "kosten": 5.95 }, + { "abWarenwert": 50, "kosten": 4.95 } + ], + "ch": [ + { "abWarenwert": 0, "kosten": 9.95 }, + { "abWarenwert": 25, "kosten": 8.95 }, + { "abWarenwert": 50, "kosten": 7.95 } + ], + "lu": [ + { "abWarenwert": 0, "kosten": 6.95 }, + { "abWarenwert": 25, "kosten": 5.95 }, + { "abWarenwert": 50, "kosten": 4.95 } + ] +} diff --git a/src/data/products.ts b/src/data/products.ts index 96f8607..7887e47 100644 --- a/src/data/products.ts +++ b/src/data/products.ts @@ -29,6 +29,11 @@ export interface Product { lieferzeit?: string; badges: Badge[]; bilder?: string[]; // Pfade zu Produktfotos, von VanVan im Admin-Bereich hochgeladen + // Eigene Versandkosten je Land — gelten NUR, wenn genau 1 Stück dieses einen Produkts + // gekauft wird (Einzelkauf). Bei mehreren Produkten/Stückzahlen greift stattdessen die + // 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 }; } const modules = import.meta.glob("/src/content/products/*.json", { eager: true }) as Record< diff --git a/src/data/versand.ts b/src/data/versand.ts new file mode 100644 index 0000000..3ba7dc6 --- /dev/null +++ b/src/data/versand.ts @@ -0,0 +1,73 @@ +// Versandkosten-Logik. Regeln (mit dem Nutzer am 02.08.2026 abgestimmt): +// 1. Warenkorb-Gesamtwert >= 75 € → Versand immer kostenlos, egal wie viele Produkte/Stück. +// 2. Genau 1 Stück von genau 1 Produkt im Warenkorb → der am Produkt selbst hinterlegte +// Versandpreis für das Zielland gilt (falls hinterlegt, sonst Fallback auf die Staffel). +// 3. Alles andere (mehrere Produkte und/oder mehrere Stück) → Versandkosten-Staffel nach +// Gesamt-Warenwert für das Zielland (src/content/versandstaffel.json), von VanVan im +// Adminbereich bearbeitbar. + +import type { Product } from "./products"; + +export type CountryCode = "de" | "at" | "ch" | "lu"; + +export interface VersandStufe { + abWarenwert: number; + kosten: number; +} + +export interface Versandstaffel { + de: VersandStufe[]; + at: VersandStufe[]; + ch: VersandStufe[]; + lu: VersandStufe[]; +} + +// @ts-ignore -- JSON-Import, zur Build-Zeit von Astro/Vite aufgelöst +import versandstaffelRaw from "../content/versandstaffel.json"; +export const versandstaffel = versandstaffelRaw as Versandstaffel; + +export const FREE_SHIPPING_FROM = 75; + +function tierLookup(land: CountryCode, warenwert: number): number { + const stufen = versandstaffel[land] ?? []; + const sortiert = [...stufen].sort((a, b) => a.abWarenwert - b.abWarenwert); + let kosten = sortiert[0]?.kosten ?? 0; + for (const s of sortiert) { + if (warenwert >= s.abWarenwert) kosten = s.kosten; + } + return kosten; +} + +export interface MinimalCartItem { + slug: string; + preis: number; + menge: number; +} + +/** + * Berechnet die Versandkosten für einen Warenkorb in ein bestimmtes Land. + * `alleProdukte` wird gebraucht, um bei Einzelkäufen den produktspezifischen Versandpreis + * nachzuschlagen (die Cart-Items selbst kennen nur slug/preis/menge, keine Versanddaten). + */ +export function berechneVersandkosten( + land: CountryCode, + items: MinimalCartItem[], + alleProdukte: Product[] +): number { + if (items.length === 0) return 0; + + const warenwert = items.reduce((sum, i) => sum + i.preis * i.menge, 0); + if (warenwert >= FREE_SHIPPING_FROM) return 0; + + const gesamtmenge = items.reduce((sum, i) => sum + i.menge, 0); + const istEinzelkauf = items.length === 1 && gesamtmenge === 1; + + if (istEinzelkauf) { + const produkt = alleProdukte.find((p) => p.slug === items[0].slug); + const eigenerPreis = produkt?.versand?.[land]; + if (typeof eigenerPreis === "number") return eigenerPreis; + // Kein eigener Versandpreis hinterlegt -> Staffel als Rückfalllösung + } + + return tierLookup(land, warenwert); +} diff --git a/src/pages/ch/checkout/index.astro b/src/pages/ch/checkout/index.astro index 336d8f2..66fdc60 100644 --- a/src/pages/ch/checkout/index.astro +++ b/src/pages/ch/checkout/index.astro @@ -32,10 +32,10 @@ const t = useTranslations(lang);