<?php declare(strict_types=1);
namespace App\Entity\TimeControl;
use ApiPlatform\Core\Annotation\{ApiFilter, ApiResource, ApiSubresource};
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\{BooleanFilter, DateFilter, OrderFilter, SearchFilter};
use App\Controller\TasksDownloadController;
use App\Dto\TaskOutput;
use App\Dto\TimeEntryFixInput;
use App\Dto\TimeEntryFixOutput;
use App\Entity\{Project\Project, User};
use App\Filter\OrLikeSearchFilter;
use App\Interfaces\UuidKeyInterface;
use App\Repository\TaskRepository;
use App\Traits\UuidEntityTrait;
use Doctrine\Common\Collections\{ArrayCollection, Collection};
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
#[ApiResource(
collectionOperations: [
'get',
self::FILTERED_OPERATION => [
'method' => 'GET',
'output' => TaskOutput::class,
'path' => '/tasks/filtered',
'pagination_client_enabled' => true,
],
'post',
'report' => [
'method' => 'GET',
'path' => 'tasks/download',
'controller' => TasksDownloadController::class,
'openapi_context' => [
'summary' => 'Report by tasks',
'description' => 'Download report by tasks',
],
],
],
itemOperations: [
'get',
'put',
'delete' => [
'security' => "is_granted('TASK_REMOVE', object)",
'security_message' => 'Deny delete Task, because exists child fixed TimeEntries',
],
'patch',
Task::PATCH_TE_OPERATION => [
'method' => 'PATCH',
'path' => 'task/{id}/time-entries',
'input' => TimeEntryFixInput::class,
'output' => TimeEntryFixOutput::class,
'security' => "is_granted('ROLE_USER_FIXER')",
'openapi_context' => ['summary' => 'Set fix $ approved for time-entries'],
],
],
attributes: ['order' => ['createdAt' => 'DESC']],
)]
#[ApiFilter(filterClass: SearchFilter::class, properties: [
'user.slug' => SearchFilter::STRATEGY_EXACT,
'project.slug' => SearchFilter::STRATEGY_EXACT,
'user.id' => SearchFilter::STRATEGY_EXACT,
'project.id' => SearchFilter::STRATEGY_EXACT,
'title' => 'i' . SearchFilter::STRATEGY_PARTIAL,
'description' => 'i' . SearchFilter::STRATEGY_PARTIAL,
'link' => 'i' . SearchFilter::STRATEGY_PARTIAL,
])]
#[ApiFilter(filterClass: OrderFilter::class, properties: [
'createdAt' => [
'nulls_comparison' => OrderFilter::NULLS_SMALLEST,
'default_direction' => 'DESC',
],
'user.email' => 'ASC',
'project.slug' => 'ASC',
'project.title' => 'ASC',
'description' => 'ASC',
'title' => 'ASC',
'link' => 'ASC',
])]
#[ApiFilter(filterClass: DateFilter::class, properties: [
'timeEntries.start' => DateFilter::EXCLUDE_NULL,
'timeEntries.end' => DateFilter::EXCLUDE_NULL,
])]
#[ApiFilter(filterClass: BooleanFilter::class, properties: [
'favorite',
])]
#[ApiFilter(OrLikeSearchFilter::class, properties: ['strategy' => 'partial',
'fields' => [
'title', 'description',
],
])]
#[UniqueEntity(
fields: ['project', 'title', 'user'],
message: 'Task with same title already exists in project "{{ value }}"',
)]
#[ORM\Entity(repositoryClass: TaskRepository::class)]
#[ORM\Table]
#[ORM\Index(name: 'idx_task_favorite', columns: ['favorite'])]
#[ORM\Index(name: 'idx_task_is_commercial', columns: ['is_commercial'])]
class Task implements UuidKeyInterface
{
use UuidEntityTrait;
public const FILTERED_OPERATION = 'filtered';
public const PATCH_TE_OPERATION = 'patch-te';
#[Groups(['Task:read', 'Task:write'])]
#[Assert\NotBlank]
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'tasks', cascade: ['persist'])]
private ?User $user = null;
#[Groups(['Task:read', 'Task:write'])]
#[Assert\NotBlank]
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'tasks', cascade: ['persist'])]
private ?Project $project = null;
/**
* @var Collection<int, TimeEntry>
*/
#[Groups(['Task:read', 'Task:write'])]
#[ApiSubresource]
#[ORM\OneToMany(targetEntity: TimeEntry::class, mappedBy: 'task', cascade: ['remove'])]
private Collection $timeEntries;
#[Assert\Length(max: 255)]
#[Groups(['Task:read', 'Task:write'])]
#[ORM\Column(type: 'text', nullable: true)]
private ?string $description = null;
#[Assert\NotBlank]
#[Assert\Length(max: 255)]
#[Groups(['Task:read', 'Task:write'])]
#[ORM\Column(type: 'string', nullable: true)]
private ?string $title = null;
#[Assert\Length(max: 255)]
#[Groups(['Task:read', 'Task:write'])]
#[Assert\Url]
#[ORM\Column(type: 'string', nullable: true)]
private ?string $link = null;
#[Gedmo\Timestampable(on: 'create')]
#[Groups(['Task:read', 'Task:write'])]
#[ORM\Column(type: 'datetime', nullable: true)]
private ?\DateTimeInterface $createdAt = null;
#[ORM\Column(type: 'datetime', nullable: true)]
private ?\DateTimeInterface $deletedAt = null;
#[Groups(['Task:read', 'Task:write'])]
#[ORM\Column(type: 'boolean', nullable: false, options: ['default' => false])]
private bool $favorite = false;
#[Groups(['Task:read', 'Task:write'])]
#[ORM\Column(type: 'boolean', nullable: false, options: ['default' => true])]
private bool $isCommercial = true;
public function __construct()
{
$this->timeEntries = new ArrayCollection();
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setProject(?Project $project): self
{
$this->project = $project;
return $this;
}
public function getTimeEntries(): Collection
{
return $this->timeEntries;
}
public function addTimeEntry(TimeEntry $timeEntry): self
{
if (!$this->timeEntries->contains($timeEntry)) {
$this->timeEntries[] = $timeEntry;
$timeEntry->setTask($this);
}
return $this;
}
public function removeTimeEntry(TimeEntry $timeEntry): self
{
if ($this->timeEntries->contains($timeEntry)) {
$this->timeEntries->removeElement($timeEntry);
$timeEntry->setTask(null);
}
return $this;
}
public function getProject(): ?Project
{
return $this->project;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(?string $title): self
{
$this->title = $title;
return $this;
}
public function getLink(): ?string
{
return $this->link;
}
public function setLink(?string $link): self
{
$this->link = $link;
return $this;
}
#[Groups(['Task:read', 'Task:write'])]
public function getTime(): int
{
$result = 0;
foreach ($this->timeEntries as $timeEntry) {
if (!$timeEntry instanceof TimeEntry) {
continue;
}
$result += $timeEntry->getLength();
}
return $result;
}
public function getLimitedTime(\DateTimeInterface $start, \DateTimeInterface $end): int
{
$result = 0;
$limited = $this->timeEntries->filter(static function (TimeEntry $te) use ($start, $end) {
return $te->getStart() >= $start && $te->getEnd() <= $end;
});
foreach ($limited as $item) {
if ($item instanceof TimeEntry) {
$result += $item->getLength();
}
}
return $result;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(?\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function isFavorite(): bool
{
return $this->favorite;
}
public function setFavorite(bool $favorite): self
{
$this->favorite = $favorite;
return $this;
}
public function getDeletedAt(): ?\DateTimeInterface
{
return $this->deletedAt;
}
public function setDeletedAt(?\DateTimeInterface $deletedAt): void
{
$this->deletedAt = $deletedAt;
}
public function getIsCommercial(): ?bool
{
return $this->isCommercial;
}
public function setIsCommercial(bool $isCommercial): self
{
$this->isCommercial = $isCommercial;
return $this;
}
}