[[projectstructure]]
= Project Structure
:doctype: book
:imagesdir: ..
:sectnums:
:toc: left
:icons: font
:experimental:

The project is structured in the following folders:

* *optaweb-employee-rostering-backend* core planning domain model and backend REST api implemented with Quarkus
* *optaweb-employee-rostering-benchmark* solution benchmark
* *optaweb-employee-rostering-distribution* assembly logic for the deployment assets
* *optaweb-employee-rostering-docs* this documentation
* *optaweb-employee-rostering-frontend* user interface implemented with ReactJS

== Domain Model

The domain model is the most important piece of a project based on OptaPlanner. A careful design simplifies the
constraint definition. The classes of the domain model are placed in the *optaweb-employee-rostering-backend* module.

The most important classes to understand the domain model are:

* *Shift* is the _planning entity_, where is the defined the relationship with the _planning variable_ employee. Other important fields: `spot`, `rotationEmployee`, `startDateTime`, `endDateTime`.
* *Employee* is the _planning variable_, it's identified by the `name` and has a set of skills (`skillProficiencySet`).
* *Roster* is the _planning solution_, `employeeList` is the list of employees (the range of values that can be assigned to the _planning variable_), the field `score` holds the score (3 levels: hard, medium, soft), the other problem facts are: `skillList`, `spotList`, `employeeAvailabilityList`, `rosterConstraintConfiguration`, `rosterState`, `shiftList`.

== Constraints

The constraints are defined in the *optaweb-employee-rostering-backend* module, with the implementation of the backend REST
service.

* The solver configuration file:
`optaweb-employee-rostering-backend/src/main/resources/org/optaweb/employeerostering/service/solver/employeeRosteringSolverConfig.xml`
* The constraints definition file: `optaweb-employee-rostering-backend/src/main/resources/org/optaweb/employeerostering/service/solver/employeeRosteringScoreRules.drl`

=== Constraint definition

The constraints are defined using the DRL language. See: https://docs.optaplanner.org/latestFinal/optaplanner-docs/html_single/index.html#implementingAScoreRule[Implementing a score rule].

==== Hard Constraints

*Required skill for a shift*

[source,ruby]
----
rule "Required skill for a shift"
    when
        Shift(
                employee != null,
                !getEmployee().hasSkills(getSpot().getRequiredSkillSet()))
    then
        scoreHolder.addHardConstraintMatch(kcontext, -100);
end
----

Condition: there is a shift with an assigned employee that has NOT the skill set required by the spot.

Action: the hard score is decreased by 100 units.

*Unavailable time slot for an employee*

[source,ruby]
----
rule "Unavailable time slot for an employee"
    when
        EmployeeAvailability(
                state == EmployeeAvailabilityState.UNAVAILABLE,
                $e : employee,
                $startDateTime : startDateTime,
                $endDateTime : endDateTime)
        Shift(
                employee == $e,
                DateTimeUtils.doTimeslotsIntersect($startDateTime,$endDateTime,
                                                   startDateTime, endDateTime))
    then
        scoreHolder.addHardConstraintMatch(kcontext, -50);
end
----

Condition: Given an employee unavailability, there is a shift for this employee, the date time interval of the shift intersects the date time interval of the unavailability.

Action: The hard score is decreased by 50 units.

*At most one shift assignment per day per employee*

[source,ruby]
----
rule "At most one shift assignment per day per employee"
    when
        $s : Shift(
                employee != null,
                $e : employee,
                $leftDay : startDateTime.toLocalDate())
        Shift(
                employee == $e,
                startDateTime.toLocalDate() == $leftDay,
                this != $s)
    then
        scoreHolder.addHardConstraintMatch(kcontext, -10);
end
----

Condition: There are two shifts assigned to the same employee, the start date of one shift is equal to the start date of the other shift.

Action: The hard score is decreased by 10 units.

WARNING: This rule triggers for any combination of shifts for each employee. So considering `n` employees and `m`
shifts, it triggers `n*m^2` times. Luckily, the rules triggers just for shifts that are impacted by a change.

*No 2 shifts within 10 hours from each other*

[source,ruby]
----
rule "No 2 shifts within 10 hours from each other"
    when
        $s : Shift(
                employee != null,
                $e : employee,
                $leftEndDateTime : endDateTime)
        Shift(
                employee == $e,
                $leftEndDateTime <= endDateTime,
                $leftEndDateTime.until(startDateTime, ChronoUnit.HOURS) < 10,
                this != $s)
    then
        scoreHolder.addHardConstraintMatch(kcontext, -1);
end
----

Condition: There are two shifts assigned to the same employee, the end time of the _left_ shift is prior of the other
end time, the time difference between the end time of the _left_ shift and the start time of the other is less than 10 hours.

Action: The hard score is decreased by 1 unit.

*Daily minutes must not exceed contract maximum*

[source,ruby]
----
rule "Daily minutes must not exceed contract maximum"
    when
        $employee : Employee($contract : contract, $contract.getMaximumMinutesPerDay() != null)
        $s : Shift(employee == $employee, $startDateTime : startDateTime)
        Number( intValue > $contract.getMaximumMinutesPerDay() ) from accumulate(
            Shift(employee == $employee, $shiftStart : startDateTime,
            $shiftEnd : endDateTime,
            $shiftStart.toLocalDate().equals($startDateTime.toLocalDate())),
            sum(Duration.between($shiftStart, $shiftEnd).toMinutes())
        )
    then
        scoreHolder.addHardConstraintMatch(kcontext, -1);
end
----

Condition: The sum of the total minutes assigned to one employee in a day is greater than the maximum minutes
specified by the employee's contract.

Action: The hard score is decreased by 1 unit.

The remaining three hard constraints are similar to this last one, but for different time frames specified by the
contract (weekly, monthly, yearly).

==== Medium Constraints

*Assign every shift*

[source,ruby]
----
rule "Assign every shift"
    when
        Shift(employee == null)
    then
        scoreHolder.addMediumConstraintMatch(kcontext, -1);
end
----

Condition: There is a shift with no employees assigned.

Action: The medium score is decreased by 1 unit.

==== Soft Constraints

*Undesired time slot for an employee*

[source,ruby]
----
rule "Undesired time slot for an employee"
    when
    	$rosterConstraintConfiguration : RosterConstraintConfiguration(undesiredTimeSlotWeight != 0)
        EmployeeAvailability(
                state == EmployeeAvailabilityState.UNDESIRED,
                $e : employee,
                $startDateTime : startDateTime,
                $endDateTime : endDateTime)
        Shift(
                employee == $e,
                DateTimeUtils.doTimeslotsIntersect($startDateTime,$endDateTime,
                                                   startDateTime, endDateTime))
    then
        scoreHolder.addSoftConstraintMatch(kcontext, -$rosterConstraintConfiguration.getUndesiredTimeSlotWeight());
end
----

TIP: The first line of the `when` clause is a technique to dynamically change the weight of the constraint. If `undesiredTimeSlotWeight` is 0 the constraint is disregarded.

Condition: Given an employee's undesired date and time slot, there is a shift for this employee such that the date
and time interval of the shift intersects the undesired date and time slot.

Action: The soft score is decreased by _undesiredTimeSlotWeight_ units.

*Desired time slot for an employee*

[source,ruby]
----
rule "Desired time slot for an employee"
    when
        $rosterConstraintConfiguration : RosterConstraintConfiguration(desiredTimeSlotWeight != 0)
        EmployeeAvailability(
                state == EmployeeAvailabilityState.DESIRED,
                $e : employee,
                $startDateTime : startDateTime,
                $endDateTime : endDateTime)
        Shift(
                employee == $e,
                DateTimeUtils.doTimeslotsIntersect($startDateTime,$endDateTime,
                                                   startDateTime, endDateTime))
    then
        scoreHolder.addSoftConstraintMatch(kcontext, +$rosterConstraintConfiguration.getDesiredTimeSlotWeight());
end
----

TIP: The first line of the `when` clause is a technique to dynamically change the weight of the constraint. If `desiredTimeSlotWeight` is 0 the constraint is disregarded.

Condition:  Given an employee desired date and time slot, there is a shift for this employee such that the date and time interval of the shift intersects the desired date and time slot.

Action: The soft score is increased by _desiredTimeSlotWeight_ units.

*Employee is not rotation employee*

[source,ruby]
----
rule "Employee is not rotation employee"
    when
    	$rosterConstraintConfiguration : RosterConstraintConfiguration(rotationEmployeeMatchWeight != 0)
        Shift(
                rotationEmployee != null, employee != null, employee != rotationEmployee)
    then
        scoreHolder.addSoftConstraintMatch(kcontext, -$rosterConstraintConfiguration.getRotationEmployeeMatchWeight());
end
----

TIP: The first line of the `when` clause is a technique to dynamically change the weight of the constraint. If `rotationEmployeeMatchWeight` is 0 the constraint is disregarded.

IMPORTANT: In general, employees desire to work following a regular schedule: a rotation plan. This represents a
starting point for the actual schedule that is influenced by other factors (e.g. temporary unavailability). For this
reason, all Shifts are initialized with a `rotationEmployee`.

Condition: There a shift that is assigned to an employee which is not the rotation employee.

Action: The soft score is decreased by _rotationEmployeeMatchWeight_ units.
