393 lines
12 KiB
JavaScript
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;
|