/** * Transformer Calculators * Version 0.9.6 * * Handles: * * - input values * - slider outputs * - model availability * - energy calculations * - savings comparison * - dynamic model sorting * - best-model highlighting * - PDF generation and download * - localized PDF requests * - PDF rate-limit error handling */ (function () { 'use strict'; const DAYS_PER_YEAR = 365; const HOURS_PER_DAY = 24; const BASELINE_MODEL_KEY = 'old-din'; /** * Safely converts a value to a finite number. * * @param {*} value * @param {number} fallback * @returns {number} */ function toNumber(value, fallback = 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } /** * Replaces the first %s placeholder. * * @param {string} template * @param {string|number} value * @returns {string} */ function replacePlaceholder(template, value) { return String(template).replace( '%s', String(value) ); } /** * Returns current frontend language. * * Examples: * sv * en * no * nb * nn * * @returns {string} */ function getLanguage() { const htmlLanguage = document.documentElement.lang || 'sv'; return String(htmlLanguage) .toLowerCase() .replace('_', '-') .substring(0, 2); } /** * Returns current frontend locale. * * WPML normally changes the lang attribute * on the HTML element. * * @returns {string} */ function getLocale() { const htmlLanguage = document.documentElement.lang || 'en'; return htmlLanguage.replace( '_', '-' ); } /** * Formats a number according to current language. * * @param {number} value * @param {number} maximumFractionDigits * @returns {string} */ function formatNumber( value, maximumFractionDigits = 0 ) { return new Intl.NumberFormat( getLocale(), { minimumFractionDigits: 0, maximumFractionDigits: maximumFractionDigits } ).format(value); } /** * Reads calculator JSON data. * * @param {HTMLElement} calculator * @returns {Object|null} */ function readCalculatorData(calculator) { const dataElement = calculator.querySelector( '[data-tc-data]' ); if (!dataElement) { console.error( 'Transformer Calculators: JSON data element is missing.' ); return null; } try { return JSON.parse( dataElement.textContent ); } catch (error) { console.error( 'Transformer Calculators: JSON data could not be parsed.', error ); return null; } } /** * Get one calculator input. * * @param {HTMLElement} calculator * @param {string} name * @returns {HTMLInputElement|HTMLSelectElement|null} */ function getInput(calculator, name) { return calculator.querySelector( `[data-tc-input="${name}"]` ); } /** * Reads current calculator inputs. * * @param {HTMLElement} calculator * @returns {Object} */ function readInputs(calculator) { const powerInput = getInput( calculator, 'power' ); const lifetimeInput = getInput( calculator, 'lifetime' ); const hoursInput = getInput( calculator, 'hours' ); const loadInput = getInput( calculator, 'load' ); const priceInput = getInput( calculator, 'price' ); return { power: powerInput ? String( powerInput.value ) : '', lifetimeYears: lifetimeInput ? Math.min( 60, Math.max( 1, toNumber( lifetimeInput.value, 40 ) ) ) : 40, hoursPerDay: hoursInput ? Math.min( 24, Math.max( 1, toNumber( hoursInput.value, 24 ) ) ) : 24, loadPercent: loadInput ? Math.min( 100, Math.max( 0, toNumber( loadInput.value, 70 ) ) ) : 70, priceMinorPerKwh: priceInput ? Math.max( 0, toNumber( priceInput.value, 100 ) ) : 100 }; } /** * Updates displayed slider values. * * @param {HTMLElement} calculator * @param {Object} inputs * @returns {void} */ function updateInputOutputs( calculator, inputs ) { const translations = calculator.querySelector( '.tc-calculator__translations' ); if (!translations) { return; } const lifetimeOutput = calculator.querySelector( '[data-tc-output="lifetime"]' ); const hoursOutput = calculator.querySelector( '[data-tc-output="hours"]' ); const loadOutput = calculator.querySelector( '[data-tc-output="load"]' ); if (lifetimeOutput) { const lifetimeTemplate = inputs.lifetimeYears === 1 ? translations.dataset .yearSingular : translations.dataset .yearPlural; lifetimeOutput.textContent = replacePlaceholder( lifetimeTemplate, formatNumber( inputs.lifetimeYears ) ); } if (hoursOutput) { const hoursTemplate = inputs.hoursPerDay === 1 ? translations.dataset .hourSingular : translations.dataset .hourPlural; hoursOutput.textContent = replacePlaceholder( hoursTemplate, formatNumber( inputs.hoursPerDay ) ); } if (loadOutput) { loadOutput.textContent = `${formatNumber( inputs.loadPercent )}%`; } } /** * Calculates all energy and cost results. * * Annual no-load energy: * no-load loss × 24 × 365 / 1000 * * Annual load energy: * load loss × load factor² × operating hours × 365 / 1000 * * @param {Object} modelData * @param {Object} inputs * @param {Object} currencyData * @returns {Object} */ function calculateResults( modelData, inputs, currencyData ) { const noLoadLossWatts = toNumber( modelData.no_load_loss ); const loadLossWatts = toNumber( modelData.load_loss ); const loadFactor = inputs.loadPercent / 100; const minorPerUnit = currencyData && Number.isFinite( Number( currencyData.minor_per_unit ) ) ? Number( currencyData.minor_per_unit ) : 100; const pricePerKwh = inputs.priceMinorPerKwh / minorPerUnit; const annualNoLoadEnergy = ( noLoadLossWatts * HOURS_PER_DAY * DAYS_PER_YEAR ) / 1000; const annualLoadEnergy = ( loadLossWatts * Math.pow( loadFactor, 2 ) * inputs.hoursPerDay * DAYS_PER_YEAR ) / 1000; const annualEnergy = annualNoLoadEnergy + annualLoadEnergy; const annualCost = annualEnergy * pricePerKwh; const lifetimeCost = annualCost * inputs.lifetimeYears; return { noLoadLoss: noLoadLossWatts, loadLoss: loadLossWatts, annualNoLoadEnergy: annualNoLoadEnergy, annualLoadEnergy: annualLoadEnergy, annualEnergy: annualEnergy, annualCost: annualCost, lifetimeCost: lifetimeCost, lifetimeSavings: 0, savingsPercent: 0 }; } /** * Sets result text inside a model card. * * @param {HTMLElement} card * @param {string} key * @param {string} value * @returns {void} */ function setResult( card, key, value ) { const element = card.querySelector( `[data-tc-result="${key}"]` ); if (element) { element.textContent = value; } } /** * Shows or hides unavailable state. * * When a model is unavailable for the selected * power rating, all previously calculated values * are replaced with an en dash. * * @param {HTMLElement} card * @param {boolean} unavailable * @returns {void} */ function setUnavailable( card, unavailable ) { const unavailableMessage = card.querySelector( '[data-tc-unavailable]' ); const values = card.querySelector( '[data-tc-values]' ); const bestBadge = card.querySelector( '[data-tc-best-badge]' ); card.classList.toggle( 'tc-result-card--unavailable', unavailable ); if (unavailableMessage) { unavailableMessage.hidden = !unavailable; } if (values) { values.hidden = false; } if (unavailable) { const resultElements = card.querySelectorAll( '[data-tc-result]' ); resultElements.forEach( (element) => { element.textContent = '–'; } ); card.classList.remove( 'tc-result-card--best' ); if (bestBadge) { bestBadge.hidden = true; } } } /** * Marks or unmarks best model. * * @param {HTMLElement} card * @param {boolean} isBest * @returns {void} */ function setBestModel( card, isBest ) { const badge = card.querySelector( '[data-tc-best-badge]' ); card.classList.toggle( 'tc-result-card--best', isBest ); if (badge) { badge.hidden = !isBest; } } /** * Writes calculated results to one card. * * @param {HTMLElement} card * @param {Object} results * @returns {void} */ function writeModelResults( card, results ) { setResult( card, 'no-load-loss', formatNumber( results.noLoadLoss ) ); setResult( card, 'load-loss', formatNumber( results.loadLoss ) ); setResult( card, 'annual-no-load-energy', formatNumber( results.annualNoLoadEnergy ) ); setResult( card, 'annual-load-energy', formatNumber( results.annualLoadEnergy ) ); setResult( card, 'annual-energy', formatNumber( results.annualEnergy ) ); setResult( card, 'annual-cost', formatNumber( results.annualCost ) ); setResult( card, 'lifetime-savings', formatNumber( results.lifetimeSavings ) ); setResult( card, 'savings-percent', formatNumber( results.savingsPercent, 1 ) ); setResult( card, 'lifetime-cost', formatNumber( results.lifetimeCost ) ); } /** * Sort model cards dynamically. * * Rules: * * 1. Available models first. * 2. Available models sorted by annual energy loss, * highest to lowest. * 3. Unavailable models last. * 4. Equal values keep their existing order. * * This results in the best available transformer * being positioned furthest to the right. * * @param {HTMLElement} calculator * @param {Object} calculatedModels * @returns {void} */ function sortModelCards( calculator, calculatedModels ) { const cards = Array.from( calculator.querySelectorAll( '[data-tc-model]' ) ); if (cards.length < 2) { return; } const parent = cards[0].parentElement; if (!parent) { return; } const originalOrder = new Map(); cards.forEach( (card, index) => { originalOrder.set( card, index ); } ); cards.sort( (cardA, cardB) => { const keyA = cardA.dataset.tcModel; const keyB = cardB.dataset.tcModel; const resultA = Object.prototype .hasOwnProperty.call( calculatedModels, keyA ) ? calculatedModels[ keyA ] : null; const resultB = Object.prototype .hasOwnProperty.call( calculatedModels, keyB ) ? calculatedModels[ keyB ] : null; const availableA = resultA !== null; const availableB = resultB !== null; /* * Available models are placed * before unavailable models. */ if ( availableA !== availableB ) { return availableA ? -1 : 1; } /* * Two unavailable models retain * their existing order. */ if ( !availableA && !availableB ) { return ( originalOrder.get( cardA ) - originalOrder.get( cardB ) ); } /* * Highest annual energy loss first. */ const difference = resultB.annualEnergy - resultA.annualEnergy; if (difference !== 0) { return difference; } /* * Equal calculated values retain * their existing order. */ return ( originalOrder.get( cardA ) - originalOrder.get( cardB ) ); } ); cards.forEach( (card) => { parent.appendChild( card ); } ); } /** * Updates one complete calculator. * * @param {HTMLElement} calculator * @param {Object} data * @returns {void} */ function updateCalculator( calculator, data ) { const inputs = readInputs( calculator ); updateInputOutputs( calculator, inputs ); const powerData = data.powers && Object.prototype.hasOwnProperty.call( data.powers, inputs.power ) ? data.powers[ inputs.power ] : null; if (!powerData) { console.error( `Transformer Calculators: no data found for ${inputs.power} kVA.` ); return; } const calculatedModels = {}; Object.keys( data.models ).forEach( (modelKey) => { const modelData = Object.prototype .hasOwnProperty.call( powerData, modelKey ) ? powerData[ modelKey ] : null; if ( modelData === null || typeof modelData !== 'object' ) { calculatedModels[ modelKey ] = null; return; } calculatedModels[ modelKey ] = calculateResults( modelData, inputs, data.currency ); } ); /* * Compare all models with Old DIN. */ const baselineResults = calculatedModels[ BASELINE_MODEL_KEY ]; if (baselineResults) { Object.keys( calculatedModels ).forEach( (modelKey) => { const results = calculatedModels[ modelKey ]; if (!results) { return; } const savings = baselineResults .lifetimeCost - results .lifetimeCost; results .lifetimeSavings = savings; results .savingsPercent = baselineResults .lifetimeCost > 0 ? ( savings / baselineResults .lifetimeCost ) * 100 : 0; } ); } /* * Find model with lowest annual energy loss. */ let bestModelKey = null; let lowestAnnualEnergy = Infinity; Object.keys( calculatedModels ).forEach( (modelKey) => { const results = calculatedModels[ modelKey ]; if ( results && results.annualEnergy < lowestAnnualEnergy ) { lowestAnnualEnergy = results.annualEnergy; bestModelKey = modelKey; } } ); const cards = calculator.querySelectorAll( '[data-tc-model]' ); cards.forEach( (card) => { const modelKey = card.dataset .tcModel; const results = Object.prototype .hasOwnProperty.call( calculatedModels, modelKey ) ? calculatedModels[ modelKey ] : null; if (!results) { setUnavailable( card, true ); setBestModel( card, false ); return; } setUnavailable( card, false ); setBestModel( card, modelKey === bestModelKey ); writeModelResults( card, results ); } ); /* * Reorder cards after all calculations * and availability states are complete. */ sortModelCards( calculator, calculatedModels ); } /** * Convert Base64 PDF to Blob. * * @param {string} base64 * @returns {Blob} */ function createPdfBlob( base64 ) { const binary = window.atob( base64 ); const bytes = new Uint8Array( binary.length ); for ( let index = 0; index < binary.length; index += 1 ) { bytes[index] = binary.charCodeAt( index ); } return new Blob( [bytes], { type: 'application/pdf' } ); } /** * Download PDF file. * * @param {string} base64 * @param {string} filename * @returns {void} */ function downloadPdf( base64, filename ) { const blob = createPdfBlob( base64 ); const url = URL.createObjectURL( blob ); const link = document.createElement( 'a' ); link.href = url; link.download = filename || 'transformer-calculation.pdf'; document.body.appendChild( link ); link.click(); link.remove(); window.setTimeout( () => { URL.revokeObjectURL( url ); }, 1000 ); } /** * Generate PDF for current calculator. * * @param {HTMLElement} calculator * @returns {Promise} */ async function generatePdf( calculator ) { const button = calculator.querySelector( '[data-tc-pdf-button]' ); const status = calculator.querySelector( '[data-tc-pdf-status]' ); if (!button) { return; } if ( typeof tcPdfSettings === 'undefined' ) { console.error( 'Transformer Calculators: tcPdfSettings is missing.' ); window.alert( 'PDF-inställningarna kunde inte läsas in.' ); return; } const inputs = readInputs( calculator ); const originalText = button.textContent; const language = getLanguage(); /* * Disable button while the request * is being processed. */ button.disabled = true; /* * Localized temporary button text. */ let generatingText = 'Generating PDF…'; if ( language === 'sv' ) { generatingText = 'Genererar PDF…'; } else if ( language === 'no' || language === 'nb' || language === 'nn' ) { generatingText = 'Genererer PDF…'; } button.textContent = generatingText; /* * Clear previous PDF status/error. */ if (status) { status.textContent = ''; status.classList.remove( 'tc-pdf-status--rate-limit' ); } /* * Build AJAX request. */ const request = new URLSearchParams(); request.set( 'action', 'tc_generate_pdf' ); request.set( 'nonce', tcPdfSettings.nonce ); /* * Send the actual frontend language * explicitly to the server. * * This makes rate-limit messages * independent of admin-ajax.php's * WPML language context. */ request.set( 'language', language ); request.set( 'type', calculator.dataset .calculatorType || '' ); request.set( 'power', inputs.power ); request.set( 'lifetime', inputs.lifetimeYears ); request.set( 'hours', inputs.hoursPerDay ); request.set( 'load', inputs.loadPercent ); request.set( 'price', inputs.priceMinorPerKwh ); try { const response = await fetch( tcPdfSettings .ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: request .toString(), credentials: 'same-origin' } ); /* * Parse JSON response. */ const result = await response.json(); /* * Server-side error. */ if ( !response.ok || !result.success || !result.data || !result.data.content ) { const message = result && result.data && result.data.message ? result.data .message : tcPdfSettings .messages .error; const errorCode = result && result.data && result.data.error_code ? result.data .error_code : ''; const error = new Error( message ); /* * Store the error type on the Error * object so the catch block can apply * the appropriate visual styling. */ error.tcErrorCode = errorCode; throw error; } /* * Successful request. */ if (status) { status.textContent = ''; status.classList.remove( 'tc-pdf-status--rate-limit' ); } downloadPdf( result.data.content, result.data.filename ); } catch (error) { console.error( 'Transformer Calculators PDF error:', error ); const message = error.message || tcPdfSettings .messages .error; const isRateLimit = error.tcErrorCode === 'rate_limit'; if (status) { status.textContent = message; status.classList.toggle( 'tc-pdf-status--rate-limit', isRateLimit ); } else { window.alert( message ); } } finally { button.disabled = false; button.textContent = originalText; } } /** * Initialize one calculator. * * @param {HTMLElement} calculator * @returns {void} */ function initializeCalculator( calculator ) { if ( calculator.dataset .tcInitialized === 'true' ) { return; } const data = readCalculatorData( calculator ); if (!data) { return; } calculator.dataset .tcInitialized = 'true'; const inputs = calculator.querySelectorAll( '[data-tc-input]' ); inputs.forEach( (input) => { input.addEventListener( 'input', () => { updateCalculator( calculator, data ); } ); input.addEventListener( 'change', () => { updateCalculator( calculator, data ); } ); } ); updateCalculator( calculator, data ); } /** * Initialize all calculators on page. * * Works even if calculators are initially hidden * inside Themeco accordion elements. * * @returns {void} */ function initializeAllCalculators() { const calculators = document.querySelectorAll( '[data-tc-calculator]' ); calculators.forEach( initializeCalculator ); } /** * PDF button click handler. * * Uses event delegation so PDF buttons also work * inside accordion elements. */ document.addEventListener( 'click', (event) => { const button = event.target.closest( '[data-tc-pdf-button]' ); if (!button) { return; } event.preventDefault(); const calculator = button.closest( '[data-tc-calculator]' ); if (!calculator) { console.error( 'Transformer Calculators: calculator container not found.' ); return; } generatePdf( calculator ); } ); /** * Start calculators. */ if ( document.readyState === 'loading' ) { document.addEventListener( 'DOMContentLoaded', initializeAllCalculators ); } else { initializeAllCalculators(); } })();
Fatal error: Uncaught Error: Class "TC_PDF" not found in /home/unitrafo/test.unitrafo.se/wp-content/plugins/transformer-calculators/transformer-calculators.php:67 Stack trace: #0 /home/unitrafo/test.unitrafo.se/wp-settings.php(597): include_once() #1 /home/unitrafo/test.unitrafo.se/wp-config.php(122): require_once('/home/unitrafo/...') #2 /home/unitrafo/test.unitrafo.se/wp-load.php(50): require_once('/home/unitrafo/...') #3 /home/unitrafo/test.unitrafo.se/wp-blog-header.php(13): require_once('/home/unitrafo/...') #4 /home/unitrafo/test.unitrafo.se/index.php(17): require('/home/unitrafo/...') #5 {main} thrown in /home/unitrafo/test.unitrafo.se/wp-content/plugins/transformer-calculators/transformer-calculators.php on line 67