v1.27: Zaehleranlage ohne Haus-Feld, Ablesedatum Pflicht

This commit is contained in:
2026-10-04 17:22:14 +02:00
parent 6e2d783e12
commit 3753cec87c
4 changed files with 72 additions and 30 deletions
+8
View File
@@ -189,6 +189,14 @@ tar -czf mp-dist.tar.gz -C dist .
## 8. Verlauf / offene Punkte
**Erledigt 04.10. (v1.27):** Zähleranlage ohne Haus-Feld, mit Ablesedatum.
POST /api/meters akzeptiert standAm (Validierung via zaehlerVerlauf.parseDatum,
Default heute); stand_am + erster Verlaufseintrag (erstbefund) nutzen das Datum.
Frontend ZaehlerDialog: Haus-Textfeld entfernt – Haus folgt der Wohneinheit,
nur bei „Allgemeinzähler (ohne Wohnung)" erscheint eine Haus-Auswahl
(buildings-Select, Anzeige b.adresse). Ablesedatum (type=date, Default heute,
Pflicht) nur bei Neuanlage; Frontend wandelt YYYY-MM-DD → TT.MM.JJJJ.
**Erledigt 04.10. (v1.26):** Haus-Formular ohne Namensfeld, Adresse in Einzelfelder.
buildings um strasse/hausnummer/plz/ort/lage/wohnflaeche/grundstuecksflaeche
erweitert (7 idempotente ALTERs, quadratmeter bleibt Alt-Spalte); name (PK) und
+5 -3
View File
@@ -552,11 +552,13 @@ const ZAEHLER_ARTEN = ['Strom', 'Wasser', 'Gas', 'Wärme', 'Sonstiges']
app.post('/api/meters', auth, adminOnly, async (req, res, next) => {
try {
const { nummer, art, stand, unitId, building } = req.body || {}
const { nummer, art, stand, unitId, building, standAm } = req.body || {}
if (!String(nummer || '').trim()) return res.status(400).json({ fehler: 'Zählernummer fehlt.' })
if (!ZAEHLER_ARTEN.includes(art)) return res.status(400).json({ fehler: 'Ungültige Zählerart.' })
const standZahl = Number(stand)
if (!Number.isFinite(standZahl) || standZahl < 0) return res.status(400).json({ fehler: 'Ungültiger Zählerstand.' })
const datum = String(standAm || '').trim() || deutschesDatum()
if (!zaehlerVerlauf.parseDatum(datum)) return res.status(400).json({ fehler: 'Ungültiges Ablesedatum.' })
// Wohnung angegeben → Haus aus der Wohnung ableiten; sonst muss das Haus im Body stehen
let einheitId = unitId ? String(unitId) : null
let haus = String(building || '').trim()
@@ -571,11 +573,11 @@ app.post('/api/meters', auth, adminOnly, async (req, res, next) => {
const { rows } = await pool.query(
`INSERT INTO meters (id, unit_id, building, nummer, art, stand, stand_am, geaendert_am)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
[id, einheitId, haus, String(nummer).trim(), art, standZahl, deutschesDatum(), deutschesDatumZeit()],
[id, einheitId, haus, String(nummer).trim(), art, standZahl, datum, deutschesDatumZeit()],
)
// Anfangsstand als erster Verlaufseintrag
await zaehlerVerlauf.standEintragen({
meter: rows[0], stand: standZahl, standAm: deutschesDatum(),
meter: rows[0], stand: standZahl, standAm: datum,
erfasstVon: req.user.name, quelle: 'erstbefund',
})
res.json({ meter: mapMeter(rows[0]) })
+58 -27
View File
@@ -52,6 +52,10 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
const [nummer, setNummer] = useState(meter?.nummer ?? '')
const [art, setArt] = useState<ZaehlerArt>(meter?.art ?? 'Strom')
const [stand, setStand] = useState(String(meter?.stand ?? '').replace('.', ','))
const [standAm, setStandAm] = useState(() => {
const d = new Date()
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
})
const [unitId, setUnitId] = useState<string>(meter?.unitId ?? 'haus')
const [building, setBuilding] = useState(meter?.building ?? buildings[0]?.name ?? '')
const [sende, setSende] = useState(false)
@@ -60,16 +64,23 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
const submit = async () => {
const standZahl = Number(stand.replace(',', '.'))
if (!nummer.trim() || !Number.isFinite(standZahl) || standZahl < 0 || sende) return
if (!bearbeiten && !standAm) return
setSende(true)
setFehler(null)
const payload = {
nummer: nummer.trim(),
art,
stand: standZahl,
unitId: unitId === 'haus' ? null : unitId,
building,
let err: string | null
if (bearbeiten) {
err = await updateZaehler(meter!.id, {
nummer: nummer.trim(), art, stand: standZahl,
unitId: unitId === 'haus' ? null : unitId, building,
})
} else {
err = await addZaehler({
nummer: nummer.trim(), art, stand: standZahl,
// „YYYY-MM-DD" (Datumseingabe) → „TT.MM.JJJJ" (Serverformat)
standAm: standAm.split('-').reverse().join('.'),
unitId: unitId === 'haus' ? null : unitId, building,
})
}
const err = bearbeiten ? await updateZaehler(meter!.id, payload) : await addZaehler(payload)
setSende(false)
if (err) return setFehler(err)
setOpen(false)
@@ -115,15 +126,28 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
</Select>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="z-stand">Aktueller Zählerstand</Label>
<Input
id="z-stand"
inputMode="decimal"
placeholder="z. B. 48231,4"
value={stand}
onChange={(e) => setStand(e.target.value)}
/>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
<Label htmlFor="z-stand">Aktueller Zählerstand</Label>
<Input
id="z-stand"
inputMode="decimal"
placeholder="z. B. 48231,4"
value={stand}
onChange={(e) => setStand(e.target.value)}
/>
</div>
{!bearbeiten && (
<div className="space-y-1.5">
<Label htmlFor="z-datum">Ablesedatum</Label>
<Input
id="z-datum"
type="date"
value={standAm}
onChange={(e) => setStandAm(e.target.value)}
/>
</div>
)}
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-1.5">
@@ -139,17 +163,24 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
))}
</SelectContent>
</Select>
{unitId !== 'haus' && (
<p className="text-xs text-slate-400">Das Haus folgt automatisch der gewählten Wohneinheit.</p>
)}
</div>
<div className="space-y-1.5">
<Label htmlFor="z-haus">Haus</Label>
<Input
id="z-haus"
value={building}
onChange={(e) => setBuilding(e.target.value)}
disabled={unitId !== 'haus'}
title={unitId !== 'haus' ? 'Das Haus wird aus der Wohneinheit übernommen.' : undefined}
/>
</div>
{unitId === 'haus' && (
<div className="space-y-1.5">
<Label>Haus</Label>
<Select value={building} onValueChange={setBuilding}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{buildings.map((b) => (
<SelectItem key={b.name} value={b.name}>{b.adresse}</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-slate-400">Nur nötig für Zähler ohne Wohnungsbezug.</p>
</div>
)}
</div>
{fehler && (
<div className="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950 dark:text-red-300" role="alert">
@@ -159,7 +190,7 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
<Button onClick={submit} disabled={!nummer.trim() || !stand.trim() || sende}>
<Button onClick={submit} disabled={!nummer.trim() || !stand.trim() || (!bearbeiten && !standAm) || sende}>
{sende ? 'Wird gespeichert …' : bearbeiten ? 'Speichern' : 'Zähler anlegen'}
</Button>
</DialogFooter>
+1
View File
@@ -123,6 +123,7 @@ interface Store extends Bootstrap {
nummer: string
art: ZaehlerArt
stand: number
standAm: string // Ablesedatum des Anfangsstands (TT.MM.JJJJ)
unitId: string | null
building: string
}) => Promise<string | null>