refactor: derive the bus escort from the travel date

This commit is contained in:
fromme committed 2026-10-16 08:36:54 +02:00
1 parent 2fc9806c69
commit 0e2021e96d
9 files changed
+59 -50

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Drops the staffing matrix's Busbegleitung flag.
*
* Whether a date gets a bus escort follows from the date itself: one with boarding points has
* bus travel, and App\Service\Assignment\AssignmentPlanner pre-selects the first of them. A flag
* per product could only repeat that, and less precisely, since bus travel varies between the
* dates of one product.
*
* Written by hand rather than taken from doctrine:migrations:diff, which also carries the
* pre-existing drift of the dev database.
*/
final class Version20261016090000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Drop staffing_rule.bus_escort';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE staffing_rule DROP bus_escort');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE staffing_rule ADD bus_escort TINYINT NOT NULL');
}
}
-23
View File
@@ -62,9 +62,6 @@ class StaffingRule implements BlameableEntityInterface, TimestampableEntityInter
#[ORM\Column]
private bool $guiding = false;
#[ORM\Column]
private bool $busEscort = false;
/** Previewed either way; only enabled rows generate. */
#[ORM\Column]
private bool $enabled = false;
@@ -102,14 +99,6 @@ class StaffingRule implements BlameableEntityInterface, TimestampableEntityInter
->addViolation()
;
}
if (true === $this->busEscort && 0 === (int) $this->guides) {
$context
->buildViolation('Busbegleitung braucht mindestens eine Reiseleitung')
->atPath('busEscort')
->addViolation()
;
}
}
public function getSkiLevelCount(): int
@@ -200,18 +189,6 @@ class StaffingRule implements BlameableEntityInterface, TimestampableEntityInter
return $this;
}
public function isBusEscort(): bool
{
return $this->busEscort;
}
public function setBusEscort(bool $busEscort): static
{
$this->busEscort = $busEscort;
return $this;
}
public function isEnabled(): bool
{
return $this->enabled;
-4
View File
@@ -40,10 +40,6 @@ class StaffingRuleType extends AbstractType
'label' => 'Guiding',
'required' => false,
])
->add('busEscort', CheckboxType::class, [
'label' => 'Busbegleitung',
'required' => false,
])
->add('enabled', CheckboxType::class, [
'label' => 'aktiv',
'help' => 'Nur aktive Zeilen legen Einsätze an; die Vorschau zeigt alle',
+5 -3
View File
@@ -18,8 +18,10 @@ use App\Service\Common\PickupOptions;
* - 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.
* - 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
{
@@ -61,7 +63,7 @@ final class AssignmentPlanner
}
}
$pickup = true === $rule->isBusEscort() ? $this->firstPickup($destination) : null;
$pickup = $this->firstPickup($destination);
$planned = [];
foreach ($roles as [$role, $places]) {
@@ -6,7 +6,6 @@
{{ form_row(form.skiAdvanced) }}
{{ form_row(form.split) }}
{{ form_row(form.guiding) }}
{{ form_row(form.busEscort) }}
{{ form_row(form.enabled) }}
</div>
<button type="submit" class="btn">
@@ -15,7 +15,8 @@
</div>
<p class="pb-4">
Für jeden künftigen Termin eines aktiven Produkts legt der Einsatz-Generator die Einsätze als
Entwurf an. Termine, die schon einmal Einsätze hatten, bleiben unberührt.
Entwurf an. Termine, die schon einmal Einsätze hatten, bleiben unberührt. Die Busbegleitung
ergibt sich aus den Zustiegen des Termins: Hat er welche, ist der erste vorausgewählt.
</p>
<div class="data-table-wrapper">
<div class="data-table-wrapper__inner">
@@ -28,7 +29,6 @@
<th>Ski Fortgeschrittene</th>
<th>Split</th>
<th>Guiding</th>
<th>Busbegleitung</th>
<th>aktiv</th>
<th></th>
</tr>
@@ -42,7 +42,6 @@
<td>{{ _self.yes_no(rule.skiAdvanced) }}</td>
<td>{{ _self.yes_no(rule.split) }}</td>
<td>{{ _self.yes_no(rule.guiding) }}</td>
<td>{{ _self.yes_no(rule.busEscort) }}</td>
<td>{{ _self.yes_no(rule.enabled) }}</td>
<td>
<div class="flex items-center space-x-2 justify-end">
@@ -66,7 +65,7 @@
</tr>
{% else %}
<tr>
<td colspan="9">
<td colspan="8">
Keine Daten...
</td>
</tr>
-3
View File
@@ -39,7 +39,6 @@ class StaffingRuleTest extends TestCase
yield 'split with more levels than guides' => [self::rule(1, beginner: true, advanced: true, split: true), ['split']];
yield 'split without a level' => [self::rule(2, split: true), ['split']];
yield 'guiding without a guide' => [self::rule(0, beginner: true, guiding: true), ['guiding']];
yield 'bus escort without a guide' => [self::rule(0, beginner: true, busEscort: true), ['busEscort']];
yield 'courses without guides' => [self::rule(0, beginner: true), []];
}
@@ -49,7 +48,6 @@ class StaffingRuleTest extends TestCase
bool $advanced = false,
bool $split = false,
bool $guiding = false,
bool $busEscort = false,
): StaffingRule {
return (new StaffingRule())
->setProductCode('DKIMP')
@@ -58,7 +56,6 @@ class StaffingRuleTest extends TestCase
->setSkiAdvanced($advanced)
->setSplit($split)
->setGuiding($guiding)
->setBusEscort($busEscort)
;
}
}
@@ -85,14 +85,14 @@ class AssignmentPlannerTest extends TestCase
['busProId' => 5, 'city' => 'Göttingen', 'street' => null, 'time' => '08.03.2024 22:00:00'],
]);
$planned = $this->plan(self::rule(guides: 2, busEscort: true), $destination);
$planned = $this->plan(self::rule(guides: 2), $destination);
$this->assertSame(5, $planned[0]->pickup);
}
public function testTheBusEscortIsSetOnExactlyOneAssignment(): void
{
$planned = $this->plan(self::rule(guides: 3, beginner: true, split: true, busEscort: true));
$planned = $this->plan(self::rule(guides: 3, beginner: true, split: true), self::busTravel());
$this->assertSame([
[StaffingRole::GUIDE, 2, 5, []],
@@ -102,7 +102,7 @@ class AssignmentPlannerTest extends TestCase
public function testWithoutAPlainReiseleitungTheFirstCombinedOneEscortsTheBus(): void
{
$planned = $this->plan(self::rule(guides: 2, beginner: true, advanced: true, split: true, busEscort: true));
$planned = $this->plan(self::rule(guides: 2, beginner: true, advanced: true, split: true), self::busTravel());
$this->assertSame(5, $planned[0]->pickup);
$this->assertNull($planned[1]->pickup);
@@ -110,16 +110,16 @@ class AssignmentPlannerTest extends TestCase
public function testNoBusEscortWithoutBoardingPoints(): void
{
$planned = $this->plan(self::rule(guides: 1, busEscort: true), self::destination([]));
$planned = $this->plan(self::rule(guides: 1));
$this->assertNull($planned[0]->pickup);
}
public function testNoBusEscortWhenTheMatrixSaysNo(): void
public function testWithoutAGuideNobodyEscortsTheBus(): void
{
$planned = $this->plan(self::rule(guides: 1));
$planned = $this->plan(self::rule(guides: 0, beginner: true), self::busTravel());
$this->assertNull($planned[0]->pickup);
$this->assertSame([[StaffingRole::SKI_BEGINNER, 1, null, [StaffingFee::COURSE]]], self::summarize($planned));
}
/**
@@ -127,9 +127,14 @@ class AssignmentPlannerTest extends TestCase
*/
private function plan(StaffingRule $rule, ?Destination $destination = null): array
{
return (new AssignmentPlanner())->plan($rule, $destination ?? self::destination([
return (new AssignmentPlanner())->plan($rule, $destination ?? self::destination([]));
}
private static function busTravel(): Destination
{
return self::destination([
['busProId' => 5, 'city' => 'Göttingen', 'street' => null, 'time' => '08.03.2024 22:00:00'],
]));
]);
}
/**
@@ -149,7 +154,6 @@ class AssignmentPlannerTest extends TestCase
bool $advanced = false,
bool $split = false,
bool $guiding = false,
bool $busEscort = false,
): StaffingRule {
return (new StaffingRule())
->setProductCode('DKIMP')
@@ -158,7 +162,6 @@ class AssignmentPlannerTest extends TestCase
->setSkiAdvanced($advanced)
->setSplit($split)
->setGuiding($guiding)
->setBusEscort($busEscort)
;
}
@@ -59,7 +59,6 @@ trait StaffingFixturesTrait
->setGuides(2)
->setSkiBeginner(true)
->setGuiding(true)
->setBusEscort(true)
->setEnabled($enabled)
;