add features
This commit is contained in:
201
backend/src/database/migrations/006_create_uebungen.sql
Normal file
201
backend/src/database/migrations/006_create_uebungen.sql
Normal file
@@ -0,0 +1,201 @@
|
||||
-- =============================================================================
|
||||
-- Migration 006: Übungsplanung & Dienstkalender
|
||||
-- Training Schedule & Service Calendar for Feuerwehr Rems Dashboard
|
||||
-- =============================================================================
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1. Calendar token table for iCal subscribe URLs (no auth required per-request)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS calendar_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token VARCHAR(64) UNIQUE NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX idx_calendar_tokens_token ON calendar_tokens(token);
|
||||
CREATE INDEX idx_calendar_tokens_user_id ON calendar_tokens(user_id);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2. Main events table
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TYPE uebung_typ AS ENUM (
|
||||
'Übungsabend',
|
||||
'Lehrgang',
|
||||
'Sonderdienst',
|
||||
'Versammlung',
|
||||
'Gemeinschaftsübung',
|
||||
'Sonstiges'
|
||||
);
|
||||
|
||||
CREATE TABLE uebungen (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
titel VARCHAR(255) NOT NULL,
|
||||
beschreibung TEXT,
|
||||
typ uebung_typ NOT NULL,
|
||||
datum_von TIMESTAMPTZ NOT NULL,
|
||||
datum_bis TIMESTAMPTZ NOT NULL,
|
||||
ort VARCHAR(255),
|
||||
treffpunkt VARCHAR(255),
|
||||
pflichtveranstaltung BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mindest_teilnehmer INT CHECK (mindest_teilnehmer > 0),
|
||||
max_teilnehmer INT CHECK (max_teilnehmer > 0),
|
||||
angelegt_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
aktualisiert_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
abgesagt BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
absage_grund TEXT,
|
||||
|
||||
CONSTRAINT datum_reihenfolge CHECK (datum_bis >= datum_von),
|
||||
CONSTRAINT max_groesser_min CHECK (
|
||||
max_teilnehmer IS NULL OR
|
||||
mindest_teilnehmer IS NULL OR
|
||||
max_teilnehmer >= mindest_teilnehmer
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_uebungen_datum_von ON uebungen(datum_von);
|
||||
CREATE INDEX idx_uebungen_typ ON uebungen(typ);
|
||||
CREATE INDEX idx_uebungen_pflichtveranstaltung ON uebungen(pflichtveranstaltung) WHERE pflichtveranstaltung = TRUE;
|
||||
CREATE INDEX idx_uebungen_abgesagt ON uebungen(abgesagt) WHERE abgesagt = FALSE;
|
||||
-- Compound index for the most common calendar-range query
|
||||
CREATE INDEX idx_uebungen_datum_von_bis ON uebungen(datum_von, datum_bis);
|
||||
|
||||
-- Keep aktualisiert_am in sync via trigger (reuse function from migration 001)
|
||||
CREATE TRIGGER update_uebungen_aktualisiert_am
|
||||
BEFORE UPDATE ON uebungen
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3. Attendance / RSVP table
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TYPE teilnahme_status AS ENUM (
|
||||
'zugesagt',
|
||||
'abgesagt',
|
||||
'erschienen',
|
||||
'entschuldigt',
|
||||
'unbekannt'
|
||||
);
|
||||
|
||||
CREATE TABLE uebung_teilnahmen (
|
||||
uebung_id UUID NOT NULL REFERENCES uebungen(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
status teilnahme_status NOT NULL DEFAULT 'unbekannt',
|
||||
antwort_am TIMESTAMPTZ,
|
||||
erschienen_erfasst_am TIMESTAMPTZ,
|
||||
erschienen_erfasst_von UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
bemerkung VARCHAR(500),
|
||||
|
||||
PRIMARY KEY (uebung_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_teilnahmen_uebung_id ON uebung_teilnahmen(uebung_id);
|
||||
CREATE INDEX idx_teilnahmen_user_id ON uebung_teilnahmen(user_id);
|
||||
CREATE INDEX idx_teilnahmen_status ON uebung_teilnahmen(status);
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4. Trigger: auto-create 'unbekannt' rows for all active members on new event
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION fn_create_teilnahmen_for_all_active_members()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
INSERT INTO uebung_teilnahmen (uebung_id, user_id, status)
|
||||
SELECT NEW.id, u.id, 'unbekannt'
|
||||
FROM users u
|
||||
WHERE u.is_active = TRUE
|
||||
ON CONFLICT (uebung_id, user_id) DO NOTHING;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_auto_teilnahmen_after_insert
|
||||
AFTER INSERT ON uebungen
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION fn_create_teilnahmen_for_all_active_members();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5. Trigger: when a new member becomes active, add them to all future events
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE FUNCTION fn_add_member_to_future_events()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Only run when is_active transitions FALSE -> TRUE
|
||||
IF (OLD.is_active = FALSE OR OLD.is_active IS NULL) AND NEW.is_active = TRUE THEN
|
||||
INSERT INTO uebung_teilnahmen (uebung_id, user_id, status)
|
||||
SELECT u.id, NEW.id, 'unbekannt'
|
||||
FROM uebungen u
|
||||
WHERE u.datum_von > NOW()
|
||||
AND u.abgesagt = FALSE
|
||||
ON CONFLICT (uebung_id, user_id) DO NOTHING;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_add_member_to_future_events
|
||||
AFTER UPDATE OF is_active ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION fn_add_member_to_future_events();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 6. Convenience view: event overview with attendance counts
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW uebung_uebersicht AS
|
||||
SELECT
|
||||
u.id,
|
||||
u.titel,
|
||||
u.typ,
|
||||
u.datum_von,
|
||||
u.datum_bis,
|
||||
u.ort,
|
||||
u.treffpunkt,
|
||||
u.pflichtveranstaltung,
|
||||
u.mindest_teilnehmer,
|
||||
u.max_teilnehmer,
|
||||
u.abgesagt,
|
||||
u.absage_grund,
|
||||
u.angelegt_von,
|
||||
u.erstellt_am,
|
||||
u.aktualisiert_am,
|
||||
-- Attendance aggregates
|
||||
COUNT(t.user_id) AS gesamt_eingeladen,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'zugesagt') AS anzahl_zugesagt,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'abgesagt') AS anzahl_abgesagt,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'erschienen') AS anzahl_erschienen,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'entschuldigt') AS anzahl_entschuldigt,
|
||||
COUNT(t.user_id) FILTER (WHERE t.status = 'unbekannt') AS anzahl_unbekannt
|
||||
FROM uebungen u
|
||||
LEFT JOIN uebung_teilnahmen t ON t.uebung_id = u.id
|
||||
GROUP BY u.id;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 7. View: per-member participation statistics (feeds Tier 3 reporting)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE OR REPLACE VIEW member_participation_stats AS
|
||||
SELECT
|
||||
usr.id AS user_id,
|
||||
COALESCE(usr.name, usr.preferred_username, usr.email) AS name,
|
||||
COUNT(t.uebung_id) AS total_eingeladen,
|
||||
COUNT(t.uebung_id) FILTER (WHERE t.status = 'erschienen') AS total_erschienen,
|
||||
COUNT(t.uebung_id) FILTER (
|
||||
WHERE u.pflichtveranstaltung = TRUE AND t.status = 'erschienen'
|
||||
) AS pflicht_erschienen,
|
||||
COUNT(t.uebung_id) FILTER (WHERE u.pflichtveranstaltung = TRUE) AS pflicht_gesamt,
|
||||
ROUND(
|
||||
CASE
|
||||
WHEN COUNT(t.uebung_id) FILTER (WHERE u.typ = 'Übungsabend') = 0 THEN 0
|
||||
ELSE
|
||||
COUNT(t.uebung_id) FILTER (
|
||||
WHERE u.typ = 'Übungsabend' AND t.status = 'erschienen'
|
||||
)::NUMERIC /
|
||||
COUNT(t.uebung_id) FILTER (WHERE u.typ = 'Übungsabend') * 100
|
||||
END, 1
|
||||
) AS uebungsabend_quote_pct
|
||||
FROM users usr
|
||||
JOIN uebung_teilnahmen t ON t.user_id = usr.id
|
||||
JOIN uebungen u ON u.id = t.uebung_id
|
||||
WHERE usr.is_active = TRUE
|
||||
AND u.abgesagt = FALSE
|
||||
GROUP BY usr.id, usr.name, usr.preferred_username, usr.email;
|
||||
Reference in New Issue
Block a user