mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-04 03:08:25 +00:00 
			
		
		
		
	Compare commits
	
		
			91 Commits
		
	
	
		
			456-doc-ge
			...
			chill-bund
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						e00ece4200
	
				 | 
					
					
						|||
| 
						
						
							
						
						640fd71402
	
				 | 
					
					
						|||
| 
						
						
							
						
						aae50ca290
	
				 | 
					
					
						|||
| 
						
						
							
						
						1fa483598b
	
				 | 
					
					
						|||
| 
						
						
							
						
						e4b6a468f8
	
				 | 
					
					
						|||
| 
						 | 
					66c7758023 | ||
| 
						 | 
					4750d2c24e | ||
| 
						 | 
					ca05e3d979 | ||
| 
						 | 
					a20f9b4f86 | ||
| 
						 | 
					c73c1eb8d5 | ||
| 
						 | 
					8778bb0731 | ||
| 
						 | 
					c7d20eebc5 | ||
| 
						 | 
					b9e130c159 | ||
| 
						 | 
					3e8bc94af3 | ||
| 
						 | 
					0c914c9f9f | ||
| 
						 | 
					580a60c939 | ||
| 
						 | 
					4996ac3b7c | ||
| 
						 | 
					2a23bf19cb | ||
| 
						 | 
					650d2596d9 | ||
| 
						 | 
					2bdd5a329e | ||
| 
						
						
							
						
						78d1776733
	
				 | 
					
					
						|||
| 
						
						
							
						
						66dc603c85
	
				 | 
					
					
						|||
| 
						
						
							
						
						3a8154ecce
	
				 | 
					
					
						|||
| 
						
						
							
						
						c81828e04f
	
				 | 
					
					
						|||
| 
						 | 
					ec17dd7de2 | ||
| 76c076a5f3 | |||
| 
						 | 
					f0045edd6c | ||
| 
						 | 
					d00b76ffcd | ||
| 
						 | 
					8991f0ef3f | ||
| 
						 | 
					d6f5eae0c9 | ||
| 
						 | 
					821fce3dd8 | ||
| 
						 | 
					1d33ae1e39 | ||
| 
						 | 
					19af0feb57 | ||
| 
						 | 
					1c09e9a692 | ||
| 
						 | 
					d72e748388 | ||
| 
						 | 
					ab850b7b70 | ||
| 
						 | 
					3f9745d8cf | ||
| 
						 | 
					473765366a | ||
| 
						 | 
					6500c24a7f | ||
| 
						 | 
					1d00457141 | ||
| 
						 | 
					eb0bf56cff | ||
| 
						 | 
					7b8cd90cf1 | ||
| 
						 | 
					a27d92aba0 | ||
| 
						 | 
					85bdfb9e21 | ||
| 
						 | 
					4cffcf4de1 | ||
| 
						 | 
					b2587a688f | ||
| 
						 | 
					c9f0e9843b | ||
| 
						 | 
					b40ad9e445 | ||
| 
						 | 
					3e10e47e29 | ||
| 
						 | 
					2a1963e993 | ||
| 34c171659b | |||
| 
						
						
							
						
						2d8b960d9e
	
				 | 
					
					
						|||
| 831ae03431 | |||
| 
						
						
							
						
						45828174d1
	
				 | 
					
					
						|||
| 
						
						
							
						
						ed45f14a45
	
				 | 
					
					
						|||
| 
						
						
							
						
						fa67835690
	
				 | 
					
					
						|||
| 
						
						
							
						
						b434d38091
	
				 | 
					
					
						|||
| 
						 | 
					800a952532 | ||
| 
						
						
							
						
						9f355032a8
	
				 | 
					
					
						|||
| 
						
						
							
						
						0bc6e62d4d
	
				 | 
					
					
						|||
| 
						
						
							
						
						46fb1c04b5
	
				 | 
					
					
						|||
| 3b2c3d1464 | |||
| 
						 | 
					0bd6038160 | ||
| 
						 | 
					baab8e94ce | ||
| 
						
						
							
						
						e2deb55fdb
	
				 | 
					
					
						|||
| 
						 | 
					2cdfb50058 | ||
| 
						
						
							
						
						39d701feb2
	
				 | 
					
					
						|||
| 
						
						
							
						
						613ee8b186
	
				 | 
					
					
						|||
| 
						
						
							
						
						56a1a488de
	
				 | 
					
					
						|||
| 3f789ad0f4 | |||
| 
						
						
							
						
						467bea7cde
	
				 | 
					
					
						|||
| 
						
						
							
						
						670b8eb82b
	
				 | 
					
					
						|||
| 
						
						
							
						
						a9760b323f
	
				 | 
					
					
						|||
| 
						
						
							
						
						71a3a1924a
	
				 | 
					
					
						|||
| 
						
						
							
						
						ecdc1e25bf
	
				 | 
					
					
						|||
| 
						
						
							
						
						dd37427be1
	
				 | 
					
					
						|||
| 
						
						
							
						
						c8467df1b1
	
				 | 
					
					
						|||
| 
						
						
							
						
						4c89a954fa
	
				 | 
					
					
						|||
| 
						
						
							
						
						7c1f3b114d
	
				 | 
					
					
						|||
| 
						
						
							
						
						36bc4dab24
	
				 | 
					
					
						|||
| 
						
						
							
						
						4b30d92282
	
				 | 
					
					
						|||
| 
						
						
							
						
						75fbec5489
	
				 | 
					
					
						|||
| 
						
						
							
						
						912fdd6349
	
				 | 
					
					
						|||
| 
						
						
							
						
						5832542978
	
				 | 
					
					
						|||
| 
						
						
							
						
						5c3585a1ed
	
				 | 
					
					
						|||
| 
						
						
							
						
						a2f1e20ddf
	
				 | 
					
					
						|||
| 
						
						
							
						
						4d67702a76
	
				 | 
					
					
						|||
| 18e442db29 | |||
| 
						 | 
					deb3d92189 | ||
| 
						
						
							
						
						a59ea7db31
	
				 | 
					
					
						|||
| 
						
						
							
						
						a738b0cac9
	
				 | 
					
					
						
@@ -55,7 +55,7 @@ Arborescence:
 | 
			
		||||
    - person
 | 
			
		||||
    - personvendee
 | 
			
		||||
    - household_edit_metadata
 | 
			
		||||
        - index.js
 | 
			
		||||
        - index.ts
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Organisation des feuilles de styles
 | 
			
		||||
 
 | 
			
		||||
@@ -119,6 +119,7 @@
 | 
			
		||||
            "Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle",
 | 
			
		||||
            "Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",
 | 
			
		||||
            "Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
 | 
			
		||||
            "Chill\\TicketBundle\\": "src/Bundle/ChillTicketBundle/src",
 | 
			
		||||
            "Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
 | 
			
		||||
            "Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
 | 
			
		||||
            "Chill\\Utils\\Rector\\": "utils/rector/src"
 | 
			
		||||
@@ -126,8 +127,9 @@
 | 
			
		||||
    },
 | 
			
		||||
    "autoload-dev": {
 | 
			
		||||
        "psr-4": {
 | 
			
		||||
            "App\\": "tests/",
 | 
			
		||||
            "App\\": "tests",
 | 
			
		||||
            "Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
 | 
			
		||||
            "Chill\\TicketBundle\\Tests\\": "src/Bundle/ChillTicketBundle/tests",
 | 
			
		||||
            "Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
 | 
			
		||||
            "Chill\\Utils\\Rector\\Tests\\": "utils/rector/tests"
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,10 @@
 | 
			
		||||
      <!-- temporarily removed, the time to find a fix -->
 | 
			
		||||
      <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
 | 
			
		||||
    </testsuite>
 | 
			
		||||
 | 
			
		||||
    <testsuite name="TicketBundle">
 | 
			
		||||
      <directory suffix="Test.php">src/Bundle/ChillTicketBundle/tests/</directory>
 | 
			
		||||
    </testsuite>
 | 
			
		||||
    <!--
 | 
			
		||||
        <testsuite name="ReportBundle">
 | 
			
		||||
            <directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory>
 | 
			
		||||
 
 | 
			
		||||
@@ -211,7 +211,7 @@ class SearchController extends AbstractController
 | 
			
		||||
        $builder = $this
 | 
			
		||||
            ->get('form.factory')
 | 
			
		||||
            ->createNamedBuilder(
 | 
			
		||||
                null,
 | 
			
		||||
                '',
 | 
			
		||||
                FormType::class,
 | 
			
		||||
                $data,
 | 
			
		||||
                ['method' => Request::METHOD_POST]
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\CRUD\Controller\ApiController;
 | 
			
		||||
 | 
			
		||||
class UserGroupApiController extends ApiController {}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\DataFixtures\ORM;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Doctrine\Bundle\FixturesBundle\Fixture;
 | 
			
		||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
 | 
			
		||||
use Doctrine\Persistence\ObjectManager;
 | 
			
		||||
 | 
			
		||||
class LoadUserGroup extends Fixture implements FixtureGroupInterface
 | 
			
		||||
{
 | 
			
		||||
    public static function getGroups(): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['user-group'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function load(ObjectManager $manager)
 | 
			
		||||
    {
 | 
			
		||||
        $centerASocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_social']);
 | 
			
		||||
        $centerBSocial = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_social']);
 | 
			
		||||
        $multiCenter = $manager->getRepository(User::class)->findOneBy(['username' => 'multi_center']);
 | 
			
		||||
        $administrativeA = $manager->getRepository(User::class)->findOneBy(['username' => 'center a_administrative']);
 | 
			
		||||
        $administrativeB = $manager->getRepository(User::class)->findOneBy(['username' => 'center b_administrative']);
 | 
			
		||||
 | 
			
		||||
        $level1 = $this->generateLevelGroup('Niveau 1', '#eec84aff', '#000000ff', 'level');
 | 
			
		||||
        $level1->addUser($centerASocial)->addUser($centerBSocial);
 | 
			
		||||
        $manager->persist($level1);
 | 
			
		||||
 | 
			
		||||
        $level2 = $this->generateLevelGroup('Niveau 2', ' #e2793dff', '#000000ff', 'level');
 | 
			
		||||
        $level2->addUser($multiCenter);
 | 
			
		||||
        $manager->persist($level2);
 | 
			
		||||
 | 
			
		||||
        $level3 = $this->generateLevelGroup('Niveau 3', ' #df4949ff', '#000000ff', 'level');
 | 
			
		||||
        $level3->addUser($multiCenter);
 | 
			
		||||
        $manager->persist($level3);
 | 
			
		||||
 | 
			
		||||
        $tss = $this->generateLevelGroup('Travailleur sociaux', '#43b29dff', '#000000ff', '');
 | 
			
		||||
        $tss->addUser($multiCenter)->addUser($centerASocial)->addUser($centerBSocial);
 | 
			
		||||
        $manager->persist($tss);
 | 
			
		||||
        $admins = $this->generateLevelGroup('Administratif', '#334d5cff', '#000000ff', '');
 | 
			
		||||
        $admins->addUser($administrativeA)->addUser($administrativeB);
 | 
			
		||||
        $manager->persist($admins);
 | 
			
		||||
 | 
			
		||||
        $manager->flush();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function generateLevelGroup(string $title, string $backgroundColor, string $foregroundColor, string $excludeKey): UserGroup
 | 
			
		||||
    {
 | 
			
		||||
        $userGroup = new UserGroup();
 | 
			
		||||
 | 
			
		||||
        return $userGroup
 | 
			
		||||
            ->setLabel(['fr' => $title])
 | 
			
		||||
            ->setBackgroundColor($backgroundColor)
 | 
			
		||||
            ->setForegroundColor($foregroundColor)
 | 
			
		||||
            ->setExcludeKey($excludeKey)
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -24,6 +24,7 @@ use Chill\MainBundle\Controller\LocationTypeController;
 | 
			
		||||
use Chill\MainBundle\Controller\NewsItemController;
 | 
			
		||||
use Chill\MainBundle\Controller\RegroupmentController;
 | 
			
		||||
use Chill\MainBundle\Controller\UserController;
 | 
			
		||||
use Chill\MainBundle\Controller\UserGroupApiController;
 | 
			
		||||
use Chill\MainBundle\Controller\UserJobApiController;
 | 
			
		||||
use Chill\MainBundle\Controller\UserJobController;
 | 
			
		||||
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
 | 
			
		||||
@@ -59,6 +60,7 @@ use Chill\MainBundle\Entity\LocationType;
 | 
			
		||||
use Chill\MainBundle\Entity\NewsItem;
 | 
			
		||||
use Chill\MainBundle\Entity\Regroupment;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Chill\MainBundle\Entity\UserJob;
 | 
			
		||||
use Chill\MainBundle\Form\CenterType;
 | 
			
		||||
use Chill\MainBundle\Form\CivilityType;
 | 
			
		||||
@@ -803,6 +805,21 @@ class ChillMainExtension extends Extension implements
 | 
			
		||||
                        ],
 | 
			
		||||
                    ],
 | 
			
		||||
                ],
 | 
			
		||||
                [
 | 
			
		||||
                    'class' => UserGroup::class,
 | 
			
		||||
                    'controller' => UserGroupApiController::class,
 | 
			
		||||
                    'name' => 'user-group',
 | 
			
		||||
                    'base_path' => '/api/1.0/main/user-group',
 | 
			
		||||
                    'base_role' => 'ROLE_USER',
 | 
			
		||||
                    'actions' => [
 | 
			
		||||
                        '_index' => [
 | 
			
		||||
                            'methods' => [
 | 
			
		||||
                                Request::METHOD_GET => true,
 | 
			
		||||
                                Request::METHOD_HEAD => true,
 | 
			
		||||
                            ],
 | 
			
		||||
                        ],
 | 
			
		||||
                    ],
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										139
									
								
								src/Bundle/ChillMainBundle/Entity/UserGroup.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								src/Bundle/ChillMainBundle/Entity/UserGroup.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Doctrine\Common\Collections\ArrayCollection;
 | 
			
		||||
use Doctrine\Common\Collections\Collection;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\Table(name: 'chill_main_user_group')]
 | 
			
		||||
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['user_group' => UserGroup::class])]
 | 
			
		||||
class UserGroup
 | 
			
		||||
{
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\GeneratedValue]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private array $label = [];
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var \Doctrine\Common\Collections\Collection<int, \Chill\MainBundle\Entity\User>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\ManyToMany(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinTable(name: 'chill_main_user_group_user')]
 | 
			
		||||
    private Collection $users;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#ffffffff'])]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private string $backgroundColor = '#ffffffff';
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => '#000000ff'])]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private string $foregroundColor = '#000000ff';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Groups with same exclude key are mutually exclusive: adding one in a many-to-one relationship
 | 
			
		||||
     * will exclude others.
 | 
			
		||||
     *
 | 
			
		||||
     * An empty string means "no exclusion"
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private string $excludeKey = '';
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->users = new ArrayCollection();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function addUser(User $user): self
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->users->contains($user)) {
 | 
			
		||||
            $this->users[] = $user;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function removeUser(User $user): self
 | 
			
		||||
    {
 | 
			
		||||
        if ($this->users->contains($user)) {
 | 
			
		||||
            $this->users->removeElement($user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabel(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->label;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getUsers(): Collection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->users;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getForegroundColor(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->foregroundColor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getExcludeKey(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->excludeKey;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getBackgroundColor(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->backgroundColor;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setForegroundColor(string $foregroundColor): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->foregroundColor = $foregroundColor;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setBackgroundColor(string $backgroundColor): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->backgroundColor = $backgroundColor;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setExcludeKey(string $excludeKey): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->excludeKey = $excludeKey;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setLabel(array $label): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->label = $label;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -76,6 +76,24 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
 | 
			
		||||
            ->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws NumberParseException
 | 
			
		||||
     */
 | 
			
		||||
    public function parse(string $phoneNumber): PhoneNumber
 | 
			
		||||
    {
 | 
			
		||||
        $sanitizedPhoneNumber = $phoneNumber;
 | 
			
		||||
 | 
			
		||||
        if (str_starts_with($sanitizedPhoneNumber, '00')) {
 | 
			
		||||
            $sanitizedPhoneNumber = '+'.substr($sanitizedPhoneNumber, 2, null);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!str_starts_with($sanitizedPhoneNumber, '+') && !str_starts_with($sanitizedPhoneNumber, '0')) {
 | 
			
		||||
            $sanitizedPhoneNumber = '+'.$sanitizedPhoneNumber;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->phoneNumberUtil->parse($sanitizedPhoneNumber, $this->config['default_carrier_code']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get type (mobile, landline, ...) for phone number.
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -1,164 +1,175 @@
 | 
			
		||||
export interface DateTime {
 | 
			
		||||
  datetime: string;
 | 
			
		||||
  datetime8601: string
 | 
			
		||||
    datetime: string;
 | 
			
		||||
    datetime8601: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Civility {
 | 
			
		||||
  id: number;
 | 
			
		||||
  // TODO
 | 
			
		||||
    id: number;
 | 
			
		||||
    // TODO
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Job {
 | 
			
		||||
  id: number;
 | 
			
		||||
  type: "user_job";
 | 
			
		||||
  label: {
 | 
			
		||||
    "fr": string; // could have other key. How to do that in ts ?
 | 
			
		||||
  }
 | 
			
		||||
    id: number;
 | 
			
		||||
    type: "user_job";
 | 
			
		||||
    label: {
 | 
			
		||||
        fr: string; // could have other key. How to do that in ts ?
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Center {
 | 
			
		||||
  id: number;
 | 
			
		||||
  type: "center";
 | 
			
		||||
  name: string;
 | 
			
		||||
    id: number;
 | 
			
		||||
    type: "center";
 | 
			
		||||
    name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Scope {
 | 
			
		||||
  id: number;
 | 
			
		||||
  type: "scope";
 | 
			
		||||
  name: {
 | 
			
		||||
    "fr": string
 | 
			
		||||
  }
 | 
			
		||||
    id: number;
 | 
			
		||||
    type: "scope";
 | 
			
		||||
    name: {
 | 
			
		||||
        fr: string;
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface User {
 | 
			
		||||
  type: "user";
 | 
			
		||||
  id: number;
 | 
			
		||||
  username: string;
 | 
			
		||||
  text: string;
 | 
			
		||||
  text_without_absence: string;
 | 
			
		||||
  email: string;
 | 
			
		||||
  user_job: Job;
 | 
			
		||||
  label: string;
 | 
			
		||||
  // todo: mainCenter; mainJob; etc..
 | 
			
		||||
    type: "user";
 | 
			
		||||
    id: number;
 | 
			
		||||
    username: string;
 | 
			
		||||
    text: string;
 | 
			
		||||
    text_without_absence: string;
 | 
			
		||||
    email: string;
 | 
			
		||||
    user_job: Job;
 | 
			
		||||
    label: string;
 | 
			
		||||
    // todo: mainCenter; mainJob; etc..
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface UserGroup {
 | 
			
		||||
    type: "chill_main_user_group" | "user_group";
 | 
			
		||||
    id: number;
 | 
			
		||||
    label: TranslatableString;
 | 
			
		||||
    backgroundColor: string;
 | 
			
		||||
    foregroundColor: string;
 | 
			
		||||
    excludeKey: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type UserGroupOrUser = User | UserGroup;
 | 
			
		||||
 | 
			
		||||
export interface UserAssociatedInterface {
 | 
			
		||||
  type: "user";
 | 
			
		||||
  id: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type TranslatableString = {
 | 
			
		||||
  fr?: string;
 | 
			
		||||
  nl?: string;
 | 
			
		||||
    type: "user";
 | 
			
		||||
    id: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type TranslatableString = {
 | 
			
		||||
    fr?: string;
 | 
			
		||||
    nl?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface Postcode {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: string;
 | 
			
		||||
  code: string;
 | 
			
		||||
  center: Point;
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: string;
 | 
			
		||||
    code: string;
 | 
			
		||||
    center: Point;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Point = {
 | 
			
		||||
  type: "Point";
 | 
			
		||||
  coordinates: [lat: number, lon: number];
 | 
			
		||||
}
 | 
			
		||||
    type: "Point";
 | 
			
		||||
    coordinates: [lat: number, lon: number];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface Country {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: TranslatableString;
 | 
			
		||||
  code: string;
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: TranslatableString;
 | 
			
		||||
    code: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type AddressRefStatus = 'match'|'to_review'|'reviewed';
 | 
			
		||||
export type AddressRefStatus = "match" | "to_review" | "reviewed";
 | 
			
		||||
 | 
			
		||||
export interface Address {
 | 
			
		||||
  type: "address";
 | 
			
		||||
  address_id: number;
 | 
			
		||||
  text: string;
 | 
			
		||||
  street: string;
 | 
			
		||||
  streetNumber: string;
 | 
			
		||||
  postcode: Postcode;
 | 
			
		||||
  country: Country;
 | 
			
		||||
  floor: string | null;
 | 
			
		||||
  corridor: string | null;
 | 
			
		||||
  steps: string | null;
 | 
			
		||||
  flat: string | null;
 | 
			
		||||
  buildingName: string | null;
 | 
			
		||||
  distribution: string | null;
 | 
			
		||||
  extra: string | null;
 | 
			
		||||
  confidential: boolean;
 | 
			
		||||
  lines: string[];
 | 
			
		||||
  addressReference: AddressReference | null;
 | 
			
		||||
  validFrom: DateTime;
 | 
			
		||||
  validTo: DateTime | null;
 | 
			
		||||
  point: Point | null;
 | 
			
		||||
  refStatus: AddressRefStatus;
 | 
			
		||||
  isNoAddress: boolean;
 | 
			
		||||
    type: "address";
 | 
			
		||||
    address_id: number;
 | 
			
		||||
    text: string;
 | 
			
		||||
    street: string;
 | 
			
		||||
    streetNumber: string;
 | 
			
		||||
    postcode: Postcode;
 | 
			
		||||
    country: Country;
 | 
			
		||||
    floor: string | null;
 | 
			
		||||
    corridor: string | null;
 | 
			
		||||
    steps: string | null;
 | 
			
		||||
    flat: string | null;
 | 
			
		||||
    buildingName: string | null;
 | 
			
		||||
    distribution: string | null;
 | 
			
		||||
    extra: string | null;
 | 
			
		||||
    confidential: boolean;
 | 
			
		||||
    lines: string[];
 | 
			
		||||
    addressReference: AddressReference | null;
 | 
			
		||||
    validFrom: DateTime;
 | 
			
		||||
    validTo: DateTime | null;
 | 
			
		||||
    point: Point | null;
 | 
			
		||||
    refStatus: AddressRefStatus;
 | 
			
		||||
    isNoAddress: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AddressWithPoint extends Address {
 | 
			
		||||
  point: Point
 | 
			
		||||
    point: Point;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AddressReference {
 | 
			
		||||
  id: number;
 | 
			
		||||
  createdAt: DateTime | null;
 | 
			
		||||
  deletedAt: DateTime | null;
 | 
			
		||||
  municipalityCode: string;
 | 
			
		||||
  point: Point;
 | 
			
		||||
  postcode: Postcode;
 | 
			
		||||
  refId: string;
 | 
			
		||||
  source: string;
 | 
			
		||||
  street: string;
 | 
			
		||||
  streetNumber: string;
 | 
			
		||||
  updatedAt: DateTime | null;
 | 
			
		||||
    id: number;
 | 
			
		||||
    createdAt: DateTime | null;
 | 
			
		||||
    deletedAt: DateTime | null;
 | 
			
		||||
    municipalityCode: string;
 | 
			
		||||
    point: Point;
 | 
			
		||||
    postcode: Postcode;
 | 
			
		||||
    refId: string;
 | 
			
		||||
    source: string;
 | 
			
		||||
    street: string;
 | 
			
		||||
    streetNumber: string;
 | 
			
		||||
    updatedAt: DateTime | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SimpleGeographicalUnit {
 | 
			
		||||
  id: number;
 | 
			
		||||
  layerId: number;
 | 
			
		||||
  unitName: string;
 | 
			
		||||
  unitRefId: string;
 | 
			
		||||
    id: number;
 | 
			
		||||
    layerId: number;
 | 
			
		||||
    unitName: string;
 | 
			
		||||
    unitRefId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GeographicalUnitLayer {
 | 
			
		||||
  id: number;
 | 
			
		||||
  name: TranslatableString;
 | 
			
		||||
  refId: string;
 | 
			
		||||
    id: number;
 | 
			
		||||
    name: TranslatableString;
 | 
			
		||||
    refId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Location {
 | 
			
		||||
  type: "location";
 | 
			
		||||
  id: number;
 | 
			
		||||
  active: boolean;
 | 
			
		||||
  address: Address | null;
 | 
			
		||||
  availableForUsers: boolean;
 | 
			
		||||
  createdAt: DateTime | null;
 | 
			
		||||
  createdBy: User | null;
 | 
			
		||||
  updatedAt: DateTime | null;
 | 
			
		||||
  updatedBy: User | null;
 | 
			
		||||
  email: string | null
 | 
			
		||||
  name: string;
 | 
			
		||||
  phonenumber1: string | null;
 | 
			
		||||
  phonenumber2: string | null;
 | 
			
		||||
  locationType: LocationType;
 | 
			
		||||
    type: "location";
 | 
			
		||||
    id: number;
 | 
			
		||||
    active: boolean;
 | 
			
		||||
    address: Address | null;
 | 
			
		||||
    availableForUsers: boolean;
 | 
			
		||||
    createdAt: DateTime | null;
 | 
			
		||||
    createdBy: User | null;
 | 
			
		||||
    updatedAt: DateTime | null;
 | 
			
		||||
    updatedBy: User | null;
 | 
			
		||||
    email: string | null;
 | 
			
		||||
    name: string;
 | 
			
		||||
    phonenumber1: string | null;
 | 
			
		||||
    phonenumber2: string | null;
 | 
			
		||||
    locationType: LocationType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface LocationAssociated {
 | 
			
		||||
  type: "location";
 | 
			
		||||
  id: number;
 | 
			
		||||
    type: "location";
 | 
			
		||||
    id: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface LocationType {
 | 
			
		||||
  type: "location-type";
 | 
			
		||||
  id: number;
 | 
			
		||||
  active: boolean;
 | 
			
		||||
  addressRequired: "optional" | "required";
 | 
			
		||||
  availableForUsers: boolean;
 | 
			
		||||
  editableByUsers: boolean;
 | 
			
		||||
  contactData: "optional" | "required";
 | 
			
		||||
  title: TranslatableString;
 | 
			
		||||
    type: "location-type";
 | 
			
		||||
    id: number;
 | 
			
		||||
    active: boolean;
 | 
			
		||||
    addressRequired: "optional" | "required";
 | 
			
		||||
    availableForUsers: boolean;
 | 
			
		||||
    editableByUsers: boolean;
 | 
			
		||||
    contactData: "optional" | "required";
 | 
			
		||||
    title: TranslatableString;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NewsItemType {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,24 @@
 | 
			
		||||
<template>
 | 
			
		||||
<span class="chill-entity entity-user">
 | 
			
		||||
    {{ user.label }}
 | 
			
		||||
    <span class="user-job" v-if="user.user_job !== null">({{ user.user_job.label.fr }})</span> <span class="main-scope" v-if="user.main_scope !== null">({{ user.main_scope.name.fr }})</span> <span v-if="user.isAbsent" class="badge bg-danger rounded-pill" :title="Absent">A</span>
 | 
			
		||||
</span>
 | 
			
		||||
    <span class="chill-entity entity-user">
 | 
			
		||||
        {{ user.label }}
 | 
			
		||||
        <span class="user-job" v-if="user.user_job !== null"
 | 
			
		||||
            >({{ user.user_job.label.fr }})</span
 | 
			
		||||
        >
 | 
			
		||||
        <span class="main-scope" v-if="user.main_scope !== null"
 | 
			
		||||
            >({{ user.main_scope.name.fr }})</span
 | 
			
		||||
        >
 | 
			
		||||
        <span
 | 
			
		||||
            v-if="user.isAbsent"
 | 
			
		||||
            class="badge bg-danger rounded-pill"
 | 
			
		||||
            :title="Absent"
 | 
			
		||||
            >A</span
 | 
			
		||||
        >
 | 
			
		||||
    </span>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
export default {
 | 
			
		||||
    name: "UserRenderBoxBadge",
 | 
			
		||||
    props: ['user'],
 | 
			
		||||
}
 | 
			
		||||
    props: ["user"],
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -69,35 +69,37 @@
 | 
			
		||||
                    </div>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
 | 
			
		||||
                {% block content %}
 | 
			
		||||
                    <div class="col-8 main_search">
 | 
			
		||||
                        {% if app.user.isAbsent %}
 | 
			
		||||
                            <div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
 | 
			
		||||
                                <p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
 | 
			
		||||
                                <span class="ms-auto">
 | 
			
		||||
                                    <a class="btn btn-remove" title="Modifier" href="{{ path('chill_main_user_absence_index') }}">{{ 'absence.Unset absence'|trans }}</a>
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        {% endif %}
 | 
			
		||||
                        <h2>{{ 'Search'|trans }}</h2>
 | 
			
		||||
                {% block wrapping_content %}
 | 
			
		||||
                    {% block content %}
 | 
			
		||||
                        <div class="col-8 main_search">
 | 
			
		||||
                            {% if app.user.isAbsent %}
 | 
			
		||||
                                <div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
 | 
			
		||||
                                    <p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
 | 
			
		||||
                                    <span class="ms-auto">
 | 
			
		||||
                                        <a class="btn btn-remove" title="Modifier" href="{{ path('chill_main_user_absence_index') }}">{{ 'absence.Unset absence'|trans }}</a>
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            {% endif %}
 | 
			
		||||
                            <h2>{{ 'Search'|trans }}</h2>
 | 
			
		||||
 | 
			
		||||
                        <form action="{{ path('chill_main_search') }}" method="get">
 | 
			
		||||
                            <input class="form-control form-control-lg" name="q" type="search" placeholder="{{ 'Search persons, ...'|trans }}" />
 | 
			
		||||
                            <div class="text-center">
 | 
			
		||||
                                <button type="submit" class="btn btn-lg btn-warning mt-3">
 | 
			
		||||
                                    <i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }}
 | 
			
		||||
                                </button>
 | 
			
		||||
                                <a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}">
 | 
			
		||||
                                    <i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }}
 | 
			
		||||
                                </a>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </form>
 | 
			
		||||
                    </div>
 | 
			
		||||
                            <form action="{{ path('chill_main_search') }}" method="get">
 | 
			
		||||
                                <input class="form-control form-control-lg" name="q" type="search" placeholder="{{ 'Search persons, ...'|trans }}" />
 | 
			
		||||
                                <div class="text-center">
 | 
			
		||||
                                    <button type="submit" class="btn btn-lg btn-warning mt-3">
 | 
			
		||||
                                        <i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }}
 | 
			
		||||
                                    </button>
 | 
			
		||||
                                    <a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}">
 | 
			
		||||
                                        <i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }}
 | 
			
		||||
                                    </a>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                    {#  DISABLED {{  chill_widget('homepage', {} ) }} #}
 | 
			
		||||
                        {#  DISABLED {{  chill_widget('homepage', {} ) }} #}
 | 
			
		||||
 | 
			
		||||
                    {% include '@ChillMain/Homepage/index.html.twig' %}
 | 
			
		||||
                        {% include '@ChillMain/Homepage/index.html.twig' %}
 | 
			
		||||
 | 
			
		||||
                    {% endblock %}
 | 
			
		||||
                {% endblock %}
 | 
			
		||||
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\MainBundle\Tests\Phonenumber;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Phonenumber\PhonenumberHelper;
 | 
			
		||||
use libphonenumber\PhoneNumber;
 | 
			
		||||
use libphonenumber\PhoneNumberUtil;
 | 
			
		||||
use Psr\Log\NullLogger;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
@@ -52,12 +53,36 @@ final class PhonenumberHelperTest extends KernelTestCase
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function providePhoneNumbersToParse(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        $util = PhoneNumberUtil::getInstance();
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            'FR',
 | 
			
		||||
            '+32486544999',
 | 
			
		||||
            $util->parse('+32486544999', 'FR'),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            'FR',
 | 
			
		||||
            '32486544999',
 | 
			
		||||
            $util->parse('+32486544999', 'FR'),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        yield [
 | 
			
		||||
            'FR',
 | 
			
		||||
            '0228858040',
 | 
			
		||||
            $util->parse('+33228858040', 'FR'),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider formatPhonenumbers
 | 
			
		||||
     */
 | 
			
		||||
    public function testFormatPhonenumbers(string $defaultCarrierCode, string $phoneNumber, string $expected)
 | 
			
		||||
    {
 | 
			
		||||
        $util = PhoneNumberUtil::getInstance();
 | 
			
		||||
 | 
			
		||||
        $subject = new PhonenumberHelper(
 | 
			
		||||
            new ArrayAdapter(),
 | 
			
		||||
            new ParameterBag([
 | 
			
		||||
@@ -70,4 +95,24 @@ final class PhonenumberHelperTest extends KernelTestCase
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals($expected, $subject->format($util->parse($phoneNumber)));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider providePhoneNumbersToParse
 | 
			
		||||
     */
 | 
			
		||||
    public function testParsePhonenumbers(string $defaultCarrierCode, string $phoneNumber, PhoneNumber $expected): void
 | 
			
		||||
    {
 | 
			
		||||
        $subject = new PhonenumberHelper(
 | 
			
		||||
            new ArrayAdapter(),
 | 
			
		||||
            new ParameterBag([
 | 
			
		||||
                'chill_main.phone_helper' => [
 | 
			
		||||
                    'default_carrier_code' => $defaultCarrierCode,
 | 
			
		||||
                ],
 | 
			
		||||
            ]),
 | 
			
		||||
            new NullLogger()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $actual = $subject->parse($phoneNumber);
 | 
			
		||||
 | 
			
		||||
        self::assertTrue($expected->equals($actual));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,91 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Tests\Validation\Validator;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Chill\MainBundle\Validation\Validator\UserGroupDoNotExclude;
 | 
			
		||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class UserGroupDoNotExcludeTest extends ConstraintValidatorTestCase
 | 
			
		||||
{
 | 
			
		||||
    protected function createValidator()
 | 
			
		||||
    {
 | 
			
		||||
        return new UserGroupDoNotExclude(
 | 
			
		||||
            new class () implements TranslatableStringHelperInterface {
 | 
			
		||||
                public function localize(array $translatableStrings): ?string
 | 
			
		||||
                {
 | 
			
		||||
                    return $translatableStrings['fr'];
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testEmptyArrayIsValid(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->validator->validate([], new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude());
 | 
			
		||||
 | 
			
		||||
        $this->assertNoViolation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testMixedUserGroupAndUsersIsValid(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->validator->validate(
 | 
			
		||||
            [new User(), new UserGroup()],
 | 
			
		||||
            new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->assertNoViolation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testDifferentExcludeKeysIsValid(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->validator->validate(
 | 
			
		||||
            [(new UserGroup())->setExcludeKey('A'), (new UserGroup())->setExcludeKey('B')],
 | 
			
		||||
            new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->assertNoViolation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testMultipleGroupsWithEmptyExcludeKeyIsValid(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->validator->validate(
 | 
			
		||||
            [(new UserGroup())->setExcludeKey(''), (new UserGroup())->setExcludeKey('')],
 | 
			
		||||
            new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->assertNoViolation();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testSameExclusionKeyWillRaiseError(): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->validator->validate(
 | 
			
		||||
            [
 | 
			
		||||
                (new UserGroup())->setExcludeKey('A')->setLabel(['fr' => 'Group 1']),
 | 
			
		||||
                (new UserGroup())->setExcludeKey('A')->setLabel(['fr' => 'Group 2']),
 | 
			
		||||
            ],
 | 
			
		||||
            new \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->buildViolation('The groups {{ excluded_groups }} do exclude themselves. Please choose one between them')
 | 
			
		||||
            ->setParameter('excluded_groups', 'Group 1, Group 2')
 | 
			
		||||
            ->setCode('e16c8226-0090-11ef-8560-f7239594db09')
 | 
			
		||||
            ->assertRaised();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Validation\Constraint;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Validator\Constraint;
 | 
			
		||||
 | 
			
		||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
 | 
			
		||||
class UserGroupDoNotExclude extends Constraint
 | 
			
		||||
{
 | 
			
		||||
    public string $message = 'The groups {{ excluded_groups }} do exclude themselves. Please choose one between them';
 | 
			
		||||
    public string $code = 'e16c8226-0090-11ef-8560-f7239594db09';
 | 
			
		||||
 | 
			
		||||
    public function getTargets()
 | 
			
		||||
    {
 | 
			
		||||
        return [self::PROPERTY_CONSTRAINT];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function validatedBy()
 | 
			
		||||
    {
 | 
			
		||||
        return \Chill\MainBundle\Validation\Validator\UserGroupDoNotExclude::class;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Validation\Validator;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
 | 
			
		||||
use Symfony\Component\Validator\Constraint;
 | 
			
		||||
use Symfony\Component\Validator\ConstraintValidator;
 | 
			
		||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
 | 
			
		||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
 | 
			
		||||
 | 
			
		||||
final class UserGroupDoNotExclude extends ConstraintValidator
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
 | 
			
		||||
 | 
			
		||||
    public function validate($value, Constraint $constraint)
 | 
			
		||||
    {
 | 
			
		||||
        if (!$constraint instanceof \Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude) {
 | 
			
		||||
            throw new UnexpectedTypeException($constraint, UserGroupDoNotExclude::class);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null === $value) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!is_iterable($value)) {
 | 
			
		||||
            throw new UnexpectedValueException($value, 'iterable');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $groups = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($value as $gr) {
 | 
			
		||||
            if ($gr instanceof UserGroup) {
 | 
			
		||||
                $groups[$gr->getExcludeKey()][] = $gr;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($groups as $excludeKey => $groupByKey) {
 | 
			
		||||
            if ('' === $excludeKey) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (1 < count($groupByKey)) {
 | 
			
		||||
                $excludedGroups = implode(
 | 
			
		||||
                    ', ',
 | 
			
		||||
                    array_map(
 | 
			
		||||
                        fn (UserGroup $group) => $this->translatableStringHelper->localize($group->getLabel()),
 | 
			
		||||
                        $groupByKey
 | 
			
		||||
                    )
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                $this->context
 | 
			
		||||
                    ->buildViolation($constraint->message)
 | 
			
		||||
                    ->setCode($constraint->code)
 | 
			
		||||
                    ->setParameters(['excluded_groups' => $excludedGroups])
 | 
			
		||||
                    ->addViolation();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -29,6 +29,42 @@ components:
 | 
			
		||||
                    type: string
 | 
			
		||||
                text:
 | 
			
		||||
                    type: string
 | 
			
		||||
        UserById:
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
                id:
 | 
			
		||||
                    type: integer
 | 
			
		||||
                type:
 | 
			
		||||
                    type: string
 | 
			
		||||
                    enum:
 | 
			
		||||
                        - user
 | 
			
		||||
        UserGroup:
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
                id:
 | 
			
		||||
                    type: integer
 | 
			
		||||
                type:
 | 
			
		||||
                    type: string
 | 
			
		||||
                    enum:
 | 
			
		||||
                        - user_group
 | 
			
		||||
                label:
 | 
			
		||||
                    type: object
 | 
			
		||||
                    additionalProperties: true
 | 
			
		||||
                backgroundColor:
 | 
			
		||||
                    type: string
 | 
			
		||||
                foregroundColor:
 | 
			
		||||
                    type: string
 | 
			
		||||
                exclusionKey:
 | 
			
		||||
                    type: string
 | 
			
		||||
        UserGroupById:
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
                id:
 | 
			
		||||
                    type: integer
 | 
			
		||||
                type:
 | 
			
		||||
                    type: string
 | 
			
		||||
                    enum:
 | 
			
		||||
                        - user_group
 | 
			
		||||
        Center:
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
@@ -908,3 +944,19 @@ paths:
 | 
			
		||||
                                    $ref: '#/components/schemas/NewsItem'
 | 
			
		||||
                403:
 | 
			
		||||
                    description: "Unauthorized"
 | 
			
		||||
    /1.0/main/user-group.json:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
                - user-group
 | 
			
		||||
            summary: Return a list of users-groups
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "ok"
 | 
			
		||||
                    content:
 | 
			
		||||
                        application/json:
 | 
			
		||||
                            schema:
 | 
			
		||||
                                type: array
 | 
			
		||||
                                items:
 | 
			
		||||
                                    $ref: '#/components/schemas/UserGroup'
 | 
			
		||||
                403:
 | 
			
		||||
                    description: "Unauthorized"
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,9 @@ services:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
 | 
			
		||||
    Chill\MainBundle\Validation\:
 | 
			
		||||
        resource: '../../Validation'
 | 
			
		||||
 | 
			
		||||
    chill_main.validator_user_circle_consistency:
 | 
			
		||||
        class: Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistencyValidator
 | 
			
		||||
        arguments:
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\Main;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20240416145021 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Create tables for user_group';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_main_user_group_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_main_user_group (id INT NOT NULL, label JSON DEFAULT \'[]\' NOT NULL, PRIMARY KEY(id))');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_main_user_group_user (usergroup_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(usergroup_id, user_id))');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_1E07F044D2112630 ON chill_main_user_group_user (usergroup_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_1E07F044A76ED395 ON chill_main_user_group_user (user_id)');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_user_group_user ADD CONSTRAINT FK_1E07F044D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_user_group_user ADD CONSTRAINT FK_1E07F044A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('DROP SEQUENCE chill_main_user_group_id_seq');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_main_user_group_user');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_main_user_group');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\Main;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20240422091752 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add colors and exclude string to user groups';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_user_group ADD backgroundColor TEXT DEFAULT \'#ffffffff\' NOT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_user_group ADD foregroundColor TEXT DEFAULT \'#000000ff\' NOT NULL');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_user_group ADD excludeKey TEXT DEFAULT \'\' NOT NULL');
 | 
			
		||||
        $this->addSql('ALTER INDEX idx_1e07f044d2112630 RENAME TO IDX_738BC82BD2112630');
 | 
			
		||||
        $this->addSql('ALTER INDEX idx_1e07f044a76ed395 RENAME TO IDX_738BC82BA76ED395');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_user_group DROP backgroundColor');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_user_group DROP foregroundColor');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_user_group DROP excludeKey');
 | 
			
		||||
        $this->addSql('ALTER INDEX idx_738bc82bd2112630 RENAME TO idx_1e07f044d2112630');
 | 
			
		||||
        $this->addSql('ALTER INDEX idx_738bc82ba76ed395 RENAME TO idx_1e07f044a76ed395');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -28,7 +28,11 @@ use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class GeographicalUnitStatAggregator implements AggregatorInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository, private TranslatableStringHelperInterface $translatableStringHelper, private RollingDateConverterInterface $rollingDateConverter) {}
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository,
 | 
			
		||||
        private TranslatableStringHelperInterface $translatableStringHelper,
 | 
			
		||||
        private RollingDateConverterInterface $rollingDateConverter
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function addRole(): ?string
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,8 @@ use Chill\PersonBundle\Security\Authorization\PersonVoter;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\NonUniqueResultException;
 | 
			
		||||
use Doctrine\ORM\Query;
 | 
			
		||||
use libphonenumber\PhoneNumber;
 | 
			
		||||
use libphonenumber\PhoneNumberFormat;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
final readonly class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
 | 
			
		||||
@@ -298,4 +300,27 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
 | 
			
		||||
                \array_map(static fn (Center $c) => $c->getId(), $authorizedCenters)
 | 
			
		||||
            );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findByPhone(PhoneNumber $phoneNumber, int $start = 0, int $limit = 20): array
 | 
			
		||||
    {
 | 
			
		||||
        $authorizedCenters = $this->authorizationHelper
 | 
			
		||||
            ->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
 | 
			
		||||
 | 
			
		||||
        if ([] === $authorizedCenters) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $util = \libphonenumber\PhoneNumberUtil::getInstance();
 | 
			
		||||
 | 
			
		||||
        return $this->em->createQuery(
 | 
			
		||||
            'SELECT p FROM '.Person::class.' p LEFT JOIN p.otherPhoneNumbers opn JOIN p.centerCurrent pcc '.
 | 
			
		||||
            'WHERE (p.phonenumber LIKE :phone OR p.mobilenumber LIKE :phone OR opn.phonenumber LIKE :phone) '.
 | 
			
		||||
            'AND pcc.center IN (:centers)'
 | 
			
		||||
        )
 | 
			
		||||
            ->setMaxResults($limit)
 | 
			
		||||
            ->setFirstResult($start)
 | 
			
		||||
            ->setParameter('phone', $util->format($phoneNumber, PhoneNumberFormat::E164))
 | 
			
		||||
            ->setParameter('centers', $authorizedCenters)
 | 
			
		||||
            ->getResult();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Search\SearchApiQuery;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use libphonenumber\PhoneNumber;
 | 
			
		||||
 | 
			
		||||
interface PersonACLAwareRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
@@ -60,4 +61,13 @@ interface PersonACLAwareRepositoryInterface
 | 
			
		||||
        ?string $phonenumber = null,
 | 
			
		||||
        ?string $city = null
 | 
			
		||||
    ): array;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return list<Person>
 | 
			
		||||
     */
 | 
			
		||||
    public function findByPhone(
 | 
			
		||||
        PhoneNumber $phoneNumber,
 | 
			
		||||
        int $start = 0,
 | 
			
		||||
        int $limit = 20
 | 
			
		||||
    ): array;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,10 +12,12 @@ declare(strict_types=1);
 | 
			
		||||
namespace Chill\PersonBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\PersonBundle\Entity\PersonPhone;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
use libphonenumber\PhoneNumber;
 | 
			
		||||
 | 
			
		||||
class PersonRepository implements ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
@@ -29,6 +31,8 @@ class PersonRepository implements ObjectRepository
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws \Doctrine\ORM\NoResultException
 | 
			
		||||
     * @throws \Doctrine\ORM\NonUniqueResultException
 | 
			
		||||
     *
 | 
			
		||||
     * @deprecated
 | 
			
		||||
     */
 | 
			
		||||
    public function countByPhone(
 | 
			
		||||
        string $phonenumber,
 | 
			
		||||
@@ -71,6 +75,8 @@ class PersonRepository implements ObjectRepository
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     *
 | 
			
		||||
     * @deprecated Use @see{self::findByPhoneNumber} or use a dedicated method in PersonACLAwareRepository
 | 
			
		||||
     */
 | 
			
		||||
    public function findByPhone(
 | 
			
		||||
        string $phonenumber,
 | 
			
		||||
@@ -91,6 +97,25 @@ class PersonRepository implements ObjectRepository
 | 
			
		||||
        return $qb->getQuery()->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find a person which is associated to the given phonenumber, without restrictions
 | 
			
		||||
     * on any.
 | 
			
		||||
     *
 | 
			
		||||
     * @return list<Person>
 | 
			
		||||
     */
 | 
			
		||||
    public function findByPhoneNumber(PhoneNumber $phoneNumber, int $firstResult = 0, int $maxResults = 50): array
 | 
			
		||||
    {
 | 
			
		||||
        $qb = $this->repository->createQueryBuilder('p');
 | 
			
		||||
        $qb->select('p');
 | 
			
		||||
 | 
			
		||||
        $this->searchByPhoneNumbers($qb, $phoneNumber);
 | 
			
		||||
 | 
			
		||||
        $qb->setFirstResult($firstResult)
 | 
			
		||||
            ->setMaxResults($maxResults);
 | 
			
		||||
 | 
			
		||||
        return $qb->getQuery()->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findOneBy($criteria);
 | 
			
		||||
@@ -109,6 +134,20 @@ class PersonRepository implements ObjectRepository
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function searchByPhoneNumbers(QueryBuilder $qb, PhoneNumber $phoneNumber): void
 | 
			
		||||
    {
 | 
			
		||||
        $qb->setParameter('number', $phoneNumber, 'phone_number');
 | 
			
		||||
 | 
			
		||||
        $orX = $qb->expr()->orX();
 | 
			
		||||
        $orX->add($qb->expr()->eq('p.mobilenumber', ':number'));
 | 
			
		||||
        $orX->add($qb->expr()->eq('p.phonenumber', ':number'));
 | 
			
		||||
        $orX->add(
 | 
			
		||||
            $qb->expr()->exists('SELECT 1 FROM '.PersonPhone::class.' k WHERE k.phonenumber = :number AND k.person = p')
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $qb->andWhere($orX);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,10 @@
 | 
			
		||||
   <h2><a id="section-10"></a>{{ $t('persons_associated.title')}}</h2>
 | 
			
		||||
 | 
			
		||||
   <div v-if="currentParticipations.length > 0">
 | 
			
		||||
      <label class="col-form-label">{{ $tc('persons_associated.counter', counter) }}</label>
 | 
			
		||||
      <label class="col-form-label">{{ $t('persons_associated.counter', { count: counter }) }}</label>
 | 
			
		||||
   </div>
 | 
			
		||||
   <div v-else>
 | 
			
		||||
      <label class="chill-no-data-statement">{{ $tc('persons_associated.counter', counter) }}</label>
 | 
			
		||||
      <label class="chill-no-data-statement">{{ $t('persons_associated.counter', { count: counter }) }}</label>
 | 
			
		||||
   </div>
 | 
			
		||||
 | 
			
		||||
   <div v-if="participationWithoutHousehold.length > 0" class="alert alert-warning no-household">
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,10 @@
 | 
			
		||||
   <h2><a id="section-90"></a>{{ $t('resources.title')}}</h2>
 | 
			
		||||
 | 
			
		||||
   <div v-if="resources.length > 0">
 | 
			
		||||
      <label class="col-form-label">{{ $tc('resources.counter', counter) }}</label>
 | 
			
		||||
      <label class="col-form-label">{{ $t('resources.counter', { count: counter }) }}</label>
 | 
			
		||||
   </div>
 | 
			
		||||
   <div v-else>
 | 
			
		||||
      <label class="chill-no-data-statement">{{ $tc('resources.counter', counter) }}</label>
 | 
			
		||||
      <label class="chill-no-data-statement">{{ $t('resources.counter', { count: counter }) }}</label>
 | 
			
		||||
   </div>
 | 
			
		||||
 | 
			
		||||
   <div class="flex-table mb-3">
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
                  data-bs-toggle="collapse"
 | 
			
		||||
                  aria-expanded="false"
 | 
			
		||||
                  @click="toggleHouseholdSuggestion">
 | 
			
		||||
            {{ $tc('household_members_editor.show_household_suggestion', countHouseholdSuggestion) }}
 | 
			
		||||
            {{ $t('household_members_editor.show_household_suggestion', { count: countHouseholdSuggestion }) }}
 | 
			
		||||
          </button>
 | 
			
		||||
          <button v-if="showHouseholdSuggestion"
 | 
			
		||||
                  class="accordion-button"
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
               <div class="search">
 | 
			
		||||
 | 
			
		||||
                  <label class="col-form-label" style="float: right;">
 | 
			
		||||
                     {{ $tc('add_persons.suggested_counter', suggestedCounter) }}
 | 
			
		||||
                     {{ $t('add_persons.suggested_counter', { count: suggestedCounter }) }}
 | 
			
		||||
                  </label>
 | 
			
		||||
 | 
			
		||||
                  <input id="search-persons"
 | 
			
		||||
@@ -42,7 +42,7 @@
 | 
			
		||||
                     </a>
 | 
			
		||||
                  </span>
 | 
			
		||||
                  <span v-if="selectedCounter > 0">
 | 
			
		||||
                     {{ $tc('add_persons.selected_counter', selectedCounter) }}
 | 
			
		||||
                     {{ $t('add_persons.selected_counter', { count: selectedCounter }) }}
 | 
			
		||||
                  </span>
 | 
			
		||||
               </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -52,9 +52,7 @@
 | 
			
		||||
                {{ $t('renderbox.deathdate') + ' ' + deathdate }}
 | 
			
		||||
              </time>
 | 
			
		||||
 | 
			
		||||
              <span v-if="options.addAge && person.birthdate" class="age">{{
 | 
			
		||||
                  $tc('renderbox.years_old', person.age)
 | 
			
		||||
                }}</span>
 | 
			
		||||
              <span v-if="options.addAge && person.birthdate" class="age">{{ $t('renderbox.years_old', { n: person.age }) }}</span>
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
            <span :class="'altname altname-' + altNameKey"> ({{ altNameLabel }})</span>
 | 
			
		||||
        </span>
 | 
			
		||||
        <span v-if="person.suffixText" class="suffixtext"> {{ person.suffixText }}</span>
 | 
			
		||||
        <span class="age" v-if="this.addAge && person.birthdate !== null && person.deathdate === null">{{ $tc('renderbox.years_old', person.age) }}</span>
 | 
			
		||||
        <span class="age" v-if="this.addAge && person.birthdate !== null && person.deathdate === null">{{ $t('renderbox.years_old', { n: person.age }) }}</span>
 | 
			
		||||
        <span v-else-if="this.addAge && person.deathdate !== null"> (‡)</span>
 | 
			
		||||
    </span>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,14 +11,17 @@ declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\PersonBundle\Tests\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\Center;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Repository\CenterRepositoryInterface;
 | 
			
		||||
use Chill\MainBundle\Repository\CountryRepository;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\PersonBundle\Entity\PersonPhone;
 | 
			
		||||
use Chill\PersonBundle\Repository\PersonACLAwareRepository;
 | 
			
		||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use PHPUnit\Framework\Attributes\DataProvider;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
@@ -98,4 +101,67 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
 | 
			
		||||
            $this->assertStringContainsString('diallo', strtolower($person->getFirstName().' '.$person->getLastName()));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider providePersonsWithPhoneNumbers
 | 
			
		||||
     */
 | 
			
		||||
    public function testFindByPhonenumber(\libphonenumber\PhoneNumber $phoneNumber, ?int $expectedId): void
 | 
			
		||||
    {
 | 
			
		||||
        $user = new User();
 | 
			
		||||
 | 
			
		||||
        $authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
 | 
			
		||||
        $authorizationHelper->getReachableCenters(Argument::exact($user), Argument::exact(PersonVoter::SEE))
 | 
			
		||||
            ->willReturn($this->centerRepository->findAll());
 | 
			
		||||
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->getUser()->willReturn($user);
 | 
			
		||||
 | 
			
		||||
        $repository = new PersonACLAwareRepository(
 | 
			
		||||
            $security->reveal(),
 | 
			
		||||
            $this->entityManager,
 | 
			
		||||
            $this->countryRepository,
 | 
			
		||||
            $authorizationHelper->reveal()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $actual = $repository->findByPhone($phoneNumber, 0, 10);
 | 
			
		||||
 | 
			
		||||
        if (null === $expectedId) {
 | 
			
		||||
            self::assertCount(0, $actual);
 | 
			
		||||
        } else {
 | 
			
		||||
            $actualIds = array_map(fn (Person $person) => $person->getId(), $actual);
 | 
			
		||||
 | 
			
		||||
            self::assertContains($expectedId, $actualIds);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function providePersonsWithPhoneNumbers(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
        $em = self::getContainer()->get(EntityManagerInterface::class);
 | 
			
		||||
        $center = $em->createQuery('SELECT c FROM '.Center::class.' c ')->setMaxResults(1)
 | 
			
		||||
            ->getSingleResult();
 | 
			
		||||
        $util = \libphonenumber\PhoneNumberUtil::getInstance();
 | 
			
		||||
 | 
			
		||||
        $mobile = $util->parse('+32486123456');
 | 
			
		||||
        $fixed = $util->parse('+3281136917');
 | 
			
		||||
        $anotherMobile = $util->parse('+32486123478');
 | 
			
		||||
        $person = (new Person())->setFirstName('diallo')->setLastName('diallo')->setCenter($center);
 | 
			
		||||
        $person->setMobilenumber($mobile)->setPhonenumber($fixed);
 | 
			
		||||
        $otherPhone = new PersonPhone();
 | 
			
		||||
        $otherPhone->setPerson($person);
 | 
			
		||||
        $otherPhone->setPhonenumber($anotherMobile);
 | 
			
		||||
        $otherPhone->setType('mobile');
 | 
			
		||||
 | 
			
		||||
        $em->persist($person);
 | 
			
		||||
        $em->persist($otherPhone);
 | 
			
		||||
 | 
			
		||||
        $em->flush();
 | 
			
		||||
 | 
			
		||||
        self::ensureKernelShutdown();
 | 
			
		||||
 | 
			
		||||
        yield [$mobile, $person->getId()];
 | 
			
		||||
        yield [$anotherMobile, $person->getId()];
 | 
			
		||||
        yield [$fixed, $person->getId()];
 | 
			
		||||
        yield [$util->parse('+331234567890'), null];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										163
									
								
								src/Bundle/ChillTicketBundle/chill.api.specs.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/Bundle/ChillTicketBundle/chill.api.specs.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,163 @@
 | 
			
		||||
components:
 | 
			
		||||
    schemas:
 | 
			
		||||
        Motive:
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
                id:
 | 
			
		||||
                    type: integer
 | 
			
		||||
                label:
 | 
			
		||||
                    type: object
 | 
			
		||||
                    additionalProperties:
 | 
			
		||||
                        type: string
 | 
			
		||||
                    example:
 | 
			
		||||
                        fr: Retard de livraison
 | 
			
		||||
                active:
 | 
			
		||||
                    type: boolean
 | 
			
		||||
        MotiveById:
 | 
			
		||||
            type: object
 | 
			
		||||
            properties:
 | 
			
		||||
                id:
 | 
			
		||||
                    type: integer
 | 
			
		||||
                type:
 | 
			
		||||
                    type: string
 | 
			
		||||
                    enum:
 | 
			
		||||
                        - ticket_motive
 | 
			
		||||
            required:
 | 
			
		||||
                - id
 | 
			
		||||
                - type
 | 
			
		||||
 | 
			
		||||
paths:
 | 
			
		||||
    /1.0/ticket/motive.json:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
                - ticket
 | 
			
		||||
            summary: A list of available ticket's motive
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "OK"
 | 
			
		||||
 | 
			
		||||
    /1.0/ticket/{id}/motive/set:
 | 
			
		||||
        post:
 | 
			
		||||
            tags:
 | 
			
		||||
                - ticket
 | 
			
		||||
            summary: Replace the existing ticket's motive by a new one
 | 
			
		||||
            parameters:
 | 
			
		||||
                -   name: id
 | 
			
		||||
                    in: path
 | 
			
		||||
                    required: true
 | 
			
		||||
                    description: The ticket id
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: integer
 | 
			
		||||
                        format: integer
 | 
			
		||||
                        minimum: 1
 | 
			
		||||
            requestBody:
 | 
			
		||||
                required: true
 | 
			
		||||
                content:
 | 
			
		||||
                    application/json:
 | 
			
		||||
                        schema:
 | 
			
		||||
                            type: object
 | 
			
		||||
                            properties:
 | 
			
		||||
                                motive:
 | 
			
		||||
                                    $ref: "#/components/schemas/MotiveById"
 | 
			
		||||
            responses:
 | 
			
		||||
                201:
 | 
			
		||||
                    description: "ACCEPTED"
 | 
			
		||||
                422:
 | 
			
		||||
                    description: "UNPROCESSABLE ENTITY"
 | 
			
		||||
 | 
			
		||||
    /1.0/ticket/{id}/comment/add:
 | 
			
		||||
        post:
 | 
			
		||||
            tags:
 | 
			
		||||
                - ticket
 | 
			
		||||
            summary: Add a comment to an existing ticket
 | 
			
		||||
            parameters:
 | 
			
		||||
                -   name: id
 | 
			
		||||
                    in: path
 | 
			
		||||
                    required: true
 | 
			
		||||
                    description: The ticket id
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: integer
 | 
			
		||||
                        format: integer
 | 
			
		||||
                        minimum: 1
 | 
			
		||||
            requestBody:
 | 
			
		||||
                required: true
 | 
			
		||||
                content:
 | 
			
		||||
                    application/json:
 | 
			
		||||
                        schema:
 | 
			
		||||
                            type: object
 | 
			
		||||
                            properties:
 | 
			
		||||
                                content:
 | 
			
		||||
                                    type: string
 | 
			
		||||
            responses:
 | 
			
		||||
                201:
 | 
			
		||||
                    description: "ACCEPTED"
 | 
			
		||||
                422:
 | 
			
		||||
                    description: "UNPROCESSABLE ENTITY"
 | 
			
		||||
 | 
			
		||||
    /1.0/ticket/{id}/addressees/set:
 | 
			
		||||
        post:
 | 
			
		||||
            tags:
 | 
			
		||||
                - ticket
 | 
			
		||||
            summary: Set the addresses for an existing ticket (will replace all the existing addresses)
 | 
			
		||||
            parameters:
 | 
			
		||||
                -   name: id
 | 
			
		||||
                    in: path
 | 
			
		||||
                    required: true
 | 
			
		||||
                    description: The ticket id
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: integer
 | 
			
		||||
                        format: integer
 | 
			
		||||
                        minimum: 1
 | 
			
		||||
            requestBody:
 | 
			
		||||
                required: true
 | 
			
		||||
                content:
 | 
			
		||||
                    application/json:
 | 
			
		||||
                        schema:
 | 
			
		||||
                            type: object
 | 
			
		||||
                            properties:
 | 
			
		||||
                                addressees:
 | 
			
		||||
                                    type: array
 | 
			
		||||
                                    items:
 | 
			
		||||
                                        oneOf:
 | 
			
		||||
                                            - $ref: '#/components/schemas/UserGroupById'
 | 
			
		||||
                                            - $ref: '#/components/schemas/UserById'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            responses:
 | 
			
		||||
                201:
 | 
			
		||||
                    description: "ACCEPTED"
 | 
			
		||||
                422:
 | 
			
		||||
                    description: "UNPROCESSABLE ENTITY"
 | 
			
		||||
 | 
			
		||||
    /1.0/ticket/{id}/addressee/add:
 | 
			
		||||
        post:
 | 
			
		||||
            tags:
 | 
			
		||||
                - ticket
 | 
			
		||||
            summary: Add an addressee to a ticket, without removing existing ones.
 | 
			
		||||
            parameters:
 | 
			
		||||
                -   name: id
 | 
			
		||||
                    in: path
 | 
			
		||||
                    required: true
 | 
			
		||||
                    description: The ticket id
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: integer
 | 
			
		||||
                        format: integer
 | 
			
		||||
                        minimum: 1
 | 
			
		||||
            requestBody:
 | 
			
		||||
                required: true
 | 
			
		||||
                content:
 | 
			
		||||
                    application/json:
 | 
			
		||||
                        schema:
 | 
			
		||||
                            type: object
 | 
			
		||||
                            properties:
 | 
			
		||||
                                addressee:
 | 
			
		||||
                                    oneOf:
 | 
			
		||||
                                        - $ref: '#/components/schemas/UserGroupById'
 | 
			
		||||
                                        - $ref: '#/components/schemas/UserById'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            responses:
 | 
			
		||||
                201:
 | 
			
		||||
                    description: "ACCEPTED"
 | 
			
		||||
                422:
 | 
			
		||||
                    description: "UNPROCESSABLE ENTITY"
 | 
			
		||||
							
								
								
									
										4
									
								
								src/Bundle/ChillTicketBundle/chill.webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/Bundle/ChillTicketBundle/chill.webpack.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
module.exports = function(encore, entries) {
 | 
			
		||||
    encore.addEntry('page_ticket', __dirname + '/src/Resources/public/page/ticket/index.ts');
 | 
			
		||||
    encore.addEntry('vue_ticket_app', __dirname + '/src/Resources/public/vuejs/TicketApp/index.ts');
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation\Groups;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Add a single addressee to the ticket.
 | 
			
		||||
 *
 | 
			
		||||
 * This command is converted into an "SetAddresseesCommand" for handling
 | 
			
		||||
 */
 | 
			
		||||
final readonly class AddAddresseeCommand
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public User|UserGroup $addressee
 | 
			
		||||
    ) {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
 | 
			
		||||
final readonly class AddCommentCommand
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[Assert\NotBlank()]
 | 
			
		||||
        #[Assert\NotNull]
 | 
			
		||||
        #[Serializer\Groups(['write'])]
 | 
			
		||||
        public ?string $content = null,
 | 
			
		||||
    ) {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket;
 | 
			
		||||
 | 
			
		||||
class AssociateByPhonenumberCommand
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public string $phonenumber,
 | 
			
		||||
    ) {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket;
 | 
			
		||||
 | 
			
		||||
final readonly class CreateTicketCommand
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        public string $externalReference = '',
 | 
			
		||||
    ) {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\AddCommentCommand;
 | 
			
		||||
use Chill\TicketBundle\Entity\Comment;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class AddCommentCommandHandler
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function handle(Ticket $ticket, AddCommentCommand $command): void
 | 
			
		||||
    {
 | 
			
		||||
        $comment = new Comment($command->content, $ticket);
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->persist($comment);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Phonenumber\PhonenumberHelper;
 | 
			
		||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\AssociateByPhonenumberCommand;
 | 
			
		||||
use Chill\TicketBundle\Entity\PersonHistory;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
 | 
			
		||||
class AssociateByPhonenumberCommandHandler
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private PersonACLAwareRepositoryInterface $personRepository,
 | 
			
		||||
        private PhonenumberHelper $phonenumberHelper,
 | 
			
		||||
        private ClockInterface $clock,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function __invoke(Ticket $ticket, AssociateByPhonenumberCommand $command): void
 | 
			
		||||
    {
 | 
			
		||||
        $phone = $this->phonenumberHelper->parse($command->phonenumber);
 | 
			
		||||
        $persons = $this->personRepository->findByPhone($phone);
 | 
			
		||||
 | 
			
		||||
        foreach ($persons as $person) {
 | 
			
		||||
            $history = new PersonHistory($person, $ticket, $this->clock->now());
 | 
			
		||||
            $this->entityManager->persist($history);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\CreateTicketCommand;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
 | 
			
		||||
class CreateTicketCommandHandler
 | 
			
		||||
{
 | 
			
		||||
    public function __invoke(CreateTicketCommand $command): Ticket
 | 
			
		||||
    {
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $ticket->setExternalRef($command->externalReference);
 | 
			
		||||
 | 
			
		||||
        return $ticket;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand;
 | 
			
		||||
use Chill\TicketBundle\Entity\MotiveHistory;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class ReplaceMotiveCommandHandler
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private ClockInterface $clock,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function handle(Ticket $ticket, ReplaceMotiveCommand $command): void
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $command->motive) {
 | 
			
		||||
            throw new \InvalidArgumentException('The new motive cannot be null');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // will add if there are no existing motive
 | 
			
		||||
        $readyToAdd = 0 === count($ticket->getMotiveHistories());
 | 
			
		||||
 | 
			
		||||
        foreach ($ticket->getMotiveHistories() as $history) {
 | 
			
		||||
            if (null !== $history->getEndDate()) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if ($history->getMotive() === $command->motive) {
 | 
			
		||||
                // we apply the same motive, we do nothing
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $history->setEndDate($this->clock->now());
 | 
			
		||||
            $readyToAdd = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($readyToAdd) {
 | 
			
		||||
            $history = new MotiveHistory($command->motive, $ticket, $this->clock->now());
 | 
			
		||||
            $this->entityManager->persist($history);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\SetAddresseesCommand;
 | 
			
		||||
use Chill\TicketBundle\Entity\AddresseeHistory;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Component\Clock\ClockInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
final readonly class SetAddresseesCommandHandler
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private ClockInterface $clock,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public function handle(Ticket $ticket, SetAddresseesCommand $command): void
 | 
			
		||||
    {
 | 
			
		||||
        // remove existing addresses which are not in the new addresses
 | 
			
		||||
        foreach ($ticket->getAddresseeHistories() as $addressHistory) {
 | 
			
		||||
            if (null !== $addressHistory->getEndDate()) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!in_array($addressHistory->getAddressee(), $command->addressees, true)) {
 | 
			
		||||
                $addressHistory->setEndDate($this->clock->now());
 | 
			
		||||
                if (($user = $this->security->getUser()) instanceof User) {
 | 
			
		||||
                    $addressHistory->setRemovedBy($user);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // add new addresses
 | 
			
		||||
        foreach ($command->addressees as $address) {
 | 
			
		||||
            if (in_array($address, $ticket->getCurrentAddressee(), true)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $history = new AddresseeHistory($address, $this->clock->now(), $ticket);
 | 
			
		||||
            $this->entityManager->persist($history);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Entity\Motive;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation\Groups;
 | 
			
		||||
use Symfony\Component\Validator\Constraints as Assert;
 | 
			
		||||
 | 
			
		||||
final readonly class ReplaceMotiveCommand
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[Assert\NotNull]
 | 
			
		||||
        #[Groups(['write'])]
 | 
			
		||||
        public ?Motive $motive,
 | 
			
		||||
    ) {}
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Action\Ticket;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Chill\MainBundle\Validation\Constraint\UserGroupDoNotExclude;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation\Groups;
 | 
			
		||||
use Symfony\Component\Validator\Constraints\GreaterThan;
 | 
			
		||||
 | 
			
		||||
final readonly class SetAddresseesCommand
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        /**
 | 
			
		||||
         * @var list<UserGroup|User>
 | 
			
		||||
         */
 | 
			
		||||
        #[UserGroupDoNotExclude]
 | 
			
		||||
        #[GreaterThan(0)]
 | 
			
		||||
        #[Groups(['read'])]
 | 
			
		||||
        public array $addressees
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    public static function fromAddAddresseeCommand(AddAddresseeCommand $command, Ticket $ticket): self
 | 
			
		||||
    {
 | 
			
		||||
        return new self([
 | 
			
		||||
            $command->addressee,
 | 
			
		||||
            ...$ticket->getCurrentAddressee(),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								src/Bundle/ChillTicketBundle/src/ChillTicketBundle.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/Bundle/ChillTicketBundle/src/ChillTicketBundle.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
 | 
			
		||||
 | 
			
		||||
class ChillTicketBundle extends Bundle {}
 | 
			
		||||
@@ -0,0 +1,68 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\AddCommentCommand;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\AddCommentCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class AddCommentController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private SerializerInterface $serializer,
 | 
			
		||||
        private ValidatorInterface $validator,
 | 
			
		||||
        private AddCommentCommandHandler $addCommentCommandHandler,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/ticket/{id}/comment/add', name: 'chill_ticket_comment_add', methods: ['POST'])]
 | 
			
		||||
    public function __invoke(Ticket $ticket, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted('ROLE_USER')) {
 | 
			
		||||
            throw new AccessDeniedHttpException('Only user can add ticket comments.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $command = $this->serializer->deserialize($request->getContent(), AddCommentCommand::class, 'json', ['groups' => 'write']);
 | 
			
		||||
 | 
			
		||||
        $errors = $this->validator->validate($command);
 | 
			
		||||
 | 
			
		||||
        if (count($errors) > 0) {
 | 
			
		||||
            return new JsonResponse(
 | 
			
		||||
                $this->serializer->serialize($errors, 'json'),
 | 
			
		||||
                Response::HTTP_UNPROCESSABLE_ENTITY,
 | 
			
		||||
                [],
 | 
			
		||||
                true
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->addCommentCommandHandler->handle($ticket, $command);
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(
 | 
			
		||||
            $this->serializer->serialize($ticket, 'json', ['groups' => 'read']),
 | 
			
		||||
            Response::HTTP_CREATED,
 | 
			
		||||
            [],
 | 
			
		||||
            true
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,70 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\AssociateByPhonenumberCommand;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\AssociateByPhonenumberCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\CreateTicketCommand;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\CreateTicketCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Repository\TicketRepositoryInterface;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\RedirectResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
final readonly class CreateTicketController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private CreateTicketCommandHandler $createTicketCommandHandler,
 | 
			
		||||
        private AssociateByPhonenumberCommandHandler $associateByPhonenumberCommandHandler,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private UrlGeneratorInterface $urlGenerator,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
        private TicketRepositoryInterface $ticketRepository,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('{_locale}/ticket/ticket/create')]
 | 
			
		||||
    public function __invoke(Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted('ROLE_USER')) {
 | 
			
		||||
            throw new AccessDeniedHttpException('Only users are allowed to create tickets.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ('' !== $extId = $request->query->get('extId', '')) {
 | 
			
		||||
            if (null !== $ticket = $this->ticketRepository->findOneByExternalRef($extId)) {
 | 
			
		||||
                return new RedirectResponse(
 | 
			
		||||
                    $this->urlGenerator->generate('chill_ticket_ticket_edit', ['id' => $ticket->getId()])
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $createCommand = new CreateTicketCommand($request->query->get('extId', ''));
 | 
			
		||||
        $ticket = $this->createTicketCommandHandler->__invoke($createCommand);
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->persist($ticket);
 | 
			
		||||
 | 
			
		||||
        if ($request->query->has('caller')) {
 | 
			
		||||
            $associateByPhonenumberCommand = new AssociateByPhonenumberCommand($request->query->get('caller'));
 | 
			
		||||
            $this->associateByPhonenumberCommandHandler->__invoke($ticket, $associateByPhonenumberCommand);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
        return new RedirectResponse(
 | 
			
		||||
            $this->urlGenerator->generate('chill_ticket_ticket_edit', ['id' => $ticket->getId()])
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,38 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Twig\Environment;
 | 
			
		||||
 | 
			
		||||
class EditTicketController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Environment $templating
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/{_locale}/ticket/ticket/{id}/edit', name: 'chill_ticket_ticket_edit')]
 | 
			
		||||
    public function __invoke(
 | 
			
		||||
        Ticket $ticket
 | 
			
		||||
    ): Response {
 | 
			
		||||
        return new Response(
 | 
			
		||||
            $this->templating->render(
 | 
			
		||||
                '@ChillTicket/Ticket/edit.html.twig',
 | 
			
		||||
                [
 | 
			
		||||
                    'ticket' => $ticket,
 | 
			
		||||
                ]
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,58 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Phonenumber\PhonenumberHelper;
 | 
			
		||||
use Chill\PersonBundle\Repository\PersonRepository;
 | 
			
		||||
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
 | 
			
		||||
use libphonenumber\NumberParseException;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Controller for a rest api to find a caller for a given phonenumber.
 | 
			
		||||
 *
 | 
			
		||||
 * TODO: currently, this rest api is not secured
 | 
			
		||||
 */
 | 
			
		||||
class FindCallerController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private PhonenumberHelper $phonenumberHelper, private PersonRepository $personRepository, private PersonRenderInterface $personRender) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/public/api/1.0/ticket/find-caller', name: 'find-caller', methods: ['GET'])]
 | 
			
		||||
    public function findCaller(Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        $caller = $request->query->get('caller', '');
 | 
			
		||||
 | 
			
		||||
        if ('' === $caller) {
 | 
			
		||||
            throw new BadRequestHttpException('Missing "caller" query parameter');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $phoneNumber = $this->phonenumberHelper->parse($caller);
 | 
			
		||||
        } catch (NumberParseException $e) {
 | 
			
		||||
            throw new BadRequestHttpException('Unable to parse number', $e);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $persons = $this->personRepository->findByPhoneNumber($phoneNumber, 0, 2);
 | 
			
		||||
 | 
			
		||||
        $asArray = match (count($persons)) {
 | 
			
		||||
            0 => ['found' => false, 'name' => null],
 | 
			
		||||
            1 => ['found' => true, 'name' => $this->personRender->renderString($persons[0], ['addAge' => false])],
 | 
			
		||||
            default => ['found' => true, 'name' => 'multiple'],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse($asArray);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\CRUD\Controller\ApiController;
 | 
			
		||||
use Doctrine\ORM\QueryBuilder;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
final class MotiveApiController extends ApiController
 | 
			
		||||
{
 | 
			
		||||
    protected function customizeQuery(string $action, Request $request, $query): void
 | 
			
		||||
    {
 | 
			
		||||
        /* @var $query QueryBuilder */
 | 
			
		||||
        $query->andWhere('e.active = TRUE');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\ReplaceMotiveCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class ReplaceMotiveController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private ReplaceMotiveCommandHandler $replaceMotiveCommandHandler,
 | 
			
		||||
        private SerializerInterface $serializer,
 | 
			
		||||
        private ValidatorInterface $validator,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/ticket/{id}/motive/set', name: 'chill_ticket_motive_set', methods: ['POST'])]
 | 
			
		||||
    public function __invoke(Ticket $ticket, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted('ROLE_USER')) {
 | 
			
		||||
            throw new AccessDeniedHttpException('');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $command = $this->serializer->deserialize($request->getContent(), ReplaceMotiveCommand::class, 'json', [
 | 
			
		||||
            AbstractNormalizer::GROUPS => ['write'],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $errors = $this->validator->validate($command);
 | 
			
		||||
 | 
			
		||||
        if (0 < $errors->count()) {
 | 
			
		||||
            return new JsonResponse(
 | 
			
		||||
                $this->serializer->serialize($errors, 'json'),
 | 
			
		||||
                Response::HTTP_UNPROCESSABLE_ENTITY,
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->replaceMotiveCommandHandler->handle($ticket, $command);
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(
 | 
			
		||||
            $this->serializer->serialize($ticket, 'json', ['groups' => 'read']),
 | 
			
		||||
            Response::HTTP_CREATED,
 | 
			
		||||
            [],
 | 
			
		||||
            true
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,85 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\AddAddresseeCommand;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\SetAddresseesCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\SetAddresseesCommand;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
 | 
			
		||||
 | 
			
		||||
final readonly class SetAddresseesController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private EntityManagerInterface $entityManager,
 | 
			
		||||
        private SerializerInterface $serializer,
 | 
			
		||||
        private SetAddresseesCommandHandler $addressesCommandHandler,
 | 
			
		||||
        private ValidatorInterface $validator,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/ticket/{id}/addressees/set', methods: ['POST'])]
 | 
			
		||||
    public function setAddressees(Ticket $ticket, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted('ROLE_USER')) {
 | 
			
		||||
            throw new AccessDeniedHttpException('Only users can set addressees.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $command = $this->serializer->deserialize($request->getContent(), SetAddresseesCommand::class, 'json', [AbstractNormalizer::GROUPS => ['read']]);
 | 
			
		||||
 | 
			
		||||
        return $this->registerSetAddressees($command, $ticket);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/ticket/{id}/addressee/add', methods: ['POST'])]
 | 
			
		||||
    public function addAddressee(Ticket $ticket, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted('ROLE_USER')) {
 | 
			
		||||
            throw new AccessDeniedHttpException('Only users can add addressees.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $command = $this->serializer->deserialize($request->getContent(), AddAddresseeCommand::class, 'json', [AbstractNormalizer::GROUPS => ['read']]);
 | 
			
		||||
 | 
			
		||||
        return $this->registerSetAddressees(SetAddresseesCommand::fromAddAddresseeCommand($command, $ticket), $ticket);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function registerSetAddressees(SetAddresseesCommand $command, Ticket $ticket): Response
 | 
			
		||||
    {
 | 
			
		||||
        if (0 < count($errors = $this->validator->validate($command))) {
 | 
			
		||||
            return new JsonResponse(
 | 
			
		||||
                $this->serializer->serialize($errors, 'json'),
 | 
			
		||||
                Response::HTTP_UNPROCESSABLE_ENTITY,
 | 
			
		||||
                [],
 | 
			
		||||
                true
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->addressesCommandHandler->handle($ticket, $command);
 | 
			
		||||
 | 
			
		||||
        $this->entityManager->flush();
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(
 | 
			
		||||
            $this->serializer->serialize($ticket, 'json', ['groups' => 'read']),
 | 
			
		||||
            Response::HTTP_OK,
 | 
			
		||||
            [],
 | 
			
		||||
            true,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,82 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\DataFixtures\ORM;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Entity\Motive;
 | 
			
		||||
use Doctrine\Bundle\FixturesBundle\Fixture;
 | 
			
		||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
 | 
			
		||||
use Doctrine\Persistence\ObjectManager;
 | 
			
		||||
 | 
			
		||||
final class LoadMotives extends Fixture implements FixtureGroupInterface
 | 
			
		||||
{
 | 
			
		||||
    public static function getGroups(): array
 | 
			
		||||
    {
 | 
			
		||||
        return ['ticket'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function load(ObjectManager $manager)
 | 
			
		||||
    {
 | 
			
		||||
        foreach (explode("\n", self::MOTIVES) as $label) {
 | 
			
		||||
            if ('' === trim($label)) {
 | 
			
		||||
                continue;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $motive = new Motive();
 | 
			
		||||
            $motive->setLabel(['fr' => trim($label)]);
 | 
			
		||||
 | 
			
		||||
            $manager->persist($motive);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $manager->flush();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private const MOTIVES = <<<'TXT'
 | 
			
		||||
        Coordonnées
 | 
			
		||||
        Horaire de passage
 | 
			
		||||
        Retard de livraison
 | 
			
		||||
        Erreur de livraison
 | 
			
		||||
        Colis incomplet
 | 
			
		||||
        MATLOC
 | 
			
		||||
        Retard DASRI
 | 
			
		||||
        Planning d'astreintes
 | 
			
		||||
        Planning des tournées
 | 
			
		||||
        Contrôle pompe
 | 
			
		||||
        Changement de rendez-vous
 | 
			
		||||
        Renseignement facturation/prestation
 | 
			
		||||
        Décès patient
 | 
			
		||||
        Demande de prise en charge
 | 
			
		||||
        Information absence
 | 
			
		||||
        Demande bulletin de situation
 | 
			
		||||
        Difficultés accès logement
 | 
			
		||||
        Déplacement inutile
 | 
			
		||||
        Problème de prélèvement/de commande
 | 
			
		||||
        Parc auto
 | 
			
		||||
        Demande d'admission
 | 
			
		||||
        Retrait de matériel au domicile
 | 
			
		||||
        Comptes-rendus
 | 
			
		||||
        Démarchage commercial
 | 
			
		||||
        Demande de transport
 | 
			
		||||
        Demande laboratoire
 | 
			
		||||
        Demande admission
 | 
			
		||||
        Suivi de prise en charge
 | 
			
		||||
        Mauvaise adresse
 | 
			
		||||
        Patient absent
 | 
			
		||||
        Annulation
 | 
			
		||||
        Colis perdu
 | 
			
		||||
        Changement de rendez-vous
 | 
			
		||||
        Coordination interservices
 | 
			
		||||
        Problème de substitution produits
 | 
			
		||||
        Problème ordonnance
 | 
			
		||||
        Réclamations facture
 | 
			
		||||
        Préparation urgente
 | 
			
		||||
        TXT;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,64 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\DependencyInjection;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Controller\MotiveApiController;
 | 
			
		||||
use Chill\TicketBundle\Entity\Motive;
 | 
			
		||||
use Symfony\Component\Config\FileLocator;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
 | 
			
		||||
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
 | 
			
		||||
use Symfony\Component\DependencyInjection\Loader;
 | 
			
		||||
use Symfony\Component\DependencyInjection\Extension\Extension;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
class ChillTicketExtension extends Extension implements PrependExtensionInterface
 | 
			
		||||
{
 | 
			
		||||
    public function load(array $configs, ContainerBuilder $container)
 | 
			
		||||
    {
 | 
			
		||||
        $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
 | 
			
		||||
        $loader->load('services.yaml');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function prepend(ContainerBuilder $container)
 | 
			
		||||
    {
 | 
			
		||||
        $this->prependApi($container);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function prependApi(ContainerBuilder $container): void
 | 
			
		||||
    {
 | 
			
		||||
        $container->prependExtensionConfig('chill_main', [
 | 
			
		||||
            'apis' => [
 | 
			
		||||
                [
 | 
			
		||||
                    'class' => Motive::class,
 | 
			
		||||
                    'name' => 'motive',
 | 
			
		||||
                    'base_path' => '/api/1.0/ticket/motive',
 | 
			
		||||
                    'controller' => MotiveApiController::class,
 | 
			
		||||
                    'base_role' => 'ROLE_USER',
 | 
			
		||||
                    'actions' => [
 | 
			
		||||
                        '_index' => [
 | 
			
		||||
                            'methods' => [
 | 
			
		||||
                                Request::METHOD_GET => true,
 | 
			
		||||
                                Request::METHOD_HEAD => true,
 | 
			
		||||
                            ],
 | 
			
		||||
                        ],
 | 
			
		||||
                        '_entity' => [
 | 
			
		||||
                            'methods' => [
 | 
			
		||||
                                Request::METHOD_GET => true,
 | 
			
		||||
                                Request::METHOD_HEAD => true,
 | 
			
		||||
                            ],
 | 
			
		||||
                        ],
 | 
			
		||||
                    ],
 | 
			
		||||
                ],
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										130
									
								
								src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/Bundle/ChillTicketBundle/src/Entity/AddresseeHistory.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,130 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity()]
 | 
			
		||||
#[ORM\Table(name: 'addressee_history', schema: 'chill_ticket')]
 | 
			
		||||
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_addressee_history' => AddresseeHistory::class])]
 | 
			
		||||
class AddresseeHistory implements TrackUpdateInterface, TrackCreationInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?User $addresseeUser = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: UserGroup::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?UserGroup $addresseeGroup = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => 'null'])]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?\DateTimeImmutable $endDate = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?User $removedBy = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        User|UserGroup $addressee,
 | 
			
		||||
        #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        private \DateTimeImmutable $startDate,
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: Ticket::class)]
 | 
			
		||||
        #[ORM\JoinColumn(nullable: false)]
 | 
			
		||||
        private Ticket $ticket,
 | 
			
		||||
    ) {
 | 
			
		||||
        if ($addressee instanceof User) {
 | 
			
		||||
            $this->addresseeUser = $addressee;
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->addresseeGroup = $addressee;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->ticket->addAddresseeHistory($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    public function getAddressee(): UserGroup|User
 | 
			
		||||
    {
 | 
			
		||||
        if (null !== $this->addresseeGroup) {
 | 
			
		||||
            return $this->addresseeGroup;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->addresseeUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAddresseeGroup(): ?UserGroup
 | 
			
		||||
    {
 | 
			
		||||
        return $this->addresseeGroup;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getAddresseeUser(): ?User
 | 
			
		||||
    {
 | 
			
		||||
        return $this->addresseeUser;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getEndDate(): ?\DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->endDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStartDate(): \DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->startDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTicket(): Ticket
 | 
			
		||||
    {
 | 
			
		||||
        return $this->ticket;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRemovedBy(): ?User
 | 
			
		||||
    {
 | 
			
		||||
        return $this->removedBy;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setRemovedBy(?User $removedBy): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->removedBy = $removedBy;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setEndDate(?\DateTimeImmutable $endDate): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->endDate = $endDate;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								src/Bundle/ChillTicketBundle/src/Entity/Comment.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/Bundle/ChillTicketBundle/src/Entity/Comment.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Doctrine\ORM\Mapping\JoinColumn;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity()]
 | 
			
		||||
#[ORM\Table(name: 'comment', schema: 'chill_ticket')]
 | 
			
		||||
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_comment' => Comment::class])]
 | 
			
		||||
class Comment implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        private string $content,
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: Ticket::class, inversedBy: 'comments')]
 | 
			
		||||
        #[JoinColumn(nullable: false)]
 | 
			
		||||
        private Ticket $ticket,
 | 
			
		||||
    ) {
 | 
			
		||||
        $ticket->addComment($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getContent(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->content;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTicket(): Ticket
 | 
			
		||||
    {
 | 
			
		||||
        return $this->ticket;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								src/Bundle/ChillTicketBundle/src/Entity/InputHistory.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/Bundle/ChillTicketBundle/src/Entity/InputHistory.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity()]
 | 
			
		||||
#[ORM\Table(name: 'input_history', schema: 'chill_ticket')]
 | 
			
		||||
class InputHistory
 | 
			
		||||
{
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: Person::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?Person $person = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: ThirdParty::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?ThirdParty $thirdParty = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])]
 | 
			
		||||
    private ?\DateTimeImmutable $endDate = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?User $removedBy = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        Person|ThirdParty $input,
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: Ticket::class)]
 | 
			
		||||
        #[ORM\JoinColumn(nullable: false)]
 | 
			
		||||
        private Ticket $ticket,
 | 
			
		||||
        #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
 | 
			
		||||
        private \DateTimeImmutable $startDate,
 | 
			
		||||
    ) {
 | 
			
		||||
        if ($input instanceof Person) {
 | 
			
		||||
            $this->person = $input;
 | 
			
		||||
        } else {
 | 
			
		||||
            $this->thirdParty = $input;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getEndDate(): ?\DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->endDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRemovedBy(): ?User
 | 
			
		||||
    {
 | 
			
		||||
        return $this->removedBy;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStartDate(): \DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->startDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTicket(): Ticket
 | 
			
		||||
    {
 | 
			
		||||
        return $this->ticket;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getInput(): Person|ThirdParty
 | 
			
		||||
    {
 | 
			
		||||
        if (null !== $this->person) {
 | 
			
		||||
            return $this->person;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->thirdParty;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								src/Bundle/ChillTicketBundle/src/Entity/Motive.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/Bundle/ChillTicketBundle/src/Entity/Motive.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity()]
 | 
			
		||||
#[ORM\Table(name: 'motive', schema: 'chill_ticket')]
 | 
			
		||||
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_motive' => Motive::class])]
 | 
			
		||||
class Motive
 | 
			
		||||
{
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private array $label = [];
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private bool $active = true;
 | 
			
		||||
 | 
			
		||||
    public function isActive(): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $this->active;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setActive(bool $active): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->active = $active;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getLabel(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->label;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setLabel(array $label): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->label = $label;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								src/Bundle/ChillTicketBundle/src/Entity/MotiveHistory.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/Bundle/ChillTicketBundle/src/Entity/MotiveHistory.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\Table(name: 'motives_history', schema: 'chill_ticket')]
 | 
			
		||||
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_motive_history' => MotiveHistory::class])]
 | 
			
		||||
class MotiveHistory implements TrackCreationInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?\DateTimeImmutable $endDate = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: Motive::class)]
 | 
			
		||||
        #[ORM\JoinColumn(nullable: false)]
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        private Motive $motive,
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: Ticket::class)]
 | 
			
		||||
        #[ORM\JoinColumn(nullable: false)]
 | 
			
		||||
        private Ticket $ticket,
 | 
			
		||||
        #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        private \DateTimeImmutable $startDate = new \DateTimeImmutable('now')
 | 
			
		||||
    ) {
 | 
			
		||||
        $ticket->addMotiveHistory($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getEndDate(): ?\DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->endDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getMotive(): Motive
 | 
			
		||||
    {
 | 
			
		||||
        return $this->motive;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStartDate(): \DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->startDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTicket(): Ticket
 | 
			
		||||
    {
 | 
			
		||||
        return $this->ticket;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setEndDate(?\DateTimeImmutable $endDate): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->endDate = $endDate;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										94
									
								
								src/Bundle/ChillTicketBundle/src/Entity/PersonHistory.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/Bundle/ChillTicketBundle/src/Entity/PersonHistory.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\Table(name: 'person_history', schema: 'chill_ticket')]
 | 
			
		||||
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_person_history' => PersonHistory::class])]
 | 
			
		||||
class PersonHistory implements TrackCreationInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?\DateTimeImmutable $endDate = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    #[Serializer\Groups(['read'])]
 | 
			
		||||
    private ?User $removedBy = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: Person::class, fetch: 'EAGER')]
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        private Person $person,
 | 
			
		||||
        #[ORM\ManyToOne(targetEntity: Ticket::class)]
 | 
			
		||||
        #[ORM\JoinColumn(nullable: false)]
 | 
			
		||||
        private Ticket $ticket,
 | 
			
		||||
        #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
 | 
			
		||||
        #[Serializer\Groups(['read'])]
 | 
			
		||||
        private \DateTimeImmutable $startDate,
 | 
			
		||||
    ) {
 | 
			
		||||
        // keep ticket instance in sync with this
 | 
			
		||||
        $this->ticket->addPersonHistory($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getPerson(): Person
 | 
			
		||||
    {
 | 
			
		||||
        return $this->person;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getEndDate(): ?\DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->endDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getTicket(): Ticket
 | 
			
		||||
    {
 | 
			
		||||
        return $this->ticket;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getStartDate(): \DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->startDate;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getRemovedBy(): ?User
 | 
			
		||||
    {
 | 
			
		||||
        return $this->removedBy;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setEndDate(?\DateTimeImmutable $endDate): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->endDate = $endDate;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										231
									
								
								src/Bundle/ChillTicketBundle/src/Entity/Ticket.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								src/Bundle/ChillTicketBundle/src/Entity/Ticket.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,231 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Entity;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
 | 
			
		||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
 | 
			
		||||
use Doctrine\Common\Collections\ArrayCollection;
 | 
			
		||||
use Doctrine\Common\Collections\Collection;
 | 
			
		||||
use Doctrine\Common\Collections\ReadableCollection;
 | 
			
		||||
use Doctrine\ORM\Mapping as ORM;
 | 
			
		||||
 | 
			
		||||
#[ORM\Entity]
 | 
			
		||||
#[ORM\Table(name: 'ticket', schema: 'chill_ticket')]
 | 
			
		||||
class Ticket implements TrackCreationInterface, TrackUpdateInterface
 | 
			
		||||
{
 | 
			
		||||
    use TrackCreationTrait;
 | 
			
		||||
    use TrackUpdateTrait;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Id]
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
 | 
			
		||||
    #[ORM\GeneratedValue(strategy: 'AUTO')]
 | 
			
		||||
    private ?int $id = null;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<int, AddresseeHistory>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: AddresseeHistory::class, mappedBy: 'ticket')]
 | 
			
		||||
    private Collection $addresseeHistory;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<int, Comment>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'ticket')]
 | 
			
		||||
    private Collection $comments;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
 | 
			
		||||
    private string $externalRef = '';
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<int, InputHistory>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: InputHistory::class, mappedBy: 'ticket')]
 | 
			
		||||
    private Collection $inputHistories;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<int, MotiveHistory>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: MotiveHistory::class, mappedBy: 'ticket')]
 | 
			
		||||
    private Collection $motiveHistories;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var Collection<int, PersonHistory>
 | 
			
		||||
     */
 | 
			
		||||
    #[ORM\OneToMany(targetEntity: PersonHistory::class, mappedBy: 'ticket')]
 | 
			
		||||
    private Collection $personHistories;
 | 
			
		||||
 | 
			
		||||
    #[ORM\ManyToOne(targetEntity: User::class)]
 | 
			
		||||
    #[ORM\JoinColumn(nullable: true)]
 | 
			
		||||
    private ?User $updatedBy = null;
 | 
			
		||||
 | 
			
		||||
    #[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true)]
 | 
			
		||||
    private ?\DateTimeImmutable $createdAt = null;
 | 
			
		||||
 | 
			
		||||
    public function __construct()
 | 
			
		||||
    {
 | 
			
		||||
        $this->addresseeHistory = new ArrayCollection();
 | 
			
		||||
        $this->comments = new ArrayCollection();
 | 
			
		||||
        $this->motiveHistories = new ArrayCollection();
 | 
			
		||||
        $this->personHistories = new ArrayCollection();
 | 
			
		||||
        $this->inputHistories = new ArrayCollection();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getId(): ?int
 | 
			
		||||
    {
 | 
			
		||||
        return $this->id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getExternalRef(): string
 | 
			
		||||
    {
 | 
			
		||||
        return $this->externalRef;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setExternalRef(string $externalRef): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->externalRef = $externalRef;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return list<Person>
 | 
			
		||||
     */
 | 
			
		||||
    public function getPersons(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->personHistories
 | 
			
		||||
            ->filter(fn (PersonHistory $personHistory) => null === $personHistory->getEndDate())
 | 
			
		||||
            ->map(fn (PersonHistory $personHistory) => $personHistory->getPerson())
 | 
			
		||||
            ->getValues();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @internal use @see{Comment::__construct} instead
 | 
			
		||||
     */
 | 
			
		||||
    public function addComment(Comment $comment): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->comments->add($comment);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Add a PersonHistory.
 | 
			
		||||
     *
 | 
			
		||||
     * @internal use @see{PersonHistory::__construct} instead
 | 
			
		||||
     */
 | 
			
		||||
    public function addPersonHistory(PersonHistory $personHistory): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->personHistories->add($personHistory);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @internal use @see{MotiveHistory::__construct} instead
 | 
			
		||||
     */
 | 
			
		||||
    public function addMotiveHistory(MotiveHistory $motiveHistory): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->motiveHistories->add($motiveHistory);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @internal use @see{AddresseHistory::__construct} instead
 | 
			
		||||
     */
 | 
			
		||||
    public function addAddresseeHistory(AddresseeHistory $addresseeHistory): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addresseeHistory->add($addresseeHistory);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return list<UserGroup|User>
 | 
			
		||||
     */
 | 
			
		||||
    public function getCurrentAddressee(): array
 | 
			
		||||
    {
 | 
			
		||||
        $addresses = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($this->addresseeHistory
 | 
			
		||||
            ->filter(fn (AddresseeHistory $addresseeHistory) => null === $addresseeHistory->getEndDate()) as $addressHistory) {
 | 
			
		||||
            $addresses[] = $addressHistory->getAddressee();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $addresses;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ReadableCollection<int, Comment>
 | 
			
		||||
     */
 | 
			
		||||
    public function getComments(): ReadableCollection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->comments;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return list<ThirdParty|Person>
 | 
			
		||||
     */
 | 
			
		||||
    public function getCurrentInputs(): array
 | 
			
		||||
    {
 | 
			
		||||
        $inputs = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($this->inputHistories
 | 
			
		||||
            ->filter(fn (InputHistory $inputHistory) => null === $inputHistory->getEndDate()) as $inputHistory
 | 
			
		||||
        ) {
 | 
			
		||||
            $inputs[] = $inputHistory->getInput();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $inputs;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getMotive(): ?Motive
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($this->motiveHistories as $motiveHistory) {
 | 
			
		||||
            if (null === $motiveHistory->getEndDate()) {
 | 
			
		||||
                return $motiveHistory->getMotive();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ReadableCollection<int, MotiveHistory>
 | 
			
		||||
     */
 | 
			
		||||
    public function getMotiveHistories(): ReadableCollection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->motiveHistories;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ReadableCollection<int, PersonHistory>
 | 
			
		||||
     */
 | 
			
		||||
    public function getPersonHistories(): ReadableCollection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->personHistories;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return ReadableCollection<int, AddresseeHistory>
 | 
			
		||||
     */
 | 
			
		||||
    public function getAddresseeHistories(): ReadableCollection
 | 
			
		||||
    {
 | 
			
		||||
        return $this->addresseeHistory;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getCreatedAt(): \DateTimeImmutable
 | 
			
		||||
    {
 | 
			
		||||
        return $this->createdAt;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getUpdatedBy(): ?User
 | 
			
		||||
    {
 | 
			
		||||
        return $this->updatedBy;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
final readonly class TicketRepository implements TicketRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
    private ObjectRepository $repository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $objectManager)
 | 
			
		||||
    {
 | 
			
		||||
        $this->repository = $objectManager->getRepository($this->getClassName());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function find($id): ?Ticket
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->find($id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAll(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria): ?Ticket
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName()
 | 
			
		||||
    {
 | 
			
		||||
        return Ticket::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneByExternalRef(string $extId): ?Ticket
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findOneBy(['externalRef' => $extId]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,23 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @extends ObjectRepository<Ticket>
 | 
			
		||||
 */
 | 
			
		||||
interface TicketRepositoryInterface extends ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    public function findOneByExternalRef(string $extId): ?Ticket;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,24 @@
 | 
			
		||||
@import '~ChillMainAssets/module/bootstrap/shared';
 | 
			
		||||
 | 
			
		||||
div.banner {
 | 
			
		||||
    div#header-ticket-main {
 | 
			
		||||
        background: none repeat scroll 0 0 #ae986fFF;
 | 
			
		||||
        color: $white;
 | 
			
		||||
        padding-top: 1em;
 | 
			
		||||
        padding-bottom: 1em;
 | 
			
		||||
    }
 | 
			
		||||
    div#header-ticket-details {
 | 
			
		||||
        background: none repeat scroll 0 0 #d3c7b1FF;
 | 
			
		||||
        color: $white;
 | 
			
		||||
        padding-top: 1em;
 | 
			
		||||
        padding-bottom: 1em;
 | 
			
		||||
        div.contact {
 | 
			
		||||
            display: flex;
 | 
			
		||||
            align-content: center;
 | 
			
		||||
            & > * {
 | 
			
		||||
                margin-right: 1em;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
import './banner.scss';
 | 
			
		||||
							
								
								
									
										86
									
								
								src/Bundle/ChillTicketBundle/src/Resources/public/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/Bundle/ChillTicketBundle/src/Resources/public/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
import {
 | 
			
		||||
    DateTime,
 | 
			
		||||
    TranslatableString,
 | 
			
		||||
    User,
 | 
			
		||||
    UserGroupOrUser
 | 
			
		||||
} from "../../../../ChillMainBundle/Resources/public/types";
 | 
			
		||||
import { Person } from "../../../../ChillPersonBundle/Resources/public/types";
 | 
			
		||||
 | 
			
		||||
export interface Motive {
 | 
			
		||||
    type: "ticket_motive"
 | 
			
		||||
    id: number,
 | 
			
		||||
    active: boolean,
 | 
			
		||||
    label: TranslatableString
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TicketHistory<T extends string, D extends object> {
 | 
			
		||||
    event_type: T,
 | 
			
		||||
    at: DateTime,
 | 
			
		||||
    by: User,
 | 
			
		||||
    data: D
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PersonHistory {
 | 
			
		||||
    type: "ticket_person_history",
 | 
			
		||||
    id: number,
 | 
			
		||||
    startDate: DateTime,
 | 
			
		||||
    endDate: null|DateTime,
 | 
			
		||||
    person: Person,
 | 
			
		||||
    removedBy: null,
 | 
			
		||||
    createdBy: User|null,
 | 
			
		||||
    createdAt: DateTime|null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MotiveHistory {
 | 
			
		||||
    type: "ticket_motive_history",
 | 
			
		||||
    id: number,
 | 
			
		||||
    startDate: null,
 | 
			
		||||
    endDate: null|DateTime,
 | 
			
		||||
    motive: Motive,
 | 
			
		||||
    createdBy: User|null,
 | 
			
		||||
    createdAt: DateTime|null,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Comment {
 | 
			
		||||
    type: "ticket_comment",
 | 
			
		||||
    id: number,
 | 
			
		||||
    content: string,
 | 
			
		||||
    createdBy: User|null,
 | 
			
		||||
    createdAt: DateTime|null,
 | 
			
		||||
    updatedBy: User|null,
 | 
			
		||||
    updatedAt: DateTime|null,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AddresseeHistory {
 | 
			
		||||
    type: "ticket_addressee_history",
 | 
			
		||||
    id: number,
 | 
			
		||||
    startDate: DateTime|null,
 | 
			
		||||
    addressee: UserGroupOrUser,
 | 
			
		||||
    endDate: DateTime|null,
 | 
			
		||||
    removedBy: User|null,
 | 
			
		||||
    createdBy: User|null,
 | 
			
		||||
    createdAt: DateTime|null,
 | 
			
		||||
    updatedBy: User|null,
 | 
			
		||||
    updatedAt: DateTime|null,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface AddPersonEvent extends TicketHistory<"add_person", PersonHistory> {};
 | 
			
		||||
interface AddCommentEvent extends TicketHistory<"add_comment", Comment> {};
 | 
			
		||||
interface SetMotiveEvent extends TicketHistory<"set_motive", MotiveHistory> {};
 | 
			
		||||
interface AddAddressee extends TicketHistory<"add_addressee", AddresseeHistory> {};
 | 
			
		||||
interface RemoveAddressee extends TicketHistory<"remove_addressee", AddresseeHistory> {};
 | 
			
		||||
 | 
			
		||||
type TicketHistoryLine = AddPersonEvent | AddCommentEvent | SetMotiveEvent | AddAddressee | RemoveAddressee;
 | 
			
		||||
 | 
			
		||||
export interface Ticket {
 | 
			
		||||
    type: "ticket_ticket",
 | 
			
		||||
    id: number,
 | 
			
		||||
    externalRef: string,
 | 
			
		||||
    currentAddressees: UserGroupOrUser[],
 | 
			
		||||
    currentPersons: Person[],
 | 
			
		||||
    currentMotive: null|Motive,
 | 
			
		||||
    history: TicketHistoryLine[],
 | 
			
		||||
    createdAt: DateTime|null,
 | 
			
		||||
    updatedBy: User|null,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <banner-component :ticket="ticket" />
 | 
			
		||||
    <div class="container-xxl pt-1" style="padding-bottom: 55px">
 | 
			
		||||
        <ticket-selector-component :tickets="[]" />
 | 
			
		||||
        <ticket-history-list-component :history="ticketHistory" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <action-toolbar-component />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, inject, onMounted, ref } from "vue";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import { Motive, Ticket } from "../../types";
 | 
			
		||||
 | 
			
		||||
// Components
 | 
			
		||||
import TicketSelectorComponent from "./components/TicketSelectorComponent.vue";
 | 
			
		||||
import TicketHistoryListComponent from "./components/TicketHistoryListComponent.vue";
 | 
			
		||||
import ActionToolbarComponent from "./components/ActionToolbarComponent.vue";
 | 
			
		||||
import BannerComponent from "./components/BannerComponent.vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "App",
 | 
			
		||||
    components: {
 | 
			
		||||
        TicketSelectorComponent,
 | 
			
		||||
        TicketHistoryListComponent,
 | 
			
		||||
        ActionToolbarComponent,
 | 
			
		||||
        BannerComponent,
 | 
			
		||||
    },
 | 
			
		||||
    setup() {
 | 
			
		||||
        const store = useStore();
 | 
			
		||||
        const toast = inject("toast") as any;
 | 
			
		||||
 | 
			
		||||
        store.commit("setTicket", JSON.parse(window.initialTicket) as Ticket);
 | 
			
		||||
 | 
			
		||||
        const motives = computed(() => store.getters.getMotives as Motive[]);
 | 
			
		||||
        const ticket = computed(() => store.getters.getTicket as Ticket);
 | 
			
		||||
 | 
			
		||||
        const ticketHistory = computed(
 | 
			
		||||
            () => store.getters.getDistinctAddressesHistory
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        onMounted(async () => {
 | 
			
		||||
            try {
 | 
			
		||||
                await store.dispatch("fetchMotives");
 | 
			
		||||
                await store.dispatch("fetchUserGroups");
 | 
			
		||||
                await store.dispatch("fetchUsers");
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                toast.error(error);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            ticketHistory,
 | 
			
		||||
            motives,
 | 
			
		||||
            ticket,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,254 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="fixed-bottom">
 | 
			
		||||
        <div class="footer-ticket-details" v-if="activeTab">
 | 
			
		||||
            <div class="tab-content p-2">
 | 
			
		||||
                <div>
 | 
			
		||||
                    <label class="col-form-label">
 | 
			
		||||
                        {{ $t(`${activeTab}.title`) }}
 | 
			
		||||
                    </label>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <form @submit.prevent="submitAction">
 | 
			
		||||
                    <add-comment-component
 | 
			
		||||
                        v-model="content"
 | 
			
		||||
                        v-if="activeTab === 'add_comment'"
 | 
			
		||||
                    />
 | 
			
		||||
 | 
			
		||||
                    <addressee-selector-component
 | 
			
		||||
                        v-model="addressees"
 | 
			
		||||
                        :user-groups="userGroups"
 | 
			
		||||
                        :users="users"
 | 
			
		||||
                        v-if="activeTab === 'add_addressee'"
 | 
			
		||||
                    />
 | 
			
		||||
 | 
			
		||||
                    <motive-selector-component
 | 
			
		||||
                        v-model="motive"
 | 
			
		||||
                        :motives="motives"
 | 
			
		||||
                        v-if="activeTab === 'set_motive'"
 | 
			
		||||
                    />
 | 
			
		||||
 | 
			
		||||
                    <ul class="record_actions sticky-form-buttons">
 | 
			
		||||
                        <li class="cancel">
 | 
			
		||||
                            <button
 | 
			
		||||
                                @click="activeTab = ''"
 | 
			
		||||
                                class="btn btn-cancel"
 | 
			
		||||
                            >
 | 
			
		||||
                                {{ $t("ticket.cancel") }}
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </li>
 | 
			
		||||
                        <li>
 | 
			
		||||
                            <button class="btn btn-create" type="submit">
 | 
			
		||||
                                {{ $t("ticket.save") }}
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </form>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="footer-ticket-main">
 | 
			
		||||
            <ul class="nav nav-tabs justify-content-end">
 | 
			
		||||
                <li class="nav-item p-2">
 | 
			
		||||
                    <button
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        :class="`btn ${
 | 
			
		||||
                            activeTab === 'set_motive'
 | 
			
		||||
                                ? 'btn-primary'
 | 
			
		||||
                                : 'btn-light'
 | 
			
		||||
                        }`"
 | 
			
		||||
                        @click="
 | 
			
		||||
                            activeTab === 'set_motive'
 | 
			
		||||
                                ? (activeTab = '')
 | 
			
		||||
                                : (activeTab = 'set_motive')
 | 
			
		||||
                        "
 | 
			
		||||
                    >
 | 
			
		||||
                        <i :class="actionIcons['set_motive']"></i>
 | 
			
		||||
                        {{ $t("set_motive.title") }}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item p-2">
 | 
			
		||||
                    <button
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        :class="`btn ${
 | 
			
		||||
                            activeTab === 'add_comment'
 | 
			
		||||
                                ? 'btn-primary'
 | 
			
		||||
                                : 'btn-light'
 | 
			
		||||
                        }`"
 | 
			
		||||
                        @click="
 | 
			
		||||
                            activeTab === 'add_comment'
 | 
			
		||||
                                ? (activeTab = '')
 | 
			
		||||
                                : (activeTab = 'add_comment')
 | 
			
		||||
                        "
 | 
			
		||||
                    >
 | 
			
		||||
                        <i :class="actionIcons['add_comment']"></i>
 | 
			
		||||
                        {{ $t("add_comment.title") }}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li class="nav-item p-2">
 | 
			
		||||
                    <button
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        :class="`btn ${
 | 
			
		||||
                            activeTab === 'add_addressee'
 | 
			
		||||
                                ? 'btn-primary'
 | 
			
		||||
                                : 'btn-light'
 | 
			
		||||
                        }`"
 | 
			
		||||
                        @click="
 | 
			
		||||
                            activeTab === 'add_addressee'
 | 
			
		||||
                                ? (activeTab = '')
 | 
			
		||||
                                : (activeTab = 'add_addressee')
 | 
			
		||||
                        "
 | 
			
		||||
                    >
 | 
			
		||||
                        <i :class="actionIcons['add_addressee']"></i>
 | 
			
		||||
                        {{ $t("add_addressee.title") }}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </li>
 | 
			
		||||
 | 
			
		||||
                <li class="nav-item p-2">
 | 
			
		||||
                    <button
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        class="btn btn-light"
 | 
			
		||||
                        @click="handleClick()"
 | 
			
		||||
                    >
 | 
			
		||||
                        <i class="fa fa-bolt"></i>
 | 
			
		||||
                        {{ $t("ticket.close") }}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, inject, ref } from "vue";
 | 
			
		||||
import { useI18n } from "vue-i18n";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import {
 | 
			
		||||
    User,
 | 
			
		||||
    UserGroup,
 | 
			
		||||
    UserGroupOrUser,
 | 
			
		||||
} from "../../../../../../../ChillMainBundle/Resources/public/types";
 | 
			
		||||
import { Comment, Motive, Ticket } from "../../../types";
 | 
			
		||||
 | 
			
		||||
// Component
 | 
			
		||||
import MotiveSelectorComponent from "./MotiveSelectorComponent.vue";
 | 
			
		||||
import AddresseeSelectorComponent from "./AddresseeSelectorComponent.vue";
 | 
			
		||||
import AddCommentComponent from "./AddCommentComponent.vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "ActionToolbarComponent",
 | 
			
		||||
    components: {
 | 
			
		||||
        AddCommentComponent,
 | 
			
		||||
        MotiveSelectorComponent,
 | 
			
		||||
        AddresseeSelectorComponent,
 | 
			
		||||
    },
 | 
			
		||||
    setup() {
 | 
			
		||||
        const store = useStore();
 | 
			
		||||
        const { t } = useI18n();
 | 
			
		||||
        const toast = inject("toast") as any;
 | 
			
		||||
        const activeTab = ref(
 | 
			
		||||
            "" as "" | "add_comment" | "set_motive" | "add_addressee"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const ticket = computed(() => store.getters.getTicket as Ticket);
 | 
			
		||||
        const motives = computed(() => store.getters.getMotives as Motive[]);
 | 
			
		||||
        const userGroups = computed(
 | 
			
		||||
            () => store.getters.getUserGroups as UserGroup[]
 | 
			
		||||
        );
 | 
			
		||||
        const users = computed(() => store.getters.getUsers as User[]);
 | 
			
		||||
 | 
			
		||||
        const motive = ref(
 | 
			
		||||
            ticket.value.currentMotive
 | 
			
		||||
                ? ticket.value.currentMotive
 | 
			
		||||
                : ({} as Motive)
 | 
			
		||||
        );
 | 
			
		||||
        const content = ref("" as Comment["content"]);
 | 
			
		||||
        const addressees = ref(
 | 
			
		||||
            ticket.value.currentAddressees as Array<UserGroupOrUser>
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        async function submitAction() {
 | 
			
		||||
            try {
 | 
			
		||||
                switch (activeTab.value) {
 | 
			
		||||
                    case "add_comment":
 | 
			
		||||
                        if (!content.value) {
 | 
			
		||||
                            toast.error(t("add_comment.error"));
 | 
			
		||||
                        } else {
 | 
			
		||||
                            await store.dispatch("createComment", {
 | 
			
		||||
                                ticketId: ticket.value.id,
 | 
			
		||||
                                content: content.value,
 | 
			
		||||
                            });
 | 
			
		||||
                            content.value = "";
 | 
			
		||||
                            activeTab.value = "";
 | 
			
		||||
                            toast.success(t("add_comment.success"));
 | 
			
		||||
                        }
 | 
			
		||||
                        break;
 | 
			
		||||
                    case "set_motive":
 | 
			
		||||
                        if (!motive.value.id) {
 | 
			
		||||
                            toast.error(t("set_motive.error"));
 | 
			
		||||
                        } else {
 | 
			
		||||
                            await store.dispatch("createMotive", {
 | 
			
		||||
                                ticketId: ticket.value.id,
 | 
			
		||||
                                motive: motive.value,
 | 
			
		||||
                            });
 | 
			
		||||
                            activeTab.value = "";
 | 
			
		||||
                            toast.success(t("set_motive.success"));
 | 
			
		||||
                        }
 | 
			
		||||
                        break;
 | 
			
		||||
                    case "add_addressee":
 | 
			
		||||
                        if (!addressees.value.length) {
 | 
			
		||||
                            toast.error(t("add_addressee.error"));
 | 
			
		||||
                        } else {
 | 
			
		||||
                            await store.dispatch("setAdressees", {
 | 
			
		||||
                                ticketId: ticket.value.id,
 | 
			
		||||
                                addressees: addressees.value,
 | 
			
		||||
                            });
 | 
			
		||||
                            activeTab.value = "";
 | 
			
		||||
                            toast.success(t("add_addressee.success"));
 | 
			
		||||
                        }
 | 
			
		||||
                        break;
 | 
			
		||||
                }
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                toast.error(error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function handleClick() {
 | 
			
		||||
            alert("Sera disponible plus tard");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            actionIcons: ref(store.getters.getActionIcons),
 | 
			
		||||
            activeTab,
 | 
			
		||||
            ticket,
 | 
			
		||||
            motives,
 | 
			
		||||
            motive,
 | 
			
		||||
            userGroups,
 | 
			
		||||
            addressees,
 | 
			
		||||
            users,
 | 
			
		||||
            content,
 | 
			
		||||
            submitAction,
 | 
			
		||||
            handleClick,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.sticky-form-buttons {
 | 
			
		||||
    margin-top: 0px;
 | 
			
		||||
    background: none;
 | 
			
		||||
}
 | 
			
		||||
div.footer-ticket-main {
 | 
			
		||||
    background: none repeat scroll 0 0 #cabb9f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.footer-ticket-details {
 | 
			
		||||
    background: none repeat scroll 0 0 #efe2ca;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.fixed-bottom {
 | 
			
		||||
    max-width: 1272px;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,49 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <ckeditor
 | 
			
		||||
                name="content"
 | 
			
		||||
                :placeholder="$t('add_comment.content')"
 | 
			
		||||
                :editor="editor"
 | 
			
		||||
                v-model="content"
 | 
			
		||||
                tag-name="textarea"
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { defineComponent, ref, watch } from "vue";
 | 
			
		||||
 | 
			
		||||
import CKEditor from "@ckeditor/ckeditor5-vue";
 | 
			
		||||
import ClassicEditor from "../../../../../../../ChillMainBundle/Resources/public/module/ckeditor5";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "AddCommentComponent",
 | 
			
		||||
    props: {
 | 
			
		||||
        modelValue: {
 | 
			
		||||
            type: String,
 | 
			
		||||
            required: false,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
        ckeditor: CKEditor.component,
 | 
			
		||||
    },
 | 
			
		||||
    emits: ["update:modelValue"],
 | 
			
		||||
 | 
			
		||||
    setup(props, ctx) {
 | 
			
		||||
        const content = ref(props.modelValue);
 | 
			
		||||
 | 
			
		||||
        watch(content, (content) => {
 | 
			
		||||
            ctx.emit("update:modelValue", content);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            content,
 | 
			
		||||
            editor: ClassicEditor,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,77 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <h3>
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <span
 | 
			
		||||
                class="badge m-1"
 | 
			
		||||
                :style="`background-color: ${userGroup.backgroundColor}; color: white;`"
 | 
			
		||||
                v-for="userGroup in userGroupLevels"
 | 
			
		||||
                :key="userGroup.id"
 | 
			
		||||
            >
 | 
			
		||||
                {{ userGroup.label.fr }}
 | 
			
		||||
            </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <span
 | 
			
		||||
                class="badge m-1"
 | 
			
		||||
                :style="`background-color: ${userGroup.backgroundColor}; color: white;`"
 | 
			
		||||
                v-for="userGroup in userGroups"
 | 
			
		||||
                :key="userGroup.id"
 | 
			
		||||
            >
 | 
			
		||||
                {{ userGroup.label.fr }}
 | 
			
		||||
            </span>
 | 
			
		||||
        </div>
 | 
			
		||||
    </h3>
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <span class="badge bg-primary m-1" v-for="user in users" :key="user.id">
 | 
			
		||||
            {{ user.label }}
 | 
			
		||||
        </span>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, computed, defineComponent, ref } from "vue";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import {
 | 
			
		||||
    User,
 | 
			
		||||
    UserGroup,
 | 
			
		||||
    UserGroupOrUser,
 | 
			
		||||
} from "../../../../../../../ChillMainBundle/Resources/public/types";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "AddresseeComponent",
 | 
			
		||||
    props: {
 | 
			
		||||
        addressees: {
 | 
			
		||||
            type: Array as PropType<UserGroupOrUser[]>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    setup(props, ctx) {
 | 
			
		||||
        const userGroups = computed(
 | 
			
		||||
            () =>
 | 
			
		||||
                props.addressees.filter(
 | 
			
		||||
                    (addressee) =>
 | 
			
		||||
                        addressee.type == "user_group" &&
 | 
			
		||||
                        addressee.excludeKey == ""
 | 
			
		||||
                ) as UserGroup[]
 | 
			
		||||
        );
 | 
			
		||||
        const userGroupLevels = computed(
 | 
			
		||||
            () =>
 | 
			
		||||
                props.addressees.filter(
 | 
			
		||||
                    (addressee) =>
 | 
			
		||||
                        addressee.type == "user_group" &&
 | 
			
		||||
                        addressee.excludeKey == "level"
 | 
			
		||||
                ) as UserGroup[]
 | 
			
		||||
        );
 | 
			
		||||
        const users = computed(
 | 
			
		||||
            () =>
 | 
			
		||||
                props.addressees.filter(
 | 
			
		||||
                    (addressee) => addressee.type == "user"
 | 
			
		||||
                ) as User[]
 | 
			
		||||
        );
 | 
			
		||||
        return { userGroups, users, userGroupLevels };
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,241 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12 col-lg-6 col-md-6 text-center">
 | 
			
		||||
            <div class="mb-2">
 | 
			
		||||
                <span
 | 
			
		||||
                    v-for="userGroupItem in userGroups.filter(
 | 
			
		||||
                        (userGroup) => userGroup.excludeKey == 'level'
 | 
			
		||||
                    )"
 | 
			
		||||
                    :key="userGroupItem.id"
 | 
			
		||||
                    class="m-1"
 | 
			
		||||
                >
 | 
			
		||||
                    <input
 | 
			
		||||
                        type="radio"
 | 
			
		||||
                        class="btn-check"
 | 
			
		||||
                        name="options-outlined"
 | 
			
		||||
                        :id="`level-${userGroupItem.id}`"
 | 
			
		||||
                        autocomplete="off"
 | 
			
		||||
                        :value="userGroupItem"
 | 
			
		||||
                        v-model="userGroupLevel"
 | 
			
		||||
                        @click="
 | 
			
		||||
                            Object.values(userGroupLevel).includes(
 | 
			
		||||
                                userGroupItem.id
 | 
			
		||||
                            )
 | 
			
		||||
                                ? (userGroupLevel = {})
 | 
			
		||||
                                : (userGroupLevel = userGroupItem)
 | 
			
		||||
                        "
 | 
			
		||||
                    />
 | 
			
		||||
                    <label
 | 
			
		||||
                        :class="`btn btn-${userGroupItem.id}`"
 | 
			
		||||
                        :for="`level-${userGroupItem.id}`"
 | 
			
		||||
                        :style="getUserGroupBtnColor(userGroupItem)"
 | 
			
		||||
                    >
 | 
			
		||||
                        {{ userGroupItem.label.fr }}
 | 
			
		||||
                    </label>
 | 
			
		||||
                </span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="mb-2">
 | 
			
		||||
                <span
 | 
			
		||||
                    v-for="userGroupItem in userGroups.filter(
 | 
			
		||||
                        (userGroup) => userGroup.excludeKey == ''
 | 
			
		||||
                    )"
 | 
			
		||||
                    :key="userGroupItem.id"
 | 
			
		||||
                    class="m-1"
 | 
			
		||||
                >
 | 
			
		||||
                    <input
 | 
			
		||||
                        type="checkbox"
 | 
			
		||||
                        class="btn-check"
 | 
			
		||||
                        name="options-outlined"
 | 
			
		||||
                        :id="`user-group-${userGroupItem.id}`"
 | 
			
		||||
                        autocomplete="off"
 | 
			
		||||
                        :value="userGroupItem"
 | 
			
		||||
                        v-model="userGroup"
 | 
			
		||||
                    />
 | 
			
		||||
                    <label
 | 
			
		||||
                        :class="`btn btn-${userGroupItem.id}`"
 | 
			
		||||
                        :for="`user-group-${userGroupItem.id}`"
 | 
			
		||||
                        :style="getUserGroupBtnColor(userGroupItem)"
 | 
			
		||||
                    >
 | 
			
		||||
                        {{ userGroupItem.label.fr }}
 | 
			
		||||
                    </label>
 | 
			
		||||
                </span>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="col-12 col-lg-6 col-md-6 mb-2 mb-2 text-center">
 | 
			
		||||
            <add-persons
 | 
			
		||||
                :options="addPersonsOptions"
 | 
			
		||||
                key="add-person-ticket"
 | 
			
		||||
                buttonTitle="add_addressee.user_label"
 | 
			
		||||
                modalTitle="add_addressee.user_label"
 | 
			
		||||
                ref="addPersons"
 | 
			
		||||
                @addNewPersons="addNewEntity"
 | 
			
		||||
            />
 | 
			
		||||
            <div class="p-2">
 | 
			
		||||
                <ul class="list-suggest inline remove-items">
 | 
			
		||||
                    <li v-for="user in users" :key="user.id">
 | 
			
		||||
                        <span :title="user.username" @click="removeUser(user)">
 | 
			
		||||
                            {{ user.username }}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </li>
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, computed, defineComponent, ref, watch } from "vue";
 | 
			
		||||
import { useI18n } from "vue-i18n";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import {
 | 
			
		||||
    User,
 | 
			
		||||
    UserGroup,
 | 
			
		||||
    UserGroupOrUser,
 | 
			
		||||
} from "../../../../../../../ChillMainBundle/Resources/public/types";
 | 
			
		||||
 | 
			
		||||
// Components
 | 
			
		||||
import AddPersons from "../../../../../../../ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "AddresseeSelectorComponent",
 | 
			
		||||
    props: {
 | 
			
		||||
        modelValue: {
 | 
			
		||||
            type: Array as PropType<UserGroupOrUser[]>,
 | 
			
		||||
            default: [],
 | 
			
		||||
            required: false,
 | 
			
		||||
        },
 | 
			
		||||
        userGroups: {
 | 
			
		||||
            type: Array as PropType<UserGroup[]>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
        users: {
 | 
			
		||||
            type: Array as PropType<User[]>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
        AddPersons,
 | 
			
		||||
    },
 | 
			
		||||
    emits: ["update:modelValue"],
 | 
			
		||||
 | 
			
		||||
    setup(props, ctx) {
 | 
			
		||||
        const addressees = ref([...props.modelValue] as UserGroupOrUser[]);
 | 
			
		||||
        const userGroups = [
 | 
			
		||||
            ...props.modelValue.filter(
 | 
			
		||||
                (addressee) => addressee.type == "user_group"
 | 
			
		||||
            ),
 | 
			
		||||
        ] as UserGroup[];
 | 
			
		||||
 | 
			
		||||
        const userGroupLevel = ref(
 | 
			
		||||
            userGroups.filter(
 | 
			
		||||
                (userGroup) => userGroup.excludeKey == "level"
 | 
			
		||||
            )[0] as UserGroup | {}
 | 
			
		||||
        );
 | 
			
		||||
        const userGroup = ref(
 | 
			
		||||
            userGroups.filter((userGroup) => userGroup.excludeKey == "") as
 | 
			
		||||
                | UserGroup[]
 | 
			
		||||
        );
 | 
			
		||||
        const users = ref([
 | 
			
		||||
            ...props.modelValue.filter((addressee) => addressee.type == "user"),
 | 
			
		||||
        ] as User[]);
 | 
			
		||||
        const addPersons = ref();
 | 
			
		||||
 | 
			
		||||
        const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
        function getUserGroupBtnColor(userGroup: UserGroup) {
 | 
			
		||||
            return [
 | 
			
		||||
                `.btn-check:checked + .btn-${userGroup.id} {
 | 
			
		||||
                color: ${userGroup.foregroundColor};
 | 
			
		||||
                background-color: ${userGroup.backgroundColor};
 | 
			
		||||
                }`,
 | 
			
		||||
            ];
 | 
			
		||||
        }
 | 
			
		||||
        function addNewEntity(datas: any) {
 | 
			
		||||
            const { selected, modal } = datas;
 | 
			
		||||
            users.value = selected.map((selected: any) => selected.result);
 | 
			
		||||
            addressees.value = addressees.value.filter(
 | 
			
		||||
                (addressee) => addressee.type === "user_group"
 | 
			
		||||
            );
 | 
			
		||||
            addressees.value = [...addressees.value, ...users.value];
 | 
			
		||||
            ctx.emit("update:modelValue", addressees.value);
 | 
			
		||||
            addPersons.value.resetSearch();
 | 
			
		||||
            modal.showModal = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const addPersonsOptions = computed(() => {
 | 
			
		||||
            return {
 | 
			
		||||
                uniq: false,
 | 
			
		||||
                type: ["user"],
 | 
			
		||||
                priority: null,
 | 
			
		||||
                button: {
 | 
			
		||||
                    size: "btn-sm",
 | 
			
		||||
                    class: "btn-submit",
 | 
			
		||||
                },
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        function removeUser(user: User) {
 | 
			
		||||
            users.value.splice(users.value.indexOf(user), 1);
 | 
			
		||||
            addressees.value = addressees.value.filter(
 | 
			
		||||
                (addressee) => addressee.id !== user.id
 | 
			
		||||
            );
 | 
			
		||||
            ctx.emit("update:modelValue", addressees.value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        watch(userGroupLevel, (userGroupLevelAdd, userGroupLevelRem) => {
 | 
			
		||||
            const index = addressees.value.indexOf(
 | 
			
		||||
                userGroupLevelRem as UserGroup
 | 
			
		||||
            );
 | 
			
		||||
            if (index !== -1) {
 | 
			
		||||
                addressees.value.splice(index, 1);
 | 
			
		||||
            }
 | 
			
		||||
            addressees.value.push(userGroupLevelAdd as UserGroup);
 | 
			
		||||
            ctx.emit("update:modelValue", addressees.value);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        watch(userGroup, (userGroupAdd) => {
 | 
			
		||||
            const userGroupLevel = addressees.value.filter(
 | 
			
		||||
                (addressee) =>
 | 
			
		||||
                    addressee.type == "user_group" &&
 | 
			
		||||
                    addressee.excludeKey == "level"
 | 
			
		||||
            ) as UserGroup[];
 | 
			
		||||
            const users = addressees.value.filter(
 | 
			
		||||
                (addressee) => addressee.type == "user"
 | 
			
		||||
            ) as UserGroup[];
 | 
			
		||||
            addressees.value = [...users, ...userGroupLevel, ...userGroupAdd];
 | 
			
		||||
            ctx.emit("update:modelValue", addressees.value);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            addressees,
 | 
			
		||||
            userGroupLevel,
 | 
			
		||||
            userGroup,
 | 
			
		||||
            users,
 | 
			
		||||
            addPersons,
 | 
			
		||||
            addPersonsOptions,
 | 
			
		||||
            addNewEntity,
 | 
			
		||||
            removeUser,
 | 
			
		||||
            getUserGroupBtnColor,
 | 
			
		||||
            customUserGroupLabel(selectedUserGroup: UserGroup) {
 | 
			
		||||
                return selectedUserGroup.label
 | 
			
		||||
                    ? selectedUserGroup.label.fr
 | 
			
		||||
                    : t("add_addresseeuser_group_label");
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
.btn-check:checked + .btn,
 | 
			
		||||
:not(.btn-check) + .btn:active,
 | 
			
		||||
.btn:first-child:active,
 | 
			
		||||
.btn.active,
 | 
			
		||||
.btn.show {
 | 
			
		||||
    color: white;
 | 
			
		||||
    box-shadow: 0 0 0 0.2rem var(--bs-chill-green);
 | 
			
		||||
    outline: 0;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,138 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <Teleport to="#header-ticket-main">
 | 
			
		||||
        <div class="container-xxl text-primary">
 | 
			
		||||
            <div class="row">
 | 
			
		||||
                <div class="col-md-6 col-sm-12 ps-md-5 ps-xxl-0">
 | 
			
		||||
                    <h2>#{{ ticket.id }}</h2>
 | 
			
		||||
                    <h1 v-if="ticket.currentMotive">
 | 
			
		||||
                        {{ ticket.currentMotive.label.fr }}
 | 
			
		||||
                    </h1>
 | 
			
		||||
                    <p class="chill-no-data-statement" v-else>
 | 
			
		||||
                        {{ $t("banner.no_motive") }}
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="col-md-6 col-sm-12">
 | 
			
		||||
                    <div class="d-flex justify-content-end">
 | 
			
		||||
                        <h1>
 | 
			
		||||
                            <span class="badge text-bg-chill-green text-white">
 | 
			
		||||
                                {{ $t("banner.open") }}
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </h1>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div class="d-flex justify-content-end">
 | 
			
		||||
                        <h3 class="fst-italic" v-if="ticket.createdAt">
 | 
			
		||||
                            {{
 | 
			
		||||
                                $t("banner.since", {
 | 
			
		||||
                                    time: since,
 | 
			
		||||
                                })
 | 
			
		||||
                            }}
 | 
			
		||||
                        </h3>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </Teleport>
 | 
			
		||||
    <Teleport to="#header-ticket-details">
 | 
			
		||||
        <div class="container-xxl">
 | 
			
		||||
            <div class="row justify-content-between">
 | 
			
		||||
                <div class="col-md-6 col-sm-12 ps-md-5 ps-xxl-0">
 | 
			
		||||
                    <h3 class="text-primary">
 | 
			
		||||
                        {{ $t("banner.concerned_patient") }}
 | 
			
		||||
                    </h3>
 | 
			
		||||
 | 
			
		||||
                    <person-render-box
 | 
			
		||||
                        render="badge"
 | 
			
		||||
                        v-for="person in ticket.currentPersons"
 | 
			
		||||
                        :key="person.id"
 | 
			
		||||
                        :person="person"
 | 
			
		||||
                        :options="{
 | 
			
		||||
                            addLink: true,
 | 
			
		||||
                            addId: false,
 | 
			
		||||
                            addAltNames: false,
 | 
			
		||||
                            addEntity: true,
 | 
			
		||||
                            addInfo: true,
 | 
			
		||||
                            hLevel: 3,
 | 
			
		||||
                            isMultiline: true,
 | 
			
		||||
                            isConfidential: false,
 | 
			
		||||
                        }"
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="col-md-6 col-sm-12">
 | 
			
		||||
                    <h3 class="text-primary">{{ $t("banner.speaker") }}</h3>
 | 
			
		||||
                    <addressee-component
 | 
			
		||||
                        :addressees="ticket.currentAddressees"
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </Teleport>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, computed, defineComponent, ref } from "vue";
 | 
			
		||||
import { useI18n } from "vue-i18n";
 | 
			
		||||
 | 
			
		||||
// Components
 | 
			
		||||
import PersonRenderBox from "../../../../../../../ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue";
 | 
			
		||||
import AddresseeComponent from "./AddresseeComponent.vue";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import { Ticket } from "../../../types";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "BannerComponent",
 | 
			
		||||
    props: {
 | 
			
		||||
        ticket: {
 | 
			
		||||
            type: Object as PropType<Ticket>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
        PersonRenderBox,
 | 
			
		||||
        AddresseeComponent,
 | 
			
		||||
    },
 | 
			
		||||
    setup(props) {
 | 
			
		||||
        const { t } = useI18n();
 | 
			
		||||
        const today = ref(new Date());
 | 
			
		||||
        const createdAt = ref(props.ticket.createdAt as any);
 | 
			
		||||
 | 
			
		||||
        setInterval(function () {
 | 
			
		||||
            today.value = new Date();
 | 
			
		||||
        }, 1000);
 | 
			
		||||
 | 
			
		||||
        const since = computed(() => {
 | 
			
		||||
            const date = new Date(createdAt.value.date);
 | 
			
		||||
 | 
			
		||||
            const timeDiff = Math.abs(today.value.getTime() - date.getTime());
 | 
			
		||||
            const daysDiff = Math.floor(timeDiff / (1000 * 3600 * 24));
 | 
			
		||||
            const hoursDiff = Math.floor(
 | 
			
		||||
                (timeDiff % (1000 * 3600 * 24)) / (1000 * 3600)
 | 
			
		||||
            );
 | 
			
		||||
            const minutesDiff = Math.floor(
 | 
			
		||||
                (timeDiff % (1000 * 3600)) / (1000 * 60)
 | 
			
		||||
            );
 | 
			
		||||
            const secondsDiff = Math.floor((timeDiff % (1000 * 60)) / 1000);
 | 
			
		||||
 | 
			
		||||
            if (daysDiff < 1 && hoursDiff < 1 && minutesDiff < 1) {
 | 
			
		||||
                return `${t("banner.seconds", { count: secondsDiff })}`;
 | 
			
		||||
            } else if (daysDiff < 1) {
 | 
			
		||||
                return `${t("banner.hours", { count: hoursDiff })}
 | 
			
		||||
                ${t("banner.minutes", { count: minutesDiff })}
 | 
			
		||||
                ${t("banner.seconds", { count: secondsDiff })}`;
 | 
			
		||||
            } else {
 | 
			
		||||
                return `${t("banner.days", { count: daysDiff })}, ${t(
 | 
			
		||||
                    "banner.hours",
 | 
			
		||||
                    {
 | 
			
		||||
                        count: hoursDiff,
 | 
			
		||||
                    }
 | 
			
		||||
                )}  ${t("banner.minutes", {
 | 
			
		||||
                    count: minutesDiff,
 | 
			
		||||
                })}`;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return { since };
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
@@ -0,0 +1,72 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="row">
 | 
			
		||||
        <div class="col-12">
 | 
			
		||||
            <vue-multiselect
 | 
			
		||||
                name="selectMotive"
 | 
			
		||||
                id="selectMotive"
 | 
			
		||||
                label="label"
 | 
			
		||||
                :custom-label="customLabel"
 | 
			
		||||
                track-by="id"
 | 
			
		||||
                open-direction="top"
 | 
			
		||||
                :multiple="false"
 | 
			
		||||
                :searchable="true"
 | 
			
		||||
                :placeholder="$t('set_motive.label')"
 | 
			
		||||
                :select-label="$t('multiselect.select_label')"
 | 
			
		||||
                :deselect-label="$t('multiselect.deselect_label')"
 | 
			
		||||
                :selected-label="$t('multiselect.selected_label')"
 | 
			
		||||
                :options="motives"
 | 
			
		||||
                v-model="motive"
 | 
			
		||||
                class="mb-4"
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, defineComponent, ref, watch } from "vue";
 | 
			
		||||
import { useI18n } from "vue-i18n";
 | 
			
		||||
import VueMultiselect from "vue-multiselect";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import { Motive } from "../../../types";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "MotiveSelectorComponent",
 | 
			
		||||
    props: {
 | 
			
		||||
        modelValue: {
 | 
			
		||||
            type: Object as PropType<Motive>,
 | 
			
		||||
            required: false,
 | 
			
		||||
        },
 | 
			
		||||
        motives: {
 | 
			
		||||
            type: Object as PropType<Motive[]>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
        VueMultiselect,
 | 
			
		||||
    },
 | 
			
		||||
    emits: ["update:modelValue"],
 | 
			
		||||
 | 
			
		||||
    setup(props, ctx) {
 | 
			
		||||
        const motive = ref(props.modelValue);
 | 
			
		||||
        const { t } = useI18n();
 | 
			
		||||
 | 
			
		||||
        watch(motive, (motive) => {
 | 
			
		||||
            ctx.emit("update:modelValue", motive);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            motive,
 | 
			
		||||
            customLabel(motive: Motive) {
 | 
			
		||||
                return motive.label ? motive.label.fr : t("set_motive.label");
 | 
			
		||||
            },
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
#selectMotive {
 | 
			
		||||
    margin-bottom: 1.5em;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <addressee-component :addressees="addressees" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, defineComponent } from "vue";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import { UserGroupOrUser } from "../../../../../../../ChillMainBundle/Resources/public/types";
 | 
			
		||||
 | 
			
		||||
// Components
 | 
			
		||||
import AddresseeComponent from "./AddresseeComponent.vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "TicketHistoryAddresseeComponenvt",
 | 
			
		||||
    props: {
 | 
			
		||||
        addressees: {
 | 
			
		||||
            type: Array as PropType<UserGroupOrUser[]>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
        AddresseeComponent,
 | 
			
		||||
    },
 | 
			
		||||
    setup() {
 | 
			
		||||
        return {};
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <blockquote class="chill-user-quote">
 | 
			
		||||
            <p v-html="convertMarkdownToHtml(commentHistory.content)"></p>
 | 
			
		||||
        </blockquote>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, defineComponent } from "vue";
 | 
			
		||||
import { marked } from "marked";
 | 
			
		||||
import DOMPurify from "dompurify";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import { Comment } from "../../../types";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "TicketHistoryCommentComponent",
 | 
			
		||||
    props: {
 | 
			
		||||
        commentHistory: {
 | 
			
		||||
            type: Object as PropType<Comment>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    setup() {
 | 
			
		||||
        const preprocess = (markdown: string): string => {
 | 
			
		||||
            return markdown;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const postprocess = (html: string): string => {
 | 
			
		||||
            DOMPurify.addHook("afterSanitizeAttributes", (node: any) => {
 | 
			
		||||
                if ("target" in node) {
 | 
			
		||||
                    node.setAttribute("target", "_blank");
 | 
			
		||||
                    node.setAttribute("rel", "noopener noreferrer");
 | 
			
		||||
                }
 | 
			
		||||
                if (
 | 
			
		||||
                    !node.hasAttribute("target") &&
 | 
			
		||||
                    (node.hasAttribute("xlink:href") ||
 | 
			
		||||
                        node.hasAttribute("href"))
 | 
			
		||||
                ) {
 | 
			
		||||
                    node.setAttribute("xlink:show", "new");
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return DOMPurify.sanitize(html);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const convertMarkdownToHtml = (markdown: string): string => {
 | 
			
		||||
            marked.use({ hooks: { postprocess, preprocess } });
 | 
			
		||||
            const rawHtml = marked(markdown) as string;
 | 
			
		||||
            return rawHtml;
 | 
			
		||||
        };
 | 
			
		||||
        return {
 | 
			
		||||
            convertMarkdownToHtml,
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,98 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div
 | 
			
		||||
        class="card my-2 bg-light"
 | 
			
		||||
        v-for="history_line in history"
 | 
			
		||||
        :key="history.indexOf(history_line)"
 | 
			
		||||
    >
 | 
			
		||||
        <template v-if="!Array.isArray(history_line)">
 | 
			
		||||
            <div class="card-header">
 | 
			
		||||
                <i :class="`${actionIcons[history_line.event_type]} me-1`"></i>
 | 
			
		||||
                <span class="fw-bold fst-italic mx-1">
 | 
			
		||||
                    {{ formatDate(history_line.at) }}
 | 
			
		||||
                </span>
 | 
			
		||||
                <span class="badge bg-white text-black mx-1">
 | 
			
		||||
                    {{ history_line.by.username }}
 | 
			
		||||
                </span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body row">
 | 
			
		||||
                <ticket-history-person-component
 | 
			
		||||
                    :personHistory="history_line.data"
 | 
			
		||||
                    v-if="history_line.event_type == 'add_person'"
 | 
			
		||||
                />
 | 
			
		||||
                <ticket-history-motive-component
 | 
			
		||||
                    :motiveHistory="history_line.data"
 | 
			
		||||
                    v-else-if="history_line.event_type == 'set_motive'"
 | 
			
		||||
                />
 | 
			
		||||
                <ticket-history-comment-component
 | 
			
		||||
                    :commentHistory="history_line.data"
 | 
			
		||||
                    v-else-if="history_line.event_type == 'add_comment'"
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </template>
 | 
			
		||||
        <template v-else>
 | 
			
		||||
            <div class="card-header">
 | 
			
		||||
                <i class="fa fa-paper-plane me-1"></i>
 | 
			
		||||
                <span class="fw-bold fst-italic mx-1">
 | 
			
		||||
                    {{ formatDate(history_line[0].at) }}
 | 
			
		||||
                </span>
 | 
			
		||||
                <span class="badge bg-white text-black mx-1">
 | 
			
		||||
                    {{ history_line[0].by.username }}
 | 
			
		||||
                </span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-body row">
 | 
			
		||||
                <ticket-history-addressee-component
 | 
			
		||||
                    :addressees="
 | 
			
		||||
                        history_line
 | 
			
		||||
                            .map((line) => line.data)
 | 
			
		||||
                            .map((data) => data.addressee)
 | 
			
		||||
                    "
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </template>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, defineComponent, ref } from "vue";
 | 
			
		||||
import { useStore } from "vuex";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import { DateTime } from "../../../../../../../ChillMainBundle/Resources/public/types";
 | 
			
		||||
import { Ticket } from "../../../types";
 | 
			
		||||
 | 
			
		||||
// Components
 | 
			
		||||
import TicketHistoryPersonComponent from "./TicketHistoryPersonComponent.vue";
 | 
			
		||||
import TicketHistoryMotiveComponent from "./TicketHistoryMotiveComponent.vue";
 | 
			
		||||
import TicketHistoryCommentComponent from "./TicketHistoryCommentComponent.vue";
 | 
			
		||||
import TicketHistoryAddresseeComponent from "./TicketHistoryAddresseeComponent.vue";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "TicketHistoryListComponent",
 | 
			
		||||
    components: {
 | 
			
		||||
        TicketHistoryPersonComponent,
 | 
			
		||||
        TicketHistoryMotiveComponent,
 | 
			
		||||
        TicketHistoryCommentComponent,
 | 
			
		||||
        TicketHistoryAddresseeComponent,
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
        history: {
 | 
			
		||||
            type: Array as PropType<Ticket["history"]>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    setup() {
 | 
			
		||||
        const store = useStore();
 | 
			
		||||
 | 
			
		||||
        function formatDate(d: DateTime) {
 | 
			
		||||
            const date = new Date(d.datetime);
 | 
			
		||||
            const month = date.toLocaleString("default", { month: "long" });
 | 
			
		||||
            return `${date.getDate()} ${month} ${date.getFullYear()}, ${date.toLocaleTimeString()}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return { actionIcons: ref(store.getters.getActionIcons), formatDate };
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="col-12 fw-bolder">
 | 
			
		||||
        {{ motiveHistory.motive.label.fr }}
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, defineComponent } from "vue";
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import { MotiveHistory } from "../../../types";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "TicketHistoryMotiveComponent",
 | 
			
		||||
    props: {
 | 
			
		||||
        motiveHistory: {
 | 
			
		||||
            type: Object as PropType<MotiveHistory>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    setup() {},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="col-12" v-if="personHistory.createdBy">
 | 
			
		||||
        <span class="mx-1">
 | 
			
		||||
            {{ $t("history.user") }}
 | 
			
		||||
            <span class="badge bg-primary m-1">
 | 
			
		||||
                {{ personHistory.createdBy.username }}
 | 
			
		||||
            </span>
 | 
			
		||||
        </span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="col-12">
 | 
			
		||||
        <span class="mx-1">
 | 
			
		||||
            {{ $t("history.person") }}
 | 
			
		||||
        </span>
 | 
			
		||||
        <person-render-box
 | 
			
		||||
            render="badge"
 | 
			
		||||
            :key="personHistory.person.id"
 | 
			
		||||
            :person="personHistory.person"
 | 
			
		||||
            :options="{
 | 
			
		||||
                addLink: true,
 | 
			
		||||
                addId: false,
 | 
			
		||||
                addAltNames: false,
 | 
			
		||||
                addEntity: true,
 | 
			
		||||
                addInfo: true,
 | 
			
		||||
                hLevel: 3,
 | 
			
		||||
                isMultiline: true,
 | 
			
		||||
                isConfidential: false,
 | 
			
		||||
            }"
 | 
			
		||||
        />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { PropType, defineComponent } from "vue";
 | 
			
		||||
 | 
			
		||||
// Components
 | 
			
		||||
import PersonRenderBox from "../../../../../../../ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue";
 | 
			
		||||
 | 
			
		||||
// Type
 | 
			
		||||
import { PersonHistory } from "../../../types";
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: "TicketHistoryPersonComponent",
 | 
			
		||||
    props: {
 | 
			
		||||
        personHistory: {
 | 
			
		||||
            type: Object as PropType<PersonHistory>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    components: {
 | 
			
		||||
        PersonRenderBox,
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    setup() {},
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,41 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="d-flex justify-content-end">
 | 
			
		||||
        <div class="btn-group" @click="handleClick">
 | 
			
		||||
        <button type="button" class="btn btn-light dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
 | 
			
		||||
            {{ $t('ticket.previous_tickets') }}
 | 
			
		||||
            <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-chill-green">
 | 
			
		||||
                {{ tickets.length }}
 | 
			
		||||
                <span class="visually-hidden">Tickets</span>
 | 
			
		||||
            </span>
 | 
			
		||||
 | 
			
		||||
        </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
 | 
			
		||||
import { PropType, defineComponent } from 'vue';
 | 
			
		||||
 | 
			
		||||
// Types
 | 
			
		||||
import { Ticket } from '../../../types';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
    name: 'TicketSelectorComponent',
 | 
			
		||||
    props: {
 | 
			
		||||
        tickets: {
 | 
			
		||||
            type: Object as PropType<Ticket[]>,
 | 
			
		||||
            required: true,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    setup() {
 | 
			
		||||
        function handleClick() {
 | 
			
		||||
            alert('Sera disponible plus tard')
 | 
			
		||||
        }
 | 
			
		||||
        return { handleClick }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped></style>
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
import { multiSelectMessages } from "../../../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
 | 
			
		||||
import { personMessages } from "../../../../../../../ChillPersonBundle/Resources/public/vuejs/_js/i18n";
 | 
			
		||||
 | 
			
		||||
const messages = {
 | 
			
		||||
    fr: {
 | 
			
		||||
        ticket: {
 | 
			
		||||
            previous_tickets: "Précédents tickets",
 | 
			
		||||
            cancel: "Annuler",
 | 
			
		||||
            save: "Enregistrer",
 | 
			
		||||
            close: "Fermer",
 | 
			
		||||
        },
 | 
			
		||||
        history: {
 | 
			
		||||
            person: "Ouverture par appel téléphonique de ",
 | 
			
		||||
            user: "Prise en charge par ",
 | 
			
		||||
        },
 | 
			
		||||
        add_comment: {
 | 
			
		||||
            title: "Commentaire",
 | 
			
		||||
            label: "Ajouter un commentaire",
 | 
			
		||||
            success: "Commentaire enregistré",
 | 
			
		||||
            content: "Ajouter un commentaire",
 | 
			
		||||
            error: "Aucun commentaire ajouté",
 | 
			
		||||
        },
 | 
			
		||||
        set_motive: {
 | 
			
		||||
            title: "Motif",
 | 
			
		||||
            label: "Choisir un motif",
 | 
			
		||||
            success: "Motif enregistré",
 | 
			
		||||
            error: "Aucun motif sélectionné",
 | 
			
		||||
        },
 | 
			
		||||
        add_addressee: {
 | 
			
		||||
            title: "Transfert",
 | 
			
		||||
            user_group_label: "Transferer vers un groupe",
 | 
			
		||||
            user_label: "Transferer vers un ou plusieurs utilisateurs",
 | 
			
		||||
            success: "Transfert effectué",
 | 
			
		||||
            error: "Aucun destinataire sélectionné",
 | 
			
		||||
        },
 | 
			
		||||
        banner: {
 | 
			
		||||
            concerned_patient: "Patient concerné",
 | 
			
		||||
            speaker: "Destinataire(s)",
 | 
			
		||||
            open: "Ouvert",
 | 
			
		||||
            since: "Depuis {time}",
 | 
			
		||||
            and: "et",
 | 
			
		||||
            days: "|1 jour|{count} jours",
 | 
			
		||||
            hours: "|1 heure et|{count} heures",
 | 
			
		||||
            minutes: "|1 minute|{count} minutes",
 | 
			
		||||
            seconds: "|1 seconde|{count} secondes",
 | 
			
		||||
            no_motive: "Pas de motif",
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
Object.assign(messages.fr, multiSelectMessages.fr);
 | 
			
		||||
Object.assign(messages.fr, personMessages.fr);
 | 
			
		||||
export default messages;
 | 
			
		||||
@@ -0,0 +1,32 @@
 | 
			
		||||
import App from './App.vue';
 | 
			
		||||
import {createApp} from "vue";
 | 
			
		||||
 | 
			
		||||
import { _createI18n } from "../../../../../../ChillMainBundle/Resources/public/vuejs/_js/i18n";
 | 
			
		||||
 | 
			
		||||
import VueToast from 'vue-toast-notification';
 | 
			
		||||
import 'vue-toast-notification/dist/theme-sugar.css';
 | 
			
		||||
 | 
			
		||||
import { store } from "./store";
 | 
			
		||||
import messages from './i18n/messages';
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
    interface Window {
 | 
			
		||||
        initialTicket: string
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const i18n = _createI18n(messages, false);
 | 
			
		||||
 | 
			
		||||
const _app = createApp({
 | 
			
		||||
        template: '<app></app>',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    _app
 | 
			
		||||
    .use(store)
 | 
			
		||||
    .use(i18n)
 | 
			
		||||
    // Cant use this.$toast in components in composition API so we need to provide it
 | 
			
		||||
    // Fix: with vue-toast-notification@^3
 | 
			
		||||
    .use(VueToast).provide('toast', _app.config.globalProperties.$toast)
 | 
			
		||||
    .component('app', App)
 | 
			
		||||
    .mount('#ticketRoot');
 | 
			
		||||
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
import { createStore } from "vuex";
 | 
			
		||||
import { State as MotiveStates, moduleMotive } from "./modules/motive";
 | 
			
		||||
import { State as TicketStates, moduleTicket } from "./modules/ticket";
 | 
			
		||||
import { State as CommentStates, moduleComment } from "./modules/comment";
 | 
			
		||||
import { State as AddresseeStates, moduleAddressee } from "./modules/addressee";
 | 
			
		||||
 | 
			
		||||
export type RootState = {
 | 
			
		||||
    motive: MotiveStates;
 | 
			
		||||
    ticket: TicketStates;
 | 
			
		||||
    comment: CommentStates;
 | 
			
		||||
    addressee: AddresseeStates;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const store = createStore<RootState>({
 | 
			
		||||
    modules: {
 | 
			
		||||
        motive: moduleMotive,
 | 
			
		||||
        ticket: moduleTicket,
 | 
			
		||||
        comment: moduleComment,
 | 
			
		||||
        addressee: moduleAddressee,
 | 
			
		||||
    },
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,84 @@
 | 
			
		||||
import {
 | 
			
		||||
    fetchResults,
 | 
			
		||||
    makeFetch,
 | 
			
		||||
} from "../../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
 | 
			
		||||
 | 
			
		||||
import { Module } from "vuex";
 | 
			
		||||
import { RootState } from "..";
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    User,
 | 
			
		||||
    UserGroup,
 | 
			
		||||
    UserGroupOrUser,
 | 
			
		||||
} from "../../../../../../../../ChillMainBundle/Resources/public/types";
 | 
			
		||||
 | 
			
		||||
export interface State {
 | 
			
		||||
    userGroups: Array<UserGroup>;
 | 
			
		||||
    users: Array<User>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const moduleAddressee: Module<State, RootState> = {
 | 
			
		||||
    state: () => ({
 | 
			
		||||
        userGroups: [] as Array<UserGroup>,
 | 
			
		||||
        users: [] as Array<User>,
 | 
			
		||||
    }),
 | 
			
		||||
    getters: {
 | 
			
		||||
        getUserGroups(state) {
 | 
			
		||||
            return state.userGroups;
 | 
			
		||||
        },
 | 
			
		||||
        getUsers(state) {
 | 
			
		||||
            return state.users;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    mutations: {
 | 
			
		||||
        setUserGroups(state, userGroups) {
 | 
			
		||||
            state.userGroups = userGroups;
 | 
			
		||||
        },
 | 
			
		||||
        setUsers(state, users) {
 | 
			
		||||
            state.users = users;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    actions: {
 | 
			
		||||
        fetchUserGroups({ commit }) {
 | 
			
		||||
            try {
 | 
			
		||||
                fetchResults("/api/1.0/main/user-group.json").then(
 | 
			
		||||
                    (results) => {
 | 
			
		||||
                        commit("setUserGroups", results);
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
            } catch (e: any) {
 | 
			
		||||
                throw e.name;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        fetchUsers({ commit }) {
 | 
			
		||||
            try {
 | 
			
		||||
                fetchResults("/api/1.0/main/user.json").then((results) => {
 | 
			
		||||
                    commit("setUsers", results);
 | 
			
		||||
                });
 | 
			
		||||
            } catch (e: any) {
 | 
			
		||||
                throw e.name;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        async setAdressees(
 | 
			
		||||
            { commit },
 | 
			
		||||
            datas: { ticketId: number; addressees: Array<UserGroupOrUser> }
 | 
			
		||||
        ) {
 | 
			
		||||
            const { ticketId, addressees } = datas;
 | 
			
		||||
            try {
 | 
			
		||||
                const result = await makeFetch(
 | 
			
		||||
                    "POST",
 | 
			
		||||
                    `/api/1.0/ticket/${ticketId}/addressees/set`,
 | 
			
		||||
                    {
 | 
			
		||||
                        addressees: addressees.map((addressee) => {
 | 
			
		||||
                            return { id: addressee.id, type: addressee.type };
 | 
			
		||||
                        }),
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
                commit("setTicket", result);
 | 
			
		||||
            } catch (e: any) {
 | 
			
		||||
                throw e.name;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
import {
 | 
			
		||||
    fetchResults,
 | 
			
		||||
    makeFetch,
 | 
			
		||||
} from "../../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
 | 
			
		||||
 | 
			
		||||
import { Module } from "vuex";
 | 
			
		||||
import { RootState } from "..";
 | 
			
		||||
 | 
			
		||||
import { Comment } from "../../../../types";
 | 
			
		||||
 | 
			
		||||
export interface State {
 | 
			
		||||
    comments: Array<Comment>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const moduleComment: Module<State, RootState> = {
 | 
			
		||||
    state: () => ({
 | 
			
		||||
        comments: [] as Array<Comment>,
 | 
			
		||||
    }),
 | 
			
		||||
    getters: {},
 | 
			
		||||
    mutations: {},
 | 
			
		||||
    actions: {
 | 
			
		||||
        async createComment(
 | 
			
		||||
            { commit },
 | 
			
		||||
            datas: { ticketId: number; content: Comment["content"] }
 | 
			
		||||
        ) {
 | 
			
		||||
            const { ticketId, content } = datas;
 | 
			
		||||
            try {
 | 
			
		||||
                const result = await makeFetch(
 | 
			
		||||
                    "POST",
 | 
			
		||||
                    `/api/1.0/ticket/${ticketId}/comment/add`,
 | 
			
		||||
                    { content }
 | 
			
		||||
                );
 | 
			
		||||
                commit("setTicket", result);
 | 
			
		||||
            }
 | 
			
		||||
            catch(e: any) {
 | 
			
		||||
                throw e.name;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
import {
 | 
			
		||||
    fetchResults,
 | 
			
		||||
    makeFetch,
 | 
			
		||||
} from "../../../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
 | 
			
		||||
 | 
			
		||||
import { Module } from "vuex";
 | 
			
		||||
import { RootState } from "..";
 | 
			
		||||
 | 
			
		||||
import { Motive } from "../../../../types";
 | 
			
		||||
 | 
			
		||||
export interface State {
 | 
			
		||||
    motives: Array<Motive>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const moduleMotive: Module<State, RootState> = {
 | 
			
		||||
    state: () => ({
 | 
			
		||||
        motives: [] as Array<Motive>,
 | 
			
		||||
    }),
 | 
			
		||||
    getters: {
 | 
			
		||||
        getMotives(state) {
 | 
			
		||||
            return state.motives;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    mutations: {
 | 
			
		||||
        setMotives(state, motives) {
 | 
			
		||||
            state.motives = motives;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    actions: {
 | 
			
		||||
        async fetchMotives({ commit }) {
 | 
			
		||||
            try {
 | 
			
		||||
                const results = (await fetchResults(
 | 
			
		||||
                    "/api/1.0/ticket/motive.json"
 | 
			
		||||
                )) as Motive[];
 | 
			
		||||
                commit("setMotives", results);
 | 
			
		||||
            } catch (e: any) {
 | 
			
		||||
                throw e.name;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        async createMotive(
 | 
			
		||||
            { commit },
 | 
			
		||||
            datas: { ticketId: number; motive: Motive }
 | 
			
		||||
        ) {
 | 
			
		||||
            const { ticketId, motive } = datas;
 | 
			
		||||
            try {
 | 
			
		||||
                const result = await makeFetch(
 | 
			
		||||
                    "POST",
 | 
			
		||||
                    `/api/1.0/ticket/${ticketId}/motive/set`,
 | 
			
		||||
                    {
 | 
			
		||||
                        motive: {
 | 
			
		||||
                            id: motive.id,
 | 
			
		||||
                            type: motive.type,
 | 
			
		||||
                        },
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
                commit("setTicket", result);
 | 
			
		||||
            } catch (e: any) {
 | 
			
		||||
                throw e.name;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,66 @@
 | 
			
		||||
import { Module } from "vuex";
 | 
			
		||||
import { RootState } from "..";
 | 
			
		||||
 | 
			
		||||
import { Ticket } from "../../../../types";
 | 
			
		||||
 | 
			
		||||
export interface State {
 | 
			
		||||
    ticket: Ticket;
 | 
			
		||||
    action_icons: Object;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const moduleTicket: Module<State, RootState> = {
 | 
			
		||||
    state: () => ({
 | 
			
		||||
        ticket: {} as Ticket,
 | 
			
		||||
        action_icons: {
 | 
			
		||||
            add_person: "fa fa-eyedropper",
 | 
			
		||||
            add_comment: "fa fa-comment",
 | 
			
		||||
            set_motive: "fa fa-paint-brush",
 | 
			
		||||
            add_addressee: "fa fa-paper-plane",
 | 
			
		||||
        },
 | 
			
		||||
        toto: "toto",
 | 
			
		||||
    }),
 | 
			
		||||
    getters: {
 | 
			
		||||
        getTicket(state) {
 | 
			
		||||
            state.ticket.history = state.ticket.history.sort((a, b) =>
 | 
			
		||||
                b.at.datetime.localeCompare(a.at.datetime)
 | 
			
		||||
            );
 | 
			
		||||
            return state.ticket;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        getActionIcons(state) {
 | 
			
		||||
            return state.action_icons;
 | 
			
		||||
        },
 | 
			
		||||
        getDistinctAddressesHistory(state) {
 | 
			
		||||
            const addresseeHistory = state.ticket.history.reduce(
 | 
			
		||||
                (result, item) => {
 | 
			
		||||
                    const { datetime } = item.at;
 | 
			
		||||
                    if (
 | 
			
		||||
                        !["add_addressee", "remove_addressee"].includes(
 | 
			
		||||
                            item.event_type
 | 
			
		||||
                        )
 | 
			
		||||
                    ) {
 | 
			
		||||
                        result[datetime] = item;
 | 
			
		||||
                        return result;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (!result[datetime]) {
 | 
			
		||||
                        result[datetime] = [];
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (item.event_type === "add_addressee") {
 | 
			
		||||
                        result[datetime].push(item);
 | 
			
		||||
                    }
 | 
			
		||||
                    return result;
 | 
			
		||||
                },
 | 
			
		||||
                {} as any
 | 
			
		||||
            );
 | 
			
		||||
            return Object.values(addresseeHistory) as Array<Ticket["history"]>;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    mutations: {
 | 
			
		||||
        setTicket(state, ticket) {
 | 
			
		||||
            state.ticket = ticket;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    actions: {},
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
<div class="banner banner-ticket ">
 | 
			
		||||
	<div id="header-ticket-main" class="header-name">
 | 
			
		||||
 | 
			
		||||
	</div>
 | 
			
		||||
	<div id="header-ticket-details" class="header-details">
 | 
			
		||||
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
{% extends '@ChillTicket/layout.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
    {{ parent() }}
 | 
			
		||||
    {{ encore_entry_link_tags('vue_ticket_app') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block js %}
 | 
			
		||||
    {{ parent() }}
 | 
			
		||||
    <script type="text/javascript">
 | 
			
		||||
        window.initialTicket = "{{ ticket|serialize('json', {'groups': 'read'})|escape('js') }}";
 | 
			
		||||
    </script>
 | 
			
		||||
    {{ encore_entry_script_tags('vue_ticket_app') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
    <div id="ticketRoot"></div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -0,0 +1,17 @@
 | 
			
		||||
{% extends '@ChillMain/layout.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
    {{ encore_entry_link_tags('page_ticket') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block js %}
 | 
			
		||||
    {{ encore_entry_script_tags('page_ticket') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block top_banner %}
 | 
			
		||||
    {{ include('@ChillTicket/Banner/banner.html.twig') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block wrapping_content %}
 | 
			
		||||
    {% block content %}{% endblock %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -0,0 +1,56 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Serializer\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\SetAddresseesCommand;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
 | 
			
		||||
 | 
			
		||||
final class SetAddresseesCommandDenormalizer implements DenormalizerInterface, DenormalizerAwareInterface
 | 
			
		||||
{
 | 
			
		||||
    use DenormalizerAwareTrait;
 | 
			
		||||
 | 
			
		||||
    public function denormalize($data, string $type, ?string $format = null, array $context = [])
 | 
			
		||||
    {
 | 
			
		||||
        if (null === $data) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!array_key_exists('addressees', $data)) {
 | 
			
		||||
            throw new UnexpectedValueException("key 'addressees' does exists");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!is_array($data['addressees'])) {
 | 
			
		||||
            throw new UnexpectedValueException("key 'addressees' must be an array");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $addresses = [];
 | 
			
		||||
        foreach ($data['addressees'] as $address) {
 | 
			
		||||
            $addresses[] = match ($address['type'] ?? '') {
 | 
			
		||||
                'user_group' => $this->denormalizer->denormalize($address, UserGroup::class, $format, $context),
 | 
			
		||||
                'user' => $this->denormalizer->denormalize($address, User::class, $format, $context),
 | 
			
		||||
                default => throw new UnexpectedValueException('the type is not set or not supported')
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return new SetAddresseesCommand($addresses);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsDenormalization($data, string $type, ?string $format = null)
 | 
			
		||||
    {
 | 
			
		||||
        return SetAddresseesCommand::class === $type && 'json' === $format;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,122 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Serializer\Normalizer;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Entity\AddresseeHistory;
 | 
			
		||||
use Chill\TicketBundle\Entity\Comment;
 | 
			
		||||
use Chill\TicketBundle\Entity\MotiveHistory;
 | 
			
		||||
use Chill\TicketBundle\Entity\PersonHistory;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
 | 
			
		||||
 | 
			
		||||
final class TicketNormalizer implements NormalizerInterface, NormalizerAwareInterface
 | 
			
		||||
{
 | 
			
		||||
    use NormalizerAwareTrait;
 | 
			
		||||
 | 
			
		||||
    public function normalize($object, ?string $format = null, array $context = [])
 | 
			
		||||
    {
 | 
			
		||||
        if (!$object instanceof Ticket) {
 | 
			
		||||
            throw new UnexpectedValueException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            'type' => 'ticket_ticket',
 | 
			
		||||
            'id' => $object->getId(),
 | 
			
		||||
            'externalRef' => $object->getExternalRef(),
 | 
			
		||||
            'currentPersons' => $this->normalizer->normalize($object->getPersons(), $format, [
 | 
			
		||||
                'groups' => 'read',
 | 
			
		||||
            ]),
 | 
			
		||||
            'currentAddressees' => $this->normalizer->normalize($object->getCurrentAddressee(), $format, ['groups' => 'read']),
 | 
			
		||||
            'currentInputs' => $this->normalizer->normalize($object->getCurrentInputs(), $format, ['groups' => 'read']),
 | 
			
		||||
            'currentMotive' => $this->normalizer->normalize($object->getMotive(), $format, ['groups' => 'read']),
 | 
			
		||||
            'history' => array_values($this->serializeHistory($object, $format, ['groups' => 'read'])),
 | 
			
		||||
            'createdAt' => $object->getCreatedAt(),
 | 
			
		||||
            'updatedBy' => $object->getUpdatedBy(),
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsNormalization($data, ?string $format = null)
 | 
			
		||||
    {
 | 
			
		||||
        return 'json' === $format && $data instanceof Ticket;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function serializeHistory(Ticket $ticket, string $format, array $context): array
 | 
			
		||||
    {
 | 
			
		||||
        $events = [
 | 
			
		||||
            ...array_map(
 | 
			
		||||
                fn (MotiveHistory $motiveHistory) => [
 | 
			
		||||
                    'event_type' => 'set_motive',
 | 
			
		||||
                    'at' => $motiveHistory->getStartDate(),
 | 
			
		||||
                    'by' => $motiveHistory->getCreatedBy(),
 | 
			
		||||
                    'data' => $motiveHistory,
 | 
			
		||||
                ],
 | 
			
		||||
                $ticket->getMotiveHistories()->toArray()
 | 
			
		||||
            ),
 | 
			
		||||
            ...array_map(
 | 
			
		||||
                fn (PersonHistory $personHistory) =>  [
 | 
			
		||||
                    'event_type' => 'add_person',
 | 
			
		||||
                    'at' => $personHistory->getStartDate(),
 | 
			
		||||
                    'by' => $personHistory->getCreatedBy(),
 | 
			
		||||
                    'data' => $personHistory,
 | 
			
		||||
                ],
 | 
			
		||||
                $ticket->getPersonHistories()->toArray(),
 | 
			
		||||
            ),
 | 
			
		||||
            ...array_map(
 | 
			
		||||
                fn (Comment $comment) => [
 | 
			
		||||
                    'event_type' => 'add_comment',
 | 
			
		||||
                    'at' => $comment->getCreatedAt(),
 | 
			
		||||
                    'by' => $comment->getCreatedBy(),
 | 
			
		||||
                    'data' => $comment,
 | 
			
		||||
                ],
 | 
			
		||||
                $ticket->getComments()->toArray(),
 | 
			
		||||
            ),
 | 
			
		||||
            ...array_map(
 | 
			
		||||
                fn (AddresseeHistory $history) => [
 | 
			
		||||
                    'event_type' => 'add_addressee',
 | 
			
		||||
                    'at' => $history->getStartDate(),
 | 
			
		||||
                    'by' => $history->getCreatedBy(),
 | 
			
		||||
                    'data' => $history,
 | 
			
		||||
                ],
 | 
			
		||||
                $ticket->getAddresseeHistories()->toArray(),
 | 
			
		||||
            ),
 | 
			
		||||
            ...array_map(
 | 
			
		||||
                fn (AddresseeHistory $history) => [
 | 
			
		||||
                    'event_type' => 'remove_addressee',
 | 
			
		||||
                    'at' => $history->getStartDate(),
 | 
			
		||||
                    'by' => $history->getRemovedBy(),
 | 
			
		||||
                    'data' => $history,
 | 
			
		||||
                ],
 | 
			
		||||
                $ticket->getAddresseeHistories()->filter(fn (AddresseeHistory $history) => null !== $history->getEndDate())->toArray()
 | 
			
		||||
            ),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        usort(
 | 
			
		||||
            $events,
 | 
			
		||||
            static function (array $a, array $b): int {
 | 
			
		||||
                return $a['at'] <=> $b['at'];
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return array_map(
 | 
			
		||||
            fn ($data) => [
 | 
			
		||||
                'event_type' => $data['event_type'],
 | 
			
		||||
                'at' => $this->normalizer->normalize($data['at'], $format, $context),
 | 
			
		||||
                'by' => $this->normalizer->normalize($data['by'], $format, $context),
 | 
			
		||||
                'data' => $this->normalizer->normalize($data['data'], $format, $context),
 | 
			
		||||
            ],
 | 
			
		||||
            $events
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/Bundle/ChillTicketBundle/src/config/services.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/Bundle/ChillTicketBundle/src/config/services.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
services:
 | 
			
		||||
    _defaults:
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
        autowire: true
 | 
			
		||||
 | 
			
		||||
    Chill\TicketBundle\Action\Ticket\Handler\:
 | 
			
		||||
        resource: '../Action/Ticket/Handler/'
 | 
			
		||||
 | 
			
		||||
    Chill\TicketBundle\Controller\:
 | 
			
		||||
        resource: '../Controller/'
 | 
			
		||||
        tags:
 | 
			
		||||
            - controller.service_arguments
 | 
			
		||||
 | 
			
		||||
    Chill\TicketBundle\Repository\:
 | 
			
		||||
        resource: '../Repository/'
 | 
			
		||||
 | 
			
		||||
    Chill\TicketBundle\Serializer\:
 | 
			
		||||
        resource: '../Serializer/'
 | 
			
		||||
 | 
			
		||||
    Chill\TicketBundle\DataFixtures\:
 | 
			
		||||
        resource: '../DataFixtures/'
 | 
			
		||||
@@ -0,0 +1,139 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\Ticket;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20240416145919 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Create schema and tables for chill ticket';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('CREATE SCHEMA chill_ticket');
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_ticket.addressee_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_ticket.comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_ticket.input_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_ticket.motive_id_seq INCREMENT BY 1 MINVALUE 1 START 1000');
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_ticket.motives_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_ticket.person_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE SEQUENCE chill_ticket.ticket_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_ticket.addressee_history (id INT NOT NULL, ticket_id INT NOT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, addresseeUser_id INT DEFAULT NULL, addresseeGroup_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_434EBDBD4D06F00C ON chill_ticket.addressee_history (addresseeUser_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_434EBDBD776D9A84 ON chill_ticket.addressee_history (addresseeGroup_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_434EBDBD700047D2 ON chill_ticket.addressee_history (ticket_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_434EBDBD3174800F ON chill_ticket.addressee_history (createdBy_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_434EBDBD65FF1AEC ON chill_ticket.addressee_history (updatedBy_id)');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.addressee_history.startDate IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.addressee_history.createdAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.addressee_history.updatedAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_ticket.comment (id INT NOT NULL, ticket_id INT NOT NULL, content TEXT DEFAULT \'\' NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_79EBD416700047D2 ON chill_ticket.comment (ticket_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_79EBD4163174800F ON chill_ticket.comment (createdBy_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_79EBD41665FF1AEC ON chill_ticket.comment (updatedBy_id)');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.comment.createdAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.comment.updatedAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_ticket.input_history (id INT NOT NULL, person_id INT DEFAULT NULL, ticket_id INT NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, thirdParty_id INT DEFAULT NULL, removedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_E2AA301F217BBB47 ON chill_ticket.input_history (person_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_E2AA301F3EA5CAB0 ON chill_ticket.input_history (thirdParty_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_E2AA301FB8346CCF ON chill_ticket.input_history (removedBy_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_E2AA301F700047D2 ON chill_ticket.input_history (ticket_id)');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.input_history.endDate IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.input_history.startDate IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_ticket.motive (id INT NOT NULL, label JSON DEFAULT \'[]\' NOT NULL, active BOOLEAN DEFAULT true NOT NULL, PRIMARY KEY(id))');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_ticket.motives_history (id INT NOT NULL, motive_id INT NOT NULL, ticket_id INT NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_48995CFF9658649C ON chill_ticket.motives_history (motive_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_48995CFF700047D2 ON chill_ticket.motives_history (ticket_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_48995CFF3174800F ON chill_ticket.motives_history (createdBy_id)');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.motives_history.endDate IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.motives_history.startDate IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.motives_history.createdAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_ticket.person_history (id INT NOT NULL, person_id INT DEFAULT NULL, ticket_id INT NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, removedBy_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_F2969246B8346CCF ON chill_ticket.person_history (removedBy_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_F2969246217BBB47 ON chill_ticket.person_history (person_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_F2969246700047D2 ON chill_ticket.person_history (ticket_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_F29692463174800F ON chill_ticket.person_history (createdBy_id)');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.person_history.endDate IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.person_history.startDate IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.person_history.createdAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('CREATE TABLE chill_ticket.ticket (id INT NOT NULL, externalRef TEXT DEFAULT \'\' NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_B0A5F7233174800F ON chill_ticket.ticket (createdBy_id)');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_B0A5F72365FF1AEC ON chill_ticket.ticket (updatedBy_id)');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.ticket.createdAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.ticket.updatedAt IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD CONSTRAINT FK_434EBDBD4D06F00C FOREIGN KEY (addresseeUser_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD CONSTRAINT FK_434EBDBD776D9A84 FOREIGN KEY (addresseeGroup_id) REFERENCES chill_main_user_group (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD CONSTRAINT FK_434EBDBD700047D2 FOREIGN KEY (ticket_id) REFERENCES chill_ticket.ticket (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD CONSTRAINT FK_434EBDBD3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD CONSTRAINT FK_434EBDBD65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.comment ADD CONSTRAINT FK_79EBD416700047D2 FOREIGN KEY (ticket_id) REFERENCES chill_ticket.ticket (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.comment ADD CONSTRAINT FK_79EBD4163174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.comment ADD CONSTRAINT FK_79EBD41665FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.input_history ADD CONSTRAINT FK_E2AA301F217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.input_history ADD CONSTRAINT FK_E2AA301F3EA5CAB0 FOREIGN KEY (thirdParty_id) REFERENCES chill_3party.third_party (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.input_history ADD CONSTRAINT FK_E2AA301FB8346CCF FOREIGN KEY (removedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.input_history ADD CONSTRAINT FK_E2AA301F700047D2 FOREIGN KEY (ticket_id) REFERENCES chill_ticket.ticket (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.motives_history ADD CONSTRAINT FK_48995CFF9658649C FOREIGN KEY (motive_id) REFERENCES chill_ticket.motive (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.motives_history ADD CONSTRAINT FK_48995CFF700047D2 FOREIGN KEY (ticket_id) REFERENCES chill_ticket.ticket (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.motives_history ADD CONSTRAINT FK_48995CFF3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.person_history ADD CONSTRAINT FK_F2969246B8346CCF FOREIGN KEY (removedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.person_history ADD CONSTRAINT FK_F2969246217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.person_history ADD CONSTRAINT FK_F2969246700047D2 FOREIGN KEY (ticket_id) REFERENCES chill_ticket.ticket (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.person_history ADD CONSTRAINT FK_F29692463174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.ticket ADD CONSTRAINT FK_B0A5F7233174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.ticket ADD CONSTRAINT FK_B0A5F72365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('DROP SEQUENCE chill_ticket.addressee_history_id_seq CASCADE');
 | 
			
		||||
        $this->addSql('DROP SEQUENCE chill_ticket.comment_id_seq CASCADE');
 | 
			
		||||
        $this->addSql('DROP SEQUENCE chill_ticket.input_history_id_seq CASCADE');
 | 
			
		||||
        $this->addSql('DROP SEQUENCE chill_ticket.motive_id_seq CASCADE');
 | 
			
		||||
        $this->addSql('DROP SEQUENCE chill_ticket.motives_history_id_seq CASCADE');
 | 
			
		||||
        $this->addSql('DROP SEQUENCE chill_ticket.person_history_id_seq CASCADE');
 | 
			
		||||
        $this->addSql('DROP SEQUENCE chill_ticket.ticket_id_seq CASCADE');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP CONSTRAINT FK_434EBDBD4D06F00C');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP CONSTRAINT FK_434EBDBD776D9A84');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP CONSTRAINT FK_434EBDBD700047D2');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP CONSTRAINT FK_434EBDBD3174800F');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP CONSTRAINT FK_434EBDBD65FF1AEC');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.comment DROP CONSTRAINT FK_79EBD416700047D2');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.comment DROP CONSTRAINT FK_79EBD4163174800F');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.comment DROP CONSTRAINT FK_79EBD41665FF1AEC');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.input_history DROP CONSTRAINT FK_E2AA301F217BBB47');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.input_history DROP CONSTRAINT FK_E2AA301F3EA5CAB0');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.input_history DROP CONSTRAINT FK_E2AA301FB8346CCF');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.input_history DROP CONSTRAINT FK_E2AA301F700047D2');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.motives_history DROP CONSTRAINT FK_48995CFF9658649C');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.motives_history DROP CONSTRAINT FK_48995CFF700047D2');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.motives_history DROP CONSTRAINT FK_48995CFF3174800F');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.person_history DROP CONSTRAINT FK_F2969246B8346CCF');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.person_history DROP CONSTRAINT FK_F2969246217BBB47');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.person_history DROP CONSTRAINT FK_F2969246700047D2');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.person_history DROP CONSTRAINT FK_F29692463174800F');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.ticket DROP CONSTRAINT FK_B0A5F7233174800F');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.ticket DROP CONSTRAINT FK_B0A5F72365FF1AEC');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_ticket.addressee_history');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_ticket.comment');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_ticket.input_history');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_ticket.motive');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_ticket.motives_history');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_ticket.person_history');
 | 
			
		||||
        $this->addSql('DROP TABLE chill_ticket.ticket');
 | 
			
		||||
        $this->addSql('DROP SCHEMA chill_ticket CASCADE');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\Ticket;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20240423212824 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add endDate and removedBy columns on addressee history (ticket)';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT null');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD removedBy_id INT DEFAULT NULL');
 | 
			
		||||
        $this->addSql('COMMENT ON COLUMN chill_ticket.addressee_history.endDate IS \'(DC2Type:datetime_immutable)\'');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history ADD CONSTRAINT FK_434EBDBDB8346CCF FOREIGN KEY (removedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
 | 
			
		||||
        $this->addSql('CREATE INDEX IDX_434EBDBDB8346CCF ON chill_ticket.addressee_history (removedBy_id)');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP CONSTRAINT FK_434EBDBDB8346CCF');
 | 
			
		||||
        $this->addSql('DROP INDEX chill_ticket.IDX_434EBDBDB8346CCF');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP endDate');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_ticket.addressee_history DROP removedBy_id');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Tests\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\AddCommentCommand;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\AddCommentCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Entity\Comment;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class AddCommentCommandHandlerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    public function testAddComment(): void
 | 
			
		||||
    {
 | 
			
		||||
        $handler = $this->buildCommand();
 | 
			
		||||
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $command = new AddCommentCommand(content: 'test');
 | 
			
		||||
 | 
			
		||||
        $handler->handle($ticket, $command);
 | 
			
		||||
 | 
			
		||||
        self::assertCount(1, $ticket->getComments());
 | 
			
		||||
        self::assertEquals('test', $ticket->getComments()[0]->getContent());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildCommand(): AddCommentCommandHandler
 | 
			
		||||
    {
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $entityManager->persist(Argument::type(Comment::class))->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
        return new AddCommentCommandHandler($entityManager->reveal());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,74 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Tests\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Phonenumber\PhonenumberHelper;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\AssociateByPhonenumberCommand;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\AssociateByPhonenumberCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Psr\Log\NullLogger;
 | 
			
		||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
 | 
			
		||||
use Symfony\Component\Clock\MockClock;
 | 
			
		||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class AssociateByPhonenumberCommandHandlerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    private function getHandler(
 | 
			
		||||
        PersonACLAwareRepositoryInterface $personACLAwareRepository,
 | 
			
		||||
    ): AssociateByPhonenumberCommandHandler {
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $phonenumberHelper = new PhonenumberHelper(
 | 
			
		||||
            new ArrayAdapter(),
 | 
			
		||||
            new ParameterBag([
 | 
			
		||||
                'chill_main.phone_helper' => [
 | 
			
		||||
                    'default_carrier_code' => 'BE',
 | 
			
		||||
                ],
 | 
			
		||||
            ]),
 | 
			
		||||
            new NullLogger()
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return new AssociateByPhonenumberCommandHandler(
 | 
			
		||||
            $personACLAwareRepository,
 | 
			
		||||
            $phonenumberHelper,
 | 
			
		||||
            new MockClock(),
 | 
			
		||||
            $entityManager->reveal()
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testHandleWithPersonFoundByPhonenumber(): void
 | 
			
		||||
    {
 | 
			
		||||
        $person = new Person();
 | 
			
		||||
 | 
			
		||||
        $personAclAwareRepository = $this->prophesize(PersonACLAwareRepositoryInterface::class);
 | 
			
		||||
        $personAclAwareRepository->findByPhone(Argument::any())->willReturn([$person]);
 | 
			
		||||
 | 
			
		||||
        $handler = $this->getHandler($personAclAwareRepository->reveal());
 | 
			
		||||
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $handler($ticket, new AssociateByPhonenumberCommand('+3281136917'));
 | 
			
		||||
 | 
			
		||||
        self::assertSame($person, $ticket->getPersons()[0]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Tests\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\CreateTicketCommand;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\CreateTicketCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class CreateTicketCommandHandlerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    private function getHandler(): CreateTicketCommandHandler
 | 
			
		||||
    {
 | 
			
		||||
        return new CreateTicketCommandHandler();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testHandleWithoutReference(): void
 | 
			
		||||
    {
 | 
			
		||||
        $command = new CreateTicketCommand();
 | 
			
		||||
        $actual = ($this->getHandler())($command);
 | 
			
		||||
 | 
			
		||||
        self::assertInstanceOf(Ticket::class, $actual);
 | 
			
		||||
        self::assertEquals('', $actual->getExternalRef());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testHandleWithReference(): void
 | 
			
		||||
    {
 | 
			
		||||
        $command = new CreateTicketCommand($ref = 'external-ref');
 | 
			
		||||
        $actual = ($this->getHandler())($command);
 | 
			
		||||
 | 
			
		||||
        self::assertInstanceOf(Ticket::class, $actual);
 | 
			
		||||
        self::assertEquals($ref, $actual->getExternalRef());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,108 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Tests\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\ReplaceMotiveCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand;
 | 
			
		||||
use Chill\TicketBundle\Entity\Motive;
 | 
			
		||||
use Chill\TicketBundle\Entity\MotiveHistory;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
use Symfony\Component\Clock\MockClock;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
final class ReplaceMotiveCommandHandlerTest extends KernelTestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    private function buildHandler(
 | 
			
		||||
        EntityManagerInterface $entityManager,
 | 
			
		||||
    ): ReplaceMotiveCommandHandler {
 | 
			
		||||
        $clock = new MockClock();
 | 
			
		||||
 | 
			
		||||
        return new ReplaceMotiveCommandHandler($clock, $entityManager);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testHandleOnTicketWithoutMotive(): void
 | 
			
		||||
    {
 | 
			
		||||
        $motive = new Motive();
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $entityManager->persist(Argument::that(static function ($arg) use ($motive): bool {
 | 
			
		||||
            if (!$arg instanceof MotiveHistory) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $arg->getMotive() === $motive;
 | 
			
		||||
        }))->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
        $handler = $this->buildHandler($entityManager->reveal());
 | 
			
		||||
 | 
			
		||||
        $handler->handle($ticket, new ReplaceMotiveCommand($motive));
 | 
			
		||||
 | 
			
		||||
        self::assertSame($motive, $ticket->getMotive());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testHandleReplaceMotiveOnTicketWithExistingMotive(): void
 | 
			
		||||
    {
 | 
			
		||||
        $motive = new Motive();
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $history = new MotiveHistory(new Motive(), $ticket);
 | 
			
		||||
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $entityManager->persist(Argument::that(static function ($arg) use ($motive): bool {
 | 
			
		||||
            if (!$arg instanceof MotiveHistory) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $arg->getMotive() === $motive;
 | 
			
		||||
        }))->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
        $handler = $this->buildHandler($entityManager->reveal());
 | 
			
		||||
 | 
			
		||||
        $handler->handle($ticket, new ReplaceMotiveCommand($motive));
 | 
			
		||||
 | 
			
		||||
        self::assertSame($motive, $ticket->getMotive());
 | 
			
		||||
        self::assertCount(2, $ticket->getMotiveHistories());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testHandleReplaceMotiveOnTicketWithSameMotive(): void
 | 
			
		||||
    {
 | 
			
		||||
        $motive = new Motive();
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $history = new MotiveHistory($motive, $ticket);
 | 
			
		||||
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $entityManager->persist(Argument::that(static function ($arg) use ($motive): bool {
 | 
			
		||||
            if (!$arg instanceof MotiveHistory) {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return $arg->getMotive() === $motive;
 | 
			
		||||
        }))->shouldNotBeCalled();
 | 
			
		||||
 | 
			
		||||
        $handler = $this->buildHandler($entityManager->reveal());
 | 
			
		||||
 | 
			
		||||
        $handler->handle($ticket, new ReplaceMotiveCommand($motive));
 | 
			
		||||
 | 
			
		||||
        self::assertSame($motive, $ticket->getMotive());
 | 
			
		||||
        self::assertCount(1, $ticket->getMotiveHistories());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,122 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Tests\Action\Ticket\Handler;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\User;
 | 
			
		||||
use Chill\MainBundle\Entity\UserGroup;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\SetAddresseesCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\SetAddresseesCommand;
 | 
			
		||||
use Chill\TicketBundle\Entity\AddresseeHistory;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Component\Clock\MockClock;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
final class SetAddressesCommandHandlerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    public function testHandleOnEmptyAddresses(): void
 | 
			
		||||
    {
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $command = new SetAddresseesCommand([$user1 = new User(), $group1 = new UserGroup()]);
 | 
			
		||||
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $entityManager->persist(Argument::that(function ($arg) use ($user1) {
 | 
			
		||||
            return $arg instanceof AddresseeHistory && $arg->getAddressee() === $user1;
 | 
			
		||||
        }))->shouldBeCalledOnce();
 | 
			
		||||
        $entityManager->persist(Argument::that(function ($arg) use ($group1) {
 | 
			
		||||
            return $arg instanceof AddresseeHistory && $arg->getAddressee() === $group1;
 | 
			
		||||
        }))->shouldBeCalledOnce();
 | 
			
		||||
 | 
			
		||||
        $handler = $this->buildHandler($entityManager->reveal());
 | 
			
		||||
 | 
			
		||||
        $handler->handle($ticket, $command);
 | 
			
		||||
 | 
			
		||||
        self::assertCount(2, $ticket->getCurrentAddressee());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testHandleExistingUserIsNotRemovedNorCreatingDouble(): void
 | 
			
		||||
    {
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $user = new User();
 | 
			
		||||
        $history = new AddresseeHistory($user, new \DateTimeImmutable('1 month ago'), $ticket);
 | 
			
		||||
        $command = new SetAddresseesCommand([$user]);
 | 
			
		||||
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $entityManager->persist(Argument::that(function ($arg) use ($user) {
 | 
			
		||||
            return $arg instanceof AddresseeHistory && $arg->getAddressee() === $user;
 | 
			
		||||
        }))->shouldNotBeCalled();
 | 
			
		||||
 | 
			
		||||
        $handler = $this->buildHandler($entityManager->reveal());
 | 
			
		||||
 | 
			
		||||
        $handler->handle($ticket, $command);
 | 
			
		||||
 | 
			
		||||
        self::assertNull($history->getEndDate());
 | 
			
		||||
        self::assertCount(1, $ticket->getCurrentAddressee());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testHandleRemoveExistingAddressee(): void
 | 
			
		||||
    {
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $user = new User();
 | 
			
		||||
        $group = new UserGroup();
 | 
			
		||||
        $history = new AddresseeHistory($user, new \DateTimeImmutable('1 month ago'), $ticket);
 | 
			
		||||
        $command = new SetAddresseesCommand([$group]);
 | 
			
		||||
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $entityManager->persist(Argument::that(function ($arg) use ($group) {
 | 
			
		||||
            return $arg instanceof AddresseeHistory && $arg->getAddressee() === $group;
 | 
			
		||||
        }))->shouldBeCalled();
 | 
			
		||||
 | 
			
		||||
        $handler = $this->buildHandler($entityManager->reveal());
 | 
			
		||||
 | 
			
		||||
        $handler->handle($ticket, $command);
 | 
			
		||||
 | 
			
		||||
        self::assertNotNull($history->getEndDate());
 | 
			
		||||
        self::assertContains($group, $ticket->getCurrentAddressee());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testAddingDoublingAddresseeDoesNotCreateDoubleHistories(): void
 | 
			
		||||
    {
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $group = new UserGroup();
 | 
			
		||||
        $command = new SetAddresseesCommand([$group, $group]);
 | 
			
		||||
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
        $entityManager->persist(Argument::that(function ($arg) use ($group) {
 | 
			
		||||
            return $arg instanceof AddresseeHistory && $arg->getAddressee() === $group;
 | 
			
		||||
        }))->shouldBeCalledOnce();
 | 
			
		||||
 | 
			
		||||
        $handler = $this->buildHandler($entityManager->reveal());
 | 
			
		||||
 | 
			
		||||
        $handler->handle($ticket, $command);
 | 
			
		||||
 | 
			
		||||
        self::assertCount(1, $ticket->getCurrentAddressee());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildHandler(EntityManagerInterface $entityManager): SetAddresseesCommandHandler
 | 
			
		||||
    {
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->getUser()->willReturn(new User());
 | 
			
		||||
 | 
			
		||||
        return new SetAddresseesCommandHandler(new MockClock(), $entityManager, $security->reveal());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,104 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Chill\TicketBundle\Tests\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\TicketBundle\Action\Ticket\Handler\AddCommentCommandHandler;
 | 
			
		||||
use Chill\TicketBundle\Controller\AddCommentController;
 | 
			
		||||
use Chill\TicketBundle\Entity\Comment;
 | 
			
		||||
use Chill\TicketBundle\Entity\Ticket;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Prophecy\Argument;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
use Symfony\Component\Serializer\SerializerInterface;
 | 
			
		||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class AddCommentControllerTest extends KernelTestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    private SerializerInterface $serializer;
 | 
			
		||||
    private ValidatorInterface $validator;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
        $this->validator = self::getContainer()->get(ValidatorInterface::class);
 | 
			
		||||
        $this->serializer = self::getContainer()->get(SerializerInterface::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testAddComment(): void
 | 
			
		||||
    {
 | 
			
		||||
        $controller = $this->buildController(willFlush: true);
 | 
			
		||||
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $request = new Request(content: <<<'JSON'
 | 
			
		||||
            {"content":  "test"}
 | 
			
		||||
            JSON);
 | 
			
		||||
 | 
			
		||||
        $response = $controller->__invoke($ticket, $request);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(201, $response->getStatusCode());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testAddCommentWithBlankContent(): void
 | 
			
		||||
    {
 | 
			
		||||
        $controller = $this->buildController(willFlush: false);
 | 
			
		||||
 | 
			
		||||
        $ticket = new Ticket();
 | 
			
		||||
        $request = new Request(content: <<<'JSON'
 | 
			
		||||
            {"content":  ""}
 | 
			
		||||
            JSON);
 | 
			
		||||
 | 
			
		||||
        $response = $controller->__invoke($ticket, $request);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(422, $response->getStatusCode());
 | 
			
		||||
 | 
			
		||||
        $request = new Request(content: <<<'JSON'
 | 
			
		||||
            {"content":  null}
 | 
			
		||||
            JSON);
 | 
			
		||||
 | 
			
		||||
        $response = $controller->__invoke($ticket, $request);
 | 
			
		||||
 | 
			
		||||
        self::assertEquals(422, $response->getStatusCode());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildController(bool $willFlush): AddCommentController
 | 
			
		||||
    {
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted('ROLE_USER')->willReturn(true);
 | 
			
		||||
 | 
			
		||||
        $entityManager = $this->prophesize(EntityManagerInterface::class);
 | 
			
		||||
 | 
			
		||||
        if ($willFlush) {
 | 
			
		||||
            $entityManager->persist(Argument::type(Comment::class))->shouldBeCalled();
 | 
			
		||||
            $entityManager->flush()->shouldBeCalled();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $commandHandler = new AddCommentCommandHandler($entityManager->reveal());
 | 
			
		||||
 | 
			
		||||
        return new AddCommentController(
 | 
			
		||||
            $security->reveal(),
 | 
			
		||||
            $this->serializer,
 | 
			
		||||
            $this->validator,
 | 
			
		||||
            $commandHandler,
 | 
			
		||||
            $entityManager->reveal(),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user