Files
Skrift-Kofnigurator/skrift-configurator/assets/js/configurator-api.js
2026-02-07 13:04:04 +01:00

393 lines
12 KiB
JavaScript

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