v1.27: Zaehleranlage ohne Haus-Feld, Ablesedatum Pflicht
This commit is contained in:
@@ -189,6 +189,14 @@ tar -czf mp-dist.tar.gz -C dist .
|
|||||||
|
|
||||||
## 8. Verlauf / offene Punkte
|
## 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.
|
**Erledigt 04.10. (v1.26):** Haus-Formular ohne Namensfeld, Adresse in Einzelfelder.
|
||||||
buildings um strasse/hausnummer/plz/ort/lage/wohnflaeche/grundstuecksflaeche
|
buildings um strasse/hausnummer/plz/ort/lage/wohnflaeche/grundstuecksflaeche
|
||||||
erweitert (7 idempotente ALTERs, quadratmeter bleibt Alt-Spalte); name (PK) und
|
erweitert (7 idempotente ALTERs, quadratmeter bleibt Alt-Spalte); name (PK) und
|
||||||
|
|||||||
+5
-3
@@ -552,11 +552,13 @@ const ZAEHLER_ARTEN = ['Strom', 'Wasser', 'Gas', 'Wärme', 'Sonstiges']
|
|||||||
|
|
||||||
app.post('/api/meters', auth, adminOnly, async (req, res, next) => {
|
app.post('/api/meters', auth, adminOnly, async (req, res, next) => {
|
||||||
try {
|
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 (!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.' })
|
if (!ZAEHLER_ARTEN.includes(art)) return res.status(400).json({ fehler: 'Ungültige Zählerart.' })
|
||||||
const standZahl = Number(stand)
|
const standZahl = Number(stand)
|
||||||
if (!Number.isFinite(standZahl) || standZahl < 0) return res.status(400).json({ fehler: 'Ungültiger Zählerstand.' })
|
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
|
// Wohnung angegeben → Haus aus der Wohnung ableiten; sonst muss das Haus im Body stehen
|
||||||
let einheitId = unitId ? String(unitId) : null
|
let einheitId = unitId ? String(unitId) : null
|
||||||
let haus = String(building || '').trim()
|
let haus = String(building || '').trim()
|
||||||
@@ -571,11 +573,11 @@ app.post('/api/meters', auth, adminOnly, async (req, res, next) => {
|
|||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`INSERT INTO meters (id, unit_id, building, nummer, art, stand, stand_am, geaendert_am)
|
`INSERT INTO meters (id, unit_id, building, nummer, art, stand, stand_am, geaendert_am)
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING *`,
|
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
|
// Anfangsstand als erster Verlaufseintrag
|
||||||
await zaehlerVerlauf.standEintragen({
|
await zaehlerVerlauf.standEintragen({
|
||||||
meter: rows[0], stand: standZahl, standAm: deutschesDatum(),
|
meter: rows[0], stand: standZahl, standAm: datum,
|
||||||
erfasstVon: req.user.name, quelle: 'erstbefund',
|
erfasstVon: req.user.name, quelle: 'erstbefund',
|
||||||
})
|
})
|
||||||
res.json({ meter: mapMeter(rows[0]) })
|
res.json({ meter: mapMeter(rows[0]) })
|
||||||
|
|||||||
+58
-27
@@ -52,6 +52,10 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
|
|||||||
const [nummer, setNummer] = useState(meter?.nummer ?? '')
|
const [nummer, setNummer] = useState(meter?.nummer ?? '')
|
||||||
const [art, setArt] = useState<ZaehlerArt>(meter?.art ?? 'Strom')
|
const [art, setArt] = useState<ZaehlerArt>(meter?.art ?? 'Strom')
|
||||||
const [stand, setStand] = useState(String(meter?.stand ?? '').replace('.', ','))
|
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 [unitId, setUnitId] = useState<string>(meter?.unitId ?? 'haus')
|
||||||
const [building, setBuilding] = useState(meter?.building ?? buildings[0]?.name ?? '')
|
const [building, setBuilding] = useState(meter?.building ?? buildings[0]?.name ?? '')
|
||||||
const [sende, setSende] = useState(false)
|
const [sende, setSende] = useState(false)
|
||||||
@@ -60,16 +64,23 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
|
|||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const standZahl = Number(stand.replace(',', '.'))
|
const standZahl = Number(stand.replace(',', '.'))
|
||||||
if (!nummer.trim() || !Number.isFinite(standZahl) || standZahl < 0 || sende) return
|
if (!nummer.trim() || !Number.isFinite(standZahl) || standZahl < 0 || sende) return
|
||||||
|
if (!bearbeiten && !standAm) return
|
||||||
setSende(true)
|
setSende(true)
|
||||||
setFehler(null)
|
setFehler(null)
|
||||||
const payload = {
|
let err: string | null
|
||||||
nummer: nummer.trim(),
|
if (bearbeiten) {
|
||||||
art,
|
err = await updateZaehler(meter!.id, {
|
||||||
stand: standZahl,
|
nummer: nummer.trim(), art, stand: standZahl,
|
||||||
unitId: unitId === 'haus' ? null : unitId,
|
unitId: unitId === 'haus' ? null : unitId, building,
|
||||||
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)
|
setSende(false)
|
||||||
if (err) return setFehler(err)
|
if (err) return setFehler(err)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
@@ -115,15 +126,28 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<Label htmlFor="z-stand">Aktueller Zählerstand</Label>
|
<div className="space-y-1.5">
|
||||||
<Input
|
<Label htmlFor="z-stand">Aktueller Zählerstand</Label>
|
||||||
id="z-stand"
|
<Input
|
||||||
inputMode="decimal"
|
id="z-stand"
|
||||||
placeholder="z. B. 48231,4"
|
inputMode="decimal"
|
||||||
value={stand}
|
placeholder="z. B. 48231,4"
|
||||||
onChange={(e) => setStand(e.target.value)}
|
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>
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
@@ -139,17 +163,24 @@ function ZaehlerDialog({ meter }: { meter?: Meter }) {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
{unitId !== 'haus' && (
|
||||||
|
<p className="text-xs text-slate-400">Das Haus folgt automatisch der gewählten Wohneinheit.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
{unitId === 'haus' && (
|
||||||
<Label htmlFor="z-haus">Haus</Label>
|
<div className="space-y-1.5">
|
||||||
<Input
|
<Label>Haus</Label>
|
||||||
id="z-haus"
|
<Select value={building} onValueChange={setBuilding}>
|
||||||
value={building}
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
onChange={(e) => setBuilding(e.target.value)}
|
<SelectContent>
|
||||||
disabled={unitId !== 'haus'}
|
{buildings.map((b) => (
|
||||||
title={unitId !== 'haus' ? 'Das Haus wird aus der Wohneinheit übernommen.' : undefined}
|
<SelectItem key={b.name} value={b.name}>{b.adresse}</SelectItem>
|
||||||
/>
|
))}
|
||||||
</div>
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-slate-400">Nur nötig für Zähler ohne Wohnungsbezug.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{fehler && (
|
{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">
|
<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>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setOpen(false)}>Abbrechen</Button>
|
<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'}
|
{sende ? 'Wird gespeichert …' : bearbeiten ? 'Speichern' : 'Zähler anlegen'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ interface Store extends Bootstrap {
|
|||||||
nummer: string
|
nummer: string
|
||||||
art: ZaehlerArt
|
art: ZaehlerArt
|
||||||
stand: number
|
stand: number
|
||||||
|
standAm: string // Ablesedatum des Anfangsstands (TT.MM.JJJJ)
|
||||||
unitId: string | null
|
unitId: string | null
|
||||||
building: string
|
building: string
|
||||||
}) => Promise<string | null>
|
}) => Promise<string | null>
|
||||||
|
|||||||
Reference in New Issue
Block a user