Calendar::class])] #[ORM\Entity] #[ORM\Table(name: 'chill_calendar.calendar')] #[ORM\UniqueConstraint(name: 'idx_calendar_remote', columns: ['remoteId'], options: ['where' => "remoteId <> ''"])] class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCentersInterface { use RemoteCalendarTrait; use TrackCreationTrait; use TrackUpdateTrait; final public const SMS_CANCEL_PENDING = 'sms_cancel_pending'; final public const SMS_PENDING = 'sms_pending'; final public const SMS_SENT = 'sms_sent'; final public const STATUS_CANCELED = 'canceled'; /** * @deprecated */ final public const STATUS_MOVED = 'moved'; final public const STATUS_VALID = 'valid'; /** * a list of invite which have been added during this session. * * @var array|Invite[] */ public array $newInvites = []; /** * a list of invite which have been removed during this session. * * @var array|Invite[] */ public array $oldInvites = []; public ?CalendarRange $previousCalendarRange = null; public ?User $previousMainUser = null; #[Serializer\Groups(['calendar:read', 'read'])] #[ORM\ManyToOne(targetEntity: AccompanyingPeriod::class, inversedBy: 'calendars')] private ?AccompanyingPeriod $accompanyingPeriod = null; #[ORM\ManyToOne(targetEntity: Activity::class)] private ?Activity $activity = null; #[Serializer\Groups(['calendar:read', 'read'])] #[ORM\OneToOne(targetEntity: CalendarRange::class, inversedBy: 'calendar')] private ?CalendarRange $calendarRange = null; #[ORM\ManyToOne(targetEntity: CancelReason::class)] private ?CancelReason $cancelReason = null; #[Serializer\Groups(['calendar:read', 'read', 'docgen:read'])] #[ORM\Embedded(class: CommentEmbeddable::class, columnPrefix: 'comment_')] private CommentEmbeddable $comment; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false, options: ['default' => 0])] private int $dateTimeVersion = 0; /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'calendar', targetEntity: CalendarDoc::class, orphanRemoval: true)] private Collection $documents; #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])] #[Assert\NotNull(message: 'calendar.An end date is required')] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)] private ?\DateTimeImmutable $endDate = null; #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])] #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER)] private ?int $id = null; /** * @var Collection&Selectable */ #[Serializer\Groups(['read', 'docgen:read'])] #[ORM\OneToMany(mappedBy: 'calendar', targetEntity: Invite::class, cascade: ['persist', 'remove', 'merge', 'detach'], orphanRemoval: true)] #[ORM\JoinTable(name: 'chill_calendar.calendar_to_invites')] private Collection&Selectable $invites; #[Serializer\Groups(['read', 'docgen:read'])] #[Assert\NotNull(message: 'calendar.A location is required')] #[ORM\ManyToOne(targetEntity: Location::class)] private ?Location $location = null; #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])] #[Assert\NotNull(message: 'calendar.A main user is mandatory')] #[ORM\ManyToOne(targetEntity: User::class)] #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])] private ?User $mainUser = null; #[ORM\ManyToOne(targetEntity: Person::class)] #[ORM\JoinColumn(nullable: true)] private ?Person $person = null; /** * @var Collection */ #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])] #[Assert\Count(min: 1, minMessage: 'calendar.At least {{ limit }} person is required.')] #[ORM\ManyToMany(targetEntity: Person::class, inversedBy: 'calendars')] #[ORM\JoinTable(name: 'chill_calendar.calendar_to_persons')] #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])] private Collection $persons; #[Serializer\Groups(['calendar:read'])] #[ORM\Embedded(class: PrivateCommentEmbeddable::class, columnPrefix: 'privateComment_')] private PrivateCommentEmbeddable $privateComment; /** * @var Collection */ #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])] #[ORM\ManyToMany(targetEntity: ThirdParty::class)] #[ORM\JoinTable(name: 'chill_calendar.calendar_to_thirdparties')] #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])] private Collection $professionals; #[Serializer\Groups(['docgen:read'])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: true)] private ?bool $sendSMS = false; #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => Calendar::SMS_PENDING])] private string $smsStatus = self::SMS_PENDING; #[Serializer\Groups(['calendar:read', 'read', 'calendar:light', 'docgen:read'])] #[Assert\NotNull(message: 'calendar.A start date is required')] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)] #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])] private ?\DateTimeImmutable $startDate = null; #[Serializer\Groups(['calendar:read', 'read', 'calendar:light'])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 255, nullable: false, options: ['default' => 'valid'])] #[Serializer\Context(normalizationContext: ['read'], groups: ['calendar:light'])] private string $status = self::STATUS_VALID; #[Serializer\Groups(['docgen:read'])] #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: true)] private ?bool $urgent = false; public function __construct() { $this->comment = new CommentEmbeddable(); $this->documents = new ArrayCollection(); $this->privateComment = new PrivateCommentEmbeddable(); $this->persons = new ArrayCollection(); $this->professionals = new ArrayCollection(); $this->invites = new ArrayCollection(); } /** * @internal use @{CalendarDoc::__construct} instead */ public function addDocument(CalendarDoc $calendarDoc): self { if ($this->documents->contains($calendarDoc)) { $this->documents[] = $calendarDoc; } return $this; } /** * @internal Use {@link (Calendar::addUser)} instead */ public function addInvite(Invite $invite): self { if ($invite->getCalendar() instanceof Calendar && $invite->getCalendar() !== $this) { throw new \LogicException('Not allowed to move an invitation to another Calendar'); } $this->invites[] = $invite; $this->newInvites[] = $invite; $invite->setCalendar($this); return $this; } public function addPerson(Person $person): self { $this->persons[] = $person; return $this; } public function addProfessional(ThirdParty $professional): self { $this->professionals[] = $professional; return $this; } public function addUser(User $user): self { if (!$this->getUsers()->contains($user) && $this->getMainUser() !== $user) { $this->addInvite((new Invite())->setUser($user)); } return $this; } public function getAccompanyingPeriod(): ?AccompanyingPeriod { return $this->accompanyingPeriod; } public function getActivity(): ?Activity { return $this->activity; } public function getCalendarRange(): ?CalendarRange { return $this->calendarRange; } public function getCancelReason(): ?CancelReason { return $this->cancelReason; } public function getCenters(): ?iterable { return match ($this->getContext()) { 'person' => [$this->getPerson()->getCenter()], 'accompanying_period' => $this->getAccompanyingPeriod()->getCenters(), default => throw new \LogicException('context not supported: '.$this->getContext()), }; } public function getComment(): CommentEmbeddable { return $this->comment; } /** * @return 'person'|'accompanying_period'|null */ public function getContext(): ?string { if (null !== $this->getAccompanyingPeriod()) { return 'accompanying_period'; } if (null !== $this->getPerson()) { return 'person'; } return null; } /** * Each time the date and time is update, this version is incremented. */ public function getDateTimeVersion(): int { return $this->dateTimeVersion; } public function getDocuments(): Collection { return $this->documents; } #[Serializer\Groups(['docgen:read'])] public function getDuration(): ?\DateInterval { if (null === $this->getStartDate() || null === $this->getEndDate()) { return null; } return $this->getStartDate()->diff($this->getEndDate()); } public function getEndDate(): ?\DateTimeImmutable { return $this->endDate; } public function getId(): ?int { return $this->id; } public function getInviteForUser(User $user): ?Invite { $criteria = Criteria::create(); $criteria->where(Criteria::expr()->eq('user', $user)); $matchings = $this->invites ->matching($criteria); if (1 === $matchings->count()) { return $matchings->first(); } return null; } /** * @return Collection|Invite[] */ public function getInvites(): Collection { return $this->invites; } public function getLocation(): ?Location { return $this->location; } public function getMainUser(): ?User { return $this->mainUser; } public function getPerson(): ?Person { return $this->person; } /** * @return Collection|Person[] */ public function getPersons(): Collection { return $this->persons; } public function getPersonsAssociated(): array { if (null !== $this->accompanyingPeriod) { $personsAssociated = []; foreach ($this->accompanyingPeriod->getParticipations() as $participation) { if ($this->persons->contains($participation->getPerson())) { $personsAssociated[] = $participation->getPerson(); } } return $personsAssociated; } return []; } public function getPersonsNotAssociated(): array { if (null !== $this->accompanyingPeriod) { $personsNotAssociated = []; foreach ($this->persons as $person) { if (!\in_array($person, $this->getPersonsAssociated(), true)) { $personsNotAssociated[] = $person; } } return $personsNotAssociated; } return []; } public function getPrivateComment(): PrivateCommentEmbeddable { return $this->privateComment; } /** * @return Collection|ThirdParty[] */ public function getProfessionals(): Collection { return $this->professionals; } public function getSendSMS(): ?bool { return $this->sendSMS; } public function getSmsStatus(): string { return $this->smsStatus; } public function getStartDate(): ?\DateTimeImmutable { return $this->startDate; } /** * get the date of the calendar. * * Useful for showing the date of the calendar event, required by twig in some places. */ public function getDate(): ?\DateTimeImmutable { return $this->getStartDate(); } public function getStatus(): ?string { return $this->status; } public function getThirdParties(): Collection { return $this->getProfessionals(); } public function getUrgent(): ?bool { return $this->urgent; } /** * @return ReadableCollection<(int|string), User> */ #[Serializer\Groups(['calendar:read', 'read'])] public function getUsers(): ReadableCollection { return $this->getInvites()->map(static fn (Invite $i) => $i->getUser()); } public function hasCalendarRange(): bool { return null !== $this->calendarRange; } public function hasLocation(): bool { return null !== $this->getLocation(); } /** * return true if the user is invited. */ public function isInvited(User $user): bool { if ($this->getMainUser() === $user) { return false; } return $this->getUsers()->contains($user); } public static function loadValidatorMetadata(ClassMetadata $metadata): void { $metadata->addPropertyConstraint('startDate', new NotBlank()); $metadata->addPropertyConstraint('startDate', new Range([ 'min' => '2 years ago', 'max' => '+ 2 years', ])); $metadata->addPropertyConstraint('endDate', new NotBlank()); $metadata->addPropertyConstraint('endDate', new Range([ 'min' => '2 years ago', 'max' => '+ 2 years', ])); } /** * @internal use @{CalendarDoc::setCalendar} with null instead */ public function removeDocument(CalendarDoc $calendarDoc): self { if ($calendarDoc->getCalendar() !== $this) { throw new \LogicException('cannot remove document of another calendar'); } return $this; } /** * @internal Use {@link (Calendar::removeUser)} instead */ public function removeInvite(Invite $invite): self { if ($this->invites->removeElement($invite)) { $invite->setCalendar(null); $this->oldInvites[] = $invite; } return $this; } public function removePerson(Person $person): self { $this->persons->removeElement($person); return $this; } public function removeProfessional(ThirdParty $professional): self { $this->professionals->removeElement($professional); return $this; } public function removeUser(User $user): self { if (!$this->getUsers()->contains($user)) { return $this; } $invite = $this->invites ->filter(static fn (Invite $invite) => $invite->getUser() === $user) ->first(); $this->removeInvite($invite); return $this; } public function setAccompanyingPeriod(?AccompanyingPeriod $accompanyingPeriod): self { $this->accompanyingPeriod = $accompanyingPeriod; return $this; } public function setActivity(?Activity $activity): self { $this->activity = $activity; return $this; } public function setCalendarRange(?CalendarRange $calendarRange): self { if ($this->calendarRange !== $calendarRange) { $this->previousCalendarRange = $this->calendarRange; if (null !== $this->previousCalendarRange) { $this->previousCalendarRange->setCalendar(null); } } $this->calendarRange = $calendarRange; if ($this->calendarRange instanceof CalendarRange) { $this->calendarRange->setCalendar($this); } return $this; } public function setCancelReason(?CancelReason $cancelReason): self { $this->cancelReason = $cancelReason; return $this; } public function setComment(CommentEmbeddable $comment): self { $this->comment = $comment; return $this; } public function setEndDate(\DateTimeImmutable $endDate): self { if (null === $this->endDate || $this->endDate->getTimestamp() !== $endDate->getTimestamp()) { $this->increaseaDatetimeVersion(); } $this->endDate = $endDate; return $this; } public function setLocation(?Location $location): Calendar { $this->location = $location; return $this; } public function setMainUser(?User $mainUser): self { if ($this->mainUser !== $mainUser) { $this->previousMainUser = $this->mainUser; } $this->mainUser = $mainUser; $this->removeUser($mainUser); return $this; } public function setPerson(?Person $person): Calendar { $this->person = $person; return $this; } public function setPrivateComment(PrivateCommentEmbeddable $privateComment): self { $this->privateComment = $privateComment; return $this; } public function setSendSMS(?bool $sendSMS): self { $this->sendSMS = $sendSMS; return $this; } public function setSmsStatus(string $smsStatus): self { $this->smsStatus = $smsStatus; return $this; } public function setStartDate(\DateTimeImmutable $startDate): self { if (null === $this->startDate || $this->startDate->getTimestamp() !== $startDate->getTimestamp()) { $this->increaseaDatetimeVersion(); } $this->startDate = $startDate; return $this; } public function setStatus(string $status): self { $this->status = $status; if (self::STATUS_CANCELED === $status && self::SMS_SENT === $this->getSmsStatus()) { $this->setSmsStatus(self::SMS_CANCEL_PENDING); } return $this; } public function setUrgent(bool $urgent): self { $this->urgent = $urgent; return $this; } private function increaseaDatetimeVersion(): void { ++$this->dateTimeVersion; } }