Merge branch 'feature/timeline' into develop
This commit is contained in:
@@ -4,4 +4,5 @@
|
||||
@import "components/datatable.css";
|
||||
@import "components/datepicker.css";
|
||||
@import "components/toast.css";
|
||||
@import "components/tooltip.css";
|
||||
@import "components/tooltip.css";
|
||||
@import "components/timeline.css";
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.scroll-container {
|
||||
@apply overflow-scroll;
|
||||
max-height: calc(100vh - 140px);
|
||||
}
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
{% extends 'administrative/layout.html.twig' %}
|
||||
|
||||
{% block title %}Einsatzübersicht{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="scroll-container border border-gray-300 mb-16">
|
||||
<table>
|
||||
<tr class="[&_th]:sticky [&_th]:top-0 [&_th]:z-10">
|
||||
<th></th>
|
||||
{% for day in period %}
|
||||
<th>
|
||||
{{ day | date('d.m.Y') }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for hotelCode, rows in data %}
|
||||
<tr class="border-t-2 border-gray-700">
|
||||
<td class="border border-gray-700 bg-gray-200 align-middle font-bold sticky left-0 z-10" rowspan="{{ rows | length }}">
|
||||
{{ hotelCode }}
|
||||
</td>
|
||||
{% 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 %}
|
||||
<td class="{{ html_classes('border border-gray-700', {
|
||||
'bg-orange-300': item.type == 'assignment',
|
||||
'bg-yellow-300': item.type == 'disposition' and item.status != 'confirmed',
|
||||
'bg-green-300': item.type == 'disposition' and item.status == 'confirmed',
|
||||
}) }}" colspan="{{ colspan }}">
|
||||
{% if item.type == 'disposition' %}
|
||||
<button type="button"
|
||||
class="flex items-center space-x-1"
|
||||
title="Teamer:inneninfo {{ item.teamerName }}"
|
||||
hx-get="{{ path('app_administrative_teamer_info', { 'uuid': item.teamerUuid }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
<span>{{ item.teamerName | raw }}</span>
|
||||
{{ icon('info', 'w-4 h-4') }}
|
||||
</button>
|
||||
{{ item.jobProfile }}<br>{{ item.dateRange }}
|
||||
{% else %}
|
||||
{{ item.jobProfile }}<br>{{ item.dateRange }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="border border-gray-700"></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% for row in rows[1:] %}
|
||||
<tr>
|
||||
{% 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 %}
|
||||
<td class="{{ html_classes('border border-gray-700', {
|
||||
'bg-orange-300': item.type == 'assignment',
|
||||
'bg-yellow-300': item.type == 'disposition' and item.status != 'confirmed',
|
||||
'bg-green-300': item.type == 'disposition' and item.status == 'confirmed',
|
||||
}) }}" colspan="{{ colspan }}">
|
||||
{% if item.type == 'disposition' %}
|
||||
<button type="button"
|
||||
class="flex items-center space-x-1"
|
||||
title="Teamer:inneninfo {{ item.teamerName }}"
|
||||
hx-get="{{ path('app_administrative_teamer_info', { 'uuid': item.teamerUuid }) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend">
|
||||
<span>{{ item.teamerName | raw }}</span>
|
||||
{{ icon('info', 'w-4 h-4') }}
|
||||
</button>
|
||||
{{ item.jobProfile }}<br>{{ item.dateRange }}
|
||||
{% else %}
|
||||
{{ item.jobProfile }}<br>{{ item.dateRange }}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="border border-gray-700"></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user