|
|
from solverforge_legacy.solver import SolverStatus |
|
|
from solverforge_legacy.solver.domain import ( |
|
|
planning_entity, |
|
|
planning_solution, |
|
|
PlanningId, |
|
|
PlanningVariable, |
|
|
PlanningEntityCollectionProperty, |
|
|
ProblemFactCollectionProperty, |
|
|
ValueRangeProvider, |
|
|
PlanningScore, |
|
|
) |
|
|
from solverforge_legacy.solver.score import HardSoftDecimalScore |
|
|
from datetime import datetime, date |
|
|
from typing import Annotated, List, Optional, Union |
|
|
from dataclasses import dataclass, field |
|
|
from .json_serialization import JsonDomainBase |
|
|
from pydantic import Field |
|
|
|
|
|
|
|
|
@dataclass |
|
|
class Employee: |
|
|
name: Annotated[str, PlanningId] |
|
|
skills: set[str] = field(default_factory=set) |
|
|
unavailable_dates: set[date] = field(default_factory=set) |
|
|
undesired_dates: set[date] = field(default_factory=set) |
|
|
desired_dates: set[date] = field(default_factory=set) |
|
|
|
|
|
|
|
|
@planning_entity |
|
|
@dataclass |
|
|
class Shift: |
|
|
id: Annotated[str, PlanningId] |
|
|
start: datetime |
|
|
end: datetime |
|
|
location: str |
|
|
required_skill: str |
|
|
employee: Annotated[Employee | None, PlanningVariable] = None |
|
|
|
|
|
def has_required_skill(self) -> bool: |
|
|
"""Check if assigned employee has the required skill.""" |
|
|
if self.employee is None: |
|
|
return False |
|
|
return self.required_skill in self.employee.skills |
|
|
|
|
|
def is_overlapping_with_date(self, dt: date) -> bool: |
|
|
"""Check if shift overlaps with a specific date.""" |
|
|
return self.start.date() == dt or self.end.date() == dt |
|
|
|
|
|
def get_overlapping_duration_in_minutes(self, dt: date) -> int: |
|
|
"""Calculate overlap duration in minutes for a specific date.""" |
|
|
start_date_time = datetime.combine(dt, datetime.min.time()) |
|
|
end_date_time = datetime.combine(dt, datetime.max.time()) |
|
|
|
|
|
|
|
|
max_start_time = max(start_date_time, self.start) |
|
|
min_end_time = min(end_date_time, self.end) |
|
|
|
|
|
minutes = (min_end_time - max_start_time).total_seconds() / 60 |
|
|
return int(max(0, minutes)) |
|
|
|
|
|
|
|
|
@planning_solution |
|
|
@dataclass |
|
|
class EmployeeSchedule: |
|
|
employees: Annotated[ |
|
|
list[Employee], ProblemFactCollectionProperty, ValueRangeProvider |
|
|
] |
|
|
shifts: Annotated[list[Shift], PlanningEntityCollectionProperty] |
|
|
score: Annotated[HardSoftDecimalScore | None, PlanningScore] = None |
|
|
solver_status: SolverStatus = SolverStatus.NOT_SOLVING |
|
|
|
|
|
|
|
|
|
|
|
class EmployeeModel(JsonDomainBase): |
|
|
name: str |
|
|
skills: List[str] = Field(default_factory=list) |
|
|
unavailable_dates: List[str] = Field(default_factory=list, alias="unavailableDates") |
|
|
undesired_dates: List[str] = Field(default_factory=list, alias="undesiredDates") |
|
|
desired_dates: List[str] = Field(default_factory=list, alias="desiredDates") |
|
|
|
|
|
|
|
|
class ShiftModel(JsonDomainBase): |
|
|
id: str |
|
|
start: str |
|
|
end: str |
|
|
location: str |
|
|
required_skill: str = Field(..., alias="requiredSkill") |
|
|
employee: Union[str, EmployeeModel, None] = None |
|
|
|
|
|
|
|
|
class EmployeeScheduleModel(JsonDomainBase): |
|
|
employees: List[EmployeeModel] |
|
|
shifts: List[ShiftModel] |
|
|
score: Optional[str] = None |
|
|
solver_status: Optional[str] = None |
|
|
|