1146 lines
39 KiB
JavaScript
1146 lines
39 KiB
JavaScript
/**
|
||
* Skrift Preisrechner
|
||
* Einfacher Preisrechner mit 2-Step-Flow: Produktauswahl -> Details & Berechnung
|
||
* Nutzt die gleiche Preislogik und Produktdefinitionen wie der Konfigurator
|
||
*/
|
||
|
||
import {
|
||
formatEUR,
|
||
} from "./configurator-pricing.js?ver=0.3.0";
|
||
|
||
import {
|
||
listProducts,
|
||
getAvailableProductsForCustomerType,
|
||
isFollowups as checkIsFollowups,
|
||
supportsMotif as checkSupportsMotif,
|
||
} from "./configurator-state.js?ver=0.3.0";
|
||
|
||
// ========== KONSTANTEN ==========
|
||
|
||
const STEPS = {
|
||
PRODUCT: 0,
|
||
CALCULATOR: 1,
|
||
};
|
||
|
||
// ========== STATE MANAGEMENT ==========
|
||
|
||
function createInitialState() {
|
||
return {
|
||
step: STEPS.PRODUCT,
|
||
customerType: null, // 'business' | 'private'
|
||
product: null,
|
||
// Calculator-Felder
|
||
quantity: 100,
|
||
format: 'a6h', // a4, a6h (a6p), a6q (a6l)
|
||
shippingMode: 'direct', // direct, bulk
|
||
envelope: true, // Bei Sammelversand: Kuvert ja/nein
|
||
envelopeMode: 'recipientData', // recipientData, customText, none (Beschriftungsart)
|
||
followupYearlyVolume: '50-199',
|
||
followupCreateMode: 'manual', // auto, manual
|
||
// Motiv (nur für Postkarten/Einladungen mit A6)
|
||
motifNeed: false,
|
||
motifSource: null, // upload, design
|
||
// Inhalt
|
||
contentCreateMode: 'self', // self, textservice
|
||
};
|
||
}
|
||
|
||
function reducer(state, action) {
|
||
switch (action.type) {
|
||
case 'SET_CUSTOMER_TYPE':
|
||
return {
|
||
...state,
|
||
customerType: action.customerType,
|
||
product: null, // Reset Produkt bei Kundentyp-Wechsel
|
||
};
|
||
|
||
case 'SET_PRODUCT': {
|
||
// Default-Format setzen
|
||
const defaultFormat = action.product.formats?.[0] || 'a4';
|
||
// Default-Menge je nach Kundentyp
|
||
const settings = window.SkriftPreisrechner?.settings || window.SkriftConfigurator?.settings || {};
|
||
const dynamicPricing = settings.dynamic_pricing || {};
|
||
const isB2B = state.customerType === 'business';
|
||
const normalQty = isB2B
|
||
? (dynamicPricing.business_normal_quantity || 200)
|
||
: (dynamicPricing.private_normal_quantity || 50);
|
||
|
||
return {
|
||
...state,
|
||
product: action.product,
|
||
format: defaultFormat,
|
||
quantity: normalQty,
|
||
step: STEPS.CALCULATOR,
|
||
// Reset Motiv bei Produktwechsel
|
||
motifNeed: false,
|
||
motifSource: null,
|
||
};
|
||
}
|
||
|
||
case 'SET_QUANTITY':
|
||
return { ...state, quantity: action.quantity };
|
||
|
||
case 'SET_FORMAT':
|
||
return {
|
||
...state,
|
||
format: action.format,
|
||
// Motiv zurücksetzen wenn nicht mehr A6
|
||
motifNeed: action.format !== 'a4' ? state.motifNeed : false,
|
||
motifSource: action.format !== 'a4' ? state.motifSource : null,
|
||
};
|
||
|
||
case 'SET_SHIPPING_MODE':
|
||
return {
|
||
...state,
|
||
shippingMode: action.shippingMode,
|
||
// Bei Direktversand ist Umschlag immer dabei mit Beschriftung
|
||
envelope: action.shippingMode === 'direct' ? true : state.envelope,
|
||
envelopeMode: action.shippingMode === 'direct' ? 'recipientData' : state.envelopeMode,
|
||
};
|
||
|
||
case 'SET_ENVELOPE':
|
||
return {
|
||
...state,
|
||
envelope: action.envelope,
|
||
// Wenn kein Kuvert, dann auch keine Beschriftung
|
||
envelopeMode: action.envelope ? state.envelopeMode : 'none',
|
||
};
|
||
|
||
case 'SET_ENVELOPE_MODE':
|
||
return { ...state, envelopeMode: action.mode };
|
||
|
||
case 'SET_FOLLOWUP_VOLUME':
|
||
return { ...state, followupYearlyVolume: action.volume };
|
||
|
||
case 'SET_FOLLOWUP_CREATE_MODE':
|
||
return { ...state, followupCreateMode: action.mode };
|
||
|
||
case 'SET_MOTIF_NEED':
|
||
return {
|
||
...state,
|
||
motifNeed: action.motifNeed,
|
||
motifSource: action.motifNeed ? (state.motifSource || 'upload') : null,
|
||
};
|
||
|
||
case 'SET_MOTIF_SOURCE':
|
||
return { ...state, motifSource: action.source };
|
||
|
||
case 'SET_CONTENT_CREATE_MODE':
|
||
return { ...state, contentCreateMode: action.mode };
|
||
|
||
case 'GO_BACK':
|
||
return {
|
||
...state,
|
||
step: STEPS.PRODUCT,
|
||
product: null,
|
||
};
|
||
|
||
default:
|
||
return state;
|
||
}
|
||
}
|
||
|
||
// ========== PRODUKT-DEFINITIONEN ==========
|
||
// Nutzt getAvailableProductsForCustomerType und listProducts aus configurator-state.js
|
||
// Diese enthalten die korrekten Formate, supportsMotif, isFollowUp etc.
|
||
|
||
// ========== PREIS-BERECHNUNG ==========
|
||
|
||
function calculatePrice(state) {
|
||
if (!state.product) return null;
|
||
|
||
const settings = window.SkriftPreisrechner?.settings || window.SkriftConfigurator?.settings || {};
|
||
const prices = settings.prices || {};
|
||
const dynamicPricing = settings.dynamic_pricing || {};
|
||
const isB2B = state.customerType === 'business';
|
||
const taxRate = (prices.tax_rate || 19) / 100;
|
||
|
||
// Follow-ups haben spezielle Preislogik
|
||
if (state.product.isFollowUp === true) {
|
||
return calculateFollowupsPrice(state, prices, taxRate, isB2B);
|
||
}
|
||
|
||
// Normale Produkte
|
||
return calculateStandardPrice(state, prices, dynamicPricing, taxRate, isB2B);
|
||
}
|
||
|
||
function calculateFollowupsPrice(state, prices, taxRate, isB2B) {
|
||
const basePrice = state.product.basePrice;
|
||
|
||
// Multiplikator basierend auf Monatsvolumen
|
||
const multipliers = {
|
||
'5-49': prices.followup_mult_5_49 || 2.0,
|
||
'50-199': prices.followup_mult_50_199 || 1.7,
|
||
'200-499': prices.followup_mult_200_499 || 1.4,
|
||
'500-999': prices.followup_mult_500_999 || 1.2,
|
||
'1000+': prices.followup_mult_1000_plus || 1.0,
|
||
};
|
||
|
||
const multiplier = multipliers[state.followupYearlyVolume] || 1.7;
|
||
|
||
// A4-Aufschlag bei Follow-ups (wie bei anderen Produkten mit supportsMotif)
|
||
let formatSurcharge = 0;
|
||
if (state.format === 'a4' && state.product.supportsMotif) {
|
||
formatSurcharge = parseFloat(prices.a4_upgrade_surcharge) || 0.50;
|
||
}
|
||
|
||
const pricePerPiece = (basePrice * multiplier) + formatSurcharge;
|
||
|
||
// Versandkosten (immer Direktversand bei Follow-ups, immer mit Kuvert + Beschriftung)
|
||
const shippingDomestic = parseFloat(prices.shipping_domestic) || 0.95;
|
||
const shippingService = parseFloat(prices.shipping_service) || 0.95;
|
||
const envelopeBase = parseFloat(prices.envelope_base) || 0.50;
|
||
const envelopeLabeling = parseFloat(prices.envelope_labeling) || 0.50; // Immer dabei
|
||
|
||
const shippingPerPiece = shippingDomestic + shippingService + envelopeBase + envelopeLabeling;
|
||
const totalPerPiece = pricePerPiece + shippingPerPiece;
|
||
|
||
// Brutto/Netto
|
||
const netPerPiece = totalPerPiece;
|
||
const grossPerPiece = totalPerPiece * (1 + taxRate);
|
||
|
||
// Einmalige Kosten
|
||
const setupCosts = [];
|
||
|
||
// API-Anbindung (nur bei automatischer Erstellung)
|
||
if (state.followupCreateMode === 'auto') {
|
||
const apiConnection = parseFloat(prices.api_connection) || 250.00;
|
||
setupCosts.push({ label: 'API-Anbindung', amount: apiConnection });
|
||
}
|
||
|
||
// Textservice (auch bei Follow-ups möglich)
|
||
if (state.contentCreateMode === 'textservice') {
|
||
const textservicePrice = parseFloat(prices.textservice) || 0;
|
||
if (textservicePrice > 0) {
|
||
setupCosts.push({ label: 'Textservice', amount: textservicePrice });
|
||
}
|
||
}
|
||
|
||
return {
|
||
pricePerPiece: isB2B ? netPerPiece : grossPerPiece,
|
||
isGross: !isB2B,
|
||
details: {
|
||
basePrice: basePrice,
|
||
multiplier: multiplier,
|
||
volumeTier: state.followupYearlyVolume,
|
||
formatSurcharge: formatSurcharge,
|
||
productPrice: pricePerPiece,
|
||
shippingDomestic: shippingDomestic,
|
||
shippingService: shippingService,
|
||
envelopeBase: envelopeBase,
|
||
envelopeLabeling: envelopeLabeling,
|
||
shippingTotal: shippingPerPiece,
|
||
netPerPiece: netPerPiece,
|
||
grossPerPiece: grossPerPiece,
|
||
taxRate: taxRate * 100,
|
||
setupCosts: setupCosts,
|
||
},
|
||
};
|
||
}
|
||
|
||
function calculateStandardPrice(state, prices, dynamicPricing, taxRate, isB2B) {
|
||
const basePrice = state.product.basePrice;
|
||
const quantity = state.quantity || 1;
|
||
|
||
// Dynamischer Multiplikator basierend auf Menge
|
||
const normalQty = isB2B
|
||
? (dynamicPricing.business_normal_quantity || 200)
|
||
: (dynamicPricing.private_normal_quantity || 50);
|
||
|
||
let multiplier = 1.0;
|
||
if (quantity < normalQty) {
|
||
// Formel: 2 - sqrt(qty / normalQty)
|
||
multiplier = 2 - Math.sqrt(quantity / normalQty);
|
||
}
|
||
|
||
// A4-Aufschlag für Postkarten/Einladungen
|
||
let formatSurcharge = 0;
|
||
const isPostcardLike = state.product.supportsMotif;
|
||
if (state.format === 'a4' && isPostcardLike) {
|
||
formatSurcharge = parseFloat(prices.a4_upgrade_surcharge) || 0.50;
|
||
}
|
||
|
||
const productPrice = (basePrice * multiplier) + formatSurcharge;
|
||
|
||
// Versandkosten
|
||
let shippingTotal = 0;
|
||
let shippingDetails = {};
|
||
|
||
if (state.shippingMode === 'direct') {
|
||
// Direktversand: Porto + Service + Kuvert + Beschriftung (immer dabei)
|
||
const shippingDomestic = parseFloat(prices.shipping_domestic) || 0.95;
|
||
const shippingService = parseFloat(prices.shipping_service) || 0.95;
|
||
const envelopeBase = parseFloat(prices.envelope_base) || 0.50;
|
||
const envelopeLabeling = parseFloat(prices.envelope_labeling) || 0.50; // Immer bei Direktversand
|
||
|
||
shippingTotal = shippingDomestic + shippingService + envelopeBase + envelopeLabeling;
|
||
shippingDetails = {
|
||
type: 'direct',
|
||
porto: shippingDomestic,
|
||
service: shippingService,
|
||
envelope: envelopeBase,
|
||
labeling: envelopeLabeling,
|
||
};
|
||
} else {
|
||
// Sammelversand: Nur Kuvert + optionale Beschriftung (KEINE anteilige Versandpauschale pro Stück!)
|
||
let envelopeCost = 0;
|
||
let labelingCost = 0;
|
||
if (state.envelope) {
|
||
envelopeCost = parseFloat(prices.envelope_base) || 0.50;
|
||
// Beschriftung nur wenn envelopeMode nicht 'none' ist
|
||
if (state.envelopeMode && state.envelopeMode !== 'none') {
|
||
labelingCost = parseFloat(prices.envelope_labeling) || 0.50;
|
||
}
|
||
}
|
||
shippingTotal = envelopeCost + labelingCost;
|
||
shippingDetails = {
|
||
type: 'bulk',
|
||
flatRate: parseFloat(prices.shipping_bulk) || 4.95, // Wird separat als Pauschale angezeigt
|
||
envelopeCost: envelopeCost,
|
||
labelingCost: labelingCost,
|
||
envelopeMode: state.envelopeMode,
|
||
};
|
||
}
|
||
|
||
const totalPerPiece = productPrice + shippingTotal;
|
||
|
||
// Brutto/Netto
|
||
const netPerPiece = totalPerPiece;
|
||
const grossPerPiece = totalPerPiece * (1 + taxRate);
|
||
|
||
// Einmalige Kosten
|
||
const setupCosts = [];
|
||
|
||
// Motiv-Upload
|
||
if (state.motifNeed && state.motifSource === 'upload') {
|
||
const motifUploadPrice = parseFloat(prices.motif_upload) || 0.30;
|
||
if (motifUploadPrice > 0) {
|
||
setupCosts.push({ label: 'Motiv-Upload', amount: motifUploadPrice });
|
||
}
|
||
}
|
||
|
||
// Motiv-Design
|
||
if (state.motifNeed && state.motifSource === 'design') {
|
||
const motifDesignPrice = parseFloat(prices.motif_design) || 0;
|
||
if (motifDesignPrice > 0) {
|
||
setupCosts.push({ label: 'Motiv-Design', amount: motifDesignPrice });
|
||
}
|
||
}
|
||
|
||
// Textservice
|
||
if (state.contentCreateMode === 'textservice') {
|
||
const textservicePrice = parseFloat(prices.textservice) || 0;
|
||
if (textservicePrice > 0) {
|
||
setupCosts.push({ label: 'Textservice', amount: textservicePrice });
|
||
}
|
||
}
|
||
|
||
// Bei Sammelversand: Versandpauschale zum Gesamtpreis hinzufügen (einmalig, nicht pro Stück)
|
||
const bulkShippingFlat = (shippingDetails.type === 'bulk') ? (shippingDetails.flatRate || 0) : 0;
|
||
const totalNet = (netPerPiece * quantity) + bulkShippingFlat;
|
||
const totalGross = (grossPerPiece * quantity) + (bulkShippingFlat * (1 + taxRate));
|
||
|
||
return {
|
||
pricePerPiece: isB2B ? netPerPiece : grossPerPiece,
|
||
isGross: !isB2B,
|
||
totalPrice: isB2B ? totalNet : totalGross,
|
||
details: {
|
||
basePrice: basePrice,
|
||
multiplier: multiplier,
|
||
normalQuantity: normalQty,
|
||
formatSurcharge: formatSurcharge,
|
||
productPrice: productPrice,
|
||
shipping: shippingDetails,
|
||
shippingTotal: shippingTotal,
|
||
netPerPiece: netPerPiece,
|
||
grossPerPiece: grossPerPiece,
|
||
taxRate: taxRate * 100,
|
||
quantity: quantity,
|
||
setupCosts: setupCosts,
|
||
},
|
||
};
|
||
}
|
||
|
||
// ========== DOM HELPER ==========
|
||
|
||
function h(tag, attrs = {}, children = []) {
|
||
const el = document.createElement(tag);
|
||
for (const [key, val] of Object.entries(attrs)) {
|
||
if (key === 'text') {
|
||
el.textContent = val;
|
||
} else if (key === 'html') {
|
||
el.innerHTML = val;
|
||
} else if (key.startsWith('on') && typeof val === 'function') {
|
||
el.addEventListener(key.slice(2).toLowerCase(), val);
|
||
} else if (key === 'class') {
|
||
el.className = val;
|
||
} else if (key === 'style' && typeof val === 'object') {
|
||
Object.assign(el.style, val);
|
||
} else if (val !== null && val !== undefined) {
|
||
el.setAttribute(key, val);
|
||
}
|
||
}
|
||
for (const child of children) {
|
||
if (typeof child === 'string') {
|
||
el.appendChild(document.createTextNode(child));
|
||
} else if (child instanceof Node) {
|
||
el.appendChild(child);
|
||
}
|
||
}
|
||
return el;
|
||
}
|
||
|
||
// ========== UI RENDERING ==========
|
||
|
||
function renderCard(title, subtitle, bodyContent) {
|
||
return h('div', { class: 'sk-card' }, [
|
||
h('div', { class: 'sk-card-head' }, [
|
||
h('h3', { class: 'sk-card-title', text: title }),
|
||
subtitle ? h('p', { class: 'sk-card-subtitle', text: subtitle }) : null,
|
||
].filter(Boolean)),
|
||
h('div', { class: 'sk-card-body' }, bodyContent),
|
||
]);
|
||
}
|
||
|
||
function renderProductStep(state, dispatch) {
|
||
const blocks = [];
|
||
|
||
// Kundentyp Auswahl (identisch zum Konfigurator)
|
||
const customerOptions = [
|
||
{
|
||
value: 'business',
|
||
label: 'Geschäftskunde',
|
||
desc: 'Für Unternehmen, Selbstständige und gewerbliche Kunden',
|
||
icon: '🏢',
|
||
},
|
||
{
|
||
value: 'private',
|
||
label: 'Privatkunde',
|
||
desc: 'Für private Anlässe und persönliche Korrespondenz',
|
||
icon: '🏠',
|
||
},
|
||
];
|
||
|
||
const customerBody = customerOptions.map(opt =>
|
||
h('div', {
|
||
class: `sk-option ${state.customerType === opt.value ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_CUSTOMER_TYPE', customerType: opt.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'customerType',
|
||
checked: state.customerType === opt.value ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: `${opt.icon} ${opt.label}` }),
|
||
h('div', { class: 'sk-option-desc', text: opt.desc }),
|
||
]),
|
||
])
|
||
);
|
||
|
||
blocks.push(renderCard(
|
||
'Als was möchten Sie einkaufen?',
|
||
'Wählen Sie Ihren Kundentyp aus',
|
||
[h('div', { class: 'sk-options' }, customerBody)]
|
||
));
|
||
|
||
// Produktauswahl (nur wenn Kundentyp gewählt) - nutzt getAvailableProductsForCustomerType aus Konfigurator
|
||
if (state.customerType) {
|
||
const baseProducts = getAvailableProductsForCustomerType(state.customerType);
|
||
const settings = window.SkriftPreisrechner?.settings || window.SkriftConfigurator?.settings || {};
|
||
const productSettings = settings.products || {};
|
||
const prices = settings.prices || {};
|
||
const taxRate = (prices.tax_rate || 19) / 100;
|
||
const isB2B = state.customerType === 'business';
|
||
|
||
// Produkte mit korrekten Labels und Preisen aus Settings anreichern
|
||
const products = baseProducts.map(p => {
|
||
const pSettings = productSettings[p.key] || {};
|
||
return {
|
||
...p,
|
||
label: pSettings.label || p.label,
|
||
description: pSettings.description || p.description,
|
||
basePrice: parseFloat(pSettings.base_price) || p.basePrice || 2.50,
|
||
};
|
||
});
|
||
|
||
const grid = h('div', { class: 'sk-selection-grid' });
|
||
|
||
for (const p of products) {
|
||
const displayPrice = isB2B ? p.basePrice : p.basePrice * (1 + taxRate);
|
||
const priceText = `ab ${formatEUR(displayPrice)}`;
|
||
|
||
const card = h('div', {
|
||
class: `sk-selection-card ${state.product?.key === p.key ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_PRODUCT', product: p }),
|
||
}, [
|
||
h('div', { class: 'sk-selection-card-image' }, [
|
||
h('div', { text: '📄', style: 'font-size: 48px' }),
|
||
]),
|
||
h('div', { class: 'sk-selection-card-content' }, [
|
||
h('div', { class: 'sk-selection-card-title', text: p.label }),
|
||
h('div', { class: 'sk-selection-card-price', text: priceText }),
|
||
h('div', { class: 'sk-selection-card-desc', text: p.description }),
|
||
]),
|
||
]);
|
||
grid.appendChild(card);
|
||
}
|
||
|
||
blocks.push(renderCard(
|
||
'Welches Produkt benötigen Sie?',
|
||
'Wählen Sie das passende Produkt für Ihren Bedarf',
|
||
[grid]
|
||
));
|
||
}
|
||
|
||
return h('div', { class: 'sk-stack' }, blocks);
|
||
}
|
||
|
||
function renderCalculatorStep(state, dispatch) {
|
||
const blocks = [];
|
||
const settings = window.SkriftPreisrechner?.settings || window.SkriftConfigurator?.settings || {};
|
||
const prices = settings.prices || {};
|
||
const dynamicPricing = settings.dynamic_pricing || {};
|
||
const isB2B = state.customerType === 'business';
|
||
const taxRate = (prices.tax_rate || 19) / 100;
|
||
// Hilfsfunktion: Bei B2C Brutto-Preis anzeigen (inkl. 19% MwSt.)
|
||
const displayPrice = (nettoPrice) => isB2B ? nettoPrice : nettoPrice * (1 + taxRate);
|
||
// Nutze Helper-Funktionen aus Konfigurator für korrekte Prüfung
|
||
const isFollowups = state.product?.isFollowUp === true;
|
||
const isPostcardLike = state.product?.supportsMotif === true;
|
||
|
||
// Produkt-Info Header
|
||
blocks.push(h('div', { class: 'sk-topbar' }, [
|
||
h('div', { class: 'sk-product-info' }, [
|
||
h('div', { class: 'sk-product-icon', text: state.product?.icon || '📄' }),
|
||
h('div', {}, [
|
||
h('div', { class: 'sk-product-label', text: state.product?.label }),
|
||
h('div', { class: 'sk-text-small sk-text-muted', text: isB2B ? 'Geschäftskunde' : 'Privatkunde' }),
|
||
]),
|
||
]),
|
||
h('button', {
|
||
class: 'sk-btn sk-btn-secondary',
|
||
onclick: () => dispatch({ type: 'GO_BACK' }),
|
||
text: '← Produkt ändern',
|
||
}),
|
||
]));
|
||
|
||
// ========== MENGE / FOLLOW-UP VOLUMEN ==========
|
||
if (isFollowups) {
|
||
// Follow-ups: Monatsvolumen (identisch zum Konfigurator)
|
||
const volumes = [
|
||
{ value: '5-49', label: '5-49 Follow-ups pro Monat' },
|
||
{ value: '50-199', label: '50-199 Follow-ups pro Monat' },
|
||
{ value: '200-499', label: '200-499 Follow-ups pro Monat' },
|
||
{ value: '500-999', label: '500-999 Follow-ups pro Monat' },
|
||
{ value: '1000+', label: '1000+ Follow-ups pro Monat' },
|
||
];
|
||
|
||
const volBody = volumes.map(vol =>
|
||
h('div', {
|
||
class: `sk-option ${state.followupYearlyVolume === vol.value ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_FOLLOWUP_VOLUME', volume: vol.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'volume',
|
||
checked: state.followupYearlyVolume === vol.value ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: vol.label }),
|
||
]),
|
||
])
|
||
);
|
||
|
||
blocks.push(renderCard(
|
||
'Um wie viele Follow-ups geht es ca. pro Monat?',
|
||
'Dies hilft uns bei der Preiskalkulation',
|
||
[h('div', { class: 'sk-options' }, volBody)]
|
||
));
|
||
} else {
|
||
// Normale Produkte: Menge (identisch zum Konfigurator)
|
||
const minQuantity = isB2B
|
||
? (dynamicPricing.business_min_quantity || 50)
|
||
: (dynamicPricing.private_min_quantity || 10);
|
||
const normalQuantity = isB2B
|
||
? (dynamicPricing.business_normal_quantity || 200)
|
||
: (dynamicPricing.private_normal_quantity || 50);
|
||
|
||
const quantityInput = h('input', {
|
||
class: 'sk-input',
|
||
type: 'number',
|
||
min: String(minQuantity),
|
||
value: String(state.quantity),
|
||
onchange: (e) => {
|
||
let val = parseInt(e.target.value) || minQuantity;
|
||
if (val < minQuantity) val = minQuantity;
|
||
dispatch({ type: 'SET_QUANTITY', quantity: val });
|
||
},
|
||
});
|
||
|
||
blocks.push(renderCard(
|
||
'Wie viele Schriftstücke benötigen Sie?',
|
||
`Mindestmenge: ${minQuantity} Stück • Unser bester Preis gilt ab ${normalQuantity} Stück`,
|
||
[quantityInput]
|
||
));
|
||
}
|
||
|
||
// ========== FORMAT (nur A4 vs A6 fragen - Hoch/Querformat macht keinen Preisunterschied) ==========
|
||
// Prüfen ob sowohl A4 als auch A6 verfügbar sind
|
||
const hasA4 = state.product?.formats?.includes('a4');
|
||
const hasA6 = state.product?.formats?.some(f => f === 'a6p' || f === 'a6l');
|
||
|
||
if (hasA4 && hasA6) {
|
||
const a4Surcharge = prices.a4_upgrade_surcharge || 0;
|
||
const hasA4Surcharge = isPostcardLike;
|
||
|
||
const formatOptions = [
|
||
{ value: 'a6p', label: 'A6', desc: 'Kompaktes Format, ideal für Postkarten und kurze Nachrichten', price: '' },
|
||
{ value: 'a4', label: 'A4', desc: 'Standardformat für ausführliche Briefe', price: hasA4Surcharge ? `+ ${formatEUR(displayPrice(a4Surcharge))}` : '' },
|
||
];
|
||
|
||
const formatBody = formatOptions.map(opt => {
|
||
// Für A6: prüfen ob a6p oder a6l aktuell gewählt ist
|
||
const isSelected = opt.value === 'a4' ? state.format === 'a4' : (state.format === 'a6p' || state.format === 'a6l');
|
||
return h('div', {
|
||
class: `sk-option ${isSelected ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_FORMAT', format: opt.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'format',
|
||
checked: isSelected ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: opt.label }),
|
||
h('div', { class: 'sk-option-desc', text: opt.desc }),
|
||
opt.price ? h('div', { class: 'sk-option-price', text: opt.price }) : null,
|
||
].filter(Boolean)),
|
||
]);
|
||
});
|
||
|
||
blocks.push(renderCard(
|
||
'Welches Format soll das Schriftstück haben?',
|
||
null,
|
||
[h('div', { class: 'sk-options' }, formatBody)]
|
||
));
|
||
}
|
||
|
||
// ========== FOLLOW-UP ERSTELLUNGSMODUS ==========
|
||
if (isFollowups) {
|
||
const apiPrice = prices.api_connection || 250;
|
||
|
||
const createModes = [
|
||
{
|
||
value: 'auto',
|
||
label: 'Automatisch aus System',
|
||
desc: 'Wir verbinden uns mit Ihrem CRM/Shop-System',
|
||
price: `Einmalig ${formatEUR(displayPrice(apiPrice))}`,
|
||
},
|
||
{
|
||
value: 'manual',
|
||
label: 'Manuell',
|
||
desc: 'Sie senden uns die Empfängerliste im gewählten Rhythmus',
|
||
price: '',
|
||
},
|
||
];
|
||
|
||
const createBody = createModes.map(mode =>
|
||
h('div', {
|
||
class: `sk-option ${state.followupCreateMode === mode.value ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_FOLLOWUP_CREATE_MODE', mode: mode.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'createMode',
|
||
checked: state.followupCreateMode === mode.value ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: mode.label }),
|
||
h('div', { class: 'sk-option-desc', text: mode.desc }),
|
||
mode.price ? h('div', { class: 'sk-option-price', text: mode.price }) : null,
|
||
].filter(Boolean)),
|
||
])
|
||
);
|
||
|
||
blocks.push(renderCard(
|
||
'Wie sollen die Follow-ups erstellt werden?',
|
||
null,
|
||
[h('div', { class: 'sk-options' }, createBody)]
|
||
));
|
||
}
|
||
|
||
// ========== VERSANDART (nicht bei Follow-ups) ==========
|
||
if (!isFollowups) {
|
||
const shippingBulk = prices.shipping_bulk || 4.95;
|
||
const shippingDomestic = prices.shipping_domestic || 0.95;
|
||
const shippingService = prices.shipping_service || 0.95;
|
||
const envelopeBase = prices.envelope_base || 0.50;
|
||
const envelopeLabeling = prices.envelope_labeling || 0.50;
|
||
|
||
const domesticPrice = shippingDomestic + shippingService + envelopeBase + envelopeLabeling;
|
||
const internationalPrice = (prices.shipping_international || 1.25) + shippingService + envelopeBase + envelopeLabeling;
|
||
|
||
const shippingOptions = [
|
||
{
|
||
value: 'direct',
|
||
label: 'Einzeln an die Empfänger',
|
||
desc: 'Wir versenden direkt an Ihre Empfänger – inklusive Kuvertierung und Beschriftung',
|
||
price: `Inland: ${formatEUR(displayPrice(domesticPrice))} / Ausland: ${formatEUR(displayPrice(internationalPrice))} pro Stück`,
|
||
},
|
||
{
|
||
value: 'bulk',
|
||
label: 'Sammelversand an Sie',
|
||
desc: 'Sie erhalten alle Schriftstücke zur eigenen Verteilung',
|
||
price: `Einmalig ${formatEUR(displayPrice(shippingBulk))} Versandkosten`,
|
||
},
|
||
];
|
||
|
||
const shippingBody = shippingOptions.map(opt =>
|
||
h('div', {
|
||
class: `sk-option ${state.shippingMode === opt.value ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_SHIPPING_MODE', shippingMode: opt.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'shippingMode',
|
||
checked: state.shippingMode === opt.value ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: opt.label }),
|
||
h('div', { class: 'sk-option-desc', text: opt.desc }),
|
||
h('div', { class: 'sk-option-price', text: opt.price }),
|
||
]),
|
||
])
|
||
);
|
||
|
||
blocks.push(renderCard(
|
||
'Wie sollen die Schriftstücke versendet werden?',
|
||
'Wählen Sie die passende Versandart',
|
||
[h('div', { class: 'sk-options' }, shippingBody)]
|
||
));
|
||
|
||
// ========== KUVERT BEI SAMMELVERSAND ==========
|
||
if (state.shippingMode === 'bulk') {
|
||
const envOptions = [
|
||
{ value: true, label: 'Ja, ich wünsche ein Kuvert', price: `+ ${formatEUR(displayPrice(envelopeBase))}` },
|
||
{ value: false, label: 'Nein, ich benötige kein Kuvert', price: '' },
|
||
];
|
||
|
||
const envBody = envOptions.map(opt =>
|
||
h('div', {
|
||
class: `sk-option ${state.envelope === opt.value ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_ENVELOPE', envelope: opt.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'envelope',
|
||
checked: state.envelope === opt.value ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: opt.label }),
|
||
opt.price ? h('div', { class: 'sk-option-price', text: opt.price }) : null,
|
||
].filter(Boolean)),
|
||
])
|
||
);
|
||
|
||
blocks.push(renderCard(
|
||
'Benötigen Sie ein Kuvert?',
|
||
null,
|
||
[h('div', { class: 'sk-options' }, envBody)]
|
||
));
|
||
|
||
// Beschriftungsart nur wenn Kuvert gewählt
|
||
if (state.envelope) {
|
||
const modeOptions = [
|
||
{
|
||
value: 'recipientData',
|
||
label: 'Empfängerdaten',
|
||
desc: 'Klassische Adressierung mit Name und Anschrift',
|
||
price: `+ ${formatEUR(displayPrice(envelopeLabeling))}`,
|
||
},
|
||
{
|
||
value: 'customText',
|
||
label: 'Individueller Text',
|
||
desc: 'Freier Text mit Platzhaltern',
|
||
price: `+ ${formatEUR(displayPrice(envelopeLabeling))}`,
|
||
},
|
||
{
|
||
value: 'none',
|
||
label: 'Keine Beschriftung',
|
||
desc: 'Umschlag bleibt unbeschriftet',
|
||
price: '',
|
||
},
|
||
];
|
||
|
||
const modeBody = modeOptions.map(opt =>
|
||
h('div', {
|
||
class: `sk-option ${state.envelopeMode === opt.value ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_ENVELOPE_MODE', mode: opt.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'envelopeMode',
|
||
checked: state.envelopeMode === opt.value ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: opt.label }),
|
||
h('div', { class: 'sk-option-desc', text: opt.desc }),
|
||
opt.price ? h('div', { class: 'sk-option-price', text: opt.price }) : null,
|
||
].filter(Boolean)),
|
||
])
|
||
);
|
||
|
||
blocks.push(renderCard(
|
||
'Wie soll das Kuvert beschriftet werden?',
|
||
null,
|
||
[h('div', { class: 'sk-options' }, modeBody)]
|
||
));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Follow-ups: Kein Umschlag-Optionen Block - immer Direktversand mit Kuvert
|
||
|
||
// ========== MOTIV (nur bei Postkarten/Einladungen mit A6) ==========
|
||
if (isPostcardLike && state.format !== 'a4') {
|
||
const motifOptions = [
|
||
{ value: true, label: 'Ja, ich möchte ein Motiv' },
|
||
{ value: false, label: 'Nein, kein Motiv' },
|
||
];
|
||
|
||
const motifBody = motifOptions.map(opt =>
|
||
h('div', {
|
||
class: `sk-option ${state.motifNeed === opt.value ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_MOTIF_NEED', motifNeed: opt.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'motif',
|
||
checked: state.motifNeed === opt.value ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: opt.label }),
|
||
]),
|
||
])
|
||
);
|
||
|
||
blocks.push(renderCard(
|
||
'Soll ein Motiv auf der Vorderseite abgebildet werden?',
|
||
null,
|
||
[h('div', { class: 'sk-options' }, motifBody)]
|
||
));
|
||
|
||
// Motiv-Quelle
|
||
if (state.motifNeed) {
|
||
const motifUploadPrice = prices.motif_upload || 0.30;
|
||
const motifDesignPrice = prices.motif_design || 0;
|
||
|
||
const sourceOptions = [
|
||
{
|
||
value: 'upload',
|
||
label: 'Eigenes Motiv hochladen',
|
||
desc: 'Sie laden Ihr eigenes Bild hoch',
|
||
price: motifUploadPrice > 0 ? `Einmalig ${formatEUR(displayPrice(motifUploadPrice))}` : '',
|
||
},
|
||
{
|
||
value: 'design',
|
||
label: 'Motiv erstellen lassen',
|
||
desc: 'Wir gestalten ein Motiv nach Ihren Wünschen',
|
||
price: motifDesignPrice > 0 ? `Einmalig ${formatEUR(displayPrice(motifDesignPrice))}` : '',
|
||
},
|
||
];
|
||
|
||
const sourceBody = sourceOptions.map(opt =>
|
||
h('div', {
|
||
class: `sk-option ${state.motifSource === opt.value ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_MOTIF_SOURCE', source: opt.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'motifSource',
|
||
checked: state.motifSource === opt.value ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: opt.label }),
|
||
h('div', { class: 'sk-option-desc', text: opt.desc }),
|
||
opt.price ? h('div', { class: 'sk-option-price', text: opt.price }) : null,
|
||
].filter(Boolean)),
|
||
])
|
||
);
|
||
|
||
blocks.push(renderCard(
|
||
'Woher kommt das Motiv?',
|
||
null,
|
||
[h('div', { class: 'sk-options' }, sourceBody)]
|
||
));
|
||
}
|
||
}
|
||
|
||
// ========== TEXTERSTELLUNG (für alle Produkte inkl. Follow-ups) ==========
|
||
{
|
||
const textservicePrice = prices.textservice || 0;
|
||
|
||
const contentOptions = [
|
||
{
|
||
value: 'self',
|
||
label: 'Ich erstelle den Text selbst',
|
||
desc: 'Sie geben den vollständigen Text vor',
|
||
price: '',
|
||
},
|
||
{
|
||
value: 'textservice',
|
||
label: 'Textservice beauftragen',
|
||
desc: 'Wir erstellen den Text professionell für Sie',
|
||
price: textservicePrice > 0 ? `Einmalig ${formatEUR(displayPrice(textservicePrice))}` : '',
|
||
},
|
||
];
|
||
|
||
const contentBody = contentOptions.map(opt =>
|
||
h('div', {
|
||
class: `sk-option ${state.contentCreateMode === opt.value ? 'is-selected' : ''}`,
|
||
onclick: () => dispatch({ type: 'SET_CONTENT_CREATE_MODE', mode: opt.value }),
|
||
}, [
|
||
h('input', {
|
||
type: 'radio',
|
||
name: 'contentMode',
|
||
checked: state.contentCreateMode === opt.value ? 'checked' : null,
|
||
}),
|
||
h('div', { class: 'sk-option-content' }, [
|
||
h('div', { class: 'sk-option-label', text: opt.label }),
|
||
h('div', { class: 'sk-option-desc', text: opt.desc }),
|
||
opt.price ? h('div', { class: 'sk-option-price', text: opt.price }) : null,
|
||
].filter(Boolean)),
|
||
])
|
||
);
|
||
|
||
blocks.push(renderCard(
|
||
'Möchten Sie den Inhalt selbst erstellen?',
|
||
null,
|
||
[h('div', { class: 'sk-options' }, contentBody)]
|
||
));
|
||
}
|
||
|
||
return h('div', { class: 'sk-stack' }, blocks);
|
||
}
|
||
|
||
function renderPriceSidebar(state) {
|
||
const priceData = calculatePrice(state);
|
||
|
||
if (!priceData) {
|
||
return h('div', { class: 'sk-preview-card' }, [
|
||
h('div', { class: 'sk-preview-title', text: 'Preisberechnung' }),
|
||
h('div', { class: 'sk-preview-sub', text: 'Wählen Sie ein Produkt aus' }),
|
||
]);
|
||
}
|
||
|
||
const isFollowups = state.product?.isFollowUp === true;
|
||
const taxRate = Math.round(priceData.details.taxRate) || 19;
|
||
const taxMultiplier = priceData.isGross ? (1 + taxRate / 100) : 1; // Brutto-Multiplikator für B2C
|
||
const taxNote = priceData.isGross ? `inkl. ${taxRate}% MwSt.` : `zzgl. ${taxRate}% MwSt.`;
|
||
|
||
// Preiszeilen sammeln - bei B2C (isGross) Brutto-Werte anzeigen
|
||
const priceRows = [];
|
||
|
||
// Produktpreis (dynamisch) - OHNE Formataufschlag, da dieser separat angezeigt wird
|
||
const baseProductPrice = priceData.details.productPrice - (priceData.details.formatSurcharge || 0);
|
||
priceRows.push({ label: 'Schriftstück', value: baseProductPrice * taxMultiplier });
|
||
|
||
// Format-Aufschlag (falls vorhanden)
|
||
if (priceData.details.formatSurcharge > 0) {
|
||
priceRows.push({ label: 'A4-Aufschlag', value: priceData.details.formatSurcharge * taxMultiplier });
|
||
}
|
||
|
||
// Direktversand: Zusammengefasst als ein Posten
|
||
if (priceData.details.shipping?.type === 'direct') {
|
||
const totalDirectShipping = priceData.details.shipping.porto +
|
||
priceData.details.shipping.service +
|
||
priceData.details.shipping.envelope +
|
||
(priceData.details.shipping.labeling || 0);
|
||
priceRows.push({ label: 'Einzeln an Empfänger', value: totalDirectShipping * taxMultiplier });
|
||
} else if (priceData.details.shipping?.type === 'bulk') {
|
||
// Sammelversand: Kuvert + Beschriftung (falls gewählt)
|
||
if (priceData.details.shipping.envelopeCost > 0) {
|
||
priceRows.push({ label: 'Kuvert', value: priceData.details.shipping.envelopeCost * taxMultiplier });
|
||
}
|
||
if (priceData.details.shipping.labelingCost > 0) {
|
||
// Label je nach Modus
|
||
const labelText = priceData.details.shipping.envelopeMode === 'customText'
|
||
? 'Individueller Text'
|
||
: 'Empfängerdaten';
|
||
priceRows.push({ label: labelText, value: priceData.details.shipping.labelingCost * taxMultiplier });
|
||
}
|
||
// KEINE anteilige Versandpauschale pro Stück - wird separat als Pauschale angezeigt
|
||
}
|
||
|
||
// Follow-ups: Versand zusammengefasst
|
||
if (isFollowups) {
|
||
const totalDirectShipping = priceData.details.shippingDomestic +
|
||
priceData.details.shippingService +
|
||
priceData.details.envelopeBase +
|
||
(priceData.details.envelopeLabeling || 0);
|
||
priceRows.push({ label: 'Einzeln an Empfänger', value: totalDirectShipping * taxMultiplier });
|
||
}
|
||
|
||
// Preistabelle rendern
|
||
const priceTable = h('table', { class: 'sk-calc-price-table' }, [
|
||
h('tbody', {}, priceRows.map(row =>
|
||
h('tr', {}, [
|
||
h('td', { text: row.label }),
|
||
h('td', { text: formatEUR(row.value) }),
|
||
])
|
||
)),
|
||
]);
|
||
|
||
// Einmalige Kosten
|
||
let setupSection = null;
|
||
if (priceData.details.setupCosts?.length > 0) {
|
||
const setupTable = h('table', { class: 'sk-calc-price-table' }, [
|
||
h('tbody', {}, priceData.details.setupCosts.map(cost =>
|
||
h('tr', {}, [
|
||
h('td', { text: cost.label }),
|
||
h('td', { text: formatEUR(cost.amount * taxMultiplier) }),
|
||
])
|
||
)),
|
||
]);
|
||
|
||
setupSection = h('div', { class: 'sk-calc-setup-section' }, [
|
||
h('div', { class: 'sk-calc-section-label', text: 'Einmalige Kosten' }),
|
||
setupTable,
|
||
]);
|
||
}
|
||
|
||
// Gesamtpreis (nur bei Standardprodukten) - netto für B2B, brutto für B2C
|
||
let totalSection = null;
|
||
if (!isFollowups && priceData.totalPrice) {
|
||
const totalLabel = priceData.isGross
|
||
? `Gesamt inkl. MwSt. (${state.quantity} St.)`
|
||
: `Gesamt netto (${state.quantity} St.)`;
|
||
totalSection = h('div', { class: 'sk-calc-total-section' }, [
|
||
h('div', { class: 'sk-calc-total-row' }, [
|
||
h('span', { text: totalLabel }),
|
||
h('span', { class: 'sk-calc-total-value', text: formatEUR(priceData.totalPrice) }),
|
||
]),
|
||
]);
|
||
}
|
||
|
||
// Sammelversand Hinweis
|
||
let bulkNote = null;
|
||
if (priceData.details.shipping?.type === 'bulk') {
|
||
bulkNote = h('div', { class: 'sk-calc-note' }, [
|
||
h('span', { text: `zzgl. ${formatEUR(priceData.details.shipping.flatRate)} Versandpauschale` }),
|
||
]);
|
||
}
|
||
|
||
// Mengenhinweis
|
||
let quantityNote = null;
|
||
if (!isFollowups && priceData.details.multiplier > 1.01) {
|
||
quantityNote = h('div', { class: 'sk-calc-note sk-calc-note--info' }, [
|
||
h('span', { text: `Tipp: Ab ${priceData.details.normalQuantity} Stück erhalten Sie unseren besten Preis.` }),
|
||
]);
|
||
}
|
||
|
||
// Staffelpreis-Hinweis bei Follow-ups
|
||
let tierNote = null;
|
||
if (isFollowups) {
|
||
tierNote = h('div', { class: 'sk-calc-note' }, [
|
||
h('span', { text: `Staffelpreis für ${priceData.details.volumeTier} Follow-ups/Monat` }),
|
||
]);
|
||
}
|
||
|
||
return h('div', { class: 'sk-calc-sidebar-card' }, [
|
||
// Hauptpreis
|
||
h('div', { class: 'sk-calc-main-price' }, [
|
||
h('div', { class: 'sk-calc-main-price__value', text: formatEUR(priceData.pricePerPiece) }),
|
||
h('div', { class: 'sk-calc-main-price__label', text: 'pro Stück' }),
|
||
h('div', { class: 'sk-calc-main-price__note', text: taxNote }),
|
||
]),
|
||
|
||
// Preisaufschlüsselung
|
||
h('div', { class: 'sk-calc-details' }, [
|
||
priceTable,
|
||
bulkNote,
|
||
quantityNote,
|
||
tierNote,
|
||
].filter(Boolean)),
|
||
|
||
// Einmalige Kosten
|
||
setupSection,
|
||
|
||
// Gesamtpreis
|
||
totalSection,
|
||
|
||
// CTA Button
|
||
h('a', {
|
||
class: 'sk-calc-cta-btn',
|
||
href: `/konfigurator/?${state.product.key}`,
|
||
}, [
|
||
h('span', { text: 'Jetzt konfigurieren' }),
|
||
h('span', { class: 'sk-calc-cta-arrow', text: '→' }),
|
||
]),
|
||
].filter(Boolean));
|
||
}
|
||
|
||
// ========== MAIN RENDER ==========
|
||
|
||
function render(state, dispatch) {
|
||
const container = document.querySelector('[data-skrift-preisrechner]');
|
||
if (!container) return;
|
||
|
||
// Bei Produktauswahl: Volle Breite (1200px wie Calculator)
|
||
if (state.step === STEPS.PRODUCT) {
|
||
const wrapper = h('div', {});
|
||
wrapper.appendChild(renderProductStep(state, dispatch));
|
||
container.innerHTML = '';
|
||
container.appendChild(wrapper);
|
||
return;
|
||
}
|
||
|
||
// Calculator-Step: 2-Spalten-Layout mit Sidebar
|
||
const layout = h('div', { class: 'sk-configurator__layout' });
|
||
const main = h('div', { class: 'sk-main' });
|
||
const side = h('aside', { class: 'sk-side' });
|
||
|
||
// Hauptinhalt
|
||
const calculatorContent = renderCalculatorStep(state, dispatch);
|
||
main.appendChild(calculatorContent);
|
||
|
||
// Mobile Sidebar (wird am Ende des Hauptinhalts angezeigt)
|
||
const mobileSidebar = h('div', { class: 'sk-calc-mobile-sidebar' });
|
||
mobileSidebar.appendChild(renderPriceSidebar(state));
|
||
main.appendChild(mobileSidebar);
|
||
|
||
// Desktop Sidebar
|
||
side.appendChild(renderPriceSidebar(state));
|
||
|
||
layout.appendChild(main);
|
||
layout.appendChild(side);
|
||
|
||
container.innerHTML = '';
|
||
container.appendChild(layout);
|
||
}
|
||
|
||
// ========== INIT ==========
|
||
|
||
export function initPriceCalculator() {
|
||
const container = document.querySelector('[data-skrift-preisrechner]');
|
||
if (!container) return;
|
||
|
||
let state = createInitialState();
|
||
|
||
function dispatch(action) {
|
||
state = reducer(state, action);
|
||
render(state, dispatch);
|
||
}
|
||
|
||
// Initial render
|
||
render(state, dispatch);
|
||
}
|
||
|
||
// Auto-Init wenn DOM ready
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initPriceCalculator);
|
||
} else {
|
||
initPriceCalculator();
|
||
}
|