src/Entity/TimeControl/TimeEntry.php line 113

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Entity\TimeControl;
  4. use ApiPlatform\Core\Annotation\{ApiFilterApiPropertyApiResource};
  5. use ApiPlatform\Core\Bridge\Doctrine\Orm\{Filter\DateFilterFilter\ExistsFilterFilter\OrderFilterFilter\SearchFilter};
  6. use App\Dto\{DeveloperTimeOutputTimeEntryApproveInputTimeEntryBreakInputTimeEntryFixInputTimeEntryFixOutput};
  7. use App\Entity\{ProfileProject\ProjectUser};
  8. use App\Interfaces\UuidKeyInterface;
  9. use App\Repository\TimeEntryRepository;
  10. use App\Service\DateTimeHelper;
  11. use App\Traits\TimeEntryTrait;
  12. use App\Traits\TimestampableEntity;
  13. use App\Traits\UuidEntityTrait;
  14. use App\Validator\{TimeEntryBlocked as AssertBlockedTimeEntryLength as AssertTimeEntryLength};
  15. use Doctrine\ORM\Mapping as ORM;
  16. use Symfony\Component\Serializer\Annotation\Groups;
  17. use Symfony\Component\Validator\{Constraints as AssertContext\ExecutionContextInterface};
  18. /**
  19.  * @see \App\EventSubscriber\TimeEntryEventSubscriber
  20.  */
  21. #[ApiResource(
  22.     collectionOperations: [
  23.         'get',
  24.         'post' => [
  25.             'security_post_denormalize' => "is_granted('ENTRY_CREATE', object)",
  26.             'security_message' => 'Only developer himself or Project Manager can create Time entries',
  27.             'validation_groups' => [TimeEntry::VG_CREATE],
  28.         ],
  29.         'developers' => [
  30.             'method' => 'GET',
  31.             'path' => 'time-entries/developers',
  32.             'openapi_context' => ['summary' => 'Get time for developers'],
  33.             'output' => DeveloperTimeOutput::class,
  34.         ],
  35.         'open' => [
  36.             'method' => 'POST',
  37.             'security_post_denormalize' => "is_granted('ENTRY_OPEN', object)",
  38.             'security_message' => 'Unable to open time entry for another user',
  39.             'path' => 'time-entries/open',
  40.             'openapi_context' => ['summary' => 'Open Time entry'],
  41.             'validation_groups' => [TimeEntry::VG_OPEN],
  42.         ],
  43.     ],
  44.     itemOperations: [
  45.         'get',
  46.         'put' => ['security' => "is_granted('ENTRY_MODIFY', object)"],
  47.         'delete' => [
  48.             'security' => "is_granted('ENTRY_DELETE', object)",
  49.             'security_message' => 'No access to Time Entry delete, or TE is Blocked',
  50.         ],
  51.         'patch' => [
  52.             'security' => "is_granted('ENTRY_MODIFY', object)",
  53.             'security_message' => 'No access to Time Entry modification',
  54.         ],
  55.         TimeEntry::ITEM_CLOSE => [
  56.             'method' => 'PATCH',
  57.             'path' => 'time-entries/{id}/close',
  58.             'security' => "is_granted('ENTRY_MODIFY', object)",
  59.             'openapi_context' => ['summary' => 'Closes current Time entry'],
  60.             'validation_groups' => [TimeEntry::VG_OPEN],
  61.         ],
  62.         TimeEntry::ITEM_BREAK => [
  63.             'method' => 'PATCH',
  64.             'path' => 'time-entries/{id}/break',
  65.             'security' => "is_granted('ENTRY_MODIFY', object)",
  66.             'input' => TimeEntryBreakInput::class,
  67.             'openapi_context' => ['summary' => 'Break Time entry'],
  68.         ],
  69.         TimeEntry::ITEM_APPROVED => [
  70.             'method' => 'PATCH',
  71.             'path' => 'time-entries/{id}/approved',
  72.             'security' => "is_granted('ENTRY_APPROVED', object)",
  73.             'input' => TimeEntryApproveInput::class,
  74.             'openapi_context' => ['summary' => 'Approved flag for TimeEntry is set by project manager or higher'],
  75.         ],
  76.         TimeEntry::PATCH_TE_OPERATION => [
  77.             'method' => 'PATCH',
  78.             'path' => 'time-entries/{id}/fix',
  79.             'input' => TimeEntryFixInput::class,
  80.             'output' => TimeEntryFixOutput::class,
  81.             'security' => "is_granted('ROLE_USER_FIXER')",
  82.             'openapi_context' => ['summary' => 'Set fix $ approved for time-entries'],
  83.         ],
  84.     ],
  85.     attributes: ['order' => ['start' => 'ASC']],
  86. )]
  87. #[ApiFilter(DateFilter::class, properties: [
  88.     'start' => DateFilter::EXCLUDE_NULL,
  89.     'end' => DateFilter::INCLUDE_NULL_BEFORE,
  90. ])]
  91. #[ApiFilter(SearchFilter::class, properties: [
  92.     'user.slug' => 'exact',
  93.     'project.slug' => 'exact',
  94. ])]
  95. #[ApiFilter(ExistsFilter::class, properties: ['end'])]
  96. #[ApiFilter(OrderFilter::class, properties: [
  97.     'start' => 'ASC',
  98.     'end' => 'ASC',
  99.     'user.slug' => 'ASC',
  100.     'project.slug' => 'ASC',
  101. ])]
  102. #[AssertBlocked()]
  103. #[AssertTimeEntryLength(minLengthAssertTimeEntryLength::MIN_TIME_LENGTHmaxlengthAssertTimeEntryLength::MAX_TIME_LENGTH)]
  104. #[ORM\Entity(repositoryClassTimeEntryRepository::class)]
  105. #[ORM\Table]
  106. #[ORM\Index(name'idx_time_entry_timer_start'columns: ['timer_start'])]
  107. #[ORM\Index(name'idx_time_entry_timer_end'columns: ['timer_end'])]
  108. #[ORM\Index(name'idx_time_entry_is_fixed'columns: ['is_fixed'])]
  109. class TimeEntry implements UuidKeyInterface\Stringable
  110. {
  111.     use UuidEntityTrait;
  112.     use TimestampableEntity;
  113.     use TimeEntryTrait;
  114.     public const ITEM_CLOSE 'close';
  115.     public const ITEM_BREAK 'break';
  116.     public const ITEM_APPROVED 'approved';
  117.     public const VG_CREATE 'create';
  118.     public const VG_UPDATE 'update';
  119.     public const VG_OPEN 'open';
  120.     public const VG_CLOSE 'close';
  121.     public const PATCH_TE_OPERATION 'patch-te';
  122.     public function __construct()
  123.     {
  124.         $this->createdAt = new \DateTime();
  125.     }
  126.     #[Assert\NotNull(groups: [self::VG_CREATE])]
  127.     #[ApiProperty(description'начало'openapiContext: ['format' => 'date-time'])]
  128.     #[Groups(['TimeEntry:read''TimeEntry:write''Project:read'])]
  129.     #[ORM\Column(type'datetime'nullablefalsename'timer_start')]
  130.     private ?\DateTimeInterface $start null;
  131.     #[ApiProperty(description'конец'openapiContext: ['format' => 'date-time'])]
  132.     #[Groups(['TimeEntry:read''TimeEntry:write''Project:read'])]
  133.     #[Assert\NotNull(groups: [self::VG_CLOSEself::VG_CREATE])]
  134.     #[ORM\Column(type'datetime'nullabletruename'timer_end')]
  135.     private ?\DateTimeInterface $end null;
  136.     #[Assert\Valid]
  137.     #[Groups(['TimeEntry:read'])]
  138.     #[ORM\ManyToOne(targetEntity'App\Entity\User'inversedBy'timeEntries'cascade: ['persist'])]
  139.     private ?User $user null;
  140.     #[Groups(['TimeEntry:read'])]
  141.     #[ORM\ManyToOne(targetEntity'App\Entity\Project\Project'inversedBy'timeEntries'cascade: ['persist'])]
  142.     #[ORM\JoinColumn(nullabletrue)]
  143.     private ?Project $project null;
  144.     #[Groups(['TimeEntry:read''TimeEntry:write''Project:read'])]
  145.     #[ORM\Column(type'text'nullabletrue)]
  146.     private ?string $description null;
  147.     #[Groups(['TimeEntry:read''TimeEntry:write''Project:read'])]
  148.     #[ORM\Column(type'string'nullabletrue)]
  149.     private ?string $title null;
  150.     #[Assert\Url]
  151.     #[Groups(['TimeEntry:read''TimeEntry:write''Project:read'])]
  152.     #[ORM\Column(type'string'nullabletrue)]
  153.     private ?string $link null;
  154.     #[Groups(['TimeEntry:read''TimeEntry:write''Project:read'])]
  155.     #[Assert\Valid(groups: [self::VG_CREATEself::VG_UPDATE])]
  156.     #[Assert\NotNull(groups: [self::VG_CREATEself::VG_UPDATE])]
  157.     #[ORM\ManyToOne(targetEntityTask::class, inversedBy'timeEntries'cascade: ['persist'], fetch'EAGER')]
  158.     private ?Task $task null;
  159.     #[Groups(['TimeEntry:read''TimeEntry:write''Project:read'])]
  160.     #[ApiProperty(description'защищена от изменений?')]
  161.     #[ORM\Column(type'boolean'options: ['default' => false'comment' => 'Change prohibition'])]
  162.     private bool $isFixed false;
  163.     #[ORM\Column(type'datetimetz'nullabletruename'timer_start_absolute')]
  164.     private ?\DateTimeInterface $startAbs null;
  165.     #[ORM\Column(type'datetimetz'nullabletruename'timer_end_absolute')]
  166.     private ?\DateTimeInterface $endAbs null;
  167.     #[Groups(['TimeEntry:read''Project:read'])]
  168.     #[ApiProperty(description'подтверждение'deprecationReason'состояние подтверждения доступно в isApproved')]
  169.     #[ORM\OneToOne(targetEntityTimeEntryValidation::class, mappedBy'timeEntry'fetch'EAGER'cascade: ['persist''remove'])]
  170.     private ?TimeEntryValidation $timeEntryValidation null;
  171.     #[ORM\Column(type'datetime'nullabletrue)]
  172.     private ?\DateTimeInterface $deletedAt null;
  173.     public function __toString(): string
  174.     {
  175.         $from $this->start !== null $this->start->format(\DateTimeInterface::ATOM) : 'Not defined';
  176.         $to $this->end !== null $this->end->format(\DateTimeInterface::ATOM) : 'Not defined';
  177.         $username 'Unknown user';
  178.         $user $this->task?->getUser() ?? $this->user;
  179.         if ($user instanceof User) {
  180.             $username $user->getUsername();
  181.             if (($profile $user->getProfile()) instanceof Profile) {
  182.                 $username $profile->getPerson()->getFullName();
  183.             }
  184.         }
  185.         $taskTitle 'Unknown task';
  186.         if (($task $this->task) instanceof Task) {
  187.             $taskTitle \sprintf('%s – %s'$task->getTitle() ?? 'Unknown task'$task->getProject()?->getTitle() ?? 'Unknown project');
  188.         }
  189.         return \sprintf('%s – %s (%s, %s)'$from$to$taskTitle$username);
  190.     }
  191.     #[Assert\Callback]
  192.     public function validate(ExecutionContextInterface $context): void
  193.     {
  194.         $maxAllowedDate = new \DateTime('now +12hour', new \DateTimeZone('UTC'));
  195.         if ($this->start $maxAllowedDate) {
  196.             $context->buildViolation('Start date {{ start }} cannot be later than {{ now }}')
  197.                 ->setParameter('{{ start }}'$this->startAbs?->format(\DateTimeInterface::ATOM))
  198.                 ->setParameter('{{ now }}'$maxAllowedDate->format(\DateTimeInterface::ATOM))
  199.                 ->atPath('start')
  200.                 ->addViolation();
  201.         }
  202.         if (isset($this->end)) {
  203.             if ($this->end $maxAllowedDate) {
  204.                 $context->buildViolation('End date {{ end }} cannot be later than {{ now }}')
  205.                     ->setParameter('{{ end }}'$this->endAbs?->format(\DateTimeInterface::ATOM))
  206.                     ->setParameter('{{ now }}'$maxAllowedDate->format(\DateTimeInterface::ATOM))
  207.                     ->atPath('end')
  208.                     ->addViolation();
  209.             }
  210.             if ($this->start >= $this->end) {
  211.                 $context->buildViolation('End of period {{ end }} must be later than start {{ start }}')
  212.                     ->setParameter('{{ start }}'$this->startAbs?->format(\DateTimeInterface::ATOM))
  213.                     ->setParameter('{{ end }}'$this->endAbs?->format(\DateTimeInterface::ATOM))
  214.                     ->atPath('start')
  215.                     ->addViolation();
  216.             }
  217.         }
  218.     }
  219.     public function getStart(): ?\DateTimeInterface
  220.     {
  221.         return DateTimeHelper::truncateSeconds($this->start);
  222.     }
  223.     public function setStart(\DateTimeInterface $start): self
  224.     {
  225.         $datetime DateTimeHelper::truncateSeconds($start);
  226.         \is_null($datetime) ? $this->setStartAbs($start) : $this->setStartAbs($datetime);
  227.         $this->start $datetime;
  228.         return $this;
  229.     }
  230.     public function getEnd(): ?\DateTimeInterface
  231.     {
  232.         return DateTimeHelper::truncateSeconds($this->end);
  233.     }
  234.     public function setEnd(\DateTimeInterface $datetime null): self
  235.     {
  236.         $datetime DateTimeHelper::truncateSeconds($datetime);
  237.         $this->setEndAbs($datetime);
  238.         $this->end \is_null($datetime) ? $datetime DateTimeHelper::recreateDateTimeWithTimeZoneUTC($datetime);
  239.         return $this;
  240.     }
  241.     public function getLength(): int
  242.     {
  243.         return $this->getTimeEntryMinutes($this);
  244.     }
  245.     /**
  246.      * длительность в минутах.
  247.      */
  248.     #[Groups(['TimeEntry:read''Project:read'])]
  249.     #[ApiProperty(description'длительность в минутах')]
  250.     public function getDuration(): int
  251.     {
  252.         return $this->getTimeEntryMinutes($this);
  253.     }
  254.     #[Groups(['TimeEntry:read''Project:read'])]
  255.     #[ApiProperty(description'подтвержденная длительность в минутах')]
  256.     public function getTimeMinute(): int
  257.     {
  258.         return $this->getIsApproved() ? $this->getTimeEntryMinutes($this) : 0;
  259.     }
  260.     #[Groups(['TimeEntry:read''Project:read'])]
  261.     #[ApiProperty(description'описание задачи')]
  262.     public function getTaskDescription(): ?string
  263.     {
  264.         return $this->getTask()?->getDescription();
  265.     }
  266.     public function getDescription(): ?string
  267.     {
  268.         return $this->description ?? $this->getTask()?->getDescription();
  269.     }
  270.     public function setDescription(?string $description): self
  271.     {
  272.         $this->description $description;
  273.         return $this;
  274.     }
  275.     public function getTitle(): ?string
  276.     {
  277.         return $this->title ?? $this->getTask()?->getTitle();
  278.     }
  279.     public function setTitle(?string $title): self
  280.     {
  281.         $this->title $title;
  282.         return $this;
  283.     }
  284.     public function getLink(): ?string
  285.     {
  286.         return $this->link ?? $this->getTask()?->getLink();
  287.     }
  288.     public function setLink(?string $link): self
  289.     {
  290.         $this->link $link;
  291.         return $this;
  292.     }
  293.     public function getUser(): ?User
  294.     {
  295.         return $this->getTask()?->getUser() ?? $this->user;
  296.     }
  297.     public function setUser(?User $user): self
  298.     {
  299.         $this->user $user;
  300.         if ($this->task instanceof Task) {
  301.             $this->task->setUser($user);
  302.         }
  303.         return $this;
  304.     }
  305.     public function getProject(): ?Project
  306.     {
  307.         return $this->getTask()?->getProject() ?? $this->project;
  308.     }
  309.     public function setProject(?Project $project): self
  310.     {
  311.         $this->project $project;
  312.         if ($this->task instanceof Task) {
  313.             $this->task->setProject($project);
  314.         }
  315.         return $this;
  316.     }
  317.     public function getProjectName(): string
  318.     {
  319.         return $this->getProject()?->getTitle() ?? 'Unknown';
  320.     }
  321.     public function getTask(): ?Task
  322.     {
  323.         return $this->task;
  324.     }
  325.     public function setTask(?Task $task): TimeEntry
  326.     {
  327.         $this->task $task;
  328.         return $this;
  329.     }
  330.     public function getIsFixed(): bool
  331.     {
  332.         return $this->isFixed;
  333.     }
  334.     public function setIsFixed(bool $isFixed): self
  335.     {
  336.         $this->isFixed $isFixed;
  337.         return $this;
  338.     }
  339.     /**
  340.      * подтверждена?
  341.      */
  342.     #[Groups(['TimeEntry:read''Project:read'])]
  343.     #[ApiProperty(description'подтверждена?')]
  344.     public function getIsApproved(): bool
  345.     {
  346.         return $this->getTimeEntryValidation()?->getIsApproved() ?? false;
  347.     }
  348.     public function getStartAbs(): ?\DateTimeInterface
  349.     {
  350.         return $this->startAbs;
  351.     }
  352.     private function setStartAbs(\DateTimeInterface $start): self
  353.     {
  354.         $this->startAbs = clone $start;
  355.         return $this;
  356.     }
  357.     public function getEndAbs(): ?\DateTimeInterface
  358.     {
  359.         return $this->endAbs;
  360.     }
  361.     private function setEndAbs(?\DateTimeInterface $end): self
  362.     {
  363.         $this->endAbs \is_null($end) ? $end : clone $end;
  364.         return $this;
  365.     }
  366.     public function getTimeEntryValidation(): ?TimeEntryValidation
  367.     {
  368.         return $this->timeEntryValidation;
  369.     }
  370.     public function setTimeEntryValidation(?TimeEntryValidation $timeEntryValidation): self
  371.     {
  372.         // set the owning side of the relation if necessary
  373.         if ($timeEntryValidation && $timeEntryValidation->getTimeEntry() !== $this) {
  374.             $timeEntryValidation->setTimeEntry($this);
  375.         }
  376.         $this->timeEntryValidation $timeEntryValidation;
  377.         return $this;
  378.     }
  379.     public function getDeletedAt(): ?\DateTimeInterface
  380.     {
  381.         return $this->deletedAt;
  382.     }
  383.     public function setDeletedAt(?\DateTimeInterface $deletedAt): void
  384.     {
  385.         $this->deletedAt $deletedAt;
  386.     }
  387. }