mirror of
https://github.com/MrUnknownDE/techem-exporter.git
synced 2026-05-06 06:56:06 +02:00
add PDF Export :)
This commit is contained in:
@@ -6,6 +6,7 @@ const dayjs = require('dayjs');
|
|||||||
async function runExport() {
|
async function runExport() {
|
||||||
const isDebug = process.env.DEBUG_MODE === 'true';
|
const isDebug = process.env.DEBUG_MODE === 'true';
|
||||||
|
|
||||||
|
// Konfiguration aus .env laden
|
||||||
const config = {
|
const config = {
|
||||||
email: process.env.TECHEM_EMAIL,
|
email: process.env.TECHEM_EMAIL,
|
||||||
password: process.env.TECHEM_PASSWORD,
|
password: process.env.TECHEM_PASSWORD,
|
||||||
@@ -16,7 +17,7 @@ async function runExport() {
|
|||||||
selectorTimeout: parseInt(process.env.TIMEOUT_SELECTOR, 10) || 15000,
|
selectorTimeout: parseInt(process.env.TIMEOUT_SELECTOR, 10) || 15000,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`🚀 Techem Export (Horizontal-Layout) gestartet...`);
|
console.log(`🚀 Techem Export gestartet... (Debug: ${isDebug ? 'AN' : 'AUS'})`);
|
||||||
|
|
||||||
const browser = await chromium.launch({
|
const browser = await chromium.launch({
|
||||||
headless: !isDebug,
|
headless: !isDebug,
|
||||||
@@ -27,15 +28,20 @@ async function runExport() {
|
|||||||
const page = await context.newPage();
|
const page = await context.newPage();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// --- LOGIN FLOW ---
|
// ==========================================
|
||||||
console.log('🔑 Login-Prozess...');
|
// 1. LOGIN & AUTHENTIFIZIERUNG
|
||||||
|
// ==========================================
|
||||||
|
console.log('🔑 Login-Prozess startet...');
|
||||||
await page.goto('https://mieter.techem.de/', { waitUntil: 'networkidle', timeout: config.pageTimeout });
|
await page.goto('https://mieter.techem.de/', { waitUntil: 'networkidle', timeout: config.pageTimeout });
|
||||||
|
|
||||||
// Cookie-Banner weg
|
// Cookie-Banner killen
|
||||||
try {
|
try {
|
||||||
const cookieBtn = await page.waitForSelector('#CybotCookiebotDialogBodyButtonDecline', { timeout: 5000 });
|
const cookieBtn = await page.waitForSelector('#CybotCookiebotDialogBodyButtonDecline', { timeout: 5000 });
|
||||||
if (cookieBtn) await cookieBtn.click();
|
if (cookieBtn) {
|
||||||
} catch (e) {}
|
await cookieBtn.click();
|
||||||
|
await page.waitForTimeout(1000); // Kurz warten bis Animation weg ist
|
||||||
|
}
|
||||||
|
} catch (e) { /* Kein Banner da */ }
|
||||||
|
|
||||||
// Login-Bereich öffnen & Formular ausfüllen
|
// Login-Bereich öffnen & Formular ausfüllen
|
||||||
const loginTrigger = page.locator('text=/login|anmelden/i').first();
|
const loginTrigger = page.locator('text=/login|anmelden/i').first();
|
||||||
@@ -46,10 +52,13 @@ async function runExport() {
|
|||||||
await page.fill('input[type="password"]', config.password);
|
await page.fill('input[type="password"]', config.password);
|
||||||
await page.click('button[type="submit"]');
|
await page.click('button[type="submit"]');
|
||||||
|
|
||||||
|
console.log('⏳ Warte auf Dashboard-Weiterleitung...');
|
||||||
await page.waitForURL(/.*consumptions.*/, { timeout: config.pageTimeout });
|
await page.waitForURL(/.*consumptions.*/, { timeout: config.pageTimeout });
|
||||||
console.log('✅ Login erfolgreich.');
|
console.log('✅ Login erfolgreich.');
|
||||||
|
|
||||||
// --- DATEN EXTRAKTION ---
|
// ==========================================
|
||||||
|
// 2. DATEN EXTRAKTION
|
||||||
|
// ==========================================
|
||||||
const months = [];
|
const months = [];
|
||||||
let current = dayjs(config.startDate);
|
let current = dayjs(config.startDate);
|
||||||
const end = dayjs(config.endDate);
|
const end = dayjs(config.endDate);
|
||||||
@@ -67,11 +76,10 @@ async function runExport() {
|
|||||||
let wWert = 0, wUnit = 'm³';
|
let wWert = 0, wUnit = 'm³';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Heizung abrufen
|
// Heizung
|
||||||
const urlHeating = `https://mieter.techem.de/en/${config.unitId}/consumptions/heating/${date}`;
|
const urlHeating = `https://mieter.techem.de/en/${config.unitId}/consumptions/heating/${date}`;
|
||||||
// Turbo an: Wir warten nur auf das DOM, nicht auf Tracking-Skripte
|
|
||||||
await page.goto(urlHeating, { waitUntil: 'domcontentloaded', timeout: config.pageTimeout });
|
await page.goto(urlHeating, { waitUntil: 'domcontentloaded', timeout: config.pageTimeout });
|
||||||
await page.waitForTimeout(1000); // Kurze 1-Sekunden-Atempause für React
|
await page.waitForTimeout(1000); // React kurz rendern lassen
|
||||||
|
|
||||||
const bodyHeating = await page.innerText('body');
|
const bodyHeating = await page.innerText('body');
|
||||||
const matchH = bodyHeating.match(/([\d,.]+)\s*(kWh|units)/i);
|
const matchH = bodyHeating.match(/([\d,.]+)\s*(kWh|units)/i);
|
||||||
@@ -80,7 +88,7 @@ async function runExport() {
|
|||||||
hUnit = matchH[2];
|
hUnit = matchH[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Warmwasser abrufen
|
// Warmwasser
|
||||||
const urlWater = `https://mieter.techem.de/en/${config.unitId}/consumptions/hot-water/${date}`;
|
const urlWater = `https://mieter.techem.de/en/${config.unitId}/consumptions/hot-water/${date}`;
|
||||||
await page.goto(urlWater, { waitUntil: 'domcontentloaded', timeout: config.pageTimeout });
|
await page.goto(urlWater, { waitUntil: 'domcontentloaded', timeout: config.pageTimeout });
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
@@ -92,15 +100,7 @@ async function runExport() {
|
|||||||
wUnit = matchW[2];
|
wUnit = matchW[2];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daten zusammenführen
|
results.push({ monat: date, water: wWert, waterUnit: wUnit, heating: hWert, heatingUnit: hUnit });
|
||||||
results.push({
|
|
||||||
monat: date,
|
|
||||||
water: wWert,
|
|
||||||
waterUnit: wUnit,
|
|
||||||
heating: hWert,
|
|
||||||
heatingUnit: hUnit
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(` ✅ HZ: ${hWert} ${hUnit} | WW: ${wWert} ${wUnit}`);
|
console.log(` ✅ HZ: ${hWert} ${hUnit} | WW: ${wWert} ${wUnit}`);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -108,8 +108,10 @@ async function runExport() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- EXCEL DESIGN ---
|
// ==========================================
|
||||||
console.log('📊 Erstelle übersichtliche Excel-Datei...');
|
// 3. EXCEL EXPORT (Monatlich, Horizontal)
|
||||||
|
// ==========================================
|
||||||
|
console.log('📊 Erstelle Excel-Datei...');
|
||||||
const workbook = new ExcelJS.Workbook();
|
const workbook = new ExcelJS.Workbook();
|
||||||
const sheet = workbook.addWorksheet('Verbrauchsübersicht');
|
const sheet = workbook.addWorksheet('Verbrauchsübersicht');
|
||||||
|
|
||||||
@@ -121,7 +123,6 @@ async function runExport() {
|
|||||||
{ header: 'Einheit HZ', key: 'heatingUnit', width: 12 }
|
{ header: 'Einheit HZ', key: 'heatingUnit', width: 12 }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Styling (Blauer Header, alternierende Zeilen)
|
|
||||||
sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } };
|
||||||
sheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF00549F' } };
|
sheet.getRow(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF00549F' } };
|
||||||
sheet.getRow(1).alignment = { vertical: 'middle', horizontal: 'center' };
|
sheet.getRow(1).alignment = { vertical: 'middle', horizontal: 'center' };
|
||||||
@@ -129,22 +130,128 @@ async function runExport() {
|
|||||||
results.forEach((data, index) => {
|
results.forEach((data, index) => {
|
||||||
const row = sheet.addRow(data);
|
const row = sheet.addRow(data);
|
||||||
row.alignment = { vertical: 'middle', horizontal: 'center' };
|
row.alignment = { vertical: 'middle', horizontal: 'center' };
|
||||||
if (index % 2 !== 0) {
|
if (index % 2 !== 0) row.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF2F2F2' } };
|
||||||
row.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF2F2F2' } };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
sheet.autoFilter = 'A1:E1';
|
sheet.autoFilter = 'A1:E1';
|
||||||
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
sheet.views = [{ state: 'frozen', ySplit: 1 }];
|
||||||
|
|
||||||
const fileName = `Techem_Smart_Export_${dayjs().format('YYYY-MM-DD')}.xlsx`;
|
const excelName = `Techem_Export_${dayjs().format('YYYY-MM-DD')}.xlsx`;
|
||||||
await workbook.xlsx.writeFile(fileName);
|
await workbook.xlsx.writeFile(excelName);
|
||||||
|
console.log(`✨ Excel gespeichert: ${excelName}`);
|
||||||
|
|
||||||
console.log(`✨ Datei wurde generiert: ${fileName}`);
|
// ==========================================
|
||||||
|
// 4. PDF EXPORT (Jährlich mit Graphen)
|
||||||
|
// ==========================================
|
||||||
|
if (results.length > 0) {
|
||||||
|
console.log('📄 Erstelle PDF-Report...');
|
||||||
|
|
||||||
|
// Daten nach Jahren aggregieren
|
||||||
|
const yearlyStats = results.reduce((acc, curr) => {
|
||||||
|
const year = curr.monat.split('-')[0];
|
||||||
|
if (!acc[year]) acc[year] = { year, heating: 0, water: 0, heatingUnit: curr.heatingUnit, waterUnit: curr.waterUnit };
|
||||||
|
acc[year].heating += curr.heating;
|
||||||
|
acc[year].water += curr.water;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
const statsArray = Object.values(yearlyStats);
|
||||||
|
|
||||||
|
const htmlContent = `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; padding: 40px; color: #333; }
|
||||||
|
h1 { color: #00549F; border-bottom: 2px solid #00549F; padding-bottom: 10px; }
|
||||||
|
.info { margin-bottom: 30px; font-size: 0.9em; color: #666; }
|
||||||
|
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||||
|
th, td { border: 1px solid #ddd; padding: 12px; text-align: center; }
|
||||||
|
th { background-color: #f4f4f4; }
|
||||||
|
.chart-container { width: 100%; height: 400px; margin: 40px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Techem Jahres-Report</h1>
|
||||||
|
<div class="info">
|
||||||
|
Einheit-ID: ${config.unitId}<br>
|
||||||
|
Zeitraum: ${config.startDate} bis ${config.endDate}<br>
|
||||||
|
Erstellt am: ${dayjs().format('DD.MM.YYYY HH:mm')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="myChart"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Jahr</th>
|
||||||
|
<th>Heizung Gesamt (${results[0].heatingUnit})</th>
|
||||||
|
<th>Warmwasser Gesamt (${results[0].waterUnit})</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${statsArray.map(s => `
|
||||||
|
<tr>
|
||||||
|
<td><b>${s.year}</b></td>
|
||||||
|
<td>${s.heating.toFixed(2)}</td>
|
||||||
|
<td>${s.water.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const ctx = document.getElementById('myChart').getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ${JSON.stringify(statsArray.map(s => s.year))},
|
||||||
|
datasets: [{
|
||||||
|
label: 'Heizung',
|
||||||
|
data: ${JSON.stringify(statsArray.map(s => s.heating))},
|
||||||
|
backgroundColor: '#00549F',
|
||||||
|
yAxisID: 'y'
|
||||||
|
}, {
|
||||||
|
label: 'Warmwasser',
|
||||||
|
data: ${JSON.stringify(statsArray.map(s => s.water))},
|
||||||
|
backgroundColor: '#5CB85C',
|
||||||
|
yAxisID: 'y1'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
animation: false, // WICHTIG FÜR PDF: Keine Animationen beim Rendern!
|
||||||
|
scales: {
|
||||||
|
y: { type: 'linear', position: 'left', title: { display: true, text: 'Heizung' } },
|
||||||
|
y1: { type: 'linear', position: 'right', grid: { drawOnChartArea: false }, title: { display: true, text: 'Wasser' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
|
||||||
|
const reportPage = await context.newPage();
|
||||||
|
// networkidle stellt sicher, dass das externe Chart.js CDN geladen wurde
|
||||||
|
await reportPage.setContent(htmlContent, { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
|
const pdfName = `Techem_Report_${dayjs().format('YYYY-MM-DD')}.pdf`;
|
||||||
|
await reportPage.pdf({
|
||||||
|
path: pdfName,
|
||||||
|
format: 'A4',
|
||||||
|
printBackground: true,
|
||||||
|
margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' }
|
||||||
|
});
|
||||||
|
console.log(`✨ PDF gespeichert: ${pdfName}`);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ Kritischer Fehler:', err.message);
|
console.error('❌ Kritischer Fehler im Skript:', err.message);
|
||||||
} finally {
|
} finally {
|
||||||
|
console.log('🧹 Räume auf und schließe Browser...');
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user