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
+44
View File
@@ -0,0 +1,44 @@
import uuid
from datetime import datetime
from typing import TYPE_CHECKING
from sqlalchemy import Boolean, DateTime, ForeignKey, Text, func
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
if TYPE_CHECKING:
from app.models.user import User
class AbsenceComment(Base):
"""Kommentar-Thread an einem Abwesenheitsantrag.
`company_id` wird redundant gespeichert, damit die Tabelle über die normale
company_id-RLS-Policy gefenced werden kann (kein Join nötig). System-Kommentare
(`is_system=True`) werden bei Statuswechseln automatisch erzeugt.
"""
__tablename__ = "absence_comments"
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
absence_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("absences.id", ondelete="CASCADE"), nullable=False, index=True
)
company_id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True), ForeignKey("companies.id", ondelete="CASCADE"), nullable=False, index=True
)
author_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL")
)
body: Mapped[str] = mapped_column(Text, nullable=False)
is_system: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
author: Mapped["User | None"] = relationship(
"User", primaryjoin="AbsenceComment.author_id == User.id",
foreign_keys="[AbsenceComment.author_id]", lazy="noload",
)
def __repr__(self) -> str:
return f"<AbsenceComment {self.absence_id} by {self.author_id}>"