feat: agent-11 PR1 – Vertretung, Storno-Re-Genehmigung, Kommentare
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled

Abwesenheits-Modul abgerundet (Feature-Parität mit Urlaubsverwaltung):

- Vertretung: Overlap-Warnung beim Anlegen, E-Mail an Vertretung bei
  Genehmigung, GET /absences/?as_substitute=true, neuer schlanker
  GET /users/colleagues (alle Rollen, RLS-gefenced) für die Auswahl;
  Vertreter-Dropdown + Anzeige in der Liste.
- Stornierung mit Re-Genehmigung: neuer Status CANCELLATION_REQUESTED,
  POST /absences/{id}/request-cancellation; Manager genehmigt/lehnt über
  bestehende approve/reject ab (Urlaub + FZA-Rückbuchung via _apply_cancellation).
- Kommentare: Model AbsenceComment (company_id-RLS), GET/POST comments,
  System-Kommentare bei Statuswechsel, AbsenceCommentsModal.
- Fix: CalDAV fire-and-forget nutzte die Request-Session weiter (in Tests
  geteilt -> "another operation in progress"); jetzt sync_*_bg mit eigener
  Session + RLS-Bypass.

Migration 0035. 178/178 Tests grün. Deployed auf 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 11:47:47 +02:00
co-authored by Claude Opus 4.8
parent c3cb9ce073
commit 3b2df1c978
18 changed files with 793 additions and 27 deletions
@@ -0,0 +1,61 @@
"""Absence comments + cancellation-request status (agent-11 PR1)
Revision ID: 0035
Revises: 0034
Create Date: 2026-06-23
- Neuer Enum-Wert AbsenceStatus.CANCELLATION_REQUESTED (Mitarbeiter-Stornoantrag
für genehmigte Anträge → Manager genehmigt/lehnt ab)
- Neue Tabelle absence_comments (Kommentar-Thread + System-Kommentare bei Statuswechsel)
mit company_id-Spalte → reguläre company_id-RLS-Policy (analog 0024).
"""
from alembic import op
from sqlalchemy import text
revision = "0035"
down_revision = "0034"
branch_labels = None
depends_on = None
_BYPASS = "COALESCE(current_setting('app.bypass_rls', true), 'off') = 'on'"
_CID = "company_id = NULLIF(current_setting('app.company_id', true), '')::uuid"
_USING = f"({_BYPASS} OR {_CID})"
def _exec(sql: str) -> None:
op.execute(text(sql))
def upgrade() -> None:
# 1) Enum-Wert ergänzen (idempotent)
_exec("ALTER TYPE absencestatus ADD VALUE IF NOT EXISTS 'cancellation_requested'")
# 2) Tabelle anlegen
_exec("""
CREATE TABLE IF NOT EXISTS absence_comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
absence_id UUID NOT NULL REFERENCES absences(id) ON DELETE CASCADE,
company_id UUID NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
author_id UUID REFERENCES users(id) ON DELETE SET NULL,
body TEXT NOT NULL,
is_system BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
""")
_exec("CREATE INDEX IF NOT EXISTS ix_absence_comments_absence_id ON absence_comments(absence_id)")
_exec("CREATE INDEX IF NOT EXISTS ix_absence_comments_company_id ON absence_comments(company_id)")
# 3) RLS (company_id-gefenced, analog 0024)
_exec("ALTER TABLE absence_comments ENABLE ROW LEVEL SECURITY")
_exec("ALTER TABLE absence_comments FORCE ROW LEVEL SECURITY")
for cmd in ("select", "insert", "update", "delete"):
_exec(f"DROP POLICY IF EXISTS rls_absence_comments_{cmd} ON absence_comments")
_exec(f"CREATE POLICY rls_absence_comments_select ON absence_comments FOR SELECT USING {_USING}")
_exec(f"CREATE POLICY rls_absence_comments_insert ON absence_comments FOR INSERT WITH CHECK {_USING}")
_exec(f"CREATE POLICY rls_absence_comments_update ON absence_comments FOR UPDATE USING {_USING} WITH CHECK {_USING}")
_exec(f"CREATE POLICY rls_absence_comments_delete ON absence_comments FOR DELETE USING {_USING}")
def downgrade() -> None:
# Enum-Wert kann in PostgreSQL nicht entfernt werden Tabelle wird gedroppt.
_exec("DROP TABLE IF EXISTS absence_comments")