104 lines
3.2 KiB
PHP
104 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Service\Assignment;
|
|
|
|
use App\Entity\Destination;
|
|
use App\Entity\StaffingRule;
|
|
use App\Enum\StaffingFee;
|
|
use App\Enum\StaffingRole;
|
|
use App\Model\PlannedAssignment;
|
|
use App\Service\Common\PickupOptions;
|
|
|
|
/**
|
|
* Turns a staffing matrix row into the draft assignments for one travel date.
|
|
*
|
|
* The rules, agreed 2026-10-15 (see .ddev/plans/teamer-auto-assignments-plan.md):
|
|
* - one assignment per role, the role's head count as its places;
|
|
* - without split, each ski level is a Kursleitung of its own, one place;
|
|
* - with split, a Reiseleitung takes over each level through the combined profile, one place
|
|
* per level, and the plain Reiseleitung keeps the remaining places;
|
|
* - Guiding is the guiding fee on every guide-type assignment, not an assignment of its own;
|
|
* - a date with bus travel (boarding points) gets its first boarding point pre-selected as
|
|
* Busbegleitung on exactly one assignment, the first guide-type one - the plain Reiseleitung
|
|
* where there is one. Taken from the date, not the matrix: bus travel varies between the
|
|
* dates of one product.
|
|
*/
|
|
final class AssignmentPlanner
|
|
{
|
|
/**
|
|
* @return list<PlannedAssignment>
|
|
*/
|
|
public function plan(StaffingRule $rule, Destination $destination): array
|
|
{
|
|
$roles = [];
|
|
$guides = (int) $rule->getGuides();
|
|
|
|
if (true === $rule->isSplit()) {
|
|
$combined = [];
|
|
|
|
if (true === $rule->isSkiBeginner()) {
|
|
$combined[] = StaffingRole::GUIDE_SKI_BEGINNER;
|
|
}
|
|
|
|
if (true === $rule->isSkiAdvanced()) {
|
|
$combined[] = StaffingRole::GUIDE_SKI_ADVANCED;
|
|
}
|
|
|
|
// The matrix validation refuses more levels than guides; max() keeps a row that
|
|
// slipped past it from planning a negative head count.
|
|
$roles[] = [StaffingRole::GUIDE, max(0, $guides - count($combined))];
|
|
|
|
foreach ($combined as $role) {
|
|
$roles[] = [$role, 1];
|
|
}
|
|
} else {
|
|
$roles[] = [StaffingRole::GUIDE, $guides];
|
|
|
|
if (true === $rule->isSkiBeginner()) {
|
|
$roles[] = [StaffingRole::SKI_BEGINNER, 1];
|
|
}
|
|
|
|
if (true === $rule->isSkiAdvanced()) {
|
|
$roles[] = [StaffingRole::SKI_ADVANCED, 1];
|
|
}
|
|
}
|
|
|
|
$pickup = $this->firstPickup($destination);
|
|
$planned = [];
|
|
|
|
foreach ($roles as [$role, $places]) {
|
|
if (0 === $places) {
|
|
continue;
|
|
}
|
|
|
|
$fees = [];
|
|
|
|
if (true === $role->isGuide() && true === $rule->isGuiding()) {
|
|
$fees[] = StaffingFee::GUIDING;
|
|
}
|
|
|
|
if (true === $role->isCourse()) {
|
|
$fees[] = StaffingFee::COURSE;
|
|
}
|
|
|
|
$planned[] = new PlannedAssignment(
|
|
$role,
|
|
$places,
|
|
true === $role->isGuide() ? $pickup : null,
|
|
$fees,
|
|
);
|
|
|
|
if (true === $role->isGuide()) {
|
|
$pickup = null;
|
|
}
|
|
}
|
|
|
|
return $planned;
|
|
}
|
|
|
|
private function firstPickup(Destination $destination): ?int
|
|
{
|
|
return PickupOptions::inDepartureOrder($destination)[0]['busProId'] ?? null;
|
|
}
|
|
}
|