Initial commit
This commit is contained in:
1463
skrift-configurator/assets/css/configurator.css
Normal file
1463
skrift-configurator/assets/css/configurator.css
Normal file
File diff suppressed because it is too large
Load Diff
392
skrift-configurator/assets/js/configurator-api.js
Normal file
392
skrift-configurator/assets/js/configurator-api.js
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* Skrift Backend API Client
|
||||
* Kommunikation mit dem Node.js Backend über WordPress Proxy
|
||||
* Der API-Token wird serverseitig gehandhabt und ist nicht im Frontend exponiert
|
||||
*/
|
||||
|
||||
class SkriftBackendAPI {
|
||||
constructor() {
|
||||
// WordPress REST API URL für den Proxy
|
||||
this.restUrl = window.SkriftConfigurator?.restUrl || '/wp-json/';
|
||||
// API Key für WordPress REST API Authentifizierung
|
||||
this.apiKey = window.SkriftConfigurator?.apiKey || '';
|
||||
// WordPress Nonce für CSRF-Schutz
|
||||
this.nonce = window.SkriftConfigurator?.nonce || '';
|
||||
// Direkte Backend-URL nur für Preview-Bilder (read-only)
|
||||
this.backendUrl = window.SkriftConfigurator?.settings?.backend_connection?.api_url || '';
|
||||
// Alias für Kompatibilität mit PreviewManager
|
||||
this.baseURL = this.backendUrl;
|
||||
this.sessionId = null;
|
||||
this.previewCache = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Standard-Headers für WordPress REST API zurück
|
||||
*/
|
||||
getHeaders() {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': this.nonce,
|
||||
};
|
||||
|
||||
if (this.apiKey) {
|
||||
headers['X-Skrift-API-Key'] = this.apiKey;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine eindeutige Session-ID für Preview-Caching
|
||||
*/
|
||||
generateSessionId() {
|
||||
return `session-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Health-Check: Prüft ob Backend erreichbar ist (über WordPress Proxy)
|
||||
*/
|
||||
async healthCheck() {
|
||||
try {
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/health`, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Health check failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.status === 'ok';
|
||||
} catch (error) {
|
||||
console.error('[API] Health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview Batch: Generiert eine Vorschau von Briefen (über WordPress Proxy)
|
||||
* Briefe und Umschläge werden in derselben Session gespeichert
|
||||
*/
|
||||
async generatePreviewBatch(letters, options = {}) {
|
||||
try {
|
||||
// SessionId nur generieren wenn noch keine existiert oder explizit angefordert
|
||||
// So bleiben Briefe und Umschläge in derselben Session
|
||||
if (!this.sessionId || options.newSession) {
|
||||
this.sessionId = this.generateSessionId();
|
||||
console.log('[API] New session created:', this.sessionId);
|
||||
} else {
|
||||
console.log('[API] Reusing existing session:', this.sessionId);
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
sessionId: this.sessionId,
|
||||
letters: letters.map(letter => ({
|
||||
index: letter.index,
|
||||
text: letter.text,
|
||||
font: letter.font || 'tilda',
|
||||
format: letter.format || 'A4',
|
||||
placeholders: letter.placeholders || {},
|
||||
type: letter.type || 'letter',
|
||||
envelopeType: letter.envelopeType || 'recipient',
|
||||
envelope: letter.envelope || null,
|
||||
})),
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/preview/batch`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
|
||||
if (response.status === 429 && error.retryAfter) {
|
||||
const err = new Error(error.error || 'Rate limit exceeded');
|
||||
err.retryAfter = error.retryAfter;
|
||||
err.statusCode = 429;
|
||||
throw err;
|
||||
}
|
||||
|
||||
throw new Error(error.error || error.message || `Preview generation failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Session-ID vom Backend übernehmen (falls anders als gesendet)
|
||||
if (data.sessionId) {
|
||||
this.sessionId = data.sessionId;
|
||||
}
|
||||
|
||||
const previews = data.files ? data.files.map((file, index) => ({
|
||||
index: file.index !== undefined ? file.index : index,
|
||||
url: file.url || file.path,
|
||||
format: file.format,
|
||||
pages: file.pages || 1,
|
||||
lineCount: file.lineCount,
|
||||
lineLimit: file.lineLimit,
|
||||
overflow: file.overflow,
|
||||
recipientName: file.recipientName,
|
||||
})) : [];
|
||||
|
||||
previews.forEach((preview, index) => {
|
||||
this.previewCache.set(`${this.sessionId}-${index}`, preview);
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionId: this.sessionId,
|
||||
previews: previews,
|
||||
batchInfo: data.batchInfo,
|
||||
hasOverflow: data.hasOverflow || false,
|
||||
overflowFiles: data.overflowFiles || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Preview batch error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Preview: Ruft eine einzelne Preview-URL ab
|
||||
* Hinweis: Diese Methode wird aktuell nicht verwendet, da Preview-URLs direkt vom Backend kommen
|
||||
*/
|
||||
async getPreviewUrl(sessionId, index) {
|
||||
try {
|
||||
const cacheKey = `${sessionId}-${index}`;
|
||||
|
||||
if (this.previewCache.has(cacheKey)) {
|
||||
return this.previewCache.get(cacheKey).url;
|
||||
}
|
||||
|
||||
// Über WordPress Proxy abrufen
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/preview/${sessionId}/${index}`, {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Preview not found: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const svgText = await response.text();
|
||||
// Sicheres Base64-Encoding für Unicode
|
||||
const dataUrl = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(svgText)))}`;
|
||||
|
||||
return dataUrl;
|
||||
} catch (error) {
|
||||
console.error('[API] Get preview URL error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalize Order: Finalisiert eine Bestellung aus dem Preview-Cache (über WordPress Proxy)
|
||||
*/
|
||||
async finalizeOrder(sessionId, orderNumber, metadata = {}) {
|
||||
try {
|
||||
const requestBody = {
|
||||
sessionId: sessionId,
|
||||
orderNumber: orderNumber,
|
||||
metadata: {
|
||||
customer: metadata.customer || {},
|
||||
orderDate: metadata.orderDate || new Date().toISOString(),
|
||||
...metadata,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/order/finalize`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || `Order finalization failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
orderNumber: data.orderNumber,
|
||||
path: data.path,
|
||||
files: data.files,
|
||||
envelopesGenerated: data.envelopesGenerated || 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Finalize order error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Order: Generiert eine Bestellung ohne Preview (direkt, über WordPress Proxy)
|
||||
* Backend erwartet alle Dokumente (Briefe + Umschläge) im letters-Array mit type-Property
|
||||
*/
|
||||
async generateOrder(orderNumber, letters, envelopes = [], metadata = {}) {
|
||||
try {
|
||||
// Letters vorbereiten
|
||||
const preparedLetters = letters.map((letter, index) => ({
|
||||
index: letter.index !== undefined ? letter.index : index,
|
||||
text: letter.text,
|
||||
font: letter.font || 'tilda',
|
||||
format: letter.format || 'A4',
|
||||
placeholders: letter.placeholders || {},
|
||||
type: 'letter',
|
||||
}));
|
||||
|
||||
// Envelopes vorbereiten und anhängen
|
||||
const preparedEnvelopes = envelopes.map((envelope, index) => ({
|
||||
index: envelope.index !== undefined ? envelope.index : index,
|
||||
text: envelope.text || '',
|
||||
font: envelope.font || 'tilda',
|
||||
format: envelope.format || 'DIN_LANG',
|
||||
placeholders: envelope.placeholders || {},
|
||||
type: 'envelope',
|
||||
envelopeType: envelope.envelopeType || 'recipient',
|
||||
}));
|
||||
|
||||
// Alle Dokumente in einem Array für Backend
|
||||
const allDocuments = [...preparedLetters, ...preparedEnvelopes];
|
||||
|
||||
const requestBody = {
|
||||
orderNumber: orderNumber,
|
||||
letters: allDocuments,
|
||||
metadata: {
|
||||
customer: metadata.customer || {},
|
||||
orderDate: metadata.orderDate || new Date().toISOString(),
|
||||
...metadata,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/order/generate`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || `Order generation failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
orderNumber: data.orderNumber,
|
||||
path: data.path,
|
||||
files: data.files,
|
||||
summary: data.summary,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Generate order error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Order Number: Holt fortlaufende Bestellnummer vom WordPress-Backend
|
||||
* Schema: S-JAHR-MONAT-TAG-fortlaufendeNummer (z.B. S-2026-01-12-001)
|
||||
*/
|
||||
async generateOrderNumber() {
|
||||
try {
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/order/generate-number`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to generate order number: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.orderNumber;
|
||||
} catch (error) {
|
||||
console.error('[API] Failed to generate order number from WP:', error);
|
||||
// Fallback: Lokale Generierung (sollte nicht passieren)
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const random = String(Math.floor(Math.random() * 1000)).padStart(3, '0');
|
||||
return `S-${year}-${month}-${day}-${random}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload Motif: Lädt ein Motiv-Bild hoch (über WordPress Proxy)
|
||||
* @param {File} file - Die hochzuladende Datei
|
||||
* @param {string} orderNumber - Die Bestellnummer für die Dateinamenszuordnung
|
||||
* @returns {Promise<{success: boolean, filename?: string, url?: string, error?: string}>}
|
||||
*/
|
||||
async uploadMotif(file, orderNumber) {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('motif', file);
|
||||
formData.append('orderNumber', orderNumber || '');
|
||||
|
||||
const response = await fetch(`${this.restUrl}skrift/v1/proxy/motif/upload`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-WP-Nonce': this.nonce,
|
||||
...(this.apiKey ? { 'X-Skrift-API-Key': this.apiKey } : {}),
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || `Motif upload failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
filename: data.filename,
|
||||
url: data.url,
|
||||
path: data.path,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[API] Motif upload error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear Preview Cache: Löscht den Preview-Cache und setzt Session zurück
|
||||
*/
|
||||
clearPreviewCache() {
|
||||
this.previewCache.clear();
|
||||
this.sessionId = null; // Wird beim nächsten Preview-Aufruf neu generiert
|
||||
}
|
||||
|
||||
/**
|
||||
* Start New Session: Erzwingt eine neue Session für den nächsten Preview-Aufruf
|
||||
*/
|
||||
startNewSession() {
|
||||
this.sessionId = null;
|
||||
this.previewCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanz exportieren
|
||||
window.SkriftBackendAPI = new SkriftBackendAPI();
|
||||
|
||||
export default SkriftBackendAPI;
|
||||
160
skrift-configurator/assets/js/configurator-app.js
Normal file
160
skrift-configurator/assets/js/configurator-app.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/* global SkriftConfigurator */
|
||||
import {
|
||||
createInitialState,
|
||||
deriveContextFromUrl,
|
||||
reducer,
|
||||
STEPS,
|
||||
} from "./configurator-state.js?ver=0.3.0";
|
||||
import { render, showValidationOverlay, hideValidationOverlay, showOverflowWarning, showValidationError, flushAllTables } from "./configurator-ui.js?ver=0.3.0";
|
||||
import './configurator-api.js'; // Backend API initialisieren
|
||||
import PreviewManager from './configurator-preview-manager.js'; // Preview Management
|
||||
|
||||
(function boot() {
|
||||
const root = document.querySelector('[data-skrift-konfigurator="1"]');
|
||||
if (!root) return;
|
||||
|
||||
// Cleanup bei Navigation (SPA) oder Seiten-Unload
|
||||
const cleanupHandlers = [];
|
||||
|
||||
const cleanup = () => {
|
||||
flushAllTables(); // Tabellen-Daten speichern vor Cleanup/Seiten-Unload
|
||||
cleanupHandlers.forEach(handler => handler());
|
||||
cleanupHandlers.length = 0;
|
||||
if (window.envelopePreviewManager) {
|
||||
window.envelopePreviewManager.destroy();
|
||||
}
|
||||
if (window.contentPreviewManager) {
|
||||
window.contentPreviewManager.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup bei Seiten-Unload
|
||||
window.addEventListener('beforeunload', cleanup);
|
||||
cleanupHandlers.push(() => window.removeEventListener('beforeunload', cleanup));
|
||||
|
||||
const ctx = deriveContextFromUrl(window.location.search);
|
||||
let state = createInitialState(ctx);
|
||||
|
||||
const dom = {
|
||||
topbar: document.getElementById("sk-topbar"),
|
||||
stepper: document.getElementById("sk-stepper"),
|
||||
form: document.getElementById("sk-form"),
|
||||
prev: document.getElementById("sk-prev"),
|
||||
next: document.getElementById("sk-next"),
|
||||
preview: document.getElementById("sk-preview"),
|
||||
previewMobile: document.getElementById("sk-preview-mobile"),
|
||||
contactMobile: document.getElementById("sk-contact-mobile"),
|
||||
};
|
||||
|
||||
// Preview Manager initialisieren
|
||||
const api = window.SkriftBackendAPI;
|
||||
if (api) {
|
||||
window.envelopePreviewManager = new PreviewManager(api);
|
||||
window.contentPreviewManager = new PreviewManager(api);
|
||||
}
|
||||
|
||||
const dispatch = (action) => {
|
||||
// Scroll-Position VOR dem State-Update speichern
|
||||
const scrollY = window.scrollY;
|
||||
const scrollX = window.scrollX;
|
||||
|
||||
state = reducer(state, action);
|
||||
render({ state, dom, dispatch });
|
||||
|
||||
// Scroll-Position NACH dem Render wiederherstellen
|
||||
// Nur bei Actions die NICHT den Step wechseln (Navigation)
|
||||
const navigationActions = ['NAV_NEXT', 'NAV_PREV', 'SET_STEP'];
|
||||
if (!navigationActions.includes(action.type)) {
|
||||
// requestAnimationFrame stellt sicher, dass das DOM komplett gerendert ist
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo(scrollX, scrollY);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Event-Handler mit Cleanup-Tracking
|
||||
const prevClickHandler = () => {
|
||||
flushAllTables(); // Tabellen-Daten speichern vor Navigation
|
||||
dispatch({ type: "NAV_PREV" });
|
||||
};
|
||||
|
||||
// Next-Handler mit Validierung bei Content-Step
|
||||
const nextClickHandler = async () => {
|
||||
flushAllTables(); // Tabellen-Daten speichern vor Navigation
|
||||
|
||||
// WICHTIG: Aktuellen State verwenden, nicht den gecachten aus dem Closure
|
||||
const currentState = window.currentGlobalState || state;
|
||||
|
||||
// Bei Content-Step: Textlänge validieren und alle Previews generieren
|
||||
if (currentState.step === STEPS.CONTENT) {
|
||||
const previewManager = window.contentPreviewManager;
|
||||
if (previewManager) {
|
||||
showValidationOverlay();
|
||||
try {
|
||||
const validation = await previewManager.validateTextLength(currentState);
|
||||
|
||||
if (!validation.valid) {
|
||||
hideValidationOverlay();
|
||||
// Bei Overflow: Warnung anzeigen
|
||||
if (validation.overflowFiles && validation.overflowFiles.length > 0) {
|
||||
showOverflowWarning(validation.overflowFiles, dom.form);
|
||||
} else if (validation.error) {
|
||||
// Bei Fehler (z.B. keine Anfragen mehr): Fehlermeldung anzeigen
|
||||
showValidationError(validation.error, dom.form);
|
||||
}
|
||||
return; // Nicht weiter navigieren
|
||||
}
|
||||
|
||||
// Nach erfolgreicher Validierung: Umschlag-Previews generieren (gleiche Session)
|
||||
// So sind alle Dokumente im Cache wenn die Bestellung finalisiert wird
|
||||
const envelopeManager = window.envelopePreviewManager;
|
||||
if (envelopeManager && currentState.answers?.envelope === true) {
|
||||
try {
|
||||
envelopeManager.previewCount = parseInt(currentState.answers?.quantity) || 1;
|
||||
await envelopeManager.loadAllPreviews(currentState, true, true);
|
||||
console.log('[App] Envelope previews generated for cache');
|
||||
} catch (envError) {
|
||||
console.error('[App] Envelope preview generation failed:', envError);
|
||||
// Nicht blockieren - Umschläge sind nicht kritisch für Navigation
|
||||
}
|
||||
}
|
||||
|
||||
hideValidationOverlay();
|
||||
} catch (error) {
|
||||
hideValidationOverlay();
|
||||
console.error('[App] Validation error:', error);
|
||||
showValidationError(error.message || 'Validierung fehlgeschlagen', dom.form);
|
||||
return; // Nicht weiter navigieren
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch({ type: "NAV_NEXT" });
|
||||
};
|
||||
|
||||
dom.prev.addEventListener("click", prevClickHandler);
|
||||
dom.next.addEventListener("click", nextClickHandler);
|
||||
cleanupHandlers.push(() => {
|
||||
dom.prev.removeEventListener("click", prevClickHandler);
|
||||
dom.next.removeEventListener("click", nextClickHandler);
|
||||
});
|
||||
|
||||
// Keyboard Navigation für Previews (nur innerhalb des aktuellen Batches)
|
||||
const keydownHandler = (e) => {
|
||||
if (window.envelopePreviewManager && window.envelopePreviewManager.currentBatchPreviews.length > 0) {
|
||||
window.envelopePreviewManager.handleKeyboardNavigation(e, state, dom.preview, true);
|
||||
}
|
||||
if (window.contentPreviewManager && window.contentPreviewManager.currentBatchPreviews.length > 0) {
|
||||
window.contentPreviewManager.handleKeyboardNavigation(e, state, dom.preview, false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', keydownHandler);
|
||||
cleanupHandlers.push(() => document.removeEventListener('keydown', keydownHandler));
|
||||
|
||||
render({ state, dom, dispatch });
|
||||
|
||||
// Konfigurator sichtbar machen nachdem erster Render abgeschlossen ist
|
||||
requestAnimationFrame(() => {
|
||||
root.classList.add('sk-ready');
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,584 @@
|
||||
/**
|
||||
* Backend Integration für Skrift Konfigurator
|
||||
* Erweitert handleOrderSubmit um Backend-API Calls
|
||||
*/
|
||||
|
||||
import SkriftBackendAPI from './configurator-api.js';
|
||||
import { preparePlaceholdersForIndex } from './configurator-utils.js';
|
||||
|
||||
/**
|
||||
* Bereitet Letter-Daten für Backend vor
|
||||
*/
|
||||
function prepareLettersForBackend(state) {
|
||||
const letters = [];
|
||||
const quantity = parseInt(state.answers?.quantity) || 1;
|
||||
|
||||
// Haupttext
|
||||
const mainText = state.answers?.letterText || state.answers?.text || state.answers?.briefText || '';
|
||||
const font = state.answers?.font || 'tilda';
|
||||
const format = state.answers?.format || 'A4';
|
||||
|
||||
// Für jede Kopie einen Letter-Eintrag erstellen mit individuellen Platzhaltern
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const placeholders = preparePlaceholdersForIndex(state, i);
|
||||
|
||||
letters.push({
|
||||
index: i,
|
||||
text: mainText,
|
||||
font: mapFontToBackend(font),
|
||||
format: mapFormatToBackend(format),
|
||||
placeholders: placeholders,
|
||||
type: 'letter',
|
||||
});
|
||||
}
|
||||
|
||||
return letters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereitet Envelope-Daten für Backend vor
|
||||
*/
|
||||
function prepareEnvelopesForBackend(state) {
|
||||
const envelopes = [];
|
||||
// envelope ist ein boolean (true/false), nicht 'yes'/'no'
|
||||
const hasEnvelope = state.answers?.envelope === true;
|
||||
|
||||
console.log('[Backend Integration] prepareEnvelopesForBackend:', {
|
||||
envelope: state.answers?.envelope,
|
||||
hasEnvelope,
|
||||
envelopeMode: state.answers?.envelopeMode,
|
||||
});
|
||||
|
||||
if (!hasEnvelope) {
|
||||
return envelopes;
|
||||
}
|
||||
|
||||
const quantity = parseInt(state.answers?.quantity) || 1;
|
||||
const envelopeMode = state.answers?.envelopeMode || 'recipientData';
|
||||
const format = state.answers?.format || 'A4';
|
||||
const font = state.answers?.envelopeFont || state.answers?.font || 'tilda';
|
||||
|
||||
// Envelope Format bestimmen
|
||||
const envelopeFormat = format === 'a4' ? 'DIN_LANG' : 'C6';
|
||||
|
||||
if (envelopeMode === 'recipientData') {
|
||||
// Empfängeradresse-Modus: Ein Envelope pro Brief mit individuellen Empfängerdaten
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const placeholders = preparePlaceholdersForIndex(state, i);
|
||||
const recipient = state.recipientRows?.[i] || {};
|
||||
|
||||
// Umschlagtext aus Empfängerdaten zusammenbauen
|
||||
const lines = [];
|
||||
const fullName = `${recipient.firstName || ''} ${recipient.lastName || ''}`.trim();
|
||||
if (fullName) lines.push(fullName);
|
||||
|
||||
const streetLine = `${recipient.street || ''} ${recipient.houseNumber || ''}`.trim();
|
||||
if (streetLine) lines.push(streetLine);
|
||||
|
||||
const location = `${recipient.zip || ''} ${recipient.city || ''}`.trim();
|
||||
if (location) lines.push(location);
|
||||
|
||||
if (recipient.country && recipient.country !== 'Deutschland') {
|
||||
lines.push(recipient.country);
|
||||
}
|
||||
|
||||
envelopes.push({
|
||||
index: i,
|
||||
text: lines.join('\n'),
|
||||
font: mapFontToBackend(font),
|
||||
format: envelopeFormat,
|
||||
placeholders: placeholders,
|
||||
type: 'envelope',
|
||||
envelopeType: 'recipient',
|
||||
});
|
||||
}
|
||||
} else if (envelopeMode === 'customText') {
|
||||
// Custom Text Modus mit Platzhaltern
|
||||
const customText = state.answers?.envelopeCustomText || '';
|
||||
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
const placeholders = preparePlaceholdersForIndex(state, i);
|
||||
|
||||
envelopes.push({
|
||||
index: i,
|
||||
text: customText,
|
||||
font: mapFontToBackend(font),
|
||||
format: envelopeFormat,
|
||||
placeholders: placeholders,
|
||||
type: 'envelope',
|
||||
envelopeType: 'custom',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return envelopes;
|
||||
}
|
||||
|
||||
// Hinweis: preparePlaceholdersForIndex ist jetzt in configurator-utils.js
|
||||
|
||||
/**
|
||||
* Mapped Frontend-Font zu Backend-Font
|
||||
*/
|
||||
function mapFontToBackend(frontendFont) {
|
||||
const fontMap = {
|
||||
'tilda': 'tilda',
|
||||
'alva': 'alva',
|
||||
'ellie': 'ellie',
|
||||
// Füge weitere Mappings hinzu falls nötig
|
||||
};
|
||||
|
||||
return fontMap[frontendFont] || 'tilda';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapped Frontend-Format zu Backend-Format
|
||||
*/
|
||||
function mapFormatToBackend(frontendFormat) {
|
||||
const formatMap = {
|
||||
'a4': 'A4',
|
||||
'a6p': 'A6_PORTRAIT',
|
||||
'a6l': 'A6_LANDSCAPE',
|
||||
'A4': 'A4',
|
||||
'A6_PORTRAIT': 'A6_PORTRAIT',
|
||||
'A6_LANDSCAPE': 'A6_LANDSCAPE',
|
||||
};
|
||||
|
||||
return formatMap[frontendFormat] || 'A4';
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt das Umschlag-Format basierend auf Brief-Format
|
||||
*/
|
||||
function getEnvelopeFormat(letterFormat) {
|
||||
const format = String(letterFormat).toLowerCase();
|
||||
if (format === 'a4') return 'DIN_LANG';
|
||||
if (format === 'a6p' || format === 'a6l') return 'C6';
|
||||
return 'DIN_LANG';
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Format für lesbare Ausgabe
|
||||
*/
|
||||
function formatFormatLabel(format) {
|
||||
const labels = {
|
||||
'a4': 'A4 Hochformat',
|
||||
'a6p': 'A6 Hochformat',
|
||||
'a6l': 'A6 Querformat',
|
||||
};
|
||||
return labels[format] || format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatiert Font für lesbare Ausgabe
|
||||
*/
|
||||
function formatFontLabel(font) {
|
||||
const labels = {
|
||||
'tilda': 'Tilda',
|
||||
'alva': 'Alva',
|
||||
'ellie': 'Ellie',
|
||||
};
|
||||
return labels[font] || font;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut das komplette Webhook-Datenobjekt zusammen
|
||||
* Enthält ALLE relevanten Felder für Bestellbestätigung und n8n Workflow
|
||||
*/
|
||||
function buildWebhookData(state, backendResult) {
|
||||
const answers = state.answers || {};
|
||||
const order = state.order || {};
|
||||
const quote = state.quote || {};
|
||||
const ctx = state.ctx || {};
|
||||
|
||||
// Gutschein-Informationen
|
||||
const voucherCode = order.voucherStatus?.valid ? order.voucherCode : null;
|
||||
const voucherDiscount = order.voucherStatus?.valid ? (order.voucherStatus.discount || 0) : 0;
|
||||
|
||||
// Umschlag-Format ermitteln
|
||||
const envelopeFormat = answers.envelope ? getEnvelopeFormat(answers.format) : null;
|
||||
|
||||
// Inland/Ausland zählen
|
||||
let domesticCount = 0;
|
||||
let internationalCount = 0;
|
||||
const addressMode = state.addressMode || 'classic';
|
||||
const rows = addressMode === 'free' ? (state.freeAddressRows || []) : (state.recipientRows || []);
|
||||
|
||||
for (const row of rows) {
|
||||
if (!row) continue;
|
||||
const country = addressMode === 'free' ? (row.line5 || '') : (row.country || '');
|
||||
const countryLower = country.toLowerCase().trim();
|
||||
const isDomestic = !countryLower ||
|
||||
countryLower === 'deutschland' ||
|
||||
countryLower === 'germany' ||
|
||||
countryLower === 'de';
|
||||
|
||||
if (isDomestic) {
|
||||
domesticCount++;
|
||||
} else {
|
||||
internationalCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// === BESTELLNUMMER & ZEITSTEMPEL ===
|
||||
orderNumber: backendResult?.orderNumber || null,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
||||
// === KUNDE ===
|
||||
customerType: answers.customerType || 'private',
|
||||
customerTypeLabel: answers.customerType === 'business' ? 'Geschäftskunde' : 'Privatkunde',
|
||||
|
||||
// === PRODUKT ===
|
||||
product: ctx.product?.key || null,
|
||||
productLabel: ctx.product?.label || null,
|
||||
productCategory: ctx.product?.category || null,
|
||||
|
||||
// === MENGE ===
|
||||
quantity: parseInt(answers.quantity) || 0,
|
||||
domesticCount: domesticCount,
|
||||
internationalCount: internationalCount,
|
||||
|
||||
// === FORMAT & SCHRIFT ===
|
||||
format: answers.format || null,
|
||||
formatLabel: formatFormatLabel(answers.format),
|
||||
font: answers.font || 'tilda',
|
||||
fontLabel: formatFontLabel(answers.font || 'tilda'),
|
||||
|
||||
// === VERSAND ===
|
||||
shippingMode: answers.shippingMode || null,
|
||||
shippingModeLabel: answers.shippingMode === 'direct' ? 'Einzelversand durch Skrift' : 'Sammellieferung',
|
||||
|
||||
// === UMSCHLAG ===
|
||||
envelopeIncluded: answers.envelope === true,
|
||||
envelopeFormat: envelopeFormat,
|
||||
envelopeFormatLabel: envelopeFormat === 'DIN_LANG' ? 'DIN Lang' : (envelopeFormat === 'C6' ? 'C6' : null),
|
||||
envelopeMode: answers.envelopeMode || null,
|
||||
envelopeModeLabel: answers.envelopeMode === 'recipientData' ? 'Empfängeradresse' :
|
||||
(answers.envelopeMode === 'customText' ? 'Individueller Text' : null),
|
||||
envelopeFont: answers.envelopeFont || answers.font || 'tilda',
|
||||
envelopeFontLabel: formatFontLabel(answers.envelopeFont || answers.font || 'tilda'),
|
||||
envelopeCustomText: answers.envelopeCustomText || null,
|
||||
|
||||
// === INHALT ===
|
||||
contentCreateMode: answers.contentCreateMode || null,
|
||||
contentCreateModeLabel: answers.contentCreateMode === 'self' ? 'Selbst erstellt' :
|
||||
(answers.contentCreateMode === 'textservice' ? 'Textservice' : null),
|
||||
letterText: answers.letterText || null,
|
||||
|
||||
// === MOTIV ===
|
||||
motifNeeded: answers.motifNeed === true,
|
||||
motifSource: answers.motifSource || null,
|
||||
motifSourceLabel: answers.motifSource === 'upload' ? 'Eigenes Motiv hochgeladen' :
|
||||
(answers.motifSource === 'printed' ? 'Bedruckte Karten verwenden' :
|
||||
(answers.motifSource === 'design' ? 'Designservice' : null)),
|
||||
motifFileName: answers.motifFileName || null,
|
||||
motifFileMeta: answers.motifFileMeta || null,
|
||||
|
||||
// === SERVICES ===
|
||||
serviceText: answers.serviceText === true,
|
||||
serviceDesign: answers.serviceDesign === true,
|
||||
serviceApi: answers.serviceApi === true,
|
||||
|
||||
// === FOLLOW-UP DETAILS (nur bei Follow-ups) ===
|
||||
followupYearlyVolume: ctx.product?.isFollowUp ? (answers.followupYearlyVolume || null) : null,
|
||||
followupCreateMode: ctx.product?.isFollowUp ? (answers.followupCreateMode || null) : null,
|
||||
followupCreateModeLabel: ctx.product?.isFollowUp ? (
|
||||
answers.followupCreateMode === 'auto' ? 'Automatisch (API)' :
|
||||
(answers.followupCreateMode === 'manual' ? 'Manuell' : null)
|
||||
) : null,
|
||||
followupSourceSystem: ctx.product?.isFollowUp ? (answers.followupSourceSystem || null) : null,
|
||||
followupTriggerDescription: ctx.product?.isFollowUp ? (answers.followupTriggerDescription || null) : null,
|
||||
followupCheckCycle: ctx.product?.isFollowUp ? (answers.followupCheckCycle || null) : null,
|
||||
followupCheckCycleLabel: ctx.product?.isFollowUp ? (
|
||||
answers.followupCheckCycle === 'weekly' ? 'Wöchentlich' :
|
||||
(answers.followupCheckCycle === 'monthly' ? 'Monatlich' :
|
||||
(answers.followupCheckCycle === 'quarterly' ? 'Quartalsweise' : null))
|
||||
) : null,
|
||||
|
||||
// === GUTSCHEIN ===
|
||||
voucherCode: voucherCode,
|
||||
voucherDiscount: voucherDiscount,
|
||||
|
||||
// === PREISE ===
|
||||
currency: quote.currency || 'EUR',
|
||||
subtotalNet: quote.subtotalNet || 0,
|
||||
vatRate: quote.vatRate || 0.19,
|
||||
vatAmount: quote.vatAmount || 0,
|
||||
totalGross: quote.totalGross || 0,
|
||||
priceLines: quote.lines || [],
|
||||
|
||||
// === KUNDENDATEN (Rechnungsadresse) ===
|
||||
billingFirstName: order.billing?.firstName || '',
|
||||
billingLastName: order.billing?.lastName || '',
|
||||
billingCompany: order.billing?.company || '',
|
||||
billingEmail: order.billing?.email || '',
|
||||
billingPhone: order.billing?.phone || '',
|
||||
billingStreet: order.billing?.street || '',
|
||||
billingHouseNumber: order.billing?.houseNumber || '',
|
||||
billingZip: order.billing?.zip || '',
|
||||
billingCity: order.billing?.city || '',
|
||||
billingCountry: order.billing?.country || 'Deutschland',
|
||||
|
||||
// === LIEFERADRESSE (falls abweichend) ===
|
||||
shippingDifferent: order.shippingDifferent || false,
|
||||
shippingFirstName: order.shippingDifferent ? (order.shipping?.firstName || '') : null,
|
||||
shippingLastName: order.shippingDifferent ? (order.shipping?.lastName || '') : null,
|
||||
shippingCompany: order.shippingDifferent ? (order.shipping?.company || '') : null,
|
||||
shippingStreet: order.shippingDifferent ? (order.shipping?.street || '') : null,
|
||||
shippingHouseNumber: order.shippingDifferent ? (order.shipping?.houseNumber || '') : null,
|
||||
shippingZip: order.shippingDifferent ? (order.shipping?.zip || '') : null,
|
||||
shippingCity: order.shippingDifferent ? (order.shipping?.city || '') : null,
|
||||
shippingCountry: order.shippingDifferent ? (order.shipping?.country || 'Deutschland') : null,
|
||||
|
||||
// === EMPFÄNGERLISTE ===
|
||||
addressMode: addressMode,
|
||||
addressModeLabel: addressMode === 'free' ? 'Freie Adresszeilen' : 'Klassische Adresse',
|
||||
recipients: addressMode === 'classic' ? (state.recipientRows || []).map((r, i) => ({
|
||||
index: i,
|
||||
firstName: r?.firstName || '',
|
||||
lastName: r?.lastName || '',
|
||||
street: r?.street || '',
|
||||
houseNumber: r?.houseNumber || '',
|
||||
zip: r?.zip || '',
|
||||
city: r?.city || '',
|
||||
country: r?.country || 'Deutschland',
|
||||
})) : null,
|
||||
recipientsFree: addressMode === 'free' ? (state.freeAddressRows || []).map((r, i) => ({
|
||||
index: i,
|
||||
line1: r?.line1 || '',
|
||||
line2: r?.line2 || '',
|
||||
line3: r?.line3 || '',
|
||||
line4: r?.line4 || '',
|
||||
line5: r?.line5 || '',
|
||||
})) : null,
|
||||
|
||||
// === PLATZHALTER ===
|
||||
placeholdersEnvelope: state.placeholders?.envelope || [],
|
||||
placeholdersLetter: state.placeholders?.letter || [],
|
||||
placeholderValues: state.placeholderValues || {},
|
||||
|
||||
// === BACKEND RESULT (falls vorhanden) ===
|
||||
backendPath: backendResult?.path || null,
|
||||
backendFiles: backendResult?.files || [],
|
||||
backendSummary: backendResult?.summary || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereitet Metadaten für Backend vor
|
||||
*/
|
||||
function prepareOrderMetadata(state) {
|
||||
return {
|
||||
customer: {
|
||||
type: state.answers?.customerType || 'private',
|
||||
firstName: state.order?.firstName || '',
|
||||
lastName: state.order?.lastName || '',
|
||||
company: state.order?.company || '',
|
||||
email: state.order?.email || '',
|
||||
phone: state.order?.phone || '',
|
||||
street: state.order?.street || '',
|
||||
zip: state.order?.zip || '',
|
||||
city: state.order?.city || '',
|
||||
},
|
||||
orderDate: new Date().toISOString(),
|
||||
product: state.ctx?.product?.key || '',
|
||||
quantity: state.answers?.quantity || 1,
|
||||
format: state.answers?.format || 'A4',
|
||||
shippingMode: state.answers?.shippingMode || 'direct',
|
||||
quote: state.quote || {},
|
||||
voucherCode: state.order?.voucherCode || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erweiterte Order-Submit Funktion mit Backend-Integration
|
||||
*/
|
||||
export async function handleOrderSubmitWithBackend(state) {
|
||||
const isB2B = state.answers?.customerType === "business";
|
||||
const backend = window.SkriftConfigurator?.settings?.backend_connection || {};
|
||||
const webhookUrl = backend.order_webhook_url;
|
||||
const redirectUrlBusiness = backend.redirect_url_business;
|
||||
const redirectUrlPrivate = backend.redirect_url_private;
|
||||
const api = window.SkriftBackendAPI;
|
||||
|
||||
// Prüfe ob Backend konfiguriert ist
|
||||
if (!backend.api_url) {
|
||||
console.warn('[Backend Integration] Backend API URL nicht konfiguriert');
|
||||
// Fallback zur alten Logik
|
||||
return handleOrderSubmitLegacy(state);
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Backend Health Check
|
||||
const isHealthy = await api.healthCheck();
|
||||
if (!isHealthy) {
|
||||
throw new Error('Backend ist nicht erreichbar');
|
||||
}
|
||||
|
||||
// 2. Bestellnummer generieren (fortlaufend vom WP-Backend)
|
||||
const orderNumber = await api.generateOrderNumber();
|
||||
|
||||
// 3. Daten vorbereiten
|
||||
const letters = prepareLettersForBackend(state);
|
||||
const envelopes = prepareEnvelopesForBackend(state);
|
||||
const metadata = prepareOrderMetadata(state);
|
||||
|
||||
console.log('[Backend Integration] Generating order:', {
|
||||
orderNumber,
|
||||
letters,
|
||||
envelopes,
|
||||
metadata,
|
||||
});
|
||||
|
||||
// 4. Order im Backend generieren
|
||||
const result = await api.generateOrder(
|
||||
orderNumber,
|
||||
letters,
|
||||
envelopes,
|
||||
metadata
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Order generation failed');
|
||||
}
|
||||
|
||||
console.log('[Backend Integration] Order generated successfully:', result);
|
||||
|
||||
// 5. Gutschein als verwendet markieren (falls vorhanden)
|
||||
const voucherCode = state.order?.voucherStatus?.valid
|
||||
? state.order.voucherCode
|
||||
: null;
|
||||
if (voucherCode) {
|
||||
try {
|
||||
const restUrl = window.SkriftConfigurator?.restUrl || "/wp-json/";
|
||||
await fetch(restUrl + "skrift/v1/voucher/use", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ code: voucherCode }),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[Backend Integration] Fehler beim Markieren des Gutscheins:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Webhook aufrufen (wenn konfiguriert)
|
||||
if (webhookUrl) {
|
||||
try {
|
||||
const webhookData = buildWebhookData(state, result);
|
||||
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(webhookData),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn('[Backend Integration] Webhook call failed:', response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[Backend Integration] Webhook error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Weiterleitung
|
||||
if (isB2B) {
|
||||
if (redirectUrlBusiness) {
|
||||
// Bestellnummer als Query-Parameter anhängen
|
||||
const redirectUrl = new URL(redirectUrlBusiness);
|
||||
redirectUrl.searchParams.set('orderNumber', result.orderNumber);
|
||||
window.location.href = redirectUrl.toString();
|
||||
} else {
|
||||
alert(
|
||||
`Vielen Dank für Ihre Bestellung!\n\nBestellnummer: ${result.orderNumber}\n\nSie erhalten in Kürze eine Bestätigungs-E-Mail mit allen Details.`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Privatkunde: Zu PayPal weiterleiten
|
||||
if (redirectUrlPrivate) {
|
||||
const redirectUrl = new URL(redirectUrlPrivate);
|
||||
redirectUrl.searchParams.set('orderNumber', result.orderNumber);
|
||||
window.location.href = redirectUrl.toString();
|
||||
} else {
|
||||
alert(
|
||||
`Bestellung erfolgreich erstellt!\n\nBestellnummer: ${result.orderNumber}\n\nWeiterleitung zu PayPal folgt...`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Backend Integration] Order submission failed:', error);
|
||||
|
||||
alert(
|
||||
`Fehler bei der Bestellverarbeitung:\n\n${error.message}\n\nBitte versuchen Sie es erneut oder kontaktieren Sie uns.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy Order-Submit (Fallback ohne Backend)
|
||||
*/
|
||||
async function handleOrderSubmitLegacy(state) {
|
||||
const isB2B = state.answers?.customerType === "business";
|
||||
const backend = window.SkriftConfigurator?.settings?.backend_connection || {};
|
||||
const webhookUrl = backend.order_webhook_url;
|
||||
const redirectUrlBusiness = backend.redirect_url_business;
|
||||
const redirectUrlPrivate = backend.redirect_url_private;
|
||||
|
||||
// Gutschein als verwendet markieren
|
||||
const voucherCode = state.order?.voucherStatus?.valid
|
||||
? state.order.voucherCode
|
||||
: null;
|
||||
if (voucherCode) {
|
||||
try {
|
||||
const restUrl = window.SkriftConfigurator?.restUrl || "/wp-json/";
|
||||
await fetch(restUrl + "skrift/v1/voucher/use", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ code: voucherCode }),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Fehler beim Markieren des Gutscheins:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Webhook aufrufen
|
||||
if (isB2B && webhookUrl) {
|
||||
try {
|
||||
const webhookData = buildWebhookData(state, null);
|
||||
|
||||
await fetch(webhookUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(webhookData),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Webhook error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Weiterleitung
|
||||
if (isB2B && redirectUrlBusiness) {
|
||||
window.location.href = redirectUrlBusiness;
|
||||
} else if (!isB2B && redirectUrlPrivate) {
|
||||
window.location.href = redirectUrlPrivate;
|
||||
} else {
|
||||
alert("Vielen Dank für Ihre Bestellung!");
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
handleOrderSubmitWithBackend,
|
||||
prepareLettersForBackend,
|
||||
prepareEnvelopesForBackend,
|
||||
mapFontToBackend,
|
||||
mapFormatToBackend,
|
||||
buildWebhookData,
|
||||
};
|
||||
622
skrift-configurator/assets/js/configurator-preview-manager.js
Normal file
622
skrift-configurator/assets/js/configurator-preview-manager.js
Normal file
@@ -0,0 +1,622 @@
|
||||
/**
|
||||
* Preview Manager - Verwaltet Preview-Generation mit Batch-Loading, Navigation und Rate-Limiting
|
||||
*/
|
||||
|
||||
import { preparePlaceholdersForIndex } from './configurator-utils.js';
|
||||
|
||||
class PreviewManager {
|
||||
constructor(api) {
|
||||
this.api = api;
|
||||
this.currentBatchIndex = 0;
|
||||
this.currentDocIndex = 0;
|
||||
this.previewCount = 0;
|
||||
this.batchSize = 25;
|
||||
this.currentBatchPreviews = [];
|
||||
this.requestsRemaining = 10;
|
||||
this.maxRequests = 10;
|
||||
// Für Änderungserkennung und Validierungs-Caching
|
||||
this.lastValidatedTextHash = null;
|
||||
this.lastValidationResult = null;
|
||||
this.lastOverflowFiles = null; // null = keine Validierung durchgeführt
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt einen einfachen Hash für Text-Vergleich
|
||||
*/
|
||||
hashText(text, quantity, format, font) {
|
||||
const str = `${text || ''}|${quantity || 1}|${format || 'a4'}|${font || 'tilda'}`;
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return hash.toString(36);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob sich der Text seit der letzten Validierung geändert hat
|
||||
*/
|
||||
hasTextChanged(state) {
|
||||
const currentHash = this.hashText(
|
||||
state.answers?.letterText,
|
||||
state.answers?.quantity,
|
||||
state.answers?.format,
|
||||
state.answers?.font
|
||||
);
|
||||
return currentHash !== this.lastValidatedTextHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Speichert den aktuellen Text-Hash nach Validierung
|
||||
*/
|
||||
saveTextHash(state) {
|
||||
this.lastValidatedTextHash = this.hashText(
|
||||
state.answers?.letterText,
|
||||
state.answers?.quantity,
|
||||
state.answers?.format,
|
||||
state.answers?.font
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereitet Platzhalter für ein bestimmtes Dokument vor
|
||||
* Verwendet jetzt die gemeinsame Utility-Funktion
|
||||
*/
|
||||
preparePlaceholders(state, index) {
|
||||
return preparePlaceholdersForIndex(state, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert ein Letter-Objekt für ein bestimmtes Dokument
|
||||
*/
|
||||
prepareLetter(state, index, isEnvelope = false) {
|
||||
const placeholders = this.preparePlaceholders(state, index);
|
||||
|
||||
if (isEnvelope) {
|
||||
const isRecipientMode = state.answers?.envelopeMode === 'recipientData';
|
||||
const isCustomMode = state.answers?.envelopeMode === 'customText';
|
||||
const addressMode = state.addressMode || 'classic';
|
||||
|
||||
let envelopeText = '';
|
||||
let envelopeType = 'recipient';
|
||||
|
||||
if (isRecipientMode) {
|
||||
if (addressMode === 'free' && Array.isArray(state.freeAddressRows) && state.freeAddressRows.length > index) {
|
||||
// Freie Adresse: Bis zu 5 Zeilen
|
||||
const freeAddr = state.freeAddressRows[index];
|
||||
const lines = [];
|
||||
if (freeAddr.line1) lines.push(freeAddr.line1);
|
||||
if (freeAddr.line2) lines.push(freeAddr.line2);
|
||||
if (freeAddr.line3) lines.push(freeAddr.line3);
|
||||
if (freeAddr.line4) lines.push(freeAddr.line4);
|
||||
if (freeAddr.line5) lines.push(freeAddr.line5);
|
||||
envelopeText = lines.join('\n');
|
||||
envelopeType = 'free';
|
||||
} else if (addressMode === 'classic' && Array.isArray(state.recipientRows) && state.recipientRows.length > index) {
|
||||
// Klassische Adresse
|
||||
const recipient = state.recipientRows[index];
|
||||
const lines = [];
|
||||
|
||||
const fullName = `${recipient.firstName || ''} ${recipient.lastName || ''}`.trim();
|
||||
if (fullName) lines.push(fullName);
|
||||
|
||||
const streetLine = `${recipient.street || ''} ${recipient.houseNumber || ''}`.trim();
|
||||
if (streetLine) lines.push(streetLine);
|
||||
|
||||
const location = `${recipient.zip || ''} ${recipient.city || ''}`.trim();
|
||||
if (location) lines.push(location);
|
||||
|
||||
if (recipient.country && recipient.country !== 'Deutschland') {
|
||||
lines.push(recipient.country);
|
||||
}
|
||||
|
||||
envelopeText = lines.join('\n');
|
||||
envelopeType = 'recipient';
|
||||
}
|
||||
} else if (isCustomMode) {
|
||||
envelopeText = state.answers?.envelopeCustomText || '';
|
||||
envelopeType = 'custom';
|
||||
}
|
||||
|
||||
return {
|
||||
index: index,
|
||||
text: envelopeText,
|
||||
font: state.answers?.envelopeFont || 'tilda',
|
||||
format: state.answers?.format === 'a4' ? 'DIN_LANG' : 'C6',
|
||||
placeholders: placeholders,
|
||||
type: 'envelope',
|
||||
envelopeType: envelopeType,
|
||||
envelope: {
|
||||
type: envelopeType
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
index: index,
|
||||
text: state.answers?.letterText || state.answers?.text || state.answers?.briefText || '',
|
||||
font: state.answers?.font || 'tilda',
|
||||
format: state.answers?.format || 'a4',
|
||||
placeholders: placeholders,
|
||||
type: 'letter'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt ALLE Previews auf einmal
|
||||
* @param {boolean} skipLimitCheck - Wenn true, wird das Request-Limit ignoriert (für Validierung)
|
||||
*/
|
||||
async loadAllPreviews(state, isEnvelope = false, skipLimitCheck = false) {
|
||||
// Request-Limit nur prüfen wenn nicht übersprungen (normale Preview-Generierung)
|
||||
if (!skipLimitCheck) {
|
||||
if (this.requestsRemaining <= 0) {
|
||||
throw new Error(`Maximale Anzahl von ${this.maxRequests} Vorschau-Anfragen erreicht.`);
|
||||
}
|
||||
this.requestsRemaining--;
|
||||
}
|
||||
|
||||
const letters = [];
|
||||
for (let i = 0; i < this.previewCount; i++) {
|
||||
letters.push(this.prepareLetter(state, i, isEnvelope));
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.api.generatePreviewBatch(letters);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Preview generation failed');
|
||||
}
|
||||
|
||||
this.currentBatchPreviews = result.previews;
|
||||
this.lastValidationResult = result; // Speichere für Overflow-Check
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('[PreviewManager] Load error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Textlänge durch Preview-Generierung
|
||||
* Wenn Text nicht geändert wurde und Preview bereits existiert, wird gecachtes Ergebnis verwendet
|
||||
* @returns {Object} { valid: boolean, overflowFiles: Array, fromCache: boolean }
|
||||
*/
|
||||
async validateTextLength(state, forceRevalidate = false) {
|
||||
// Prüfe ob wir gecachtes Ergebnis verwenden können
|
||||
if (!forceRevalidate && !this.hasTextChanged(state) && this.lastOverflowFiles !== null) {
|
||||
console.log('[PreviewManager] Using cached validation result');
|
||||
return {
|
||||
valid: this.lastOverflowFiles.length === 0,
|
||||
overflowFiles: this.lastOverflowFiles,
|
||||
fromCache: true
|
||||
};
|
||||
}
|
||||
|
||||
this.previewCount = parseInt(state.answers?.quantity) || 1;
|
||||
|
||||
try {
|
||||
// skipLimitCheck = true: Validierung soll immer möglich sein, auch ohne verbleibende Anfragen
|
||||
await this.loadAllPreviews(state, false, true);
|
||||
|
||||
const result = this.lastValidationResult;
|
||||
if (!result) {
|
||||
this.lastOverflowFiles = [];
|
||||
this.saveTextHash(state);
|
||||
return { valid: true, overflowFiles: [], fromCache: false };
|
||||
}
|
||||
|
||||
const hasOverflow = result.hasOverflow || false;
|
||||
const overflowFiles = (result.overflowFiles || []).map(f => ({
|
||||
index: f.index,
|
||||
lineCount: f.lineCount,
|
||||
lineLimit: f.lineLimit
|
||||
}));
|
||||
|
||||
// Cache das Ergebnis
|
||||
this.lastOverflowFiles = overflowFiles;
|
||||
this.saveTextHash(state);
|
||||
|
||||
return {
|
||||
valid: !hasOverflow,
|
||||
overflowFiles: overflowFiles,
|
||||
fromCache: false
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[PreviewManager] Validation error:', error);
|
||||
|
||||
// Bei Fehlern: Nicht durchlassen - Nutzer muss es erneut versuchen
|
||||
return {
|
||||
valid: false,
|
||||
overflowFiles: [],
|
||||
error: error.message,
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt gecachte Overflow-Infos zurück (ohne neue Anfrage)
|
||||
*/
|
||||
getCachedOverflowFiles() {
|
||||
return this.lastOverflowFiles || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Validierung bereits erfolgt ist (für UI-Anzeige)
|
||||
*/
|
||||
hasValidationResult() {
|
||||
return this.lastOverflowFiles !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Previews und zeigt Overflow-Warnung wenn nötig
|
||||
* @returns {Object} { success: boolean, hasOverflow: boolean, overflowFiles: Array }
|
||||
*/
|
||||
async generatePreviews(state, dom, isEnvelope = false) {
|
||||
const btn = dom.querySelector('.sk-preview-generate-btn');
|
||||
const statusEl = dom.querySelector('.sk-preview-status');
|
||||
const requestCounterEl = dom.querySelector('.sk-preview-request-counter');
|
||||
|
||||
if (!btn) return { success: false, hasOverflow: false, overflowFiles: [] };
|
||||
|
||||
try {
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generiere Vorschau...';
|
||||
|
||||
this.previewCount = parseInt(state.answers?.quantity) || 1;
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Lade alle ${this.previewCount} Dokumente...`;
|
||||
}
|
||||
|
||||
await this.loadAllPreviews(state, isEnvelope);
|
||||
|
||||
// Text-Hash speichern für Änderungserkennung
|
||||
if (!isEnvelope) {
|
||||
this.saveTextHash(state);
|
||||
}
|
||||
|
||||
this.currentDocIndex = 0;
|
||||
this.showPreview(0, dom);
|
||||
this.showNavigationControls(dom, state, isEnvelope);
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Dokument 1 von ${this.previewCount}`;
|
||||
}
|
||||
|
||||
if (requestCounterEl) {
|
||||
requestCounterEl.textContent = `Verbleibende Vorschau-Anfragen: ${this.requestsRemaining}/${this.maxRequests}`;
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.textContent.includes('Umschlag') ? 'Umschlag Vorschau generieren' : 'Vorschau Schriftstück generieren';
|
||||
|
||||
// Overflow-Prüfung für Briefe (nicht Umschläge)
|
||||
if (!isEnvelope && this.lastValidationResult) {
|
||||
const hasOverflow = this.lastValidationResult.hasOverflow || false;
|
||||
const overflowFiles = (this.lastValidationResult.overflowFiles || []).map(f => ({
|
||||
index: f.index,
|
||||
lineCount: f.lineCount,
|
||||
lineLimit: f.lineLimit
|
||||
}));
|
||||
|
||||
// Cache speichern
|
||||
this.lastOverflowFiles = overflowFiles;
|
||||
|
||||
return { success: true, hasOverflow, overflowFiles };
|
||||
}
|
||||
|
||||
return { success: true, hasOverflow: false, overflowFiles: [] };
|
||||
|
||||
} catch (error) {
|
||||
console.error('[PreviewManager] Error:', error);
|
||||
btn.disabled = false;
|
||||
btn.textContent = isEnvelope ? 'Umschlag Vorschau generieren' : 'Vorschau generieren';
|
||||
|
||||
// Fehlermeldung im Preview-Bereich anzeigen
|
||||
const previewBox = dom.querySelector('.sk-preview-box');
|
||||
if (previewBox) {
|
||||
previewBox.innerHTML = '';
|
||||
const notice = document.createElement('div');
|
||||
notice.style.cssText = 'padding: 20px; text-align: center; color: #666;';
|
||||
notice.innerHTML = `
|
||||
<p style="margin-bottom: 15px;">Die Vorschau konnte nicht generiert werden.</p>
|
||||
<p style="color: #999; font-size: 13px;">Bitte versuchen Sie es erneut oder kontaktieren Sie uns bei anhaltenden Problemen.</p>
|
||||
`;
|
||||
previewBox.appendChild(notice);
|
||||
}
|
||||
|
||||
return { success: false, hasOverflow: false, overflowFiles: [], error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt eine Preview an einem bestimmten Index
|
||||
*/
|
||||
showPreview(index, dom) {
|
||||
const preview = this.currentBatchPreviews[index];
|
||||
if (!preview) {
|
||||
console.warn('[PreviewManager] Preview not loaded:', index);
|
||||
return;
|
||||
}
|
||||
|
||||
const previewBox = dom.querySelector('.sk-preview-box');
|
||||
const statusEl = dom.querySelector('.sk-preview-status');
|
||||
|
||||
if (!previewBox) {
|
||||
console.warn('[PreviewManager] Preview box not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.style.cssText = 'position: relative; overflow: hidden; margin-top: 15px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer;';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = `${this.api.baseURL}${preview.url}?t=${Date.now()}`;
|
||||
img.style.cssText = 'width: 100%; display: block;';
|
||||
|
||||
imgContainer.addEventListener('click', () => {
|
||||
this.showFullscreenPreview(img.src);
|
||||
});
|
||||
|
||||
imgContainer.addEventListener('mouseenter', () => {
|
||||
imgContainer.style.opacity = '0.9';
|
||||
});
|
||||
|
||||
imgContainer.addEventListener('mouseleave', () => {
|
||||
imgContainer.style.opacity = '1';
|
||||
});
|
||||
|
||||
imgContainer.appendChild(img);
|
||||
previewBox.innerHTML = '';
|
||||
previewBox.appendChild(imgContainer);
|
||||
|
||||
if (statusEl) {
|
||||
statusEl.textContent = `Dokument ${index + 1} von ${this.previewCount}`;
|
||||
}
|
||||
|
||||
this.currentDocIndex = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigiert zur nächsten/vorherigen Preview
|
||||
*/
|
||||
navigateWithinBatch(direction, dom) {
|
||||
const newIndex = this.currentDocIndex + direction;
|
||||
|
||||
if (newIndex < 0 || newIndex >= this.currentBatchPreviews.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showPreview(newIndex, dom);
|
||||
this.updateNavigationButtons();
|
||||
}
|
||||
|
||||
// navigateToBatch wurde entfernt - Batch-Loading wird nicht mehr verwendet
|
||||
// Alle Previews werden jetzt auf einmal geladen (loadAllPreviews)
|
||||
|
||||
/**
|
||||
* Ermittelt den aktuellen lokalen Index
|
||||
*/
|
||||
getCurrentLocalIndex(dom) {
|
||||
const statusEl = dom.querySelector('.sk-preview-status');
|
||||
if (!statusEl) return 0;
|
||||
|
||||
const match = statusEl.textContent.match(/Dokument (\d+) von/);
|
||||
if (match) {
|
||||
return parseInt(match[1]) - 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert den Request Counter im DOM
|
||||
*/
|
||||
updateRequestCounter(dom) {
|
||||
const requestCounterEl = dom.querySelector('.sk-preview-request-counter');
|
||||
if (requestCounterEl) {
|
||||
requestCounterEl.textContent = `Verbleibende Vorschau-Anfragen: ${this.requestsRemaining}/${this.maxRequests}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert Button-States der Navigation
|
||||
*/
|
||||
updateNavigationButtons() {
|
||||
const navWrapper = document.querySelector('.sk-preview-navigation-container');
|
||||
if (!navWrapper) return;
|
||||
|
||||
const navContainer = navWrapper.querySelector('.sk-preview-navigation');
|
||||
if (!navContainer) return;
|
||||
|
||||
const prevDocBtn = navContainer.querySelector('.sk-preview-prev-doc');
|
||||
const nextDocBtn = navContainer.querySelector('.sk-preview-next-doc');
|
||||
|
||||
if (prevDocBtn) {
|
||||
const canPrev = this.currentDocIndex > 0;
|
||||
prevDocBtn.disabled = !canPrev;
|
||||
prevDocBtn.style.opacity = canPrev ? '1' : '0.5';
|
||||
prevDocBtn.style.cursor = canPrev ? 'pointer' : 'not-allowed';
|
||||
}
|
||||
|
||||
if (nextDocBtn) {
|
||||
const canNext = this.currentDocIndex < this.currentBatchPreviews.length - 1;
|
||||
nextDocBtn.disabled = !canNext;
|
||||
nextDocBtn.style.opacity = canNext ? '1' : '0.5';
|
||||
nextDocBtn.style.cursor = canNext ? 'pointer' : 'not-allowed';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Navigation Controls
|
||||
*/
|
||||
showNavigationControls(dom, state, isEnvelope) {
|
||||
const navWrapper = dom.querySelector('.sk-preview-navigation-container');
|
||||
if (!navWrapper) return;
|
||||
|
||||
let navContainer = navWrapper.querySelector('.sk-preview-navigation');
|
||||
if (navContainer) {
|
||||
navContainer.remove();
|
||||
}
|
||||
|
||||
navContainer = document.createElement('div');
|
||||
navContainer.className = 'sk-preview-navigation';
|
||||
navContainer.style.display = 'flex';
|
||||
navContainer.style.gap = '10px';
|
||||
navContainer.style.alignItems = 'center';
|
||||
|
||||
const buttonStyle = {
|
||||
padding: '8px 12px',
|
||||
fontSize: '20px',
|
||||
lineHeight: '1',
|
||||
minWidth: '40px',
|
||||
backgroundColor: '#fff',
|
||||
border: '1px solid #ddd',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
};
|
||||
|
||||
const prevDocBtn = document.createElement('button');
|
||||
prevDocBtn.type = 'button';
|
||||
prevDocBtn.className = 'sk-btn sk-btn-secondary sk-preview-prev-doc';
|
||||
prevDocBtn.textContent = '‹';
|
||||
prevDocBtn.title = 'Vorheriges Dokument';
|
||||
Object.assign(prevDocBtn.style, buttonStyle);
|
||||
prevDocBtn.onclick = () => {
|
||||
this.navigateWithinBatch(-1, dom);
|
||||
};
|
||||
|
||||
const nextDocBtn = document.createElement('button');
|
||||
nextDocBtn.type = 'button';
|
||||
nextDocBtn.className = 'sk-btn sk-btn-secondary sk-preview-next-doc';
|
||||
nextDocBtn.textContent = '›';
|
||||
nextDocBtn.title = 'Nächstes Dokument';
|
||||
Object.assign(nextDocBtn.style, buttonStyle);
|
||||
nextDocBtn.onclick = () => {
|
||||
this.navigateWithinBatch(1, dom);
|
||||
};
|
||||
|
||||
navContainer.appendChild(prevDocBtn);
|
||||
navContainer.appendChild(nextDocBtn);
|
||||
|
||||
navWrapper.appendChild(navContainer);
|
||||
this.updateNavigationButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyboard Event Handler
|
||||
*/
|
||||
handleKeyboardNavigation(event, state, dom, isEnvelope = false) {
|
||||
if (this.currentBatchPreviews.length === 0) return;
|
||||
|
||||
const currentLocalIndex = this.getCurrentLocalIndex(dom);
|
||||
|
||||
if (event.key === 'ArrowLeft' && currentLocalIndex > 0) {
|
||||
event.preventDefault();
|
||||
this.navigateWithinBatch(-1, dom);
|
||||
} else if (event.key === 'ArrowRight' && currentLocalIndex < this.currentBatchPreviews.length - 1) {
|
||||
event.preventDefault();
|
||||
this.navigateWithinBatch(1, dom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup
|
||||
*/
|
||||
destroy() {
|
||||
this.currentBatchPreviews = [];
|
||||
this.currentBatchIndex = 0;
|
||||
this.previewCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset für neue Session
|
||||
*/
|
||||
reset() {
|
||||
this.currentBatchPreviews = [];
|
||||
this.currentBatchIndex = 0;
|
||||
this.previewCount = 0;
|
||||
this.requestsRemaining = this.maxRequests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeigt Fullscreen-Vorschau als Modal
|
||||
*/
|
||||
showFullscreenPreview(imgSrc) {
|
||||
const existingModal = document.getElementById('sk-preview-fullscreen-modal');
|
||||
if (existingModal) {
|
||||
existingModal.remove();
|
||||
}
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'sk-preview-fullscreen-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const modalImg = document.createElement('img');
|
||||
modalImg.src = imgSrc;
|
||||
modalImg.style.cssText = `
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
`;
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.innerHTML = '×';
|
||||
closeBtn.style.cssText = `
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 30px;
|
||||
font-size: 40px;
|
||||
color: white;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
`;
|
||||
|
||||
const hint = document.createElement('div');
|
||||
hint.textContent = 'Klicken zum Schließen';
|
||||
hint.style.cssText = `
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
modal.appendChild(modalImg);
|
||||
modal.appendChild(closeBtn);
|
||||
modal.appendChild(hint);
|
||||
|
||||
const closeModal = () => modal.remove();
|
||||
modal.addEventListener('click', closeModal);
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeModal();
|
||||
}, { once: true });
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
}
|
||||
|
||||
// Globale Instanzen
|
||||
window.envelopePreviewManager = null;
|
||||
window.contentPreviewManager = null;
|
||||
|
||||
export default PreviewManager;
|
||||
1035
skrift-configurator/assets/js/configurator-pricing.js
Normal file
1035
skrift-configurator/assets/js/configurator-pricing.js
Normal file
File diff suppressed because it is too large
Load Diff
1030
skrift-configurator/assets/js/configurator-state.js
Normal file
1030
skrift-configurator/assets/js/configurator-state.js
Normal file
File diff suppressed because it is too large
Load Diff
5446
skrift-configurator/assets/js/configurator-ui.js
Normal file
5446
skrift-configurator/assets/js/configurator-ui.js
Normal file
File diff suppressed because it is too large
Load Diff
96
skrift-configurator/assets/js/configurator-utils.js
Normal file
96
skrift-configurator/assets/js/configurator-utils.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Gemeinsame Utility-Funktionen für Skrift Konfigurator
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bereitet Platzhalter für einen bestimmten Index vor
|
||||
* Wird von PreviewManager und Backend-Integration verwendet
|
||||
*/
|
||||
export function preparePlaceholdersForIndex(state, index) {
|
||||
const placeholders = {};
|
||||
|
||||
// Prüfen ob strukturierte Empfängerdaten (klassische Adresse) im aktuellen Flow verwendet werden:
|
||||
// - Adressmodus muss 'classic' sein (bei 'free' gibt es keine Felder wie vorname, name etc.)
|
||||
// - UND: Direktversand ODER Kuvert mit Empfängeradresse
|
||||
const isClassicAddress = (state.addressMode || 'classic') === 'classic';
|
||||
const needsRecipientData = isClassicAddress && (
|
||||
state.answers?.shippingMode === 'direct' ||
|
||||
(state.answers?.envelope === true && state.answers?.envelopeMode === 'recipientData')
|
||||
);
|
||||
|
||||
// Empfänger-Feldnamen, die aus recipientRows kommen können
|
||||
const recipientFields = ['vorname', 'name', 'ort', 'strasse', 'hausnummer', 'plz', 'land'];
|
||||
|
||||
// Platzhalter aus placeholderValues extrahieren
|
||||
if (state.placeholderValues) {
|
||||
for (const [name, values] of Object.entries(state.placeholderValues)) {
|
||||
// Empfängerfelder nur überspringen wenn sie aus recipientRows kommen
|
||||
if (needsRecipientData && recipientFields.includes(name)) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(values) && values.length > index) {
|
||||
placeholders[name] = values[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empfängerdaten aus recipientRows hinzufügen (nur wenn benötigt)
|
||||
if (needsRecipientData && Array.isArray(state.recipientRows) && state.recipientRows.length > index) {
|
||||
const recipient = state.recipientRows[index];
|
||||
placeholders['vorname'] = recipient.firstName || '';
|
||||
placeholders['name'] = recipient.lastName || '';
|
||||
placeholders['ort'] = recipient.city || '';
|
||||
placeholders['strasse'] = recipient.street || '';
|
||||
placeholders['hausnummer'] = recipient.houseNumber || '';
|
||||
placeholders['plz'] = recipient.zip || '';
|
||||
placeholders['land'] = recipient.country || '';
|
||||
}
|
||||
|
||||
return placeholders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Empfängerzeilen (gemeinsame Logik für klassische und freie Adressen)
|
||||
*/
|
||||
export function validateRecipientRows(state, requiredCount) {
|
||||
const addressMode = state.addressMode || 'classic';
|
||||
|
||||
if (addressMode === 'free') {
|
||||
// Freie Adresse: mindestens Zeile 1 muss ausgefüllt sein
|
||||
const rows = state.freeAddressRows || [];
|
||||
if (rows.length !== requiredCount) 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 !== requiredCount) return false;
|
||||
|
||||
const required = [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"street",
|
||||
"houseNumber",
|
||||
"zip",
|
||||
"city",
|
||||
"country",
|
||||
];
|
||||
|
||||
for (const r of rows) {
|
||||
if (!r) return false;
|
||||
for (const k of required) {
|
||||
if (!String(r[k] || "").trim()) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {
|
||||
preparePlaceholdersForIndex,
|
||||
validateRecipientRows,
|
||||
};
|
||||
1145
skrift-configurator/assets/js/price-calculator.js
Normal file
1145
skrift-configurator/assets/js/price-calculator.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user