211 lines
7.7 KiB
JavaScript
211 lines
7.7 KiB
JavaScript
/** PostgreSQL-Verbindung + Schema für das Mieterportal.
|
|
*
|
|
* Tabellen werden beim Start idempotent angelegt (CREATE TABLE IF NOT EXISTS),
|
|
* danach lädt seed.js die Demo-Daten, falls die Datenbank leer ist.
|
|
*/
|
|
|
|
const { Pool } = require('pg')
|
|
|
|
const pool = new Pool({
|
|
connectionString:
|
|
process.env.DATABASE_URL || 'postgres://mp:mp@localhost:5432/mieterportal',
|
|
max: 10,
|
|
})
|
|
|
|
const SCHEMA = `
|
|
CREATE TABLE IF NOT EXISTS units (
|
|
id TEXT PRIMARY KEY,
|
|
building TEXT NOT NULL,
|
|
number TEXT NOT NULL,
|
|
floor INT NOT NULL,
|
|
size NUMERIC(8,2) NOT NULL,
|
|
cold_rent NUMERIC(10,2) NOT NULL,
|
|
warm_rent NUMERIC(10,2) NOT NULL,
|
|
tenant TEXT,
|
|
status TEXT NOT NULL CHECK (status IN ('vermietet','frei'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS transactions (
|
|
id TEXT PRIMARY KEY,
|
|
unit_id TEXT NOT NULL REFERENCES units(id),
|
|
tenant TEXT NOT NULL,
|
|
month TEXT NOT NULL,
|
|
year INT NOT NULL,
|
|
amount NUMERIC(10,2) NOT NULL,
|
|
type TEXT NOT NULL,
|
|
status TEXT NOT NULL CHECK (status IN ('bezahlt','offen','überfällig')),
|
|
paid_at TEXT
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_transactions_unit ON transactions(unit_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS tickets (
|
|
id TEXT PRIMARY KEY,
|
|
unit_id TEXT NOT NULL REFERENCES units(id),
|
|
tenant TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
description TEXT NOT NULL DEFAULT '',
|
|
category TEXT NOT NULL,
|
|
priority TEXT NOT NULL CHECK (priority IN ('niedrig','mittel','hoch')),
|
|
status TEXT NOT NULL DEFAULT 'offen'
|
|
CHECK (status IN ('offen','in Bearbeitung','erledigt')),
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_tickets_unit ON tickets(unit_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS documents (
|
|
id TEXT PRIMARY KEY,
|
|
unit_id TEXT REFERENCES units(id), -- NULL = Hausdokument für alle
|
|
title TEXT NOT NULL,
|
|
category TEXT NOT NULL,
|
|
size TEXT NOT NULL,
|
|
date TEXT NOT NULL
|
|
);
|
|
ALTER TABLE documents ADD COLUMN IF NOT EXISTS file_name TEXT; -- Originalname beim Upload
|
|
ALTER TABLE documents ADD COLUMN IF NOT EXISTS file_path TEXT; -- Dateiname im Upload-Verzeichnis
|
|
|
|
CREATE TABLE IF NOT EXISTS threads (
|
|
id TEXT PRIMARY KEY,
|
|
unit_id TEXT NOT NULL REFERENCES units(id),
|
|
tenant TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
unread_by_admin BOOLEAN NOT NULL DEFAULT FALSE,
|
|
unread_by_tenant BOOLEAN NOT NULL DEFAULT FALSE
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_threads_unit ON threads(unit_id);
|
|
-- Verknüpfung Unterhaltung ↔ Schadensmeldung (v1.19)
|
|
ALTER TABLE threads ADD COLUMN IF NOT EXISTS ticket_id TEXT REFERENCES tickets(id);
|
|
CREATE INDEX IF NOT EXISTS idx_threads_ticket ON threads(ticket_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id TEXT PRIMARY KEY,
|
|
thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
|
|
from_role TEXT NOT NULL CHECK (from_role IN ('tenant','admin')),
|
|
author TEXT NOT NULL,
|
|
text TEXT NOT NULL,
|
|
at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS announcements (
|
|
id TEXT PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
text TEXT NOT NULL,
|
|
date TEXT NOT NULL,
|
|
tag TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id TEXT PRIMARY KEY,
|
|
benutzername TEXT NOT NULL UNIQUE,
|
|
name TEXT NOT NULL,
|
|
passwort_hash TEXT NOT NULL,
|
|
rolle TEXT NOT NULL CHECK (rolle IN ('verwaltung','mieter')),
|
|
email TEXT NOT NULL,
|
|
einheit_id TEXT REFERENCES units(id),
|
|
aktiv BOOLEAN NOT NULL DEFAULT TRUE,
|
|
erstellt_am TEXT NOT NULL,
|
|
letzter_login TEXT
|
|
);
|
|
-- Stammdaten zur Selbstpflege durch den Nutzer (v1.20)
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS telefon TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS kontoinhaber TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS iban TEXT;
|
|
ALTER TABLE users ADD COLUMN IF NOT EXISTS bankname TEXT;
|
|
|
|
CREATE TABLE IF NOT EXISTS registration_codes (
|
|
code TEXT PRIMARY KEY,
|
|
einheit_id TEXT REFERENCES units(id),
|
|
erstellt_am TEXT NOT NULL,
|
|
verwendet_am TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
token TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
expires TIMESTAMPTZ NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS meters (
|
|
id TEXT PRIMARY KEY,
|
|
unit_id TEXT REFERENCES units(id), -- NULL = Haus-/Allgemeinzähler
|
|
building TEXT NOT NULL,
|
|
nummer TEXT NOT NULL, -- Zählernummer
|
|
art TEXT NOT NULL CHECK (art IN ('Strom','Wasser','Gas','Wärme','Sonstiges')),
|
|
stand NUMERIC(12,3) NOT NULL,
|
|
stand_am TEXT NOT NULL, -- Datum des abgelesenen Standes
|
|
geaendert_am TEXT NOT NULL -- letzte Pflege durch die Verwaltung
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_meters_unit ON meters(unit_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS buildings (
|
|
name TEXT PRIMARY KEY, -- eindeutiger Hausname (z. B. "Hauptstraße 12")
|
|
adresse TEXT NOT NULL,
|
|
baujahr INT NOT NULL,
|
|
stockwerke INT NOT NULL,
|
|
etagenhoehe NUMERIC(4,2) NOT NULL, -- Deckenhöhe in m
|
|
wohneinheiten INT NOT NULL, -- Anzahl Wohneinheiten (Soll)
|
|
quadratmeter NUMERIC(9,1) NOT NULL -- Gesamtwohnfläche in m²
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS costs (
|
|
id TEXT PRIMARY KEY,
|
|
building TEXT NOT NULL REFERENCES buildings(name), -- Haus
|
|
unit_id TEXT REFERENCES units(id), -- NULL = hausweite Position
|
|
jahr INT NOT NULL,
|
|
kostenart TEXT NOT NULL,
|
|
betrag NUMERIC(10,2) NOT NULL,
|
|
umlegbar BOOLEAN NOT NULL DEFAULT TRUE, -- umlegbar / nicht umlegbar
|
|
notiz TEXT NOT NULL DEFAULT '',
|
|
erfasst_am TEXT NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_costs_building ON costs(building);
|
|
CREATE INDEX IF NOT EXISTS idx_costs_unit ON costs(unit_id);
|
|
-- FK hausweit: Umbenennung eines Hauses muss costs.building automatisch
|
|
-- mitziehen (sonst bricht jede Haus-Umbenennung mit FK-Verletzung ab,
|
|
-- sobald Kostenpositionen existieren). Constraint idempotent neu setzen.
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'costs_building_fkey') THEN
|
|
ALTER TABLE costs DROP CONSTRAINT costs_building_fkey;
|
|
END IF;
|
|
ALTER TABLE costs ADD CONSTRAINT costs_building_fkey
|
|
FOREIGN KEY (building) REFERENCES buildings(name) ON UPDATE CASCADE;
|
|
END $$;
|
|
|
|
-- Gescannte Belege aus SparkReceipt (Cloud-Archiv), per Webhook importiert.
|
|
-- (quelle, externe_id) eindeutig → wiederholte Events aktualisieren statt zu duplizieren.
|
|
CREATE TABLE IF NOT EXISTS belege (
|
|
id TEXT PRIMARY KEY,
|
|
quelle TEXT NOT NULL DEFAULT 'sparkreceipt',
|
|
externe_id TEXT NOT NULL,
|
|
haendler TEXT NOT NULL DEFAULT '',
|
|
betrag NUMERIC(10,2),
|
|
waehrung TEXT NOT NULL DEFAULT 'EUR',
|
|
datum TEXT NOT NULL DEFAULT '',
|
|
jahr INT,
|
|
kategorie TEXT NOT NULL DEFAULT '',
|
|
notiz TEXT NOT NULL DEFAULT '',
|
|
datei_url TEXT NOT NULL DEFAULT '',
|
|
zugeordnet BOOLEAN NOT NULL DEFAULT FALSE,
|
|
erfasst_am TEXT NOT NULL,
|
|
UNIQUE (quelle, externe_id)
|
|
);
|
|
-- Auto-Zuordnung: primär Stromzählernummer, sekundär Adresse (v1.17)
|
|
ALTER TABLE belege ADD COLUMN IF NOT EXISTS unit_id TEXT REFERENCES units(id);
|
|
ALTER TABLE belege ADD COLUMN IF NOT EXISTS gebaude TEXT;
|
|
ALTER TABLE belege ADD COLUMN IF NOT EXISTS zuordnung TEXT; -- zaehlernummer | adresse | manuell
|
|
ALTER TABLE belege ADD COLUMN IF NOT EXISTS rohtext TEXT; -- Volltext des Events für die Zuordnung
|
|
`
|
|
|
|
async function init() {
|
|
await pool.query(SCHEMA)
|
|
}
|
|
|
|
/** Aufräumen abgelaufener Sessions (gelegentlich aufgerufen). */
|
|
async function sessionsAufraeumen() {
|
|
await pool.query('DELETE FROM sessions WHERE expires < now()')
|
|
}
|
|
|
|
module.exports = { pool, init, sessionsAufraeumen }
|