- Wasserzähler-Stammdaten Import (Drag & Drop Excel/CSV) - QR-Code Druckseite mit Browser-Vorschau - QR-Code Excel Download (ExcelJS, eingebettete QR-PNGs) - Serienbrief wie Vorlage wasserablesung.pdf - HTML-Vorschau (max 20) + PDF Download (PDFKit, 1000+ skalierbar) - Antwortkarte mit QR-Code, Briefmarke, Zählerdaten - Bürgerseite: nur Kundennr./Zählernr./Stand (Datenschutz) - Kundennummer + letzter_stand + letzte_ablesung zum Schema - Bürgermeister: Patrick Krutzler - CAPTCHA verify API Route Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
143 lines
4.5 KiB
TypeScript
143 lines
4.5 KiB
TypeScript
/**
|
|
* Import-Script: Excel → Supabase (Wasserzähler)
|
|
*
|
|
* Liest zaehlerablesen_2025.xlsx und importiert Kundendaten in die
|
|
* Supabase-Tabelle `wasserzaehler`. Generiert pro Zeile einen access_token.
|
|
*
|
|
* Verwendung:
|
|
* npx tsx scripts/import-zaehler.ts [pfad-zur-excel-datei]
|
|
*
|
|
* Erwartet .env.local im Projekt-Root mit:
|
|
* NEXT_PUBLIC_SUPABASE_URL
|
|
* SUPABASE_SERVICE_ROLE_KEY
|
|
* NEXT_PUBLIC_BASE_URL (optional, default: https://gemeindeportal.vercel.app)
|
|
*/
|
|
|
|
import { readFileSync, existsSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
import { randomUUID } from 'crypto';
|
|
import * as XLSX from 'xlsx';
|
|
import { createClient } from '@supabase/supabase-js';
|
|
|
|
// .env.local laden
|
|
function loadEnv() {
|
|
const envPath = resolve(__dirname, '..', '.env.local');
|
|
if (!existsSync(envPath)) {
|
|
console.error('FEHLER: .env.local nicht gefunden unter:', envPath);
|
|
process.exit(1);
|
|
}
|
|
const content = readFileSync(envPath, 'utf-8');
|
|
for (const line of content.split('\n')) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
const eqIndex = trimmed.indexOf('=');
|
|
if (eqIndex === -1) continue;
|
|
const key = trimmed.slice(0, eqIndex).trim();
|
|
const value = trimmed.slice(eqIndex + 1).trim();
|
|
if (!process.env[key]) {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
loadEnv();
|
|
|
|
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
const SERVICE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'https://gemeindeportal.vercel.app';
|
|
|
|
if (!SUPABASE_URL || !SERVICE_KEY) {
|
|
console.error('FEHLER: NEXT_PUBLIC_SUPABASE_URL und SUPABASE_SERVICE_ROLE_KEY müssen gesetzt sein.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const supabase = createClient(SUPABASE_URL, SERVICE_KEY);
|
|
|
|
interface ExcelRow {
|
|
Kundennummer: string;
|
|
Zählernummer: string;
|
|
Name: string;
|
|
Adresse: string;
|
|
'Letzter Stand (m³)': number;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
async function main() {
|
|
const excelPath = process.argv[2] || resolve(__dirname, '..', 'zaehlerablesen_2025.xlsx');
|
|
|
|
if (!existsSync(excelPath)) {
|
|
console.error(`FEHLER: Excel-Datei nicht gefunden: ${excelPath}`);
|
|
console.error('Verwendung: npx tsx scripts/import-zaehler.ts [pfad-zur-excel-datei]');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Lese Excel-Datei: ${excelPath}\n`);
|
|
|
|
const workbook = XLSX.readFile(excelPath);
|
|
const sheetName = workbook.SheetNames[0];
|
|
const sheet = workbook.Sheets[sheetName];
|
|
const rows = XLSX.utils.sheet_to_json<ExcelRow>(sheet);
|
|
|
|
if (rows.length === 0) {
|
|
console.error('FEHLER: Keine Daten in der Excel-Datei gefunden.');
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`${rows.length} Zeile(n) gefunden.\n`);
|
|
|
|
const results: { kundennummer: string; name: string; token: string; url: string }[] = [];
|
|
|
|
for (const row of rows) {
|
|
const kundennummer = String(row.Kundennummer || '').trim();
|
|
const zaehlernummer = String(row.Zählernummer || row['Zaehlernummer'] || '').trim();
|
|
const name = String(row.Name || '').trim();
|
|
const adresse = String(row.Adresse || '').trim();
|
|
const alterStandRaw = row['Letzter Stand (m³)'] ?? row['Letzter Stand'] ?? 0;
|
|
const alter_stand = typeof alterStandRaw === 'number' ? alterStandRaw : parseFloat(String(alterStandRaw)) || 0;
|
|
|
|
if (!kundennummer || !zaehlernummer) {
|
|
console.warn(`WARNUNG: Zeile übersprungen (fehlende Kundennummer/Zählernummer):`, row);
|
|
continue;
|
|
}
|
|
|
|
const access_token = randomUUID();
|
|
|
|
const { error } = await supabase.from('wasserzaehler').insert({
|
|
kundennummer,
|
|
zaehlernummer,
|
|
haushalt_name: name,
|
|
adresse,
|
|
alter_stand,
|
|
access_token,
|
|
});
|
|
|
|
if (error) {
|
|
console.error(`FEHLER bei Kundennr. ${kundennummer}:`, error.message);
|
|
continue;
|
|
}
|
|
|
|
const url = `${BASE_URL}/wasserzaehler?token=${access_token}`;
|
|
results.push({ kundennummer, name, token: access_token, url });
|
|
console.log(`OK: ${kundennummer} — ${name}`);
|
|
}
|
|
|
|
console.log('\n' + '='.repeat(80));
|
|
console.log('ERGEBNIS — QR-Code URLs');
|
|
console.log('='.repeat(80));
|
|
|
|
console.log('\n%-15s %-25s %-40s'.replace('%-15s', 'Kundennr.').replace('%-25s', 'Name').replace('%-40s', 'URL'));
|
|
console.log('-'.repeat(80));
|
|
|
|
for (const r of results) {
|
|
console.log(`${r.kundennummer.padEnd(15)} ${r.name.padEnd(25)} ${r.url}`);
|
|
}
|
|
|
|
console.log(`\n${results.length} von ${rows.length} Einträgen erfolgreich importiert.`);
|
|
console.log(`\nAdmin-Seite mit QR-Codes: ${BASE_URL}/admin/qrcodes`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('Unerwarteter Fehler:', err);
|
|
process.exit(1);
|
|
});
|