From e8bed435708d1c036e819d4e9562c96beab166a2 Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 23 Jun 2026 12:03:42 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20agent-11=20PR2=20=E2=80=93=20Urlaubsans?= =?UTF-8?q?pruch=20(Verfall=20scharf,=20Pro-rata,=20Teilzeit)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/app/models/company.py | 8 ++ backend/app/models/user.py | 8 +- backend/app/routers/absences.py | 16 +--- backend/app/schemas/absence.py | 2 + backend/app/schemas/company.py | 6 ++ backend/app/schemas/user.py | 7 +- backend/app/services/absence_service.py | 80 ++++++++++++++++++- .../versions/0036_vacation_entitlement.py | 36 +++++++++ backend/tests/test_absences.py | 67 ++++++++++++++++ frontend/src/pages/AbsencesPage.tsx | 7 +- frontend/src/pages/CompanySettingsPage.tsx | 41 ++++++++++ frontend/src/pages/UsersPage.tsx | 18 +++++ frontend/src/types/absence.ts | 1 + 13 files changed, 277 insertions(+), 20 deletions(-) create mode 100644 backend/migrations/versions/0036_vacation_entitlement.py diff --git a/backend/app/models/company.py b/backend/app/models/company.py index 83774b4..f8b4a19 100644 --- a/backend/app/models/company.py +++ b/backend/app/models/company.py @@ -70,6 +70,14 @@ class Company(Base): # Mobile-Konfiguration mobile_stamping_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) + # Urlaubsanspruch-Konfiguration + # Basis-Jahresurlaub (Vollzeit) für neu angelegte Urlaubskonten. + vacation_default_days: Mapped[int] = mapped_column(Integer, nullable=False, default=30) + # Anteilige Berechnung im Ein-/Austrittsjahr (Zwölftel-Regel). + vacation_prorate_first_year: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) + # Teilzeit: Anspruch aus Arbeitstagen/Woche des WorkSchedule ableiten (opt-in). + vacation_from_schedule: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + # Freizeitausgleich-Konfiguration 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) diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 5ef9f2b..d7e4c3c 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,9 +1,9 @@ import uuid import enum -from datetime import datetime +from datetime import datetime, date from typing import TYPE_CHECKING -from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, String, Text, func +from sqlalchemy import Boolean, Date, DateTime, Enum, ForeignKey, String, Text, func from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -50,6 +50,10 @@ class User(Base): UUID(as_uuid=True), ForeignKey("work_schedules.id", ondelete="SET NULL") ) + # Ein-/Austrittsdatum – Basis für anteilige Urlaubsberechnung (Zwölftel-Regel) + entry_date: Mapped[date | None] = mapped_column(Date) + exit_date: Mapped[date | None] = mapped_column(Date) + # Kiosk auth kiosk_pin_hash: Mapped[str | None] = mapped_column(Text) kiosk_qr_token: Mapped[str | None] = mapped_column(Text, unique=True) diff --git a/backend/app/routers/absences.py b/backend/app/routers/absences.py index 88da6f3..54f5ad1 100644 --- a/backend/app/routers/absences.py +++ b/backend/app/routers/absences.py @@ -39,18 +39,8 @@ router = APIRouter(tags=["Abwesenheiten"]) def _carryover_expiry(company: Company, year: int) -> tuple[date | None, bool]: - """Verfallsdatum für Resturlaub berechnen. - Gibt (expires_at, is_expired) zurück. None wenn kein Verfall konfiguriert.""" - s = company.settings or {} - month = s.get("carryover_expires_month") - day = s.get("carryover_expires_day") - if not month or not day: - return None, False - try: - expires_at = date(year, int(month), int(day)) - return expires_at, date.today() > expires_at - except ValueError: - return None, False + """Verfallsdatum für Resturlaub. Delegiert an den Service (eine Quelle der Wahrheit).""" + return absence_service._carryover_expired(company, year) _admin_roles = (UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN) _manager_roles = (UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN) @@ -142,6 +132,7 @@ async def get_own_balance( expires_at, expired = _carryover_expiry(company, year) if company else (None, False) return VacationBalanceOut.model_validate(balance).model_copy(update={ "pending_days": pending, + "available_days": absence_service.effective_available(balance, expired), "carried_over_expires_at": expires_at, "carried_over_expired": expired, }) @@ -165,6 +156,7 @@ async def get_balance_for_user( expires_at, expired = _carryover_expiry(company, year) if company else (None, False) return VacationBalanceOut.model_validate(balance).model_copy(update={ "pending_days": pending, + "available_days": absence_service.effective_available(balance, expired), "carried_over_expires_at": expires_at, "carried_over_expired": expired, }) diff --git a/backend/app/schemas/absence.py b/backend/app/schemas/absence.py index 9a3903b..5e360c7 100644 --- a/backend/app/schemas/absence.py +++ b/backend/app/schemas/absence.py @@ -188,6 +188,8 @@ class VacationBalanceOut(BaseModel): used_days: int total_days: int remaining_days: int + # Effektiv verfügbar: berücksichtigt verfallenen Resturlaub (zur Laufzeit befüllt). + available_days: int = 0 pending_days: float = 0 # Resturlaub-Verfall (wird zur Laufzeit befüllt, nicht in DB) carried_over_expires_at: date | None = None diff --git a/backend/app/schemas/company.py b/backend/app/schemas/company.py index 8ec8999..e236164 100644 --- a/backend/app/schemas/company.py +++ b/backend/app/schemas/company.py @@ -47,6 +47,9 @@ class CompanyOut(BaseModel): kiosk_track_current_user: bool = True kiosk_heartbeat_interval_sec: int = 30 public_stamp_enabled: bool = False + vacation_default_days: int = 30 + vacation_prorate_first_year: bool = True + vacation_from_schedule: bool = False class PublicStampTokenStatus(BaseModel): @@ -82,6 +85,9 @@ class CompanyUpdate(BaseModel): kiosk_track_current_user: bool | None = None kiosk_heartbeat_interval_sec: int | None = Field(None, ge=10, le=120) public_stamp_enabled: bool | None = None + vacation_default_days: int | None = Field(None, ge=0, le=365) + vacation_prorate_first_year: bool | None = None + vacation_from_schedule: bool | None = None class DepartmentOut(BaseModel): diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index bbb8315..3fc044f 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime +from datetime import datetime, date from pydantic import BaseModel, EmailStr, Field, model_validator @@ -27,6 +27,9 @@ class UserOut(BaseModel): kuerzel: str | None = None personnel_number: str | None = None can_manual_time_entry: bool = False + work_schedule_id: uuid.UUID | None = None + entry_date: date | None = None + exit_date: date | None = None class UserUpdate(BaseModel): @@ -39,6 +42,8 @@ class UserUpdate(BaseModel): personnel_number: str | None = Field(None, max_length=50, pattern=PERSONNEL_NUMBER_PATTERN) can_manual_time_entry: bool | None = None is_active: bool | None = None + entry_date: date | None = None + exit_date: date | None = None class InviteRequest(BaseModel): diff --git a/backend/app/services/absence_service.py b/backend/app/services/absence_service.py index a59121f..05e872d 100644 --- a/backend/app/services/absence_service.py +++ b/backend/app/services/absence_service.py @@ -1,4 +1,5 @@ import asyncio +import math from datetime import date, timedelta from uuid import UUID @@ -168,10 +169,13 @@ class AbsenceService: warnings: list[str] = [] if absence_type.deducts_vacation: balance = await self._get_or_create_balance(current_user.id, data.start_date.year, db) - if balance.remaining_days < working_days: + company = await db.get(Company, current_user.company_id) + _, expired = self._carryover_expired(company, data.start_date.year) + available = self.effective_available(balance, expired) + if available < working_days: warnings.append( f"Urlaubskonto reicht möglicherweise nicht aus: " - f"{balance.remaining_days} Tage verfügbar, {working_days} Tage beantragt." + f"{available} Tage verfügbar, {working_days} Tage beantragt." ) # Überschneidung mit eigenen Abwesenheiten prüfen @@ -631,7 +635,13 @@ class AbsenceService: ) ) carried = max(0, prev.remaining_days) if prev else 0 - entitled = prev.entitled_days if prev else 30 + # Grundanspruch frisch berechnen (Firmen-Default · Teilzeit · Pro-rata). + # Fallback auf Vorjahres-Anspruch bzw. 30, falls keine Konfig greift. + user = await db.get(User, user_id) + company = await db.get(Company, user.company_id) if user and user.company_id else None + entitled = await self._compute_entitlement(user, company, year, db) + if entitled is None: + entitled = prev.entitled_days if prev else 30 balance = VacationBalance( user_id=user_id, year=year, @@ -642,6 +652,70 @@ class AbsenceService: await db.flush() return balance + @staticmethod + def _working_days_per_week(schedule: WorkSchedule) -> int: + return sum( + 1 for h in [schedule.mon_h, schedule.tue_h, schedule.wed_h, + schedule.thu_h, schedule.fri_h, schedule.sat_h, schedule.sun_h] + if h and h > 0 + ) + + async def _compute_entitlement( + self, user: "User | None", company: "Company | None", year: int, db: AsyncSession + ) -> int | None: + """Grundurlaub für ein neues Konto: Firmen-Default, optional Teilzeit-Skalierung + (Arbeitstage/Woche aus WorkSchedule) und anteilig im Ein-/Austrittsjahr (Zwölftel). + Gibt None zurück, wenn keine Berechnung möglich ist (→ Aufrufer nutzt Fallback).""" + if company is None: + return None + days = float(company.vacation_default_days) + + # Teilzeit: Anspruch proportional zu den Arbeitstagen/Woche (5 = Vollzeit) + if company.vacation_from_schedule and user and user.work_schedule_id: + schedule = await db.get(WorkSchedule, user.work_schedule_id) + if schedule: + wd = self._working_days_per_week(schedule) + days = days * wd / 5.0 + + # Anteilig im Ein-/Austrittsjahr (Zwölftel ab/bis Monat) + if company.vacation_prorate_first_year and user: + months = 12 + if user.entry_date: + if user.entry_date.year > year: + months = 0 + elif user.entry_date.year == year: + months = min(months, 12 - user.entry_date.month + 1) + if user.exit_date: + if user.exit_date.year < year: + months = 0 + elif user.exit_date.year == year: + months = min(months, user.exit_date.month) + days = days * max(0, months) / 12.0 + + # Kaufmännisch runden + return int(math.floor(days + 0.5)) + + @staticmethod + def _carryover_expired(company: "Company | None", year: int) -> tuple[date | None, bool]: + """Verfallsdatum des Resturlaubs aus company.settings. (expires_at, is_expired).""" + s = (company.settings or {}) if company else {} + month, day = s.get("carryover_expires_month"), s.get("carryover_expires_day") + if not month or not day: + return None, False + try: + expires_at = date(year, int(month), int(day)) + return expires_at, date.today() > expires_at + except (ValueError, TypeError): + return None, False + + @staticmethod + def effective_available(balance: VacationBalance, expired: bool) -> int: + """Effektiv verfügbare Urlaubstage. Bei verfallenem Resturlaub zählt der noch + nicht verbrauchte Teil des Übertrags nicht mehr mit.""" + if expired: + return balance.entitled_days + balance.special_days - max(0, balance.used_days - balance.carried_over) + return balance.remaining_days + async def _deduct_vacation( self, user_id: UUID, year: int, days: int, db: AsyncSession ) -> None: diff --git a/backend/migrations/versions/0036_vacation_entitlement.py b/backend/migrations/versions/0036_vacation_entitlement.py new file mode 100644 index 0000000..50da1a4 --- /dev/null +++ b/backend/migrations/versions/0036_vacation_entitlement.py @@ -0,0 +1,36 @@ +"""Vacation entitlement: pro-rata + part-time config (agent-11 PR2) + +Revision ID: 0036 +Revises: 0035 +Create Date: 2026-06-23 + +- users.entry_date / exit_date (Basis für anteilige Urlaubsberechnung, Zwölftel) +- companies.vacation_default_days (Vollzeit-Grundurlaub für neue Konten) +- companies.vacation_prorate_first_year (anteilig im Ein-/Austrittsjahr) +- companies.vacation_from_schedule (Teilzeit-Anspruch aus WorkSchedule ableiten) + +Resturlaub-Verfall bleibt in companies.settings (carryover_expires_month/day), +wird in PR2 nur scharf geschaltet (effektiv verfügbarer Saldo) – keine Spalte nötig. +""" +from alembic import op + +revision = "0036" +down_revision = "0035" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.execute("ALTER TABLE users ADD COLUMN IF NOT EXISTS entry_date DATE") + op.execute("ALTER TABLE users ADD COLUMN IF NOT EXISTS exit_date DATE") + op.execute("ALTER TABLE companies ADD COLUMN IF NOT EXISTS vacation_default_days INTEGER NOT NULL DEFAULT 30") + op.execute("ALTER TABLE companies ADD COLUMN IF NOT EXISTS vacation_prorate_first_year BOOLEAN NOT NULL DEFAULT TRUE") + op.execute("ALTER TABLE companies ADD COLUMN IF NOT EXISTS vacation_from_schedule BOOLEAN NOT NULL DEFAULT FALSE") + + +def downgrade() -> None: + op.execute("ALTER TABLE companies DROP COLUMN IF EXISTS vacation_from_schedule") + op.execute("ALTER TABLE companies DROP COLUMN IF EXISTS vacation_prorate_first_year") + op.execute("ALTER TABLE companies DROP COLUMN IF EXISTS vacation_default_days") + op.execute("ALTER TABLE users DROP COLUMN IF EXISTS exit_date") + op.execute("ALTER TABLE users DROP COLUMN IF EXISTS entry_date") diff --git a/backend/tests/test_absences.py b/backend/tests/test_absences.py index f8609fe..9882bb9 100644 --- a/backend/tests/test_absences.py +++ b/backend/tests/test_absences.py @@ -625,3 +625,70 @@ async def test_absence_comments( assert lst.status_code == 200 bodies = [c["body"] for c in lst.json()] assert "Bitte zuegig pruefen" in bodies + + +# ── agent-11 PR2: Urlaubsanspruch (Verfall · Pro-rata · Teilzeit) ────────────── + +def test_effective_available_not_expired(): + import uuid as _u + from app.services.absence_service import AbsenceService + from app.models.vacation_balance import VacationBalance + b = VacationBalance(user_id=_u.uuid4(), year=2026, entitled_days=30, special_days=0, carried_over=5, used_days=3) + assert AbsenceService.effective_available(b, False) == 32 # 30 + 5 - 3 + assert AbsenceService.effective_available(b, True) == 30 # Resturlaub verfällt (3 < 5) + + +def test_effective_available_expired_overdraw(): + import uuid as _u + from app.services.absence_service import AbsenceService + from app.models.vacation_balance import VacationBalance + b = VacationBalance(user_id=_u.uuid4(), year=2026, entitled_days=30, special_days=0, carried_over=5, used_days=8) + assert AbsenceService.effective_available(b, True) == 27 # 30 - max(0, 8 - 5) + + +@pytest.mark.asyncio +async def test_company_vacation_settings_roundtrip(client: AsyncClient, abs_headers): + r = await client.patch("/api/v1/companies/me", json={ + "vacation_default_days": 28, "vacation_prorate_first_year": True, "vacation_from_schedule": False, + }, headers=abs_headers) + assert r.status_code == 200, r.text + assert r.json()["vacation_default_days"] == 28 + await client.patch("/api/v1/companies/me", json={"vacation_default_days": 30}, headers=abs_headers) + + +@pytest.mark.asyncio +async def test_prorate_entry_year(client: AsyncClient, abs_headers): + inv = await client.post("/api/v1/users/invite", json={ + "first_name": "Pro", "last_name": "Rata", "email": "prorata@absenceag.de", + "role": "EMPLOYEE", "initial_password": "Secret123", + }, headers=abs_headers) + uid = inv.json()["id"] + yr = date.today().year + await client.patch(f"/api/v1/users/{uid}", json={"entry_date": f"{yr}-07-01"}, headers=abs_headers) + bal = await client.get(f"/api/v1/absences/balance/{uid}?year={yr}", headers=abs_headers) + assert bal.status_code == 200, bal.text + assert bal.json()["entitled_days"] == 15 # 30 * 6/12 (Eintritt Juli) + + +@pytest.mark.asyncio +async def test_part_time_entitlement_from_schedule(client: AsyncClient, abs_headers): + sched = await client.post("/api/v1/time/schedules", json={ + "name": "Teilzeit 3 Tage", "mon_h": 8, "tue_h": 8, "wed_h": 8, + "thu_h": 0, "fri_h": 0, "valid_from": "2026-01-01", + }, headers=abs_headers) + assert sched.status_code == 201, sched.text + sid = sched.json()["id"] + await client.patch("/api/v1/companies/me", json={ + "vacation_from_schedule": True, "vacation_default_days": 30, + }, headers=abs_headers) + inv = await client.post("/api/v1/users/invite", json={ + "first_name": "Teil", "last_name": "Zeit", "email": "teilzeit@absenceag.de", + "role": "EMPLOYEE", "initial_password": "Secret123", + }, headers=abs_headers) + uid = inv.json()["id"] + await client.patch(f"/api/v1/users/{uid}", json={"work_schedule_id": sid}, headers=abs_headers) + yr = date.today().year + bal = await client.get(f"/api/v1/absences/balance/{uid}?year={yr}", headers=abs_headers) + assert bal.status_code == 200, bal.text + assert bal.json()["entitled_days"] == 18 # 30 * 3/5 + await client.patch("/api/v1/companies/me", json={"vacation_from_schedule": False}, headers=abs_headers) diff --git a/frontend/src/pages/AbsencesPage.tsx b/frontend/src/pages/AbsencesPage.tsx index 0cff54a..b08dc8a 100644 --- a/frontend/src/pages/AbsencesPage.tsx +++ b/frontend/src/pages/AbsencesPage.tsx @@ -374,8 +374,11 @@ export function AbsencesPage() {

Genommen

{balance.used_days}

{balance.pending_days > 0 &&

Beantragt

{balance.pending_days}

}
-

Verbleibend

-

5 ? 'text-green-600' : balance.remaining_days > 0 ? 'text-yellow-600' : 'text-red-600'}`}>{balance.remaining_days}

+

Verfügbar

+

5 ? 'text-green-600' : balance.available_days > 0 ? 'text-yellow-600' : 'text-red-600'}`}>{balance.available_days}

+ {balance.carried_over_expired && balance.carried_over > 0 && ( +

Resturlaub verfallen

+ )}
diff --git a/frontend/src/pages/CompanySettingsPage.tsx b/frontend/src/pages/CompanySettingsPage.tsx index 1850112..fbcec76 100644 --- a/frontend/src/pages/CompanySettingsPage.tsx +++ b/frontend/src/pages/CompanySettingsPage.tsx @@ -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() {

+ +
+ 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" + /> + Tage/Jahr für neue Urlaubskonten +
+
+ + +
diff --git a/frontend/src/pages/UsersPage.tsx b/frontend/src/pages/UsersPage.tsx index 6c3746c..a52d4c4 100644 --- a/frontend/src/pages/UsersPage.tsx +++ b/frontend/src/pages/UsersPage.tsx @@ -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('') const [editPersonnelNr, setEditPersonnelNr] = useState('') const [editCanManual, setEditCanManual] = useState(false) + const [editEntryDate, setEditEntryDate] = useState('') + const [editExitDate, setEditExitDate] = useState('') 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() {

Noch keine Arbeitszeitmodelle angelegt.

)}
+
+
+ + setEditEntryDate(e.target.value)} className={inputClass} /> +
+
+ + setEditExitDate(e.target.value)} className={inputClass} /> +
+

Basis für anteilige Urlaubsberechnung (Zwölftel-Regel).

+