diff --git a/migrations/Version20260310211017.php b/migrations/Version20260310211017.php new file mode 100644 index 0000000..c918883 --- /dev/null +++ b/migrations/Version20260310211017.php @@ -0,0 +1,49 @@ +addSql('CREATE TABLE time_entry (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, title VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, started_at TIMESTAMP(0) WITH TIME ZONE NOT NULL, stopped_at TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, user_id INT NOT NULL, project_id INT DEFAULT NULL, task_id INT DEFAULT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_6E537C0CA76ED395 ON time_entry (user_id)'); + $this->addSql('CREATE INDEX IDX_6E537C0C166D1F9C ON time_entry (project_id)'); + $this->addSql('CREATE INDEX IDX_6E537C0C8DB60186 ON time_entry (task_id)'); + $this->addSql('CREATE UNIQUE INDEX uniq_active_timer ON time_entry (user_id) WHERE (stopped_at IS NULL)'); + $this->addSql('CREATE TABLE time_entry_task_type (time_entry_id INT NOT NULL, task_type_id INT NOT NULL, PRIMARY KEY (time_entry_id, task_type_id))'); + $this->addSql('CREATE INDEX IDX_BE7A719D1EB30A8E ON time_entry_task_type (time_entry_id)'); + $this->addSql('CREATE INDEX IDX_BE7A719DDAADA679 ON time_entry_task_type (task_type_id)'); + $this->addSql('ALTER TABLE time_entry ADD CONSTRAINT FK_6E537C0CA76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE'); + $this->addSql('ALTER TABLE time_entry ADD CONSTRAINT FK_6E537C0C166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE SET NULL NOT DEFERRABLE'); + $this->addSql('ALTER TABLE time_entry ADD CONSTRAINT FK_6E537C0C8DB60186 FOREIGN KEY (task_id) REFERENCES task (id) ON DELETE SET NULL NOT DEFERRABLE'); + $this->addSql('ALTER TABLE time_entry_task_type ADD CONSTRAINT FK_BE7A719D1EB30A8E FOREIGN KEY (time_entry_id) REFERENCES time_entry (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE time_entry_task_type ADD CONSTRAINT FK_BE7A719DDAADA679 FOREIGN KEY (task_type_id) REFERENCES task_type (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE time_entry DROP CONSTRAINT FK_6E537C0CA76ED395'); + $this->addSql('ALTER TABLE time_entry DROP CONSTRAINT FK_6E537C0C166D1F9C'); + $this->addSql('ALTER TABLE time_entry DROP CONSTRAINT FK_6E537C0C8DB60186'); + $this->addSql('ALTER TABLE time_entry_task_type DROP CONSTRAINT FK_BE7A719D1EB30A8E'); + $this->addSql('ALTER TABLE time_entry_task_type DROP CONSTRAINT FK_BE7A719DDAADA679'); + $this->addSql('DROP TABLE time_entry'); + $this->addSql('DROP TABLE time_entry_task_type'); + } +} diff --git a/src/Entity/TimeEntry.php b/src/Entity/TimeEntry.php new file mode 100644 index 0000000..5852b0e --- /dev/null +++ b/src/Entity/TimeEntry.php @@ -0,0 +1,206 @@ + ['time_entry:read']], + denormalizationContext: ['groups' => ['time_entry:write']], + order: ['startedAt' => 'DESC'], +)] +#[ApiFilter(SearchFilter::class, properties: ['user' => 'exact', 'project' => 'exact', 'types' => 'exact'])] +#[ApiFilter(DateFilter::class, properties: ['startedAt'])] +#[ORM\Entity(repositoryClass: TimeEntryRepository::class)] +#[ORM\UniqueConstraint(name: 'uniq_active_timer', columns: ['user_id'], options: ['where' => '(stopped_at IS NULL)'])] +class TimeEntry +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + #[Groups(['time_entry:read'])] + private ?int $id = null; + + #[ORM\Column(length: 255, nullable: true)] + #[Groups(['time_entry:read', 'time_entry:write'])] + private ?string $title = null; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + #[Groups(['time_entry:read', 'time_entry:write'])] + private ?string $description = null; + + #[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE)] + #[Groups(['time_entry:read', 'time_entry:write'])] + private ?DateTimeImmutable $startedAt = null; + + #[ORM\Column(type: Types::DATETIMETZ_IMMUTABLE, nullable: true)] + #[Groups(['time_entry:read', 'time_entry:write'])] + private ?DateTimeImmutable $stoppedAt = null; + + #[ORM\ManyToOne(targetEntity: User::class)] + #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] + #[Groups(['time_entry:read', 'time_entry:write'])] + private ?User $user = null; + + #[ORM\ManyToOne(targetEntity: Project::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['time_entry:read', 'time_entry:write'])] + private ?Project $project = null; + + #[ORM\ManyToOne(targetEntity: Task::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['time_entry:read', 'time_entry:write'])] + private ?Task $task = null; + + /** @var Collection */ + #[ORM\ManyToMany(targetEntity: TaskType::class)] + #[ORM\JoinTable(name: 'time_entry_task_type')] + #[Groups(['time_entry:read', 'time_entry:write'])] + private Collection $types; + + public function __construct() + { + $this->types = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): static + { + $this->title = $title; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setDescription(?string $description): static + { + $this->description = $description; + + return $this; + } + + public function getStartedAt(): ?DateTimeImmutable + { + return $this->startedAt; + } + + public function setStartedAt(DateTimeImmutable $startedAt): static + { + $this->startedAt = $startedAt; + + return $this; + } + + public function getStoppedAt(): ?DateTimeImmutable + { + return $this->stoppedAt; + } + + public function setStoppedAt(?DateTimeImmutable $stoppedAt): static + { + $this->stoppedAt = $stoppedAt; + + return $this; + } + + public function getUser(): ?User + { + return $this->user; + } + + public function setUser(?User $user): static + { + $this->user = $user; + + return $this; + } + + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): static + { + $this->project = $project; + + return $this; + } + + public function getTask(): ?Task + { + return $this->task; + } + + public function setTask(?Task $task): static + { + $this->task = $task; + + return $this; + } + + /** @return Collection */ + public function getTypes(): Collection + { + return $this->types; + } + + public function addType(TaskType $type): static + { + if (!$this->types->contains($type)) { + $this->types->add($type); + } + + return $this; + } + + public function removeType(TaskType $type): static + { + $this->types->removeElement($type); + + return $this; + } +} diff --git a/src/Repository/TimeEntryRepository.php b/src/Repository/TimeEntryRepository.php new file mode 100644 index 0000000..5e7bcf9 --- /dev/null +++ b/src/Repository/TimeEntryRepository.php @@ -0,0 +1,29 @@ + + */ +class TimeEntryRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, TimeEntry::class); + } + + public function findActiveByUser(User $user): ?TimeEntry + { + return $this->findOneBy([ + 'user' => $user, + 'stoppedAt' => null, + ]); + } +}