/** * 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;