feat: agent-11 PR2 – Urlaubsanspruch (Verfall scharf, Pro-rata, Teilzeit)
Security Audit / Python Dependency Audit (push) Has been cancelled
Security Audit / Node.js Dependency Audit (push) Has been cancelled

Korrektheit der Urlaubskonten (Feature-Parität mit Urlaubsverwaltung):

- Verfall scharfgeschaltet: neuer effektiv verfügbarer Saldo (available_days)
  schließt verfallenen, noch nicht verbrauchten Resturlaub aus; Konto-Warnung
  beim Antrag nutzt jetzt available statt remaining. (Verfallsdatum bleibt in
  company.settings, UI bereits vorhanden.)
- Anteilige Berechnung (Zwölftel) im Ein-/Austrittsjahr anhand neuer Felder
  users.entry_date / exit_date; opt-in pro Firma (vacation_prorate_first_year).
- Teilzeit: Anspruch optional aus Arbeitstagen/Woche des WorkSchedule abgeleitet
  (vacation_from_schedule), Basis vacation_default_days.
- _get_or_create_balance berechnet den Grundanspruch jetzt frisch
  (_compute_entitlement); _carryover_expired/effective_available als Service-API,
  Router delegiert.

Frontend: CompanySettingsPage (Jahresurlaub + Pro-rata- und Teilzeit-Toggles),
UsersPage (Ein-/Austrittsdatum im Edit-Modal), AbsencesPage ("Verfügbar" + Hinweis
bei verfallenem Resturlaub).

Migration 0036. 183/183 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 12:03:42 +02:00
co-authored by Claude Opus 4.8
parent 3b2df1c978
commit c9c197ddae
13 changed files with 277 additions and 20 deletions
@@ -49,6 +49,10 @@ export function CompanySettingsPage() {
// Resturlaub-Verfall
const [carryoverMonth, setCarryoverMonth] = useState('')
const [carryoverDay, setCarryoverDay] = useState('')
// Urlaubsanspruch (PR2)
const [vacationDefaultDays, setVacationDefaultDays] = useState(30)
const [vacationProrate, setVacationProrate] = useState(true)
const [vacationFromSchedule, setVacationFromSchedule] = useState(false)
// Personalnummern
const [pnRequired, setPnRequired] = useState(false)
const [pnMode, setPnMode] = useState<'manual' | 'auto'>('manual')
@@ -106,6 +110,10 @@ export function CompanySettingsPage() {
setOvertimeExpiryMonth(cc.overtime_expiry_month ?? 3)
setOvertimeExpiryDay(cc.overtime_expiry_day ?? 31)
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 }
setVacationDefaultDays(vc.vacation_default_days ?? 30)
setVacationProrate(vc.vacation_prorate_first_year ?? true)
setVacationFromSchedule(vc.vacation_from_schedule ?? false)
}).catch(() => {})
api.get<{ configured: boolean; created_at: string | null }>('/companies/me/busylight-token')
.then(setBlStatus)
@@ -174,6 +182,9 @@ export function CompanySettingsPage() {
personnel_number_mode: pnMode,
personnel_number_next: pnNext,
mobile_stamping_enabled: mobileStamping,
vacation_default_days: vacationDefaultDays,
vacation_prorate_first_year: vacationProrate,
vacation_from_schedule: vacationFromSchedule,
overtime_overdraft_allowed: fzaOverdraftAllowed,
overtime_warning_threshold_hours: fzaWarningThreshold,
kiosk_require_approval: kioskRequireApproval,
@@ -347,6 +358,36 @@ export function CompanySettingsPage() {
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Jahresurlaub (Vollzeit)
</label>
<div className="flex items-center gap-2">
<input
type="number" min={0} max={365} value={vacationDefaultDays}
onChange={e => setVacationDefaultDays(parseInt(e.target.value) || 0)}
disabled={!isAdmin}
className="w-24 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-50"
/>
<span className="text-sm text-gray-500">Tage/Jahr für neue Urlaubskonten</span>
</div>
</div>
<label className={`flex items-start gap-3 ${isAdmin ? 'cursor-pointer' : 'opacity-60'}`}>
<input type="checkbox" checked={vacationProrate} disabled={!isAdmin}
onChange={e => setVacationProrate(e.target.checked)} className="mt-0.5" />
<span className="text-sm text-gray-700">
Anteilig im Ein-/Austrittsjahr
<span className="block text-xs text-gray-400">Zwölftel-Regel anhand Ein-/Austrittsdatum des Mitarbeiters.</span>
</span>
</label>
<label className={`flex items-start gap-3 ${isAdmin ? 'cursor-pointer' : 'opacity-60'}`}>
<input type="checkbox" checked={vacationFromSchedule} disabled={!isAdmin}
onChange={e => setVacationFromSchedule(e.target.checked)} className="mt-0.5" />
<span className="text-sm text-gray-700">
Teilzeit-Anspruch aus Arbeitsplan ableiten
<span className="block text-xs text-gray-400">Anspruch = Jahresurlaub × Arbeitstage pro Woche ÷ 5.</span>
</span>
</label>
<div className="border-t border-gray-100 pt-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
Resturlaub verfällt am
</label>