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:
@@ -374,8 +374,11 @@ export function AbsencesPage() {
|
||||
<div><p className='text-xs text-gray-500'>Genommen</p><p className='text-xl font-bold text-gray-800'>{balance.used_days}</p></div>
|
||||
{balance.pending_days > 0 && <div><p className='text-xs text-gray-500'>Beantragt</p><p className='text-xl font-bold text-yellow-600'>{balance.pending_days}</p></div>}
|
||||
<div>
|
||||
<p className='text-xs text-gray-500'>Verbleibend</p>
|
||||
<p className={`text-xl font-bold ${balance.remaining_days > 5 ? 'text-green-600' : balance.remaining_days > 0 ? 'text-yellow-600' : 'text-red-600'}`}>{balance.remaining_days}</p>
|
||||
<p className='text-xs text-gray-500'>Verfügbar</p>
|
||||
<p className={`text-xl font-bold ${balance.available_days > 5 ? 'text-green-600' : balance.available_days > 0 ? 'text-yellow-600' : 'text-red-600'}`}>{balance.available_days}</p>
|
||||
{balance.carried_over_expired && balance.carried_over > 0 && (
|
||||
<p className='text-[11px] text-red-500'>Resturlaub verfallen</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user