Merge branch 'feature/timeline' into develop

This commit is contained in:
Björn Fromme
2025-03-19 12:47:58 +01:00
8 changed files with 402 additions and 2 deletions
@@ -0,0 +1,45 @@
<?php
namespace App\Controller\Administrative\Assignment;
use App\Model\AssignmentFilterDto;
use App\Repository\AssignmentRepository;
use App\Service\Assignment\TimelineService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class TimelineController extends AbstractController
{
public function __construct(
private readonly AssignmentRepository $assignmentRepository,
private readonly TimelineService $timelineService
) {
}
#[Route('/administrative/timeline', name: 'app_administrative_assignment_timeline')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(): Response
{
$period = $this->timelineService->getActiveWindow();
$filterDto = new AssignmentFilterDto();
$filterDto
->setDateFrom($period->first()->toDateTimeImmutable())
->setDateTo($period->last()->toDateTimeImmutable())
->setIncludePast(true)
;
$assignments = $this
->assignmentRepository
->getListQuery($filterDto, 'destination.dateFrom')
->getResult()
;
$timelineData = $this->timelineService->preprocess($assignments);
return $this->render('administrative/assignment/timeline.html.twig', [
'data' => $timelineData,
'period' => $period,
]);
}
}
+13
View File
@@ -147,6 +147,19 @@ class Destination implements TimestampableEntityInterface, SoftDeletableEntityIn
return $this->hotelCode;
}
public function getHotelCodeNormalized(): ?string
{
if (null === $hotelCode = $this->hotelCode) {
return null;
}
if (str_starts_with($hotelCode, 'SER')) {
return substr($hotelCode, 2, 3);
}
return substr($hotelCode, 0, 3);
}
public function setHotelCode(?string $hotelCode): static
{
$this->hotelCode = $hotelCode;
+164
View File
@@ -0,0 +1,164 @@
<?php
namespace App\Model;
use App\Entity\Assignment;
use App\Entity\Disposition;
class TimelineItem
{
public const TYPE_ASSIGNMENT = 'assignment';
public const TYPE_DISPOSITION = 'disposition';
private ?\DateTimeImmutable $dateFrom = null;
private ?\DateTimeImmutable $dateTo = null;
private ?string $status = null;
private ?string $dateRange = null;
private ?string $jobProfile = null;
private ?string $teamerName = null;
private ?string $teamerUuid = null;
public function __construct(private readonly int $assignmentId, private readonly string $type)
{
}
public static function fromAssignment(Assignment $assignment): static
{
$dateFrom = $assignment->getDestination()->getDateFrom();
$dateTo = $assignment->getDestination()->getDateTo();
return (new static($assignment->getId(), static::TYPE_ASSIGNMENT))
->setDateFrom($dateFrom)
->setDateTo($dateTo)
->setJobProfile($assignment->getJobProfile()->getName())
->setDateRange(sprintf('%s - %s', $dateFrom->format('d.m.Y'), $dateTo->format('d.m.Y')))
->setStatus($assignment->getStatus());
;
}
public static function fromDisposition(Disposition $disposition): static
{
$assignment = $disposition->getAssignment();
$dateFrom = $assignment->getDestination()->getDateFrom();
$dateTo = $assignment->getDestination()->getDateTo();
return (new static($assignment->getId(), static::TYPE_DISPOSITION))
->setDateFrom($dateFrom)
->setDateTo($dateTo)
->setJobProfile($assignment->getJobProfile()->getName())
->setDateRange(sprintf('%s - %s', $dateFrom->format('d.m.Y'), $dateTo->format('d.m.Y')))
->setStatus($disposition->getStatus())
->setTeamerName($disposition->getTeamer()->getFullName())
->setTeamerUuid($disposition->getTeamer()->getUuid())
;
}
public function getAssignmentId(): int
{
return $this->assignmentId;
}
public function getType(): ?string
{
return $this->type;
}
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;
}
public function getDateRange(): ?string
{
return $this->dateRange;
}
public function setDateRange(?string $dateRange): static
{
$this->dateRange = $dateRange;
return $this;
}
public function getJobProfile(): ?string
{
return $this->jobProfile;
}
public function setJobProfile(?string $jobProfile): static
{
$this->jobProfile = $jobProfile;
return $this;
}
public function getStatus(): ?string
{
return $this->status;
}
public function setStatus(?string $status): static
{
$this->status = $status;
return $this;
}
public function getTeamerName(): ?string
{
return $this->teamerName;
}
public function setTeamerName(?string $teamerName): static
{
$this->teamerName = $teamerName;
return $this;
}
public function getTeamerUuid(): ?string
{
return $this->teamerUuid;
}
public function setTeamerUuid(?string $teamerUuid): static
{
$this->teamerUuid = $teamerUuid;
return $this;
}
public function getKey(): ?string
{
return $this->getDateFrom()?->format('Ymd');
}
public function getNumberOfDays(): int
{
if (null === $this->getDateFrom() || null === $this->getDateTo()) {
return 1;
}
return $this->getDateFrom()->diff($this->getDateTo())->days;
}
}
+5 -1
View File
@@ -27,7 +27,7 @@ class AssignmentRepository extends ServiceEntityRepository
parent::__construct($registry, Assignment::class);
}
public function getListQuery(AssignmentFilterDto $filterDto): Query
public function getListQuery(AssignmentFilterDto $filterDto, ?string $orderBy = null): Query
{
$qb = $this->createQueryBuilder('assignment');
@@ -46,6 +46,10 @@ class AssignmentRepository extends ServiceEntityRepository
->setParameter('applicationStatus', Application::STATUS_REJECTED)
;
if (null !== $orderBy) {
$qb->orderBy($qb->expr()->asc($orderBy));
}
$this->applyFilterSettings($filterDto, $qb);
return $qb->getQuery();
@@ -0,0 +1,80 @@
<?php
namespace App\Service\Assignment;
use App\Entity\Assignment;
use App\Model\TimelineItem;
use Carbon\CarbonPeriod;
class TimelineService
{
/**
* @param Assignment[] $assignments
* @return array
*/
public function preprocess(array $assignments): array
{
$rows = [];
foreach ($assignments as $assignment) {
$destination = $assignment->getDestination();
$hotelCode = $destination->getHotelCodeNormalized();
// add first row for current hotel unless existing
if (false === isset($rows[$hotelCode])) {
$rows[$hotelCode] = [[]];
}
if (0 === $assignment->getDispositions()->count()) {
$item = TimelineItem::fromAssignment($assignment);
$this->addItem($rows[$hotelCode], $item);
} else {
foreach ($assignment->getDispositions() as $disposition) {
$item = TimelineItem::fromDisposition($disposition);
$this->addItem($rows[$hotelCode], $item);
}
}
}
return $rows;
}
private function addItem(array &$rows, TimelineItem $item): void
{
$itemAdded = false;
foreach ($rows as $index => $row) {
$addToRow = true;
foreach ($row as $entry) {
/** @var TimelineItem $entry */
if (
$item->getDateFrom() >= $entry->getDateFrom() && $item->getDateFrom() < $entry->getDateTo()
|| $item->getDateTo() >= $entry->getDateFrom() && $item->getDateTo() < $entry->getDateTo()
) {
$addToRow = false;
}
}
if (true === $addToRow) {
$rows[$index][$item->getKey()] = $item;
$itemAdded = true;
break;
}
}
if (false === $itemAdded) {
$rows[] = [
$item->getKey() => $item,
];
}
}
public function getActiveWindow(): CarbonPeriod
{
$dateFrom = (new \DateTimeImmutable())->modify('-6 weeks');
$dateTo = (new \DateTimeImmutable())->modify('+6 months');
return CarbonPeriod::create($dateFrom, $dateTo);
}
}