diff --git a/public/admin/config.yml b/public/admin/config.yml index 0db4062..9ae0043 100644 --- a/public/admin/config.yml +++ b/public/admin/config.yml @@ -367,21 +367,52 @@ collections: - label: "Kleines Abo" name: klein widget: object - fields: + fields: &abo-stufe-fields - { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 } - { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 } + - { label: "Aktuell neu buchbar?", name: aktiv, widget: boolean, default: true, hint: "Aus: Diese Stufe erscheint im Konto ausgegraut und kann nicht neu abgeschlossen werden. Bestehende Kund:innen dieser Stufe behalten trotzdem alle Vorteile." } + - { label: "Kleiner Teddy: alle wie viele Monate?", name: kleinIntervallMonate, widget: number, value_type: int, min: 0, required: false, hint: "0 oder leer lassen = diese Stufe vergibt gar keinen kleinen Teddy." } + - { label: "Großer Teddy: alle wie viele Monate?", name: grossIntervallMonate, widget: number, value_type: int, min: 0, required: false, hint: "0 oder leer lassen = diese Stufe vergibt gar keinen großen Teddy." } + - label: "Name der Stufe" + name: name + widget: object + fields: + - { label: Deutsch, name: de, widget: string } + - { label: Englisch, name: en, widget: string } + - { label: "Schweizerdeutsch", name: ch, widget: string } + - { label: Französisch, name: fr, widget: string } + - label: "Vorteilstext (erscheint auf der Plan-Karte im Kundenkonto)" + name: vorteil + widget: object + fields: + - { label: Deutsch, name: de, widget: text } + - { label: Englisch, name: en, widget: text } + - { label: "Schweizerdeutsch", name: ch, widget: text } + - { label: Französisch, name: fr, widget: text } - label: "Mittleres Abo" name: mittel widget: object - fields: - - { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 } - - { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 } + fields: *abo-stufe-fields - label: "Großes Abo" name: gross widget: object - fields: - - { label: "Preis pro Monat (€)", name: preis, widget: number, value_type: float, step: 0.01 } - - { label: "Rabatt auf jede Bestellung (%)", name: rabattProzent, widget: number, value_type: int, min: 0, max: 100 } + fields: *abo-stufe-fields + - label: "Welches Produkt ist der 'kleine Teddy'?" + name: kleinTeddySlug + widget: relation + collection: products + search_fields: ["name.de"] + value_field: "slug" + display_fields: ["name.de"] + hint: "Dieses Produkt wird kostenlos in den Warenkorb gelegt, sobald jemand eine 'kleiner Teddy'-Prämie einlöst." + - label: "Welches Produkt ist der 'große Teddy'?" + name: grossTeddySlug + widget: relation + collection: products + search_fields: ["name.de"] + value_field: "slug" + display_fields: ["name.de"] + hint: "Dieses Produkt wird kostenlos in den Warenkorb gelegt, sobald jemand eine 'großer Teddy'-Prämie einlöst." - label: "Abo-Rabatt mit anderen Rabatten kombinierbar (Gutschein/Partnercode/Treuebonus)?" name: kombinierbarMitAnderenRabatten widget: boolean diff --git a/src/content/abo-einstellungen.json b/src/content/abo-einstellungen.json index 56f668a..67da8d4 100644 --- a/src/content/abo-einstellungen.json +++ b/src/content/abo-einstellungen.json @@ -1,7 +1,63 @@ { - "klein": { "preis": 4.99, "rabattProzent": 5 }, - "mittel": { "preis": 9.99, "rabattProzent": 10 }, - "gross": { "preis": 19.99, "rabattProzent": 20 }, + "klein": { + "preis": 4.99, + "rabattProzent": 5, + "aktiv": true, + "kleinIntervallMonate": 12, + "grossIntervallMonate": 0, + "name": { + "de": "Kleines Abo", + "en": "Small plan", + "ch": "Chliises Abo", + "fr": "Petit abonnement" + }, + "vorteil": { + "de": "5 % Rabatt auf jede Bestellung. Nach jedem vollen Abo-Jahr: 1 kleiner Teddy gratis.", + "en": "5% off every order. After each full subscription year: 1 small teddy for free.", + "ch": "5 % Rabatt uf jedi Bstellig. Nach jedem volle Abo-Jahr: 1 chliine Teddy gratis.", + "fr": "5 % de remise sur chaque commande. Après chaque année d'abonnement complète : 1 petit ours en peluche offert." + } + }, + "mittel": { + "preis": 9.99, + "rabattProzent": 10, + "aktiv": true, + "kleinIntervallMonate": 0, + "grossIntervallMonate": 12, + "name": { + "de": "Mittleres Abo", + "en": "Medium plan", + "ch": "Mittleres Abo", + "fr": "Abonnement moyen" + }, + "vorteil": { + "de": "10 % Rabatt auf jede Bestellung. Nach jedem vollen Abo-Jahr: 1 großer Teddy gratis.", + "en": "10% off every order. After each full subscription year: 1 large teddy for free.", + "ch": "10 % Rabatt uf jedi Bstellig. Nach jedem volle Abo-Jahr: 1 grosse Teddy gratis.", + "fr": "10 % de remise sur chaque commande. Après chaque année d'abonnement complète : 1 grand ours en peluche offert." + } + }, + "gross": { + "preis": 19.99, + "rabattProzent": 20, + "aktiv": true, + "kleinIntervallMonate": 6, + "grossIntervallMonate": 12, + "name": { + "de": "Großes Abo", + "en": "Large plan", + "ch": "Grosses Abo", + "fr": "Grand abonnement" + }, + "vorteil": { + "de": "20 % Rabatt auf jede Bestellung. Alle 6 Monate ein kleiner Teddy, nach jedem vollen Jahr zusätzlich ein großer Teddy.", + "en": "20% off every order. A small teddy every 6 months, plus a large teddy after every full year.", + "ch": "20 % Rabatt uf jedi Bstellig. Alli 6 Monet en chliine Teddy, nach jedem volle Jahr zuesätzlich en grosse Teddy.", + "fr": "20 % de remise sur chaque commande. Un petit ours tous les 6 mois, et un grand ours en plus après chaque année complète." + } + }, + "kleinTeddySlug": "kleiner-teddy", + "grossTeddySlug": "grosser-teddy", "kombinierbarMitAnderenRabatten": false, "versandKostenlosBeiPraemie": false, "fortsetzungBeiWiederanmeldung": false, diff --git a/src/data/abo.ts b/src/data/abo.ts index 6d9501d..bce1230 100644 --- a/src/data/abo.ts +++ b/src/data/abo.ts @@ -8,15 +8,38 @@ import raw from "../content/abo-einstellungen.json"; export type AboStufe = "klein" | "mittel" | "gross"; +export interface MehrsprachigerText { + de: string; + en: string; + ch: string; + fr: string; +} + export interface AboStufenKonfig { preis: number; rabattProzent: number; + /** Ob diese Stufe aktuell neu buchbar ist. Bestehende Abonnent:innen dieser Stufe behalten + * ihre Vorteile unabhängig davon — nur NEUE Buchungen werden im Kundenkonto gesperrt. */ + aktiv: boolean; + /** Nach wie vielen durchgehend bezahlten Monaten es jeweils einen kleinen/großen Teddy gibt. + * 0 oder nicht gesetzt = diese Stufe kennt diese Prämienart gar nicht. Admin-editierbar, + * ersetzt die früher fest im Code verankerten 6-/12-Monats-Intervalle. */ + kleinIntervallMonate: number; + grossIntervallMonate: number; + /** Anzeigename/Vorteilstext der Stufe, pro Sprache — ersetzt die früher fest in i18n/ui.ts + * verankerten Texte, damit VanVan Name und Beschreibung jeder Stufe selbst pflegen kann. */ + name: MehrsprachigerText; + vorteil: MehrsprachigerText; } export interface AboEinstellungen { klein: AboStufenKonfig; mittel: AboStufenKonfig; gross: AboStufenKonfig; + /** Produkt-Slugs (siehe src/content/products/*.json), die als "kleiner"/"großer" Teddy für + * die Prämien-Einlösung verwendet werden — im Admin per Produkt-Auswahl änderbar. */ + kleinTeddySlug: string; + grossTeddySlug: string; kombinierbarMitAnderenRabatten: boolean; versandKostenlosBeiPraemie: boolean; fortsetzungBeiWiederanmeldung: boolean; @@ -31,27 +54,42 @@ export function aboStufenKonfig(stufe: AboStufe): AboStufenKonfig { export const ABO_STUFEN: AboStufe[] = ["klein", "mittel", "gross"]; +/** Name/Vorteilstext der Stufe in der gewünschten Sprache, mit Fallback auf Deutsch, falls das + * Feld im Admin (noch) leer gelassen wurde. */ +export function aboStufeName(stufe: AboStufe, lang: keyof MehrsprachigerText): string { + const cfg = aboStufenKonfig(stufe); + return cfg.name?.[lang] || cfg.name?.de || stufe; +} +export function aboStufeVorteil(stufe: AboStufe, lang: keyof MehrsprachigerText): string { + const cfg = aboStufenKonfig(stufe); + return cfg.vorteil?.[lang] || cfg.vorteil?.de || ""; +} + /** Wie viele Teddy-Prämien welcher Art bei X durchgehend bezahlten Monaten in dieser Stufe - * INSGESAMT freigeschaltet sein müssten (kumulativ, nicht nur "neu diesen Monat"): - * - Klein-Abo: 1 kleiner Teddy pro volles Jahr. - * - Mittel-Abo: 1 großer Teddy pro volles Jahr. - * - Groß-Abo: 1 kleiner Teddy alle 6 Monate UND zusätzlich 1 großer Teddy pro volles Jahr - * (nach 6 Monaten: 1 klein · nach 12: 2 klein + 1 groß · nach 18: 3 klein · nach 24: 4 klein + 2 groß). */ + * INSGESAMT freigeschaltet sein müssten (kumulativ, nicht nur "neu diesen Monat") — rechnet + * rein aus den admin-konfigurierten Intervallen (siehe AboStufenKonfig), keine fest im Code + * verankerten Zahlen mehr. Ein Intervall von 0 (oder nicht gesetzt) bedeutet "diese Stufe + * kennt diese Prämienart nicht". */ export function teddyMeilensteine(stufe: AboStufe, monate: number): { klein: number; gross: number } { if (monate <= 0) return { klein: 0, gross: 0 }; - if (stufe === "klein") return { klein: Math.floor(monate / 12), gross: 0 }; - if (stufe === "mittel") return { klein: 0, gross: Math.floor(monate / 12) }; - return { klein: Math.floor(monate / 6), gross: Math.floor(monate / 12) }; + const cfg = aboStufenKonfig(stufe); + const kleinIntervall = cfg.kleinIntervallMonate || 0; + const grossIntervall = cfg.grossIntervallMonate || 0; + return { + klein: kleinIntervall > 0 ? Math.floor(monate / kleinIntervall) : 0, + gross: grossIntervall > 0 ? Math.floor(monate / grossIntervall) : 0, + }; } /** Wie viele Monate fehlen noch bis zum jeweils nächsten Meilenstein (für die * Fortschrittsanzeige im Kundenkonto), null wenn diese Stufe die Prämienart gar nicht kennt. */ export function naechsteMeilensteine(stufe: AboStufe, monate: number): { klein: number | null; gross: number | null } { - const kleinIntervall = stufe === "gross" ? 6 : stufe === "klein" ? 12 : null; - const grossIntervall = stufe === "klein" ? null : 12; + const cfg = aboStufenKonfig(stufe); + const kleinIntervall = cfg.kleinIntervallMonate || 0; + const grossIntervall = cfg.grossIntervallMonate || 0; return { - klein: kleinIntervall ? kleinIntervall - (monate % kleinIntervall) : null, - gross: grossIntervall ? grossIntervall - (monate % grossIntervall) : null, + klein: kleinIntervall > 0 ? kleinIntervall - (monate % kleinIntervall) : null, + gross: grossIntervall > 0 ? grossIntervall - (monate % grossIntervall) : null, }; } diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index ed0d281..7ec5dea 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -252,6 +252,7 @@ export const ui = { aboAbschliessenBtn: "Abo abschließen", aboAktivesLabel: "✓ Dein aktuelles Abo", aboWechselnBtn: "Zu diesem Abo wechseln", + aboNichtBuchbar: "Derzeit nicht buchbar", aboStatusTitle: "Dein Abo-Status", aboFieldStufe: "Abo-Stufe", aboFieldPreis: "Monatlicher Preis", aboFieldRabatt: "Enthaltener Rabatt", aboFieldStart: "Startdatum", aboFieldNaechsteZahlung: "Nächste Zahlung", aboFieldLaufzeit: "Bisherige Laufzeit", @@ -548,6 +549,7 @@ export const ui = { aboAbschliessenBtn: "Subscribe", aboAktivesLabel: "✓ Your current plan", aboWechselnBtn: "Switch to this plan", + aboNichtBuchbar: "Currently unavailable", aboStatusTitle: "Your subscription status", aboFieldStufe: "Plan", aboFieldPreis: "Monthly price", aboFieldRabatt: "Included discount", aboFieldStart: "Start date", aboFieldNaechsteZahlung: "Next payment", aboFieldLaufzeit: "Duration so far", @@ -844,6 +846,7 @@ export const ui = { aboAbschliessenBtn: "Abo abschliesse", aboAktivesLabel: "✓ Dis aktuells Abo", aboWechselnBtn: "Zu dem Abo wechsle", + aboNichtBuchbar: "Momentan nöd buechbar", aboStatusTitle: "Din Abo-Status", aboFieldStufe: "Abo-Stufe", aboFieldPreis: "Monetliche Priis", aboFieldRabatt: "Enthaltene Rabatt", aboFieldStart: "Startdatum", aboFieldNaechsteZahlung: "Nächschti Zahlig", aboFieldLaufzeit: "Bisherigi Laufziit", @@ -1140,6 +1143,7 @@ export const ui = { aboAbschliessenBtn: "S'abonner", aboAktivesLabel: "✓ Votre abonnement actuel", aboWechselnBtn: "Passer à cet abonnement", + aboNichtBuchbar: "Actuellement indisponible", aboStatusTitle: "Statut de votre abonnement", aboFieldStufe: "Formule", aboFieldPreis: "Prix mensuel", aboFieldRabatt: "Remise incluse", aboFieldStart: "Date de début", aboFieldNaechsteZahlung: "Prochain paiement", aboFieldLaufzeit: "Durée jusqu'ici", diff --git a/src/pages/ch/konto/angemeldet.astro b/src/pages/ch/konto/angemeldet.astro index 6b7107f..1c7fa94 100644 --- a/src/pages/ch/konto/angemeldet.astro +++ b/src/pages/ch/konto/angemeldet.astro @@ -5,7 +5,7 @@ import { products, getProduct } from "../../../data/products"; import type { Locale } from "../../../i18n/config"; import { useTranslations } from "../../../i18n/ui"; import { formatPrice } from "../../../i18n/format"; -import { ABO_STUFEN, aboStufenKonfig, type AboStufe } from "../../../data/abo"; +import { ABO_STUFEN, aboStufenKonfig, aboStufeName, aboStufeVorteil, type AboStufe } from "../../../data/abo"; const lang: Locale = "ch"; const t = useTranslations(lang); @@ -53,9 +53,9 @@ const statusLabel = { // Abo-Pläne: Namen/Vorteilstexte kommen aus i18n, Preis/Rabatt aus den admin-editierbaren // Einstellungen (src/content/abo-einstellungen.json). Reihenfolge fest klein → mittel → groß. const aboPlanLabels: Record = { - klein: { name: t.accountDash.aboNameKlein, vorteil: t.accountDash.aboVorteilKlein }, - mittel: { name: t.accountDash.aboNameMittel, vorteil: t.accountDash.aboVorteilMittel }, - gross: { name: t.accountDash.aboNameGross, vorteil: t.accountDash.aboVorteilGross }, + klein: { name: aboStufeName("klein", lang), vorteil: aboStufeVorteil("klein", lang) }, + mittel: { name: aboStufeName("mittel", lang), vorteil: aboStufeVorteil("mittel", lang) }, + gross: { name: aboStufeName("gross", lang), vorteil: aboStufeVorteil("gross", lang) }, }; // "Zuletzt angesehen" zeigt in dieser Vorschau einfach die ersten echten Produkte aus dem Shop — @@ -350,7 +350,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === const cfg = aboStufenKonfig(stufe); const labels = aboPlanLabels[stufe]; return ( -
+

{labels.name}

{formatPrice(cfg.preis, lang)} {t.accountDash.aboPreisSuffix}

@@ -410,7 +410,7 @@ const angekommeneBestellungen = beispielBestellungen.filter((b) => b.status === -