diff --git a/migrations/Version20240318162352.php b/migrations/Version20240318162352.php deleted file mode 100644 index 1b6f5a8..0000000 --- a/migrations/Version20240318162352.php +++ /dev/null @@ -1,73 +0,0 @@ -addSql('CREATE TABLE comment (id INT AUTO_INCREMENT NOT NULL, owner_id INT NOT NULL, posting_id INT NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_9474526C7E3C61F9 (owner_id), INDEX IDX_9474526C9AE985F6 (posting_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('CREATE TABLE contact (id INT AUTO_INCREMENT NOT NULL, partner_id INT NOT NULL, image_id INT DEFAULT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, birthday DATE DEFAULT NULL, position VARCHAR(255) DEFAULT NULL, phone VARCHAR(255) DEFAULT NULL, email VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_4C62E6389393F8FE (partner_id), INDEX IDX_4C62E6383DA5256D (image_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('CREATE TABLE media_object (id INT AUTO_INCREMENT NOT NULL, file_path VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('CREATE TABLE partner (id INT AUTO_INCREMENT NOT NULL, logo_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, street VARCHAR(255) DEFAULT NULL, street_no VARCHAR(255) DEFAULT NULL, zip VARCHAR(255) DEFAULT NULL, city VARCHAR(255) DEFAULT NULL, country VARCHAR(255) DEFAULT NULL, website VARCHAR(255) DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_312B3E16F98F144A (logo_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('CREATE TABLE posting (id INT AUTO_INCREMENT NOT NULL, owner_id INT NOT NULL, partner_id INT NOT NULL, contact_id INT DEFAULT NULL, headline VARCHAR(255) NOT NULL, message LONGTEXT NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_BD275D737E3C61F9 (owner_id), INDEX IDX_BD275D739393F8FE (partner_id), INDEX IDX_BD275D73E7A1254A (contact_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('CREATE TABLE product (id INT AUTO_INCREMENT NOT NULL, image_id INT DEFAULT NULL, name VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_D34A04AD3DA5256D (image_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('CREATE TABLE task (id INT AUTO_INCREMENT NOT NULL, created_by_id INT NOT NULL, assigned_to_id INT NOT NULL, partner_id INT DEFAULT NULL, contact_id INT DEFAULT NULL, headline VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, due_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', prio VARCHAR(255) NOT NULL, completed TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_527EDB25B03A8386 (created_by_id), INDEX IDX_527EDB25F4BD7827 (assigned_to_id), INDEX IDX_527EDB259393F8FE (partner_id), INDEX IDX_527EDB25E7A1254A (contact_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, image_id INT DEFAULT NULL, email VARCHAR(180) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, roles JSON NOT NULL COMMENT \'(DC2Type:json)\', password VARCHAR(255) NOT NULL, active TINYINT(1) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), INDEX IDX_8D93D6493DA5256D (image_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); - $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C7E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id)'); - $this->addSql('ALTER TABLE comment ADD CONSTRAINT FK_9474526C9AE985F6 FOREIGN KEY (posting_id) REFERENCES posting (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE contact ADD CONSTRAINT FK_4C62E6389393F8FE FOREIGN KEY (partner_id) REFERENCES partner (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE contact ADD CONSTRAINT FK_4C62E6383DA5256D FOREIGN KEY (image_id) REFERENCES media_object (id) ON DELETE SET NULL'); - $this->addSql('ALTER TABLE partner ADD CONSTRAINT FK_312B3E16F98F144A FOREIGN KEY (logo_id) REFERENCES media_object (id) ON DELETE SET NULL'); - $this->addSql('ALTER TABLE posting ADD CONSTRAINT FK_BD275D737E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE posting ADD CONSTRAINT FK_BD275D739393F8FE FOREIGN KEY (partner_id) REFERENCES partner (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE posting ADD CONSTRAINT FK_BD275D73E7A1254A FOREIGN KEY (contact_id) REFERENCES contact (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE product ADD CONSTRAINT FK_D34A04AD3DA5256D FOREIGN KEY (image_id) REFERENCES media_object (id) ON DELETE SET NULL'); - $this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB25B03A8386 FOREIGN KEY (created_by_id) REFERENCES `user` (id)'); - $this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB25F4BD7827 FOREIGN KEY (assigned_to_id) REFERENCES `user` (id)'); - $this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB259393F8FE FOREIGN KEY (partner_id) REFERENCES partner (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB25E7A1254A FOREIGN KEY (contact_id) REFERENCES contact (id) ON DELETE CASCADE'); - $this->addSql('ALTER TABLE `user` ADD CONSTRAINT FK_8D93D6493DA5256D FOREIGN KEY (image_id) REFERENCES media_object (id) ON DELETE SET NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526C7E3C61F9'); - $this->addSql('ALTER TABLE comment DROP FOREIGN KEY FK_9474526C9AE985F6'); - $this->addSql('ALTER TABLE contact DROP FOREIGN KEY FK_4C62E6389393F8FE'); - $this->addSql('ALTER TABLE contact DROP FOREIGN KEY FK_4C62E6383DA5256D'); - $this->addSql('ALTER TABLE partner DROP FOREIGN KEY FK_312B3E16F98F144A'); - $this->addSql('ALTER TABLE posting DROP FOREIGN KEY FK_BD275D737E3C61F9'); - $this->addSql('ALTER TABLE posting DROP FOREIGN KEY FK_BD275D739393F8FE'); - $this->addSql('ALTER TABLE posting DROP FOREIGN KEY FK_BD275D73E7A1254A'); - $this->addSql('ALTER TABLE product DROP FOREIGN KEY FK_D34A04AD3DA5256D'); - $this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB25B03A8386'); - $this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB25F4BD7827'); - $this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB259393F8FE'); - $this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB25E7A1254A'); - $this->addSql('ALTER TABLE `user` DROP FOREIGN KEY FK_8D93D6493DA5256D'); - $this->addSql('DROP TABLE comment'); - $this->addSql('DROP TABLE contact'); - $this->addSql('DROP TABLE media_object'); - $this->addSql('DROP TABLE partner'); - $this->addSql('DROP TABLE posting'); - $this->addSql('DROP TABLE product'); - $this->addSql('DROP TABLE task'); - $this->addSql('DROP TABLE `user`'); - } -} diff --git a/migrations/Version20240319114858.php b/migrations/Version20240319114858.php new file mode 100644 index 0000000..5c50357 --- /dev/null +++ b/migrations/Version20240319114858.php @@ -0,0 +1,49 @@ +addSql('ALTER TABLE sale ADD CONSTRAINT FK_E54BC0057E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE sale ADD CONSTRAINT FK_E54BC0059393F8FE FOREIGN KEY (partner_id) REFERENCES partner (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE sale ADD CONSTRAINT FK_E54BC0054584665A FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB25B03A8386 FOREIGN KEY (created_by_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB25F4BD7827 FOREIGN KEY (assigned_to_id) REFERENCES `user` (id)'); + $this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB259393F8FE FOREIGN KEY (partner_id) REFERENCES partner (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB25E7A1254A FOREIGN KEY (contact_id) REFERENCES contact (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE task_note ADD CONSTRAINT FK_BC0E6E6F7E3C61F9 FOREIGN KEY (owner_id) REFERENCES `user` (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE task_note ADD CONSTRAINT FK_BC0E6E6F8DB60186 FOREIGN KEY (task_id) REFERENCES task (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE user ADD CONSTRAINT FK_8D93D6493DA5256D FOREIGN KEY (image_id) REFERENCES media_object (id) ON DELETE SET NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE task_note DROP FOREIGN KEY FK_BC0E6E6F7E3C61F9'); + $this->addSql('ALTER TABLE task_note DROP FOREIGN KEY FK_BC0E6E6F8DB60186'); + $this->addSql('ALTER TABLE sale DROP FOREIGN KEY FK_E54BC0057E3C61F9'); + $this->addSql('ALTER TABLE sale DROP FOREIGN KEY FK_E54BC0059393F8FE'); + $this->addSql('ALTER TABLE sale DROP FOREIGN KEY FK_E54BC0054584665A'); + $this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB25B03A8386'); + $this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB25F4BD7827'); + $this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB259393F8FE'); + $this->addSql('ALTER TABLE task DROP FOREIGN KEY FK_527EDB25E7A1254A'); + $this->addSql('ALTER TABLE `user` DROP FOREIGN KEY FK_8D93D6493DA5256D'); + } +} diff --git a/src/ApiResource/CommentApi.php b/src/ApiResource/CommentApi.php index 254f01b..9093205 100644 --- a/src/ApiResource/CommentApi.php +++ b/src/ApiResource/CommentApi.php @@ -13,6 +13,7 @@ use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use App\Entity\Comment; +use App\Interface\OwnerInterface; use App\State\EntityClassDtoStateProcessor; use App\State\EntityToDtoStateProvider; use ApiPlatform\Metadata\Delete; @@ -30,7 +31,9 @@ use Symfony\Component\Validator\Constraints\NotBlank; new Get( security: 'is_granted("ROLE_USER")' ), - new GetCollection(), + new GetCollection( + security: 'is_granted("ROLE_USER")', + ), new Post( security: 'is_granted("ROLE_USER")', ), @@ -47,7 +50,7 @@ use Symfony\Component\Validator\Constraints\NotBlank; stateOptions: new Options(entityClass: Comment::class), )] #[ApiFilter(SearchFilter::class, properties: ['partner' => 'exact', 'contact' => 'exact'])] -class CommentApi +class CommentApi implements OwnerInterface { #[ApiProperty(readable: false, writable: false, identifier: true)] public ?int $id = null; @@ -66,4 +69,8 @@ class CommentApi #[ApiProperty(writable: false)] public ?\DateTimeImmutable $createdAt = null; + public function getOwner(): UserApi + { + return $this->owner; + } } \ No newline at end of file diff --git a/src/ApiResource/PartnerApi.php b/src/ApiResource/PartnerApi.php index 03cdb3f..5c877e5 100644 --- a/src/ApiResource/PartnerApi.php +++ b/src/ApiResource/PartnerApi.php @@ -83,7 +83,7 @@ class PartnerApi public ?\DateTimeImmutable $createdAt = null; /** - * @var array + * @var $posts array */ #[ApiProperty(writable: false)] public array $posts = []; diff --git a/src/ApiResource/PostingApi.php b/src/ApiResource/PostingApi.php index b77edd3..fa45590 100644 --- a/src/ApiResource/PostingApi.php +++ b/src/ApiResource/PostingApi.php @@ -13,6 +13,7 @@ use ApiPlatform\Metadata\ApiFilter; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use App\Entity\Posting; +use App\Interface\OwnerInterface; use App\State\EntityClassDtoStateProcessor; use App\State\EntityToDtoStateProvider; use ApiPlatform\Metadata\Delete; @@ -53,7 +54,7 @@ use Symfony\Component\Validator\Constraints\NotBlank; stateOptions: new Options(entityClass: Posting::class) )] #[ApiFilter(SearchFilter::class, properties: ['partner' => 'exact', 'contact' => 'exact'])] -class PostingApi +class PostingApi implements OwnerInterface { #[ApiProperty(readable: false, writable: false, identifier: true)] public ?int $id = null; @@ -81,6 +82,10 @@ class PostingApi #[Groups(['posting:create'])] public ?ContactApi $contact = null; + #[ApiProperty(writable: true)] + #[Groups(['posting:create'])] + public ?SaleApi $sale = null; + /** * @var $comments array */ @@ -101,4 +106,8 @@ class PostingApi #[ApiProperty(writable: false)] public ?\DateTimeImmutable $createdAt = null; + public function getOwner(): UserApi + { + return $this->owner; + } } \ No newline at end of file diff --git a/src/ApiResource/SaleApi.php b/src/ApiResource/SaleApi.php new file mode 100644 index 0000000..8f29b68 --- /dev/null +++ b/src/ApiResource/SaleApi.php @@ -0,0 +1,92 @@ + + * @date 12.12.23 + */ + + +namespace App\ApiResource; + +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use App\Entity\Sale; +use App\Interface\OwnerInterface; +use App\State\EntityClassDtoStateProcessor; +use App\State\EntityToDtoStateProvider; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use Symfony\Component\Validator\Constraints\NotBlank; + +#[ApiResource( + shortName: 'Sale', + operations: [ + new Get( + security: 'is_granted("ROLE_USER")' + ), + new GetCollection(), + new Post( + security: 'is_granted("ROLE_USER")', + ), + new Patch( + security: 'is_granted("EDIT", object)', + ), + new Delete( + security: 'is_granted("ROLE_ADMIN")', + ) + ], + security: 'is_granted("ROLE_USER")', + provider: EntityToDtoStateProvider::class, + processor: EntityClassDtoStateProcessor::class, + stateOptions: new Options(entityClass: Sale::class), +)] +#[ApiFilter(SearchFilter::class, properties: ['owner' => 'exact', 'partner' => 'exact'])] +class SaleApi implements OwnerInterface +{ + #[ApiProperty(readable: false, writable: false, identifier: true)] + public ?int $id = null; + + #[ApiProperty(writable: false)] + public ?UserApi $owner = null; + + #[ApiProperty(writable: false)] + public ?string $ownerName = null; + + #[ApiProperty(writable: true)] + public ?PartnerApi $partner = null; + + #[ApiProperty(writable: false)] + public ?string $partnerName = null; + + #[ApiProperty(writable: true)] + public ?ProductApi $product = null; + + #[ApiProperty(writable: false)] + public ?string $productName = null; + + #[NotBlank] + public ?int $turnover = null; + + public ?int $profit = null; + + public ?string $comment = null; + + #[ApiProperty(writable: false)] + public ?\DateTimeImmutable $createdAt = null; + + /** + * @var $posts array + */ + #[ApiProperty(writable: false)] + public array $posts = []; + + public function getOwner(): UserApi + { + return $this->owner; + } +} \ No newline at end of file diff --git a/src/ApiResource/SaleSummary.php b/src/ApiResource/SaleSummary.php new file mode 100644 index 0000000..9479fb7 --- /dev/null +++ b/src/ApiResource/SaleSummary.php @@ -0,0 +1,47 @@ + + * @date 12.12.23 + */ + + +namespace App\ApiResource; + +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use App\Entity\Comment; +use App\Interface\OwnerInterface; +use App\State\EntityClassDtoStateProcessor; +use App\State\EntityToDtoStateProvider; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use App\State\SaleSummaryStateProvider; +use App\Validator\IsValidOwner; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Attribute\Groups; +use Symfony\Component\Validator\Constraints\NotBlank; + +#[ApiResource( + shortName: 'SaleSummary', + operations: [ + new GetCollection(), + ], + security: 'is_granted("ROLE_USER")', + provider: SaleSummaryStateProvider::class, +)] +class SaleSummary +{ + public ?UserApi $owner = null; + + public ?string $ownerName = null; + + public ?int $turnover = null; + + public ?int $profit = null; +} \ No newline at end of file diff --git a/src/ApiResource/TaskApi.php b/src/ApiResource/TaskApi.php index 7f9d24f..fa12dea 100644 --- a/src/ApiResource/TaskApi.php +++ b/src/ApiResource/TaskApi.php @@ -21,6 +21,7 @@ use App\Entity\Task; use App\Enum\PrioType; use App\State\EntityClassDtoStateProcessor; use App\State\EntityToDtoStateProvider; +use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Validator\Constraints as Assert; #[ApiResource( @@ -49,7 +50,11 @@ use Symfony\Component\Validator\Constraints as Assert; processor: EntityClassDtoStateProcessor::class, stateOptions: new Options(entityClass: Task::class), )] -#[ApiFilter(SearchFilter::class, properties: ['partner' => 'exact', 'contact' => 'exact'])] +#[ApiFilter(SearchFilter::class, properties: [ + 'owner' => 'exact', + 'partner' => 'exact', + 'contact' => 'exact' +])] class TaskApi { #[ApiProperty(readable: false, writable: false, identifier: true)] @@ -89,8 +94,26 @@ class TaskApi #[Assert\NotBlank] public PrioType $prio; + #[Assert\NotNull] public ?bool $completed = null; + /** + * @var $taskNotes array + */ + #[ApiProperty( + readableLink: true, + writableLink: true, + builtinTypes: [ + new Type( + 'object', + collection: true, + collectionKeyType: [new Type('int')], + collectionValueType: new Type('object', class: TaskNoteApi::class) + ) + ] + )] + public array $taskNotes = []; + #[ApiProperty(writable: false)] public ?\DateTimeImmutable $createdAt = null; diff --git a/src/ApiResource/TaskNoteApi.php b/src/ApiResource/TaskNoteApi.php new file mode 100644 index 0000000..10a8b6e --- /dev/null +++ b/src/ApiResource/TaskNoteApi.php @@ -0,0 +1,72 @@ + + * @date 12.12.23 + */ + + +namespace App\ApiResource; + +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use App\Entity\TaskNote; +use App\Interface\OwnerInterface; +use App\State\EntityClassDtoStateProcessor; +use App\State\EntityToDtoStateProvider; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use Symfony\Component\Validator\Constraints\NotBlank; + +#[ApiResource( + shortName: 'TaskNote', + operations: [ + new Get( + security: 'is_granted("ROLE_USER")' + ), + new GetCollection(), + new Post( + security: 'is_granted("ROLE_USER")', + ), + new Patch( + security: 'is_granted("EDIT", object)', + ), + new Delete( + security: 'is_granted("ROLE_ADMIN")', + ) + ], + security: 'is_granted("ROLE_USER")', + provider: EntityToDtoStateProvider::class, + processor: EntityClassDtoStateProcessor::class, + stateOptions: new Options(entityClass: TaskNote::class), +)] +#[ApiFilter(SearchFilter::class, properties: ['task' => 'exact'])] +class TaskNoteApi implements OwnerInterface +{ + #[ApiProperty(readable: false, writable: false, identifier: true)] + public ?int $id = null; + + #[NotBlank] + public ?string $message = null; + + #[ApiProperty(writable: false)] + public ?UserApi $owner = null; + + #[ApiProperty(writable: false)] + public ?string $ownerName = null; + + public ?TaskApi $task = null; + + #[ApiProperty(writable: false)] + public ?\DateTimeImmutable $createdAt = null; + + public function getOwner(): UserApi + { + return $this->owner; + } +} \ No newline at end of file diff --git a/src/ApiResource/UserApi.php b/src/ApiResource/UserApi.php index bf05def..a142411 100644 --- a/src/ApiResource/UserApi.php +++ b/src/ApiResource/UserApi.php @@ -16,6 +16,7 @@ use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use App\Entity\MediaObject; use App\Entity\User; +use App\Interface\AdminOrOwnerInterface; use App\State\EntityClassDtoStateProcessor; use App\State\EntityToDtoStateProvider; use Symfony\Component\Validator\Constraints as Assert; @@ -76,4 +77,5 @@ class UserApi #[ApiProperty(writable: false)] public ?\DateTimeImmutable $createdAt = null; + } diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index 5178e9b..5bd3881 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -11,7 +11,9 @@ use App\Factory\MediaObjectUserFactory; use App\Factory\PartnerFactory; use App\Factory\PostingFactory; use App\Factory\ProductFactory; +use App\Factory\SaleFactory; use App\Factory\TaskFactory; +use App\Factory\TaskNoteFactory; use App\Factory\UserFactory; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; @@ -64,10 +66,12 @@ class AppFixtures extends Fixture PartnerFactory::createMany(100); MediaObjectContactFactory::createMany(50); ContactFactory::createMany(200); + ProductFactory::createMany(100); + SaleFactory::createMany(50); PostingFactory::createMany(200); CommentFactory::createMany(300); MediaObjectProductFactory::createMany(50); - ProductFactory::createMany(100); TaskFactory::createMany(50); + TaskNoteFactory::createMany(100); } } diff --git a/src/Entity/Partner.php b/src/Entity/Partner.php index b55e088..fa32ecb 100644 --- a/src/Entity/Partner.php +++ b/src/Entity/Partner.php @@ -54,11 +54,15 @@ class Partner #[ORM\OneToMany(mappedBy: 'partner', targetEntity: Posting::class)] private Collection $postings; + #[ORM\OneToMany(mappedBy: 'partner', targetEntity: Sale::class)] + private Collection $sales; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); $this->contacts = new ArrayCollection(); $this->postings = new ArrayCollection(); + $this->sales = new ArrayCollection(); } public function getId(): ?int @@ -206,4 +210,34 @@ class Partner return $this; } + /** + * @return Collection + */ + public function getSales(): Collection + { + return $this->sales; + } + + public function addSale(Sale $sale): static + { + if (!$this->sales->contains($sale)) { + $this->sales->add($sale); + $sale->setPartner($this); + } + + return $this; + } + + public function removeSale(Sale $sale): static + { + if ($this->sales->removeElement($sale)) { + // set the owning side to null (unless already changed) + if ($sale->getPartner() === $this) { + $sale->setPartner(null); + } + } + + return $this; + } + } diff --git a/src/Entity/Posting.php b/src/Entity/Posting.php index 46d1a93..3766f7a 100644 --- a/src/Entity/Posting.php +++ b/src/Entity/Posting.php @@ -34,17 +34,26 @@ class Posting #[ORM\JoinColumn(nullable: true, onDelete: "CASCADE")] private ?Contact $contact = null; + #[ORM\ManyToOne(inversedBy: 'postings')] + #[ORM\JoinColumn(nullable: true, onDelete: "CASCADE")] + private ?Sale $sale = null; + #[ORM\Column] private ?\DateTimeImmutable $createdAt = null; #[ORM\OneToMany(mappedBy: 'posting', targetEntity: Comment::class)] private Collection $comments; - public function __construct(User $owner, Partner $partner, Contact $contact = null) - { + public function __construct( + User $owner, + Partner $partner, + Contact $contact = null, + Sale $sale = null + ) { $this->owner = $owner; $this->partner = $partner; $this->contact = $contact; + $this->sale = $sale; $this->createdAt = new \DateTimeImmutable(); $this->comments = new ArrayCollection(); } @@ -146,4 +155,16 @@ class Posting return $this; } + + public function getSale(): ?Sale + { + return $this->sale; + } + + public function setSale(?Sale $sale): static + { + $this->sale = $sale; + + return $this; + } } diff --git a/src/Entity/Product.php b/src/Entity/Product.php index 09c0855..5894182 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -3,6 +3,8 @@ namespace App\Entity; use App\Repository\ProductRepository; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -27,9 +29,13 @@ class Product #[ORM\Column] private ?\DateTimeImmutable $createdAt = null; + #[ORM\OneToMany(mappedBy: 'product', targetEntity: Sale::class)] + private Collection $sales; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); + $this->sales = new ArrayCollection(); } public function getId(): ?int @@ -77,4 +83,34 @@ class Product { return $this->createdAt; } + + /** + * @return Collection + */ + public function getSales(): Collection + { + return $this->sales; + } + + public function addSale(Sale $sale): static + { + if (!$this->sales->contains($sale)) { + $this->sales->add($sale); + $sale->setProduct($this); + } + + return $this; + } + + public function removeSale(Sale $sale): static + { + if ($this->sales->removeElement($sale)) { + // set the owning side to null (unless already changed) + if ($sale->getProduct() === $this) { + $sale->setProduct(null); + } + } + + return $this; + } } diff --git a/src/Entity/Sale.php b/src/Entity/Sale.php new file mode 100644 index 0000000..3b783a3 --- /dev/null +++ b/src/Entity/Sale.php @@ -0,0 +1,173 @@ +owner = $owner; + $this->partner = $partner; + $this->product = $product; + $this->createdAt = new \DateTimeImmutable(); + $this->postings = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getOwner(): ?User + { + return $this->owner; + } + + public function setOwner(?User $owner): static + { + $this->owner = $owner; + + return $this; + } + + public function getPartner(): ?Partner + { + return $this->partner; + } + + public function setPartner(?Partner $partner): static + { + $this->partner = $partner; + + return $this; + } + + public function getProduct(): ?Product + { + return $this->product; + } + + public function setProduct(?Product $product): static + { + $this->product = $product; + + return $this; + } + + public function getTurnover(): ?int + { + return $this->turnover; + } + + public function setTurnover(int $turnover): static + { + $this->turnover = $turnover; + + return $this; + } + + public function getProfit(): ?int + { + return $this->profit; + } + + public function setProfit(?int $profit): static + { + $this->profit = $profit; + + return $this; + } + + public function getComment(): ?string + { + return $this->comment; + } + + public function setComment(?string $comment): static + { + $this->comment = $comment; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } + + /** + * @return Collection + */ + public function getPostings(): Collection + { + return $this->postings; + } + + public function addPosting(Posting $posting): static + { + if (!$this->postings->contains($posting)) { + $this->postings->add($posting); + $posting->setSale($this); + } + + return $this; + } + + public function removePosting(Posting $posting): static + { + if ($this->postings->removeElement($posting)) { + // set the owning side to null (unless already changed) + if ($posting->getSale() === $this) { + $posting->setSale(null); + } + } + + return $this; + } +} diff --git a/src/Entity/Task.php b/src/Entity/Task.php index ccc9c1b..0b28016 100644 --- a/src/Entity/Task.php +++ b/src/Entity/Task.php @@ -4,6 +4,7 @@ namespace App\Entity; use App\Enum\PrioType; use App\Repository\TaskRepository; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; @@ -43,12 +44,15 @@ class Task #[ORM\Column(type: 'string', enumType: PrioType::class)] private PrioType $prio; - #[ORM\Column] + #[ORM\Column(nullable: false)] private ?bool $completed = null; #[ORM\Column] private ?\DateTimeImmutable $createdAt = null; + #[ORM\OneToMany(mappedBy: 'task', targetEntity: TaskNote::class)] + private Collection $taskNotes; + public function __construct(User $createdBy, User $assignedTo) { $this->createdBy = $createdBy; @@ -156,4 +160,9 @@ class Task return $this->createdAt; } + public function getTaskNotes(): Collection + { + return $this->taskNotes; + } + } diff --git a/src/Entity/TaskNote.php b/src/Entity/TaskNote.php new file mode 100644 index 0000000..9c609c4 --- /dev/null +++ b/src/Entity/TaskNote.php @@ -0,0 +1,90 @@ +owner = $owner; + $this->task = $task; + $this->createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getMessage(): ?string + { + return $this->message; + } + + public function setMessage(string $message): static + { + $this->message = $message; + + return $this; + } + + public function getOwner(): ?User + { + return $this->owner; + } + + public function setOwner(?User $owner): static + { + $this->owner = $owner; + + return $this; + } + + public function getTask(): ?Task + { + return $this->task; + } + + public function setTask(?Task $task): static + { + $this->task = $task; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index 037b7df..f2a63e7 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -52,6 +52,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Comment::class)] private Collection $comments; + #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Sale::class)] + private Collection $sales; + public function __construct() { @@ -59,6 +62,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface $this->postings = new ArrayCollection(); $this->active = true; $this->comments = new ArrayCollection(); + $this->sales = new ArrayCollection(); } public function getId(): ?int @@ -246,4 +250,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + + /** + * @return Collection + */ + public function getSales(): Collection + { + return $this->sales; + } + + public function addSale(Sale $sale): static + { + if (!$this->sales->contains($sale)) { + $this->sales->add($sale); + $sale->setOwner($this); + } + + return $this; + } + + public function removeSale(Sale $sale): static + { + if ($this->sales->removeElement($sale)) { + // set the owning side to null (unless already changed) + if ($sale->getOwner() === $this) { + $sale->setOwner(null); + } + } + + return $this; + } } diff --git a/src/Factory/PostingFactory.php b/src/Factory/PostingFactory.php index e3ec9e0..2408932 100644 --- a/src/Factory/PostingFactory.php +++ b/src/Factory/PostingFactory.php @@ -47,12 +47,14 @@ final class PostingFactory extends ModelFactory protected function getDefaults(): array { $randomBoolean = ContactFactory::count() > 0 && (bool)random_int(0, 1); + $random4 = SaleFactory::count() > 0 && !(bool)random_int(0, 3); return [ 'headline' => self::faker()->words(random_int(1, 5), true), 'message' => $randomBoolean ? self::faker()->sentence() : self::faker()->sentences(random_int(1, 3), true), 'owner' => UserFactory::random(), 'partner' => PartnerFactory::random(), 'contact' => $randomBoolean ? ContactFactory::random() : null, + 'sale' => !$random4 ? SaleFactory::random() : null, ]; } diff --git a/src/Factory/SaleFactory.php b/src/Factory/SaleFactory.php new file mode 100644 index 0000000..1d3e97f --- /dev/null +++ b/src/Factory/SaleFactory.php @@ -0,0 +1,77 @@ + + * + * @method Sale|Proxy create(array|callable $attributes = []) + * @method static Sale|Proxy createOne(array $attributes = []) + * @method static Sale|Proxy find(object|array|mixed $criteria) + * @method static Sale|Proxy findOrCreate(array $attributes) + * @method static Sale|Proxy first(string $sortedField = 'id') + * @method static Sale|Proxy last(string $sortedField = 'id') + * @method static Sale|Proxy random(array $attributes = []) + * @method static Sale|Proxy randomOrCreate(array $attributes = []) + * @method static SaleRepository|RepositoryProxy repository() + * @method static Sale[]|Proxy[] all() + * @method static Sale[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static Sale[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static Sale[]|Proxy[] findBy(array $attributes) + * @method static Sale[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static Sale[]|Proxy[] randomSet(int $number, array $attributes = []) + */ +final class SaleFactory extends ModelFactory +{ + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services + * + * @todo inject services if required + */ + public function __construct() + { + parent::__construct(); + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + * + * @todo add your default values here + */ + protected function getDefaults(): array + { + + $turnover = self::faker()->randomNumber(5); + $randNumber = self::faker()->randomNumber(2); + $profit = (int)($turnover * ($randNumber / 100)); + return [ + 'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()), + 'owner' => UserFactory::random(), + 'partner' => PartnerFactory::random(), + 'product' => ProductFactory::random(), + 'turnover' => $turnover, + 'profit' => $profit + ]; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): self + { + return $this + // ->afterInstantiate(function(Sale $sale): void {}) + ; + } + + protected static function getClass(): string + { + return Sale::class; + } +} diff --git a/src/Factory/TaskNoteFactory.php b/src/Factory/TaskNoteFactory.php new file mode 100644 index 0000000..142bdd0 --- /dev/null +++ b/src/Factory/TaskNoteFactory.php @@ -0,0 +1,71 @@ + + * + * @method TaskNote|Proxy create(array|callable $attributes = []) + * @method static TaskNote|Proxy createOne(array $attributes = []) + * @method static TaskNote|Proxy find(object|array|mixed $criteria) + * @method static TaskNote|Proxy findOrCreate(array $attributes) + * @method static TaskNote|Proxy first(string $sortedField = 'id') + * @method static TaskNote|Proxy last(string $sortedField = 'id') + * @method static TaskNote|Proxy random(array $attributes = []) + * @method static TaskNote|Proxy randomOrCreate(array $attributes = []) + * @method static TaskNoteRepository|RepositoryProxy repository() + * @method static TaskNote[]|Proxy[] all() + * @method static TaskNote[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static TaskNote[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static TaskNote[]|Proxy[] findBy(array $attributes) + * @method static TaskNote[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static TaskNote[]|Proxy[] randomSet(int $number, array $attributes = []) + */ +final class TaskNoteFactory extends ModelFactory +{ + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services + * + * @todo inject services if required + */ + public function __construct() + { + parent::__construct(); + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories + * + * @todo add your default values here + */ + protected function getDefaults(): array + { + return [ + 'createdAt' => \DateTimeImmutable::createFromMutable(self::faker()->dateTime()), + 'message' => self::faker()->text(), + 'owner' => UserFactory::random(), + 'task' => TaskFactory::random(), + ]; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): self + { + return $this + // ->afterInstantiate(function(TaskNote $taskNote): void {}) + ; + } + + protected static function getClass(): string + { + return TaskNote::class; + } +} diff --git a/src/Interface/OwnerInterface.php b/src/Interface/OwnerInterface.php new file mode 100644 index 0000000..61475eb --- /dev/null +++ b/src/Interface/OwnerInterface.php @@ -0,0 +1,8 @@ + 1, ]); } + $sale = null; + if ($dto->sale) { + assert($dto->sale instanceof SaleApi); + $sale = $this->microMapper->map($dto->sale, Sale::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + } assert($partner instanceof Partner); assert($contact === null || $contact instanceof Contact); + assert($sale === null || $sale instanceof Sale); $entity = new Posting($user, $partner, $contact); } diff --git a/src/Mapper/SaleApiToEntityMapper.php b/src/Mapper/SaleApiToEntityMapper.php new file mode 100644 index 0000000..90a913a --- /dev/null +++ b/src/Mapper/SaleApiToEntityMapper.php @@ -0,0 +1,76 @@ +id) { + $entity = $this->repository->find($dto->id); + } else { + $user = $this->security->getUser(); + assert($user instanceof User); + if ($dto->partner === null) { + throw new \Exception('Partner missing'); + } + $partner = $this->microMapper->map($dto->partner, Partner::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + if ($dto->product === null) { + throw new \Exception('Product missing'); + } + $product = $this->microMapper->map($dto->product, Product::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + assert($partner instanceof Partner); + assert($product instanceof Product); + $entity = new Sale($user, $partner, $product); + } + + if (!$entity) { + throw new \Exception('Sale not found'); + } + + return $entity; + } + + public function populate(object $from, object $to, array $context): object + { + $dto = $from; + assert($dto instanceof SaleApi); + $entity = $to; + assert($entity instanceof Sale); + $entity->setTurnover($dto->turnover); + $entity->setProfit($dto->profit); + $entity->setComment($dto->comment); + + return $entity; + } +} diff --git a/src/Mapper/SaleEntityToApiMapper.php b/src/Mapper/SaleEntityToApiMapper.php new file mode 100644 index 0000000..e8efb9b --- /dev/null +++ b/src/Mapper/SaleEntityToApiMapper.php @@ -0,0 +1,74 @@ +id = $entity->getId(); + + return $dto; + } + + public function populate(object $from, object $to, array $context): object + { + $entity = $from; + $dto = $to; + assert($entity instanceof Sale); + assert($dto instanceof SaleApi); + + $dto->owner = $this->microMapper->map($entity->getOwner(), UserApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + $dto->ownerName = $entity->getOwner()?->getFirstName()." ".$entity->getOwner()?->getLastName(); + + $dto->partner = $this->microMapper->map($entity->getPartner(), PartnerApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + $dto->partnerName = $entity->getPartner()?->getName(); + + $dto->product = $this->microMapper->map($entity->getProduct(), ProductApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + $dto->productName = $entity->getProduct()?->getName(); + + $dto->turnover = $entity->getTurnover(); + $dto->profit = $entity->getProfit(); + $dto->comment = $entity->getComment(); + $dto->createdAt = $entity->getCreatedAt(); + + $dto->posts = array_map(function(Posting $posting) { + return $this->microMapper->map($posting, PostingApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + }, $entity->getPostings()->getValues()); + + return $dto; + } +} diff --git a/src/Mapper/TaskEntityToApiMapper.php b/src/Mapper/TaskEntityToApiMapper.php index d58003e..2263589 100644 --- a/src/Mapper/TaskEntityToApiMapper.php +++ b/src/Mapper/TaskEntityToApiMapper.php @@ -6,10 +6,12 @@ use App\ApiResource\CommentApi; use App\ApiResource\ContactApi; use App\ApiResource\PartnerApi; use App\ApiResource\TaskApi; +use App\ApiResource\TaskNoteApi; use App\ApiResource\UserApi; use App\ApiResource\PostingApi; use App\Entity\Comment; use App\Entity\Task; +use App\Entity\TaskNote; use Symfony\Bundle\SecurityBundle\Security; use Symfonycasts\MicroMapper\AsMapper; use Symfonycasts\MicroMapper\MapperInterface; @@ -68,6 +70,12 @@ class TaskEntityToApiMapper implements MapperInterface $dto->contactName = $entity->getContact()?->getFirstName()." ".$entity->getContact()?->getLastName(); } + $dto->taskNotes = array_map(function(TaskNote $taskNote) { + return $this->microMapper->map($taskNote, TaskNoteApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + }, $entity->getTaskNotes()->getValues()); + $dto->prio = $entity->getPrio(); $dto->completed = $entity->getCompleted(); $dto->createdAt = $entity->getCreatedAt(); diff --git a/src/Mapper/TaskNoteApiToEntityMapper.php b/src/Mapper/TaskNoteApiToEntityMapper.php new file mode 100644 index 0000000..dc12af3 --- /dev/null +++ b/src/Mapper/TaskNoteApiToEntityMapper.php @@ -0,0 +1,64 @@ +id) { + $entity = $this->repository->find($dto->id); + } else { + $user = $this->security->getUser(); + assert($user instanceof User); + if ($dto->task === null) { + throw new \Exception('Task missing'); + } + $task = $this->microMapper->map($dto->task, Task::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + assert($task instanceof Task); + $entity = new TaskNote($user, $task); + } + + if (!$entity) { + throw new \Exception('TaskNote not found'); + } + + return $entity; + } + + public function populate(object $from, object $to, array $context): object + { + $dto = $from; + assert($dto instanceof TaskNoteApi); + $entity = $to; + assert($entity instanceof TaskNote); + $entity->setMessage($dto->message); + + return $entity; + } +} diff --git a/src/Mapper/TaskNoteEntityToApiMapper.php b/src/Mapper/TaskNoteEntityToApiMapper.php new file mode 100644 index 0000000..b3debd8 --- /dev/null +++ b/src/Mapper/TaskNoteEntityToApiMapper.php @@ -0,0 +1,54 @@ +id = $entity->getId(); + + return $dto; + } + + public function populate(object $from, object $to, array $context): object + { + $entity = $from; + $dto = $to; + assert($entity instanceof TaskNote); + assert($dto instanceof TaskNoteApi); + + $dto->message = $entity->getMessage(); + $dto->owner = $this->microMapper->map($entity->getOwner(), UserApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + $dto->ownerName = $entity->getOwner()?->getFirstName()." ".$entity->getOwner()?->getLastName(); + + $dto->task = $this->microMapper->map($entity->getTask(), TaskApi::class, [ + MicroMapperInterface::MAX_DEPTH => 1, + ]); + + $dto->createdAt = $entity->getCreatedAt(); + + return $dto; + } +} diff --git a/src/Repository/SaleRepository.php b/src/Repository/SaleRepository.php new file mode 100644 index 0000000..ef64ad4 --- /dev/null +++ b/src/Repository/SaleRepository.php @@ -0,0 +1,48 @@ + + * + * @method Sale|null find($id, $lockMode = null, $lockVersion = null) + * @method Sale|null findOneBy(array $criteria, array $orderBy = null) + * @method Sale[] findAll() + * @method Sale[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class SaleRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Sale::class); + } + +// /** +// * @return Sale[] Returns an array of Sale objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('s') +// ->andWhere('s.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('s.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?Sale +// { +// return $this->createQueryBuilder('s') +// ->andWhere('s.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/src/Repository/TaskNoteRepository.php b/src/Repository/TaskNoteRepository.php new file mode 100644 index 0000000..f45ab3f --- /dev/null +++ b/src/Repository/TaskNoteRepository.php @@ -0,0 +1,48 @@ + + * + * @method TaskNote|null find($id, $lockMode = null, $lockVersion = null) + * @method TaskNote|null findOneBy(array $criteria, array $orderBy = null) + * @method TaskNote[] findAll() + * @method TaskNote[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class TaskNoteRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, TaskNote::class); + } + + // /** + // * @return TaskNote[] Returns an array of TaskNote objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('t.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?TaskNote + // { + // return $this->createQueryBuilder('t') + // ->andWhere('t.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/State/SaleSummaryStateProvider.php b/src/State/SaleSummaryStateProvider.php new file mode 100644 index 0000000..0b664c7 --- /dev/null +++ b/src/State/SaleSummaryStateProvider.php @@ -0,0 +1,67 @@ +pagination->getPage($context); + $itemsPerPage = $this->pagination->getLimit($operation, $context); + $offset = $this->pagination->getOffset($operation, $context); + $salesSummaries = $this->createSaleSummaries($offset, $itemsPerPage); + + return new TraversablePaginator( + new \ArrayIterator($salesSummaries), + $currentPage, + $itemsPerPage, + count($salesSummaries), + ); + } + + $salesSummaries = $this->createSaleSummaries(0); + + return $this->createSaleSummaries(0, count($salesSummaries)); + } + + private function createSaleSummaries(int $offset, int $limit = 50): array + { + $users = $this->userRepository->findBy([], [], $limit, $offset); + $salesSummaries = []; + /** @var User $user */ + foreach ($users as $user) { + $sales = $user->getSales(); + $salesSummary = new SaleSummary(); + $salesSummary->owner = $this->microMapper->map($user, UserApi::class); + $salesSummary->ownerName = $user->getFirstName() . ' ' . $user->getLastName(); + $salesSummary->turnover = 0; + $salesSummary->profit = 0; + foreach ($sales as $sale) { + $salesSummary->turnover += $sale->getTurnover(); + $salesSummary->profit += $sale->getProfit(); + } + $salesSummaries[] = $salesSummary; + } + return $salesSummaries; + } +} diff --git a/src/Voter/CommentApiVoter.php b/src/Voter/CommentApiVoter.php deleted file mode 100644 index aa02c54..0000000 --- a/src/Voter/CommentApiVoter.php +++ /dev/null @@ -1,46 +0,0 @@ -getUser(); - // if the user is anonymous, do not grant access - if (!$user instanceof User) { - return false; - } - - assert($subject instanceof CommentApi); - - // ... (check conditions and return true to grant permission) ... - switch ($attribute) { - case self::EDIT: - if ($subject->owner?->id === $user->getId()) { - return true; - } - break; - } - - return false; - } -} diff --git a/src/Voter/PostingApiVoter.php b/src/Voter/OwnerApiEditVoter.php similarity index 74% rename from src/Voter/PostingApiVoter.php rename to src/Voter/OwnerApiEditVoter.php index e7ab12a..a36fa18 100644 --- a/src/Voter/PostingApiVoter.php +++ b/src/Voter/OwnerApiEditVoter.php @@ -2,13 +2,12 @@ namespace App\Voter; -use App\ApiResource\PostingApi; use App\Entity\User; -use Symfony\Bundle\SecurityBundle\Security; +use App\Interface\OwnerInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; -class PostingApiVoter extends Voter +class OwnerApiEditVoter extends Voter { public const EDIT = 'EDIT'; @@ -18,7 +17,7 @@ class PostingApiVoter extends Voter protected function supports(string $attribute, mixed $subject): bool { - return $attribute === self::EDIT && $subject instanceof PostingApi; + return $attribute === self::EDIT && ($subject instanceof OwnerInterface); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool @@ -29,12 +28,12 @@ class PostingApiVoter extends Voter return false; } - assert($subject instanceof PostingApi); + assert($subject instanceof OwnerInterface); // ... (check conditions and return true to grant permission) ... switch ($attribute) { case self::EDIT: - if ($subject->owner?->id === $user->getId()) { + if ($subject->getOwner()?->id === $user->getId()) { return true; } break; diff --git a/tests/Functional/TaskNoteResourceTest.php b/tests/Functional/TaskNoteResourceTest.php new file mode 100644 index 0000000..4b29023 --- /dev/null +++ b/tests/Functional/TaskNoteResourceTest.php @@ -0,0 +1,92 @@ + + * @date 12.12.23 + */ + + +namespace App\Tests\Functional; + +use App\Enum\PartnerType; +use App\Factory\CommentFactory; +use App\Factory\MediaObjectLogoFactory; +use App\Factory\MediaObjectContactFactory; +use App\Factory\PartnerFactory; +use App\Factory\PostingFactory; +use App\Factory\TaskFactory; +use App\Factory\TaskNoteFactory; +use App\Factory\UserFactory; +use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Browser\Test\HasBrowser; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; + +class TaskNoteResourceTest extends KernelTestCase +{ + use HasBrowser; + use ResetDatabase; + use Factories; + + private JWTTokenManagerInterface $JWTManager; + + protected function setUp(): void + { + parent::setUp(); + $this->JWTManager = self::getContainer()->get('lexik_jwt_authentication.jwt_manager'); + } + + public function testPostTaskNote(): void + { + $user = UserFactory::createOne( + [ + 'firstName' => 'Peter', + 'lastName' => 'Test', + 'password' => 'test', + 'email' => 'peter@test.de', + ] + ); + + PartnerFactory::createOne(); + $task = TaskFactory::createOne(); + TaskNoteFactory::createOne(); + + $token = $this->JWTManager->create($user->object()); + + $this->browser() + ->get('/api/task_notes', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + ], + ]) + ->assertSuccessful() + ->assertJsonMatches('"hydra:totalItems"', 1) + ; + + + $this->browser() + ->post('/api/task_notes' , [ + 'json' => [ + 'message' => 'my comment', + 'task' => '/api/tasks/' . $task->getId(), + ], + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + ] + ]) + ->assertSuccessful() + ; + + $this->browser() + ->get('/api/task_notes', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + ], + ]) + ->assertSuccessful() + ->assertJsonMatches('"hydra:totalItems"', 2) + ->assertJsonMatches('"hydra:member"[1].message', 'my comment') + ; + + } +} \ No newline at end of file