feat: generate draft assignments from a staffing matrix
This commit is contained in:
1 parent
3116d23199
commit
2fc9806c69
41 files changed
+2618
No files matched your search
@@ -0,0 +1,101 @@
|
||||
<?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;
|
||||
* - Busbegleitung pre-selects the first boarding point on exactly one assignment, the first
|
||||
* guide-type one - the plain Reiseleitung where there is one.
|
||||
*/
|
||||
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 = true === $rule->isBusEscort() ? $this->firstPickup($destination) : null;
|
||||
$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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user