feat: agent-12 – Zwei-Stufen-Genehmigung für Abwesenheiten
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled

Optionale zweistufige Freigabe (Feature-Parität mit Urlaubsverwaltung,
Second-Stage-Authority), ohne SSO:

- Firmen-Opt-in companies.two_stage_approval_enabled + two_stage_min_days
  (nur Anträge ab X Arbeitstagen brauchen Stufe 2; 0 = alle).
- Ablauf PENDING → FIRST_APPROVED → APPROVED: erste Stufe durch Manager-Rollen,
  finale Stufe nur HR/Admin und zwingend eine ANDERE Person als Stufe 1.
- Urlaubs-/FZA-Abzug, CalDAV-Sync und Vertreter-Mail erst bei finaler Genehmigung.
  Ablehnen in beiden Stufen möglich; Eigentümer darf FIRST_APPROVED noch stornieren.
- pending_days, Kalender und Reminder-Digest berücksichtigen FIRST_APPROVED.
- Neuer Status-Wert + absences.first_approved_by; System-Kommentar bei Stufe 1.

Frontend: CompanySettingsPage (Toggle + Schwellwert), AbsencesPage
("Endgültig genehmigen"/Ablehnen für HR/Admin ≠ Erstgenehmiger, Status-Badge).

Migration 0038. 190/190 Tests grün. Deployed 137 + 164.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 13:17:32 +02:00
co-authored by Claude Opus 4.8
parent c83fb31408
commit 450dbdb64a
12 changed files with 198 additions and 7 deletions
+6
View File
@@ -17,6 +17,8 @@ if TYPE_CHECKING:
class AbsenceStatus(str, enum.Enum): class AbsenceStatus(str, enum.Enum):
PENDING = "pending" PENDING = "pending"
# Zwei-Stufen-Genehmigung: erste Stufe erteilt, wartet auf finale Genehmigung.
FIRST_APPROVED = "first_approved"
APPROVED = "approved" APPROVED = "approved"
REJECTED = "rejected" REJECTED = "rejected"
CANCELLED = "cancelled" CANCELLED = "cancelled"
@@ -47,6 +49,10 @@ class Absence(Base):
approved_by: Mapped[uuid.UUID | None] = mapped_column( approved_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL") UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL")
) )
# Zwei-Stufen-Genehmigung: Genehmiger der ersten Stufe (muss != finalem Genehmiger sein).
first_approved_by: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL")
)
substitute_id: Mapped[uuid.UUID | None] = mapped_column( substitute_id: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL") UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL")
) )
+5
View File
@@ -78,6 +78,11 @@ class Company(Base):
# Teilzeit: Anspruch aus Arbeitstagen/Woche des WorkSchedule ableiten (opt-in). # Teilzeit: Anspruch aus Arbeitstagen/Woche des WorkSchedule ableiten (opt-in).
vacation_from_schedule: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) vacation_from_schedule: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
# Zwei-Stufen-Genehmigung (agent-12): erst Manager, dann HR/Admin (anderer Genehmiger).
two_stage_approval_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
# Nur Anträge ab dieser Arbeitstage-Zahl brauchen die zweite Stufe (0 = alle).
two_stage_min_days: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
# Freizeitausgleich-Konfiguration # Freizeitausgleich-Konfiguration
overtime_overdraft_allowed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) overtime_overdraft_allowed: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
overtime_warning_threshold_hours: Mapped[int] = mapped_column(Integer, nullable=False, default=0) overtime_warning_threshold_hours: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
+1
View File
@@ -71,6 +71,7 @@ class AbsenceOut(BaseModel):
fza_hours: Decimal | None = None fza_hours: Decimal | None = None
status: AbsenceStatus status: AbsenceStatus
approved_by: uuid.UUID | None approved_by: uuid.UUID | None
first_approved_by: uuid.UUID | None = None
substitute_id: uuid.UUID | None substitute_id: uuid.UUID | None
note: str | None note: str | None
correction_note: str | None correction_note: str | None
+4
View File
@@ -50,6 +50,8 @@ class CompanyOut(BaseModel):
vacation_default_days: int = 30 vacation_default_days: int = 30
vacation_prorate_first_year: bool = True vacation_prorate_first_year: bool = True
vacation_from_schedule: bool = False vacation_from_schedule: bool = False
two_stage_approval_enabled: bool = False
two_stage_min_days: int = 0
class PublicStampTokenStatus(BaseModel): class PublicStampTokenStatus(BaseModel):
@@ -88,6 +90,8 @@ class CompanyUpdate(BaseModel):
vacation_default_days: int | None = Field(None, ge=0, le=365) vacation_default_days: int | None = Field(None, ge=0, le=365)
vacation_prorate_first_year: bool | None = None vacation_prorate_first_year: bool | None = None
vacation_from_schedule: bool | None = None vacation_from_schedule: bool | None = None
two_stage_approval_enabled: bool | None = None
two_stage_min_days: int | None = Field(None, ge=0, le=365)
class DepartmentOut(BaseModel): class DepartmentOut(BaseModel):
+37 -5
View File
@@ -351,7 +351,7 @@ class AbsenceService:
await self._refund_overtime( await self._refund_overtime(
absence.user_id, absence.working_days, db, fza_hours=absence.fza_hours absence.user_id, absence.working_days, db, fza_hours=absence.fza_hours
) )
elif absence.status != AbsenceStatus.PENDING: elif absence.status not in (AbsenceStatus.PENDING, AbsenceStatus.FIRST_APPROVED):
raise HTTPException( raise HTTPException(
status_code=409, status_code=409,
detail="Nur ausstehende oder genehmigte Anträge können storniert werden." detail="Nur ausstehende oder genehmigte Anträge können storniert werden."
@@ -407,7 +407,39 @@ class AbsenceService:
await self._apply_cancellation(absence, current_user, db, from_request=True) await self._apply_cancellation(absence, current_user, db, from_request=True)
return absence, [] return absence, []
if absence.status != AbsenceStatus.PENDING: # Zwei-Stufen-Genehmigung (firmenweit opt-in, optional ab Schwellwert)
company = await db.get(Company, current_user.company_id)
two_stage = bool(
company and company.two_stage_approval_enabled
and absence.working_days >= (company.two_stage_min_days or 0)
)
_final_roles = (UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN)
if absence.status == AbsenceStatus.FIRST_APPROVED:
# Zweite (finale) Stufe nur HR/Admin und ein anderer Genehmiger als Stufe 1
if current_user.role not in _final_roles:
raise HTTPException(status_code=403, detail="Nur HR/Admin kann die finale Genehmigung erteilen.")
if absence.first_approved_by == current_user.id:
raise HTTPException(status_code=409, detail="Die finale Genehmigung muss von einer anderen Person erfolgen.")
# → fällt durch zur finalen Genehmigung unten
elif absence.status == AbsenceStatus.PENDING:
if two_stage:
absence.status = AbsenceStatus.FIRST_APPROVED
absence.first_approved_by = current_user.id
db.add(AuditLog(
company_id=current_user.company_id, user_id=current_user.id,
action="absence_first_approved", entity_type="absence", entity_id=absence.id,
old_value={"status": "pending"},
new_value={"status": "first_approved", "first_approved_by": str(current_user.id),
"absence_user_id": str(absence.user_id)},
))
await self._add_system_comment(
absence, current_user.company_id, current_user.id,
f"Erste Stufe genehmigt von {current_user.full_name} wartet auf finale Genehmigung.", db,
)
return absence, []
# einstufig → direkt finale Genehmigung unten
else:
raise HTTPException(status_code=409, detail="Nur ausstehende Anträge können genehmigt werden.") raise HTTPException(status_code=409, detail="Nur ausstehende Anträge können genehmigt werden.")
absence.status = AbsenceStatus.APPROVED absence.status = AbsenceStatus.APPROVED
@@ -487,7 +519,7 @@ class AbsenceService:
) )
return absence return absence
if absence.status != AbsenceStatus.PENDING: if absence.status not in (AbsenceStatus.PENDING, AbsenceStatus.FIRST_APPROVED):
raise HTTPException(status_code=409, detail="Nur ausstehende Anträge können abgelehnt werden.") raise HTTPException(status_code=409, detail="Nur ausstehende Anträge können abgelehnt werden.")
absence.status = AbsenceStatus.REJECTED absence.status = AbsenceStatus.REJECTED
@@ -532,7 +564,7 @@ class AbsenceService:
.join(AbsenceType, Absence.type_id == AbsenceType.id) .join(AbsenceType, Absence.type_id == AbsenceType.id)
.where( .where(
User.company_id == company_id, User.company_id == company_id,
Absence.status.in_([AbsenceStatus.PENDING, AbsenceStatus.APPROVED]), Absence.status.in_([AbsenceStatus.PENDING, AbsenceStatus.FIRST_APPROVED, AbsenceStatus.APPROVED]),
) )
) )
if month: if month:
@@ -576,7 +608,7 @@ class AbsenceService:
.join(AbsenceType, Absence.type_id == AbsenceType.id) .join(AbsenceType, Absence.type_id == AbsenceType.id)
.where( .where(
Absence.user_id == user_id, Absence.user_id == user_id,
Absence.status == AbsenceStatus.PENDING, Absence.status.in_([AbsenceStatus.PENDING, AbsenceStatus.FIRST_APPROVED]),
AbsenceType.deducts_vacation.is_(True), AbsenceType.deducts_vacation.is_(True),
func.extract("year", Absence.start_date) == year, func.extract("year", Absence.start_date) == year,
) )
+4 -1
View File
@@ -63,7 +63,10 @@ async def run_pending_approvals(db: AsyncSession, company_id=None) -> int:
select(Absence, User, AbsenceType) select(Absence, User, AbsenceType)
.join(User, Absence.user_id == User.id) .join(User, Absence.user_id == User.id)
.join(AbsenceType, Absence.type_id == AbsenceType.id) .join(AbsenceType, Absence.type_id == AbsenceType.id)
.where(User.company_id == company.id, Absence.status == AbsenceStatus.PENDING) .where(
User.company_id == company.id,
Absence.status.in_([AbsenceStatus.PENDING, AbsenceStatus.FIRST_APPROVED]),
)
.order_by(Absence.start_date) .order_by(Absence.start_date)
)).all() )).all()
if not rows: if not rows:
@@ -0,0 +1,39 @@
"""Two-stage absence approval (agent-12)
Revision ID: 0038
Revises: 0037
Create Date: 2026-06-23
- AbsenceStatus.FIRST_APPROVED (erste Stufe erteilt, wartet auf finale Genehmigung)
- absences.first_approved_by (Genehmiger Stufe 1; muss != finalem Genehmiger sein)
- companies.two_stage_approval_enabled + two_stage_min_days (firmenweites Opt-in)
"""
from alembic import op
revision = "0038"
down_revision = "0037"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("ALTER TYPE absencestatus ADD VALUE IF NOT EXISTS 'first_approved'")
op.execute(
"ALTER TABLE absences ADD COLUMN IF NOT EXISTS first_approved_by UUID "
"REFERENCES users(id) ON DELETE SET NULL"
)
op.execute(
"ALTER TABLE companies ADD COLUMN IF NOT EXISTS "
"two_stage_approval_enabled BOOLEAN NOT NULL DEFAULT FALSE"
)
op.execute(
"ALTER TABLE companies ADD COLUMN IF NOT EXISTS "
"two_stage_min_days INTEGER NOT NULL DEFAULT 0"
)
def downgrade() -> None:
# Enum-Wert bleibt (PostgreSQL kann ihn nicht entfernen).
op.execute("ALTER TABLE companies DROP COLUMN IF EXISTS two_stage_min_days")
op.execute("ALTER TABLE companies DROP COLUMN IF EXISTS two_stage_approval_enabled")
op.execute("ALTER TABLE absences DROP COLUMN IF EXISTS first_approved_by")
+66
View File
@@ -692,3 +692,69 @@ async def test_part_time_entitlement_from_schedule(client: AsyncClient, abs_head
assert bal.status_code == 200, bal.text assert bal.status_code == 200, bal.text
assert bal.json()["entitled_days"] == 18 # 30 * 3/5 assert bal.json()["entitled_days"] == 18 # 30 * 3/5
await client.patch("/api/v1/companies/me", json={"vacation_from_schedule": False}, headers=abs_headers) await client.patch("/api/v1/companies/me", json={"vacation_from_schedule": False}, headers=abs_headers)
# ── agent-12: Zwei-Stufen-Genehmigung ─────────────────────────────────────────
@pytest.mark.asyncio
async def test_two_stage_approval_flow(
client: AsyncClient, abs_headers, abs_approver_headers, vacation_type_id
):
await client.patch("/api/v1/companies/me",
json={"two_stage_approval_enabled": True}, headers=abs_headers)
inv = await client.post("/api/v1/users/invite", json={
"first_name": "Two", "last_name": "Stage", "email": "twostage@absenceag.de",
"role": "EMPLOYEE", "initial_password": "Secret123",
}, headers=abs_headers)
assert inv.status_code == 201, inv.text
login = await client.post("/api/v1/auth/login", json={
"email": "twostage@absenceag.de", "password": "Secret123"})
emp_h = {"Authorization": f"Bearer {login.json()['access_token']}"}
start = _future_monday(17)
cr = await client.post("/api/v1/absences/", json={
"type_id": str(vacation_type_id), "start_date": str(start),
"end_date": str(start + timedelta(days=2)),
}, headers=emp_h)
aid = cr.json()["id"]
# Erste Stufe: Approver A → first_approved (noch kein Abzug)
r1 = await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers)
assert r1.status_code == 200, r1.text
assert r1.json()["status"] == "first_approved"
# Gleicher Genehmiger darf die finale Stufe nicht erteilen
same = await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers)
assert same.status_code == 409
# Finale Stufe: anderer Admin → approved
r2 = await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_headers)
assert r2.status_code == 200, r2.text
assert r2.json()["status"] == "approved"
await client.patch("/api/v1/companies/me",
json={"two_stage_approval_enabled": False}, headers=abs_headers)
@pytest.mark.asyncio
async def test_two_stage_reject_at_first_stage(
client: AsyncClient, abs_headers, abs_approver_headers, vacation_type_id
):
await client.patch("/api/v1/companies/me",
json={"two_stage_approval_enabled": True}, headers=abs_headers)
login = await client.post("/api/v1/auth/login", json={
"email": "twostage@absenceag.de", "password": "Secret123"})
emp_h = {"Authorization": f"Bearer {login.json()['access_token']}"}
start = _future_monday(19)
cr = await client.post("/api/v1/absences/", json={
"type_id": str(vacation_type_id), "start_date": str(start),
"end_date": str(start + timedelta(days=1)),
}, headers=emp_h)
aid = cr.json()["id"]
await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers)
rej = await client.post(f"/api/v1/absences/{aid}/reject",
json={"rejection_reason": "Doch nicht"}, headers=abs_headers)
assert rej.status_code == 200, rej.text
assert rej.json()["status"] == "rejected"
await client.patch("/api/v1/companies/me",
json={"two_stage_approval_enabled": False}, headers=abs_headers)
+6
View File
@@ -477,6 +477,12 @@ export function AbsencesPage() {
<button onClick={() => approve(a.id)} className='text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-700'>Genehmigen</button> <button onClick={() => approve(a.id)} className='text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-700'>Genehmigen</button>
<button onClick={() => { setShowReject(a.id); setRejectReason('') }} className='text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700'>Ablehnen</button> <button onClick={() => { setShowReject(a.id); setRejectReason('') }} className='text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700'>Ablehnen</button>
</>)} </>)}
{a.status === 'first_approved'
&& ['HR', 'COMPANY_ADMIN', 'SUPER_ADMIN'].includes(user.role)
&& a.first_approved_by !== user.id && (<>
<button onClick={() => approve(a.id)} className='text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-700' title='Finale Genehmigung'>Endgültig genehmigen</button>
<button onClick={() => { setShowReject(a.id); setRejectReason('') }} className='text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700'>Ablehnen</button>
</>)}
{isManager && a.status === 'cancellation_requested' && (<> {isManager && a.status === 'cancellation_requested' && (<>
<button onClick={() => approve(a.id)} className='text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-700' title='Stornierung genehmigen'>Storno </button> <button onClick={() => approve(a.id)} className='text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-700' title='Stornierung genehmigen'>Storno </button>
<button onClick={() => { setShowReject(a.id); setRejectReason('') }} className='text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700' title='Stornierung ablehnen'>Storno </button> <button onClick={() => { setShowReject(a.id); setRejectReason('') }} className='text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700' title='Stornierung ablehnen'>Storno </button>
+27 -1
View File
@@ -53,6 +53,9 @@ export function CompanySettingsPage() {
const [vacationDefaultDays, setVacationDefaultDays] = useState(30) const [vacationDefaultDays, setVacationDefaultDays] = useState(30)
const [vacationProrate, setVacationProrate] = useState(true) const [vacationProrate, setVacationProrate] = useState(true)
const [vacationFromSchedule, setVacationFromSchedule] = useState(false) const [vacationFromSchedule, setVacationFromSchedule] = useState(false)
// Zwei-Stufen-Genehmigung (agent-12)
const [twoStageEnabled, setTwoStageEnabled] = useState(false)
const [twoStageMinDays, setTwoStageMinDays] = useState(0)
// Personalnummern // Personalnummern
const [pnRequired, setPnRequired] = useState(false) const [pnRequired, setPnRequired] = useState(false)
const [pnMode, setPnMode] = useState<'manual' | 'auto'>('manual') const [pnMode, setPnMode] = useState<'manual' | 'auto'>('manual')
@@ -110,10 +113,12 @@ export function CompanySettingsPage() {
setOvertimeExpiryMonth(cc.overtime_expiry_month ?? 3) setOvertimeExpiryMonth(cc.overtime_expiry_month ?? 3)
setOvertimeExpiryDay(cc.overtime_expiry_day ?? 31) setOvertimeExpiryDay(cc.overtime_expiry_day ?? 31)
setOvertimeMaxCarryoverHours(cc.overtime_max_carryover_hours ?? null) setOvertimeMaxCarryoverHours(cc.overtime_max_carryover_hours ?? null)
const vc = c as CompanyOut & { vacation_default_days?: number; vacation_prorate_first_year?: boolean; vacation_from_schedule?: boolean } const vc = c as CompanyOut & { vacation_default_days?: number; vacation_prorate_first_year?: boolean; vacation_from_schedule?: boolean; two_stage_approval_enabled?: boolean; two_stage_min_days?: number }
setVacationDefaultDays(vc.vacation_default_days ?? 30) setVacationDefaultDays(vc.vacation_default_days ?? 30)
setVacationProrate(vc.vacation_prorate_first_year ?? true) setVacationProrate(vc.vacation_prorate_first_year ?? true)
setVacationFromSchedule(vc.vacation_from_schedule ?? false) setVacationFromSchedule(vc.vacation_from_schedule ?? false)
setTwoStageEnabled(vc.two_stage_approval_enabled ?? false)
setTwoStageMinDays(vc.two_stage_min_days ?? 0)
}).catch(() => {}) }).catch(() => {})
api.get<{ configured: boolean; created_at: string | null }>('/companies/me/busylight-token') api.get<{ configured: boolean; created_at: string | null }>('/companies/me/busylight-token')
.then(setBlStatus) .then(setBlStatus)
@@ -185,6 +190,8 @@ export function CompanySettingsPage() {
vacation_default_days: vacationDefaultDays, vacation_default_days: vacationDefaultDays,
vacation_prorate_first_year: vacationProrate, vacation_prorate_first_year: vacationProrate,
vacation_from_schedule: vacationFromSchedule, vacation_from_schedule: vacationFromSchedule,
two_stage_approval_enabled: twoStageEnabled,
two_stage_min_days: twoStageMinDays,
overtime_overdraft_allowed: fzaOverdraftAllowed, overtime_overdraft_allowed: fzaOverdraftAllowed,
overtime_warning_threshold_hours: fzaWarningThreshold, overtime_warning_threshold_hours: fzaWarningThreshold,
kiosk_require_approval: kioskRequireApproval, kiosk_require_approval: kioskRequireApproval,
@@ -387,6 +394,25 @@ export function CompanySettingsPage() {
<span className="block text-xs text-gray-400">Anspruch = Jahresurlaub × Arbeitstage pro Woche ÷ 5.</span> <span className="block text-xs text-gray-400">Anspruch = Jahresurlaub × Arbeitstage pro Woche ÷ 5.</span>
</span> </span>
</label> </label>
<div className="border-t border-gray-100 pt-4 space-y-3">
<label className={`flex items-start gap-3 ${isAdmin ? 'cursor-pointer' : 'opacity-60'}`}>
<input type="checkbox" checked={twoStageEnabled} disabled={!isAdmin}
onChange={e => setTwoStageEnabled(e.target.checked)} className="mt-0.5" />
<span className="text-sm text-gray-700">
Zwei-Stufen-Genehmigung
<span className="block text-xs text-gray-400">Anträge müssen erst von einem Manager und dann final von HR/Admin (anderer Person) genehmigt werden.</span>
</span>
</label>
{twoStageEnabled && (
<div className="flex items-center gap-2 pl-7">
<span className="text-sm text-gray-600">Nur ab</span>
<input type="number" min={0} max={365} value={twoStageMinDays} disabled={!isAdmin}
onChange={e => setTwoStageMinDays(parseInt(e.target.value) || 0)}
className="w-20 border border-gray-300 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50" />
<span className="text-sm text-gray-600">Arbeitstagen (0 = alle Anträge)</span>
</div>
)}
</div>
<div className="border-t border-gray-100 pt-4"> <div className="border-t border-gray-100 pt-4">
<label className="block text-sm font-medium text-gray-700 mb-2"> <label className="block text-sm font-medium text-gray-700 mb-2">
Resturlaub verfällt am Resturlaub verfällt am
+1
View File
@@ -47,6 +47,7 @@ export interface AbsenceOut {
working_days: number working_days: number
status: string status: string
approved_by: string | null approved_by: string | null
first_approved_by: string | null
substitute_id: string | null substitute_id: string | null
note: string | null note: string | null
correction_note: string | null correction_note: string | null
+2
View File
@@ -18,6 +18,7 @@ export const STATUS_LABELS: Record<string, string> = {
rejected: 'Abgelehnt', rejected: 'Abgelehnt',
cancelled: 'Storniert', cancelled: 'Storniert',
cancellation_requested: 'Storno beantragt', cancellation_requested: 'Storno beantragt',
first_approved: 'Wartet auf Endgenehmigung',
} }
export const STATUS_COLORS: Record<string, string> = { export const STATUS_COLORS: Record<string, string> = {
@@ -26,6 +27,7 @@ export const STATUS_COLORS: Record<string, string> = {
rejected: 'bg-red-100 text-red-700', rejected: 'bg-red-100 text-red-700',
cancelled: 'bg-gray-100 text-gray-500', cancelled: 'bg-gray-100 text-gray-500',
cancellation_requested: 'bg-orange-100 text-orange-700', cancellation_requested: 'bg-orange-100 text-orange-700',
first_approved: 'bg-sky-100 text-sky-700',
} }
export const MANAGER_ROLES = ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR', 'MANAGER'] export const MANAGER_ROLES = ['COMPANY_ADMIN', 'SUPER_ADMIN', 'HR', 'MANAGER']