| @@ -6611,6 +6611,7 @@ components: | |||||
| - description | - description | ||||
| - assignedTo | - assignedTo | ||||
| - dueAt | - dueAt | ||||
| - product | |||||
| - prio | - prio | ||||
| - completed | - completed | ||||
| properties: | properties: | ||||
| @@ -6674,6 +6675,17 @@ components: | |||||
| - supplier | - supplier | ||||
| - service | - service | ||||
| - null | - null | ||||
| product: | |||||
| type: | |||||
| - string | |||||
| - 'null' | |||||
| format: iri-reference | |||||
| example: 'https://example.com/' | |||||
| productName: | |||||
| readOnly: true | |||||
| type: | |||||
| - string | |||||
| - 'null' | |||||
| contact: | contact: | ||||
| 'owl:maxCardinality': 1 | 'owl:maxCardinality': 1 | ||||
| type: | type: | ||||
| @@ -6717,6 +6729,7 @@ components: | |||||
| - description | - description | ||||
| - assignedTo | - assignedTo | ||||
| - dueAt | - dueAt | ||||
| - product | |||||
| - prio | - prio | ||||
| - completed | - completed | ||||
| properties: | properties: | ||||
| @@ -6789,6 +6802,17 @@ components: | |||||
| - supplier | - supplier | ||||
| - service | - service | ||||
| - null | - null | ||||
| product: | |||||
| type: | |||||
| - string | |||||
| - 'null' | |||||
| format: iri-reference | |||||
| example: 'https://example.com/' | |||||
| productName: | |||||
| readOnly: true | |||||
| type: | |||||
| - string | |||||
| - 'null' | |||||
| contact: | contact: | ||||
| 'owl:maxCardinality': 1 | 'owl:maxCardinality': 1 | ||||
| type: | type: | ||||
| @@ -6832,6 +6856,7 @@ components: | |||||
| - description | - description | ||||
| - assignedTo | - assignedTo | ||||
| - dueAt | - dueAt | ||||
| - product | |||||
| - prio | - prio | ||||
| - completed | - completed | ||||
| properties: | properties: | ||||
| @@ -6918,6 +6943,17 @@ components: | |||||
| - supplier | - supplier | ||||
| - service | - service | ||||
| - null | - null | ||||
| product: | |||||
| type: | |||||
| - string | |||||
| - 'null' | |||||
| format: iri-reference | |||||
| example: 'https://example.com/' | |||||
| productName: | |||||
| readOnly: true | |||||
| type: | |||||
| - string | |||||
| - 'null' | |||||
| contact: | contact: | ||||
| 'owl:maxCardinality': 1 | 'owl:maxCardinality': 1 | ||||
| type: | type: | ||||
| @@ -0,0 +1,35 @@ | |||||
| <?php | |||||
| declare(strict_types=1); | |||||
| namespace DoctrineMigrations; | |||||
| use Doctrine\DBAL\Schema\Schema; | |||||
| use Doctrine\Migrations\AbstractMigration; | |||||
| /** | |||||
| * Auto-generated Migration: Please modify to your needs! | |||||
| */ | |||||
| final class Version20240515100541 extends AbstractMigration | |||||
| { | |||||
| public function getDescription(): string | |||||
| { | |||||
| return ''; | |||||
| } | |||||
| public function up(Schema $schema): void | |||||
| { | |||||
| // this up() migration is auto-generated, please modify it to your needs | |||||
| $this->addSql('ALTER TABLE task ADD product_id INT NOT NULL'); | |||||
| $this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB254584665A FOREIGN KEY (product_id) REFERENCES product (id)'); | |||||
| $this->addSql('CREATE INDEX IDX_527EDB254584665A ON task (product_id)'); | |||||
| } | |||||
| public function down(Schema $schema): void | |||||
| { | |||||
| // this down() migration is auto-generated, please modify it to your needs | |||||
| $this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB254584665A'); | |||||
| $this->addSql('DROP INDEX IDX_527EDB254584665A ON task'); | |||||
| $this->addSql('ALTER TABLE task DROP product_id'); | |||||
| } | |||||
| } | |||||
| @@ -24,6 +24,7 @@ use App\State\EntityClassDtoStateProcessor; | |||||
| use App\State\EntityToDtoStateProvider; | use App\State\EntityToDtoStateProvider; | ||||
| use Symfony\Component\PropertyInfo\Type; | use Symfony\Component\PropertyInfo\Type; | ||||
| use Symfony\Component\Validator\Constraints as Assert; | use Symfony\Component\Validator\Constraints as Assert; | ||||
| use Symfony\Component\Validator\Constraints\NotBlank; | |||||
| #[ApiResource( | #[ApiResource( | ||||
| shortName: 'Task', | shortName: 'Task', | ||||
| @@ -90,6 +91,13 @@ class TaskApi | |||||
| #[ApiProperty(writable: false)] | #[ApiProperty(writable: false)] | ||||
| public ?PartnerType $partnerType = null; | public ?PartnerType $partnerType = null; | ||||
| #[ApiProperty(writable: true)] | |||||
| #[NotBlank] | |||||
| public ?ProductApi $product = null; | |||||
| #[ApiProperty(writable: false)] | |||||
| public ?string $productName = null; | |||||
| public ?ContactApi $contact = null; | public ?ContactApi $contact = null; | ||||
| #[ApiProperty(writable: false)] | #[ApiProperty(writable: false)] | ||||
| @@ -16,6 +16,9 @@ class Product | |||||
| #[ORM\Column] | #[ORM\Column] | ||||
| private ?int $id = null; | private ?int $id = null; | ||||
| #[ORM\Column(type: "integer", unique: true, nullable: true, options: ["unsigned" => true])] | |||||
| protected int $navisionId; | |||||
| #[ORM\Column(length: 255)] | #[ORM\Column(length: 255)] | ||||
| private ?string $name = null; | private ?string $name = null; | ||||
| @@ -48,6 +51,9 @@ class Product | |||||
| #[ORM\OneToMany(mappedBy: 'product', targetEntity: UserProduct::class)] | #[ORM\OneToMany(mappedBy: 'product', targetEntity: UserProduct::class)] | ||||
| private Collection $userProducts; | private Collection $userProducts; | ||||
| #[ORM\OneToMany(mappedBy: 'product', targetEntity: Task::class)] | |||||
| private Collection $tasks; | |||||
| public function __construct(User $createdBy) | public function __construct(User $createdBy) | ||||
| { | { | ||||
| $this->createdBy = $createdBy; | $this->createdBy = $createdBy; | ||||
| @@ -57,6 +63,7 @@ class Product | |||||
| $this->postings = new ArrayCollection(); | $this->postings = new ArrayCollection(); | ||||
| $this->partnerProducts = new ArrayCollection(); | $this->partnerProducts = new ArrayCollection(); | ||||
| $this->userProducts = new ArrayCollection(); | $this->userProducts = new ArrayCollection(); | ||||
| $this->tasks = new ArrayCollection(); | |||||
| } | } | ||||
| public function getId(): ?int | public function getId(): ?int | ||||
| @@ -259,4 +266,34 @@ class Product | |||||
| return $this; | return $this; | ||||
| } | } | ||||
| /** | |||||
| * @return Collection<int, Task> | |||||
| */ | |||||
| public function getTasks(): Collection | |||||
| { | |||||
| return $this->tasks; | |||||
| } | |||||
| public function addTask(Task $task): static | |||||
| { | |||||
| if (!$this->tasks->contains($task)) { | |||||
| $this->tasks->add($task); | |||||
| $task->setProduct($this); | |||||
| } | |||||
| return $this; | |||||
| } | |||||
| public function removeTask(Task $task): static | |||||
| { | |||||
| if ($this->tasks->removeElement($task)) { | |||||
| // set the owning side to null (unless already changed) | |||||
| if ($task->getProduct() === $this) { | |||||
| $task->setProduct(null); | |||||
| } | |||||
| } | |||||
| return $this; | |||||
| } | |||||
| } | } | ||||
| @@ -34,6 +34,10 @@ class Task | |||||
| #[ORM\Column] | #[ORM\Column] | ||||
| private ?\DateTimeImmutable $dueAt = null; | private ?\DateTimeImmutable $dueAt = null; | ||||
| #[ORM\ManyToOne(inversedBy: 'tasks')] | |||||
| #[ORM\JoinColumn(nullable: false)] | |||||
| private ?Product $product = null; | |||||
| #[ORM\ManyToOne] | #[ORM\ManyToOne] | ||||
| #[ORM\JoinColumn(nullable: true, onDelete: "CASCADE")] | #[ORM\JoinColumn(nullable: true, onDelete: "CASCADE")] | ||||
| private ?Partner $partner = null; | private ?Partner $partner = null; | ||||
| @@ -54,10 +58,11 @@ class Task | |||||
| #[ORM\OneToMany(mappedBy: 'task', targetEntity: TaskNote::class)] | #[ORM\OneToMany(mappedBy: 'task', targetEntity: TaskNote::class)] | ||||
| private Collection $taskNotes; | private Collection $taskNotes; | ||||
| public function __construct(User $createdBy, User $assignedTo) | |||||
| public function __construct(User $createdBy, User $assignedTo, Product $product) | |||||
| { | { | ||||
| $this->createdBy = $createdBy; | $this->createdBy = $createdBy; | ||||
| $this->assignedTo = $assignedTo; | $this->assignedTo = $assignedTo; | ||||
| $this->product = $product; | |||||
| $this->createdAt = new \DateTimeImmutable(); | $this->createdAt = new \DateTimeImmutable(); | ||||
| $this->taskNotes = new ArrayCollection(); | $this->taskNotes = new ArrayCollection(); | ||||
| } | } | ||||
| @@ -167,4 +172,16 @@ class Task | |||||
| return $this->taskNotes; | return $this->taskNotes; | ||||
| } | } | ||||
| public function getProduct(): ?Product | |||||
| { | |||||
| return $this->product; | |||||
| } | |||||
| public function setProduct(?Product $product): static | |||||
| { | |||||
| $this->product = $product; | |||||
| return $this; | |||||
| } | |||||
| } | } | ||||
| @@ -54,6 +54,7 @@ final class TaskFactory extends ModelFactory | |||||
| 'description' => self::faker()->sentence(), | 'description' => self::faker()->sentence(), | ||||
| 'createdBy' => UserFactory::random(), | 'createdBy' => UserFactory::random(), | ||||
| 'assignedTo' => UserFactory::random(), | 'assignedTo' => UserFactory::random(), | ||||
| 'product' => ProductFactory::random(), | |||||
| 'partner' => PartnerFactory::random(), | 'partner' => PartnerFactory::random(), | ||||
| 'contact' => $randomBoolean ? ContactFactory::random() : null, | 'contact' => $randomBoolean ? ContactFactory::random() : null, | ||||
| 'dueAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()), | 'dueAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()), | ||||
| @@ -7,6 +7,7 @@ use App\ApiResource\PartnerApi; | |||||
| use App\ApiResource\TaskApi; | use App\ApiResource\TaskApi; | ||||
| use App\Entity\Contact; | use App\Entity\Contact; | ||||
| use App\Entity\Partner; | use App\Entity\Partner; | ||||
| use App\Entity\Product; | |||||
| use App\Entity\Task; | use App\Entity\Task; | ||||
| use App\Entity\User; | use App\Entity\User; | ||||
| use App\Repository\TaskRepository; | use App\Repository\TaskRepository; | ||||
| @@ -44,9 +45,17 @@ class TaskApiToEntityMapper implements MapperInterface | |||||
| $entity->setAssignedTo($assignedTo); | $entity->setAssignedTo($assignedTo); | ||||
| } else { | } else { | ||||
| $user = $this->security->getUser(); | $user = $this->security->getUser(); | ||||
| if ($dto->product === null) { | |||||
| throw new \Exception('Product missing'); | |||||
| } | |||||
| $product = $this->microMapper->map($dto->product, Product::class, [ | |||||
| MicroMapperInterface::MAX_DEPTH => 1, | |||||
| ]); | |||||
| assert($product instanceof Product); | |||||
| assert($user instanceof User); | assert($user instanceof User); | ||||
| assert($assignedTo instanceof User); | assert($assignedTo instanceof User); | ||||
| $entity = new Task($user, $assignedTo); | |||||
| $entity = new Task($user, $assignedTo, $product); | |||||
| } | } | ||||
| if (!$entity) { | if (!$entity) { | ||||
| @@ -5,6 +5,7 @@ namespace App\Mapper; | |||||
| use App\ApiResource\CommentApi; | use App\ApiResource\CommentApi; | ||||
| use App\ApiResource\ContactApi; | use App\ApiResource\ContactApi; | ||||
| use App\ApiResource\PartnerApi; | use App\ApiResource\PartnerApi; | ||||
| use App\ApiResource\ProductApi; | |||||
| use App\ApiResource\TaskApi; | use App\ApiResource\TaskApi; | ||||
| use App\ApiResource\TaskNoteApi; | use App\ApiResource\TaskNoteApi; | ||||
| use App\ApiResource\UserApi; | use App\ApiResource\UserApi; | ||||
| @@ -72,6 +73,11 @@ class TaskEntityToApiMapper implements MapperInterface | |||||
| $dto->contactName = $entity->getContact()?->getFirstName()." ".$entity->getContact()?->getLastName(); | $dto->contactName = $entity->getContact()?->getFirstName()." ".$entity->getContact()?->getLastName(); | ||||
| } | } | ||||
| $dto->product = $this->microMapper->map($entity->getProduct(), ProductApi::class, [ | |||||
| MicroMapperInterface::MAX_DEPTH => 1, | |||||
| ]); | |||||
| $dto->productName = $entity->getProduct()?->getName(); | |||||
| $dto->taskNotes = array_map(function(TaskNote $taskNote) { | $dto->taskNotes = array_map(function(TaskNote $taskNote) { | ||||
| return $this->microMapper->map($taskNote, TaskNoteApi::class, [ | return $this->microMapper->map($taskNote, TaskNoteApi::class, [ | ||||
| MicroMapperInterface::MAX_DEPTH => 1, | MicroMapperInterface::MAX_DEPTH => 1, | ||||