Merge branch 'ticket-app/create-entities' into 'ticket-app-master'

Add phone number search function to PersonACLAwareRepository

See merge request Chill-Projet/chill-bundles!677
This commit is contained in:
Julien Fastré 2024-04-18 11:21:46 +00:00
commit 3f789ad0f4
52 changed files with 2437 additions and 56 deletions

View File

@ -55,7 +55,7 @@ Arborescence:
- person - person
- personvendee - personvendee
- household_edit_metadata - household_edit_metadata
- index.js - index.ts
``` ```
## Organisation des feuilles de styles ## Organisation des feuilles de styles

View File

@ -126,8 +126,9 @@
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"App\\": "tests/", "App\\": "tests",
"Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", "Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
"Chill\\TicketBundle\\Tests\\": "src/Bundle/ChillTicketBundle/tests",
"Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", "Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
"Chill\\Utils\\Rector\\Tests\\": "utils/rector/tests" "Chill\\Utils\\Rector\\Tests\\": "utils/rector/tests"
} }

View File

@ -49,6 +49,10 @@
<!-- temporarily removed, the time to find a fix --> <!-- temporarily removed, the time to find a fix -->
<exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude> <exclude>src/Bundle/ChillPersonBundle/Tests/Controller/PersonDuplicateControllerViewTest.php</exclude>
</testsuite> </testsuite>
<testsuite name="TicketBundle">
<directory suffix="Test.php">src/Bundle/ChillTicketBundle/tests/</directory>
</testsuite>
<!-- <!--
<testsuite name="ReportBundle"> <testsuite name="ReportBundle">
<directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory> <directory suffix="Test.php">src/Bundle/ChillReportBundle/Tests/</directory>

View File

@ -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\MainBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'chill_main_user_group')]
class UserGroup
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
private ?int $id = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
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;
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;
}
}

View File

@ -68,35 +68,37 @@
</div> </div>
{% endif %} {% endif %}
{% block content %} {% block wrapping_content %}
<div class="col-8 main_search"> {% block content %}
{% if app.user.isAbsent %} <div class="col-8 main_search">
<div class="d-flex flex-row mb-5 alert alert-warning" role="alert"> {% if app.user.isAbsent %}
<p class="m-2">{{'absence.You are marked as being absent'|trans }}</p> <div class="d-flex flex-row mb-5 alert alert-warning" role="alert">
<span class="ms-auto"> <p class="m-2">{{'absence.You are marked as being absent'|trans }}</p>
<a class="btn btn-remove" title="Modifier" href="{{ path('chill_main_user_absence_index') }}">{{ 'absence.Unset absence'|trans }}</a> <span class="ms-auto">
</span> <a class="btn btn-remove" title="Modifier" href="{{ path('chill_main_user_absence_index') }}">{{ 'absence.Unset absence'|trans }}</a>
</div> </span>
{% endif %} </div>
<h2>{{ 'Search'|trans }}</h2> {% endif %}
<h2>{{ 'Search'|trans }}</h2>
<form action="{{ path('chill_main_search') }}" method="get"> <form action="{{ path('chill_main_search') }}" method="get">
<input class="form-control form-control-lg" name="q" type="search" placeholder="{{ 'Search persons, ...'|trans }}" /> <input class="form-control form-control-lg" name="q" type="search" placeholder="{{ 'Search persons, ...'|trans }}" />
<div class="text-center"> <div class="text-center">
<button type="submit" class="btn btn-lg btn-warning mt-3"> <button type="submit" class="btn btn-lg btn-warning mt-3">
<i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }} <i class="fa fa-fw fa-search"></i> {{ 'Search'|trans }}
</button> </button>
<a class="btn btn-lg btn-misc mt-3" href="{{ path('chill_main_advanced_search_list') }}"> <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 }} <i class="fa fa-fw fa-search"></i> {{ 'Advanced search'|trans }}
</a> </a>
</div> </div>
</form> </form>
</div> </div>
{# DISABLED {{ chill_widget('homepage', {} ) }} #} {# DISABLED {{ chill_widget('homepage', {} ) }} #}
{% include '@ChillMain/Homepage/index.html.twig' %} {% include '@ChillMain/Homepage/index.html.twig' %}
{% endblock %}
{% endblock %} {% endblock %}
</div> </div>

View File

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

View File

@ -21,6 +21,8 @@ use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
final readonly class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface 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) \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();
}
} }

View File

@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Search\SearchApiQuery; use Chill\MainBundle\Search\SearchApiQuery;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use libphonenumber\PhoneNumber;
interface PersonACLAwareRepositoryInterface interface PersonACLAwareRepositoryInterface
{ {
@ -60,4 +61,13 @@ interface PersonACLAwareRepositoryInterface
?string $phonenumber = null, ?string $phonenumber = null,
?string $city = null ?string $city = null
): array; ): array;
/**
* @return list<Person>
*/
public function findByPhone(
PhoneNumber $phoneNumber,
int $start = 0,
int $limit = 20
): array;
} }

View File

@ -11,14 +11,17 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\Repository; namespace Chill\PersonBundle\Tests\Repository;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CenterRepositoryInterface; use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\CountryRepository; use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonPhone;
use Chill\PersonBundle\Repository\PersonACLAwareRepository; use Chill\PersonBundle\Repository\PersonACLAwareRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@ -98,4 +101,67 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
$this->assertStringContainsString('diallo', strtolower($person->getFirstName().' '.$person->getLastName())); $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];
}
} }

View File

@ -0,0 +1,66 @@
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"

View File

@ -1,4 +1,4 @@
// this file loads all assets from the Chill person bundle
module.exports = function(encore, entries) { module.exports = function(encore, entries) {
encore.addEntry('ticket_app', __dirname + '/src/Resources/public/vuejs/TicketApp/index.ts'); 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');
}; };

View File

@ -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,
) {}
}

View File

@ -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 = '',
) {}
}

View File

@ -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\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
use Chill\TicketBundle\Action\Ticket\AssociateByPhonenumberCommand;
use Chill\TicketBundle\Entity\PersonHistory;
use Chill\TicketBundle\Entity\Ticket;
use Doctrine\ORM\EntityManagerInterface;
use libphonenumber\PhoneNumberUtil;
use Symfony\Component\Clock\ClockInterface;
class AssociateByPhonenumberCommandHandler
{
public function __construct(
private PersonACLAwareRepositoryInterface $personRepository,
private PhoneNumberUtil $phoneNumberUtil,
private ClockInterface $clock,
private EntityManagerInterface $entityManager,
) {}
public function __invoke(Ticket $ticket, AssociateByPhonenumberCommand $command): void
{
$phone = $this->phoneNumberUtil->parse($command->phonenumber);
$persons = $this->personRepository->findByPhone($phone);
foreach ($persons as $person) {
$history = new PersonHistory($person, $ticket, $this->clock->now());
$this->entityManager->persist($history);
}
}
}

View File

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

View File

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

View File

@ -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,
) {}
}

View 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\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 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
) {}
#[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.');
}
$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()])
);
}
}

View File

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

View File

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

View File

@ -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\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(null, Response::HTTP_CREATED);
}
}

View File

@ -1,24 +0,0 @@
<?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 Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class ViewTicketController
{
#[Route('/{_locale}/ticket/ticket/{ticketId}', name: 'chill_ticket_ticket_view')]
public function __invoke(int $ticketId): Response
{
return new Response('ok');
}
}

View File

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

View File

@ -11,16 +11,54 @@ declare(strict_types=1);
namespace Chill\TicketBundle\DependencyInjection; namespace Chill\TicketBundle\DependencyInjection;
use Chill\TicketBundle\Controller\MotiveApiController;
use Chill\TicketBundle\Entity\Motive;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Symfony\Component\DependencyInjection\Loader; use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\HttpFoundation\Request;
class ChillTicketExtension extends Extension class ChillTicketExtension extends Extension implements PrependExtensionInterface
{ {
public function load(array $configs, ContainerBuilder $container) public function load(array $configs, ContainerBuilder $container)
{ {
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml'); $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,
],
],
],
],
],
]);
}
} }

View File

@ -0,0 +1,97 @@
<?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;
#[ORM\Entity()]
#[ORM\Table(name: 'addressee_history', schema: 'chill_ticket')]
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')]
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;
private ?\DateTimeImmutable $endDate = null;
public function __construct(
User|UserGroup $addressee,
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
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;
}
}
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;
}
}

View File

@ -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\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;
#[ORM\Entity()]
#[ORM\Table(name: 'comment', schema: 'chill_ticket')]
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')]
private ?int $id = null;
public function __construct(
#[ORM\ManyToOne(targetEntity: Ticket::class, inversedBy: 'comments')]
#[JoinColumn(nullable: false)]
private Ticket $ticket,
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::TEXT, nullable: false, options: ['default' => ''])]
private string $content = ''
) {}
public function getId(): ?int
{
return $this->id;
}
public function getContent(): string
{
return $this->content;
}
public function getTicket(): Ticket
{
return $this->ticket;
}
}

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

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

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

View File

@ -0,0 +1,87 @@
<?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;
}
}

View File

@ -0,0 +1,187 @@
<?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;
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();
}
/**
* Add a PersonHistory.
*
* This method should not be used, use @see{PersonHistory::__construct()} insted.
*/
public function addPersonHistory(PersonHistory $personHistory): void
{
$this->personHistories->add($personHistory);
}
public function addMotiveHistory(MotiveHistory $motiveHistory): void
{
$this->motiveHistories->add($motiveHistory);
}
/**
* @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;
}
}

View File

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

View File

@ -0,0 +1 @@
import './banner.scss';

View File

@ -0,0 +1,52 @@
import {DateTime, TranslatableString, User} 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
}
interface PersonHistory {
type: "ticket_person_history",
id: number,
startDate: DateTime,
endDate: null|DateTime,
person: Person,
removedBy: null,
createdBy: User|null,
createdAt: DateTime|null
}
interface MotiveHistory {
type: "ticket_motive_history",
id: number,
startDate: null,
endDate: null|DateTime,
motive: Motive,
createdBy: User|null,
createdAt: DateTime|null,
}
interface AddPersonEvent extends TicketHistory<"add_person", PersonHistory> {};
interface SetMotiveEvent extends TicketHistory<"set_motive", MotiveHistory> {};
type TicketHistoryLine = AddPersonEvent | SetMotiveEvent;
export interface Ticket {
type: "ticket_ticket"
id: number
externalRef: string
currentPersons: Person[]
currentMotive: null|Motive
history: TicketHistoryLine[],
}

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<p>{{ $t('hello', {'name': 'Boris'})}}</p>
</template>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,5 @@
export const messages = {
fr: {
hello: "Bonjour {name}"
}
};

View File

@ -0,0 +1,38 @@
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 {messages} from "./i18n/messages";
import App from './App.vue';
import {Ticket} from "../../types";
declare global {
interface Window {
initialTicket: string
}
}
const i18n = _createI18n(messages)
// the initial ticket is serialized there:
console.log(window.initialTicket);
// to have js object
const ticket = JSON.parse(window.initialTicket) as Ticket;
console.log("the ticket for this app (at page loading)", ticket);
for (const eh of ticket.history) {
if (eh.event_type === 'add_person') {
console.log("add_person", eh.data.person);
} else if (eh.event_type === 'set_motive') {
console.log("set_motive", eh.data.motive);
}
}
const _app = createApp({
template: '<app></app>',
})
.use(i18n)
.use(VueToast)
.component('app', App)
.mount('#ticketRoot');

View File

@ -0,0 +1,24 @@
<div class="banner banner-ticket">
<div id="header-ticket-main" class="header-name">
<div class="container-xxl">
<div class="row">
<div class="col-md-6 ps-md-5 ps-xxl-0">
TODO
</div>
<div class="col-md-6">
TODO
</div>
</div>
</div>
</div>
<div id="header-ticket-details" class="header-details">
<div class="container-xxl">
<div class="row justify-content-between">
<div class="col-md-12 ps-md-5 ps-xxl-0 container">
<p>TODO</p>
</div>
</div>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,21 @@
{% 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 %}
<div class="row">
<div class="col-md-8 col-sm-12">
{% block content %}{% endblock %}
</div>
</div>
{% endblock %}

View 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\Serializer\Normalizer;
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'])),
];
}
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(),
),
];
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
);
}
}

View File

@ -8,5 +8,11 @@ services:
tags: tags:
- controller.service_arguments - controller.service_arguments
Chill\TicketBundle\Action\Ticket\Handler\:
resource: '../Action/Ticket/Handler/'
Chill\TicketBundle\Serializer\:
resource: '../Serializer/'
Chill\TicketBundle\DataFixtures\:
resource: '../DataFixtures/'

View 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\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');
}
}

View File

@ -0,0 +1,62 @@
<?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\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 libphonenumber\PhoneNumberUtil;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Clock\MockClock;
/**
* @internal
*
* @coversNothing
*/
class AssociateByPhonenumberCommandHandlerTest extends TestCase
{
use ProphecyTrait;
private function getHandler(
PersonACLAwareRepositoryInterface $personACLAwareRepository,
): AssociateByPhonenumberCommandHandler {
$entityManager = $this->prophesize(EntityManagerInterface::class);
return new AssociateByPhonenumberCommandHandler(
$personACLAwareRepository,
PhoneNumberUtil::getInstance(),
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]);
}
}

View File

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

View File

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

View File

@ -0,0 +1,113 @@
<?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\ReplaceMotiveCommandHandler;
use Chill\TicketBundle\Controller\ReplaceMotiveController;
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;
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 ReplaceMotiveControllerTest extends KernelTestCase
{
use ProphecyTrait;
private SerializerInterface $serializer;
private ValidatorInterface $validator;
protected function setUp(): void
{
self::bootKernel();
$this->serializer = self::getContainer()->get(SerializerInterface::class);
$this->validator = self::getContainer()->get(ValidatorInterface::class);
}
protected function tearDown(): void
{
self::ensureKernelShutdown();
}
/**
* @dataProvider generateMotiveId
*/
public function testAddValidMotive(int $motiveId): void
{
$ticket = new Ticket();
$payload = <<<JSON
{"motive": {"type": "ticket_motive", "id": {$motiveId}}}
JSON;
$request = new Request(content: $payload);
$controller = $this->buildController();
$response = $controller($ticket, $request);
self::assertEquals(201, $response->getStatusCode());
}
private function buildController(): ReplaceMotiveController
{
$security = $this->prophesize(Security::class);
$security->isGranted('ROLE_USER')->willReturn(true);
$entityManager = $this->prophesize(EntityManagerInterface::class);
$entityManager->persist(Argument::type(MotiveHistory::class))->shouldBeCalled();
$entityManager->flush()->shouldBeCalled();
$handler = new ReplaceMotiveCommandHandler(
new MockClock(),
$entityManager->reveal()
);
return new ReplaceMotiveController(
$security->reveal(),
$handler,
$this->serializer,
$this->validator,
$entityManager->reveal(),
);
}
public static function generateMotiveId(): iterable
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$motive = $em->createQuery('SELECT m FROM '.Motive::class.' m ')
->setMaxResults(1)
->getOneOrNullResult();
if (null === $motive) {
throw new \RuntimeException('the motive table seems to be empty');
}
self::ensureKernelShutdown();
yield [$motive->getId()];
}
}

View 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\Tests\Entity;
use Chill\PersonBundle\Entity\Person;
use Chill\TicketBundle\Entity\Motive;
use Chill\TicketBundle\Entity\MotiveHistory;
use Chill\TicketBundle\Entity\PersonHistory;
use Chill\TicketBundle\Entity\Ticket;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
/**
* @internal
*
* @coversNothing
*/
class TicketTest extends KernelTestCase
{
public function testGetMotive(): void
{
$ticket = new Ticket();
$motive = new Motive();
self::assertNull($ticket->getMotive());
$history = new MotiveHistory($motive, $ticket);
self::assertSame($motive, $ticket->getMotive());
self::assertCount(1, $ticket->getMotiveHistories());
// replace motive
$motive2 = new Motive();
$history->setEndDate(new \DateTimeImmutable());
$history2 = new MotiveHistory($motive2, $ticket);
self::assertCount(2, $ticket->getMotiveHistories());
self::assertSame($motive2, $ticket->getMotive());
}
public function testGetPerson(): void
{
$ticket = new Ticket();
$person = new Person();
self::assertEquals([], $ticket->getPersons());
$history = new PersonHistory($person, $ticket, new \DateTimeImmutable('now'));
self::assertCount(1, $ticket->getPersons());
self::assertSame($person, $ticket->getPersons()[0]);
}
}

View File

@ -0,0 +1,144 @@
<?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\Serializer\Normalizer;
use Chill\PersonBundle\Entity\Person;
use Chill\TicketBundle\Entity\Motive;
use Chill\TicketBundle\Entity\MotiveHistory;
use Chill\TicketBundle\Entity\PersonHistory;
use Chill\TicketBundle\Entity\Ticket;
use Chill\TicketBundle\Serializer\Normalizer\TicketNormalizer;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* @internal
*
* @coversNothing
*/
class TicketNormalizerTest extends KernelTestCase
{
use ProphecyTrait;
/**
* @dataProvider provideTickets
*/
public function testNormalize(Ticket $ticket, array $expected): void
{
$actual = $this->buildNormalizer()->normalize($ticket, 'json', ['groups' => 'read']);
self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
foreach (array_keys($expected) as $k) {
if ('history' === $k) {
continue;
}
self::assertEqualsCanonicalizing($expected[$k], $actual[$k], sprintf("assert the content of the '%s' key", $k));
}
self::assertArrayHasKey('history', $actual);
self::assertIsArray($actual['history']);
foreach ($actual['history'] as $k => $eventType) {
self::assertEquals($expected['history'][$k]['event_type'], $eventType['event_type']);
}
}
private function buildNormalizer(): TicketNormalizer
{
$normalizer = $this->prophesize(NormalizerInterface::class);
// empty array
$normalizer->normalize(
Argument::that(fn ($arg) => is_array($arg) && 0 === count($arg)),
'json',
Argument::type('array')
)->willReturn([]);
// array of mixed objects
$normalizer->normalize(
Argument::that(fn ($arg) => is_array($arg) && 0 < count($arg) && is_object($arg[0])),
'json',
Argument::type('array')
)->will(function ($args) {
return array_fill(0, count($args[0]), 'embedded');
});
// array of event type
$normalizer->normalize(
Argument::that(fn ($arg) => is_array($arg) && 0 < count($arg) && is_array($arg[0]) && array_key_exists('event_type', $arg[0])),
'json',
Argument::type('array')
)->will(function ($args): array {
$events = [];
foreach ($args[0] as $event) {
$events[] = $event['event_type'];
}
return $events;
});
// datetime
$normalizer->normalize(Argument::type(\DateTimeImmutable::class), 'json', Argument::type('array'))
->will(function ($args) { return $args[0]->getTimestamp(); });
$normalizer->normalize(Argument::type(Motive::class), 'json', Argument::type('array'))->willReturn(['type' => 'motive', 'id' => 0]);
$normalizer->normalize(Argument::type(PersonHistory::class), 'json', Argument::type('array'))
->willReturn(['personHistory']);
$normalizer->normalize(Argument::type(MotiveHistory::class), 'json', Argument::type('array'))
->willReturn(['motiveHistory']);
$normalizer->normalize(null, 'json', Argument::type('array'))->willReturn(null);
$ticketNormalizer = new TicketNormalizer();
$ticketNormalizer->setNormalizer($normalizer->reveal());
return $ticketNormalizer;
}
public static function provideTickets(): iterable
{
yield [
new Ticket(),
[
'type' => 'ticket_ticket',
'id' => null,
'externalRef' => '',
'currentPersons' => [],
'currentAddressees' => [],
'currentInputs' => [],
'currentMotive' => null,
'history' => [],
],
];
$ticket = new Ticket();
$ticket->setExternalRef('2134');
$personHistory = new PersonHistory(new Person(), $ticket, new \DateTimeImmutable('2024-04-01T12:00:00'));
$ticketHistory = new MotiveHistory(new Motive(), $ticket, new \DateTimeImmutable('2024-04-01T12:02:00'));
yield [
$ticket,
[
'type' => 'ticket_ticket',
'id' => null,
'externalRef' => '2134',
'currentPersons' => ['embedded'],
'currentAddressees' => [],
'currentInputs' => [],
'currentMotive' => ['type' => 'motive', 'id' => 0],
'history' => [['event_type' => 'add_person'], ['event_type' => 'set_motive']],
],
];
}
}

View File

@ -23,7 +23,7 @@ if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env
(new Dotenv(false))->populate($env); (new Dotenv(false))->populate($env);
} else { } else {
// load all the .env files // load all the .env files
(new Dotenv(false))->loadEnv(dirname(__DIR__).'/../../.env.test'); (new Dotenv(false))->loadEnv(dirname(__DIR__).'/../../.env');
} }
$_SERVER += $_ENV; $_SERVER += $_ENV;

View File

@ -44,5 +44,5 @@ return [
Chill\WopiBundle\ChillWopiBundle::class => ['all' => true], Chill\WopiBundle\ChillWopiBundle::class => ['all' => true],
\Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], \Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
Chill\EventBundle\ChillEventBundle::class => ['all' => true], Chill\EventBundle\ChillEventBundle::class => ['all' => true],
Chill\TicketBundle\ChillTicketBundle::class => ['all' => true],
]; ];

View File

@ -14,6 +14,7 @@ doctrine_migrations:
'Chill\Migrations\DocGenerator': '@ChillDocGeneratorBundle/migrations' 'Chill\Migrations\DocGenerator': '@ChillDocGeneratorBundle/migrations'
'Chill\Migrations\Calendar': '@ChillCalendarBundle/migrations' 'Chill\Migrations\Calendar': '@ChillCalendarBundle/migrations'
'Chill\Migrations\Budget': '@ChillBudgetBundle/migrations' 'Chill\Migrations\Budget': '@ChillBudgetBundle/migrations'
'Chill\Migrations\Ticket': '@ChillTicketBundle/migrations'
all_or_nothing: all_or_nothing:
true true