From 7aef7676195dbaee6d7d49649721d8c088f372a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Thu, 17 Jul 2025 11:37:11 +0200 Subject: [PATCH] chore: add documentation --- .../DispositionWorkflowGuardSubscriber.php | 43 +++++++++++++++++++ src/Service/Assignment/TimelineService.php | 34 ++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/EventListener/DispositionWorkflowGuardSubscriber.php b/src/EventListener/DispositionWorkflowGuardSubscriber.php index 65fe97d..c942004 100644 --- a/src/EventListener/DispositionWorkflowGuardSubscriber.php +++ b/src/EventListener/DispositionWorkflowGuardSubscriber.php @@ -8,11 +8,22 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Workflow\Event\GuardEvent; use Symfony\Contracts\Translation\TranslatorInterface; +/** + * Workflow guard subscriber for disposition document lifecycle management. + * Enforces business rules and timing constraints for contract and invoice workflows, + * ensuring documents are processed in the correct order and within valid time windows. + */ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface { public function __construct(private readonly TranslatorInterface $translator) {} + /** + * Registers guard events for disposition workflow transitions. + * Guards control when workflow transitions can occur based on business rules. + * + * @return array Event name to method mappings + */ public static function getSubscribedEvents(): array { return [ @@ -23,12 +34,19 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface ]; } + /** + * Guards the contract confirmation transition. + * Blocks transition if no contract document exists or if it hasn't been checked by admin. + * + * @param GuardEvent $event The workflow guard event + */ public function guardConfirmContract(GuardEvent $event): void { /** @var Disposition $disposition */ $disposition = $event->getSubject(); $contractDocument = $disposition->getDocumentByType(Upload::TYPE_CONTRACT); + // Contract must exist and be in checked status to allow confirmation if (null === $contractDocument || Upload::STATUS_CHECKED !== $contractDocument->getStatus()) { $message = $this ->translator @@ -38,6 +56,13 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface } } + /** + * Guards the contract upload transition. + * Blocks upload if the deadline has passed (day before assignment starts). + * Contracts must be uploaded within 7 days of disposition creation until assignment start. + * + * @param GuardEvent $event The workflow guard event + */ public function guardUploadContract(GuardEvent $event): void { /** @var Disposition $disposition */ @@ -49,6 +74,7 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface $dueDateFrom = $disposition->getCreatedAt()->modify('+ 7days'); $dueDateTo = $assignmentPeriod->end->modify('-1 day'); + // Block upload if deadline has passed if ($dueDateTo < $today) { $message = $this ->translator @@ -61,6 +87,14 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface } } + /** + * Guards the invoice upload transition. + * Enforces two requirements: + * 1. Contract must exist and be checked before invoice upload + * 2. Invoice can only be uploaded after assignment end date + * + * @param GuardEvent $event The workflow guard event + */ public function guardUploadInvoice(GuardEvent $event): void { /** @var Disposition $disposition */ @@ -71,6 +105,7 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface $contractDocument = $disposition->getDocumentByType(Upload::TYPE_CONTRACT); + // Contract must be checked before invoice upload if (null === $contractDocument || Upload::STATUS_CHECKED !== $contractDocument->getStatus()) { $message = $this ->translator @@ -81,6 +116,7 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface $earliestDate = $destination->getDateTo(); + // Invoice can only be uploaded after assignment ends if ($earliestDate > $today) { $message = $this ->translator @@ -92,12 +128,19 @@ class DispositionWorkflowGuardSubscriber implements EventSubscriberInterface } } + /** + * Guards the invoice confirmation transition. + * Blocks transition if no invoice document exists or if it hasn't been marked as paid. + * + * @param GuardEvent $event The workflow guard event + */ public function guardConfirmInvoice(GuardEvent $event): void { /** @var Disposition $disposition */ $disposition = $event->getSubject(); $invoiceDocument = $disposition->getDocumentByType(Upload::TYPE_INVOICE); + // Invoice must exist and be in paid status to allow confirmation if (null === $invoiceDocument || Upload::STATUS_PAID !== $invoiceDocument->getStatus()) { $message = $this ->translator diff --git a/src/Service/Assignment/TimelineService.php b/src/Service/Assignment/TimelineService.php index 3742a33..d23d0ec 100644 --- a/src/Service/Assignment/TimelineService.php +++ b/src/Service/Assignment/TimelineService.php @@ -7,11 +7,20 @@ use App\Model\TimelineItem; use Carbon\CarbonPeriod; use function Symfony\Component\String\b; +/** + * Service for processing assignments into timeline visualization data. + * Organizes assignments and dispositions by hotel in a timeline format, + * handling overlapping dates by distributing items across multiple rows. + */ class TimelineService { /** - * @param Assignment[] $assignments - * @return array + * Preprocesses assignments into a timeline structure organized by hotel. + * Creates timeline items for both vacant assignment slots and actual dispositions, + * distributing them across multiple rows to avoid date overlaps. + * + * @param Assignment[] $assignments Array of assignments to process + * @return array Multi-dimensional array: [hotelCode][rowIndex][itemKey] => TimelineItem */ public function preprocess(array $assignments): array { @@ -34,18 +43,21 @@ class TimelineService $dispositionsCount = $assignment->getDispositions()->count(); $dispositionsAvailable = $assignment->getAvailableDispositions(); + // Handle assignments with no dispositions - create items for all available slots if (0 === $dispositionsCount) { $item = TimelineItem::fromAssignment($assignment); for ($i = 1; $i <= $assignment->getAvailableDispositions(); $i++) { $this->addItem($rows[$hotelCode], $item); } } else { + // Create items for remaining vacant slots if any if ($dispositionsCount < $dispositionsAvailable) { $item = TimelineItem::fromAssignment($assignment); for ($i = 1; $i <= $dispositionsAvailable - $dispositionsCount; $i++) { $this->addItem($rows[$hotelCode], $item); } } + // Create items for actual dispositions foreach ($assignment->getDispositions() as $disposition) { $item = TimelineItem::fromDisposition($disposition); $this->addItem($rows[$hotelCode], $item); @@ -56,15 +68,25 @@ class TimelineService return $rows; } + /** + * Adds a timeline item to the appropriate row, ensuring no date overlaps. + * Finds the first available row where the item doesn't overlap with existing items, + * or creates a new row if necessary. + * + * @param array $rows Reference to the rows array for a specific hotel + * @param TimelineItem $item The timeline item to add + */ private function addItem(array &$rows, TimelineItem $item): void { $rowIndex = 0; $fitsRow = false; + // Find first row where item doesn't overlap with existing items foreach ($rows as $index => $row) { $fitsRow = true; foreach ($row as $entry) { /** @var TimelineItem $entry */ + // Check for date overlap: item overlaps if its end >= entry start AND item start <= entry end if ($entry->getDateTo() >= $item->getDateFrom() && $entry->getDateFrom() <= $item->getDateTo()) { $fitsRow = false; } @@ -75,6 +97,7 @@ class TimelineService } } + // If no existing row fits, create a new row if (false === $fitsRow) { ++$rowIndex; } @@ -82,6 +105,13 @@ class TimelineService $rows[$rowIndex][$item->getKey()] = $item; } + /** + * Creates a time window for timeline visualization. + * Returns a period from 6 weeks ago to 6 months in the future, + * providing context for both recent and upcoming assignments. + * + * @return CarbonPeriod The active time window for timeline display + */ public function getActiveWindow(): CarbonPeriod { $dateFrom = (new \DateTimeImmutable())->modify('-6 weeks');