<?php
namespace App\Containers\CrmSection\GeneratedLinkContainer\Entities;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\NumericFilter;
use ApiPlatform\Doctrine\Orm\Filter\RangeFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\DBAL\Types\Types;
use Gedmo\Mapping\Annotation as Gedmo;
use App\Containers\CrmSection\GeneratedLinkContainer\Data\Repositories\GeneratedLinkRepository;
use App\Containers\SettingSection\ListContainer\Entities\DepartmentEntity;
use App\Containers\SettingSection\ListContainer\Entities\StatusEntity;
use App\Ship\ApiPlatform\Filter\SimpleSearchFilter;
use App\Ship\Utils\CarNumberUtil;
/**
* Згенероване посилання
*/
#[ApiResource(
routePrefix: '/crm',
normalizationContext: ['groups' => 'read'],
operations: [
new Get(
uriTemplate: '/generated_links/{id}', requirements: ['id' => '\d+'],
security: "is_granted('ROLE_ADMIN') or is_granted('ROLE_READONLY') or is_granted('ROLE_DEPARTMENT_EMPLOYEE')"
),
new Patch(
uriTemplate: '/generated_links/{id}', requirements: ['id' => '\d+'],
security: "is_granted('ROLE_ADMIN')"
),
new Delete(
uriTemplate: '/generated_links/{id}', requirements: ['id' => '\d+'],
security: "is_granted('ROLE_ADMIN')"
),
// Post відсутній навмисно: запис створюється лише через
// /api/crm/generated_links/generate, інакше можна обійти upsert по парі
// (номер + департамент) і правило «статус лише вперед»
new GetCollection(
uriTemplate: '/generated_links',
security: "is_granted('ROLE_ADMIN') or is_granted('ROLE_READONLY') or is_granted('ROLE_DEPARTMENT_EMPLOYEE')"
),
],
shortName: 'CRM_GeneratedLinks'
)]
#[ApiFilter(SearchFilter::class, properties: [
'id' => 'exact',
'carNumber' => 'partial',
'status' => 'exact',
'status.name' => 'partial',
'department' => 'exact',
'department.name' => 'partial',
'comment' => 'partial',
])]
#[ApiFilter(NumericFilter::class, properties: ['id'])]
#[ApiFilter(RangeFilter::class, properties: ['id'])]
#[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])]
#[ApiFilter(OrderFilter::class)]
#[ApiFilter(filterClass: SimpleSearchFilter::class, properties: [
'carNumber',
'comment',
])]
#[ORM\Entity(repositoryClass: GeneratedLinkRepository::class)]
#[ORM\Table(name: '`crm_generated_links`')]
#[ORM\UniqueConstraint(name: 'crm_generated_links_car_number_department_idx', columns: ['car_number', 'department_id'])]
class GeneratedLinkEntity
{
public const STATUS_CREATED = 'generated_link_created';
public const STATUS_REGISTERED = 'generated_link_registered';
public const STATUS_CONTRACT = 'generated_link_contract';
/**
* Ранги статусів: статус рухається лише вперед.
* Навмисно не використовуємо StatusEntity::sorting — його редагує адмін у довіднику,
* і бізнес-правило залежало б від порядку елементів випадного списку
*/
public const STATUS_RANKS = [
self::STATUS_CREATED => 10,
self::STATUS_REGISTERED => 20,
self::STATUS_CONTRACT => 30,
];
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
#[Groups(['read'])]
private ?int $id = null;
// Держномер
#[ORM\Column(type: Types::STRING, length: 50, nullable: false)]
#[Groups(['read'])]
private string $carNumber;
// Департамент: частина унікальної пари, тому NOT NULL —
// з nullable унікальний індекс у MySQL не працює (NULL != NULL)
#[ORM\ManyToOne(targetEntity: DepartmentEntity::class)]
#[ORM\JoinColumn(name: 'department_id', referencedColumnName: 'id', nullable: false)]
#[Groups(['read'])]
private DepartmentEntity $department;
// Статус
#[ORM\ManyToOne(targetEntity: StatusEntity::class)]
#[ORM\JoinColumn(name: 'status_id', referencedColumnName: 'id', nullable: true)]
#[Groups(['read'])]
private ?StatusEntity $status = null;
// Коментар
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[Groups(['read'])]
private ?string $comment = null;
// Посилання: OneLink з base64 deep_link_value — до ~450 символів, у string(255) не вміщується
#[ORM\Column(type: Types::TEXT, nullable: false)]
#[Groups(['read'])]
private string $link;
#[Gedmo\Timestampable(on: 'create')]
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
#[Context(
normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i:s']
)]
#[Groups(['read'])]
protected $createdAt;
#[Gedmo\Timestampable(on: 'update')]
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
#[Context(
normalizationContext: [DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i:s']
)]
#[Groups(['read'])]
protected $updatedAt;
public static function rank(?string $statusId): int
{
return self::STATUS_RANKS[$statusId] ?? 0;
}
public function getId(): ?int
{
return $this->id;
}
public function getCarNumber()
{
return $this->carNumber;
}
public function setCarNumber(string $number)
{
$this->carNumber = CarNumberUtil::normalize($number);
return $this;
}
public function getDepartment()
{
return $this->department;
}
public function setDepartment(DepartmentEntity $department)
{
$this->department = $department;
return $this;
}
public function getStatus()
{
return $this->status;
}
public function setStatus($status)
{
$this->status = $status;
return $this;
}
public function getComment()
{
return $this->comment;
}
public function setComment(?string $comment)
{
$this->comment = $comment;
return $this;
}
public function getLink()
{
return $this->link;
}
public function setLink(string $link)
{
$this->link = $link;
return $this;
}
public function getCreatedAt()
{
return $this->createdAt;
}
public function getUpdatedAt()
{
return $this->updatedAt;
}
}