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>
This commit is contained in:
Michael
2026-03-03 00:20:44 +01:00
parent 39eac91568
commit bb97c4b1fa
18 changed files with 4365 additions and 51 deletions

View File

@@ -359,20 +359,59 @@ export default function AdminDashboardPage() {
{/* Wasserzähler Tab */}
{activeTab === 'wasserzaehler' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-bold text-primary">Wasserzähler</h3>
<button
onClick={() => exportCSV(zaehler as unknown as Record<string, unknown>[], 'wasserzaehler')}
className="border border-border rounded-xl px-3 py-2.5 text-sm font-medium hover:bg-bg transition-colors"
>
CSV Export
</button>
<div className="flex items-center justify-between gap-2 flex-wrap">
<div>
<h3 className="text-lg font-bold text-primary">Wasserzähler</h3>
{zaehler.length > 0 && (
<p className="text-[11px] text-text-muted">
Letzter Import: {new Date(
Math.max(...zaehler.map(z => new Date(z.erstellt_am).getTime()))
).toLocaleDateString('de-AT', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })}
</p>
)}
</div>
<div className="flex gap-2 flex-wrap">
<button
onClick={() => router.push('/admin/zaehler-import')}
className="bg-accent text-white px-3 py-2.5 rounded-xl text-sm font-semibold hover:bg-accent-light active:scale-[0.98] transition-all flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
Stammdaten importieren
</button>
<button
onClick={() => router.push('/admin/serienbrief')}
className="border border-border bg-white px-3 py-2.5 rounded-xl text-sm font-semibold hover:bg-bg active:scale-[0.98] transition-all flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 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>
Serienbrief
</button>
<button
onClick={() => router.push('/admin/qrcodes')}
className="border border-border bg-white px-3 py-2.5 rounded-xl text-sm font-semibold hover:bg-bg active:scale-[0.98] transition-all flex items-center gap-1.5"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>
QR-Codes
</button>
<button
onClick={() => exportCSV(zaehler as unknown as Record<string, unknown>[], 'wasserzaehler')}
className="border border-border rounded-xl px-3 py-2.5 text-sm font-medium hover:bg-bg transition-colors"
>
CSV Export
</button>
</div>
</div>
<div className="bg-white rounded-2xl border border-border overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-bg/80 text-text-muted text-[11px] uppercase tracking-wider">
<tr>
<th className="px-4 py-3 text-left font-semibold">Kundennr.</th>
<th className="px-4 py-3 text-left font-semibold">Haushalt</th>
<th className="px-4 py-3 text-left font-semibold">Adresse</th>
<th className="px-4 py-3 text-left font-semibold">Zählernr.</th>
@@ -386,13 +425,14 @@ export default function AdminDashboardPage() {
<tbody className="divide-y divide-border/50">
{zaehler.length === 0 ? (
<tr>
<td colSpan={8} className="px-4 py-12 text-center text-text-muted">
<td colSpan={9} className="px-4 py-12 text-center text-text-muted">
Keine Wasserzähler gefunden.
</td>
</tr>
) : (
zaehler.map((z) => (
<tr key={z.id} className="hover:bg-bg/30 transition-colors">
<td className="px-4 py-3 font-mono text-xs font-semibold">{z.kundennummer}</td>
<td className="px-4 py-3 font-medium">{z.haushalt_name}</td>
<td className="px-4 py-3 text-text-muted">{z.adresse}</td>
<td className="px-4 py-3 font-mono text-xs">{z.zaehlernummer}</td>