feat: decide the staffing split per ski level

This commit is contained in:
fromme committed 2026-11-05 17:04:24 +01:00
1 parent 7aefe88610
commit f9d2ff084d
8 files changed
+158 -66

No files matched your search

+38 -16
View File
@@ -55,9 +55,13 @@ class StaffingRule implements BlameableEntityInterface, TimestampableEntityInter
#[ORM\Column]
private bool $skiAdvanced = false;
/** The Reiseleitungen take over the ski levels, one level per place. */
/** A Reiseleitung takes over the beginners' course (Reise- und Kursleitung), one place. */
#[ORM\Column]
private bool $split = false;
private bool $splitBeginner = false;
/** A Reiseleitung takes over the advanced course (Reise- und Kursleitung), one place. */
#[ORM\Column]
private bool $splitAdvanced = false;
#[ORM\Column]
private bool $guiding = false;
@@ -74,20 +78,26 @@ class StaffingRule implements BlameableEntityInterface, TimestampableEntityInter
#[Assert\Callback]
public function validateCombination(ExecutionContextInterface $context): void
{
$levels = $this->getSkiLevelCount();
if (true === $this->split && 0 === $levels) {
if (true === $this->splitBeginner && false === $this->skiBeginner) {
$context
->buildViolation('Ein Split braucht mindestens einen Skikurs')
->atPath('split')
->buildViolation('Die Reiseleitung kann nur einen Kurs übernehmen, der angeboten wird')
->atPath('splitBeginner')
->addViolation()
;
}
if (true === $this->split && $levels > (int) $this->guides) {
if (true === $this->splitAdvanced && false === $this->skiAdvanced) {
$context
->buildViolation('Bei einem Split braucht jedes Kursniveau eine eigene Reiseleitung')
->atPath('split')
->buildViolation('Die Reiseleitung kann nur einen Kurs übernehmen, der angeboten wird')
->atPath('splitAdvanced')
->addViolation()
;
}
if ($this->getSplitLevelCount() > (int) $this->guides) {
$context
->buildViolation('Jeder übernommene Kurs braucht eine eigene Reiseleitung')
->atPath('guides')
->addViolation()
;
}
@@ -101,9 +111,9 @@ class StaffingRule implements BlameableEntityInterface, TimestampableEntityInter
}
}
public function getSkiLevelCount(): int
public function getSplitLevelCount(): int
{
return (int) $this->skiBeginner + (int) $this->skiAdvanced;
return (int) $this->splitBeginner + (int) $this->splitAdvanced;
}
public function getId(): ?int
@@ -165,14 +175,26 @@ class StaffingRule implements BlameableEntityInterface, TimestampableEntityInter
return $this;
}
public function isSplit(): bool
public function isSplitBeginner(): bool
{
return $this->split;
return $this->splitBeginner;
}
public function setSplit(bool $split): static
public function setSplitBeginner(bool $splitBeginner): static
{
$this->split = $split;
$this->splitBeginner = $splitBeginner;
return $this;
}
public function isSplitAdvanced(): bool
{
return $this->splitAdvanced;
}
public function setSplitAdvanced(bool $splitAdvanced): static
{
$this->splitAdvanced = $splitAdvanced;
return $this;
}
+8 -3
View File
@@ -31,9 +31,14 @@ class StaffingRuleType extends AbstractType
'label' => 'Ski Fortgeschrittene',
'required' => false,
])
->add('split', CheckboxType::class, [
'label' => 'Split',
'help' => 'Die Reiseleitungen übernehmen die Kurse (Reise- und Kursleitung)',
->add('splitBeginner', CheckboxType::class, [
'label' => 'Reiseleitung übernimmt Ski Anfänger:innen',
'help' => 'Reise- und Kursleitung statt einer eigenen Kursleitung',
'required' => false,
])
->add('splitAdvanced', CheckboxType::class, [
'label' => 'Reiseleitung übernimmt Ski Fortgeschrittene',
'help' => 'Reise- und Kursleitung statt einer eigenen Kursleitung',
'required' => false,
])
->add('guiding', CheckboxType::class, [
+27 -32
View File
@@ -14,9 +14,10 @@ use App\Service\Common\PickupOptions;
*
* The rules, agreed with the stakeholders on 2026-10-15:
* - 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;
* - each ski level is a Kursleitung of its own, one place, unless the matrix has a Reiseleitung
* take it over: then it is the combined profile, one place, and the plain Reiseleitung keeps
* the remaining places. Decided per level (2026-11-05), so one Reiseleitung can take over one
* level while the other stays a Kursleitung;
* - 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
@@ -32,39 +33,33 @@ final class AssignmentPlanner
*/
public function plan(StaffingRule $rule, Destination $destination): array
{
$roles = [];
$guides = (int) $rule->getGuides();
$combined = [];
$courses = [];
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];
if (true === $rule->isSkiBeginner()) {
if (true === $rule->isSplitBeginner()) {
$combined[] = [StaffingRole::GUIDE_SKI_BEGINNER, 1];
} else {
$courses[] = [StaffingRole::SKI_BEGINNER, 1];
}
}
if (true === $rule->isSkiAdvanced()) {
if (true === $rule->isSplitAdvanced()) {
$combined[] = [StaffingRole::GUIDE_SKI_ADVANCED, 1];
} else {
$courses[] = [StaffingRole::SKI_ADVANCED, 1];
}
}
// The matrix validation refuses more taken-over levels than guides; max() keeps a row
// that slipped past it from planning a negative head count.
$roles = [
[StaffingRole::GUIDE, max(0, (int) $rule->getGuides() - count($combined))],
...$combined,
...$courses,
];
$pickup = $this->firstPickup($destination);
$guiding = $rule->isGuiding();
$planned = [];