Upload Neu

This commit is contained in:
s4luorth
2026-05-04 09:40:27 +02:00
parent a9c86c6dad
commit 290969f848
6 changed files with 499 additions and 291 deletions

View File

@@ -23,9 +23,6 @@ async function finalizeOrder(req, res, next) {
} }
console.log(`[Order] Finalizing order: ${orderNumber} from session: ${sessionId}`); console.log(`[Order] Finalizing order: ${orderNumber} from session: ${sessionId}`);
if (envelopes && envelopes.length > 0) {
console.log(`[Order] Envelope data provided: ${envelopes.length} envelopes`);
}
// Check if session cache exists // Check if session cache exists
const sessionDir = path.join(config.paths.previews, sessionId); const sessionDir = path.join(config.paths.previews, sessionId);
@@ -97,6 +94,7 @@ async function finalizeOrder(req, res, next) {
} }
// Create order metadata // Create order metadata
const meta = req.body.metadata || {};
const orderMetadata = { const orderMetadata = {
orderNumber, orderNumber,
sessionId, sessionId,
@@ -105,7 +103,10 @@ async function finalizeOrder(req, res, next) {
envelopeCount: copiedEnvelopes.length, envelopeCount: copiedEnvelopes.length,
letters: copiedLetters, letters: copiedLetters,
envelopes: copiedEnvelopes, envelopes: copiedEnvelopes,
other: copiedOther other: copiedOther,
letterFormat: meta.letterFormat || null,
envelopeFormat: meta.envelopeFormat || null,
envelopeBeschriftung: meta.envelopeBeschriftung || null,
}; };
const orderMetadataPath = path.join(outputDir, 'order-metadata.json'); const orderMetadataPath = path.join(outputDir, 'order-metadata.json');
@@ -317,6 +318,7 @@ async function generateOrder(req, res, next) {
} }
// Step 6: Create order metadata // Step 6: Create order metadata
const meta = req.body.metadata || {};
const orderMetadata = { const orderMetadata = {
orderNumber, orderNumber,
generatedAt: new Date().toISOString(), generatedAt: new Date().toISOString(),
@@ -326,7 +328,10 @@ async function generateOrder(req, res, next) {
envelopes: generatedEnvelopes, envelopes: generatedEnvelopes,
fonts: Object.keys(lettersByFont), fonts: Object.keys(lettersByFont),
hasPlaceholders, hasPlaceholders,
csvFiles csvFiles,
letterFormat: meta.letterFormat || null,
envelopeFormat: meta.envelopeFormat || null,
envelopeBeschriftung: meta.envelopeBeschriftung || null,
}; };
const orderMetadataPath = path.join(outputDir, 'order-metadata.json'); const orderMetadataPath = path.join(outputDir, 'order-metadata.json');

View File

@@ -5,7 +5,7 @@ const FONT_SIZE_PX = 26;
const LINE_LIMITS = { const LINE_LIMITS = {
a4: 27, a4: 27,
a6p: 14, a6p: 14,
a6l: 9, a6l: 11,
}; };
// Layout Konfiguration pro Format + Orientation // Layout Konfiguration pro Format + Orientation
@@ -19,15 +19,16 @@ const FORMAT_LAYOUTS = {
// A6 Hochformat // A6 Hochformat
a6p: { a6p: {
marginTopMm: 12, marginTopMm: 8,
marginLeftMm: 10, marginLeftMm: 8,
lineHeightFactor: 1.3, lineHeightFactor: 1.3,
}, },
// A6 Querformat (landscape) // A6 Querformat (landscape)
a6l: { a6l: {
marginTopMm: 10, marginTopMm: 8,
marginLeftMm: 8, marginLeftMm: 8,
marginRightMm: 14,
lineHeightFactor: 1.2, lineHeightFactor: 1.2,
}, },
@@ -77,6 +78,7 @@ function getLayoutForFormat(format) {
return { return {
marginTopPx: mmToPx(layoutConfig.marginTopMm), marginTopPx: mmToPx(layoutConfig.marginTopMm),
marginLeftPx: mmToPx(layoutConfig.marginLeftMm), marginLeftPx: mmToPx(layoutConfig.marginLeftMm),
marginRightPx: layoutConfig.marginRightMm ? mmToPx(layoutConfig.marginRightMm) : null,
lineHeightPx: FONT_SIZE_PX * layoutConfig.lineHeightFactor, lineHeightPx: FONT_SIZE_PX * layoutConfig.lineHeightFactor,
}; };
} }

View File

@@ -37,10 +37,10 @@ function generateLetterSVG(text, format, options = {}) {
const fontConfig = SVG_FONT_CONFIG[fontKey]; const fontConfig = SVG_FONT_CONFIG[fontKey];
const { widthPx, heightPx, widthMm, heightMm } = getPageDimensions(format); const { widthPx, heightPx, widthMm, heightMm } = getPageDimensions(format);
const { marginTopPx, marginLeftPx, lineHeightPx } = getLayoutForFormat(format); const { marginTopPx, marginLeftPx, marginRightPx, lineHeightPx } = getLayoutForFormat(format);
// Automatischer Zeilenumbruch // Automatischer Zeilenumbruch
const maxWidthPx = widthPx - 2 * marginLeftPx; const maxWidthPx = widthPx - marginLeftPx - (marginRightPx ?? marginLeftPx);
const lines = wrapTextToLinesByWidth({ const lines = wrapTextToLinesByWidth({
text: String(text || ''), text: String(text || ''),

View File

@@ -4,28 +4,35 @@ import {
deriveContextFromUrl, deriveContextFromUrl,
reducer, reducer,
STEPS, STEPS,
} from "./configurator-state.js?ver=0.3.0"; } from "./configurator-state.js?ver=0.3.1";
import { render, showValidationOverlay, hideValidationOverlay, showOverflowWarning, showValidationError, flushAllTables } from "./configurator-ui.js?ver=0.3.0"; import {
import './configurator-api.js'; // Backend API initialisieren render,
import PreviewManager from './configurator-preview-manager.js'; // Preview Management showValidationOverlay,
hideValidationOverlay,
showOverflowWarning,
showValidationError,
flushAllTables,
} from "./configurator-ui.js?ver=0.3.1";
import "./configurator-api.js"; // Backend API initialisieren
import PreviewManager from "./configurator-preview-manager.js"; // Preview Management
(function boot() { (function boot() {
const root = document.querySelector('[data-skrift-konfigurator="1"]'); const root = document.querySelector('[data-skrift-konfigurator="1"]');
if (!root) return; if (!root) return;
// Guard: Verhindere doppelte Initialisierung // Guard: Verhindere doppelte Initialisierung
if (root.dataset.skriftInitialized === '1') { if (root.dataset.skriftInitialized === "1") {
console.warn('[Konfigurator] Bereits initialisiert, überspringe.'); console.warn("[Konfigurator] Bereits initialisiert, überspringe.");
return; return;
} }
root.dataset.skriftInitialized = '1'; root.dataset.skriftInitialized = "1";
// Cleanup bei Navigation (SPA) oder Seiten-Unload // Cleanup bei Navigation (SPA) oder Seiten-Unload
const cleanupHandlers = []; const cleanupHandlers = [];
const cleanup = () => { const cleanup = () => {
flushAllTables(); // Tabellen-Daten speichern vor Cleanup/Seiten-Unload flushAllTables(); // Tabellen-Daten speichern vor Cleanup/Seiten-Unload
cleanupHandlers.forEach(handler => handler()); cleanupHandlers.forEach((handler) => handler());
cleanupHandlers.length = 0; cleanupHandlers.length = 0;
if (window.envelopePreviewManager) { if (window.envelopePreviewManager) {
window.envelopePreviewManager.destroy(); window.envelopePreviewManager.destroy();
@@ -36,8 +43,10 @@ import PreviewManager from './configurator-preview-manager.js'; // Preview Manag
}; };
// Cleanup bei Seiten-Unload // Cleanup bei Seiten-Unload
window.addEventListener('beforeunload', cleanup); window.addEventListener("beforeunload", cleanup);
cleanupHandlers.push(() => window.removeEventListener('beforeunload', cleanup)); cleanupHandlers.push(() =>
window.removeEventListener("beforeunload", cleanup),
);
const ctx = deriveContextFromUrl(window.location.search); const ctx = deriveContextFromUrl(window.location.search);
let state = createInitialState(ctx); let state = createInitialState(ctx);
@@ -70,7 +79,7 @@ import PreviewManager from './configurator-preview-manager.js'; // Preview Manag
// Scroll-Position NACH dem Render wiederherstellen // Scroll-Position NACH dem Render wiederherstellen
// Nur bei Actions die NICHT den Step wechseln (Navigation) // Nur bei Actions die NICHT den Step wechseln (Navigation)
const navigationActions = ['NAV_NEXT', 'NAV_PREV', 'SET_STEP']; const navigationActions = ["NAV_NEXT", "NAV_PREV", "SET_STEP"];
if (!navigationActions.includes(action.type)) { if (!navigationActions.includes(action.type)) {
// requestAnimationFrame stellt sicher, dass das DOM komplett gerendert ist // requestAnimationFrame stellt sicher, dass das DOM komplett gerendert ist
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -89,6 +98,21 @@ import PreviewManager from './configurator-preview-manager.js'; // Preview Manag
const nextClickHandler = async () => { const nextClickHandler = async () => {
flushAllTables(); // Tabellen-Daten speichern vor Navigation flushAllTables(); // Tabellen-Daten speichern vor Navigation
// WICHTIG: Globalen State in den Store synchronisieren
// Nötig weil Paste direkt in window.currentGlobalState schreibt (Performance)
const globalState = window.currentGlobalState;
if (globalState) {
// Alle Tabellen-Daten in einem einzigen Dispatch synchronisieren
dispatch({
type: "SYNC_GLOBAL_STATE",
payload: {
recipientRows: globalState.recipientRows,
freeAddressRows: globalState.freeAddressRows,
placeholderValues: globalState.placeholderValues,
},
});
}
// WICHTIG: Aktuellen State verwenden, nicht den gecachten aus dem Closure // WICHTIG: Aktuellen State verwenden, nicht den gecachten aus dem Closure
const currentState = window.currentGlobalState || state; const currentState = window.currentGlobalState || state;
@@ -98,12 +122,16 @@ import PreviewManager from './configurator-preview-manager.js'; // Preview Manag
if (previewManager) { if (previewManager) {
showValidationOverlay(); showValidationOverlay();
try { try {
const validation = await previewManager.validateTextLength(currentState); const validation =
await previewManager.validateTextLength(currentState);
if (!validation.valid) { if (!validation.valid) {
hideValidationOverlay(); hideValidationOverlay();
// Bei Overflow: Warnung anzeigen // Bei Overflow: Warnung anzeigen
if (validation.overflowFiles && validation.overflowFiles.length > 0) { if (
validation.overflowFiles &&
validation.overflowFiles.length > 0
) {
showOverflowWarning(validation.overflowFiles, dom.form); showOverflowWarning(validation.overflowFiles, dom.form);
} else if (validation.error) { } else if (validation.error) {
// Bei Fehler (z.B. keine Anfragen mehr): Fehlermeldung anzeigen // Bei Fehler (z.B. keine Anfragen mehr): Fehlermeldung anzeigen
@@ -117,11 +145,15 @@ import PreviewManager from './configurator-preview-manager.js'; // Preview Manag
const envelopeManager = window.envelopePreviewManager; const envelopeManager = window.envelopePreviewManager;
if (envelopeManager && currentState.answers?.envelope === true) { if (envelopeManager && currentState.answers?.envelope === true) {
try { try {
envelopeManager.previewCount = parseInt(currentState.answers?.quantity) || 1; envelopeManager.previewCount =
parseInt(currentState.answers?.quantity) || 1;
await envelopeManager.loadAllPreviews(currentState, true, true); await envelopeManager.loadAllPreviews(currentState, true, true);
console.log('[App] Envelope previews generated for cache'); console.log("[App] Envelope previews generated for cache");
} catch (envError) { } catch (envError) {
console.error('[App] Envelope preview generation failed:', envError); console.error(
"[App] Envelope preview generation failed:",
envError,
);
// Nicht blockieren - Umschläge sind nicht kritisch für Navigation // Nicht blockieren - Umschläge sind nicht kritisch für Navigation
} }
} }
@@ -129,8 +161,11 @@ import PreviewManager from './configurator-preview-manager.js'; // Preview Manag
hideValidationOverlay(); hideValidationOverlay();
} catch (error) { } catch (error) {
hideValidationOverlay(); hideValidationOverlay();
console.error('[App] Validation error:', error); console.error("[App] Validation error:", error);
showValidationError(error.message || 'Validierung fehlgeschlagen', dom.form); showValidationError(
error.message || "Validierung fehlgeschlagen",
dom.form,
);
return; // Nicht weiter navigieren return; // Nicht weiter navigieren
} }
} }
@@ -147,21 +182,39 @@ import PreviewManager from './configurator-preview-manager.js'; // Preview Manag
// Keyboard Navigation für Previews (nur innerhalb des aktuellen Batches) // Keyboard Navigation für Previews (nur innerhalb des aktuellen Batches)
const keydownHandler = (e) => { const keydownHandler = (e) => {
if (window.envelopePreviewManager && window.envelopePreviewManager.currentBatchPreviews.length > 0) { if (
window.envelopePreviewManager.handleKeyboardNavigation(e, state, dom.preview, true); window.envelopePreviewManager &&
window.envelopePreviewManager.currentBatchPreviews.length > 0
) {
window.envelopePreviewManager.handleKeyboardNavigation(
e,
state,
dom.preview,
true,
);
} }
if (window.contentPreviewManager && window.contentPreviewManager.currentBatchPreviews.length > 0) { if (
window.contentPreviewManager.handleKeyboardNavigation(e, state, dom.preview, false); window.contentPreviewManager &&
window.contentPreviewManager.currentBatchPreviews.length > 0
) {
window.contentPreviewManager.handleKeyboardNavigation(
e,
state,
dom.preview,
false,
);
} }
}; };
document.addEventListener('keydown', keydownHandler); document.addEventListener("keydown", keydownHandler);
cleanupHandlers.push(() => document.removeEventListener('keydown', keydownHandler)); cleanupHandlers.push(() =>
document.removeEventListener("keydown", keydownHandler),
);
render({ state, dom, dispatch }); render({ state, dom, dispatch });
// Konfigurator sichtbar machen nachdem erster Render abgeschlossen ist // Konfigurator sichtbar machen nachdem erster Render abgeschlossen ist
requestAnimationFrame(() => { requestAnimationFrame(() => {
root.classList.add('sk-ready'); root.classList.add("sk-ready");
}); });
})(); })();

View File

@@ -49,8 +49,11 @@ function getProductDefinitions() {
products[key] = { products[key] = {
...baseConfig, ...baseConfig,
label: backendSettings.label || key, label: backendSettings.label || key,
description: backendSettings.description || 'Professionelle handgeschriebene Korrespondenz', description:
basePrice: parseFloat(backendSettings.base_price) || 2.50, backendSettings.description ||
"Professionelle handgeschriebene Korrespondenz",
basePrice: parseFloat(backendSettings.base_price) || 2.5,
imageUrl: backendSettings.image_url || "",
}; };
} }
@@ -86,21 +89,22 @@ export function deriveContextFromUrl(search) {
} }
// Quantity aus URL // Quantity aus URL
const quantityParam = q.get('quantity'); const quantityParam = q.get("quantity");
const quantity = quantityParam ? parseInt(quantityParam, 10) : null; const quantity = quantityParam ? parseInt(quantityParam, 10) : null;
// Format aus URL (a4, a6h = A6 Hochformat, a6q = A6 Querformat) // Format aus URL (a4, a6h = A6 Hochformat, a6q = A6 Querformat)
const formatParam = q.get('format')?.toLowerCase(); const formatParam = q.get("format")?.toLowerCase();
let format = null; let format = null;
if (formatParam === 'a4') format = 'a4'; if (formatParam === "a4") format = "a4";
else if (formatParam === 'a6h') format = 'a6p'; // Hochformat else if (formatParam === "a6h")
else if (formatParam === 'a6q') format = 'a6l'; // Querformat format = "a6p"; // Hochformat
else if (formatParam === "a6q") format = "a6l"; // Querformat
// noPrice Parameter (Preise ausblenden) // noPrice Parameter (Preise ausblenden)
const noPrice = q.has('noPrice') || q.has('noprice'); const noPrice = q.has("noPrice") || q.has("noprice");
// noLimits Parameter (keine Mindestmengen) // noLimits Parameter (keine Mindestmengen)
const noLimits = q.has('noLimits') || q.has('nolimits'); const noLimits = q.has("noLimits") || q.has("nolimits");
return { return {
urlParam: urlParam || null, urlParam: urlParam || null,
@@ -162,11 +166,14 @@ export function createInitialState(ctx) {
} }
// Standardmenge auf beste Preismenge (normalQuantity) setzen, außer URL-Parameter // Standardmenge auf beste Preismenge (normalQuantity) setzen, außer URL-Parameter
const dynamicPricing = window.SkriftConfigurator?.settings?.dynamic_pricing || {}; const dynamicPricing =
window.SkriftConfigurator?.settings?.dynamic_pricing || {};
const isB2B = initialCustomerType === "business"; const isB2B = initialCustomerType === "business";
const defaultQuantity = urlQuantity || (isB2B const defaultQuantity =
? (dynamicPricing.business_normal_quantity || 200) urlQuantity ||
: (dynamicPricing.private_normal_quantity || 50)); (isB2B
? dynamicPricing.business_normal_quantity || 200
: dynamicPricing.private_normal_quantity || 50);
// Format aus URL oder null // Format aus URL oder null
const initialFormat = urlFormat || null; const initialFormat = urlFormat || null;
@@ -217,6 +224,8 @@ export function createInitialState(ctx) {
// Inhalt // Inhalt
contentCreateMode: null, // 'self' | 'textservice' contentCreateMode: null, // 'self' | 'textservice'
letterText: "", letterText: "",
font: "tilda",
envelopeFont: "tilda",
// Motiv // Motiv
motifNeed: null, motifNeed: null,
@@ -232,7 +241,7 @@ export function createInitialState(ctx) {
recipientRows: [], recipientRows: [],
// Adressmodus: 'classic' (Name, Anschrift) oder 'free' (5 freie Zeilen) // Adressmodus: 'classic' (Name, Anschrift) oder 'free' (5 freie Zeilen)
addressMode: 'classic', addressMode: "classic",
// Freie Adresszeilen (separat gespeichert, damit beim Wechsel nichts verloren geht) // Freie Adresszeilen (separat gespeichert, damit beim Wechsel nichts verloren geht)
freeAddressRows: [], freeAddressRows: [],
placeholders: { placeholders: {
@@ -314,9 +323,8 @@ export function calcEffectiveEnvelopeType(state) {
} }
export function getAvailableProductsForCustomerType(customerType) { export function getAvailableProductsForCustomerType(customerType) {
return Object.values(PRODUCT_BY_PARAM).filter( const fresh = getProductDefinitions();
(p) => p.category === customerType return Object.values(fresh).filter((p) => p.category === customerType);
);
} }
export function syncPlaceholders(state) { export function syncPlaceholders(state) {
@@ -406,109 +414,17 @@ export function validateStep(state) {
// Versandart muss gewählt sein // Versandart muss gewählt sein
if (!state.answers.shippingMode) return false; if (!state.answers.shippingMode) return false;
// Bei Versand durch Skrift ist Umschlag automatisch if (state.answers.shippingMode !== "direct") {
if (state.answers.shippingMode === "direct") {
// Automatisch gesetzt, keine Validierung nötig für envelope
if (!state.answers.envelopeMode) return false;
// Bei Follow-ups: keine Empfängerdaten-Validierung (kommt aus CRM)
// Bei regulären Produkten: Empfängerdaten müssen vorhanden sein
if (!isFollowups(state)) {
// Empfängerdaten validieren für reguläre Produkte
if (state.answers.envelopeMode === "recipientData") {
const addressMode = state.addressMode || 'classic';
if (addressMode === 'free') {
// Freie Adresse: mindestens Zeile 1 muss ausgefüllt sein
const rows = state.freeAddressRows || [];
if (rows.length !== requiredRowCount(state)) return false;
for (const r of rows) {
if (!r) return false;
// Mindestens Zeile 1 muss ausgefüllt sein
if (!String(r.line1 || "").trim()) return false;
}
} else {
// Klassische Adresse
const rows = state.recipientRows || [];
if (rows.length !== requiredRowCount(state)) return false;
for (const r of rows) {
if (!r) return false;
const required = [
"firstName",
"lastName",
"street",
"houseNumber",
"zip",
"city",
"country",
];
for (const k of required) {
if (!String(r[k] || "").trim()) return false;
}
}
}
}
}
} else {
// Bulk versand // Bulk versand
if (state.answers.envelope === null) return false; if (state.answers.envelope === null) return false;
if (state.answers.envelope === true) { if (state.answers.envelope === true) {
// Beschriftungsmodus muss gewählt sein
if (!state.answers.envelopeMode) return false; if (!state.answers.envelopeMode) return false;
// Empfängerdaten validieren für Bulk-Versand (nur wenn Umschlag gewählt)
if (state.answers.envelopeMode === "recipientData") {
const addressMode = state.addressMode || 'classic';
if (addressMode === 'free') {
// Freie Adresse: mindestens Zeile 1 muss ausgefüllt sein
const rows = state.freeAddressRows || [];
if (rows.length !== requiredRowCount(state)) return false;
for (const r of rows) {
if (!r) return false;
if (!String(r.line1 || "").trim()) return false;
}
} else {
// Klassische Adresse
const rows = state.recipientRows || [];
if (rows.length !== requiredRowCount(state)) return false;
for (const r of rows) {
if (!r) return false;
const required = [
"firstName",
"lastName",
"street",
"houseNumber",
"zip",
"city",
"country",
];
for (const k of required) {
if (!String(r[k] || "").trim()) return false;
}
}
}
}
// Custom Text validieren (nur wenn Umschlag gewählt)
if (state.answers.envelopeMode === "customText") { if (state.answers.envelopeMode === "customText") {
if (!state.answers.envelopeCustomText?.trim()) return false; if (!state.answers.envelopeCustomText?.trim()) return false;
if (state.placeholders.envelope.length > 0) {
for (const ph of state.placeholders.envelope) {
const arr = state.placeholderValues[ph] || [];
if (arr.length !== requiredRowCount(state)) return false;
if (arr.some((v) => !String(v || "").trim())) return false;
}
}
} }
} }
// Wenn envelope === false, keine weitere Validierung nötig
} }
return true; return true;
@@ -520,31 +436,6 @@ export function validateStep(state) {
// Wenn self, muss Text vorhanden sein // Wenn self, muss Text vorhanden sein
if (state.answers.contentCreateMode === "self") { if (state.answers.contentCreateMode === "self") {
if (!state.answers.letterText?.trim()) return false; if (!state.answers.letterText?.trim()) return false;
// Platzhalter validieren
const usedLetter = extractPlaceholders(state.answers.letterText);
// Platzhalter validieren
// "vorname", "name", "ort" können immer aus recipientRows kommen (bei shippingMode=direct oder envelopeMode=recipientData)
const recipientPlaceholders = new Set(["vorname", "name", "ort"]);
const hasRecipientData = state.answers.shippingMode === "direct" || state.answers.envelopeMode === "recipientData";
// Platzhalter aus dem Umschlag
const envSet = new Set(state.placeholders.envelope || []);
// Platzhalter die validiert werden müssen (nicht aus Umschlag, nicht aus recipientRows)
const needed = usedLetter.filter((p) => {
if (envSet.has(p)) return false; // aus Umschlag
if (hasRecipientData && recipientPlaceholders.has(p)) return false; // aus recipientRows
return true;
});
// Nur die übrigen Platzhalter validieren
for (const ph of needed) {
const arr = state.placeholderValues[ph] || [];
if (arr.length !== requiredRowCount(state)) return false;
if (arr.some((v) => !String(v || "").trim())) return false;
}
} }
// Motiv Validierung // Motiv Validierung
@@ -594,7 +485,14 @@ export function validateStep(state) {
if (state.order.shippingDifferent) { if (state.order.shippingDifferent) {
const sh = state.order.shipping; const sh = state.order.shipping;
// Shipping braucht kein E-Mail/Telefon - nur Adressfelder // Shipping braucht kein E-Mail/Telefon - nur Adressfelder
const shippingReq = ["firstName", "lastName", "street", "zip", "city", "country"]; const shippingReq = [
"firstName",
"lastName",
"street",
"zip",
"city",
"country",
];
for (const k of shippingReq) { for (const k of shippingReq) {
if (!String(sh[k] || "").trim()) return false; if (!String(sh[k] || "").trim()) return false;
} }
@@ -614,17 +512,17 @@ export function validateStep(state) {
// Preisrelevante Felder die eine Neuberechnung auslösen // Preisrelevante Felder die eine Neuberechnung auslösen
const PRICE_RELEVANT_FIELDS = new Set([ const PRICE_RELEVANT_FIELDS = new Set([
'quantity', "quantity",
'format', "format",
'shippingMode', "shippingMode",
'envelope', "envelope",
'envelopeMode', "envelopeMode",
'motifSource', "motifSource",
'motifNeed', "motifNeed",
'contentCreateMode', "contentCreateMode",
'followupYearlyVolume', "followupYearlyVolume",
'followupCreateMode', "followupCreateMode",
'customerType', "customerType",
]); ]);
/** /**
@@ -632,7 +530,7 @@ const PRICE_RELEVANT_FIELDS = new Set([
*/ */
function hasPriceRelevantChanges(patch) { function hasPriceRelevantChanges(patch) {
if (!patch) return false; if (!patch) return false;
return Object.keys(patch).some(key => PRICE_RELEVANT_FIELDS.has(key)); return Object.keys(patch).some((key) => PRICE_RELEVANT_FIELDS.has(key));
} }
/** /**
@@ -647,19 +545,20 @@ function countCountryDistribution(rows, addressMode) {
for (const row of rows) { for (const row of rows) {
if (!row) continue; if (!row) continue;
let country = ''; let country = "";
if (addressMode === 'free') { if (addressMode === "free") {
country = row.line5 || ''; country = row.line5 || "";
} else { } else {
country = row.country || ''; country = row.country || "";
} }
const countryLower = country.toLowerCase().trim(); const countryLower = country.toLowerCase().trim();
const isDomestic = !countryLower || const isDomestic =
countryLower === 'deutschland' || !countryLower ||
countryLower === 'germany' || countryLower === "deutschland" ||
countryLower === 'de' || countryLower === "germany" ||
countryLower === 'ger'; countryLower === "de" ||
countryLower === "ger";
if (isDomestic) { if (isDomestic) {
domestic++; domestic++;
@@ -710,6 +609,11 @@ export function reducer(state, action) {
if (p.urlFormat !== null) delete answerPatch.format; if (p.urlFormat !== null) delete answerPatch.format;
next.answers = { ...next.answers, ...answerPatch }; next.answers = { ...next.answers, ...answerPatch };
// Auto-Logik: Bei Direktversand immer envelopeMode sicherstellen
if (next.answers.shippingMode === "direct" && !next.answers.envelopeMode) {
next.answers = { ...next.answers, envelope: true, envelopeMode: "recipientData" };
}
// Empfängerdaten // Empfängerdaten
if (Array.isArray(p.recipientRows)) { if (Array.isArray(p.recipientRows)) {
next.recipientRows = p.recipientRows; next.recipientRows = p.recipientRows;
@@ -737,7 +641,11 @@ export function reducer(state, action) {
} }
// Step wiederherstellen (nur wenn nicht im PRODUCT Step) // Step wiederherstellen (nur wenn nicht im PRODUCT Step)
if (typeof p.step === "number" && p.step >= 0 && p.currentStep !== STEPS.PRODUCT) { if (
typeof p.step === "number" &&
p.step >= 0 &&
p.currentStep !== STEPS.PRODUCT
) {
next.step = p.step; next.step = p.step;
next.history = []; next.history = [];
for (let i = 0; i < p.step; i++) { for (let i = 0; i < p.step; i++) {
@@ -745,6 +653,12 @@ export function reducer(state, action) {
} }
} }
// Wenn nur ein Format verfügbar und keins gespeichert: automatisch setzen
const hydrateFormats = next.ctx?.product?.formats || [];
if (hydrateFormats.length === 1 && !next.answers.format) {
next.answers = { ...next.answers, format: hydrateFormats[0] };
}
// Quote einmal am Ende berechnen // Quote einmal am Ende berechnen
next = recalculateQuote(next); next = recalculateQuote(next);
@@ -773,6 +687,11 @@ export function reducer(state, action) {
next.answers = { ...next.answers, format: null }; next.answers = { ...next.answers, format: null };
} }
// Wenn nur ein Format verfügbar: automatisch setzen
const availFormats = action.product?.formats || [];
if (availFormats.length === 1 && !next.answers.format) {
next.answers = { ...next.answers, format: availFormats[0] };
}
// Follow-ups: Automatisch Direktversand setzen // Follow-ups: Automatisch Direktversand setzen
if (action.product?.key === "follow-ups") { if (action.product?.key === "follow-ups") {
next.answers = { ...next.answers, shippingMode: "direct" }; next.answers = { ...next.answers, shippingMode: "direct" };
@@ -796,6 +715,11 @@ export function reducer(state, action) {
next.answers = { ...next.answers, format: null }; next.answers = { ...next.answers, format: null };
} }
// Wenn nur ein Format verfügbar: automatisch setzen
const availFormats = action.product?.formats || [];
if (availFormats.length === 1 && !next.answers.format) {
next.answers = { ...next.answers, format: availFormats[0] };
}
// Follow-ups: Automatisch Direktversand setzen // Follow-ups: Automatisch Direktversand setzen
if (action.product?.key === "follow-ups") { if (action.product?.key === "follow-ups") {
next.answers = { ...next.answers, shippingMode: "direct" }; next.answers = { ...next.answers, shippingMode: "direct" };
@@ -813,7 +737,7 @@ export function reducer(state, action) {
// WICHTIG: Preview Cache löschen wenn envelopeMode geändert wird // WICHTIG: Preview Cache löschen wenn envelopeMode geändert wird
if (action.patch && "envelopeMode" in action.patch) { if (action.patch && "envelopeMode" in action.patch) {
console.log('[State] envelopeMode changed, clearing envelope previews'); console.log("[State] envelopeMode changed, clearing envelope previews");
if (window.envelopePreviewManager) { if (window.envelopePreviewManager) {
window.envelopePreviewManager.currentBatchPreviews = []; window.envelopePreviewManager.currentBatchPreviews = [];
window.envelopePreviewManager.currentDocIndex = 0; window.envelopePreviewManager.currentDocIndex = 0;
@@ -834,10 +758,13 @@ export function reducer(state, action) {
}; };
// Bei Einzelversand: Leere Länder-Felder auf "Deutschland" setzen (Default) // Bei Einzelversand: Leere Länder-Felder auf "Deutschland" setzen (Default)
if (Array.isArray(next.recipientRows)) { if (Array.isArray(next.recipientRows)) {
next.recipientRows = next.recipientRows.map(row => ({ next.recipientRows = next.recipientRows.map((row) => ({
...row, ...row,
// Nur setzen wenn Feld leer ist // Nur setzen wenn Feld leer ist
country: row.country && row.country.trim() !== "" ? row.country : "Deutschland" country:
row.country && row.country.trim() !== ""
? row.country
: "Deutschland",
})); }));
} }
} }
@@ -848,20 +775,21 @@ export function reducer(state, action) {
// Nur setzen wenn Menge noch nicht vom Benutzer geändert wurde // Nur setzen wenn Menge noch nicht vom Benutzer geändert wurde
// (d.h. wenn sie noch dem alten Default entspricht oder nicht gesetzt ist) // (d.h. wenn sie noch dem alten Default entspricht oder nicht gesetzt ist)
const currentQty = next.answers.quantity; const currentQty = next.answers.quantity;
const dynamicPricing = window.SkriftConfigurator?.settings?.dynamic_pricing || {}; const dynamicPricing =
window.SkriftConfigurator?.settings?.dynamic_pricing || {};
// Alte Default-Werte berechnen // Alte Default-Werte berechnen
const wasB2B = state.answers.customerType === "business"; const wasB2B = state.answers.customerType === "business";
const oldDefaultQty = wasB2B const oldDefaultQty = wasB2B
? (dynamicPricing.business_normal_quantity || 200) ? dynamicPricing.business_normal_quantity || 200
: (dynamicPricing.private_normal_quantity || 50); : dynamicPricing.private_normal_quantity || 50;
// Nur überschreiben wenn Menge noch dem alten Default entspricht oder nicht gesetzt ist // Nur überschreiben wenn Menge noch dem alten Default entspricht oder nicht gesetzt ist
if (!currentQty || currentQty === oldDefaultQty || currentQty === 1) { if (!currentQty || currentQty === oldDefaultQty || currentQty === 1) {
const isB2B = action.patch.customerType === "business"; const isB2B = action.patch.customerType === "business";
const newDefaultQuantity = isB2B const newDefaultQuantity = isB2B
? (dynamicPricing.business_normal_quantity || 200) ? dynamicPricing.business_normal_quantity || 200
: (dynamicPricing.private_normal_quantity || 50); : dynamicPricing.private_normal_quantity || 50;
next.answers = { ...next.answers, quantity: newDefaultQuantity }; next.answers = { ...next.answers, quantity: newDefaultQuantity };
} }
} }
@@ -885,7 +813,11 @@ export function reducer(state, action) {
// Auto-Logik für motifNeed: Wenn kein Motiv gewählt, motifSource zurücksetzen // Auto-Logik für motifNeed: Wenn kein Motiv gewählt, motifSource zurücksetzen
if (action.patch && "motifNeed" in action.patch) { if (action.patch && "motifNeed" in action.patch) {
if (action.patch.motifNeed === false) { if (action.patch.motifNeed === false) {
next.answers = { ...next.answers, motifSource: null, serviceDesign: false }; next.answers = {
...next.answers,
motifSource: null,
serviceDesign: false,
};
} }
} }
@@ -893,7 +825,10 @@ export function reducer(state, action) {
if (action.patch && "format" in action.patch) { if (action.patch && "format" in action.patch) {
if (action.patch.format === "a4") { if (action.patch.format === "a4") {
// Wenn upload oder design ausgewählt war, auf printed umstellen // Wenn upload oder design ausgewählt war, auf printed umstellen
if (next.answers.motifSource === "upload" || next.answers.motifSource === "design") { if (
next.answers.motifSource === "upload" ||
next.answers.motifSource === "design"
) {
next.answers = { ...next.answers, motifSource: "printed" }; next.answers = { ...next.answers, motifSource: "printed" };
} }
} }
@@ -914,9 +849,18 @@ export function reducer(state, action) {
// Quote nur neu berechnen wenn sich Inland/Ausland-Verteilung geändert hat // Quote nur neu berechnen wenn sich Inland/Ausland-Verteilung geändert hat
let next = { ...state, recipientRows: action.rows }; let next = { ...state, recipientRows: action.rows };
if (state.answers?.shippingMode === "direct") { if (state.answers?.shippingMode === "direct") {
const oldDist = countCountryDistribution(state.recipientRows, state.addressMode); const oldDist = countCountryDistribution(
const newDist = countCountryDistribution(action.rows, state.addressMode); state.recipientRows,
if (oldDist.domestic !== newDist.domestic || oldDist.international !== newDist.international) { state.addressMode,
);
const newDist = countCountryDistribution(
action.rows,
state.addressMode,
);
if (
oldDist.domestic !== newDist.domestic ||
oldDist.international !== newDist.international
) {
next = recalculateQuote(next); next = recalculateQuote(next);
} }
} }
@@ -936,9 +880,12 @@ export function reducer(state, action) {
// Quote nur neu berechnen wenn sich Inland/Ausland-Verteilung geändert hat // Quote nur neu berechnen wenn sich Inland/Ausland-Verteilung geändert hat
let next = { ...state, freeAddressRows: action.rows }; let next = { ...state, freeAddressRows: action.rows };
if (state.answers?.shippingMode === "direct") { if (state.answers?.shippingMode === "direct") {
const oldDist = countCountryDistribution(state.freeAddressRows, 'free'); const oldDist = countCountryDistribution(state.freeAddressRows, "free");
const newDist = countCountryDistribution(action.rows, 'free'); const newDist = countCountryDistribution(action.rows, "free");
if (oldDist.domestic !== newDist.domestic || oldDist.international !== newDist.international) { if (
oldDist.domestic !== newDist.domestic ||
oldDist.international !== newDist.international
) {
next = recalculateQuote(next); next = recalculateQuote(next);
} }
} }
@@ -967,8 +914,35 @@ export function reducer(state, action) {
return { ...state, placeholderValues: action.values || {} }; return { ...state, placeholderValues: action.values || {} };
} }
case "SYNC_GLOBAL_STATE": {
// Synchronisiert alle direkt mutierten Daten (aus window.currentGlobalState)
// in einem einzigen Dispatch ohne mehrfaches Re-Render
const p = action.payload || {};
let next = { ...state };
if (Array.isArray(p.recipientRows)) {
next.recipientRows = p.recipientRows;
}
if (Array.isArray(p.freeAddressRows)) {
next.freeAddressRows = p.freeAddressRows;
}
if (p.placeholderValues && typeof p.placeholderValues === "object") {
next.placeholderValues = p.placeholderValues;
}
// Quote neu berechnen falls relevant
if (next.answers?.shippingMode === "direct") {
next = recalculateQuote(next);
}
return next;
}
case "SET_ORDER": { case "SET_ORDER": {
const nextState = { ...state, order: { ...state.order, ...action.patch } }; const nextState = {
...state,
order: { ...state.order, ...action.patch },
};
// Quote neu berechnen wenn Gutschein geändert wurde // Quote neu berechnen wenn Gutschein geändert wurde
return recalculateQuote(nextState); return recalculateQuote(nextState);
} }
@@ -1000,7 +974,7 @@ export function reducer(state, action) {
const nextStep = Math.min(STEPS.REVIEW, state.step + 1); const nextStep = Math.min(STEPS.REVIEW, state.step + 1);
// Nach oben scrollen // Nach oben scrollen
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: "smooth" });
return { return {
...state, ...state,
@@ -1015,7 +989,7 @@ export function reducer(state, action) {
const prev = hist[hist.length - 1]; const prev = hist[hist.length - 1];
// Nach oben scrollen // Nach oben scrollen
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: "smooth" });
return { ...state, step: prev, history: hist.slice(0, -1) }; return { ...state, step: prev, history: hist.slice(0, -1) };
} }
@@ -1026,5 +1000,7 @@ export function reducer(state, action) {
} }
export function listProducts() { export function listProducts() {
return Object.values(PRODUCT_BY_PARAM); // Immer frisch laden damit Backend-Settings (inkl. imageUrl) verfügbar sind
const fresh = getProductDefinitions();
return Object.values(fresh);
} }

View File

@@ -10,7 +10,7 @@ import {
normalizePlaceholderName, normalizePlaceholderName,
validateStep, validateStep,
getAvailableProductsForCustomerType, getAvailableProductsForCustomerType,
} from "./configurator-state.js?ver=0.3.0"; } from "./configurator-state.js?ver=0.3.1";
import { import {
calculatePricePerPiece, calculatePricePerPiece,
@@ -725,7 +725,16 @@ function renderTopbar(dom, state) {
if (state?.ctx?.product) { if (state?.ctx?.product) {
productInfo.appendChild( productInfo.appendChild(
h("div", { class: "sk-product-icon", text: state.ctx.product.label[0] }), state.ctx.product.imageUrl
? h("img", {
src: state.ctx.product.imageUrl,
alt: state.ctx.product.label,
class: "sk-product-icon sk-product-icon-img",
})
: h("div", {
class: "sk-product-icon",
text: state.ctx.product.label[0],
}),
); );
productInfo.appendChild( productInfo.appendChild(
h("div", {}, [ h("div", {}, [
@@ -835,7 +844,13 @@ function renderProductStep(state, dispatch) {
}, },
[ [
h("div", { class: "sk-selection-card-image" }, [ h("div", { class: "sk-selection-card-image" }, [
h("div", { text: "📄", style: "font-size: 48px" }), // Placeholder p.imageUrl
? h("img", {
src: p.imageUrl,
alt: p.label,
class: "sk-selection-card-img",
})
: h("div", { text: "📄", style: "font-size: 48px" }),
]), ]),
h("div", { class: "sk-selection-card-content" }, [ h("div", { class: "sk-selection-card-content" }, [
h("div", { class: "sk-selection-card-title", text: p.label }), h("div", { class: "sk-selection-card-title", text: p.label }),
@@ -1059,20 +1074,6 @@ function renderQuantityStep(state, dispatch) {
h("div", { class: "sk-options" }, formatBody), h("div", { class: "sk-options" }, formatBody),
]), ]),
); );
} else if (formats.length === 1) {
// Auto-set single format (nur wenn noch nicht gesetzt)
if (!state.answers.format) {
dispatch({ type: "ANSWER", patch: { format: formats[0] } });
}
}
// Schriftart wird jetzt in Umschlag und Inhalt Steps separat gewählt (nicht hier)
// Default auf tilda wenn noch nicht gesetzt
if (!state.answers.font) {
dispatch({ type: "ANSWER", patch: { font: "tilda" } });
}
if (!state.answers.envelopeFont) {
dispatch({ type: "ANSWER", patch: { envelopeFont: "tilda" } });
} }
// Follow-ups: Erstellungsmodus // Follow-ups: Erstellungsmodus
@@ -2223,6 +2224,9 @@ async function handleOrderSubmit(state) {
finalizeResult = await api.finalizeOrder(api.sessionId, orderNumber, { finalizeResult = await api.finalizeOrder(api.sessionId, orderNumber, {
customer: state.order, customer: state.order,
quote: state.quote, quote: state.quote,
letterFormat: { a4: "A4", a6p: "A6 Hochformat", a6l: "A6 Querformat" }[state.answers.format] || state.answers.format || null,
envelopeFormat: state.answers.format === "a4" ? "DIN Lang" : (state.answers.format === "a6p" || state.answers.format === "a6l") ? "C6" : null,
envelopeBeschriftung: { recipientData: "Empfängeradresse", customText: "Individueller Text", none: "Keine Beschriftung" }[state.answers.envelopeMode] || state.answers.envelopeMode || null,
}); });
console.log("[Order] Finalize result:", finalizeResult); console.log("[Order] Finalize result:", finalizeResult);
} catch (error) { } catch (error) {
@@ -3356,6 +3360,9 @@ async function handlePayPalSuccess(state, paypalDetails) {
status: paypalDetails.status, status: paypalDetails.status,
}, },
quote: state.quote, quote: state.quote,
letterFormat: { a4: "A4", a6p: "A6 Hochformat", a6l: "A6 Querformat" }[state.answers.format] || state.answers.format || null,
envelopeFormat: state.answers.format === "a4" ? "DIN Lang" : (state.answers.format === "a6p" || state.answers.format === "a6l") ? "C6" : null,
envelopeBeschriftung: { recipientData: "Empfängeradresse", customText: "Individueller Text", none: "Keine Beschriftung" }[state.answers.envelopeMode] || state.answers.envelopeMode || null,
}); });
console.log("[PayPal] Finalize result:", finalizeResult); console.log("[PayPal] Finalize result:", finalizeResult);
} catch (error) { } catch (error) {
@@ -3671,6 +3678,7 @@ function createTableModal(dispatch, config) {
newData.push({}); newData.push({});
} }
// Werte in lokale Daten UND direkt ins DOM schreiben (kein Re-Render)
let pastedCells = 0; let pastedCells = 0;
rows.forEach((rowText, rowOffset) => { rows.forEach((rowText, rowOffset) => {
const cells = rowText.split("\t"); const cells = rowText.split("\t");
@@ -3686,23 +3694,30 @@ function createTableModal(dispatch, config) {
if (!col.readOnly) { if (!col.readOnly) {
if (!newData[targetRow]) newData[targetRow] = {}; if (!newData[targetRow]) newData[targetRow] = {};
newData[targetRow][col.key] = cellValue.trim(); newData[targetRow][col.key] = cellValue.trim();
// DOM direkt aktualisieren (kein Re-Render nötig)
const input = table.querySelector(
`input[data-row="${targetRow}"][data-key="${col.key}"]`,
);
if (input) input.value = cellValue.trim();
pastedCells++; pastedCells++;
} }
} }
}); });
}); });
// Daten direkt in globalen State schreiben (OHNE Re-Render)
if (pastedCells > 0) { if (pastedCells > 0) {
config.setData(dispatch, newData); const globalState = window.currentGlobalState;
if (globalState && config.stateKey) {
// Tabelle im Modal aktualisieren globalState[config.stateKey] = newData;
renderTable(); }
markPersistDirty();
} }
// Flag nach kurzem Delay zurücksetzen (für evtl. verzögerte input-Events) // Flag zurücksetzen
setTimeout(() => { isPasting = false;
isPasting = false;
}, 50);
}); });
return table; return table;
@@ -3933,7 +3948,9 @@ function createTableModal(dispatch, config) {
{ {
class: "sk-modal", class: "sk-modal",
onclick: (e) => { onclick: (e) => {
if (e.target.classList.contains("sk-modal")) modal.remove(); if (e.target.classList.contains("sk-modal")) {
modal.remove();
}
}, },
}, },
[modalContent], [modalContent],
@@ -4013,6 +4030,12 @@ function renderRecipientsTable(state, dispatch) {
}; };
} }
currentRows[idx][key] = e.target.value; currentRows[idx][key] = e.target.value;
// Auch globalen State aktualisieren für SYNC_GLOBAL_STATE
const globalState = window.currentGlobalState;
if (globalState) {
globalState.recipientRows = currentRows;
}
}, },
}), }),
]); ]);
@@ -4040,6 +4063,12 @@ function renderRecipientsTable(state, dispatch) {
}; };
} }
currentRows[idx].country = e.target.value; currentRows[idx].country = e.target.value;
// Auch globalen State aktualisieren für SYNC_GLOBAL_STATE
const globalState = window.currentGlobalState;
if (globalState) {
globalState.recipientRows = currentRows;
}
}, },
}), }),
]); ]);
@@ -4085,7 +4114,7 @@ function renderRecipientsTable(state, dispatch) {
hasLocalChanges = true; hasLocalChanges = true;
}); });
// Paste-Handler // Paste-Handler - Daten direkt in globalen State schreiben (OHNE Re-Render)
table.addEventListener("paste", (e) => { table.addEventListener("paste", (e) => {
e.preventDefault(); e.preventDefault();
const text = e.clipboardData.getData("text/plain"); const text = e.clipboardData.getData("text/plain");
@@ -4121,25 +4150,30 @@ function renderRecipientsTable(state, dispatch) {
"city", "city",
"country", "country",
]; ];
const currentRows = table._rows;
const newData = []; // Aktuellen globalen State holen
for (let i = 0; i < want; i++) { const globalState = window.currentGlobalState;
newData.push( if (!globalState) return;
currentRows[i]
? { ...currentRows[i] } // Kopie der aktuellen Rows erstellen
: { const currentRows = Array.isArray(globalState.recipientRows)
firstName: "", ? globalState.recipientRows.map((r) => ({ ...r }))
lastName: "", : [];
street: "",
houseNumber: "", // Sicherstellen dass genug Zeilen existieren
zip: "", while (currentRows.length < want) {
city: "", currentRows.push({
country: isDirectShipping ? "Deutschland" : "", firstName: "",
}, lastName: "",
); street: "",
houseNumber: "",
zip: "",
city: "",
country: isDirectShipping ? "Deutschland" : "",
});
} }
let pastedCells = 0; // Werte in Daten UND direkt ins DOM schreiben
pasteRows.forEach((rowText, rowOffset) => { pasteRows.forEach((rowText, rowOffset) => {
const targetRow = startRow + rowOffset; const targetRow = startRow + rowOffset;
if (targetRow >= want) return; if (targetRow >= want) return;
@@ -4149,15 +4183,35 @@ function renderRecipientsTable(state, dispatch) {
const targetCol = startCol + colOffset; const targetCol = startCol + colOffset;
if (targetCol >= keys.length) return; if (targetCol >= keys.length) return;
const key = keys[targetCol]; const key = keys[targetCol];
newData[targetRow][key] = cellValue;
pastedCells++; // Daten aktualisieren
if (!currentRows[targetRow]) {
currentRows[targetRow] = {
firstName: "",
lastName: "",
street: "",
houseNumber: "",
zip: "",
city: "",
country: isDirectShipping ? "Deutschland" : "",
};
}
currentRows[targetRow][key] = cellValue;
// DOM direkt aktualisieren (kein Re-Render nötig)
const input = table.querySelector(
`input[data-row="${targetRow}"][data-col="${key}"]`,
);
if (input) input.value = cellValue;
}); });
}); });
if (pastedCells > 0) { // Globalen State DIREKT aktualisieren (OHNE dispatch/render)
hasLocalChanges = false; // Paste übernimmt - keine alten Änderungen mehr relevant globalState.recipientRows = currentRows;
dispatch({ type: "SET_RECIPIENT_ROWS", rows: newData }); table._rows = currentRows;
}
// Für Persistenz markieren
markPersistDirty();
}); });
return table; return table;
@@ -4174,6 +4228,9 @@ function renderRecipientsTable(state, dispatch) {
{ class: "sk-table sk-table-pasteable", tabindex: "0" }, { class: "sk-table sk-table-pasteable", tabindex: "0" },
[], [],
); );
// Rows-Referenz am Table speichern für Event-Handler
table._rows = rows;
table.appendChild( table.appendChild(
h("thead", {}, [ h("thead", {}, [
h("tr", {}, [ h("tr", {}, [
@@ -4200,8 +4257,9 @@ function renderRecipientsTable(state, dispatch) {
"data-col": key, "data-col": key,
"data-sk-focus": `freeaddr.${key}.${idx}`, "data-sk-focus": `freeaddr.${key}.${idx}`,
oninput: (e) => { oninput: (e) => {
if (!rows[idx]) { const currentRows = table._rows;
rows[idx] = { if (!currentRows[idx]) {
currentRows[idx] = {
line1: "", line1: "",
line2: "", line2: "",
line3: "", line3: "",
@@ -4209,7 +4267,13 @@ function renderRecipientsTable(state, dispatch) {
line5: "", line5: "",
}; };
} }
rows[idx][key] = e.target.value; currentRows[idx][key] = e.target.value;
// Auch globalen State aktualisieren für SYNC_GLOBAL_STATE
const globalState = window.currentGlobalState;
if (globalState) {
globalState.freeAddressRows = currentRows;
}
}, },
}), }),
]); ]);
@@ -4231,7 +4295,7 @@ function renderRecipientsTable(state, dispatch) {
let hasLocalChanges = false; let hasLocalChanges = false;
const saveLocalChanges = () => { const saveLocalChanges = () => {
if (hasLocalChanges) { if (hasLocalChanges) {
dispatch({ type: "SET_FREE_ADDRESS_ROWS", rows: [...rows] }); dispatch({ type: "SET_FREE_ADDRESS_ROWS", rows: [...table._rows] });
hasLocalChanges = false; hasLocalChanges = false;
} }
}; };
@@ -4253,7 +4317,7 @@ function renderRecipientsTable(state, dispatch) {
hasLocalChanges = true; hasLocalChanges = true;
}); });
// Paste-Handler // Paste-Handler - Daten direkt in globalen State schreiben (OHNE Re-Render)
table.addEventListener("paste", (e) => { table.addEventListener("paste", (e) => {
e.preventDefault(); e.preventDefault();
const text = e.clipboardData.getData("text/plain"); const text = e.clipboardData.getData("text/plain");
@@ -4281,16 +4345,28 @@ function renderRecipientsTable(state, dispatch) {
} }
const keys = ["line1", "line2", "line3", "line4", "line5"]; const keys = ["line1", "line2", "line3", "line4", "line5"];
const newData = [];
for (let i = 0; i < want; i++) { // Aktuellen globalen State holen
newData.push( const globalState = window.currentGlobalState;
rows[i] if (!globalState) return;
? { ...rows[i] }
: { line1: "", line2: "", line3: "", line4: "", line5: "" }, // Kopie der aktuellen Rows erstellen
); const currentRows = Array.isArray(globalState.freeAddressRows)
? globalState.freeAddressRows.map((r) => ({ ...r }))
: [];
// Sicherstellen dass genug Zeilen existieren
while (currentRows.length < want) {
currentRows.push({
line1: "",
line2: "",
line3: "",
line4: "",
line5: "",
});
} }
let pastedCells = 0; // Werte in Daten UND direkt ins DOM schreiben
pasteRows.forEach((rowText, rowOffset) => { pasteRows.forEach((rowText, rowOffset) => {
const targetRow = startRow + rowOffset; const targetRow = startRow + rowOffset;
if (targetRow >= want) return; if (targetRow >= want) return;
@@ -4299,15 +4375,34 @@ function renderRecipientsTable(state, dispatch) {
cells.forEach((cellValue, colOffset) => { cells.forEach((cellValue, colOffset) => {
const targetCol = startCol + colOffset; const targetCol = startCol + colOffset;
if (targetCol >= keys.length) return; if (targetCol >= keys.length) return;
newData[targetRow][keys[targetCol]] = cellValue; const key = keys[targetCol];
pastedCells++;
// Daten aktualisieren
if (!currentRows[targetRow]) {
currentRows[targetRow] = {
line1: "",
line2: "",
line3: "",
line4: "",
line5: "",
};
}
currentRows[targetRow][key] = cellValue;
// DOM direkt aktualisieren (kein Re-Render nötig)
const input = table.querySelector(
`input[data-row="${targetRow}"][data-col="${key}"]`,
);
if (input) input.value = cellValue;
}); });
}); });
if (pastedCells > 0) { // Globalen State DIREKT aktualisieren (OHNE dispatch/render)
hasLocalChanges = false; // Paste übernimmt - keine alten Änderungen mehr relevant globalState.freeAddressRows = currentRows;
dispatch({ type: "SET_FREE_ADDRESS_ROWS", rows: newData }); table._rows = currentRows;
}
// Für Persistenz markieren
markPersistDirty();
}); });
return table; return table;
@@ -4346,6 +4441,7 @@ function renderRecipientsTable(state, dispatch) {
setData: (dispatch, data) => { setData: (dispatch, data) => {
dispatch({ type: "SET_FREE_ADDRESS_ROWS", rows: data }); dispatch({ type: "SET_FREE_ADDRESS_ROWS", rows: data });
}, },
stateKey: "freeAddressRows",
rowCount: () => want, rowCount: () => want,
exportFilename: "freie_adressen.csv", exportFilename: "freie_adressen.csv",
helpText: "Geben Sie bis zu 5 Zeilen pro Empfänger ein.", helpText: "Geben Sie bis zu 5 Zeilen pro Empfänger ein.",
@@ -4390,6 +4486,7 @@ function renderRecipientsTable(state, dispatch) {
setData: (dispatch, data) => { setData: (dispatch, data) => {
dispatch({ type: "SET_RECIPIENT_ROWS", rows: data }); dispatch({ type: "SET_RECIPIENT_ROWS", rows: data });
}, },
stateKey: "recipientRows",
rowCount: () => want, rowCount: () => want,
exportFilename: "empfaenger.csv", exportFilename: "empfaenger.csv",
helpText: helpText:
@@ -4585,6 +4682,22 @@ function renderCombinedPlaceholderTable(
// Nur lokal speichern - kein Re-Render! // Nur lokal speichern - kein Re-Render!
localValues[name][row] = e.target.value; localValues[name][row] = e.target.value;
hasLocalChanges = true; hasLocalChanges = true;
// Auch globalen State aktualisieren für SYNC_GLOBAL_STATE
const globalState = window.currentGlobalState;
if (globalState) {
if (!globalState.placeholderValues) {
globalState.placeholderValues = {};
}
if (!globalState.placeholderValues[name]) {
globalState.placeholderValues[name] = [];
}
globalState.placeholderValues[name][row] = e.target.value;
// Array auf requiredRowCount-Länge bringen (für validateStep)
while (globalState.placeholderValues[name].length < want) {
globalState.placeholderValues[name].push("");
}
}
} }
}, },
}), }),
@@ -4624,7 +4737,16 @@ function renderCombinedPlaceholderTable(
} }
} }
// Alle Änderungen in localValues sammeln und Inputs aktualisieren // Aktuellen globalen State holen
const globalState = window.currentGlobalState;
if (!globalState) return;
// Kopie der aktuellen Platzhalter-Werte erstellen
const currentValues = globalState.placeholderValues
? JSON.parse(JSON.stringify(globalState.placeholderValues))
: {};
// Alle Änderungen in currentValues sammeln und Inputs aktualisieren
let pastedCells = 0; let pastedCells = 0;
pasteRows.forEach((rowText, rowOffset) => { pasteRows.forEach((rowText, rowOffset) => {
const targetRow = startRow + rowOffset; const targetRow = startRow + rowOffset;
@@ -4640,7 +4762,11 @@ function renderCombinedPlaceholderTable(
// Nur editierbare Spalten aktualisieren // Nur editierbare Spalten aktualisieren
if (!isReadOnly) { if (!isReadOnly) {
// Daten aktualisieren
if (!currentValues[name]) currentValues[name] = [];
currentValues[name][targetRow] = cellValue;
localValues[name][targetRow] = cellValue; localValues[name][targetRow] = cellValue;
// Input-Element direkt aktualisieren // Input-Element direkt aktualisieren
const input = table.querySelector( const input = table.querySelector(
`input[data-row="${targetRow}"][data-col="${name}"]`, `input[data-row="${targetRow}"][data-col="${name}"]`,
@@ -4651,10 +4777,15 @@ function renderCombinedPlaceholderTable(
}); });
}); });
// Nach Paste: Einmal speichern // Globalen State DIREKT aktualisieren (OHNE dispatch/render)
if (pastedCells > 0) { if (pastedCells > 0) {
hasLocalChanges = true; // Arrays auf want-Länge normalisieren (für validateStep)
saveLocalChanges(); for (const name of Object.keys(currentValues)) {
while (currentValues[name].length < want)
currentValues[name].push("");
}
globalState.placeholderValues = currentValues;
markPersistDirty();
} }
}); });
@@ -4827,6 +4958,22 @@ function renderPlaceholderTable(
oninput: (e) => { oninput: (e) => {
// Wert nur lokal speichern (kein Re-Render) // Wert nur lokal speichern (kein Re-Render)
localValues[name][row] = e.target.value; localValues[name][row] = e.target.value;
// Auch globalen State aktualisieren für SYNC_GLOBAL_STATE
const globalState = window.currentGlobalState;
if (globalState) {
if (!globalState.placeholderValues) {
globalState.placeholderValues = {};
}
if (!globalState.placeholderValues[name]) {
globalState.placeholderValues[name] = [];
}
globalState.placeholderValues[name][row] = e.target.value;
// Array auf requiredRowCount-Länge bringen (für validateStep)
while (globalState.placeholderValues[name].length < want) {
globalState.placeholderValues[name].push("");
}
}
}, },
}), }),
]), ]),
@@ -4890,7 +5037,16 @@ function renderPlaceholderTable(
} }
} }
// Alle Änderungen in localValues sammeln // Aktuellen globalen State holen
const globalState = window.currentGlobalState;
if (!globalState) return;
// Kopie der aktuellen Platzhalter-Werte erstellen
const currentValues = globalState.placeholderValues
? JSON.parse(JSON.stringify(globalState.placeholderValues))
: {};
// Alle Änderungen sammeln UND direkt ins DOM schreiben
let pastedCells = 0; let pastedCells = 0;
pasteRows.forEach((rowText, rowOffset) => { pasteRows.forEach((rowText, rowOffset) => {
const targetRow = startRow + rowOffset; const targetRow = startRow + rowOffset;
@@ -4902,15 +5058,31 @@ function renderPlaceholderTable(
if (targetCol >= clean.length) return; if (targetCol >= clean.length) return;
const name = clean[targetCol]; const name = clean[targetCol];
// Daten aktualisieren
if (!currentValues[name]) currentValues[name] = [];
currentValues[name][targetRow] = cellValue;
localValues[name][targetRow] = cellValue; localValues[name][targetRow] = cellValue;
// DOM direkt aktualisieren (kein Re-Render nötig)
const input = table.querySelector(
`input[data-row="${targetRow}"][data-col="${name}"]`,
);
if (input) input.value = cellValue;
pastedCells++; pastedCells++;
}); });
}); });
// Einmal dispatch für alle Änderungen // Globalen State DIREKT aktualisieren (OHNE dispatch/render)
if (pastedCells > 0) { if (pastedCells > 0) {
hasLocalChanges = false; // Paste übernimmt - keine alten Änderungen mehr relevant // Arrays auf want-Länge normalisieren (für validateStep)
dispatch({ type: "SET_PLACEHOLDER_VALUES", values: localValues }); for (const name of Object.keys(currentValues)) {
while (currentValues[name].length < want)
currentValues[name].push("");
}
globalState.placeholderValues = currentValues;
markPersistDirty();
} }
}); });