diff --git a/migrations/Version20260316124157.php b/migrations/Version20260316124157.php new file mode 100644 index 0000000..838ea79 --- /dev/null +++ b/migrations/Version20260316124157.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE time_entry ADD client_ticket_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE time_entry ADD CONSTRAINT FK_6E537C0C9B2097DD FOREIGN KEY (client_ticket_id) REFERENCES client_ticket (id) ON DELETE SET NULL NOT DEFERRABLE'); + $this->addSql('CREATE INDEX IDX_6E537C0C9B2097DD ON time_entry (client_ticket_id)'); + } + + 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_6E537C0C9B2097DD'); + $this->addSql('DROP INDEX IDX_6E537C0C9B2097DD'); + $this->addSql('ALTER TABLE time_entry DROP client_ticket_id'); + } +} diff --git a/src/Entity/TimeEntry.php b/src/Entity/TimeEntry.php index 2c5332d..3f4449f 100644 --- a/src/Entity/TimeEntry.php +++ b/src/Entity/TimeEntry.php @@ -85,6 +85,11 @@ class TimeEntry #[Groups(['time_entry:read', 'time_entry:write'])] private ?Task $task = null; + #[ORM\ManyToOne(targetEntity: ClientTicket::class)] + #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] + #[Groups(['time_entry:read', 'time_entry:write'])] + private ?ClientTicket $clientTicket = null; + /** @var Collection */ #[ORM\ManyToMany(targetEntity: TaskTag::class)] #[ORM\JoinTable( @@ -189,6 +194,18 @@ class TimeEntry return $this; } + public function getClientTicket(): ?ClientTicket + { + return $this->clientTicket; + } + + public function setClientTicket(?ClientTicket $clientTicket): static + { + $this->clientTicket = $clientTicket; + + return $this; + } + /** @return Collection */ public function getTags(): Collection { diff --git a/src/Mcp/Tool/Serializer.php b/src/Mcp/Tool/Serializer.php index a4d4608..cd101ba 100644 --- a/src/Mcp/Tool/Serializer.php +++ b/src/Mcp/Tool/Serializer.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Mcp\Tool; +use App\Entity\ClientTicket; use App\Entity\Project; use App\Entity\Task; use App\Entity\TaskDocument; @@ -239,22 +240,39 @@ final class Serializer ]; } + /** + * @return null|array{id: ?int, number: ?int, title: ?string} + */ + public static function clientTicketRef(?ClientTicket $ticket): ?array + { + if (null === $ticket) { + return null; + } + + return [ + 'id' => $ticket->getId(), + 'number' => $ticket->getNumber(), + 'title' => $ticket->getTitle(), + ]; + } + /** * @return array */ public static function timeEntry(TimeEntry $entry): array { return [ - 'id' => $entry->getId(), - 'title' => $entry->getTitle(), - 'description' => $entry->getDescription(), - 'startedAt' => $entry->getStartedAt()?->format('c'), - 'stoppedAt' => $entry->getStoppedAt()?->format('c'), - 'duration' => self::durationMinutes($entry), - 'user' => self::user($entry->getUser()), - 'project' => $entry->getProject() ? self::projectRef($entry->getProject()) : null, - 'task' => self::taskRef($entry->getTask()), - 'tags' => self::tags($entry->getTags()), + 'id' => $entry->getId(), + 'title' => $entry->getTitle(), + 'description' => $entry->getDescription(), + 'startedAt' => $entry->getStartedAt()?->format('c'), + 'stoppedAt' => $entry->getStoppedAt()?->format('c'), + 'duration' => self::durationMinutes($entry), + 'user' => self::user($entry->getUser()), + 'project' => $entry->getProject() ? self::projectRef($entry->getProject()) : null, + 'task' => self::taskRef($entry->getTask()), + 'clientTicket' => self::clientTicketRef($entry->getClientTicket()), + 'tags' => self::tags($entry->getTags()), ]; } diff --git a/src/Mcp/Tool/TimeEntry/CreateTimeEntryTool.php b/src/Mcp/Tool/TimeEntry/CreateTimeEntryTool.php index 94ce4d6..3d14def 100644 --- a/src/Mcp/Tool/TimeEntry/CreateTimeEntryTool.php +++ b/src/Mcp/Tool/TimeEntry/CreateTimeEntryTool.php @@ -6,6 +6,7 @@ namespace App\Mcp\Tool\TimeEntry; use App\Entity\TimeEntry; use App\Mcp\Tool\Serializer; +use App\Repository\ClientTicketRepository; use App\Repository\ProjectRepository; use App\Repository\TaskRepository; use App\Repository\TaskTagRepository; @@ -28,6 +29,7 @@ class CreateTimeEntryTool private readonly TaskRepository $taskRepository, private readonly TaskTagRepository $taskTagRepository, private readonly TimeEntryRepository $timeEntryRepository, + private readonly ClientTicketRepository $clientTicketRepository, ) {} public function __invoke( @@ -39,6 +41,7 @@ class CreateTimeEntryTool ?int $taskId = null, ?array $tagIds = null, ?string $description = null, + ?int $clientTicketId = null, ): string { $user = $this->userRepository->find($userId); if (null === $user) { @@ -80,6 +83,13 @@ class CreateTimeEntryTool } $entry->setTask($task); } + if (null !== $clientTicketId) { + $clientTicket = $this->clientTicketRepository->find($clientTicketId); + if (null === $clientTicket) { + throw new InvalidArgumentException(sprintf('ClientTicket with ID %d not found.', $clientTicketId)); + } + $entry->setClientTicket($clientTicket); + } if (null !== $tagIds) { foreach ($tagIds as $tagId) { $tag = $this->taskTagRepository->find($tagId); diff --git a/src/Mcp/Tool/TimeEntry/ListTimeEntriesTool.php b/src/Mcp/Tool/TimeEntry/ListTimeEntriesTool.php index c949d1e..09980f5 100644 --- a/src/Mcp/Tool/TimeEntry/ListTimeEntriesTool.php +++ b/src/Mcp/Tool/TimeEntry/ListTimeEntriesTool.php @@ -20,6 +20,7 @@ class ListTimeEntriesTool ?int $userId = null, ?int $projectId = null, ?int $taskId = null, + ?int $clientTicketId = null, ?string $startDate = null, ?string $endDate = null, int $limit = 100, @@ -31,6 +32,7 @@ class ListTimeEntriesTool ->leftJoin('te.project', 'p')->addSelect('p') ->leftJoin('te.task', 't')->addSelect('t') ->leftJoin('te.tags', 'tg')->addSelect('tg') + ->leftJoin('te.clientTicket', 'ct')->addSelect('ct') ->orderBy('te.startedAt', 'DESC') ->setMaxResults($limit) ; @@ -44,6 +46,9 @@ class ListTimeEntriesTool if (null !== $taskId) { $qb->andWhere('t.id = :taskId')->setParameter('taskId', $taskId); } + if (null !== $clientTicketId) { + $qb->andWhere('ct.id = :clientTicketId')->setParameter('clientTicketId', $clientTicketId); + } if (null !== $startDate) { $qb->andWhere('te.startedAt >= :startDate') ->setParameter('startDate', new DateTimeImmutable($startDate.' 00:00:00')) diff --git a/src/Mcp/Tool/TimeEntry/UpdateTimeEntryTool.php b/src/Mcp/Tool/TimeEntry/UpdateTimeEntryTool.php index eb3457b..a25f4b0 100644 --- a/src/Mcp/Tool/TimeEntry/UpdateTimeEntryTool.php +++ b/src/Mcp/Tool/TimeEntry/UpdateTimeEntryTool.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Mcp\Tool\TimeEntry; use App\Mcp\Tool\Serializer; +use App\Repository\ClientTicketRepository; use App\Repository\ProjectRepository; use App\Repository\TaskRepository; use App\Repository\TaskTagRepository; @@ -24,6 +25,7 @@ class UpdateTimeEntryTool private readonly ProjectRepository $projectRepository, private readonly TaskRepository $taskRepository, private readonly TaskTagRepository $taskTagRepository, + private readonly ClientTicketRepository $clientTicketRepository, private readonly EntityManagerInterface $entityManager, ) {} @@ -36,6 +38,7 @@ class UpdateTimeEntryTool ?int $taskId = null, ?array $tagIds = null, ?string $description = null, + ?int $clientTicketId = null, ): string { $entry = $this->timeEntryRepository->find($id); @@ -69,6 +72,13 @@ class UpdateTimeEntryTool } $entry->setTask($task); } + if (null !== $clientTicketId) { + $clientTicket = $this->clientTicketRepository->find($clientTicketId); + if (null === $clientTicket) { + throw new InvalidArgumentException(sprintf('ClientTicket with ID %d not found.', $clientTicketId)); + } + $entry->setClientTicket($clientTicket); + } if (null !== $tagIds) { foreach ($entry->getTags()->toArray() as $existingTag) { $entry->removeTag($existingTag);