feat: add a booking event history for admins
This commit is contained in:
1 parent
d68b6b156c
commit
0ab39d810b
18 files changed
+1578
-6
No files matched your search
@@ -0,0 +1,295 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Enum\BookingEventType;
|
||||
use App\Enum\RecurrenceRule;
|
||||
use App\Enum\ResourceType;
|
||||
use App\Model\TimeRange;
|
||||
use App\Repository\BookingEventRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* One line of the event history: something that happened to a booking, a series, or the bookings
|
||||
* on a deleted resource.
|
||||
*
|
||||
* **A snapshot, not a reference.** Releasing a booking deletes its row, ending a series can delete
|
||||
* the series, and deleting a resource deletes both -- so a foreign key to any of them would point
|
||||
* at nothing on exactly the records worth reading. What the history shows is therefore copied in
|
||||
* at the time: labels, names, dates and times. The ids are kept as plain integers, so a record can
|
||||
* still be matched to the audit log file, which carries the same ones.
|
||||
*
|
||||
* The two people are the exception: they are kept as nullable foreign keys *and* as names. The key
|
||||
* is what the filter matches on, so a renamed account still finds its own history; the name is
|
||||
* what is shown, and survives the account itself going away (ON DELETE SET NULL).
|
||||
*
|
||||
* Written by BookingEventRecorder alone, inside the same transaction as the change it describes.
|
||||
*/
|
||||
#[ORM\Entity(repositoryClass: BookingEventRepository::class)]
|
||||
#[ORM\Index(name: 'idx_booking_event_occurred_at', columns: ['occurred_at'])]
|
||||
#[ORM\Index(name: 'idx_booking_event_actor_occurred_at', columns: ['actor_id', 'occurred_at'])]
|
||||
class BookingEvent
|
||||
{
|
||||
/** How long the history reaches back -- see app:booking-events:prune. */
|
||||
public const RETENTION_MONTHS = 12;
|
||||
|
||||
/** The actor name when nobody was signed in -- a console command, the worker. */
|
||||
public const SYSTEM_ACTOR = 'System';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private \DateTimeImmutable $occurredAt;
|
||||
|
||||
#[ORM\Column(length: 32, enumType: BookingEventType::class)]
|
||||
private BookingEventType $type;
|
||||
|
||||
/** Who did it; null for a console command or the message worker, which have no user. */
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?User $actor;
|
||||
|
||||
#[ORM\Column(length: 100)]
|
||||
private string $actorName;
|
||||
|
||||
/** Whose booking it was; null only for a resource deletion, which affects many people. */
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?User $holder = null;
|
||||
|
||||
#[ORM\Column(length: 100, nullable: true)]
|
||||
private ?string $holderName = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $resourceId;
|
||||
|
||||
#[ORM\Column(length: 50)]
|
||||
private string $resourceLabel;
|
||||
|
||||
#[ORM\Column(length: 32, enumType: ResourceType::class)]
|
||||
private ResourceType $resourceType;
|
||||
|
||||
/**
|
||||
* The booked day; for a series its first day, and for an ended series the day it ends from.
|
||||
*/
|
||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $date = null;
|
||||
|
||||
/** A series' last day. */
|
||||
#[ORM\Column(type: 'date_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $endDate = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $startMinute = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $endMinute = null;
|
||||
|
||||
/** The times a rescheduled booking had before. */
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $previousStartMinute = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $previousEndMinute = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $bookingId = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?int $seriesId = null;
|
||||
|
||||
/**
|
||||
* What only one kind of event has: a series' rule and its created/skipped counts, the number
|
||||
* of bookings a series end or a resource deletion removed.
|
||||
*
|
||||
* @var array<string, mixed>
|
||||
*/
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $details = [];
|
||||
|
||||
public function __construct(
|
||||
BookingEventType $type,
|
||||
?User $actor,
|
||||
string $actorName,
|
||||
?int $resourceId,
|
||||
string $resourceLabel,
|
||||
ResourceType $resourceType,
|
||||
) {
|
||||
$this->occurredAt = new \DateTimeImmutable();
|
||||
$this->type = $type;
|
||||
$this->actor = $actor;
|
||||
$this->actorName = $actorName;
|
||||
$this->resourceId = $resourceId;
|
||||
$this->resourceLabel = $resourceLabel;
|
||||
$this->resourceType = $resourceType;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getOccurredAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->occurredAt;
|
||||
}
|
||||
|
||||
public function getType(): BookingEventType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getActor(): ?User
|
||||
{
|
||||
return $this->actor;
|
||||
}
|
||||
|
||||
public function getActorName(): string
|
||||
{
|
||||
return $this->actorName;
|
||||
}
|
||||
|
||||
public function getHolder(): ?User
|
||||
{
|
||||
return $this->holder;
|
||||
}
|
||||
|
||||
public function getHolderName(): ?string
|
||||
{
|
||||
return $this->holderName;
|
||||
}
|
||||
|
||||
public function setHolder(User $holder): self
|
||||
{
|
||||
$this->holder = $holder;
|
||||
$this->holderName = $holder->getName();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Somebody acting on another person's booking -- a supervisor freeing a desk, say. Compared by
|
||||
* id while both accounts exist and by name once one has gone, which is the best left to go on.
|
||||
* The system acts for nobody: it has no booking of its own to tell apart from someone else's.
|
||||
*/
|
||||
public function isOnBehalf(): bool
|
||||
{
|
||||
if (null === $this->holderName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null === $this->actor && self::SYSTEM_ACTOR === $this->actorName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null !== $this->actor && null !== $this->holder) {
|
||||
return $this->actor->getId() !== $this->holder->getId();
|
||||
}
|
||||
|
||||
return $this->actorName !== $this->holderName;
|
||||
}
|
||||
|
||||
public function getResourceId(): ?int
|
||||
{
|
||||
return $this->resourceId;
|
||||
}
|
||||
|
||||
public function getResourceLabel(): string
|
||||
{
|
||||
return $this->resourceLabel;
|
||||
}
|
||||
|
||||
public function getResourceType(): ResourceType
|
||||
{
|
||||
return $this->resourceType;
|
||||
}
|
||||
|
||||
public function getDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function getEndDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->endDate;
|
||||
}
|
||||
|
||||
public function setDates(\DateTimeImmutable $date, ?\DateTimeImmutable $endDate = null): self
|
||||
{
|
||||
$this->date = $date;
|
||||
$this->endDate = $endDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTimeRange(): ?TimeRange
|
||||
{
|
||||
return null === $this->startMinute || null === $this->endMinute
|
||||
? null
|
||||
: TimeRange::fromMinutes($this->startMinute, $this->endMinute);
|
||||
}
|
||||
|
||||
public function getPreviousTimeRange(): ?TimeRange
|
||||
{
|
||||
return null === $this->previousStartMinute || null === $this->previousEndMinute
|
||||
? null
|
||||
: TimeRange::fromMinutes($this->previousStartMinute, $this->previousEndMinute);
|
||||
}
|
||||
|
||||
public function setTimeRange(TimeRange $time, ?TimeRange $previous = null): self
|
||||
{
|
||||
$this->startMinute = $time->startMinute;
|
||||
$this->endMinute = $time->endMinute;
|
||||
$this->previousStartMinute = $previous?->startMinute;
|
||||
$this->previousEndMinute = $previous?->endMinute;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBookingId(): ?int
|
||||
{
|
||||
return $this->bookingId;
|
||||
}
|
||||
|
||||
public function getSeriesId(): ?int
|
||||
{
|
||||
return $this->seriesId;
|
||||
}
|
||||
|
||||
public function setIds(?int $bookingId, ?int $seriesId): self
|
||||
{
|
||||
$this->bookingId = $bookingId;
|
||||
$this->seriesId = $seriesId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getDetails(): array
|
||||
{
|
||||
return $this->details;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $details
|
||||
*/
|
||||
public function setDetails(array $details): self
|
||||
{
|
||||
$this->details = $details;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRule(): ?RecurrenceRule
|
||||
{
|
||||
return isset($this->details['rule']) ? RecurrenceRule::tryFrom($this->details['rule']) : null;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user