🗄️ Datenbank-Schema Design

Tabellen:
- organizations (Multi-Tenancy)
- users (Chef/Disponent/Mitarbeiter)
- orders (Aufträge)
- order_assignments (Zuweisungen)
- availability (Verfügbarkeit)
- timesheets (Stundenzettel mit Foto)
- modules (Modularer Aufbau)
- organization_modules (Modul-Konfiguration)
- audit_logs (DSGVO)
- refresh_tokens (JWT Auth)

Features:
- Rollen-basierte Berechtigungen
- Multi-Tenancy Support
- Modularer Aufbau
- Audit-Logging
This commit is contained in:
2026-02-20 15:01:30 +00:00
parent 48edd95cfd
commit 70a72f4364
3 changed files with 706 additions and 0 deletions

453
docs/database-schema.md Normal file
View File

@@ -0,0 +1,453 @@
# SeCu - Datenbank-Schema
## ER-Diagramm
```mermaid
erDiagram
organizations ||--o{ users : has
users ||--o{ users : manages
users ||--o{ orders : creates
users ||--o{ orders : assigned_to
users ||--o{ availability : reports
users ||--o{ timesheets : submits
orders ||--o{ timesheets : has
orders ||--o{ order_assignments : has
users ||--o{ order_assignments : assigned
modules ||--o{ organization_modules : configured_in
organizations ||--o{ organization_modules : has
organizations {
uuid id PK
varchar name
varchar slug UK
jsonb settings
timestamp created_at
timestamp updated_at
}
users {
uuid id PK
uuid org_id FK
varchar email UK
varchar password_hash
varchar role "chef|disponent|mitarbeiter"
varchar first_name
varchar last_name
varchar phone
varchar avatar_url
uuid created_by FK
uuid managed_by FK "Disponent der den MA verwaltet"
boolean active
timestamp last_login
timestamp created_at
timestamp updated_at
}
orders {
uuid id PK
uuid org_id FK
integer number "Auto-increment pro Org"
varchar title
text description
varchar location
varchar address
varchar client_name
varchar client_contact
varchar status "draft|published|in_progress|completed|cancelled"
timestamp start_time
timestamp end_time
integer required_staff
text special_instructions
uuid created_by FK
timestamp created_at
timestamp updated_at
}
order_assignments {
uuid id PK
uuid order_id FK
uuid user_id FK
varchar status "pending|confirmed|declined|completed"
text note
timestamp confirmed_at
timestamp created_at
}
availability {
uuid id PK
uuid user_id FK
date date
boolean available
varchar time_from "Optional: 08:00"
varchar time_to "Optional: 18:00"
text note
timestamp created_at
timestamp updated_at
}
timesheets {
uuid id PK
uuid user_id FK
uuid order_id FK
date work_date
time start_time
time end_time
decimal hours_worked
varchar photo_url
varchar status "pending|approved|rejected"
uuid approved_by FK
text rejection_reason
timestamp approved_at
timestamp created_at
timestamp updated_at
}
modules {
uuid id PK
varchar name UK
varchar display_name
text description
boolean is_core "Core modules can't be disabled"
jsonb default_config
timestamp created_at
}
organization_modules {
uuid id PK
uuid org_id FK
uuid module_id FK
boolean enabled
jsonb config
timestamp enabled_at
timestamp updated_at
}
audit_logs {
uuid id PK
uuid org_id FK
uuid user_id FK
varchar action
varchar entity_type
uuid entity_id
jsonb old_values
jsonb new_values
varchar ip_address
timestamp created_at
}
refresh_tokens {
uuid id PK
uuid user_id FK
varchar token_hash UK
timestamp expires_at
timestamp created_at
}
```
## SQL Migrations
### 001_initial_schema.sql
```sql
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- ============================================
-- ORGANIZATIONS (Multi-Tenancy)
-- ============================================
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) UNIQUE NOT NULL,
settings JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_organizations_slug ON organizations(slug);
-- ============================================
-- USERS
-- ============================================
CREATE TYPE user_role AS ENUM ('chef', 'disponent', 'mitarbeiter');
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role user_role NOT NULL DEFAULT 'mitarbeiter',
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
phone VARCHAR(50),
avatar_url VARCHAR(500),
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
managed_by UUID REFERENCES users(id) ON DELETE SET NULL,
active BOOLEAN DEFAULT true,
last_login TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(org_id, email)
);
CREATE INDEX idx_users_org ON users(org_id);
CREATE INDEX idx_users_role ON users(org_id, role);
CREATE INDEX idx_users_managed_by ON users(managed_by);
-- ============================================
-- ORDERS (Aufträge)
-- ============================================
CREATE TYPE order_status AS ENUM ('draft', 'published', 'in_progress', 'completed', 'cancelled');
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
number SERIAL,
title VARCHAR(255) NOT NULL,
description TEXT,
location VARCHAR(255),
address TEXT,
client_name VARCHAR(255),
client_contact VARCHAR(255),
status order_status DEFAULT 'draft',
start_time TIMESTAMP WITH TIME ZONE,
end_time TIMESTAMP WITH TIME ZONE,
required_staff INTEGER DEFAULT 1,
special_instructions TEXT,
created_by UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_orders_org ON orders(org_id);
CREATE INDEX idx_orders_status ON orders(org_id, status);
CREATE INDEX idx_orders_dates ON orders(org_id, start_time, end_time);
-- ============================================
-- ORDER ASSIGNMENTS (Zuweisungen)
-- ============================================
CREATE TYPE assignment_status AS ENUM ('pending', 'confirmed', 'declined', 'completed');
CREATE TABLE order_assignments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status assignment_status DEFAULT 'pending',
note TEXT,
confirmed_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(order_id, user_id)
);
CREATE INDEX idx_assignments_order ON order_assignments(order_id);
CREATE INDEX idx_assignments_user ON order_assignments(user_id);
-- ============================================
-- AVAILABILITY (Verfügbarkeit)
-- ============================================
CREATE TABLE availability (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
date DATE NOT NULL,
available BOOLEAN NOT NULL DEFAULT true,
time_from TIME,
time_to TIME,
note TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, date)
);
CREATE INDEX idx_availability_user_date ON availability(user_id, date);
-- ============================================
-- TIMESHEETS (Stundenzettel)
-- ============================================
CREATE TYPE timesheet_status AS ENUM ('pending', 'approved', 'rejected');
CREATE TABLE timesheets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
order_id UUID REFERENCES orders(id) ON DELETE SET NULL,
work_date DATE NOT NULL,
start_time TIME,
end_time TIME,
hours_worked DECIMAL(5,2),
photo_url VARCHAR(500),
status timesheet_status DEFAULT 'pending',
approved_by UUID REFERENCES users(id) ON DELETE SET NULL,
rejection_reason TEXT,
approved_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_timesheets_user ON timesheets(user_id);
CREATE INDEX idx_timesheets_order ON timesheets(order_id);
CREATE INDEX idx_timesheets_status ON timesheets(status);
CREATE INDEX idx_timesheets_date ON timesheets(work_date);
-- ============================================
-- MODULES (Modularer Aufbau)
-- ============================================
CREATE TABLE modules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) UNIQUE NOT NULL,
display_name VARCHAR(255) NOT NULL,
description TEXT,
is_core BOOLEAN DEFAULT false,
default_config JSONB DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE organization_modules (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
module_id UUID NOT NULL REFERENCES modules(id) ON DELETE CASCADE,
enabled BOOLEAN DEFAULT true,
config JSONB DEFAULT '{}',
enabled_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(org_id, module_id)
);
CREATE INDEX idx_org_modules ON organization_modules(org_id);
-- ============================================
-- AUDIT LOGS
-- ============================================
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
org_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
action VARCHAR(100) NOT NULL,
entity_type VARCHAR(100),
entity_id UUID,
old_values JSONB,
new_values JSONB,
ip_address INET,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_audit_org ON audit_logs(org_id);
CREATE INDEX idx_audit_user ON audit_logs(user_id);
CREATE INDEX idx_audit_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_date ON audit_logs(created_at);
-- ============================================
-- REFRESH TOKENS
-- ============================================
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash VARCHAR(255) UNIQUE NOT NULL,
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_expires ON refresh_tokens(expires_at);
-- ============================================
-- UPDATED_AT TRIGGER
-- ============================================
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_organizations_updated_at BEFORE UPDATE ON organizations
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_orders_updated_at BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_availability_updated_at BEFORE UPDATE ON availability
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_timesheets_updated_at BEFORE UPDATE ON timesheets
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
CREATE TRIGGER update_org_modules_updated_at BEFORE UPDATE ON organization_modules
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
```
### 002_seed_modules.sql
```sql
-- Core Modules (können nicht deaktiviert werden)
INSERT INTO modules (name, display_name, description, is_core, default_config) VALUES
('core', 'Basis-System', 'Authentifizierung und Benutzerverwaltung', true, '{}'),
('orders', 'Auftragsverwaltung', 'Erstellen und Verwalten von Aufträgen', true, '{}');
-- Optionale Module
INSERT INTO modules (name, display_name, description, is_core, default_config) VALUES
('timesheets', 'Stundenzettel', 'Zeiterfassung mit Foto-Upload', false, '{"require_photo": true, "auto_calculate_hours": true}'),
('availability', 'Verfügbarkeit', 'Mitarbeiter-Verfügbarkeitsplanung', false, '{"allow_partial_day": true}'),
('notifications', 'Benachrichtigungen', 'Push/E-Mail Benachrichtigungen', false, '{"email_enabled": true, "push_enabled": false}'),
('reports', 'Berichte', 'Auswertungen und Statistiken', false, '{}'),
('developer', 'Entwickler-Panel', 'Fernverwaltung und Modul-Management', false, '{}');
```
### 003_seed_demo.sql
```sql
-- Demo Organization
INSERT INTO organizations (id, name, slug) VALUES
('11111111-1111-1111-1111-111111111111', 'Demo Sicherheit GmbH', 'demo');
-- Demo Chef User (Password: Admin123!)
INSERT INTO users (id, org_id, email, password_hash, role, first_name, last_name, phone) VALUES
('22222222-2222-2222-2222-222222222222',
'11111111-1111-1111-1111-111111111111',
'chef@demo.de',
'$argon2id$v=19$m=65536,t=3,p=4$DEMO_HASH_REPLACE_ME',
'chef',
'Max',
'Mustermann',
'+49 170 1234567');
-- Enable all modules for demo org
INSERT INTO organization_modules (org_id, module_id, enabled)
SELECT '11111111-1111-1111-1111-111111111111', id, true FROM modules;
```
## Berechtigungsmatrix
| Aktion | Chef | Disponent | Mitarbeiter |
|--------|------|-----------|-------------|
| **Users** | | | |
| Disponenten anlegen | ✅ | ❌ | ❌ |
| Mitarbeiter anlegen | ✅ | ✅ (eigene) | ❌ |
| Alle User sehen | ✅ | ❌ | ❌ |
| Eigene Mitarbeiter sehen | ✅ | ✅ | ❌ |
| **Orders** | | | |
| Aufträge erstellen | ✅ | ✅ | ❌ |
| Aufträge bearbeiten | ✅ | ✅ (eigene) | ❌ |
| Aufträge löschen | ✅ | ✅ (eigene) | ❌ |
| Alle Aufträge sehen | ✅ | ✅ | ❌ |
| Eigene Aufträge sehen | ✅ | ✅ | ✅ |
| **Availability** | | | |
| Verfügbarkeit melden | ✅ | ✅ | ✅ |
| Alle Verfügbarkeiten sehen | ✅ | ✅ | ❌ |
| **Timesheets** | | | |
| Stundenzettel hochladen | ✅ | ✅ | ✅ |
| Stundenzettel genehmigen | ✅ | ✅ | ❌ |
| Alle Stundenzettel sehen | ✅ | ✅ | ❌ |
| **Modules** | | | |
| Module verwalten | ✅ (Dev) | ❌ | ❌ |
---
*Schema Version: 1.0.0*
*Erstellt: 2026-02-20*