feat: agent-11 PR2 – Urlaubsanspruch (Verfall scharf, Pro-rata, Teilzeit)

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 6fa66b8c13
commit e8bed43570
13 changed files with 277 additions and 20 deletions
+18
View File
@@ -18,6 +18,8 @@ interface UserOut {
kuerzel: string | null
personnel_number: string | null
can_manual_time_entry: boolean
entry_date: string | null
exit_date: string | null
last_login: string | null
created_at: string
}
@@ -126,6 +128,8 @@ export function UsersPage() {
const [editKuerzel, setEditKuerzel] = useState<string>('')
const [editPersonnelNr, setEditPersonnelNr] = useState<string>('')
const [editCanManual, setEditCanManual] = useState(false)
const [editEntryDate, setEditEntryDate] = useState<string>('')
const [editExitDate, setEditExitDate] = useState<string>('')
const [editLoading, setEditLoading] = useState(false)
// Personalnummer-Verfügbarkeitsprüfung
@@ -226,6 +230,8 @@ export function UsersPage() {
work_schedule_id: editScheduleId || null,
kuerzel: editKuerzel.trim() || null,
can_manual_time_entry: editCanManual,
entry_date: editEntryDate || null,
exit_date: editExitDate || null,
}
// Personalnr. nur senden wenn geändert
const newPn = editPersonnelNr.trim()
@@ -423,6 +429,7 @@ export function UsersPage() {
setEditUser(u); setEditRole(u.role); setEditScheduleId(u.work_schedule_id)
setEditKuerzel(u.kuerzel ?? ''); setEditPersonnelNr(u.personnel_number ?? '')
setEditCanManual(u.can_manual_time_entry); setPersonnelCheck('idle')
setEditEntryDate(u.entry_date ?? ''); setEditExitDate(u.exit_date ?? '')
}}
className='text-xs text-blue-600 hover:underline'
>
@@ -691,6 +698,17 @@ export function UsersPage() {
<p className='mt-1 text-xs text-gray-400'>Noch keine Arbeitszeitmodelle angelegt.</p>
)}
</div>
<div className='grid grid-cols-2 gap-3'>
<div>
<label className='block text-xs font-medium text-gray-700 mb-1'>Eintrittsdatum</label>
<input type='date' value={editEntryDate} onChange={e => setEditEntryDate(e.target.value)} className={inputClass} />
</div>
<div>
<label className='block text-xs font-medium text-gray-700 mb-1'>Austrittsdatum</label>
<input type='date' value={editExitDate} onChange={e => setEditExitDate(e.target.value)} className={inputClass} />
</div>
<p className='col-span-2 -mt-1 text-xs text-gray-400'>Basis für anteilige Urlaubsberechnung (Zwölftel-Regel).</p>
</div>
<div>
<label className='block text-xs font-medium text-gray-700 mb-1'>
Kürzel