// pdfFillDataPGE.js const fs = require('fs'); const path = require('path'); const cheerio = require('cheerio'); // Библиотека для парсинга const pdfFillData = require('./pdfFillData'); // Родительский класс const { rgb } = require('pdf-lib'); const randomData = require('./randomDataGenerator'); class pdfFillDataPGE extends pdfFillData { constructor() { super('PGE'); this.baselineCorrectionFactor = 0.85; // --- Свойства для динамических данных (СТР. 1) --- this.service_for_name = ''; this.service_for_address = ''; this.account_no = ''; this.statement_date = ''; this.due_date = ''; // Account Summary this.previous_statement = ''; this.payment_received = ''; this.previous_unpaid_balance = ''; this.pge_delivery_charges = ''; this.svce_generation_charges = ''; this.total_amount_due_date = ''; // e.g., "08/28/2019" this.total_amount_due = ''; // Remittance Slip (отрывной купон) this.remit_account_number = ''; this.remit_due_date = ''; this.remit_total_amount_due = ''; // --- ДАННЫЕ ДЛЯ ГРАФИКОВ --- this.monthly_billing_history = []; // Массив объектов { month, year, electric, gas } this.daily_usage_electric = {}; // { year_ago, last_period, current_period } this.daily_usage_gas = {}; // { year_ago, last_period, current_period } this.electric_conservation_incentive = ''; this.electric_transmission = ''; this.electric_distribution = ''; this.electric_public_purpose = ''; this.electric_nuclear_decommissioning = ''; this.electric_dwr_bond = ''; this.electric_ctc = ''; this.electric_ecra = ''; this.electric_pcia = ''; this.electric_taxes_and_other = ''; this.electric_total_charges = ''; // --- СВОЙСТВА ДЛЯ СТР. 3 --- this.delivery_charges_period = ''; this.service_agreement_id = ''; this.rate_schedule = ''; this.meter_number = ''; this.current_meter_reading = ''; this.prior_meter_reading = ''; this.total_usage_kwh = ''; this.baseline_territory = ''; this.heat_source = ''; this.tier1_allowance_detail = ''; this.tier1_usage_amount = ''; this.tier2_usage_amount = ''; this.generation_credit = ''; this.pcia_adjustment = ''; this.franchise_fee_surcharge = ''; this.electric_usage_period_title = ''; this.daily_electric_usage_history = []; // В constructor() this.average_daily_usage = 0; this.svce_rate_schedule = ''; this.svce_generation_detail = ''; this.svce_generation_amount = ''; this.svce_energy_surcharge = ''; this.svce_total_charges = ''; // --- СВОЙСТВА ДЛЯ СТР. 5 (Gas Charges) --- this.gas_charges_period = ''; this.gas_service_agreement_id = ''; this.gas_rate_schedule = ''; this.gas_meter_number = ''; this.gas_current_reading = ''; this.gas_prior_reading = ''; this.gas_difference = ''; this.gas_multiplier = ''; this.gas_total_usage = ''; this.gas_baseline_territory = ''; this.gas_serial = ''; this.gas_tier1_allowance_detail = ''; this.gas_tier1_usage_detail = ''; this.gas_tier1_usage_amount = ''; this.gas_ppp_surcharge_detail = ''; this.gas_ppp_surcharge_amount = ''; this.gas_total_charges = ''; this.gas_procurement_period = ''; this.gas_procurement_amount = ''; this.gas_usage_period_title = ''; this.gas_average_daily_usage = 0; this.daily_gas_usage_history = []; this.fontMap = { // "Имя шрифта из SVG-атрибута font-family": индекс_из_routes.js 'Helvetica': 0, // Базовый шрифт, всегда индекс 0 'Arial-BoldMT': 1, 'Arial': 2, 'ArialNarrow-Bold': 3, 'ArialNarrow': 4, 'Arial-ItalicMT': 5, 'MyriadPro-Regular': 2, // Добавьте другие шрифты, если они есть в SVG }; } async setFields(fields) { if (!fields || Object.keys(fields).length === 0) { fields = randomData.pge(); // <-- Замените на entergy(), aep_ohio() и т.д. } this.service_for_name = fields.service_for_name; this.service_for_address = fields.service_for_address; this.account_no = fields.account_no; this.statement_date = fields.statement_date; this.due_date = fields.due_date; this.previous_statement = fields.previous_statement; this.payment_received = fields.payment_received; this.previous_unpaid_balance = fields.previous_unpaid_balance; this.pge_delivery_charges = fields.pge_delivery_charges; this.svce_generation_charges = fields.svce_generation_charges; this.total_amount_due_date = fields.total_amount_due_date; this.total_amount_due = fields.total_amount_due; this.remit_account_number = fields.remit_account_number; this.remit_due_date = fields.remit_due_date; this.remit_total_amount_due = fields.remit_total_amount_due; // Данные для графиков this.monthly_billing_history = fields.monthly_billing_history || []; this.daily_usage_electric = fields.daily_usage_electric || {}; this.daily_usage_gas = fields.daily_usage_gas || {}; this.electric_conservation_incentive = fields.electric_conservation_incentive; this.electric_transmission = fields.electric_transmission; this.electric_distribution = fields.electric_distribution; this.electric_public_purpose = fields.electric_public_purpose; this.electric_nuclear_decommissioning = fields.electric_nuclear_decommissioning; this.electric_dwr_bond = fields.electric_dwr_bond; this.electric_ctc = fields.electric_ctc; this.electric_ecra = fields.electric_ecra; this.electric_pcia = fields.electric_pcia; this.electric_taxes_and_other = fields.electric_taxes_and_other; this.electric_total_charges = fields.electric_total_charges; // --- ДАННЫЕ ДЛЯ СТР. 3 --- this.delivery_charges_period = fields.delivery_charges_period; this.service_agreement_id = fields.service_agreement_id; this.rate_schedule = fields.rate_schedule; this.meter_number = fields.meter_number; this.current_meter_reading = fields.current_meter_reading; this.prior_meter_reading = fields.prior_meter_reading; this.total_usage_kwh = fields.total_usage_kwh; this.baseline_territory = fields.baseline_territory; this.heat_source = fields.heat_source; this.tier1_allowance_detail = fields.tier1_allowance_detail; this.tier1_usage_amount = fields.tier1_usage_amount; this.tier2_usage_amount = fields.tier2_usage_amount; this.generation_credit = fields.generation_credit; this.pcia_adjustment = fields.pcia_adjustment; this.franchise_fee_surcharge = fields.franchise_fee_surcharge; this.electric_usage_period_title = fields.electric_usage_period_title; this.svce_rate_schedule = fields.svce_rate_schedule; this.svce_generation_detail = fields.svce_generation_detail; this.svce_generation_amount = fields.svce_generation_amount; this.svce_energy_surcharge = fields.svce_energy_surcharge; this.svce_total_charges = fields.svce_total_charges; // --- ДАННЫЕ ДЛЯ СТР. 5 --- this.gas_charges_period = fields.gas_charges_period; this.gas_service_agreement_id = fields.gas_service_agreement_id; this.gas_rate_schedule = fields.gas_rate_schedule; this.gas_meter_number = fields.gas_meter_number; this.gas_current_reading = fields.gas_current_reading; this.gas_prior_reading = fields.gas_prior_reading; this.gas_difference = fields.gas_difference; this.gas_multiplier = fields.gas_multiplier; this.gas_total_usage = fields.gas_total_usage; this.gas_baseline_territory = fields.gas_baseline_territory; this.gas_serial = fields.gas_serial; this.gas_tier1_allowance_detail = fields.gas_tier1_allowance_detail; this.gas_tier1_usage_detail = fields.gas_tier1_usage_detail; this.gas_tier1_usage_amount = fields.gas_tier1_usage_amount; this.gas_ppp_surcharge_detail = fields.gas_ppp_surcharge_detail; this.gas_ppp_surcharge_amount = fields.gas_ppp_surcharge_amount; this.gas_total_charges = fields.gas_total_charges; this.gas_procurement_period = fields.gas_procurement_period; this.gas_procurement_amount = fields.gas_procurement_amount; this.gas_usage_period_title = fields.gas_usage_period_title; this.gas_average_daily_usage = parseFloat(fields.gas_average_daily_usage) || 0; // В файле pdfFillDataPGE.js, метод setFields() // ... (после строки this.gas_average_daily_usage = ...) // --- НОВАЯ, УМНАЯ ПРОВЕРКА И ПАРСИНГ --- if (typeof fields.daily_gas_usage_history === 'string') { // Если пришли данные из формы (строка), парсим их try { this.daily_gas_usage_history = JSON.parse(fields.daily_gas_usage_history || '[]'); } catch (e) { this.daily_gas_usage_history = []; console.error("Error parsing daily_gas_usage_history JSON from string"); } } else { // Если пришли данные из генератора (уже массив), просто присваиваем this.daily_gas_usage_history = fields.daily_gas_usage_history || []; } if (typeof fields.daily_electric_usage_history === 'string') { try { this.daily_electric_usage_history = JSON.parse(fields.daily_electric_usage_history || '[]'); } catch (e) { this.daily_electric_usage_history = []; console.error("Error parsing daily_electric_usage_history JSON from string"); } } else { this.daily_electric_usage_history = fields.daily_electric_usage_history || []; } // Удаляем старый `try/catch` для average_daily_usage, он уже обработан this.average_daily_usage = parseFloat(fields.average_daily_usage) || 0; } /** * Универсальный метод для отрисовки столбчатых диаграмм. * Имеет два режима: * 1. ТОЧНЫЙ: Если находит в SVG метки id="chart-label-start" и id="chart-label-end", * строит сетку между их центрами. * 2. ЗАПАСНОЙ: Если метки не найдены, строит равномерную сетку по всей ширине * контейнера id="chart-area". * @param {object} options - Объект с параметрами для диаграммы. */ async _drawBarChart(options) { const { ipage, $, historyData, maxAxisValue, barWidth, electricColor, gasColor } = options; if (!historyData || historyData.length === 0) return; const page = this.getPageByIndex(ipage); if (!page) return; const pageHeight = this.getPageHeight(ipage); const chartRect = $('#chart-area'); if (chartRect.length === 0) { console.error(`Could not find boundary element with id='chart-area' in SVG for page ${ipage}.`); return; } const chartArea = { x_ill: parseFloat(chartRect.attr('x')), y_ill: parseFloat(chartRect.attr('y')), width: parseFloat(chartRect.attr('width')), height: parseFloat(chartRect.attr('height')) }; if (Object.values(chartArea).some(isNaN)) { console.error(`Invalid attributes for #chart-area in SVG for page ${ipage}.`); return; } // --- АЛГОРИТМ ВЫБОРА РЕЖИМА --- const startLabel = $('#chart-label-start'); const endLabel = $('#chart-label-end'); const numBars = historyData.length; let startPointX, stepX; if (startLabel.length > 0 && endLabel.length > 0) { // --- РЕЖИМ 1: ТОЧНЫЙ (по меткам) --- console.log(`Page ${ipage} chart: Using 'chart-label-start/end' for precise layout.`); const getLabelCenter = (element) => { const transformAttr = element.attr('transform'); const matrix = transformAttr ? transformAttr.match(/matrix\(([^)]+)\)/) : null; if (!matrix) return NaN; const [scaleX, , , , x_svg] = matrix[1].split(/[ ,]+/).map(parseFloat); const font = this.getCustomerFontByIndex(0); const textWidth = font.widthOfTextAtSize(element.text(), 7 * scaleX); return x_svg + (textWidth / 2); }; startPointX = getLabelCenter(startLabel); const endPointX = getLabelCenter(endLabel); if (isNaN(startPointX) || isNaN(endPointX)) { console.error("Could not parse coordinates from start/end labels."); return; } stepX = (numBars > 1) ? (endPointX - startPointX) / (numBars - 1) : 0; } else { // --- РЕЖИМ 2: ЗАПАСНОЙ (по ширине chart-area) --- console.log(`Page ${ipage} chart: Using '#chart-area' for uniform layout.`); const spacePerBar = chartArea.width / (numBars + 1); startPointX = chartArea.x_ill + spacePerBar; stepX = spacePerBar; } // --- ОБЩАЯ ЛОГИКА ОТРИСОВКИ --- historyData.forEach((dataPoint, index) => { const electricValue = parseFloat(dataPoint.electric || dataPoint.kwh) || 0; const gasValue = parseFloat(dataPoint.gas) || 0; const totalValue = electricValue + gasValue; if (totalValue <= 0) return; const electricBarHeight = (electricValue / maxAxisValue) * chartArea.height; const gasBarHeight = (gasValue / maxAxisValue) * chartArea.height; const barCenterX = startPointX + (index * stepX); const barX = barCenterX - (barWidth / 2); const baseY = pageHeight - chartArea.y_ill - chartArea.height; if (gasValue > 0 && gasColor) { page.drawRectangle({ x: barX, y: baseY, width: barWidth, height: gasBarHeight, color: gasColor }); } if (electricValue > 0 && electricColor) { page.drawRectangle({ x: barX, y: baseY + gasBarHeight, width: barWidth, height: electricBarHeight, color: electricColor }); } }); } /** * Рисует пунктирную линию среднего потребления на графике. * @param {number} ipage - Индекс страницы. * @param {CheerioAPI} $ - Распарсенный Cheerio объект с содержимым SVG. */ async _drawAverageLine(ipage, $, average_daily = this.average_daily_usage, max = 30) { if (!average_daily || average_daily <= 0) return; const page = this.getPageByIndex(ipage); if (!page) return; const pageHeight = this.getPageHeight(ipage); const chartRect = $('#chart-area'); if (chartRect.length === 0) { console.error("Could not find #chart-area for average line."); return; } const chartArea = { x_ill: parseFloat(chartRect.attr('x')), y_ill: parseFloat(chartRect.attr('y')), width: parseFloat(chartRect.attr('width')), height: parseFloat(chartRect.attr('height')) }; if (Object.values(chartArea).some(isNaN)) { console.error("Invalid attributes for #chart-area for average line."); return; } // Вычисляем Y-координату для линии const avgLineY = (pageHeight - chartArea.y_ill - chartArea.height) + (average_daily / max) * chartArea.height; // Рисуем линию по всей ширине графика page.drawLine({ start: { x: chartArea.x_ill, y: avgLineY }, end: { x: chartArea.x_ill + chartArea.width, y: avgLineY }, thickness: 0.5, color: rgb(0, 0, 0), dashArray: [3, 3], }); } // --- Новый главный метод отрисовки --- // В файле pdfFillDataPGE.js // --- ПОЛНОСТЬЮ ЗАМЕНИТЕ СТАРЫЙ МЕТОД draw() НА ЭТОТ --- async draw() { const svgDir = path.join(__dirname, `../../../public/template/svg/pge/`); const replacements = { // Заголовок '1234567890-1': this.account_no, '09/07/2019': this.statement_date, '09/28/2019': this.due_date, // Service For 'SPARKY JOULE': this.service_for_name, '12345 ENERGY CT': this.service_for_address, // Account Summary '$91.57': "$" + this.previous_statement, '-91.57': this.payment_received, '$0.00': "$" + this.previous_unpaid_balance, '$55.66': "$" + this.pge_delivery_charges, '$32.48': "$" + this.svce_generation_charges, '08/28/2019': this.total_amount_due_date, '$88.14': "$" + this.total_amount_due, // Remittance Slip (отрывной купон) // Примечание: '123456789-1' в SVG может быть другим текстом, чем '1234567890-1' // Проверьте SVG и используйте правильный ключ. Для примера я использую два разных. '123456789-1': this.remit_account_number, // Дата и сумма на купоне могут совпадать с основными, но могут и отличаться, // поэтому используем отдельные переменные. // '09/28/2019': this.remit_due_date, // Конфликтует с Due Date в заголовке // '$88.14': this.remit_total_amount_due, // Конфликтует с Total Amount Due выше // Daily Usage Comparison (маленькие графики) '12.50': this.daily_usage_gas.year_ago, '12.16': this.daily_usage_gas.last_period, '12.67': this.daily_usage_gas.current_period, '0.12': this.daily_usage_electric.year_ago, '0.16': this.daily_usage_electric.last_period, '0.17': this.daily_usage_electric.current_period, // --- НОВЫЕ ПЛЕЙСХОЛДЕРЫ СО СТРАНИЦЫ 2 --- '-$9.50': "-$" + this.electric_conservation_incentive * -1, '12.42': this.electric_transmission, '35.08': this.electric_distribution, '4.71': this.electric_public_purpose, '0.33': this.electric_nuclear_decommissioning, '1.91': this.electric_dwr_bond, '0.42': this.electric_ctc, '-0.22': this.electric_ecra, '10.26': this.electric_pcia, '0.25': this.electric_taxes_and_other, // --- ПЛЕЙСХОЛДЕРЫ СО СТР. 3 --- '08/02/2019 - 08/31/2019 (30 billing days)': this.delivery_charges_period, 'Service For: 12345 ENERGY CT': `Service For: ${this.service_for_address}`, 'Service Agreement ID: 111111111': `Service Agreement ID: ${this.service_agreement_id}`, 'Rate Schedule: E1 X Residential Service': `Rate Schedule: ${this.rate_schedule}`, '1111111111': this.meter_number, '37,710': this.current_meter_reading, '37,330': this.prior_meter_reading, '380.000000 kWh': this.total_usage_kwh, 'X': this.baseline_territory, 'B - Not Electric': this.heat_source, '297.00 kWh (30 days x 9.9 kWh/day)': this.tier1_allowance_detail, '$66.46': this.tier1_usage_amount, '23.37': this.tier2_usage_amount, '-44.68': this.generation_credit, '10.26': this.pcia_adjustment, '0.25': this.franchise_fee_surcharge, 'Electric Usage This Period: 380.000000 kWh, 30 billing days': this.electric_usage_period_title, // --- НОВЫЕ ПЛЕЙСХОЛДЕРЫ СО СТРАНИЦЫ 4 --- 'E-1': this.svce_rate_schedule, '380.000000 kWh @ $0.08519': this.svce_generation_detail, '380.000000 kWh': `${this.svce_generation_detail.split(" ")[0]} kWh`, '$32.37': "$" + this.svce_generation_amount, '32.37': this.svce_generation_amount, '0.11': this.svce_energy_surcharge, '$32.48': "$" + this.svce_total_charges, // --- ПЛЕЙСХОЛДЕРЫ СО СТРАНИЦЫ 5 --- '08/02/2019 - 08/31/2019 (30 billing days)': this.gas_charges_period, 'Service Agreement ID: 1111111111': `Service Agreement ID: ${this.gas_service_agreement_id}`, 'Rate Schedule: G1 X Residential Service': `Rate Schedule: ${this.gas_rate_schedule}`, '11111111': this.gas_meter_number, '2,588': this.gas_current_reading, '2,583': this.gas_prior_reading, 'diff5': this.gas_difference, '1.031647': this.gas_multiplier, '5.000000 Therms': this.gas_total_usage, // 'X': this.gas_baseline_territory, // "X" слишком общий, может вызвать проблемы // 'G': this.gas_serial, // "G" слишком общий '17.70 Therms (30 days x 0.59 Therms/day)': this.gas_tier1_allowance_detail, '5.000000 Therms @ $1.28395': this.gas_tier1_usage_detail, '$6.42': "$" + this.gas_tier1_usage_amount, '($0.09047 /Therm)': "$" + this.gas_ppp_surcharge_detail, '0.45': this.gas_ppp_surcharge_amount, '$6.87': "$" + this.gas_total_charges, '07/02/2019 - 07/31/2019': this.gas_procurement_period, '$0.28462': "$" + this.gas_procurement_amount, 'Gas Usage This Period: 5.000000 Therms, 30 billing days': this.gas_usage_period_title, }; for (let i = 0; i < this.countPages(); i++) { const svgPath = path.join(svgDir, `${i + 1}.svg`); if (fs.existsSync(svgPath)) { console.log(`Processing all elements for page ${i}: ${svgPath}`); // --- ПАРСИМ SVG ОДИН РАЗ НА СТРАНИЦУ --- const svgContent = fs.readFileSync(svgPath, 'utf8'); const $ = cheerio.load(svgContent, { xmlMode: true }); // --- ВЫЗЫВАЕМ СПЕЦИФИЧНЫЕ ДЛЯ СТРАНИЦЫ ФУНКЦИИ --- // Графики на первой странице (индекс 0) // График на первой странице if (i === 0) { await this._drawBarChart({ ipage: i, $: $, historyData: this.monthly_billing_history, maxAxisValue: 200, barWidth: 6.5, electricColor: rgb(0, 0, 0), gasColor: rgb(0.8, 0.8, 0.8) }); } // Графики на третьей странице (индекс 2) if (i === 2) { await this._drawBarChart({ ipage: i, $: $, // Передаем распарсенный объект historyData: this.daily_electric_usage_history, maxAxisValue: 30, barWidth: 4, electricColor: rgb(0, 0, 0), gasColor: null }); await this._drawAverageLine(i, $); // Вызываем отрисовку средней линии } if (i === 4) { // График на пятой странице (индекс 4) await this._drawBarChart({ ipage: i, $: $, historyData: this.daily_gas_usage_history, maxAxisValue: 5, // Максимум на оси Y для газа barWidth: 4, electricColor: null, gasColor: rgb(0.8, 0.8, 0.8) }); // Рисуем линию среднего, если нужно await this._drawAverageLine(i, $, this.gas_average_daily_usage, 5); } // Текст рисуется для каждой страницы в конце await super.parseAndDrawSVG(i, $, replacements); } } } } module.exports = pdfFillDataPGE;