Parcourir la source

sales and summaries

master
Daniel il y a 2 ans
Parent
révision
d426e67eb8
34 fichiers modifiés avec 1415 ajouts et 134 suppressions
  1. +0
    -73
      migrations/Version20240318162352.php
  2. +49
    -0
      migrations/Version20240319114858.php
  3. +9
    -2
      src/ApiResource/CommentApi.php
  4. +1
    -1
      src/ApiResource/PartnerApi.php
  5. +10
    -1
      src/ApiResource/PostingApi.php
  6. +92
    -0
      src/ApiResource/SaleApi.php
  7. +47
    -0
      src/ApiResource/SaleSummary.php
  8. +24
    -1
      src/ApiResource/TaskApi.php
  9. +72
    -0
      src/ApiResource/TaskNoteApi.php
  10. +2
    -0
      src/ApiResource/UserApi.php
  11. +5
    -1
      src/DataFixtures/AppFixtures.php
  12. +34
    -0
      src/Entity/Partner.php
  13. +23
    -2
      src/Entity/Posting.php
  14. +36
    -0
      src/Entity/Product.php
  15. +173
    -0
      src/Entity/Sale.php
  16. +10
    -1
      src/Entity/Task.php
  17. +90
    -0
      src/Entity/TaskNote.php
  18. +34
    -0
      src/Entity/User.php
  19. +2
    -0
      src/Factory/PostingFactory.php
  20. +77
    -0
      src/Factory/SaleFactory.php
  21. +71
    -0
      src/Factory/TaskNoteFactory.php
  22. +8
    -0
      src/Interface/OwnerInterface.php
  23. +10
    -0
      src/Mapper/PostingApiToEntityMapper.php
  24. +76
    -0
      src/Mapper/SaleApiToEntityMapper.php
  25. +74
    -0
      src/Mapper/SaleEntityToApiMapper.php
  26. +8
    -0
      src/Mapper/TaskEntityToApiMapper.php
  27. +64
    -0
      src/Mapper/TaskNoteApiToEntityMapper.php
  28. +54
    -0
      src/Mapper/TaskNoteEntityToApiMapper.php
  29. +48
    -0
      src/Repository/SaleRepository.php
  30. +48
    -0
      src/Repository/TaskNoteRepository.php
  31. +67
    -0
      src/State/SaleSummaryStateProvider.php
  32. +0
    -46
      src/Voter/CommentApiVoter.php
  33. +5
    -6
      src/Voter/OwnerApiEditVoter.php
  34. +92
    -0
      tests/Functional/TaskNoteResourceTest.php

+ 0
- 73
migrations/Version20240318162352.php Voir le fichier

@@ -1,73 +0,0 @@
<?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 Version20240318162352 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('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`');
}
}

+ 49
- 0
migrations/Version20240319114858.php Voir le fichier

@@ -0,0 +1,49 @@
<?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 Version20240319114858 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 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');
}
}

+ 9
- 2
src/ApiResource/CommentApi.php Voir le fichier

@@ -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;
}
}

+ 1
- 1
src/ApiResource/PartnerApi.php Voir le fichier

@@ -83,7 +83,7 @@ class PartnerApi
public ?\DateTimeImmutable $createdAt = null;

/**
* @var array<int, PostingApi>
* @var $posts array<int, PostingApi>
*/
#[ApiProperty(writable: false)]
public array $posts = [];


+ 10
- 1
src/ApiResource/PostingApi.php Voir le fichier

@@ -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<int, CommentApi>
*/
@@ -101,4 +106,8 @@ class PostingApi
#[ApiProperty(writable: false)]
public ?\DateTimeImmutable $createdAt = null;

public function getOwner(): UserApi
{
return $this->owner;
}
}

+ 92
- 0
src/ApiResource/SaleApi.php Voir le fichier

@@ -0,0 +1,92 @@
<?php
/**
* @author Daniel Knudsen <d.knudsen@spawntree.de>
* @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<int, PostingApi>
*/
#[ApiProperty(writable: false)]
public array $posts = [];

public function getOwner(): UserApi
{
return $this->owner;
}
}

+ 47
- 0
src/ApiResource/SaleSummary.php Voir le fichier

@@ -0,0 +1,47 @@
<?php
/**
* @author Daniel Knudsen <d.knudsen@spawntree.de>
* @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;
}

+ 24
- 1
src/ApiResource/TaskApi.php Voir le fichier

@@ -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<int, TaskNoteApi>
*/
#[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;



+ 72
- 0
src/ApiResource/TaskNoteApi.php Voir le fichier

@@ -0,0 +1,72 @@
<?php
/**
* @author Daniel Knudsen <d.knudsen@spawntree.de>
* @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;
}
}

+ 2
- 0
src/ApiResource/UserApi.php Voir le fichier

@@ -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;

}

+ 5
- 1
src/DataFixtures/AppFixtures.php Voir le fichier

@@ -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);
}
}

+ 34
- 0
src/Entity/Partner.php Voir le fichier

@@ -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<int, Sale>
*/
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;
}

}

+ 23
- 2
src/Entity/Posting.php Voir le fichier

@@ -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;
}
}

+ 36
- 0
src/Entity/Product.php Voir le fichier

@@ -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<int, Sale>
*/
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;
}
}

+ 173
- 0
src/Entity/Sale.php Voir le fichier

@@ -0,0 +1,173 @@
<?php

namespace App\Entity;

use App\Repository\SaleRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: SaleRepository::class)]
class Sale
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;

#[ORM\ManyToOne(inversedBy: 'sales')]
#[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")]
private ?User $owner = null;

#[ORM\ManyToOne(inversedBy: 'sales')]
#[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")]
private ?Partner $partner = null;

#[ORM\ManyToOne(inversedBy: 'sales')]
#[ORM\JoinColumn(nullable: false, onDelete: "CASCADE")]
private ?Product $product = null;

#[ORM\Column]
private ?int $turnover = null;

#[ORM\Column(nullable: true)]
private ?int $profit = null;

#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $comment = null;

#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;

#[ORM\OneToMany(mappedBy: 'sale', targetEntity: Posting::class)]
private Collection $postings;

public function __construct(User $owner, Partner $partner, Product $product)
{
$this->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<int, Posting>
*/
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;
}
}

+ 10
- 1
src/Entity/Task.php Voir le fichier

@@ -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;
}

}

+ 90
- 0
src/Entity/TaskNote.php Voir le fichier

@@ -0,0 +1,90 @@
<?php

namespace App\Entity;

use App\Repository\TaskNoteRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity(repositoryClass: TaskNoteRepository::class)]
class TaskNote
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;

#[ORM\Column(type: Types::TEXT)]
private ?string $message = null;

#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?User $owner = null;

#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private ?Task $task = null;

#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;

public function __construct(User $owner, Task $task)
{
$this->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;
}
}

+ 34
- 0
src/Entity/User.php Voir le fichier

@@ -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<int, Sale>
*/
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;
}
}

+ 2
- 0
src/Factory/PostingFactory.php Voir le fichier

@@ -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,
];
}



+ 77
- 0
src/Factory/SaleFactory.php Voir le fichier

@@ -0,0 +1,77 @@
<?php

namespace App\Factory;

use App\Entity\Sale;
use App\Repository\SaleRepository;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\RepositoryProxy;

/**
* @extends ModelFactory<Sale>
*
* @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;
}
}

+ 71
- 0
src/Factory/TaskNoteFactory.php Voir le fichier

@@ -0,0 +1,71 @@
<?php

namespace App\Factory;

use App\Entity\TaskNote;
use App\Repository\TaskNoteRepository;
use Zenstruck\Foundry\ModelFactory;
use Zenstruck\Foundry\Proxy;
use Zenstruck\Foundry\RepositoryProxy;

/**
* @extends ModelFactory<TaskNote>
*
* @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;
}
}

+ 8
- 0
src/Interface/OwnerInterface.php Voir le fichier

@@ -0,0 +1,8 @@
<?php

namespace App\Interface;
use App\ApiResource\UserApi;

interface OwnerInterface {
public function getOwner(): UserApi;
}

+ 10
- 0
src/Mapper/PostingApiToEntityMapper.php Voir le fichier

@@ -4,8 +4,10 @@ namespace App\Mapper;

use App\ApiResource\ContactApi;
use App\ApiResource\PostingApi;
use App\ApiResource\SaleApi;
use App\Entity\Contact;
use App\Entity\Partner;
use App\Entity\Sale;
use App\Entity\User;
use App\Entity\Posting;
use App\Repository\PostingRepository;
@@ -49,8 +51,16 @@ class PostingApiToEntityMapper implements MapperInterface
MicroMapperInterface::MAX_DEPTH => 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);
}



+ 76
- 0
src/Mapper/SaleApiToEntityMapper.php Voir le fichier

@@ -0,0 +1,76 @@
<?php

namespace App\Mapper;

use App\ApiResource\CommentApi;
use App\ApiResource\SaleApi;
use App\Entity\Comment;
use App\Entity\Partner;
use App\Entity\Product;
use App\Entity\Sale;
use App\Entity\User;
use App\Repository\SaleRepository;
use Symfony\Bundle\SecurityBundle\Security;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
use Symfonycasts\MicroMapper\MicroMapperInterface;

#[AsMapper(from: CommentApi::class, to: Comment::class)]
class SaleApiToEntityMapper implements MapperInterface
{
public function __construct(
private SaleRepository $repository,
private Security $security,
private MicroMapperInterface $microMapper,
)
{

}

public function load(object $from, string $toClass, array $context): object
{
$dto = $from;
assert($dto instanceof SaleApi);

if ($dto->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;
}
}

+ 74
- 0
src/Mapper/SaleEntityToApiMapper.php Voir le fichier

@@ -0,0 +1,74 @@
<?php

namespace App\Mapper;

use App\ApiResource\CommentApi;
use App\ApiResource\PartnerApi;
use App\ApiResource\ProductApi;
use App\ApiResource\SaleApi;
use App\ApiResource\UserApi;
use App\ApiResource\PostingApi;
use App\Entity\Comment;
use App\Entity\Posting;
use App\Entity\Sale;
use Symfony\Bundle\SecurityBundle\Security;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
use Symfonycasts\MicroMapper\MicroMapperInterface;

#[AsMapper(from: Sale::class, to: SaleApi::class)]
class SaleEntityToApiMapper implements MapperInterface
{
public function __construct(
private MicroMapperInterface $microMapper
)
{
}

public function load(object $from, string $toClass, array $context): object
{
$entity = $from;
assert($entity instanceof Sale);

$dto = new SaleApi();
$dto->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;
}
}

+ 8
- 0
src/Mapper/TaskEntityToApiMapper.php Voir le fichier

@@ -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();


+ 64
- 0
src/Mapper/TaskNoteApiToEntityMapper.php Voir le fichier

@@ -0,0 +1,64 @@
<?php

namespace App\Mapper;

use App\ApiResource\TaskNoteApi;
use App\Entity\Task;
use App\Entity\TaskNote;
use App\Entity\User;
use App\Repository\TaskNoteRepository;
use Symfony\Bundle\SecurityBundle\Security;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
use Symfonycasts\MicroMapper\MicroMapperInterface;

#[AsMapper(from: TaskNoteApi::class, to: TaskNote::class)]
class TaskNoteApiToEntityMapper implements MapperInterface
{
public function __construct(
private TaskNoteRepository $repository,
private Security $security,
private MicroMapperInterface $microMapper,
)
{

}

public function load(object $from, string $toClass, array $context): object
{
$dto = $from;
assert($dto instanceof TaskNoteApi);

if ($dto->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;
}
}

+ 54
- 0
src/Mapper/TaskNoteEntityToApiMapper.php Voir le fichier

@@ -0,0 +1,54 @@
<?php

namespace App\Mapper;

use App\ApiResource\TaskApi;
use App\ApiResource\TaskNoteApi;
use App\ApiResource\UserApi;
use App\Entity\TaskNote;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
use Symfonycasts\MicroMapper\MicroMapperInterface;

#[AsMapper(from: TaskNote::class, to: TaskNoteApi::class)]
class TaskNoteEntityToApiMapper implements MapperInterface
{
public function __construct(
private MicroMapperInterface $microMapper
)
{
}

public function load(object $from, string $toClass, array $context): object
{
$entity = $from;
assert($entity instanceof TaskNote);

$dto = new TaskNoteApi();
$dto->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;
}
}

+ 48
- 0
src/Repository/SaleRepository.php Voir le fichier

@@ -0,0 +1,48 @@
<?php

namespace App\Repository;

use App\Entity\Sale;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
* @extends ServiceEntityRepository<Sale>
*
* @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()
// ;
// }
}

+ 48
- 0
src/Repository/TaskNoteRepository.php Voir le fichier

@@ -0,0 +1,48 @@
<?php

namespace App\Repository;

use App\Entity\TaskNote;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

/**
* @extends ServiceEntityRepository<TaskNote>
*
* @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()
// ;
// }
}

+ 67
- 0
src/State/SaleSummaryStateProvider.php Voir le fichier

@@ -0,0 +1,67 @@
<?php

namespace App\State;

use ApiPlatform\Metadata\CollectionOperationInterface;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\Pagination\Pagination;
use ApiPlatform\State\Pagination\TraversablePaginator;
use ApiPlatform\State\ProviderInterface;
use App\ApiResource\SaleSummary;
use App\ApiResource\UserApi;
use App\Entity\User;
use App\Repository\UserRepository;
use Symfonycasts\MicroMapper\MicroMapperInterface;

class SaleSummaryStateProvider implements ProviderInterface
{
public function __construct(
private UserRepository$userRepository,
private Pagination $pagination,
private MicroMapperInterface $microMapper,
)
{
}

public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
if ($operation instanceof CollectionOperationInterface) {
$currentPage = $this->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;
}
}

+ 0
- 46
src/Voter/CommentApiVoter.php Voir le fichier

@@ -1,46 +0,0 @@
<?php

namespace App\Voter;

use App\ApiResource\CommentApi;
use App\ApiResource\PostingApi;
use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class CommentApiVoter extends Voter
{
public const EDIT = 'EDIT';

public function __construct()
{
}

protected function supports(string $attribute, mixed $subject): bool
{
return $attribute === self::EDIT && $subject instanceof CommentApi;
}

protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->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;
}
}

src/Voter/PostingApiVoter.php → src/Voter/OwnerApiEditVoter.php Voir le fichier

@@ -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;

+ 92
- 0
tests/Functional/TaskNoteResourceTest.php Voir le fichier

@@ -0,0 +1,92 @@
<?php
/**
* @author Daniel Knudsen <d.knudsen@spawntree.de>
* @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')
;

}
}

Chargement…
Annuler
Enregistrer