diff --git a/assets/styles/_components.css b/assets/styles/_components.css index d5bed8c..e86fd04 100644 --- a/assets/styles/_components.css +++ b/assets/styles/_components.css @@ -4,4 +4,5 @@ @import "components/datatable.css"; @import "components/datepicker.css"; @import "components/toast.css"; -@import "components/tooltip.css"; \ No newline at end of file +@import "components/tooltip.css"; +@import "components/timeline.css"; diff --git a/assets/styles/components/timeline.css b/assets/styles/components/timeline.css new file mode 100644 index 0000000..058b79a --- /dev/null +++ b/assets/styles/components/timeline.css @@ -0,0 +1,4 @@ +.scroll-container { + @apply overflow-scroll; + max-height: calc(100vh - 140px); +} diff --git a/src/Controller/Administrative/Assignment/TimelineController.php b/src/Controller/Administrative/Assignment/TimelineController.php new file mode 100644 index 0000000..3b55fc3 --- /dev/null +++ b/src/Controller/Administrative/Assignment/TimelineController.php @@ -0,0 +1,45 @@ +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, + ]); + } +} diff --git a/src/Entity/Destination.php b/src/Entity/Destination.php index 1dbe37e..6dd3976 100644 --- a/src/Entity/Destination.php +++ b/src/Entity/Destination.php @@ -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; diff --git a/src/Model/TimelineItem.php b/src/Model/TimelineItem.php new file mode 100644 index 0000000..5ca1d82 --- /dev/null +++ b/src/Model/TimelineItem.php @@ -0,0 +1,164 @@ +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; + } +} diff --git a/src/Repository/AssignmentRepository.php b/src/Repository/AssignmentRepository.php index 80d4bfa..7add518 100644 --- a/src/Repository/AssignmentRepository.php +++ b/src/Repository/AssignmentRepository.php @@ -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(); diff --git a/src/Service/Assignment/TimelineService.php b/src/Service/Assignment/TimelineService.php new file mode 100644 index 0000000..c94d446 --- /dev/null +++ b/src/Service/Assignment/TimelineService.php @@ -0,0 +1,80 @@ +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); + } +} diff --git a/templates/administrative/assignment/timeline.html.twig b/templates/administrative/assignment/timeline.html.twig new file mode 100644 index 0000000..950ff65 --- /dev/null +++ b/templates/administrative/assignment/timeline.html.twig @@ -0,0 +1,89 @@ +{% extends 'administrative/layout.html.twig' %} + +{% block title %}Einsatzübersicht{% endblock %} + +{% block content %} +
+ + + + {% for day in period %} + + {% endfor %} + + {% for hotelCode, rows in data %} + + + {% for day in period %} + {% if colspan is defined and colspan > 1 %} + {% set colspan = colspan - 1 %} + {% elseif rows[0][day|date('Ymd')] is defined %} + {% set item = rows[0][day|date('Ymd')] %} + {% set colspan = item.numberOfDays %} + + {% else %} + + {% endif %} + {% endfor %} + + {% for row in rows[1:] %} + + {% for day in period %} + {% if colspan is defined and colspan > 1 %} + {% set colspan = colspan - 1 %} + {% elseif row[day|date('Ymd')] is defined %} + {% set item = row[day|date('Ymd')] %} + {% set colspan = item.numberOfDays %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} + {% endfor %} +
+ {{ day | date('d.m.Y') }} +
+ {{ hotelCode }} + + {% if item.type == 'disposition' %} + + {{ item.jobProfile }}
{{ item.dateRange }} + {% else %} + {{ item.jobProfile }}
{{ item.dateRange }} + {% endif %} +
+ {% if item.type == 'disposition' %} + + {{ item.jobProfile }}
{{ item.dateRange }} + {% else %} + {{ item.jobProfile }}
{{ item.dateRange }} + {% endif %} +
+
+{% endblock %}