new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 13:08:19 +01:00
parent 83b84664ce
commit 5032e1593b
41 changed files with 5157 additions and 40 deletions

View File

@@ -0,0 +1,145 @@
-- Migration 038: Bestellungen (Vendor Orders) system
-- Tables for vendors, orders, line items, file attachments, reminders, and audit trail.
-- ═══════════════════════════════════════════════════════════════════════════
-- 1. Lieferanten (Vendors)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS lieferanten (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
kontakt_name TEXT,
email TEXT,
telefon TEXT,
adresse TEXT,
website TEXT,
notizen TEXT,
erstellt_von UUID REFERENCES users(id) ON DELETE SET NULL,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
aktualisiert_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_lieferanten_name ON lieferanten(name);
-- ═══════════════════════════════════════════════════════════════════════════
-- 2. Bestellungen (Orders)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS bestellungen (
id SERIAL PRIMARY KEY,
bezeichnung TEXT NOT NULL,
lieferant_id INT REFERENCES lieferanten(id) ON DELETE SET NULL,
besteller_id UUID REFERENCES users(id) ON DELETE SET NULL,
status TEXT NOT NULL DEFAULT 'entwurf'
CHECK (status IN ('entwurf','erstellt','bestellt','teillieferung','vollstaendig','abgeschlossen')),
budget NUMERIC(10,2),
notizen TEXT,
erstellt_von UUID REFERENCES users(id) ON DELETE SET NULL,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
aktualisiert_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
bestellt_am TIMESTAMPTZ,
abgeschlossen_am TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_bestellungen_status ON bestellungen(status);
CREATE INDEX IF NOT EXISTS idx_bestellungen_lieferant ON bestellungen(lieferant_id);
CREATE INDEX IF NOT EXISTS idx_bestellungen_besteller ON bestellungen(besteller_id);
-- ═══════════════════════════════════════════════════════════════════════════
-- 3. Bestellpositionen (Order Line Items)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS bestellpositionen (
id SERIAL PRIMARY KEY,
bestellung_id INT NOT NULL REFERENCES bestellungen(id) ON DELETE CASCADE,
bezeichnung TEXT NOT NULL,
artikelnummer TEXT,
menge NUMERIC NOT NULL DEFAULT 1,
einheit TEXT DEFAULT 'Stk',
einzelpreis NUMERIC(10,2),
erhalten_menge NUMERIC NOT NULL DEFAULT 0,
notizen TEXT,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
aktualisiert_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_bestellpositionen_bestellung ON bestellpositionen(bestellung_id);
-- ═══════════════════════════════════════════════════════════════════════════
-- 4. Bestellung Dateien (Order File Attachments)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS bestellung_dateien (
id SERIAL PRIMARY KEY,
bestellung_id INT NOT NULL REFERENCES bestellungen(id) ON DELETE CASCADE,
dateiname TEXT NOT NULL,
dateipfad TEXT NOT NULL,
dateityp TEXT NOT NULL,
dateigroesse INT,
thumbnail_pfad TEXT,
hochgeladen_von UUID REFERENCES users(id) ON DELETE SET NULL,
hochgeladen_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_bestellung_dateien_bestellung ON bestellung_dateien(bestellung_id);
-- ═══════════════════════════════════════════════════════════════════════════
-- 5. Bestellung Erinnerungen (Order Reminders)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS bestellung_erinnerungen (
id SERIAL PRIMARY KEY,
bestellung_id INT NOT NULL REFERENCES bestellungen(id) ON DELETE CASCADE,
faellig_am TIMESTAMPTZ NOT NULL,
nachricht TEXT,
erledigt BOOLEAN NOT NULL DEFAULT FALSE,
erstellt_von UUID REFERENCES users(id) ON DELETE SET NULL,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_bestellung_erinnerungen_faellig ON bestellung_erinnerungen(faellig_am) WHERE NOT erledigt;
-- ═══════════════════════════════════════════════════════════════════════════
-- 6. Bestellung Historie (Audit Trail)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS bestellung_historie (
id SERIAL PRIMARY KEY,
bestellung_id INT NOT NULL REFERENCES bestellungen(id) ON DELETE CASCADE,
aktion TEXT NOT NULL,
details JSONB,
erstellt_von UUID REFERENCES users(id) ON DELETE SET NULL,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_bestellung_historie_bestellung ON bestellung_historie(bestellung_id);
-- ═══════════════════════════════════════════════════════════════════════════
-- 7. Auto-update aktualisiert_am triggers
-- ═══════════════════════════════════════════════════════════════════════════
CREATE OR REPLACE FUNCTION update_aktualisiert_am()
RETURNS TRIGGER AS $$
BEGIN
NEW.aktualisiert_am = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_lieferanten_aktualisiert') THEN
CREATE TRIGGER trg_lieferanten_aktualisiert BEFORE UPDATE ON lieferanten
FOR EACH ROW EXECUTE FUNCTION update_aktualisiert_am();
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_bestellungen_aktualisiert') THEN
CREATE TRIGGER trg_bestellungen_aktualisiert BEFORE UPDATE ON bestellungen
FOR EACH ROW EXECUTE FUNCTION update_aktualisiert_am();
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_bestellpositionen_aktualisiert') THEN
CREATE TRIGGER trg_bestellpositionen_aktualisiert BEFORE UPDATE ON bestellpositionen
FOR EACH ROW EXECUTE FUNCTION update_aktualisiert_am();
END IF;
END $$;

View File

@@ -0,0 +1,84 @@
-- Migration 039: Internal Shop system
-- Tables for catalog items, member requests, request line items, and order linking.
-- ═══════════════════════════════════════════════════════════════════════════
-- 1. Shop Artikel (Catalog Items)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS shop_artikel (
id SERIAL PRIMARY KEY,
bezeichnung TEXT NOT NULL,
beschreibung TEXT,
kategorie TEXT,
bild_pfad TEXT,
geschaetzter_preis NUMERIC(10,2),
aktiv BOOLEAN NOT NULL DEFAULT TRUE,
erstellt_von UUID REFERENCES users(id) ON DELETE SET NULL,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
aktualisiert_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_shop_artikel_kategorie ON shop_artikel(kategorie);
CREATE INDEX IF NOT EXISTS idx_shop_artikel_aktiv ON shop_artikel(aktiv);
-- ═══════════════════════════════════════════════════════════════════════════
-- 2. Shop Anfragen (Member Requests)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS shop_anfragen (
id SERIAL PRIMARY KEY,
anfrager_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'offen'
CHECK (status IN ('offen','genehmigt','abgelehnt','bestellt','erledigt')),
notizen TEXT,
admin_notizen TEXT,
bearbeitet_von UUID REFERENCES users(id) ON DELETE SET NULL,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW(),
aktualisiert_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_shop_anfragen_anfrager ON shop_anfragen(anfrager_id);
CREATE INDEX IF NOT EXISTS idx_shop_anfragen_status ON shop_anfragen(status);
-- ═══════════════════════════════════════════════════════════════════════════
-- 3. Shop Anfrage Positionen (Request Line Items)
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS shop_anfrage_positionen (
id SERIAL PRIMARY KEY,
anfrage_id INT NOT NULL REFERENCES shop_anfragen(id) ON DELETE CASCADE,
artikel_id INT REFERENCES shop_artikel(id) ON DELETE SET NULL,
bezeichnung TEXT NOT NULL,
menge NUMERIC NOT NULL DEFAULT 1,
notizen TEXT,
erstellt_am TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_shop_anfrage_positionen_anfrage ON shop_anfrage_positionen(anfrage_id);
-- ═══════════════════════════════════════════════════════════════════════════
-- 4. Shop Anfrage ↔ Bestellung Link
-- ═══════════════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS shop_anfrage_bestellung (
anfrage_id INT NOT NULL REFERENCES shop_anfragen(id) ON DELETE CASCADE,
bestellung_id INT NOT NULL REFERENCES bestellungen(id) ON DELETE CASCADE,
PRIMARY KEY (anfrage_id, bestellung_id)
);
-- ═══════════════════════════════════════════════════════════════════════════
-- 5. Auto-update triggers
-- ═══════════════════════════════════════════════════════════════════════════
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_shop_artikel_aktualisiert') THEN
CREATE TRIGGER trg_shop_artikel_aktualisiert BEFORE UPDATE ON shop_artikel
FOR EACH ROW EXECUTE FUNCTION update_aktualisiert_am();
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_shop_anfragen_aktualisiert') THEN
CREATE TRIGGER trg_shop_anfragen_aktualisiert BEFORE UPDATE ON shop_anfragen
FOR EACH ROW EXECUTE FUNCTION update_aktualisiert_am();
END IF;
END $$;

View File

@@ -0,0 +1,158 @@
-- Migration 040: Permission updates
-- 1. Add bestellungen + shop feature groups and their permissions
-- 2. Simplify calendar permissions from 13 → 4
-- 3. Migrate existing group_permissions to new calendar scheme
-- ═══════════════════════════════════════════════════════════════════════════
-- 1. New feature groups
-- ═══════════════════════════════════════════════════════════════════════════
INSERT INTO feature_groups (id, label, sort_order) VALUES
('bestellungen', 'Bestellungen', 11),
('shop', 'Shop', 12)
ON CONFLICT (id) DO NOTHING;
-- ═══════════════════════════════════════════════════════════════════════════
-- 2. Bestellungen permissions
-- ═══════════════════════════════════════════════════════════════════════════
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
('bestellungen:view', 'bestellungen', 'Ansehen', 'Bestellungen einsehen', 1),
('bestellungen:create', 'bestellungen', 'Erstellen/Bearbeiten', 'Bestellungen erstellen und bearbeiten', 2),
('bestellungen:delete', 'bestellungen', 'Löschen', 'Bestellungen löschen', 3),
('bestellungen:manage_vendors', 'bestellungen', 'Lieferanten verwalten','Lieferanten-Datenbank verwalten', 4),
('bestellungen:export', 'bestellungen', 'PDF Export', 'Bestellungen als PDF exportieren', 5),
('bestellungen:manage_reminders', 'bestellungen', 'Erinnerungen', 'Erinnerungen für Bestellungen verwalten', 6),
('bestellungen:widget', 'bestellungen', 'Widget', 'Dashboard-Widget für Bestellungen', 7)
ON CONFLICT (id) DO NOTHING;
-- ═══════════════════════════════════════════════════════════════════════════
-- 3. Shop permissions
-- ═══════════════════════════════════════════════════════════════════════════
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
('shop:view', 'shop', 'Katalog ansehen', 'Shop-Katalog einsehen', 1),
('shop:create_request', 'shop', 'Anfrage stellen', 'Bestellanfragen an Admin stellen', 2),
('shop:manage_catalog', 'shop', 'Katalog verwalten', 'Artikel im Shop-Katalog verwalten', 3),
('shop:approve_requests', 'shop', 'Anfragen genehmigen', 'Bestellanfragen genehmigen oder ablehnen', 4),
('shop:link_orders', 'shop', 'Mit Bestellung verknüpfen', 'Anfragen mit Lieferantenbestellungen verknüpfen', 5),
('shop:widget', 'shop', 'Widget', 'Dashboard-Widget für Shop-Anfragen', 6)
ON CONFLICT (id) DO NOTHING;
-- ═══════════════════════════════════════════════════════════════════════════
-- 4. Calendar permission simplification (13 → 4)
-- ═══════════════════════════════════════════════════════════════════════════
-- New scheme:
-- kalender:view — see events + widgets
-- kalender:create — create/edit/cancel events, mark attendance, manage categories, view reports
-- kalender:view_bookings — see bookings + booking widgets
-- kalender:manage_bookings — create/edit/cancel/delete bookings
-- 4a. Collect which groups had which old permissions, then map to new ones.
-- We use a temp table so we don't lose data during the transition.
CREATE TEMP TABLE _cal_migration AS
SELECT DISTINCT authentik_group,
CASE
-- Any group that had kalender:create OR kalender:cancel OR kalender:manage_categories
-- OR kalender:view_reports OR kalender:mark_attendance → gets kalender:create
WHEN permission_id IN ('kalender:create','kalender:cancel','kalender:manage_categories',
'kalender:view_reports','kalender:mark_attendance')
THEN 'kalender:create'
-- Widget permissions → kalender:view (they already have it if they had widget perms)
WHEN permission_id IN ('kalender:widget_events','kalender:widget_quick_add')
THEN 'kalender:view'
-- Booking-related view widgets → kalender:view_bookings
WHEN permission_id IN ('kalender:widget_bookings')
THEN 'kalender:view_bookings'
-- All booking write ops → kalender:manage_bookings
WHEN permission_id IN ('kalender:create_bookings','kalender:edit_bookings',
'kalender:cancel_own_bookings','kalender:delete_bookings')
THEN 'kalender:manage_bookings'
ELSE permission_id
END AS new_perm
FROM group_permissions
WHERE permission_id LIKE 'kalender:%';
-- 4b. Delete old calendar permissions from group_permissions
DELETE FROM group_permissions WHERE permission_id LIKE 'kalender:%';
-- 4c. Delete old calendar permission definitions
DELETE FROM permissions WHERE id LIKE 'kalender:%';
-- 4d. Insert new calendar permissions
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
('kalender:view', 'kalender', 'Termine ansehen', 'Kalender-Termine und Widgets einsehen', 1),
('kalender:create', 'kalender', 'Termine verwalten', 'Termine erstellen/bearbeiten/absagen, Kategorien, Berichte', 2),
('kalender:view_bookings', 'kalender', 'Buchungen ansehen', 'Fahrzeugbuchungen und Buchungs-Widget einsehen', 3),
('kalender:manage_bookings', 'kalender', 'Buchungen verwalten', 'Buchungen erstellen/bearbeiten/stornieren/löschen', 4)
ON CONFLICT (id) DO NOTHING;
-- 4e. Re-insert migrated grants (only valid new permissions)
INSERT INTO group_permissions (authentik_group, permission_id)
SELECT DISTINCT authentik_group, new_perm
FROM _cal_migration
WHERE new_perm IN ('kalender:view','kalender:create','kalender:view_bookings','kalender:manage_bookings')
ON CONFLICT DO NOTHING;
-- Also ensure everyone who had any calendar perm gets kalender:view
INSERT INTO group_permissions (authentik_group, permission_id)
SELECT DISTINCT authentik_group, 'kalender:view'
FROM _cal_migration
ON CONFLICT DO NOTHING;
-- And everyone who had any booking perm gets kalender:view_bookings
INSERT INTO group_permissions (authentik_group, permission_id)
SELECT DISTINCT authentik_group, 'kalender:view_bookings'
FROM _cal_migration
WHERE new_perm = 'kalender:manage_bookings'
ON CONFLICT DO NOTHING;
DROP TABLE _cal_migration;
-- ═══════════════════════════════════════════════════════════════════════════
-- 5. Seed bestellungen + shop permissions for existing groups
-- ═══════════════════════════════════════════════════════════════════════════
-- kommando gets full access, other groups get view + shop request
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
-- Kommando: full bestellungen + shop
('dashboard_kommando', 'bestellungen:view'),
('dashboard_kommando', 'bestellungen:create'),
('dashboard_kommando', 'bestellungen:delete'),
('dashboard_kommando', 'bestellungen:manage_vendors'),
('dashboard_kommando', 'bestellungen:export'),
('dashboard_kommando', 'bestellungen:manage_reminders'),
('dashboard_kommando', 'bestellungen:widget'),
('dashboard_kommando', 'shop:view'),
('dashboard_kommando', 'shop:create_request'),
('dashboard_kommando', 'shop:manage_catalog'),
('dashboard_kommando', 'shop:approve_requests'),
('dashboard_kommando', 'shop:link_orders'),
('dashboard_kommando', 'shop:widget'),
-- Fahrmeister: view orders + shop request
('dashboard_fahrmeister', 'bestellungen:view'),
('dashboard_fahrmeister', 'bestellungen:widget'),
('dashboard_fahrmeister', 'shop:view'),
('dashboard_fahrmeister', 'shop:create_request'),
-- Zeugmeister: view orders + shop request
('dashboard_zeugmeister', 'bestellungen:view'),
('dashboard_zeugmeister', 'bestellungen:widget'),
('dashboard_zeugmeister', 'shop:view'),
('dashboard_zeugmeister', 'shop:create_request'),
-- Chargen: view orders + shop request
('dashboard_chargen', 'bestellungen:view'),
('dashboard_chargen', 'bestellungen:widget'),
('dashboard_chargen', 'shop:view'),
('dashboard_chargen', 'shop:create_request'),
-- Moderator: view orders + shop request
('dashboard_moderator', 'bestellungen:view'),
('dashboard_moderator', 'shop:view'),
('dashboard_moderator', 'shop:create_request'),
-- Atemschutz: shop request only
('dashboard_atemschutz', 'shop:view'),
('dashboard_atemschutz', 'shop:create_request'),
-- Mitglied: shop request only
('dashboard_mitglied', 'shop:view'),
('dashboard_mitglied', 'shop:create_request')
ON CONFLICT DO NOTHING;