- 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>
224 lines
7.7 KiB
TypeScript
224 lines
7.7 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { createServiceClient, createServerSupabaseClient } from '@/lib/supabase/server';
|
|
import QRCode from 'qrcode';
|
|
import ExcelJS from 'exceljs';
|
|
|
|
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'https://gemeindeportal.vercel.app';
|
|
|
|
export async function GET() {
|
|
try {
|
|
// Auth check
|
|
const authClient = await createServerSupabaseClient();
|
|
const { data: { user } } = await authClient.auth.getUser();
|
|
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'Nicht authentifiziert.' }, { status: 401 });
|
|
}
|
|
|
|
const supabase = createServiceClient();
|
|
|
|
const { data: zaehler, error } = await supabase
|
|
.from('wasserzaehler')
|
|
.select('*')
|
|
.order('kundennummer', { ascending: true });
|
|
|
|
if (error || !zaehler || zaehler.length === 0) {
|
|
return NextResponse.json(
|
|
{ error: 'Keine Wasserzähler gefunden.' },
|
|
{ status: 404 }
|
|
);
|
|
}
|
|
|
|
// Create workbook
|
|
const workbook = new ExcelJS.Workbook();
|
|
workbook.creator = 'Gemeindeportal';
|
|
workbook.created = new Date();
|
|
|
|
// --- Sheet 1: Übersicht (Tabelle mit allen Kunden) ---
|
|
const listSheet = workbook.addWorksheet('Übersicht', {
|
|
pageSetup: {
|
|
paperSize: 9, // A4
|
|
orientation: 'landscape',
|
|
fitToPage: true,
|
|
fitToWidth: 1,
|
|
fitToHeight: 0,
|
|
},
|
|
});
|
|
|
|
listSheet.columns = [
|
|
{ header: 'Kundennummer', key: 'kundennummer', width: 18 },
|
|
{ header: 'Zählernummer', key: 'zaehlernummer', width: 18 },
|
|
{ header: 'Name', key: 'name', width: 28 },
|
|
{ header: 'Adresse', key: 'adresse', width: 30 },
|
|
{ header: 'Letzter Stand (m³)', key: 'stand', width: 18 },
|
|
{ header: 'QR-Code URL', key: 'url', width: 55 },
|
|
];
|
|
|
|
// Header styling
|
|
const headerRow = listSheet.getRow(1);
|
|
headerRow.font = { bold: true, size: 11 };
|
|
headerRow.fill = {
|
|
type: 'pattern',
|
|
pattern: 'solid',
|
|
fgColor: { argb: 'FF0D2B4E' },
|
|
};
|
|
headerRow.font = { bold: true, size: 11, color: { argb: 'FFFFFFFF' } };
|
|
headerRow.alignment = { vertical: 'middle' };
|
|
headerRow.height = 28;
|
|
|
|
for (const z of zaehler) {
|
|
const url = `${BASE_URL}/wasserzaehler?token=${z.access_token}`;
|
|
listSheet.addRow({
|
|
kundennummer: z.kundennummer || '',
|
|
zaehlernummer: z.zaehlernummer,
|
|
name: z.haushalt_name,
|
|
adresse: z.adresse || '',
|
|
stand: z.alter_stand,
|
|
url,
|
|
});
|
|
}
|
|
|
|
// Auto-filter
|
|
listSheet.autoFilter = {
|
|
from: { row: 1, column: 1 },
|
|
to: { row: zaehler.length + 1, column: 6 },
|
|
};
|
|
|
|
// --- Sheet 2+: Ein Blatt pro Kunde mit QR-Code (Druckseiten) ---
|
|
for (let i = 0; i < zaehler.length; i++) {
|
|
const z = zaehler[i];
|
|
const url = `${BASE_URL}/wasserzaehler?token=${z.access_token}`;
|
|
|
|
// Generate QR code as PNG buffer
|
|
const qrBuffer = await QRCode.toBuffer(url, {
|
|
width: 400,
|
|
margin: 2,
|
|
errorCorrectionLevel: 'M',
|
|
type: 'png',
|
|
});
|
|
|
|
const sheetName = `${z.kundennummer || `Kunde ${i + 1}`}`.slice(0, 31);
|
|
const sheet = workbook.addWorksheet(sheetName, {
|
|
pageSetup: {
|
|
paperSize: 9, // A4
|
|
orientation: 'portrait',
|
|
horizontalCentered: true,
|
|
verticalCentered: true,
|
|
margins: {
|
|
left: 0.7,
|
|
right: 0.7,
|
|
top: 1.0,
|
|
bottom: 1.0,
|
|
header: 0.3,
|
|
footer: 0.3,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Set column widths
|
|
sheet.getColumn(1).width = 5;
|
|
sheet.getColumn(2).width = 25;
|
|
sheet.getColumn(3).width = 35;
|
|
sheet.getColumn(4).width = 5;
|
|
|
|
// Title
|
|
sheet.mergeCells('B2:C2');
|
|
const titleCell = sheet.getCell('B2');
|
|
titleCell.value = 'Gemeindeamt Weißkirchen an der Traun';
|
|
titleCell.font = { bold: true, size: 16, color: { argb: 'FF0D2B4E' } };
|
|
titleCell.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
sheet.getRow(2).height = 30;
|
|
|
|
// Subtitle
|
|
sheet.mergeCells('B3:C3');
|
|
const subtitleCell = sheet.getCell('B3');
|
|
subtitleCell.value = `Wasserzähler-Ablesung ${new Date().getFullYear()}`;
|
|
subtitleCell.font = { size: 12, color: { argb: 'FF666666' } };
|
|
subtitleCell.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
sheet.getRow(3).height = 22;
|
|
|
|
// QR Code image — rows 5-20
|
|
const imageId = workbook.addImage({
|
|
buffer: qrBuffer as unknown as ExcelJS.Buffer,
|
|
extension: 'png',
|
|
});
|
|
|
|
sheet.addImage(imageId, {
|
|
tl: { col: 1.5, row: 5 },
|
|
ext: { width: 250, height: 250 },
|
|
});
|
|
|
|
// Space for QR code
|
|
for (let row = 5; row <= 20; row++) {
|
|
sheet.getRow(row).height = 18;
|
|
}
|
|
|
|
// Customer info below QR code
|
|
const infoStartRow = 22;
|
|
|
|
// Kundennummer
|
|
sheet.getCell(`B${infoStartRow}`).value = 'Kundennummer';
|
|
sheet.getCell(`B${infoStartRow}`).font = { size: 10, color: { argb: 'FF999999' } };
|
|
sheet.getCell(`B${infoStartRow}`).alignment = { horizontal: 'center' };
|
|
sheet.mergeCells(`B${infoStartRow}:C${infoStartRow}`);
|
|
|
|
sheet.getCell(`B${infoStartRow + 1}`).value = z.kundennummer || '—';
|
|
sheet.getCell(`B${infoStartRow + 1}`).font = { bold: true, size: 22 };
|
|
sheet.getCell(`B${infoStartRow + 1}`).alignment = { horizontal: 'center' };
|
|
sheet.mergeCells(`B${infoStartRow + 1}:C${infoStartRow + 1}`);
|
|
sheet.getRow(infoStartRow + 1).height = 32;
|
|
|
|
// Zählernummer
|
|
sheet.getCell(`B${infoStartRow + 3}`).value = 'Zählernummer';
|
|
sheet.getCell(`B${infoStartRow + 3}`).font = { size: 10, color: { argb: 'FF999999' } };
|
|
sheet.getCell(`B${infoStartRow + 3}`).alignment = { horizontal: 'center' };
|
|
sheet.mergeCells(`B${infoStartRow + 3}:C${infoStartRow + 3}`);
|
|
|
|
sheet.getCell(`B${infoStartRow + 4}`).value = z.zaehlernummer;
|
|
sheet.getCell(`B${infoStartRow + 4}`).font = { bold: true, size: 16 };
|
|
sheet.getCell(`B${infoStartRow + 4}`).alignment = { horizontal: 'center' };
|
|
sheet.mergeCells(`B${infoStartRow + 4}:C${infoStartRow + 4}`);
|
|
sheet.getRow(infoStartRow + 4).height = 26;
|
|
|
|
// Instructions
|
|
sheet.mergeCells(`B${infoStartRow + 7}:C${infoStartRow + 7}`);
|
|
const instrCell = sheet.getCell(`B${infoStartRow + 7}`);
|
|
instrCell.value = 'Scannen Sie den QR-Code mit Ihrem Smartphone';
|
|
instrCell.font = { size: 11, color: { argb: 'FF666666' } };
|
|
instrCell.alignment = { horizontal: 'center', wrapText: true };
|
|
|
|
sheet.mergeCells(`B${infoStartRow + 8}:C${infoStartRow + 8}`);
|
|
const instrCell2 = sheet.getCell(`B${infoStartRow + 8}`);
|
|
instrCell2.value = 'um Ihren aktuellen Zählerstand zu melden.';
|
|
instrCell2.font = { size: 11, color: { argb: 'FF666666' } };
|
|
instrCell2.alignment = { horizontal: 'center', wrapText: true };
|
|
|
|
// Border box around content
|
|
for (let row = 1; row <= infoStartRow + 9; row++) {
|
|
const r = sheet.getRow(row);
|
|
r.getCell(2).border = { left: { style: 'thin', color: { argb: 'FFE0E0E0' } } };
|
|
r.getCell(3).border = { right: { style: 'thin', color: { argb: 'FFE0E0E0' } } };
|
|
}
|
|
}
|
|
|
|
// Generate buffer
|
|
const buffer = await workbook.xlsx.writeBuffer();
|
|
|
|
const filename = `wasserzaehler_qrcodes_${new Date().toISOString().split('T')[0]}.xlsx`;
|
|
|
|
return new NextResponse(buffer, {
|
|
status: 200,
|
|
headers: {
|
|
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
'Content-Disposition': `attachment; filename="${filename}"`,
|
|
},
|
|
});
|
|
} catch (err) {
|
|
console.error('QR-Code Excel Error:', err);
|
|
return NextResponse.json(
|
|
{ error: 'Interner Serverfehler.' },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|