v1.21: Live-Stammdaten aus Master-Data-CSV importiert, Sync verworfen, Demo-Reste entfernt
This commit is contained in:
@@ -33,5 +33,8 @@ tmp-*
|
||||
mp-dist.tar.gz
|
||||
shots/
|
||||
*.bak-v*
|
||||
# Live-Stammdaten (PII) – nur lokal, nicht ins Git
|
||||
Master Data V.csv
|
||||
import-*.sql
|
||||
.vite
|
||||
_ref/
|
||||
|
||||
@@ -189,6 +189,34 @@ tar -czf mp-dist.tar.gz -C dist .
|
||||
|
||||
## 8. Verlauf / offene Punkte
|
||||
|
||||
**Erledigt 04.10. (v1.21):** Live-Stammdaten aus SharePoint-Export-CSV
|
||||
importiert (KEIN Dauersync – Nutzer-Entscheidung: einmaliger Import,
|
||||
Pflege danach nur noch im Portal). CSV „Master Data V" (53 Zeilen,
|
||||
37 Spalten, 12 Häuser in Krefeld, 46 Wohneinheiten inkl. Garagen/Zimmer,
|
||||
43 Mietparteien) liegt NUR lokal im Workspace (PII, in .gitignore).
|
||||
Import-Skript server/import/import-masterdata.py (stdin CSV → stdout SQL,
|
||||
deterministische IDs slug(haus)-slug(we), deutsche Zahlenformate
|
||||
'2.700'→2700 / '71,4'→71.4 / Baujahr '1.905'→1905, PLZ '47.829'→47829,
|
||||
Etage aus 'Lage im Objekt' EG/1. OG/DG, WE01→'1'). buildings-NOT-NULL
|
||||
(baujahr/stockwerke/etagenhoehe/wohneinheiten/quadratmeter) per ALTER
|
||||
gelockert. Import ersetzt komplett: threads/tickets/transactions/costs/
|
||||
belege/documents/meters/codes + Mieter-User + units + buildings;
|
||||
Verwaltungs-User, Ankündigungen bleiben. Mieter bekommen Zufalls-Passwort-
|
||||
Hash (unloggbar bis Passwort-Reset auf 0815 durch Verwaltung in /benutzer),
|
||||
email='' (Mailversand überspringt leere Adressen, v1.20-Profil zum
|
||||
Nachpflegen). Zähler NICHT importiert (CSV-Spalten nur '0').
|
||||
Sync-Infrastruktur (v1.21-Vorentwicklung) komplett entfernt: mp-sync-cron
|
||||
Container gelöscht, portal.env von SP_*/INTERN_SYNC_TOKEN bereinigt,
|
||||
Code aus Arbeitsbaum entfernt (nie gepusht). WICHTIG seed.js-Fix:
|
||||
Demo-Nachseeds (seedeZaehler/Haeuser/Kosten) prüften nur Tabellen-Leere
|
||||
→ FK-Crashloop nach Import (hs-7 fehlt). Fix: demoBestandVorhanden()
|
||||
gate auf units.id='hs-7'. Frontend: hartkodierte Demo-Zahlen entfernt –
|
||||
AppLayout-Footer + Dashboard (Bestandszählung, Belegungsquote) jetzt
|
||||
dynamisch aus buildings/units, Hauslisten in Payments/Zaehler/Benutzer-
|
||||
Verwaltung dynamisch, Login-Demo-Zugang a.berger entfernt (existiert
|
||||
nicht mehr), DEMO_UNIT_ID='' (Fallback = erste Einheit). E2E: Bootstrap
|
||||
12 Häuser/46 Einheiten/43 Mieter, API healthy.
|
||||
|
||||
**Erledigt 04.10. (v1.20):** Stammdaten-Selbstpflege. users-Tabelle um
|
||||
telefon/kontoinhaber/iban/bankname erweitert (ALTER ... ADD COLUMN IF NOT
|
||||
EXISTS). PATCH /api/auth/profil (auth, eigene Daten): E-Mail (Pflicht +
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Einmaliger Import der Mieter-Stammdaten aus der SharePoint-Export-CSV
|
||||
(„Master Data V") in die PostgreSQL-Datenbank des Mieterportals.
|
||||
|
||||
Liest die CSV von stdin, schreibt SQL auf stdout:
|
||||
python import-masterdata.py < \"Master Data V.csv\" | \\
|
||||
docker exec -i mp-db psql -U mp -d mieterportal
|
||||
|
||||
ACHTUNG: Ersetzt die Demo-Daten (Häuser, Wohnungen, Mieter, Buchungen,
|
||||
Meldungen, Unterhaltungen, Dokumente, Zähler, Kosten, Belege, Codes).
|
||||
Verwaltungs-Konten, Ankündigungen und Sessions der Verwaltung bleiben.
|
||||
|
||||
IDs sind deterministisch (Slug aus Haus + WE) – das Skript ist damit
|
||||
fehlerverträglich wiederholbar, solange die CSV unverändert bleibt.
|
||||
Die CSV gehört NICHT ins Git (PII) – siehe .gitignore.
|
||||
"""
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
import secrets
|
||||
import sys
|
||||
from datetime import date
|
||||
|
||||
HEUTE = f'{date.today().day}.{date.today().month}.{date.today().year}'
|
||||
|
||||
|
||||
def q(wert):
|
||||
"""SQL-Escaping für Textfelder."""
|
||||
if wert is None:
|
||||
return 'NULL'
|
||||
return "'" + str(wert).replace("'", "''") + "'"
|
||||
|
||||
|
||||
def slug(text):
|
||||
s = str(text or '').strip().lower()
|
||||
s = (s.replace('ä', 'ae').replace('ö', 'oe').replace('ü', 'ue').replace('ß', 'ss'))
|
||||
s = re.sub(r'[^a-z0-9]+', '-', s).strip('-')
|
||||
return s or 'x'
|
||||
|
||||
|
||||
def zahl(wert):
|
||||
"""Deutsche Zahlenformate: '2.700' -> 2700, '71,4' -> 71.4, '' -> 0"""
|
||||
s = str(wert or '').strip()
|
||||
if not s:
|
||||
return 0.0
|
||||
s = s.replace('.', '').replace(',', '.')
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError:
|
||||
return 0.0
|
||||
|
||||
|
||||
def jahr(wert):
|
||||
"""'1.905' -> 1905, leer/0 -> None"""
|
||||
s = str(wert or '').strip()
|
||||
if not s:
|
||||
return None
|
||||
s = s.replace('.', '')
|
||||
try:
|
||||
j = int(s)
|
||||
except ValueError:
|
||||
return None
|
||||
return j if 1500 < j < 2100 else None
|
||||
|
||||
|
||||
def etage(lage):
|
||||
s = (lage or '').strip().lower()
|
||||
if s.startswith('eg') or 'hinterhof' in s:
|
||||
return 0
|
||||
m = re.search(r'(\d+)\.\s*og', s)
|
||||
if m:
|
||||
return int(m.group(1))
|
||||
if s.startswith('dg'):
|
||||
return 4
|
||||
return 0
|
||||
|
||||
|
||||
def wohnungsnummer(we):
|
||||
"""'WE01' -> '1', 'Garage 1' bleibt, 'Zimmer 2' bleibt."""
|
||||
s = str(we or '').strip()
|
||||
m = re.match(r'^WE0*(\d+)$', s, re.IGNORECASE)
|
||||
if m:
|
||||
return str(int(m.group(1)))
|
||||
return s
|
||||
|
||||
|
||||
def main():
|
||||
eingabe = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8-sig')
|
||||
zeilen = list(csv.DictReader(eingabe))
|
||||
if not zeilen:
|
||||
sys.exit('CSV ist leer oder konnte nicht gelesen werden.')
|
||||
|
||||
sql = []
|
||||
sql.append('BEGIN;')
|
||||
# buildings: NOT NULL lockern – die CSV liefert nicht alle Pflichtfelder
|
||||
for spalte in ('baujahr', 'stockwerke', 'etagenhoehe', 'wohneinheiten', 'quadratmeter'):
|
||||
sql.append(f'ALTER TABLE buildings ALTER COLUMN {spalte} DROP NOT NULL;')
|
||||
|
||||
# ── Demo-Daten entfernen (Reihenfolge wegen Fremdschlüsseln) ──
|
||||
sql += [
|
||||
'DELETE FROM threads;',
|
||||
'DELETE FROM tickets;',
|
||||
'DELETE FROM transactions;',
|
||||
'DELETE FROM costs;',
|
||||
'DELETE FROM belege;',
|
||||
'DELETE FROM documents;',
|
||||
'DELETE FROM meters;',
|
||||
'DELETE FROM registration_codes;',
|
||||
"DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE rolle = 'mieter');",
|
||||
"DELETE FROM users WHERE rolle = 'mieter';",
|
||||
'DELETE FROM units;',
|
||||
'DELETE FROM buildings;',
|
||||
]
|
||||
|
||||
# ── Häuser ──
|
||||
haeuser = []
|
||||
for z in zeilen:
|
||||
t = z['Titel'].strip()
|
||||
if t and t not in haeuser:
|
||||
haeuser.append(t)
|
||||
for t in haeuser:
|
||||
zeilen_haus = [z for z in zeilen if z['Titel'].strip() == t]
|
||||
we_zeilen = [z for z in zeilen_haus if z['WE'].strip()]
|
||||
strasse = zeilen_haus[0]['Straße'].strip()
|
||||
plz = zeilen_haus[0]['PLZ'].strip().replace('.', '')
|
||||
stadt = zeilen_haus[0]['Stadt'].strip()
|
||||
adresse = ', '.join(x for x in (strasse, f'{plz} {stadt}'.strip()) if x)
|
||||
bj = jahr(zeilen_haus[0]['Baujahr'])
|
||||
qm = sum(zahl(z['m²']) for z in we_zeilen) or None
|
||||
wohnungen = len(we_zeilen)
|
||||
sql.append(
|
||||
'INSERT INTO buildings (name, adresse, baujahr, stockwerke, etagenhoehe, wohneinheiten, quadratmeter)'
|
||||
f' VALUES ({q(t)}, {q(adresse)}, {bj if bj else "NULL"}, NULL, NULL, {wohnungen}, {qm if qm else "NULL"});',
|
||||
)
|
||||
|
||||
# ── Wohneinheiten + Mieter ──
|
||||
warnungen = []
|
||||
for z in zeilen:
|
||||
we = z['WE'].strip()
|
||||
if not we:
|
||||
continue
|
||||
titel = z['Titel'].strip()
|
||||
einheit_id = f"{slug(titel)}-{slug(we)}"
|
||||
nummer = wohnungsnummer(we)
|
||||
mieter = z['Name des Mieters'].strip()
|
||||
kalt = zahl(z['Nettokaltmiete'])
|
||||
warm = kalt + zahl(z['Nebenkosten'])
|
||||
sql.append(
|
||||
'INSERT INTO units (id, building, number, floor, size, cold_rent, warm_rent, tenant, status)'
|
||||
f' VALUES ({q(einheit_id)}, {q(titel)}, {q(nummer)}, {etage(z["Lage im Objekt"])},'
|
||||
f' {zahl(z["m²"])}, {kalt}, {warm}, {q(mieter) if mieter else "NULL"},'
|
||||
f" '{('vermietet' if mieter else 'frei')}');",
|
||||
)
|
||||
if mieter:
|
||||
nr = z['Nr. Mietpartei'].strip() or slug(we)
|
||||
benutzername = f"{slug(titel)}-{nr}"
|
||||
# Zufälliger unbekannter Hash -> Mieter kann sich erst nach
|
||||
# Passwort-Reset durch die Verwaltung (Button in /benutzer) anmelden.
|
||||
passwort_hash = secrets.token_hex(32)
|
||||
sql.append(
|
||||
'INSERT INTO users (id, benutzername, name, passwort_hash, rolle, email, einheit_id, aktiv, erstellt_am)'
|
||||
f" VALUES ({q('b-' + einheit_id)}, {q(benutzername)}, {q(mieter)}, {q(passwort_hash)},"
|
||||
f" 'mieter', '', {q(einheit_id)}, TRUE, {q(HEUTE)});",
|
||||
)
|
||||
# Zähler werden NICHT importiert: die CSV-Zählerspalten sind leer/'0'
|
||||
if z['Stromzähler'].strip() not in ('', '0'):
|
||||
warnungen.append(f"Stromzähler in {titel}/{we}: {z['Stromzähler'].strip()} (nicht importiert)")
|
||||
|
||||
sql.append('COMMIT;')
|
||||
|
||||
sys.stdout.write('\n'.join(sql) + '\n')
|
||||
print(f'-- {len(haeuser)} Häuser, '
|
||||
f"{sum(1 for z in zeilen if z['WE'].strip())} Wohneinheiten, "
|
||||
f"{sum(1 for z in zeilen if z['Name des Mieters'].strip())} Mieter", file=sys.stderr)
|
||||
for w in warnungen:
|
||||
print('-- HINWEIS: ' + w, file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+11
-3
@@ -107,12 +107,20 @@ async function seed() {
|
||||
|
||||
module.exports = { seed }
|
||||
|
||||
/** Der Demo-Bestand (Einheit hs-7) muss existieren, sonst gehören die
|
||||
* Demo-Nachseeds (Zähler/Häuser/Kosten) nicht in diese Datenbank – z. B. nach
|
||||
* dem Live-Import (v1.21) darf nichts mehr nachgeseedet werden. */
|
||||
async function demoBestandVorhanden() {
|
||||
const { rows } = await pool.query(`SELECT 1 FROM units WHERE id = 'hs-7'`)
|
||||
return rows.length > 0
|
||||
}
|
||||
|
||||
/** Zähler-Demo-Daten: idempotent – läuft bei jedem Start und füllt die
|
||||
* Zähler-Tabelle nur, wenn sie leer ist (auch nachträglich auf bestehenden
|
||||
* Datenbanken, z. B. nach dem v1.9-Update). */
|
||||
async function seedeZaehler() {
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS n FROM meters')
|
||||
if (rows[0].n > 0) return false
|
||||
if (rows[0].n > 0 || !(await demoBestandVorhanden())) return false
|
||||
for (const z of meters) {
|
||||
await pool.query(
|
||||
`INSERT INTO meters (id, unit_id, building, nummer, art, stand, stand_am, geaendert_am)
|
||||
@@ -129,7 +137,7 @@ module.exports = { seed, seedeZaehler }
|
||||
* leer ist (auch nachträglich auf bestehenden Datenbanken, z. B. v1.10). */
|
||||
async function seedeHaeuser() {
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS n FROM buildings')
|
||||
if (rows[0].n > 0) return false
|
||||
if (rows[0].n > 0 || !(await demoBestandVorhanden())) return false
|
||||
for (const h of buildings) {
|
||||
await pool.query(
|
||||
`INSERT INTO buildings (name, adresse, baujahr, stockwerke, etagenhoehe, wohneinheiten, quadratmeter)
|
||||
@@ -146,7 +154,7 @@ module.exports = { seed, seedeZaehler, seedeHaeuser }
|
||||
* leer ist (auch nachträglich auf bestehenden Datenbanken, z. B. v1.14). */
|
||||
async function seedeKosten() {
|
||||
const { rows } = await pool.query('SELECT COUNT(*)::int AS n FROM costs')
|
||||
if (rows[0].n > 0) return false
|
||||
if (rows[0].n > 0 || !(await demoBestandVorhanden())) return false
|
||||
for (const k of kosten) {
|
||||
await pool.query(
|
||||
`INSERT INTO costs (id, building, unit_id, jahr, kostenart, betrag, umlegbar, notiz, erfasst_am)
|
||||
|
||||
@@ -28,7 +28,7 @@ const navItems = [
|
||||
]
|
||||
|
||||
export default function AppLayout() {
|
||||
const { role, me, logout, threads } = useStore()
|
||||
const { role, me, logout, threads, buildings, units } = useStore()
|
||||
const navigate = useNavigate()
|
||||
const unread = threads.filter((t) =>
|
||||
role === 'tenant' ? t.unreadByTenant : t.unreadByAdmin,
|
||||
@@ -96,7 +96,7 @@ export default function AppLayout() {
|
||||
<div className="border-t border-white/10 p-4">
|
||||
<div className="mb-2 flex flex-wrap items-center gap-x-2 gap-y-1 px-2 text-xs text-slate-300/70">
|
||||
<Building2 className="h-3.5 w-3.5 shrink-0" />
|
||||
<span>3 Häuser · 50 Wohnungen</span>
|
||||
<span>{buildings.length} {buildings.length === 1 ? 'Haus' : 'Häuser'} · {units.length} {units.length === 1 ? 'Wohnung' : 'Wohnungen'}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
|
||||
@@ -37,7 +37,7 @@ function NeuerBenutzerDialog() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [rolle, setRolle] = useState<BenutzerRolle>('mieter')
|
||||
const [pw, setPw] = useState('0815')
|
||||
const [einheitId, setEinheitId] = useState('hs-7')
|
||||
const [einheitId, setEinheitId] = useState(units[0]?.id ?? '')
|
||||
const [fehler, setFehler] = useState<string | null>(null)
|
||||
|
||||
const submit = async () => {
|
||||
|
||||
@@ -215,7 +215,7 @@ function TenantDashboard() {
|
||||
}
|
||||
|
||||
function AdminDashboard() {
|
||||
const { units, transactions, tickets, announcements } = useStore()
|
||||
const { units, transactions, tickets, announcements, buildings: gebaeude } = useStore()
|
||||
const rented = units.filter((u) => u.status === 'vermietet')
|
||||
const occupancy = Math.round((rented.length / units.length) * 100)
|
||||
const monthlyIncome = rented.reduce((s, u) => s + u.warmRent, 0)
|
||||
@@ -229,10 +229,10 @@ function AdminDashboard() {
|
||||
.reduce((s, t) => s + t.amount, 0),
|
||||
}))
|
||||
|
||||
const buildings = ['Hauptstraße 12', 'Ringweg 3', 'Am Park 5'].map((b) => {
|
||||
const us = units.filter((u) => u.building === b)
|
||||
const buildings = gebaeude.map((g) => {
|
||||
const us = units.filter((u) => u.building === g.name)
|
||||
return {
|
||||
name: b,
|
||||
name: g.name,
|
||||
Vermietet: us.filter((u) => u.status === 'vermietet').length,
|
||||
Frei: us.filter((u) => u.status === 'frei').length,
|
||||
}
|
||||
@@ -243,13 +243,13 @@ function AdminDashboard() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Verwaltungs-Dashboard</h1>
|
||||
<p className="text-sm text-slate-500">
|
||||
Bestand: 3 Häuser · 50 Wohnungen · {rented.length} vermietet
|
||||
Bestand: {buildings.length} {buildings.length === 1 ? 'Haus' : 'Häuser'} · {units.length} Wohneinheiten · {rented.length} vermietet
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<StatCard icon={Building2} label="Wohnungen gesamt" value="50" sub={`${units.filter((u) => u.status === 'frei').length} aktuell frei`} />
|
||||
<StatCard icon={Users} label="Vermietungsquote" value={`${occupancy} %`} sub={`${rented.length} von 50 Wohnungen`} />
|
||||
<StatCard icon={Building2} label="Wohneinheiten gesamt" value={String(units.length)} sub={`${units.filter((u) => u.status === 'frei').length} aktuell frei`} />
|
||||
<StatCard icon={Users} label="Vermietungsquote" value={`${occupancy} %`} sub={`${rented.length} von ${units.length} Wohneinheiten`} />
|
||||
<StatCard icon={Euro} label="Monatliche Warmmiete (Soll)" value={fmtEUR.format(monthlyIncome)} sub="Summe aller vermieteten Einheiten" />
|
||||
<StatCard
|
||||
icon={CircleAlert}
|
||||
|
||||
@@ -137,7 +137,7 @@ function DokumentDialog({ dokument, einheiten }: { dokument?: DocumentItem; einh
|
||||
<Label htmlFor="d-titel">Titel</Label>
|
||||
<Input
|
||||
id="d-titel"
|
||||
placeholder="z. B. Mietvertrag – Hauptstraße 12, Whg. 7"
|
||||
placeholder="z. B. Mietvertrag – Blücherstr. 59, Whg. 1"
|
||||
value={titel}
|
||||
onChange={(e) => setTitel(e.target.value)}
|
||||
/>
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@ const features = [
|
||||
|
||||
const demoZugaenge = [
|
||||
{ benutzername: 'j.hinterstein', label: 'Joseph Hinterstein (Verwaltung)' },
|
||||
{ benutzername: 'a.berger', label: 'Anna Berger (Mieterin)' },
|
||||
]
|
||||
|
||||
export default function Login() {
|
||||
@@ -84,7 +83,7 @@ export default function Login() {
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-[#22333b]/85 via-transparent to-transparent" />
|
||||
<div className="absolute bottom-0 p-6 text-white">
|
||||
<div className="text-lg font-semibold">Ihr Zuhause. Ihr Portal.</div>
|
||||
<div className="text-sm text-slate-200">3 Wohnanlagen · 50 Wohnungen · persönliche Verwaltung</div>
|
||||
<div className="text-sm text-slate-200">Ihr Bestand. Ihre Mieter. Ein Portal.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export default function Messages() {
|
||||
<p className="text-sm text-slate-500">
|
||||
{role === 'tenant'
|
||||
? 'Direktdraht zur Verwaltung – Antwort meist innerhalb eines Werktags'
|
||||
: 'Mieter-Kommunikation über alle 50 Wohnungen'}
|
||||
: 'Mieter-Kommunikation über den gesamten Bestand'}
|
||||
</p>
|
||||
</div>
|
||||
<NewMessageDialog onCreated={setActiveId} />
|
||||
|
||||
@@ -138,7 +138,7 @@ function TenantPayments() {
|
||||
}
|
||||
|
||||
function AdminPayments() {
|
||||
const { units, transactions } = useStore()
|
||||
const { units, transactions, buildings } = useStore()
|
||||
const [building, setBuilding] = useState('alle')
|
||||
|
||||
const list = units
|
||||
@@ -161,7 +161,7 @@ function AdminPayments() {
|
||||
<div className="flex flex-wrap items-end justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">Mietkonto & Zahlungen</h1>
|
||||
<p className="text-sm text-slate-500">Forderungsmanagement für 50 Wohnungen</p>
|
||||
<p className="text-sm text-slate-500">Forderungsmanagement für den gesamten Bestand</p>
|
||||
</div>
|
||||
<Select value={building} onValueChange={setBuilding}>
|
||||
<SelectTrigger className="w-56">
|
||||
@@ -169,9 +169,9 @@ function AdminPayments() {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="alle">Alle Häuser</SelectItem>
|
||||
<SelectItem value="Hauptstraße 12">Hauptstraße 12</SelectItem>
|
||||
<SelectItem value="Ringweg 3">Ringweg 3</SelectItem>
|
||||
<SelectItem value="Am Park 5">Am Park 5</SelectItem>
|
||||
{buildings.map((b) => (
|
||||
<SelectItem key={b.name} value={b.name}>{b.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -46,14 +46,14 @@ const fmtStand = (n: number) =>
|
||||
n.toLocaleString('de-DE', { maximumFractionDigits: 3 })
|
||||
|
||||
function ZaehlerDialog({ meter }: { meter?: Meter }) {
|
||||
const { addZaehler, updateZaehler, units } = useStore()
|
||||
const { addZaehler, updateZaehler, units, buildings } = useStore()
|
||||
const bearbeiten = Boolean(meter)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [nummer, setNummer] = useState(meter?.nummer ?? '')
|
||||
const [art, setArt] = useState<ZaehlerArt>(meter?.art ?? 'Strom')
|
||||
const [stand, setStand] = useState(String(meter?.stand ?? '').replace('.', ','))
|
||||
const [unitId, setUnitId] = useState<string>(meter?.unitId ?? 'haus')
|
||||
const [building, setBuilding] = useState(meter?.building ?? 'Hauptstraße 12')
|
||||
const [building, setBuilding] = useState(meter?.building ?? buildings[0]?.name ?? '')
|
||||
const [sende, setSende] = useState(false)
|
||||
const [fehler, setFehler] = useState<string | null>(null)
|
||||
|
||||
|
||||
+3
-3
@@ -38,9 +38,9 @@ import type {
|
||||
import { apiGet, apiPost, apiPatch, apiDelete, ApiFehler } from '../lib/api'
|
||||
import { sha256, DEMO_PASSWORT } from '../lib/auth'
|
||||
|
||||
/** Demo-Einheit als Fallback, falls ein Mieter noch keine Wohnung zugewiesen hat. */
|
||||
export const DEMO_TENANT = 'Anna Berger'
|
||||
export const DEMO_UNIT_ID = 'hs-7'
|
||||
/** Fallback: Mieter ohne Wohnungszuweisung sehen die erste Einheit des Bestands. */
|
||||
export const DEMO_TENANT = ''
|
||||
export const DEMO_UNIT_ID = ''
|
||||
|
||||
interface Bootstrap {
|
||||
units: Unit[]
|
||||
|
||||
Reference in New Issue
Block a user