/** * 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 = `
Die Vorschau konnte nicht generiert werden.
Bitte versuchen Sie es erneut oder kontaktieren Sie uns bei anhaltenden Problemen.
`; 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;