# -*- coding: utf-8 -*- """Service Layer – Orchestriert Business-Logik für Abwesenheiten.""" import datetime from typing import Optional from sqlalchemy.orm import Session from ..core.models import Absence from ..core.enums import AbsenceStatus, ABSENCE_META from ..core.schemas import ( AbsenceCreate, AbsenceUpdate, AbsenceRead, BalanceRead, BalanceAdjust, ApprovalDecision, ) from .repository import AbsenceRepository, BalanceRepository from .rules import validate_absence, _business_days, RuleViolation from ..employees.repository import EmployeeRepository class AbsenceService: """Zentrale Geschäftslogik – wird von API UND Desktop-Client genutzt.""" def __init__(self, db: Session): self.db = db self.abs_repo = AbsenceRepository(db) self.bal_repo = BalanceRepository(db) self.emp_repo = EmployeeRepository(db) def create_absence(self, data: AbsenceCreate) -> Absence: emp = self.emp_repo.get_by_id(data.employee_id) if not emp: raise ValueError(f"Mitarbeiter {data.employee_id} nicht gefunden") validate_absence(self.db, data) bd = _business_days(data.start_date, data.end_date) meta = ABSENCE_META.get(data.category, {}) absence = self.abs_repo.create(data, business_days=bd) if not meta.get("requires_approval"): absence.status = AbsenceStatus.APPROVED self.db.commit() return absence def update_absence(self, absence_id: str, data: AbsenceUpdate) -> Optional[Absence]: absence = self.abs_repo.get_by_id(absence_id) if not absence: return None if data.start_date or data.end_date: start = data.start_date or absence.start_date end = data.end_date or absence.end_date check_data = AbsenceCreate( employee_id=absence.employee_id, category=data.category or absence.category, start_date=start, end_date=end, reason=data.reason or absence.reason, ) validate_absence(self.db, check_data, skip_rules=[]) absence.business_days = _business_days(start, end) result = self.abs_repo.update(absence_id, data) self.db.commit() return result def delete_absence(self, absence_id: str) -> bool: ok = self.abs_repo.delete(absence_id) if ok: self.db.commit() return ok def approve(self, decision: ApprovalDecision) -> Absence: absence = self.abs_repo.get_by_id(decision.absence_id) if not absence: raise ValueError("Abwesenheit nicht gefunden") if absence.status != AbsenceStatus.PENDING: raise ValueError(f"Status ist bereits {absence.status.value}") absence.approver_id = decision.approver_id absence.approved_at = datetime.datetime.utcnow() absence.status = AbsenceStatus.APPROVED if decision.approved else AbsenceStatus.REJECTED self.db.commit() return absence def get_absences_for_employee(self, employee_id: str, year: Optional[int] = None) -> list[Absence]: return self.abs_repo.list_for_employee(employee_id, year) def get_absences_for_period( self, start: datetime.date, end: datetime.date ) -> list[Absence]: return self.abs_repo.list_for_period(start, end) def get_all_absences(self, year: Optional[int] = None) -> list[Absence]: return self.abs_repo.list_all(year) def get_balance(self, employee_id: str, year: int) -> BalanceRead: emp = self.emp_repo.get_by_id(employee_id) if not emp: raise ValueError(f"Mitarbeiter {employee_id} nicht gefunden") ba = self.bal_repo.get_or_create(employee_id, year) used = self.abs_repo.used_days(employee_id, year) return BalanceRead( employee_id=employee_id, employee_name=emp.name, year=year, yearly_quota=ba.yearly_quota, carry_over=ba.carry_over, manual_adjustment=ba.manual_adjustment, total_available=ba.total_available, used_days=used, remaining=ba.total_available - used, ) def adjust_balance(self, employee_id: str, year: int, data: BalanceAdjust) -> BalanceRead: self.bal_repo.update( employee_id, year, carry_over=data.carry_over, manual_adjustment=data.manual_adjustment, yearly_quota=data.yearly_quota, ) self.db.commit() return self.get_balance(employee_id, year)