202 lines
8.5 KiB
PL/PgSQL
202 lines
8.5 KiB
PL/PgSQL
-- =============================================================================
|
|
-- 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;
|