Files
gemeindeportal/src/app/admin/qrcodes/page.tsx
Michael bb97c4b1fa Wasserzähler QR-Code System + Serienbrief + Admin Import
- 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>
2026-03-03 00:20:44 +01:00

231 lines
8.7 KiB
TypeScript

'use client';
import { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { createClient } from '@/lib/supabase/client';
import Header from '@/components/Header';
import QRCode from 'qrcode';
import { Wasserzaehler } from '@/types';
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL || 'https://gemeindeportal.vercel.app';
interface QRData {
zaehler: Wasserzaehler;
dataUrl: string;
}
export default function AdminQRCodesPage() {
const router = useRouter();
const supabase = createClient();
const [loading, setLoading] = useState(true);
const [qrItems, setQrItems] = useState<QRData[]>([]);
const [error, setError] = useState('');
const loadAndGenerate = useCallback(async () => {
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
router.push('/admin/login');
return;
}
const { data, error: fetchError } = await supabase
.from('wasserzaehler')
.select('*')
.order('kundennummer', { ascending: true });
if (fetchError) {
setError('Fehler beim Laden der Wasserzähler.');
setLoading(false);
return;
}
const zaehlerList: Wasserzaehler[] = data || [];
const items: QRData[] = await Promise.all(
zaehlerList.map(async (z) => {
const url = `${BASE_URL}/wasserzaehler?token=${z.access_token}`;
const dataUrl = await QRCode.toDataURL(url, {
width: 300,
margin: 2,
errorCorrectionLevel: 'M',
});
return { zaehler: z, dataUrl };
})
);
setQrItems(items);
} catch {
setError('Unerwarteter Fehler.');
} finally {
setLoading(false);
}
}, [supabase, router]);
useEffect(() => {
loadAndGenerate();
}, [loadAndGenerate]);
if (loading) {
return (
<div className="min-h-screen flex flex-col bg-bg">
<Header back={{ href: '/admin/dashboard', label: 'Dashboard' }} />
<main className="flex-1 max-w-4xl mx-auto px-4 py-6 w-full">
<div className="space-y-4">
<div className="skeleton h-8 w-64" />
<div className="skeleton h-4 w-48" />
<div className="skeleton h-96 w-full" />
</div>
</main>
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex flex-col bg-bg">
<Header back={{ href: '/admin/dashboard', label: 'Dashboard' }} />
<main className="flex-1 flex items-center justify-center px-4">
<div className="bg-white rounded-2xl border border-border p-8 max-w-sm w-full text-center">
<p className="text-danger">{error}</p>
</div>
</main>
</div>
);
}
return (
<div className="min-h-screen flex flex-col bg-bg">
{/* Screen-only header */}
<div className="print:hidden">
<Header back={{ href: '/admin/dashboard', label: 'Dashboard' }} />
{/* Admin Bar */}
<div className="bg-white border-b border-border">
<div className="max-w-4xl mx-auto px-4 py-3 flex items-center justify-between">
<div>
<h2 className="text-lg font-bold text-primary">QR-Codes Wasserzähler</h2>
<p className="text-sm text-text-muted">
{qrItems.length} Zähler je 1 Seite pro Kunde beim Drucken
</p>
</div>
<div className="flex gap-2">
<a
href="/api/zaehler-qrcodes-excel"
className="border border-border bg-white px-4 py-2.5 rounded-xl text-sm font-semibold hover:bg-bg active:scale-[0.98] transition-all flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Excel Download
</a>
<button
onClick={() => window.print()}
className="bg-accent text-white px-5 py-2.5 rounded-xl text-sm font-semibold hover:bg-accent-light active:scale-[0.98] transition-all flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
Drucken (Strg+P)
</button>
</div>
</div>
</div>
</div>
{/* Preview on screen / Print pages */}
<main className="flex-1 max-w-4xl mx-auto px-4 py-5 w-full print:max-w-none print:p-0 print:m-0">
{qrItems.length === 0 ? (
<div className="bg-white rounded-2xl border border-border p-12 text-center print:hidden">
<p className="text-text-muted">Keine Wasserzähler gefunden. Bitte zuerst das Import-Script ausführen.</p>
<code className="block mt-2 text-xs bg-bg p-2 rounded-lg">
npx tsx scripts/import-zaehler.ts zaehlerablesen_2025.xlsx
</code>
</div>
) : (
<div className="space-y-6 print:space-y-0">
{qrItems.map((item, index) => (
<div
key={item.zaehler.id}
className="bg-white rounded-2xl border border-border p-8 print:rounded-none print:border-0 print:p-0 print:break-after-page"
>
<div className="flex flex-col items-center justify-center print:min-h-screen print:py-16">
{/* Gemeinde Header */}
<div className="text-center mb-8 print:mb-12">
<h1 className="text-xl font-bold text-primary print:text-2xl">
Gemeindeamt Weißkirchen an der Traun
</h1>
<p className="text-text-muted text-sm mt-1 print:text-base">
Wasserzähler-Ablesung {new Date().getFullYear()}
</p>
</div>
{/* QR Code */}
<div className="mb-6 print:mb-10">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={item.dataUrl}
alt={`QR-Code für Kundennr. ${item.zaehler.kundennummer}`}
className="w-48 h-48 print:w-64 print:h-64"
/>
</div>
{/* Info */}
<div className="text-center space-y-2 print:space-y-3">
<div>
<span className="text-xs text-text-muted uppercase tracking-wider print:text-sm">Kundennummer</span>
<div className="text-2xl font-bold font-mono tracking-wider print:text-3xl">
{item.zaehler.kundennummer}
</div>
</div>
<div>
<span className="text-xs text-text-muted uppercase tracking-wider print:text-sm">Zählernummer</span>
<div className="text-lg font-semibold font-mono print:text-xl">
{item.zaehler.zaehlernummer}
</div>
</div>
</div>
{/* Instructions */}
<div className="mt-8 p-4 bg-bg rounded-xl max-w-sm text-center print:mt-12 print:bg-gray-50 print:max-w-md print:p-6">
<p className="text-sm text-text-muted print:text-base">
Scannen Sie den QR-Code mit Ihrem Smartphone um Ihren aktuellen Zählerstand zu melden.
</p>
</div>
{/* Screen-only badge */}
<div className="mt-4 text-xs text-text-muted print:hidden">
Seite {index + 1} von {qrItems.length}
</div>
</div>
</div>
))}
</div>
)}
</main>
{/* Print Styles */}
<style jsx global>{`
@media print {
body {
margin: 0 !important;
padding: 0 !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.print\\:hidden {
display: none !important;
}
.print\\:break-after-page {
break-after: page;
}
.print\\:min-h-screen {
min-height: 100vh;
}
}
`}</style>
</div>
);
}