Initial commit
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user