<?php
namespace App\Entity;
use ApiPlatform\Core\{
Annotation\ApiFilter,
Annotation\ApiProperty,
Annotation\ApiResource,
Annotation\ApiSubresource,
Bridge\Doctrine\Common\Filter\SearchFilterInterface,
Bridge\Doctrine\Orm\Filter\OrderFilter,
Bridge\Doctrine\Orm\Filter\SearchFilter
};
use App\Doctrine\Filter\ApiArchivesFilter;
use App\Dto\{
PlanDayFillOutput,
UserInput,
UserInputTimeEntry,
UserInputTimeEntryFix,
UserMissingOutput,
UserOutput,
UserOutputTimeEntryFix,
UserSetMissingInput,
UserSetPlanDayInput,
UserUnsetMissingInput
};
use App\Entity\{
Embed\Person,
Plan\PlanDay,
Project\Developer,
Project\Manager,
TimeControl\Task,
TimeControl\TimeEntry,
TimeNormalization\Day,
TimeNormalization\WorkPosition
};
use App\Filter\OrLikeSearchFilter;
use App\Interfaces\{SluggableInterface, UuidKeyInterface};
use App\Repository\WorkPositionRepository;
use App\Traits\SlugAsIdTrait;
use Doctrine\Common\Collections\{ArrayCollection, Collection};
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\PersistentCollection;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\{
Security\Core\User\PasswordAuthenticatedUserInterface,
Security\Core\User\UserInterface,
Validator\Constraints as Assert
};
#[ApiResource(
collectionOperations: ['get'],
itemOperations: [
'get' => [
'openapi_context' => [
'parameters' => [
['name' => 'id', 'in' => 'path', 'required' => true, 'schema' => ['type' => 'string']],
['name' => 'archived', 'in' => 'query', 'required' => false, 'schema' => ['type' => 'boolean']],
],
],
'normalization_context' => [
'groups' => ['User:item:read'],
],
],
'put' => ['security' => "is_granted('ROLE_ADMIN') or is_granted('ROLE_MASTER_MANAGER') or is_granted('ROLE_HR')"],
'patch' => ['security' => "is_granted('ROLE_ADMIN') or is_granted('ROLE_MASTER_MANAGER') or is_granted('ROLE_HR')"],
'delete' => ['security' => "is_granted('ROLE_ADMIN')"],
User::ITEM_MISSING_PLAN_DAYS => [
'method' => 'GET',
'path' => 'users/{slug}/missing',
'input' => false,
'output' => UserMissingOutput::class,
'security' => "is_granted('ROLE_MANAGER')",
'openapi_context' => [
'summary' => 'Get missing period for users',
'parameters' => [
['name' => 'periodStart', 'in' => 'query', 'required' => true, 'schema' => ['type' => 'string']],
['name' => 'periodEnd', 'in' => 'query', 'required' => true, 'schema' => ['type' => 'string']],
],
],
],
User::PATCH_TE_OPERATION => [
'method' => 'PATCH',
'path' => 'users/{slug}/time-entries',
'input' => UserInputTimeEntry::class,
'output' => UserOutputTimeEntryFix::class,
'security' => "is_granted('ROLE_USER_FIXER')",
'openapi_context' => ['summary' => 'Set fix $ approved for time-entries'],
],
User::ITEM_SET_MISSING_PLAN_DAYS => [
'method' => 'PATCH',
'path' => 'users/{slug}/missing/set',
'input' => UserSetMissingInput::class,
'output' => false,
'security' => "is_granted('ROLE_MANAGER')",
'openapi_context' => ['summary' => 'Set missing period for users'],
],
User::ITEM_UNSET_MISSING_PLAN_DAYS => [
'method' => 'PATCH',
'path' => 'users/{slug}/missing/unset',
'input' => UserUnsetMissingInput::class,
'output' => false,
'security' => "is_granted('ROLE_MANAGER')",
'openapi_context' => ['summary' => 'Delete missing period for users'],
],
User::ITEM_SET_PLAN_DAYS => [
'method' => 'PATCH',
'path' => 'users/{slug}/' . User::ITEM_SET_PLAN_DAYS,
'status' => 202,
'input' => UserSetPlanDayInput::class,
'output' => PlanDayFillOutput::class,
'security' => "is_granted('ROLE_MANAGER')",
'openapi_context' => ['summary' => 'Fill plan days for user in period'],
],
User::ITEM_FIX_TIME_ENTRIES => [
'method' => 'PATCH',
'path' => 'users/{slug}/time-entries/fix',
'input' => UserInputTimeEntryFix::class,
'output' => UserOutputTimeEntryFix::class,
'security' => "is_granted('ROLE_USER_FIXER')",
'openapi_context' => ['summary' => 'Fix TimeEntry allowed to project manager or higher'],
],
User::ITEM_UNFIX_TIME_ENTRIES => [
'method' => 'PATCH',
'path' => 'users/{slug}/time-entries/unfix',
'input' => UserInputTimeEntryFix::class,
'output' => false,
'security' => "is_granted('ROLE_USER_FIXER')",
'openapi_context' => ['summary' => 'Unfix TimeEntry allowed to Main project manager or higher'],
],
User::ITEM_DISMISS => [
'method' => 'PATCH',
'path' => 'users/{slug}/dismiss',
'input' => false,
'output' => false,
'security' => "is_granted('ROLE_HR') or is_granted('ROLE_MASTER_MANAGER')",
'openapi_context' => ['summary' => 'Dismiss employee'],
],
User::ITEM_UNDISMISS => [
'method' => 'PATCH',
'path' => 'users/{slug}/undismiss',
'input' => false,
'output' => false,
'security' => "is_granted('ROLE_HR') or is_granted('ROLE_MASTER_MANAGER')",
'openapi_context' => ['summary' => 'Cancel the dismissal employee'],
],
],
subresourceOperations: [
'teams_get_subresource' => [
'method' => 'GET',
'openapi_context' => [
'summary' => 'Получить команды пользователя',
'description' => 'Получить команды пользователя',
],
],
],
attributes: [
'pagination_items_per_page' => 300,
'order' => ['slug' => 'ASC'],
],
input: UserInput::class,
output: UserOutput::class,
)]
#[ApiFilter(OrLikeSearchFilter::class, properties: [
'filter_name' => 'search',
'fields' => [
'email',
'profile' => ['person.firstName', 'person.lastName', 'person.surname'],
],
])]
#[ApiFilter(OrderFilter::class, properties: ['profile.person.lastName'])]
#[ApiFilter(ApiArchivesFilter::class)]
#[ApiFilter(SearchFilter::class, properties: [
'workPositions.workingStatus' => SearchFilterInterface::STRATEGY_EXACT,
])]
#[ORM\Entity(repositoryClass: 'App\Repository\UserRepository')]
#[ORM\Table(name: 'hub_user')]
class User implements UserInterface, UuidKeyInterface, SluggableInterface, PasswordAuthenticatedUserInterface
{
use SlugAsIdTrait;
public const ITEM_FIX_TIME_ENTRIES = 'fix-time-entries';
public const ITEM_UNFIX_TIME_ENTRIES = 'unfix-time-entries';
public const ITEM_MISSING_PLAN_DAYS = 'missing-plan-days';
public const ITEM_SET_MISSING_PLAN_DAYS = 'set-missing-plan-days';
public const ITEM_UNSET_MISSING_PLAN_DAYS = 'unset-missing-plan-days';
public const ITEM_SET_PLAN_DAYS = 'set-plan-days';
public const PATCH_TE_OPERATION = 'patch-te';
public const ITEM_DISMISS = 'user-dismiss';
public const ITEM_UNDISMISS = 'user-undismiss';
public const READ_GROUP = 'User:read';
#[Gedmo\Slug(fields: ['email'], updatable: true, separator: '-')]
#[ApiProperty(iri: 'https://schema.org/identifier', identifier: true)]
#[ORM\Column(type: 'string', length: 255, unique: true)]
protected ?string $slug = null;
#[ApiFilter(SearchFilter::class, strategy: SearchFilter::STRATEGY_START)]
#[Assert\NotBlank]
#[Assert\Email]
#[ORM\Column(type: 'string', length: 180, unique: true)]
private ?string $email = null;
#[ORM\Column(type: 'json')]
private array $roles = [];
#[ORM\Column(type: 'string')]
private string $password = '';
#[ApiSubresource]
#[ORM\OneToOne(targetEntity: 'App\Entity\Profile', mappedBy: 'user', cascade: ['persist', 'remove'], fetch: 'EAGER')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?Profile $profile = null;
/**
* @var Collection<int, TimeEntry>
*/
#[ORM\OneToMany(targetEntity: 'App\Entity\TimeControl\TimeEntry', mappedBy: 'user')]
private Collection $timeEntries;
#[ORM\OneToOne(targetEntity: 'App\Entity\Project\Manager', cascade: ['persist'], orphanRemoval: true, fetch: 'EAGER')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?Manager $manager = null;
#[ORM\OneToOne(targetEntity: 'App\Entity\Project\Developer', cascade: ['persist'], orphanRemoval: true, fetch: 'EAGER')]
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
private ?Developer $developer = null;
/**
* @var Collection<int, Task>
*/
#[ApiSubresource]
#[ORM\OneToMany(targetEntity: Task::class, mappedBy: 'user')]
private Collection $tasks;
/**
* @var Collection<int, Day>
*/
#[ORM\OneToMany(targetEntity: Day::class, mappedBy: 'user')]
private Collection $days;
#[ApiSubresource]
#[ORM\ManyToMany(targetEntity: Team::class)]
#[ORM\JoinTable(name: 'team_members')]
private Collection $teams;
#[ORM\ManyToOne(targetEntity: Unit::class)]
#[ORM\JoinColumn(nullable: true)]
private ?Unit $unit = null;
/**
* @var Collection<int, WorkPosition>
*/
#[ORM\OneToMany(targetEntity: WorkPosition::class, mappedBy: 'user')]
private Collection $workPositions;
/**
* @var Collection<int, PlanDay>
*/
#[ORM\OneToMany(targetEntity: PlanDay::class, mappedBy: 'user')]
private Collection $planDay;
#[ORM\Column(type: 'boolean', nullable: true)]
private ?bool $archived = null;
#[ORM\Column(type: 'boolean', nullable: false)]
private bool $isTgNotify;
public function __construct()
{
$this->timeEntries = new ArrayCollection();
$this->tasks = new ArrayCollection();
$this->days = new ArrayCollection();
$this->teams = new ArrayCollection();
$this->workPositions = new ArrayCollection();
$this->planDay = new ArrayCollection();
$this->isTgNotify = true;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getUserIdentifier(): string
{
return (string) $this->email;
}
public function getUsername(): string
{
return (string) $this->email;
}
public function getRoles(): array
{
$roles = $this->roles;
$roles[] = 'ROLE_USER';
return \array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
public function getSalt(): ?string
{
return null;
}
public function eraseCredentials(): void
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getProfile(): ?Profile
{
return $this->profile;
}
public function setProfile(?Profile $profile): self
{
$this->profile = $profile;
if ($profile === null) {
return $this;
}
if ($this !== $profile->getUser()) {
$profile->setUser($this);
}
return $this;
}
public function getTimeEntries(): Collection
{
return $this->timeEntries;
}
public function addTimeEntry(TimeEntry $timeEntry): self
{
if (!$this->timeEntries->contains($timeEntry)) {
$this->timeEntries[] = $timeEntry;
$timeEntry->setUser($this);
}
return $this;
}
public function removeTimeEntry(TimeEntry $timeEntry): self
{
if ($this->timeEntries->contains($timeEntry)) {
$this->timeEntries->removeElement($timeEntry);
// set the owning side to null (unless already changed)
if ($timeEntry->getUser() === $this) {
$timeEntry->setUser(null);
}
}
return $this;
}
public function getManager(): ?Manager
{
return $this->manager;
}
public function setManager(?Manager $manager): self
{
$this->manager = $manager;
return $this;
}
public function getDeveloper(): ?Developer
{
return $this->developer;
}
public function setDeveloper(?Developer $developer): self
{
$this->developer = $developer;
return $this;
}
public function getTasks(): Collection
{
return $this->tasks;
}
public function addTask(Task $task): self
{
if (!$this->tasks->contains($task)) {
$this->tasks[] = $task;
$task->setUser($this);
}
return $this;
}
public function removeTask(Task $task): self
{
if ($this->tasks->contains($task)) {
$this->tasks->removeElement($task);
$task->setUser(null);
}
return $this;
}
public function getDays(): Collection
{
return $this->days;
}
public function addDay(Day $day): self
{
if (!$this->days->contains($day)) {
$this->days[] = $day;
$day->setUser($this);
}
return $this;
}
public function removeDay(Day $day): self
{
if ($this->days->contains($day)) {
$this->days->removeElement($day);
$day->setUser(null);
}
return $this;
}
public function getTeams(): Collection
{
return $this->teams;
}
public function addTeam(TeamMember $team): self
{
if (!$this->teams->contains($team)) {
$this->teams[] = $team;
$team->setUser($this);
}
return $this;
}
public function removeTeam(TeamMember $team): self
{
if ($this->teams->contains($team)) {
$this->teams->removeElement($team);
}
return $this;
}
public function getUnit(): ?Unit
{
return $this->unit;
}
public function setUnit(?Unit $unit): self
{
$this->unit = $unit;
return $this;
}
public function getArchived(): ?bool
{
return $this->archived;
}
public function setArchived(?bool $archived): self
{
$this->archived = $archived;
return $this;
}
public function getWorkPositions(): Collection
{
return $this->workPositions;
}
public function addWorkPosition(WorkPosition $workPosition): self
{
if (!$this->workPositions->contains($workPosition)) {
$this->workPositions[] = $workPosition;
$workPosition->setUser($this);
}
return $this;
}
/**
* рабочая позиция в заданный момент времени (по умлочанию - now).
*/
public function getWorkPosition(\DateTime $onDate = null): ?WorkPosition
{
$criteria = WorkPositionRepository::createOnDateCriteria($onDate);
$workPositions = $this->getWorkPositions();
if (!$workPositions instanceof PersistentCollection) {
return null;
}
return $workPositions->matching($criteria)->getValues()[0] ?? null;
}
public function removeWorkPosition(WorkPosition $workPosition): self
{
if ($this->workPositions->contains($workPosition)) {
$this->workPositions->removeElement($workPosition);
}
return $this;
}
public function getPlanDay(): Collection
{
return $this->planDay;
}
public function addPlanDay(PlanDay $planDay): self
{
if (!$this->planDay->contains($planDay)) {
$this->planDay[] = $planDay;
$planDay->setUser($this);
}
return $this;
}
public function removePlanDay(PlanDay $planDay): self
{
if ($this->planDay->contains($planDay)) {
$this->planDay->removeElement($planDay);
}
return $this;
}
public function isDeveloper(): bool
{
$roles = $this->getRoles();
return \in_array('ROLE_DEVELOPER', $roles, true);
}
public function isManager(): bool
{
$roles = $this->getRoles();
return \in_array('ROLE_MANAGER', $roles, true);
}
public function isMasterManager(): bool
{
$roles = $this->getRoles();
return \in_array('ROLE_MASTER_MANAGER', $roles, true);
}
public function isAdmin(): bool
{
$roles = $this->getRoles();
return \in_array('ROLE_ADMIN', $roles, true);
}
public function isTgNotify(): bool
{
return $this->isTgNotify;
}
/**
* @return $this
*/
public function setIsTgNotify(bool $isTgNotify): self
{
$this->isTgNotify = $isTgNotify;
return $this;
}
public function __toString(): string
{
$profile = $this->getProfile();
$person = ($profile instanceof Profile) ? $profile->getPerson() : null;
$name = ($person instanceof Person) ? $person->getFullName() : '';
$result = $name ?: $this->slug;
return $result ?: 'Unknown user';
}
}