mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-10 00:34:58 +00:00
Compare commits
91 Commits
user_edit_
...
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