feat: remove obsolete date range override
This commit is contained in:
+2
-3
@@ -204,9 +204,8 @@ pinned in tests without a database.
|
||||
|
||||
### A period means the season, not when the row was written
|
||||
|
||||
`StatisticsDateBasis::SEASON` (the default) filters on the assignment's dates, falling back to
|
||||
the destination's — the translation of `Assignment::getEffectivePeriod()` into DQL, which
|
||||
lives in `src/Repository/Filter/SeasonPeriodFilter.php` and is shared with
|
||||
`StatisticsDateBasis::SEASON` (the default) filters on the destination's date range through
|
||||
`src/Repository/Filter/SeasonPeriodFilter.php`, which is shared with
|
||||
`DispositionRepository`. This matches every other statistics screen and
|
||||
`StatisticsFilterType`. Change what a season means there, once, not per screen.
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Drops the per-assignment date range override (assignment.date_from / date_to).
|
||||
*
|
||||
* The columns were never writable from the UI and hold no data; every consumer now reads the
|
||||
* destination's dates directly. See .ddev/plans/remove-assignment-date-override.md.
|
||||
*/
|
||||
final class Version20260831144511 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Drop the unused per-assignment date range override columns';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE assignment DROP date_from, DROP date_to');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE assignment ADD date_from DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\', ADD date_to DATE DEFAULT NULL COMMENT \'(DC2Type:date_immutable)\'');
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class IndexController extends AbstractController
|
||||
$request->query->getInt('page', 1),
|
||||
$request->query->getInt('limit', 50),
|
||||
[
|
||||
'defaultSortFieldName' => 'assignment.dateFrom',
|
||||
'defaultSortFieldName' => 'destination.dateFrom',
|
||||
'defaultSortDirection' => 'asc',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ class IndexController extends AbstractController
|
||||
$request->query->getInt('page', 1),
|
||||
9,
|
||||
[
|
||||
'defaultSortFieldName' => 'assignment.dateFrom',
|
||||
'defaultSortFieldName' => 'destination.dateFrom',
|
||||
'defaultSortDirection' => 'asc',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ class IndexController extends AbstractController
|
||||
$request->query->getInt('page', 1),
|
||||
9,
|
||||
[
|
||||
'defaultSortFieldName' => 'assignment.dateFrom',
|
||||
'defaultSortFieldName' => 'destination.dateFrom',
|
||||
'defaultSortDirection' => 'asc',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -35,8 +35,8 @@ class IcsController extends AbstractController
|
||||
$filename = sprintf(
|
||||
'Einteilung_%s_%s-%s.ics',
|
||||
$jobProfile->getName(),
|
||||
$assignment->getEffectivePeriod()->start->format('d.m.Y'),
|
||||
$assignment->getEffectivePeriod()->end->format('d.m.Y')
|
||||
$destination->getDateFrom()->format('d.m.Y'),
|
||||
$destination->getDateTo()->format('d.m.Y')
|
||||
);
|
||||
$slugger = new AsciiSlugger();
|
||||
$filename = $slugger->slug($filename);
|
||||
|
||||
@@ -37,7 +37,7 @@ class RecentController extends AbstractController
|
||||
$request->query->getInt('page', 1),
|
||||
9,
|
||||
[
|
||||
'defaultSortFieldName' => 'assignment.dateFrom',
|
||||
'defaultSortFieldName' => 'destination.dateFrom',
|
||||
'defaultSortDirection' => 'asc',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ class UpcomingController extends AbstractController
|
||||
$request->query->getInt('page', 1),
|
||||
9,
|
||||
[
|
||||
'defaultSortFieldName' => 'assignment.dateFrom',
|
||||
'defaultSortFieldName' => 'destination.dateFrom',
|
||||
'defaultSortDirection' => 'asc',
|
||||
]
|
||||
);
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Entity\Traits\BlameableEntity;
|
||||
use App\Entity\Traits\SoftDeletableEntity;
|
||||
use App\Entity\Traits\TimestampableEntity;
|
||||
use App\Repository\AssignmentRepository;
|
||||
use Carbon\CarbonPeriodImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
@@ -64,13 +63,6 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
||||
#[ORM\Column]
|
||||
private bool $skipFormalities = false;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $dateFrom = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
#[Assert\GreaterThan(propertyPath: 'dateFrom', message: 'Das Enddatum muss nach dem Beginndatum liegen')]
|
||||
private ?\DateTimeImmutable $dateTo = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $pickupDate = null;
|
||||
|
||||
@@ -133,8 +125,6 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
||||
->setAvailableDispositions($assignment->getAvailableDispositions())
|
||||
->setShowAvailableDispositions($assignment->isShowAvailableDispositions())
|
||||
->setSkipFormalities($assignment->isSkipFormalities())
|
||||
->setDateFrom($assignment->getDateFrom())
|
||||
->setDateTo($assignment->getDateTo())
|
||||
->setPickupDate($assignment->getPickupDate())
|
||||
->setPickup($assignment->getPickup())
|
||||
->setBenefits($assignment->getBenefits())
|
||||
@@ -266,66 +256,6 @@ class Assignment implements BlameableEntityInterface, TimestampableEntityInterfa
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateFrom(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->dateFrom;
|
||||
}
|
||||
|
||||
public function setDateFrom(?\DateTimeImmutable $dateFrom): static
|
||||
{
|
||||
$this->dateFrom = $dateFrom;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateTo(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->dateTo;
|
||||
}
|
||||
|
||||
public function setDateTo(?\DateTimeImmutable $dateTo): static
|
||||
{
|
||||
$this->dateTo = $dateTo;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable on purpose: a plain CarbonPeriod hands out mutable Carbon instances, so a caller
|
||||
* deriving one date from another - `$end = $period->getEndDate(); $end->addDays(14)` - moves
|
||||
* the original as well and silently collapses its own period. CarbonPeriodImmutable yields
|
||||
* CarbonImmutable from start, end and getEndDate(), which makes that mistake impossible.
|
||||
*/
|
||||
public function getEffectivePeriod(): ?CarbonPeriodImmutable
|
||||
{
|
||||
if (null !== $destination = $this->getDestination()) {
|
||||
$dateFrom = $this->getDateFrom() ?? $destination->getDateFrom();
|
||||
$dateTo = $this->getDateTo() ?? $destination->getDateTo();
|
||||
|
||||
return new CarbonPeriodImmutable($dateFrom, $dateTo);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getEffectiveDateFrom(): ?\DateTimeImmutable
|
||||
{
|
||||
if (null !== $effectivePeriod = $this->getEffectivePeriod()) {
|
||||
return $effectivePeriod->start->toDateTimeImmutable();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getEffectiveDateTo(): ?\DateTimeImmutable
|
||||
{
|
||||
if (null !== $effectivePeriod = $this->getEffectivePeriod()) {
|
||||
return $effectivePeriod->end->toDateTimeImmutable();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getEffectivePickupDate(): ?\DateTimeImmutable
|
||||
{
|
||||
if (null !== $this->pickupDate) {
|
||||
|
||||
@@ -246,9 +246,8 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
|
||||
}
|
||||
|
||||
$assignment = $this->getAssignment();
|
||||
$assignmentPeriod = $assignment->getEffectivePeriod();
|
||||
$dueDateFrom = $this->getCreatedAt()->modify(sprintf('+%d days', $deadlineDays));
|
||||
$dueDateTo = $assignmentPeriod->end->modify('-1 day');
|
||||
$dueDateTo = $assignment->getDestination()->getDateTo()->modify('-1 day');
|
||||
|
||||
// A short-notice assignment can end before the normal $deadlineDays window would even
|
||||
// start. Left uncapped, dueDateFrom >= dueDateTo makes isStarted() and isEnded() equal
|
||||
@@ -297,9 +296,8 @@ class Disposition implements BlameableEntityInterface, TimestampableEntityInterf
|
||||
}
|
||||
|
||||
$assignment = $this->getAssignment();
|
||||
$assignmentPeriod = $assignment->getEffectivePeriod();
|
||||
$dueDateFrom = $assignmentPeriod->getEndDate();
|
||||
$dueDateTo = $dueDateFrom->addDays($deadlineDays);
|
||||
$dueDateFrom = $assignment->getDestination()->getDateTo();
|
||||
$dueDateTo = $dueDateFrom->modify(sprintf('+%d days', $deadlineDays));
|
||||
|
||||
$period = CarbonPeriodImmutable::create($dueDateFrom, $dueDateTo);
|
||||
|
||||
|
||||
@@ -98,8 +98,8 @@ class Feedback implements BlameableEntityInterface, TimestampableEntityInterface
|
||||
|
||||
$instance
|
||||
->setAssignment($assignment)
|
||||
->setAssignmentDateFrom($assignment->getEffectiveDateFrom())
|
||||
->setAssignmentDateTo($assignment->getEffectiveDateTo())
|
||||
->setAssignmentDateFrom($destination->getDateFrom())
|
||||
->setAssignmentDateTo($destination->getDateTo())
|
||||
->setDestinationName($destination->getProduct())
|
||||
->setHotel($destination->getHotel())
|
||||
->setHotelCode($destination->getHotelCode())
|
||||
|
||||
@@ -75,14 +75,13 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface
|
||||
/** @var Disposition $disposition */
|
||||
$disposition = $event->getSubject();
|
||||
$assignment = $disposition->getAssignment();
|
||||
$assignmentPeriod = $assignment->getEffectivePeriod();
|
||||
$today = new \DateTimeImmutable();
|
||||
|
||||
$dueDateFrom = $disposition
|
||||
->getCreatedAt()
|
||||
->modify(sprintf('+%d days', $this->contractUploadDeadlineDays))
|
||||
;
|
||||
$dueDateTo = $assignmentPeriod->end->modify('-1 day');
|
||||
$dueDateTo = $assignment->getDestination()->getDateTo()->modify('-1 day');
|
||||
|
||||
// Block upload if deadline has passed
|
||||
if ($dueDateTo < $today) {
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Entity\Upload;
|
||||
use App\Event\ApplicationDeletedEvent;
|
||||
use App\Event\DocumentConfirmedEvent;
|
||||
use App\Repository\ApplicationRepository;
|
||||
use Carbon\CarbonPeriodImmutable;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
@@ -37,10 +38,12 @@ class InvalidateApplicationsListener
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $period = $disposition->getAssignment()->getEffectivePeriod()) {
|
||||
if (null === $destination = $disposition->getAssignment()->getDestination()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$period = new CarbonPeriodImmutable($destination->getDateFrom(), $destination->getDateTo());
|
||||
|
||||
$applications = $this->applicationRepository->findOverlappingForTeamer($teamer, $period);
|
||||
|
||||
if (0 === count($applications)) {
|
||||
@@ -55,8 +58,8 @@ class InvalidateApplicationsListener
|
||||
'teamer' => $teamer->getUuid(),
|
||||
'teamer_name' => (string) $teamer,
|
||||
'destination' => $assignment->getDestination()->getHotelCode(),
|
||||
'date_from' => $assignment->getEffectiveDateFrom()->format('Y-m-d'),
|
||||
'date_to' => $assignment->getEffectiveDateTo()->format('Y-m-d'),
|
||||
'date_from' => $assignment->getDestination()->getDateFrom()->format('Y-m-d'),
|
||||
'date_to' => $assignment->getDestination()->getDateTo()->format('Y-m-d'),
|
||||
]);
|
||||
$this->entityManager->remove($application);
|
||||
}
|
||||
|
||||
@@ -79,14 +79,6 @@ class AssignmentType extends AbstractType
|
||||
'rows' => 3,
|
||||
],
|
||||
])
|
||||
// ->add('dateFrom', DatepickerType::class, [
|
||||
// 'label' => 'abweichender Einsatztermin von',
|
||||
// 'required' => false,
|
||||
// ])
|
||||
// ->add('dateTo', DatepickerType::class, [
|
||||
// 'label' => 'abweichender Einsatztermin bis',
|
||||
// 'required' => false,
|
||||
// ])
|
||||
// ->add('pickupDate', DatepickerType::class, [
|
||||
// 'label' => 'abweichender Zustieg bei Busbegleitung',
|
||||
// 'required' => false,
|
||||
|
||||
@@ -163,12 +163,10 @@ class ApplicationRepository extends ServiceEntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all applications of a teamer whose effective assignment period overlaps the given period.
|
||||
* Finds all applications of a teamer whose assignment period overlaps the given period.
|
||||
*
|
||||
* The effective period of an assignment is its own date range, falling back to the date range of
|
||||
* its destination whenever the assignment does not override it - the query mirrors
|
||||
* {@see Assignment::getEffectivePeriod()}. Boundaries count as an overlap: a teamer cannot end one
|
||||
* assignment on the same day another one begins.
|
||||
* The assignment period is the date range of its destination. Boundaries count as an
|
||||
* overlap: a teamer cannot end one assignment on the same day another one begins.
|
||||
*
|
||||
* @return array<Application>
|
||||
*/
|
||||
@@ -193,26 +191,8 @@ class ApplicationRepository extends ServiceEntityRepository
|
||||
$qb->expr()->neq('assignment.status', ':assignment_called_off'),
|
||||
$qb->expr()->isNull('assignment.deletedAt'),
|
||||
$qb->expr()->isNull('destination.deletedAt'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateFrom'),
|
||||
$qb->expr()->lte('destination.dateFrom', ':dateTo')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateFrom'),
|
||||
$qb->expr()->lte('assignment.dateFrom', ':dateTo')
|
||||
)
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->lte('destination.dateFrom', ':dateTo'),
|
||||
$qb->expr()->gte('destination.dateTo', ':dateFrom')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->gte('assignment.dateTo', ':dateFrom')
|
||||
)
|
||||
)
|
||||
))
|
||||
->setParameter('teamer', $teamer)
|
||||
->setParameter('application_rejected', Application::STATUS_REJECTED)
|
||||
|
||||
@@ -73,13 +73,7 @@ class AssignmentRepository extends ServiceEntityRepository
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->innerJoin('assignment.jobProfile', 'job_profile')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gt('destination.dateFrom', ':now'),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateFrom'),
|
||||
$qb->expr()->gt('assignment.dateFrom', ':now')
|
||||
)
|
||||
),
|
||||
$qb->expr()->isNull('assignment.deletedAt'),
|
||||
$qb->expr()->notIn('assignment.status', ':status'),
|
||||
$qb->expr()->in('assignment.id', $availableAssignmentsIds)
|
||||
@@ -399,13 +393,7 @@ class AssignmentRepository extends ServiceEntityRepository
|
||||
)
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gt('destination.dateFrom', ':now'),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateFrom'),
|
||||
$qb->expr()->gt('assignment.dateFrom', ':now')
|
||||
)
|
||||
),
|
||||
$qb->expr()->notIn('assignment.status', ':status'),
|
||||
$qb->expr()->isNull('assignment.deletedAt')
|
||||
))
|
||||
|
||||
@@ -11,7 +11,6 @@ use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Comparison;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Doctrine\ORM\Query\Expr\Orx;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
@@ -43,16 +42,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
$qb->expr()->eq('disposition.teamer', ':teamer'),
|
||||
$qb->expr()->neq('assignment.status', ':assignmentStatus'),
|
||||
$qb->expr()->neq('disposition.status', ':dispositionStatus'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->gte('destination.dateTo', ':date')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->gte('assignment.dateTo', ':date')
|
||||
)
|
||||
)
|
||||
))
|
||||
->setParameter('teamer', $teamer)
|
||||
->setParameter('assignmentStatus', Assignment::STATUS_CALLED_OFF)
|
||||
@@ -85,16 +75,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
$qb->expr()->eq('disposition.teamer', ':teamer'),
|
||||
$qb->expr()->neq('assignment.status', ':assignmentStatus'),
|
||||
$qb->expr()->neq('disposition.status', ':dispositionStatus'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->lte('destination.dateTo', ':date')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->lte('assignment.dateTo', ':date')
|
||||
)
|
||||
)
|
||||
))
|
||||
->setParameter('teamer', $teamer)
|
||||
->setParameter('assignmentStatus', Assignment::STATUS_CALLED_OFF)
|
||||
@@ -145,12 +126,12 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* The effective start date of every non-called-off disposition of a teamer, one row
|
||||
* each, for the per-season tally in the info modal.
|
||||
* The start date of every non-called-off disposition of a teamer, one row each, for the
|
||||
* per-season tally in the info modal.
|
||||
*
|
||||
* Same teamer/called-off filtering as findRecentDispositionsByTeamer(), but unbounded
|
||||
* and stripped to the one date it needs: assignment.dateFrom falling back to the
|
||||
* destination's, the season convention shared with SeasonPeriodFilter.
|
||||
* and stripped to the one date it needs: the destination's start, the season convention
|
||||
* shared with SeasonPeriodFilter.
|
||||
*
|
||||
* @return array<int, array{seasonDate: string}>
|
||||
*/
|
||||
@@ -159,7 +140,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
$qb = $this->createQueryBuilder('disposition');
|
||||
|
||||
return $qb
|
||||
->select('COALESCE(assignment.dateFrom, destination.dateFrom) AS seasonDate')
|
||||
->select('destination.dateFrom AS seasonDate')
|
||||
->innerJoin('disposition.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->where($qb->expr()->andX(
|
||||
@@ -242,13 +223,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
->leftJoin('disposition.feedback', 'feedback')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->notIn('assignment.status', ':status'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->lte('assignment.dateTo', ':dateTo')
|
||||
),
|
||||
$qb->expr()->lte('destination.dateTo', ':dateTo')
|
||||
),
|
||||
$qb->expr()->lte('destination.dateTo', ':dateTo'),
|
||||
$qb->expr()->isNull('feedback'),
|
||||
$this->excludesSkipFormalities($qb)
|
||||
))
|
||||
@@ -302,13 +277,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->in('disposition.status', ':status'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->lte('assignment.dateTo', ':dateTo')
|
||||
),
|
||||
$qb->expr()->lte('destination.dateTo', ':dateTo')
|
||||
)
|
||||
))
|
||||
->setParameter('status', [
|
||||
Disposition::STATUS_NEW,
|
||||
@@ -529,10 +498,10 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
$normalDay = $qb->expr()->andX(
|
||||
$qb->expr()->gte('disposition.createdAt', ':dayStart'),
|
||||
$qb->expr()->lt('disposition.createdAt', ':dayEnd'),
|
||||
$this->effectiveDateToGreaterThan($qb, ':tomorrow'),
|
||||
$qb->expr()->gt('destination.dateTo', ':tomorrow'),
|
||||
);
|
||||
$shortNoticeDay = $qb->expr()->andX(
|
||||
$this->effectiveDateToEquals($qb, ':tomorrow'),
|
||||
$qb->expr()->eq('destination.dateTo', ':tomorrow'),
|
||||
$qb->expr()->gte('disposition.createdAt', ':dayStart'),
|
||||
);
|
||||
|
||||
@@ -599,7 +568,7 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
// in CronCommand, so a skip-formalities placement is still `confirmed` here and
|
||||
// would slip past the status filter on the day after the assignment ends.
|
||||
$this->excludesSkipFormalities($qb),
|
||||
$this->effectiveDateToEquals($qb, ':endDate'),
|
||||
$qb->expr()->eq('destination.dateTo', ':endDate'),
|
||||
))
|
||||
->setParameter('documentType', Upload::TYPE_INVOICE)
|
||||
->setParameter('dispositionStatus', [Disposition::STATUS_CALLED_OFF, Disposition::STATUS_COMPLETED])
|
||||
@@ -611,30 +580,6 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the assignment's end date against $parameter, falling back to the destination's
|
||||
* when the assignment does not override it.
|
||||
*
|
||||
* The fallback is guarded on both sides - without the isNull() on the second branch an
|
||||
* assignment that moves the end date into the future would still match on the
|
||||
* destination's date. Kept in one place because the per-assignment date override is due to
|
||||
* be removed (docs/remove-assignment-date-override.md), and this then collapses to a plain
|
||||
* comparison on destination.dateTo.
|
||||
*/
|
||||
private function effectiveDateToEquals(QueryBuilder $qb, string $parameter): Orx
|
||||
{
|
||||
return $qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->eq('assignment.dateTo', $parameter)
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->eq('destination.dateTo', $parameter)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restricts a query to assignments that still run the contract, invoice and feedback process.
|
||||
*
|
||||
@@ -653,23 +598,6 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
return $qb->expr()->eq('assignment.skipFormalities', ':skipFormalities');
|
||||
}
|
||||
|
||||
/**
|
||||
* Same fallback as effectiveDateToEquals(), but for "later than $parameter".
|
||||
*/
|
||||
private function effectiveDateToGreaterThan(QueryBuilder $qb, string $parameter): Orx
|
||||
{
|
||||
return $qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->gt('assignment.dateTo', $parameter)
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->gt('destination.dateTo', $parameter)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispositions whose contract upload period has run out, for the admin dashboard.
|
||||
*
|
||||
|
||||
@@ -5,15 +5,14 @@ namespace App\Repository\Filter;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* Narrows a query to the season an assignment runs in.
|
||||
* Narrows a query to the season an assignment runs in, which is the date range of its
|
||||
* destination.
|
||||
*
|
||||
* The season is assignment.dateFrom/dateTo falling back to the destination's, mirroring
|
||||
* Assignment::getEffectivePeriod(), which DQL cannot call. Every statistics screen means
|
||||
* this by a date range, so it lives in one place: three repositories carrying the same
|
||||
* expression by hand is three chances for "a season" to come to mean something slightly
|
||||
* different on one screen than on the one beside it.
|
||||
* Kept in one place because every statistics screen means the same thing by "a season":
|
||||
* the expression carried by hand in each repository is one more chance for it to come to
|
||||
* mean something slightly different on one screen than on the one beside it.
|
||||
*
|
||||
* Both aliases must already be joined by the caller - this only adds the conditions.
|
||||
* The destination alias must already be joined by the caller - this only adds the conditions.
|
||||
*/
|
||||
final class SeasonPeriodFilter
|
||||
{
|
||||
@@ -21,37 +20,18 @@ final class SeasonPeriodFilter
|
||||
QueryBuilder $qb,
|
||||
?\DateTimeImmutable $dateFrom,
|
||||
?\DateTimeImmutable $dateTo,
|
||||
string $assignmentAlias = 'assignment',
|
||||
string $destinationAlias = 'destination',
|
||||
): void {
|
||||
if (null !== $dateFrom) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull($assignmentAlias.'.dateFrom'),
|
||||
$qb->expr()->gte($assignmentAlias.'.dateFrom', ':dateFrom')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull($assignmentAlias.'.dateFrom'),
|
||||
$qb->expr()->gte($destinationAlias.'.dateFrom', ':dateFrom')
|
||||
)
|
||||
))
|
||||
->andWhere($qb->expr()->gte($destinationAlias.'.dateFrom', ':dateFrom'))
|
||||
->setParameter('dateFrom', $dateFrom)
|
||||
;
|
||||
}
|
||||
|
||||
if (null !== $dateTo) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull($assignmentAlias.'.dateTo'),
|
||||
$qb->expr()->lte($assignmentAlias.'.dateTo', ':dateTo')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull($assignmentAlias.'.dateTo'),
|
||||
$qb->expr()->lte($destinationAlias.'.dateTo', ':dateTo')
|
||||
)
|
||||
))
|
||||
->andWhere($qb->expr()->lte($destinationAlias.'.dateTo', ':dateTo'))
|
||||
->setParameter('dateTo', $dateTo)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ class IcsGenerator
|
||||
$label = sprintf('E&P: Einteilung als %s', $assignment->getJobProfile()->getName());
|
||||
$address = sprintf("%s\n%s", $assignment->getDestination()->getProduct(), $assignment->getDestination()->getHotel());
|
||||
|
||||
$destination = $assignment->getDestination();
|
||||
|
||||
$event = Event::create($label)
|
||||
->startsAt($assignment->getEffectivePeriod()->start)
|
||||
->endsAt($assignment->getEffectivePeriod()->end->modify('+1 day'))
|
||||
->startsAt($destination->getDateFrom())
|
||||
->endsAt($destination->getDateTo()->modify('+1 day'))
|
||||
->address($address)
|
||||
->description($label)
|
||||
;
|
||||
|
||||
@@ -39,8 +39,8 @@ class ContractRenderer extends AbstractPdfRenderer
|
||||
$pdf->Text(21, 104.5, $pdf->encodeString($assignment->getJobProfile()->getName()));
|
||||
|
||||
// Period
|
||||
$dateFrom = $assignment->getEffectivePeriod()->start->format('d.m.Y');
|
||||
$dateTo = $assignment->getEffectivePeriod()->end->format('d.m.Y');
|
||||
$dateFrom = $destination->getDateFrom()->format('d.m.Y');
|
||||
$dateTo = $destination->getDateTo()->format('d.m.Y');
|
||||
$period = sprintf('%s - %s', $dateFrom, $dateTo);
|
||||
$pdf->Text(57, 113, $period);
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class InvoiceRenderer extends AbstractPdfRenderer
|
||||
|
||||
// Invoice number
|
||||
$pdf->SetFont('Arial', '', 12);
|
||||
$date = $assignment->getEffectivePeriod()->start->format('d/m/Y');
|
||||
$date = $assignment->getDestination()->getDateFrom()->format('d/m/Y');
|
||||
$suffix = $assignment->getId();
|
||||
$number = sprintf('%s-%05d', $date, $suffix);
|
||||
$pdf->Text(85, 116, $number);
|
||||
|
||||
@@ -37,12 +37,12 @@ class ApplicationValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === $period = $application->getAssignment()->getEffectivePeriod()) {
|
||||
if (null === $destination = $application->getAssignment()->getDestination()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$applicationDateFrom = $period->start->toDateTimeImmutable();
|
||||
$applicationDateTo = $period->end->toDateTimeImmutable();
|
||||
$applicationDateFrom = $destination->getDateFrom();
|
||||
$applicationDateTo = $destination->getDateTo();
|
||||
|
||||
$qb = $this
|
||||
->dispositionRepository
|
||||
@@ -62,26 +62,8 @@ class ApplicationValidator extends ConstraintValidator
|
||||
$qb->expr()->neq('assignment.status', ':assignment_called_off'),
|
||||
$qb->expr()->isNull('assignment.deletedAt'),
|
||||
$qb->expr()->isNull('destination.deletedAt'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateFrom'),
|
||||
$qb->expr()->lte('destination.dateFrom', ':application_date_to')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateFrom'),
|
||||
$qb->expr()->lte('assignment.dateFrom', ':application_date_to')
|
||||
)
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNull('assignment.dateTo'),
|
||||
$qb->expr()->lte('destination.dateFrom', ':application_date_to'),
|
||||
$qb->expr()->gte('destination.dateTo', ':application_date_from')
|
||||
),
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->isNotNull('assignment.dateTo'),
|
||||
$qb->expr()->gte('assignment.dateTo', ':application_date_from')
|
||||
)
|
||||
)
|
||||
))
|
||||
->setParameter('teamer', $teamer)
|
||||
->setParameter('disposition_called_off', Disposition::STATUS_CALLED_OFF)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
Einsatztermin
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
{{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }}
|
||||
{{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}
|
||||
</dd>
|
||||
</div>
|
||||
{% if assignment.pickup %}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
Einsatztermin
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||
{{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }}
|
||||
{{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="py-6 first:pt-0 last:pb-0 sm:grid sm:grid-cols-3 sm:gap-4">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<span class="whitespace-nowrap">{{ disposition.teamer.fullName(true) }}</span>
|
||||
{% include '_partials/_teamer_deleted_badge.html.twig' with { 'teamer': disposition.teamer } %}
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
|
||||
{{ assignment.effectivePeriod.start|date('d.m.Y') }} -
|
||||
{{ assignment.effectivePeriod.end|date('d.m.Y') }}
|
||||
{{ assignment.destination.dateFrom|date('d.m.Y') }} -
|
||||
{{ assignment.destination.dateTo|date('d.m.Y') }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
{{ icon('hand', 'w-4 h-4 shrink-0') }}
|
||||
<span class="whitespace-nowrap">{{ application.teamer.fullName(true) }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
@@ -74,7 +74,7 @@
|
||||
{{ icon('download', 'w-4 h-4 shrink-0') }}
|
||||
<span>{{ document.owner.teamer.fullName(true) }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
</button>
|
||||
@@ -107,7 +107,7 @@
|
||||
hx-swap="beforeend">
|
||||
{{ icon('download', 'w-4 h-4 shrink-0') }}
|
||||
<span>{{ document.owner.teamer.fullName(true) }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
</button>
|
||||
@@ -162,7 +162,7 @@
|
||||
<a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}"
|
||||
class="flex items-center space-x-1">
|
||||
{{ icon('calendar', 'w-4 h-4 shrink-0') }}
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.jobProfile.name }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
@@ -286,7 +286,7 @@
|
||||
<a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}"
|
||||
class="flex items-center space-x-1">
|
||||
{{ icon('calendar', 'w-4 h-4 shrink-0') }}
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.jobProfile.name }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
|
||||
@@ -96,9 +96,9 @@
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ path('app_administrative_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
|
||||
{{ assignment.effectivePeriod.start|date('d.m.Y') }} -
|
||||
{{ assignment.destination.dateFrom|date('d.m.Y') }} -
|
||||
<br>
|
||||
{{ assignment.effectivePeriod.end|date('d.m.Y') }}
|
||||
{{ assignment.destination.dateTo|date('d.m.Y') }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
{% for disposition in pagination %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ disposition.assignment.effectivePeriod.start|date('d.m.Y') }} - {{ disposition.assignment.effectivePeriod.end|date('d.m.Y') }}
|
||||
{{ disposition.assignment.destination.dateFrom|date('d.m.Y') }} - {{ disposition.assignment.destination.dateTo|date('d.m.Y') }}
|
||||
</td>
|
||||
<td>
|
||||
{{ disposition.assignment.destination.product }}
|
||||
|
||||
@@ -63,9 +63,9 @@
|
||||
<td>
|
||||
<a href="{{ path('app_administrative_feedback_provide', { 'uuid': disposition.uuid }) }}"
|
||||
title="Feedback abgeben">
|
||||
{{ assignment.effectivePeriod.start|date('d.m.Y') }} -
|
||||
{{ assignment.destination.dateFrom|date('d.m.Y') }} -
|
||||
<br>
|
||||
{{ assignment.effectivePeriod.end|date('d.m.Y') }}
|
||||
{{ assignment.destination.dateTo|date('d.m.Y') }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
{{ icon('info', 'w-4 h-4') }}
|
||||
</button>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
{{ icon('download', 'w-4 h-4 shrink-0') }}
|
||||
<span>{{ document.owner.teamer.fullName(true) }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
</button>
|
||||
@@ -54,7 +54,7 @@
|
||||
hx-swap="beforeend">
|
||||
{{ icon('download', 'w-4 h-4 shrink-0') }}
|
||||
<span>{{ document.owner.teamer.fullName(true) }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
</button>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
{{ icon('calendar', 'w-5 h-5 shrink-0') }}
|
||||
<span class="flex-1">{{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }}</span>
|
||||
<span class="flex-1">{{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
{{ icon('profile', 'w-5 h-5 shrink-0') }}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
{{ icon('calendar', 'w-5 h-5 shrink-0') }}
|
||||
<span class="flex-1">{{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }}</span>
|
||||
<span class="flex-1">{{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
{{ icon('profile', 'w-5 h-5 shrink-0') }}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
{{ icon('calendar', 'w-5 h-5 shrink-0') }}
|
||||
<span class="flex-1">{{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }}</span>
|
||||
<span class="flex-1">{{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
{{ icon('profile', 'w-5 h-5 shrink-0') }}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
{{ icon('calendar', 'w-5 h-5 shrink-0') }}
|
||||
<span class="flex-1">{{ assignment.effectivePeriod.start|date('d.m.Y') }} - {{ assignment.effectivePeriod.end|date('d.m.Y') }}</span>
|
||||
<span class="flex-1">{{ assignment.destination.dateFrom|date('d.m.Y') }} - {{ assignment.destination.dateTo|date('d.m.Y') }}</span>
|
||||
</div>
|
||||
<div class="flex items-start space-x-2">
|
||||
{{ icon('profile', 'w-5 h-5 shrink-0') }}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="flex items-center space-x-1">
|
||||
{{ icon('hand', 'w-4 h-4 shrink-0') }}
|
||||
<div class="flex items-center space-x-1 truncate">
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
</div>
|
||||
@@ -56,7 +56,7 @@
|
||||
<div class="flex items-center space-x-1">
|
||||
{{ icon('alert', 'w-4 h-4 shrink-0') }}
|
||||
<div class="flex items-center space-x-1 truncate">
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
</div>
|
||||
@@ -124,7 +124,7 @@
|
||||
<div class="flex items-center space-x-1">
|
||||
{{ icon('check', 'w-4 h-4 shrink-0') }}
|
||||
<div class="flex items-center space-x-1 truncate">
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
</div>
|
||||
@@ -149,7 +149,7 @@
|
||||
<a href="{{ path('app_teamer_assignment_detail', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
|
||||
<div class="flex items-center space-x-1 truncate">
|
||||
{{ icon('calendar', 'w-4 h-4 shrink-0') }}
|
||||
<span class="whitespace-nowrap">{{ assignment.effectivePeriod.start|date('d.m.y') }} - {{ assignment.effectivePeriod.end|date('d.m.y') }}</span>
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.dateFrom|date('d.m.y') }} - {{ assignment.destination.dateTo|date('d.m.y') }}</span>
|
||||
{% include '_partials/_dot.html.twig' %}
|
||||
<span class="whitespace-nowrap">{{ assignment.destination.hotel }}</span>
|
||||
</div>
|
||||
|
||||
@@ -95,7 +95,7 @@ class InvalidateApplicationsListenerTest extends TestCase
|
||||
$this->assertSame([], $this->removedApplications);
|
||||
}
|
||||
|
||||
public function testIgnoresAssignmentsWithoutEffectivePeriod(): void
|
||||
public function testIgnoresAssignmentsWithoutDestination(): void
|
||||
{
|
||||
$document = $this->createContract(null, $this->createTeamer());
|
||||
|
||||
@@ -125,7 +125,7 @@ class InvalidateApplicationsListenerTest extends TestCase
|
||||
$this->assertSame([], $this->dispatchedEvents);
|
||||
}
|
||||
|
||||
public function testQueriesOverlappingApplicationsForTheTeamerAndTheEffectivePeriod(): void
|
||||
public function testQueriesOverlappingApplicationsForTheTeamerAndTheDestinationPeriod(): void
|
||||
{
|
||||
$teamer = $this->createTeamer();
|
||||
$period = new CarbonPeriodImmutable('2025-01-10', '2025-01-20');
|
||||
@@ -134,7 +134,13 @@ class InvalidateApplicationsListenerTest extends TestCase
|
||||
$this->applicationRepository
|
||||
->expects($this->once())
|
||||
->method('findOverlappingForTeamer')
|
||||
->with($teamer, $period)
|
||||
->with(
|
||||
$this->identicalTo($teamer),
|
||||
$this->callback(static function (CarbonPeriodImmutable $actual) use ($period): bool {
|
||||
return $actual->getStartDate()->equalTo($period->getStartDate())
|
||||
&& $actual->getEndDate()->equalTo($period->getEndDate());
|
||||
})
|
||||
)
|
||||
->willReturn([])
|
||||
;
|
||||
|
||||
@@ -194,7 +200,15 @@ class InvalidateApplicationsListenerTest extends TestCase
|
||||
private function createContract(?CarbonPeriodImmutable $period, Teamer $teamer): Upload&MockObject
|
||||
{
|
||||
$assignment = $this->createMock(Assignment::class);
|
||||
$assignment->method('getEffectivePeriod')->willReturn($period);
|
||||
|
||||
if (null === $period) {
|
||||
$assignment->method('getDestination')->willReturn(null);
|
||||
} else {
|
||||
$destination = $this->createMock(Destination::class);
|
||||
$destination->method('getDateFrom')->willReturn($period->getStartDate()->toDateTimeImmutable());
|
||||
$destination->method('getDateTo')->willReturn($period->getEndDate()->toDateTimeImmutable());
|
||||
$assignment->method('getDestination')->willReturn($destination);
|
||||
}
|
||||
|
||||
$disposition = $this->createMock(Disposition::class);
|
||||
$disposition->method('getAssignment')->willReturn($assignment);
|
||||
@@ -211,12 +225,12 @@ class InvalidateApplicationsListenerTest extends TestCase
|
||||
{
|
||||
$destination = $this->createMock(Destination::class);
|
||||
$destination->method('getHotelCode')->willReturn('TST');
|
||||
$destination->method('getDateFrom')->willReturn(new \DateTimeImmutable('2025-01-12'));
|
||||
$destination->method('getDateTo')->willReturn(new \DateTimeImmutable('2025-01-18'));
|
||||
|
||||
$assignment = $this->createMock(Assignment::class);
|
||||
$assignment->method('getDestination')->willReturn($destination);
|
||||
$assignment->method('getUuid')->willReturn('assignment-uuid');
|
||||
$assignment->method('getEffectiveDateFrom')->willReturn(new \DateTimeImmutable('2025-01-12'));
|
||||
$assignment->method('getEffectiveDateTo')->willReturn(new \DateTimeImmutable('2025-01-18'));
|
||||
|
||||
$application = $this->createMock(Application::class);
|
||||
$application->method('getUuid')->willReturn('application-uuid');
|
||||
|
||||
@@ -20,21 +20,13 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
*/
|
||||
class ApplicationRepositoryTest extends KernelTestCase
|
||||
{
|
||||
public function testOverlappingQueryUsesInclusiveBoundariesAndEffectiveDates(): void
|
||||
public function testOverlappingQueryUsesInclusiveBoundariesOnTheDestinationDates(): void
|
||||
{
|
||||
$dql = $this->createOverlappingQuery()->getDQL();
|
||||
|
||||
// an assignment overrides the date range of its destination, so both have to be considered
|
||||
$this->assertStringContainsString(
|
||||
'(assignment.dateFrom IS NULL AND destination.dateFrom <= :dateTo)'
|
||||
.' OR (assignment.dateFrom IS NOT NULL AND assignment.dateFrom <= :dateTo)',
|
||||
$dql
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'(assignment.dateTo IS NULL AND destination.dateTo >= :dateFrom)'
|
||||
.' OR (assignment.dateTo IS NOT NULL AND assignment.dateTo >= :dateFrom)',
|
||||
$dql
|
||||
);
|
||||
// the assignment period is the date range of its destination
|
||||
$this->assertStringContainsString('destination.dateFrom <= :dateTo', $dql);
|
||||
$this->assertStringContainsString('destination.dateTo >= :dateFrom', $dql);
|
||||
|
||||
// strict comparisons would let assignments that touch on a boundary pass and could never
|
||||
// match a single day assignment at all
|
||||
|
||||
@@ -93,14 +93,8 @@ class DispositionRepositoryTest extends KernelTestCase
|
||||
{
|
||||
$dql = $this->contractQuery()->getDQL();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'assignment.dateTo IS NOT NULL AND assignment.dateTo > :tomorrow',
|
||||
$dql
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'assignment.dateTo IS NOT NULL AND assignment.dateTo = :tomorrow',
|
||||
$dql
|
||||
);
|
||||
$this->assertStringContainsString('destination.dateTo > :tomorrow', $dql);
|
||||
$this->assertStringContainsString('destination.dateTo = :tomorrow', $dql);
|
||||
}
|
||||
|
||||
public function testContractReminderBindsTomorrowAsADateAndNotATimestamp(): void
|
||||
@@ -114,8 +108,8 @@ class DispositionRepositoryTest extends KernelTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* The bug that killed this query: destination.dateTo and assignment.dateTo are DATE columns,
|
||||
* and Doctrine binds a DateTimeImmutable as 'Y-m-d H:i:s', which a DATE never equals.
|
||||
* The bug that killed this query: destination.dateTo is a DATE column, and Doctrine binds a
|
||||
* DateTimeImmutable as 'Y-m-d H:i:s', which a DATE never equals.
|
||||
*/
|
||||
public function testInvoiceReminderBindsADateAndNotATimestamp(): void
|
||||
{
|
||||
@@ -129,21 +123,13 @@ class DispositionRepositoryTest extends KernelTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Without the IS NULL guard on the second branch, an assignment that moves the end date into
|
||||
* the future still matches on its destination's date.
|
||||
* The reminder fires for placements whose destination ends on exactly $endDate.
|
||||
*/
|
||||
public function testInvoiceReminderGuardsTheDestinationFallback(): void
|
||||
public function testInvoiceReminderComparesTheDestinationEndDate(): void
|
||||
{
|
||||
$dql = $this->invoiceQuery()->getDQL();
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'assignment.dateTo IS NOT NULL AND assignment.dateTo = :endDate',
|
||||
$dql
|
||||
);
|
||||
$this->assertStringContainsString(
|
||||
'assignment.dateTo IS NULL AND destination.dateTo = :endDate',
|
||||
$dql
|
||||
);
|
||||
$this->assertStringContainsString('destination.dateTo = :endDate', $dql);
|
||||
}
|
||||
|
||||
public function testInvoiceReminderOnlyJoinsTheInvoiceAndSkipsFinishedDispositions(): void
|
||||
|
||||
@@ -24,25 +24,21 @@ class StatisticsEventRepositoryTest extends KernelTestCase
|
||||
{
|
||||
$dql = $this->query(dateFrom: new \DateTimeImmutable('2026-07-01'))->getDQL();
|
||||
|
||||
$this->assertStringContainsString('assignment.dateFrom >= :dateFrom', $dql);
|
||||
$this->assertStringContainsString('destination.dateFrom >= :dateFrom', $dql);
|
||||
$this->assertStringNotContainsString('event.occurredAt', $dql);
|
||||
}
|
||||
|
||||
/**
|
||||
* The season start is assignment.dateFrom falling back to the destination's, per
|
||||
* Assignment::getEffectivePeriod(). Losing the fallback would silently drop every
|
||||
* assignment that inherits its dates.
|
||||
* The season is the destination's date range, the convention shared with SeasonPeriodFilter.
|
||||
*/
|
||||
public function testTheSeasonFallsBackToTheDestinationDates(): void
|
||||
public function testTheSeasonIsTheDestinationDateRange(): void
|
||||
{
|
||||
$dql = $this->query(
|
||||
dateFrom: new \DateTimeImmutable('2026-07-01'),
|
||||
dateTo: new \DateTimeImmutable('2027-06-30'),
|
||||
)->getDQL();
|
||||
|
||||
$this->assertStringContainsString('assignment.dateFrom IS NULL', $dql);
|
||||
$this->assertStringContainsString('destination.dateFrom >= :dateFrom', $dql);
|
||||
$this->assertStringContainsString('assignment.dateTo IS NULL', $dql);
|
||||
$this->assertStringContainsString('destination.dateTo <= :dateTo', $dql);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ use App\Service\Pdf\InvoiceApprover;
|
||||
use App\Service\Pdf\InvoiceRenderer;
|
||||
use App\Service\Pdf\Pdf;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
use Carbon\CarbonPeriodImmutable;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
@@ -84,7 +83,6 @@ class InvoiceRendererTest extends WebTestCase
|
||||
;
|
||||
|
||||
$assignment = $this->createMock(Assignment::class);
|
||||
$assignment->method('getEffectivePeriod')->willReturn(CarbonPeriodImmutable::create('2025-02-01', '2025-02-08'));
|
||||
$assignment->method('getId')->willReturn(1234);
|
||||
$assignment->method('getDestination')->willReturn($destination);
|
||||
$assignment->method('getJobProfile')->willReturn($jobProfile);
|
||||
|
||||
Reference in New Issue
Block a user