mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-01 02:49:42 +00:00
Support for parent/children motives
This commit is contained in:
@@ -112,7 +112,7 @@ paths:
|
|||||||
- no
|
- no
|
||||||
- name: byMotives
|
- name: byMotives
|
||||||
in: query
|
in: query
|
||||||
description: the motives of the ticket
|
description: the motives of the ticket. All the descendants of the motive are taken into account.
|
||||||
required: false
|
required: false
|
||||||
style: form
|
style: form
|
||||||
explode: false
|
explode: false
|
||||||
|
@@ -13,6 +13,7 @@ namespace Chill\TicketBundle\Controller;
|
|||||||
|
|
||||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
|
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectNormalizer;
|
||||||
use Chill\MainBundle\CRUD\Controller\ApiController;
|
use Chill\MainBundle\CRUD\Controller\ApiController;
|
||||||
|
use Chill\TicketBundle\Serializer\Normalizer\MotiveNormalizer;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
@@ -21,13 +22,13 @@ final class MotiveApiController extends ApiController
|
|||||||
protected function customizeQuery(string $action, Request $request, $query): void
|
protected function customizeQuery(string $action, Request $request, $query): void
|
||||||
{
|
{
|
||||||
/* @var $query QueryBuilder */
|
/* @var $query QueryBuilder */
|
||||||
$query->andWhere('e.active = TRUE');
|
$query->andWhere('e.active = TRUE')->andWhere('e.parent IS NULL');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array
|
protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array
|
||||||
{
|
{
|
||||||
return match ($request->getMethod()) {
|
return match ($request->getMethod()) {
|
||||||
Request::METHOD_GET => ['groups' => ['read', StoredObjectNormalizer::DOWNLOAD_LINK_ONLY]],
|
Request::METHOD_GET => ['groups' => ['read', 'read:extended', StoredObjectNormalizer::DOWNLOAD_LINK_ONLY, MotiveNormalizer::GROUP_PARENT_TO_CHILDREN]],
|
||||||
default => parent::getContextForSerialization($action, $request, $_format, $entity),
|
default => parent::getContextForSerialization($action, $request, $_format, $entity),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,6 @@ namespace Chill\TicketBundle\DataFixtures\ORM;
|
|||||||
|
|
||||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
use Chill\TicketBundle\Entity\EmergencyStatusEnum;
|
|
||||||
use Chill\TicketBundle\Entity\Motive;
|
use Chill\TicketBundle\Entity\Motive;
|
||||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||||
@@ -35,6 +34,7 @@ final class LoadMotives extends Fixture implements FixtureGroupInterface
|
|||||||
['label' => '☀️ De 07h à 21h', 'path' => __DIR__.'/docs/peloton_2.pdf'],
|
['label' => '☀️ De 07h à 21h', 'path' => __DIR__.'/docs/peloton_2.pdf'],
|
||||||
['label' => 'Dimanche et jours fériés', 'path' => __DIR__.'/docs/schema_1.png'],
|
['label' => 'Dimanche et jours fériés', 'path' => __DIR__.'/docs/schema_1.png'],
|
||||||
];
|
];
|
||||||
|
$motivesByLabel = [];
|
||||||
|
|
||||||
foreach (explode("\n", self::MOTIVES) as $row) {
|
foreach (explode("\n", self::MOTIVES) as $row) {
|
||||||
if ('' === trim($row)) {
|
if ('' === trim($row)) {
|
||||||
@@ -46,50 +46,65 @@ final class LoadMotives extends Fixture implements FixtureGroupInterface
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$motive = new Motive();
|
$labels = explode(' > ', $data[0]);
|
||||||
$motive->setLabel(['fr' => trim((string) $data[0])]);
|
$parent = null;
|
||||||
$motive->setMakeTicketEmergency(match ($data[1]) {
|
|
||||||
'true' => EmergencyStatusEnum::YES,
|
|
||||||
'false' => EmergencyStatusEnum::NO,
|
|
||||||
default => throw new \UnexpectedValueException('Unexpected value'),
|
|
||||||
});
|
|
||||||
|
|
||||||
$numberOfDocs = (int) $data[2];
|
while (count($labels) > 0) {
|
||||||
for ($i = 1; $i <= $numberOfDocs; ++$i) {
|
$label = array_shift($labels);
|
||||||
$doc = $docs[$i - 1];
|
dump($labels);
|
||||||
$storedObject = new StoredObject();
|
if (isset($motivesByLabel[$label])) {
|
||||||
$storedObject->setTitle($doc['label']);
|
$motive = $motivesByLabel[$label];
|
||||||
|
} else {
|
||||||
|
$motive = new Motive();
|
||||||
|
$motive->setLabel(['fr' => $label]);
|
||||||
|
$motivesByLabel[$label] = $motive;
|
||||||
|
}
|
||||||
|
|
||||||
$content = file_get_contents($doc['path']);
|
if (null !== $parent) {
|
||||||
$contentType = match (substr($doc['path'], -3, 3)) {
|
$motive->setParent($parent);
|
||||||
'pdf' => 'application/pdf',
|
}
|
||||||
'png' => 'image/png',
|
|
||||||
default => throw new \UnexpectedValueException('Not supported content type here'),
|
|
||||||
};
|
|
||||||
$this->storedObjectManager->write($storedObject, $content, $contentType);
|
|
||||||
|
|
||||||
$motive->addStoredObject($storedObject);
|
$manager->persist($motive);
|
||||||
$manager->persist($storedObject);
|
$parent = $motive;
|
||||||
}
|
|
||||||
|
if (0 === count($labels)) {
|
||||||
|
// this is the last one, we add data
|
||||||
|
$numberOfDocs = (int) $data[2];
|
||||||
|
for ($i = 1; $i <= $numberOfDocs; ++$i) {
|
||||||
|
$doc = $docs[$i - 1];
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
$storedObject->setTitle($doc['label']);
|
||||||
|
|
||||||
|
$content = file_get_contents($doc['path']);
|
||||||
|
$contentType = match (substr($doc['path'], -3, 3)) {
|
||||||
|
'pdf' => 'application/pdf',
|
||||||
|
'png' => 'image/png',
|
||||||
|
default => throw new \UnexpectedValueException('Not supported content type here'),
|
||||||
|
};
|
||||||
|
$this->storedObjectManager->write($storedObject, $content, $contentType);
|
||||||
|
|
||||||
|
$motive->addStoredObject($storedObject);
|
||||||
|
$manager->persist($storedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
foreach (array_slice($data, 3) as $supplementaryComment) {
|
foreach (array_slice($data, 3) as $supplementaryComment) {
|
||||||
if ('' !== trim((string) $supplementaryComment)) {
|
if ('' !== trim((string) $supplementaryComment)) {
|
||||||
$motive->addSupplementaryComment(['label' => trim((string) $supplementaryComment)]);
|
$motive->addSupplementaryComment(['label' => trim((string) $supplementaryComment)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$manager->persist($motive);
|
|
||||||
}
|
}
|
||||||
$manager->flush();
|
$manager->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
private const MOTIVES = <<<'CSV'
|
private const MOTIVES = <<<'CSV'
|
||||||
"Coordonnées",false,"3","Nouvelles coordonnées",
|
"Motif administratif > Coordonnées",false,"3","Nouvelles coordonnées",
|
||||||
"Horaire de passage",false,"0",
|
"Organisation > Horaire de passage",false,"0",
|
||||||
"Retard de livraison",false,"0",
|
"Organisation > Livraison > Retard de livraison",false,"0",
|
||||||
"Erreur de livraison",false,"0",
|
"Organisation > Livraison > Erreur de livraison",false,"0",
|
||||||
"Colis incomplet",false,"0",
|
"Organisation > Livraison > Colis incomplet",false,"0",
|
||||||
"MATLOC",false,"0",
|
"MATLOC",false,"0",
|
||||||
"Retard DASRI",false,"1",
|
"Retard DASRI",false,"1",
|
||||||
"Planning d'astreintes",false,"0",
|
"Planning d'astreintes",false,"0",
|
||||||
@@ -116,7 +131,7 @@ final class LoadMotives extends Fixture implements FixtureGroupInterface
|
|||||||
"Mauvaise adresse",false,"0",
|
"Mauvaise adresse",false,"0",
|
||||||
"Patient absent",false,"0",
|
"Patient absent",false,"0",
|
||||||
"Annulation",false,"0",
|
"Annulation",false,"0",
|
||||||
"Colis perdu",false,"0",
|
"Organisation > Livraison > Colis perdu",false,"0",
|
||||||
"Changement de rendez-vous",false,"0",
|
"Changement de rendez-vous",false,"0",
|
||||||
"Coordination interservices",false,"0",
|
"Coordination interservices",false,"0",
|
||||||
"Problème de substitution produits",true,"0",
|
"Problème de substitution produits",true,"0",
|
||||||
|
@@ -15,6 +15,7 @@ use Chill\DocStoreBundle\Entity\StoredObject;
|
|||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\Common\Collections\ReadableCollection;
|
use Doctrine\Common\Collections\ReadableCollection;
|
||||||
|
use Doctrine\Common\Collections\Selectable;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
|
|
||||||
@@ -26,19 +27,15 @@ class Motive
|
|||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
|
||||||
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||||
#[Serializer\Groups(['read'])]
|
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||||
#[Serializer\Groups(['read'])]
|
|
||||||
private array $label = [];
|
private array $label = [];
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
|
||||||
#[Serializer\Groups(['read'])]
|
|
||||||
private bool $active = true;
|
private bool $active = true;
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, nullable: true, enumType: EmergencyStatusEnum::class)]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, nullable: true, enumType: EmergencyStatusEnum::class)]
|
||||||
#[Serializer\Groups(['read'])]
|
|
||||||
private ?EmergencyStatusEnum $makeTicketEmergency = null;
|
private ?EmergencyStatusEnum $makeTicketEmergency = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,12 +46,22 @@ class Motive
|
|||||||
private Collection $storedObjects;
|
private Collection $storedObjects;
|
||||||
|
|
||||||
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['jsonb' => true, 'default' => '[]'])]
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['jsonb' => true, 'default' => '[]'])]
|
||||||
#[Serializer\Groups(['read'])]
|
|
||||||
private array $supplementaryComments = [];
|
private array $supplementaryComments = [];
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Motive::class, inversedBy: 'children')]
|
||||||
|
private ?Motive $parent = null;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection<int, Motive>&Selectable<int, Motive>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(targetEntity: Motive::class, mappedBy: 'parent')]
|
||||||
|
private Collection&Selectable $children;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->storedObjects = new ArrayCollection();
|
$this->storedObjects = new ArrayCollection();
|
||||||
|
$this->children = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addStoredObject(StoredObject $storedObject): void
|
public function addStoredObject(StoredObject $storedObject): void
|
||||||
@@ -69,7 +76,6 @@ class Motive
|
|||||||
$this->storedObjects->removeElement($storedObject);
|
$this->storedObjects->removeElement($storedObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Serializer\Groups(['read'])]
|
|
||||||
public function getStoredObjects(): ReadableCollection
|
public function getStoredObjects(): ReadableCollection
|
||||||
{
|
{
|
||||||
return $this->storedObjects;
|
return $this->storedObjects;
|
||||||
@@ -142,4 +148,74 @@ class Motive
|
|||||||
$this->supplementaryComments[$key] = $supplementaryComment;
|
$this->supplementaryComments[$key] = $supplementaryComment;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isParent(): bool
|
||||||
|
{
|
||||||
|
return $this->children->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isChild(): bool
|
||||||
|
{
|
||||||
|
return null !== $this->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setParent(?Motive $parent): void
|
||||||
|
{
|
||||||
|
if (null !== $parent) {
|
||||||
|
$parent->addChild($this);
|
||||||
|
} else {
|
||||||
|
$this->parent->removeChild($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->parent = $parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal use @see{setParent} instead
|
||||||
|
*/
|
||||||
|
public function addChild(Motive $child): void
|
||||||
|
{
|
||||||
|
if (!$this->children->contains($child)) {
|
||||||
|
$this->children->add($child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal use @see{setParent} with null as argument instead
|
||||||
|
*/
|
||||||
|
public function removeChild(Motive $child): void
|
||||||
|
{
|
||||||
|
$this->children->removeElement($child);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getChildren(): ReadableCollection&Selectable
|
||||||
|
{
|
||||||
|
return $this->children;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParent(): ?Motive
|
||||||
|
{
|
||||||
|
return $this->parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the descendants of the current entity.
|
||||||
|
*
|
||||||
|
* This method collects all descendant entities recursively, starting from the current entity
|
||||||
|
* and including all of its children and their descendants.
|
||||||
|
*
|
||||||
|
* @return ReadableCollection&Selectable A collection containing the current entity and all its descendants
|
||||||
|
*/
|
||||||
|
public function getDescendants(): ReadableCollection&Selectable
|
||||||
|
{
|
||||||
|
$collection = new ArrayCollection([$this]);
|
||||||
|
|
||||||
|
foreach ($this->getChildren() as $child) {
|
||||||
|
foreach ($child->getDescendants() as $descendant) {
|
||||||
|
$collection->add($descendant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $collection;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -42,7 +42,7 @@ class MotiveHistory implements TrackCreationInterface
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
#[ORM\ManyToOne(targetEntity: Motive::class)]
|
#[ORM\ManyToOne(targetEntity: Motive::class)]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
#[Serializer\Groups(['read'])]
|
#[Serializer\Groups(['read', 'read:children-to-parent'])]
|
||||||
private Motive $motive,
|
private Motive $motive,
|
||||||
#[ORM\ManyToOne(targetEntity: Ticket::class)]
|
#[ORM\ManyToOne(targetEntity: Ticket::class)]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
@@ -113,10 +113,11 @@ final readonly class TicketACLAwareRepository implements TicketACLAwareRepositor
|
|||||||
if (array_key_exists('byMotives', $params)) {
|
if (array_key_exists('byMotives', $params)) {
|
||||||
$byMotives = $qb->expr()->orX();
|
$byMotives = $qb->expr()->orX();
|
||||||
foreach ($params['byMotives'] as $motive) {
|
foreach ($params['byMotives'] as $motive) {
|
||||||
|
$motivesWithDescendants = $motive->getDescendants()->toArray();
|
||||||
$byMotives->add(
|
$byMotives->add(
|
||||||
$qb->expr()->exists(sprintf(
|
$qb->expr()->exists(sprintf(
|
||||||
'SELECT 1 FROM %s tp_motive_%d WHERE tp_motive_%d.ticket = t
|
'SELECT 1 FROM %s tp_motive_%d WHERE tp_motive_%d.ticket = t
|
||||||
AND tp_motive_%d.motive = :motive_%d AND tp_motive_%d.endDate IS NULL
|
AND tp_motive_%d.motive IN (:motives_%d) AND tp_motive_%d.endDate IS NULL
|
||||||
',
|
',
|
||||||
MotiveHistory::class,
|
MotiveHistory::class,
|
||||||
++$i,
|
++$i,
|
||||||
@@ -126,7 +127,7 @@ final readonly class TicketACLAwareRepository implements TicketACLAwareRepositor
|
|||||||
$i,
|
$i,
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
$qb->setParameter(sprintf('motive_%d', $i), $motive);
|
$qb->setParameter(sprintf('motives_%d', $i), $motivesWithDescendants);
|
||||||
}
|
}
|
||||||
$qb->andWhere($byMotives);
|
$qb->andWhere($byMotives);
|
||||||
}
|
}
|
||||||
|
@@ -8,14 +8,32 @@ import { Person } from "ChillPersonAssets/types";
|
|||||||
import { Thirdparty } from "../../../../ChillThirdPartyBundle/Resources/public/types";
|
import { Thirdparty } from "../../../../ChillThirdPartyBundle/Resources/public/types";
|
||||||
import { StoredObject } from "ChillDocStoreAssets/types";
|
import { StoredObject } from "ChillDocStoreAssets/types";
|
||||||
|
|
||||||
export interface Motive {
|
interface MotiveBase {
|
||||||
type: "ticket_motive";
|
type: "ticket_motive";
|
||||||
id: number;
|
id: number;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
label: TranslatableString;
|
label: TranslatableString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represent a motive with basic information and parent motive.
|
||||||
|
*
|
||||||
|
* Match the "read" and "read:children-to-parent" serializer groups.
|
||||||
|
*/
|
||||||
|
export interface MotiveWithParent extends MotiveBase {
|
||||||
|
parent: MotiveWithParent | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a motive for a ticket, including details like emergency status, stored objects, and supplementary comments.
|
||||||
|
*
|
||||||
|
* Match the "read:extended" serializer group in MotiveNormalizer.
|
||||||
|
*/
|
||||||
|
export interface Motive extends MotiveBase {
|
||||||
makeTicketEmergency: TicketEmergencyState;
|
makeTicketEmergency: TicketEmergencyState;
|
||||||
storedObjects: StoredObject[];
|
storedObjects: StoredObject[];
|
||||||
supplementaryComments: { label: string }[];
|
supplementaryComments: { label: string }[];
|
||||||
|
children: Motive[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TicketState = "open" | "closed" | "close";
|
export type TicketState = "open" | "closed" | "close";
|
||||||
@@ -45,7 +63,7 @@ export interface MotiveHistory {
|
|||||||
id: number;
|
id: number;
|
||||||
startDate: null;
|
startDate: null;
|
||||||
endDate: null | DateTime;
|
endDate: null | DateTime;
|
||||||
motive: Motive;
|
motive: MotiveWithParent;
|
||||||
createdBy: User | null;
|
createdBy: User | null;
|
||||||
createdAt: DateTime | null;
|
createdAt: DateTime | null;
|
||||||
}
|
}
|
||||||
@@ -140,7 +158,7 @@ interface BaseTicket<
|
|||||||
createdAt: DateTime | null;
|
createdAt: DateTime | null;
|
||||||
currentAddressees: UserGroupOrUser[];
|
currentAddressees: UserGroupOrUser[];
|
||||||
currentPersons: Person[];
|
currentPersons: Person[];
|
||||||
currentMotive: null | Motive;
|
currentMotive: MotiveWithParent | null;
|
||||||
currentState: TicketState | null;
|
currentState: TicketState | null;
|
||||||
emergency: TicketEmergencyState | null;
|
emergency: TicketEmergencyState | null;
|
||||||
caller: Person | Thirdparty | null;
|
caller: Person | Thirdparty | null;
|
||||||
@@ -185,7 +203,7 @@ export interface TicketFilterParams {
|
|||||||
|
|
||||||
export interface TicketInitForm {
|
export interface TicketInitForm {
|
||||||
content: string;
|
content: string;
|
||||||
motive?: Motive;
|
motive?: MotiveWithParent | null;
|
||||||
addressees: UserGroupOrUser[];
|
addressees: UserGroupOrUser[];
|
||||||
persons: Person[];
|
persons: Person[];
|
||||||
caller: Person | null;
|
caller: Person | null;
|
||||||
|
@@ -179,7 +179,7 @@ import {
|
|||||||
UserGroup,
|
UserGroup,
|
||||||
UserGroupOrUser,
|
UserGroupOrUser,
|
||||||
} from "../../../../../../../ChillMainBundle/Resources/public/types";
|
} from "../../../../../../../ChillMainBundle/Resources/public/types";
|
||||||
import { Comment, Motive, Ticket } from "../../../types";
|
import { Comment, Motive, MotiveWithParent, Ticket } from "../../../types";
|
||||||
import { Person } from "ChillPersonAssets/types";
|
import { Person } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
@@ -252,7 +252,7 @@ const returnPath = computed((): string => {
|
|||||||
return returnPath;
|
return returnPath;
|
||||||
});
|
});
|
||||||
|
|
||||||
const motive = ref(ticket.value.currentMotive as Motive);
|
const motive = ref(ticket.value.currentMotive as MotiveWithParent | null);
|
||||||
const content = ref("" as Comment["content"]);
|
const content = ref("" as Comment["content"]);
|
||||||
const addressees = ref(ticket.value.currentAddressees as UserGroupOrUser[]);
|
const addressees = ref(ticket.value.currentAddressees as UserGroupOrUser[]);
|
||||||
const persons = ref(ticket.value.currentPersons as Person[]);
|
const persons = ref(ticket.value.currentPersons as Person[]);
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
class="badge-user-group"
|
class="badge-user-group"
|
||||||
:style="`background-color: ${addressee.backgroundColor}; color: ${addressee.foregroundColor};`"
|
:style="`background-color: ${addressee.backgroundColor}; color: ${addressee.foregroundColor};`"
|
||||||
>
|
>
|
||||||
{{ addressee.label.fr }}
|
{{ localizeString(addressee.label) }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="addressee.type === 'user'" class="badge-user">
|
<span v-else-if="addressee.type === 'user'" class="badge-user">
|
||||||
<user-render-box-badge :user="addressee"
|
<user-render-box-badge :user="addressee"
|
||||||
@@ -24,6 +24,9 @@ import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRen
|
|||||||
// Types
|
// Types
|
||||||
import { UserGroupOrUser } from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
import { UserGroupOrUser } from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||||
|
|
||||||
defineProps<{ addressees: UserGroupOrUser[] }>();
|
defineProps<{ addressees: UserGroupOrUser[] }>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@@ -6,7 +6,9 @@
|
|||||||
<h1>
|
<h1>
|
||||||
{{ getTicketTitle(ticket) }}
|
{{ getTicketTitle(ticket) }}
|
||||||
<peloton-component
|
<peloton-component
|
||||||
:stored-objects="ticket.currentMotive?.storedObjects ?? null"
|
:stored-objects="
|
||||||
|
currentMotive ? currentMotive.storedObjects : null
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -91,7 +93,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from "vue";
|
import { computed, ComputedRef, ref } from "vue";
|
||||||
import { useToast } from "vue-toast-notification";
|
import { useToast } from "vue-toast-notification";
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
@@ -102,7 +104,7 @@ import StateToggleComponent from "./State/StateToggleComponent.vue";
|
|||||||
import PersonComponent from "./Person/PersonComponent.vue";
|
import PersonComponent from "./Person/PersonComponent.vue";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Ticket } from "../../../types";
|
import { Motive, Ticket } from "../../../types";
|
||||||
import { Person } from "ChillPersonAssets/types";
|
import { Person } from "ChillPersonAssets/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -127,7 +129,7 @@ import { useStore } from "vuex";
|
|||||||
// Utils
|
// Utils
|
||||||
import { getTicketTitle } from "../utils/utils";
|
import { getTicketTitle } from "../utils/utils";
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
ticket: Ticket;
|
ticket: Ticket;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -135,6 +137,10 @@ const store = useStore();
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const today = ref(new Date());
|
const today = ref(new Date());
|
||||||
|
|
||||||
|
const currentMotive: ComputedRef<Motive | null> = computed(() =>
|
||||||
|
store.getters.getMotiveById(props.ticket.currentMotive?.id),
|
||||||
|
);
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
today.value = new Date();
|
today.value = new Date();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
@@ -26,22 +26,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { reactive, ref, watch, computed } from "vue";
|
import { reactive, ref, watch, computed, ComputedRef } from "vue";
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
|
import CommentEditor from "ChillMainAssets/vuejs/_components/CommentEditor/CommentEditor.vue";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Motive } from "../../../../types";
|
import { Motive, MotiveWithParent } from "../../../../types";
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { StoredObject } from "ChillDocStoreAssets/types";
|
import { StoredObject } from "ChillDocStoreAssets/types";
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue?: string;
|
modelValue?: string;
|
||||||
motive?: Motive;
|
motive?: MotiveWithParent | null;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
const supplementaryCommentsInput = reactive<string[]>([]);
|
const supplementaryCommentsInput = reactive<string[]>([]);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -50,11 +52,14 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const content = ref(props.modelValue);
|
const content = ref(props.modelValue);
|
||||||
|
const motive: ComputedRef<Motive | null> = computed(() =>
|
||||||
|
store.getters.getMotiveById(props.motive?.id),
|
||||||
|
);
|
||||||
|
|
||||||
const aggregateSupplementaryComments = computed(() => {
|
function aggregateSupplementaryComments() {
|
||||||
let supplementaryText = " \n\n ";
|
let supplementaryText = " \n\n ";
|
||||||
if (props.motive && props.motive.supplementaryComments) {
|
if (props.motive && motive.value && motive.value.supplementaryComments) {
|
||||||
props.motive.supplementaryComments.forEach(
|
motive.value.supplementaryComments.forEach(
|
||||||
(item: { label: string }, index: number) => {
|
(item: { label: string }, index: number) => {
|
||||||
if (supplementaryCommentsInput[index]) {
|
if (supplementaryCommentsInput[index]) {
|
||||||
supplementaryText +=
|
supplementaryText +=
|
||||||
@@ -65,18 +70,18 @@ const aggregateSupplementaryComments = computed(() => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (content.value || "") + supplementaryText;
|
return (content.value || "") + supplementaryText;
|
||||||
});
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
supplementaryCommentsInput,
|
supplementaryCommentsInput,
|
||||||
() => {
|
() => {
|
||||||
emit("update:modelValue", aggregateSupplementaryComments.value);
|
emit("update:modelValue", aggregateSupplementaryComments());
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(content, () => {
|
watch(content, () => {
|
||||||
emit("update:modelValue", aggregateSupplementaryComments.value);
|
emit("update:modelValue", aggregateSupplementaryComments());
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@@ -1,24 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="col-12 fw-bolder">
|
<div class="col-12 fw-bolder">
|
||||||
{{ localizeTranslatableString(motiveHistory.motive.label) }}
|
{{ motiveLabelRecursive(props.motiveHistory.motive) }}
|
||||||
<peloton-component
|
<peloton-component
|
||||||
:stored-objects="motiveHistory.motive.storedObjects ?? null"
|
:stored-objects="motive ? motive.storedObjects : null"
|
||||||
pelotonBtnClass="float-end"
|
pelotonBtnClass="float-end"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { useStore } from "vuex";
|
||||||
// Types
|
// Types
|
||||||
import { MotiveHistory } from "../../../../types";
|
import {Motive, MotiveHistory, MotiveWithParent} from "../../../../types";
|
||||||
|
|
||||||
//Utils
|
//Utils
|
||||||
import { localizeTranslatableString } from "../../utils/utils";
|
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||||
|
|
||||||
//Components
|
//Components
|
||||||
import PelotonComponent from "../PelotonComponent.vue";
|
import PelotonComponent from "../PelotonComponent.vue";
|
||||||
|
import { computed, ComputedRef } from "vue";
|
||||||
|
import {motiveLabelRecursive} from "../../utils/utils";
|
||||||
|
|
||||||
defineProps<{ motiveHistory: MotiveHistory }>();
|
const props = defineProps<{ motiveHistory: MotiveHistory }>();
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
const motive: ComputedRef<Motive | null> = computed(() =>
|
||||||
|
store.getters.getMotiveById(props.motiveHistory.motive.id),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
@@ -5,22 +5,50 @@
|
|||||||
<vue-multiselect
|
<vue-multiselect
|
||||||
name="selectMotive"
|
name="selectMotive"
|
||||||
id="selectMotive"
|
id="selectMotive"
|
||||||
label="label"
|
label="displayLabel"
|
||||||
:custom-label="customLabel"
|
:custom-label="(value: Motive) => localizeString(value.label)"
|
||||||
track-by="id"
|
track-by="id"
|
||||||
:open-direction="openDirection"
|
:open-direction="openDirection"
|
||||||
:multiple="false"
|
:multiple="false"
|
||||||
:searchable="true"
|
:searchable="true"
|
||||||
:placeholder="trans(CHILL_TICKET_TICKET_SET_MOTIVE_LABEL)"
|
:placeholder="trans(CHILL_TICKET_TICKET_SET_MOTIVE_LABEL)"
|
||||||
:select-label="trans(MULTISELECT_SELECT_LABEL)"
|
:options="flattenedMotives"
|
||||||
:deselect-label="trans(MULTISELECT_DESELECT_LABEL)"
|
|
||||||
:selected-label="trans(MULTISELECT_SELECTED_LABEL)"
|
|
||||||
:options="motives"
|
|
||||||
v-model="motive"
|
v-model="motive"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
@remove="(value: Motive) => $emit('remove', value)"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
#option="{
|
||||||
|
option,
|
||||||
|
}: {
|
||||||
|
option: Motive & {
|
||||||
|
isChild?: boolean;
|
||||||
|
isParent?: boolean;
|
||||||
|
level?: number;
|
||||||
|
breadcrumb: string[];
|
||||||
|
};
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:data-select="trans(MULTISELECT_SELECT_LABEL)"
|
||||||
|
:data-selected="trans(MULTISELECT_SELECTED_LABEL)"
|
||||||
|
:data-deselect="trans(MULTISELECT_DESELECT_LABEL)"
|
||||||
|
>
|
||||||
|
<span v-for="(crumb, idx) in option.breadcrumb" :key="idx">
|
||||||
|
<template v-if="idx < option.breadcrumb.length - 1">
|
||||||
|
<i>{{ crumb }}</i> >
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ crumb }}
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</vue-multiselect>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<peloton-component :stored-objects="motive?.storedObjects ?? null" />
|
<peloton-component
|
||||||
|
:stored-objects="motive ? motive.storedObjects : null"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,11 +56,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from "vue";
|
import { computed } from "vue";
|
||||||
import VueMultiselect from "vue-multiselect";
|
import VueMultiselect from "vue-multiselect";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
import { Motive } from "../../../../types";
|
import { Motive, MotiveWithParent } from "../../../../types";
|
||||||
|
|
||||||
// Translations
|
// Translations
|
||||||
import {
|
import {
|
||||||
@@ -46,10 +74,14 @@ import {
|
|||||||
// Component
|
// Component
|
||||||
import PelotonComponent from "../PelotonComponent.vue";
|
import PelotonComponent from "../PelotonComponent.vue";
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Object as () => Motive | undefined,
|
type: Object as () => MotiveWithParent | Motive | null,
|
||||||
default: undefined,
|
default: null,
|
||||||
},
|
},
|
||||||
motives: {
|
motives: {
|
||||||
type: Array as () => Motive[],
|
type: Array as () => Motive[],
|
||||||
@@ -59,34 +91,73 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: "bottom",
|
default: "bottom",
|
||||||
},
|
},
|
||||||
});
|
allowParentSelection: {
|
||||||
|
type: Boolean,
|
||||||
const emit =
|
default: false,
|
||||||
defineEmits<(e: "update:modelValue", value: Motive | undefined) => void>();
|
|
||||||
|
|
||||||
const motive = ref(props.modelValue);
|
|
||||||
|
|
||||||
watch(motive, (val) => {
|
|
||||||
emit("update:modelValue", val);
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(val) => {
|
|
||||||
motive.value = val;
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
function customLabel(motive: Motive) {
|
const emit = defineEmits<{
|
||||||
return motive?.label?.fr;
|
(e: "update:modelValue", value: Motive | null): void;
|
||||||
}
|
(e: "remove", value: Motive): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const motive = computed<Motive | null>({
|
||||||
|
get() {
|
||||||
|
return store.getters.getMotiveById(props.modelValue?.id);
|
||||||
|
},
|
||||||
|
set(value: Motive | null) {
|
||||||
|
emit("update:modelValue", value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const flattenedMotives = computed(() => {
|
||||||
|
const result: (Motive & {
|
||||||
|
isChild?: boolean;
|
||||||
|
isParent?: boolean;
|
||||||
|
level?: number;
|
||||||
|
displayLabel: string;
|
||||||
|
breadcrumb: string[];
|
||||||
|
})[] = [];
|
||||||
|
|
||||||
|
const processMotiveRecursively = (
|
||||||
|
motive: Motive,
|
||||||
|
isChild = false,
|
||||||
|
level = 0,
|
||||||
|
parentBreadcrumb: string[] = [],
|
||||||
|
) => {
|
||||||
|
const hasChildren = motive.children && motive.children.length > 0;
|
||||||
|
const displayLabel = localizeString(motive.label);
|
||||||
|
const breadcrumb = [...parentBreadcrumb, displayLabel];
|
||||||
|
|
||||||
|
if (props.allowParentSelection || !hasChildren) {
|
||||||
|
result.push({
|
||||||
|
...motive,
|
||||||
|
isChild,
|
||||||
|
isParent: hasChildren,
|
||||||
|
level,
|
||||||
|
breadcrumb,
|
||||||
|
displayLabel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChildren) {
|
||||||
|
motive.children.forEach((childMotive) => {
|
||||||
|
processMotiveRecursively(childMotive, true, level + 1, breadcrumb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
props.motives.forEach((parentMotive) => {
|
||||||
|
processMotiveRecursively(parentMotive, false, 0, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
#selectMotive {
|
|
||||||
margin-bottom: 1.5em;
|
|
||||||
}
|
|
||||||
// Supprime le padding de .form-control pour ce composant
|
|
||||||
.form-control {
|
.form-control {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
@@ -57,7 +57,7 @@
|
|||||||
}}</label>
|
}}</label>
|
||||||
<comment-editor-component
|
<comment-editor-component
|
||||||
v-model="ticketForm.content"
|
v-model="ticketForm.content"
|
||||||
:motive="ticketForm.motive"
|
:motive="ticketForm.motive ? ticketForm.motive : null"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
@@ -105,6 +105,7 @@ import ToggleComponent from "../../TicketList/components/ToggleComponent.vue";
|
|||||||
// Types
|
// Types
|
||||||
import {
|
import {
|
||||||
Motive,
|
Motive,
|
||||||
|
MotiveWithParent,
|
||||||
Ticket,
|
Ticket,
|
||||||
TicketEmergencyState,
|
TicketEmergencyState,
|
||||||
TicketInitForm,
|
TicketInitForm,
|
||||||
@@ -142,7 +143,7 @@ const store = useStore();
|
|||||||
const ticketForm = reactive({
|
const ticketForm = reactive({
|
||||||
content: "",
|
content: "",
|
||||||
addressees: props.ticket.currentAddressees as UserGroupOrUser[],
|
addressees: props.ticket.currentAddressees as UserGroupOrUser[],
|
||||||
motive: props.ticket.currentMotive as Motive | null,
|
motive: props.ticket.currentMotive as MotiveWithParent | null,
|
||||||
persons: props.ticket.currentPersons as Person[],
|
persons: props.ticket.currentPersons as Person[],
|
||||||
caller: props.ticket.caller as Person | null,
|
caller: props.ticket.caller as Person | null,
|
||||||
emergency: props.ticket.emergency as TicketEmergencyState,
|
emergency: props.ticket.emergency as TicketEmergencyState,
|
||||||
@@ -174,7 +175,7 @@ function submitForm() {
|
|||||||
}
|
}
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
ticketForm.content = "";
|
ticketForm.content = "";
|
||||||
ticketForm.motive = undefined;
|
ticketForm.motive = null;
|
||||||
ticketForm.persons = [];
|
ticketForm.persons = [];
|
||||||
ticketForm.caller = null;
|
ticketForm.caller = null;
|
||||||
ticketForm.emergency = props.ticket.emergency as TicketEmergencyState;
|
ticketForm.emergency = props.ticket.emergency as TicketEmergencyState;
|
||||||
|
@@ -21,6 +21,19 @@ export const moduleMotive: Module<State, RootState> = {
|
|||||||
getMotives(state) {
|
getMotives(state) {
|
||||||
return state.motives;
|
return state.motives;
|
||||||
},
|
},
|
||||||
|
getMotiveById: (state) => (motiveId: number) => {
|
||||||
|
const findInChildren = (motives: Motive[]): Motive | null => {
|
||||||
|
for (const motive of motives) {
|
||||||
|
if (motive.id === motiveId) return motive;
|
||||||
|
const found = motive.children?.length
|
||||||
|
? findInChildren(motive.children)
|
||||||
|
: null;
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return findInChildren(state.motives);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setMotives(state, motives) {
|
setMotives(state, motives) {
|
||||||
|
@@ -3,7 +3,7 @@ import { Person } from "../../../../../../../../ChillPersonBundle/Resources/publ
|
|||||||
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
import { ApiException } from "../../../../../../../../ChillMainBundle/Resources/public/types";
|
||||||
import { Module } from "vuex";
|
import { Module } from "vuex";
|
||||||
import { RootState } from "..";
|
import { RootState } from "..";
|
||||||
import { Ticket } from "../../../../types";
|
import { Ticket } from ".././../../../types";
|
||||||
import { Thirdparty } from "src/Bundle/ChillThirdPartyBundle/Resources/public/types";
|
import { Thirdparty } from "src/Bundle/ChillThirdPartyBundle/Resources/public/types";
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
import { Module } from "vuex";
|
import { Module } from "vuex";
|
||||||
import { RootState } from "..";
|
import { RootState } from "..";
|
||||||
|
|
||||||
import { TicketFilterParams, TicketSimple } from "../../../../types";
|
import { TicketFilterParams, TicketSimple } from "../../../../types";
|
||||||
import {
|
import {
|
||||||
makeFetch,
|
makeFetch,
|
||||||
@@ -76,7 +75,7 @@ export const moduleTicketList: Module<State, RootState> = {
|
|||||||
const filteredParams = Object.fromEntries(
|
const filteredParams = Object.fromEntries(
|
||||||
Object.entries(ticketFilterParams).filter(
|
Object.entries(ticketFilterParams).filter(
|
||||||
([, value]) =>
|
([, value]) =>
|
||||||
value !== undefined &&
|
value !== null &&
|
||||||
value !== null &&
|
value !== null &&
|
||||||
(value === true ||
|
(value === true ||
|
||||||
(typeof value === "number" && !isNaN(value)) ||
|
(typeof value === "number" && !isNaN(value)) ||
|
||||||
|
@@ -8,7 +8,8 @@ import {
|
|||||||
CHILL_TICKET_TICKET_BANNER_AND,
|
CHILL_TICKET_TICKET_BANNER_AND,
|
||||||
CHILL_TICKET_TICKET_BANNER_NO_MOTIVE,
|
CHILL_TICKET_TICKET_BANNER_NO_MOTIVE,
|
||||||
} from "translator";
|
} from "translator";
|
||||||
import { Ticket, TicketSimple } from "../../../types";
|
import {MotiveWithParent, Ticket, TicketSimple} from "../../../types";
|
||||||
|
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calcule et formate le temps écoulé depuis une date de création
|
* Calcule et formate le temps écoulé depuis une date de création
|
||||||
@@ -65,17 +66,6 @@ export function getSinceCreated(createdAt: string, currentTime: Date): string {
|
|||||||
return parts[0];
|
return parts[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function localizeTranslatableString(
|
|
||||||
translatableString: Record<string, string> | string,
|
|
||||||
): string {
|
|
||||||
// This would be implemented based on your localization logic
|
|
||||||
if (typeof translatableString === "string") {
|
|
||||||
return translatableString;
|
|
||||||
}
|
|
||||||
// Assuming it's an object with locale keys
|
|
||||||
return translatableString?.fr || translatableString?.en || "Unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatDateTime(
|
export function formatDateTime(
|
||||||
dateTime: string,
|
dateTime: string,
|
||||||
dateStyle: string,
|
dateStyle: string,
|
||||||
@@ -88,7 +78,23 @@ export function formatDateTime(
|
|||||||
}
|
}
|
||||||
export function getTicketTitle(ticket: Ticket | TicketSimple): string {
|
export function getTicketTitle(ticket: Ticket | TicketSimple): string {
|
||||||
if (ticket.currentMotive) {
|
if (ticket.currentMotive) {
|
||||||
return `#${ticket.id} ${localizeTranslatableString(ticket.currentMotive.label)}`;
|
return `#${ticket.id} ${localizeString(ticket.currentMotive.label)}`;
|
||||||
}
|
}
|
||||||
return `#${ticket.id} ${trans(CHILL_TICKET_TICKET_BANNER_NO_MOTIVE)}`;
|
return `#${ticket.id} ${trans(CHILL_TICKET_TICKET_BANNER_NO_MOTIVE)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function motiveLabelRecursive(motive: MotiveWithParent|null): string {
|
||||||
|
console.log('test', motive);
|
||||||
|
if (null === motive) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = [];
|
||||||
|
let m: MotiveWithParent|null = motive;
|
||||||
|
do {
|
||||||
|
str.push(localizeString(m.label));
|
||||||
|
m = m.parent;
|
||||||
|
} while (m !== null);
|
||||||
|
|
||||||
|
return str.reverse().join(" > ");
|
||||||
|
}
|
||||||
|
@@ -57,6 +57,8 @@
|
|||||||
<motive-selector
|
<motive-selector
|
||||||
v-model="selectedMotive"
|
v-model="selectedMotive"
|
||||||
:motives="availableMotives"
|
:motives="availableMotives"
|
||||||
|
:allow-parent-selection="true"
|
||||||
|
@remove="(motive) => removeMotive(motive)"
|
||||||
id="motiveSelector"
|
id="motiveSelector"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -299,14 +301,13 @@ const selectedAddressees = ref<UserGroupOrUser[]>([]);
|
|||||||
const selectedCreator = ref<User[]>([]);
|
const selectedCreator = ref<User[]>([]);
|
||||||
|
|
||||||
// Sélection des motifs
|
// Sélection des motifs
|
||||||
const selectedMotive = ref<Motive | undefined>();
|
const selectedMotive = ref<Motive | null>();
|
||||||
const selectedMotives = ref<Motive[]>([]);
|
const selectedMotives = ref<Motive[]>([]);
|
||||||
|
|
||||||
// Watchers pour les sélecteurs
|
// Watchers pour les sélecteurs
|
||||||
watch(selectedMotive, (newMotive) => {
|
watch(selectedMotive, (newMotive) => {
|
||||||
if (newMotive && !selectedMotives.value.find((m) => m.id === newMotive.id)) {
|
if (newMotive && !selectedMotives.value.find((m) => m.id === newMotive.id)) {
|
||||||
selectedMotives.value.push(newMotive);
|
selectedMotives.value.push(newMotive);
|
||||||
selectedMotive.value = undefined; // Reset pour permettre une nouvelle sélection
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -384,6 +385,9 @@ const removeMotive = (motiveToRemove: Motive): void => {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
selectedMotives.value.splice(index, 1);
|
selectedMotives.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
if (selectedMotive.value && motiveToRemove.id == selectedMotive.value.id) {
|
||||||
|
selectedMotive.value = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyFilters = (): void => {
|
const applyFilters = (): void => {
|
||||||
@@ -456,7 +460,7 @@ const resetFilters = (): void => {
|
|||||||
selectedCreator.value = [];
|
selectedCreator.value = [];
|
||||||
selectedAddressees.value = [];
|
selectedAddressees.value = [];
|
||||||
selectedMotives.value = [];
|
selectedMotives.value = [];
|
||||||
selectedMotive.value = undefined;
|
selectedMotive.value = null;
|
||||||
isClosedToggled.value = false;
|
isClosedToggled.value = false;
|
||||||
isEmergencyToggled.value = false;
|
isEmergencyToggled.value = false;
|
||||||
applyFilters();
|
applyFilters();
|
||||||
|
@@ -17,11 +17,7 @@
|
|||||||
: classColor?.off || 'bg-danger',
|
: classColor?.off || 'bg-danger',
|
||||||
]"
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: classColor
|
backgroundColor: classColor ? '' : !modelValue ? colorOff : colorOn,
|
||||||
? undefined
|
|
||||||
: !modelValue
|
|
||||||
? colorOff
|
|
||||||
: colorOn,
|
|
||||||
height: '28px',
|
height: '28px',
|
||||||
width: toggleWidth + 'px',
|
width: toggleWidth + 'px',
|
||||||
}"
|
}"
|
||||||
@@ -72,7 +68,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
onLabel: "ON",
|
onLabel: "ON",
|
||||||
offLabel: "OFF",
|
offLabel: "OFF",
|
||||||
disabled: false,
|
disabled: false,
|
||||||
id: undefined,
|
id: "",
|
||||||
colorOn: "#4caf50",
|
colorOn: "#4caf50",
|
||||||
colorOff: "#ccc",
|
colorOff: "#ccc",
|
||||||
classColor: () => ({
|
classColor: () => ({
|
||||||
|
@@ -0,0 +1,103 @@
|
|||||||
|
<?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\Motive;
|
||||||
|
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a Motive object into an array format, supporting different serialization groups
|
||||||
|
* to customize the output depending on the context.
|
||||||
|
*
|
||||||
|
* There are several serialization groups available:
|
||||||
|
* - 'read': Basic information about the motive.
|
||||||
|
* - 'read:extended': Includes additional details like stored objects and supplementary comments.
|
||||||
|
* - 'read:parent-to-children': Normalizes children recursively without exposing parent to avoid cycles.
|
||||||
|
* - 'read:children-to-parent': Normalizes parent recursively without exposing children to avoid cycles.
|
||||||
|
*/
|
||||||
|
final class MotiveNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||||
|
{
|
||||||
|
use NormalizerAwareTrait;
|
||||||
|
|
||||||
|
public const GROUP_PARENT_TO_CHILDREN = 'read:parent-to-children';
|
||||||
|
public const GROUP_CHILDREN_TO_PARENT = 'read:children-to-parent';
|
||||||
|
|
||||||
|
public function normalize($object, ?string $format = null, array $context = []): array
|
||||||
|
{
|
||||||
|
if (!$object instanceof Motive) {
|
||||||
|
throw new UnexpectedValueException('Expected instance of '.Motive::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
$groups = $context[AbstractNormalizer::GROUPS] ?? [];
|
||||||
|
if (is_string($groups)) {
|
||||||
|
$groups = [$groups];
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
if (in_array('read', $groups, true) || in_array('read:extended', $groups, true)) {
|
||||||
|
// Build base representation
|
||||||
|
$data = [
|
||||||
|
'type' => 'ticket_motive',
|
||||||
|
'id' => $object->getId(),
|
||||||
|
'label' => $object->getLabel(),
|
||||||
|
'active' => $object->isActive(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('read:extended', $groups, true)) {
|
||||||
|
$data['makeTicketEmergency'] = $object->getMakeTicketEmergency();
|
||||||
|
$data['supplementaryComments'] = $object->getSupplementaryComments();
|
||||||
|
// Normalize stored objects (delegated to their own normalizer when present)
|
||||||
|
$storedObjects = [];
|
||||||
|
foreach ($object->getStoredObjects() as $storedObject) {
|
||||||
|
$storedObjects[] = $this->normalizer->normalize($storedObject, $format, $context);
|
||||||
|
}
|
||||||
|
$data['storedObjects'] = $storedObjects;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array(self::GROUP_PARENT_TO_CHILDREN, $groups, true)) {
|
||||||
|
// Normalize children recursively (but we do not expose parent to avoid cycles)
|
||||||
|
$children = [];
|
||||||
|
foreach ($object->getChildren() as $child) {
|
||||||
|
$children[] = $this->normalizer->normalize($child, $format, $context);
|
||||||
|
}
|
||||||
|
$data['children'] = $children;
|
||||||
|
} elseif (in_array(self::GROUP_CHILDREN_TO_PARENT, $groups, true)) {
|
||||||
|
$data['parent'] = $this->normalizer->normalize($object->getParent(), $format, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
|
||||||
|
{
|
||||||
|
return $data instanceof Motive;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimization hint for the Serializer (available since Symfony 5.3+).
|
||||||
|
*
|
||||||
|
* @return array<class-string, bool>
|
||||||
|
*/
|
||||||
|
public function getSupportedTypes(?string $format): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Motive::class => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -51,7 +51,7 @@ final class TicketNormalizer implements NormalizerInterface, NormalizerAwareInte
|
|||||||
]),
|
]),
|
||||||
'currentAddressees' => $this->normalizer->normalize($object->getCurrentAddressee(), $format, ['groups' => 'read']),
|
'currentAddressees' => $this->normalizer->normalize($object->getCurrentAddressee(), $format, ['groups' => 'read']),
|
||||||
'currentInputs' => $this->normalizer->normalize($object->getCurrentInputs(), $format, ['groups' => 'read']),
|
'currentInputs' => $this->normalizer->normalize($object->getCurrentInputs(), $format, ['groups' => 'read']),
|
||||||
'currentMotive' => $this->normalizer->normalize($object->getMotive(), $format, ['groups' => 'read']),
|
'currentMotive' => $this->normalizer->normalize($object->getMotive(), $format, ['groups' => ['read', MotiveNormalizer::GROUP_CHILDREN_TO_PARENT]]),
|
||||||
'currentState' => $object->getState()?->value ?? 'open',
|
'currentState' => $object->getState()?->value ?? 'open',
|
||||||
'emergency' => $object->getEmergencyStatus()?->value ?? 'no',
|
'emergency' => $object->getEmergencyStatus()?->value ?? 'no',
|
||||||
'caller' => $this->normalizer->normalize($object->getCaller(), $format, ['groups' => 'read']),
|
'caller' => $this->normalizer->normalize($object->getCaller(), $format, ['groups' => 'read']),
|
||||||
@@ -167,12 +167,20 @@ final class TicketNormalizer implements NormalizerInterface, NormalizerAwareInte
|
|||||||
'event_type' => $data['event_type'],
|
'event_type' => $data['event_type'],
|
||||||
'at' => $this->normalizer->normalize($data['at'], $format, $context),
|
'at' => $this->normalizer->normalize($data['at'], $format, $context),
|
||||||
'by' => $this->normalizer->normalize($data['by'], $format, $context),
|
'by' => $this->normalizer->normalize($data['by'], $format, $context),
|
||||||
'data' => $this->normalizer->normalize($data['data'], $format, $context),
|
'data' => $this->normalizer->normalize($data['data'], $format, $this->contextByEventType($data['event_type'], $context)),
|
||||||
],
|
],
|
||||||
$events
|
$events
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function contextByEventType(string $eventType, array $context): array
|
||||||
|
{
|
||||||
|
return match($eventType) {
|
||||||
|
'set_motive' => array_merge($context, ['groups' => ['read', MotiveNormalizer::GROUP_CHILDREN_TO_PARENT]]),
|
||||||
|
default => $context,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private function addresseesStates(Ticket $ticket): array
|
private function addresseesStates(Ticket $ticket): array
|
||||||
{
|
{
|
||||||
/** @var array{string, array{added: list<AddresseeHistory>, removed: list<AddresseeHistory>}} $changes */
|
/** @var array{string, array{added: list<AddresseeHistory>, removed: list<AddresseeHistory>}} $changes */
|
||||||
|
@@ -0,0 +1,37 @@
|
|||||||
|
<?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 Version20250924124214 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add parent to motive';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_ticket.motive ADD parent_id INT DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE chill_ticket.motive ADD CONSTRAINT FK_DE298BF8727ACA70 FOREIGN KEY (parent_id) REFERENCES chill_ticket.motive (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('CREATE INDEX IDX_DE298BF8727ACA70 ON chill_ticket.motive (parent_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_ticket.motive DROP CONSTRAINT FK_DE298BF8727ACA70');
|
||||||
|
$this->addSql('DROP INDEX chill_ticket.IDX_DE298BF8727ACA70');
|
||||||
|
$this->addSql('ALTER TABLE chill_ticket.motive DROP parent_id');
|
||||||
|
}
|
||||||
|
}
|
63
src/Bundle/ChillTicketBundle/tests/Entity/MotiveTest.php
Normal file
63
src/Bundle/ChillTicketBundle/tests/Entity/MotiveTest.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\TicketBundle\Tests\Entity;
|
||||||
|
|
||||||
|
use Chill\TicketBundle\Entity\Motive;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @covers \Chill\TicketBundle\Entity\Motive::getWithDescendants
|
||||||
|
*/
|
||||||
|
final class MotiveTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetDescendantsOnLeafReturnsSelfOnly(): void
|
||||||
|
{
|
||||||
|
$leaf = new Motive();
|
||||||
|
$leaf->setLabel(['fr' => 'Feuille']);
|
||||||
|
|
||||||
|
$collection = $leaf->getDescendants();
|
||||||
|
|
||||||
|
self::assertCount(1, $collection);
|
||||||
|
self::assertSame($leaf, $collection->first());
|
||||||
|
self::assertContains($leaf, $collection->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetWithDescendantsReturnsSelfAndAllDescendants(): void
|
||||||
|
{
|
||||||
|
$parent = new Motive();
|
||||||
|
$parent->setLabel(['fr' => 'Parent']);
|
||||||
|
|
||||||
|
$childA = new Motive();
|
||||||
|
$childA->setLabel(['fr' => 'Enfant A']);
|
||||||
|
$childA->setParent($parent);
|
||||||
|
|
||||||
|
$childB = new Motive();
|
||||||
|
$childB->setLabel(['fr' => 'Enfant B']);
|
||||||
|
$childB->setParent($parent);
|
||||||
|
|
||||||
|
$grandChildA1 = new Motive();
|
||||||
|
$grandChildA1->setLabel(['fr' => 'Petit-enfant A1']);
|
||||||
|
$grandChildA1->setParent($childA);
|
||||||
|
|
||||||
|
$descendants = $parent->getDescendants();
|
||||||
|
$asArray = $descendants->toArray();
|
||||||
|
|
||||||
|
// It should contain the parent itself, both children and the grand child
|
||||||
|
self::assertCount(4, $descendants, 'Expected parent + 2 children + 1 grandchild');
|
||||||
|
self::assertContains($parent, $asArray);
|
||||||
|
self::assertContains($childA, $asArray);
|
||||||
|
self::assertContains($childB, $asArray);
|
||||||
|
self::assertContains($grandChildA1, $asArray);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,154 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Chill is a software for social workers
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\TicketBundle\Tests\Serializer\Normalizer;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\TicketBundle\Entity\EmergencyStatusEnum;
|
||||||
|
use Chill\TicketBundle\Entity\Motive;
|
||||||
|
use Chill\TicketBundle\Serializer\Normalizer\MotiveNormalizer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @covers \Chill\TicketBundle\Serializer\Normalizer\MotiveNormalizer
|
||||||
|
*/
|
||||||
|
final class MotiveNormalizerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testNormalizeReadBasic(): void
|
||||||
|
{
|
||||||
|
$motive = new Motive();
|
||||||
|
$motive->setLabel(['fr' => 'Logement', 'en' => 'Housing']);
|
||||||
|
// active is true by default
|
||||||
|
|
||||||
|
$normalizer = new MotiveNormalizer();
|
||||||
|
$normalizer->setNormalizer($this->buildDummyNormalizer());
|
||||||
|
|
||||||
|
$actual = $normalizer->normalize($motive, 'json', ['groups' => 'read']);
|
||||||
|
|
||||||
|
self::assertSame('ticket_motive', $actual['type']);
|
||||||
|
self::assertNull($actual['id']);
|
||||||
|
self::assertSame(['fr' => 'Logement', 'en' => 'Housing'], $actual['label']);
|
||||||
|
self::assertTrue($actual['active']);
|
||||||
|
// no extended fields here
|
||||||
|
self::assertArrayNotHasKey('makeTicketEmergency', $actual);
|
||||||
|
self::assertArrayNotHasKey('supplementaryComments', $actual);
|
||||||
|
self::assertArrayNotHasKey('storedObjects', $actual);
|
||||||
|
self::assertArrayNotHasKey('children', $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNormalizeExtended(): void
|
||||||
|
{
|
||||||
|
$motive = new Motive();
|
||||||
|
$motive->setLabel(['fr' => 'Financier']);
|
||||||
|
$motive->setMakeTicketEmergency(EmergencyStatusEnum::YES);
|
||||||
|
$motive->addSupplementaryComment(['label' => 'Justifier le revenu']);
|
||||||
|
$motive->addStoredObject(new StoredObject('pending'));
|
||||||
|
|
||||||
|
$normalizer = new MotiveNormalizer();
|
||||||
|
$normalizer->setNormalizer($this->buildDummyNormalizer());
|
||||||
|
|
||||||
|
$actual = $normalizer->normalize($motive, 'json', ['groups' => ['read', 'read:extended']]);
|
||||||
|
|
||||||
|
self::assertSame('ticket_motive', $actual['type']);
|
||||||
|
self::assertSame(['fr' => 'Financier'], $actual['label']);
|
||||||
|
self::assertSame(EmergencyStatusEnum::YES, $actual['makeTicketEmergency']);
|
||||||
|
self::assertSame([
|
||||||
|
['label' => 'Justifier le revenu'],
|
||||||
|
], $actual['supplementaryComments']);
|
||||||
|
self::assertSame([
|
||||||
|
['stored_object'],
|
||||||
|
], $actual['storedObjects']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNormalizeParentToChildren(): void
|
||||||
|
{
|
||||||
|
$parent = new Motive();
|
||||||
|
$parent->setLabel(['fr' => 'Parent']);
|
||||||
|
$child1 = new Motive();
|
||||||
|
$child1->setLabel(['fr' => 'Enfant 1']);
|
||||||
|
$child2 = new Motive();
|
||||||
|
$child2->setLabel(['fr' => 'Enfant 2']);
|
||||||
|
|
||||||
|
// build relation
|
||||||
|
$child1->setParent($parent);
|
||||||
|
$child2->setParent($parent);
|
||||||
|
|
||||||
|
$normalizer = new MotiveNormalizer();
|
||||||
|
$normalizer->setNormalizer($this->buildDummyNormalizer());
|
||||||
|
|
||||||
|
$actual = $normalizer->normalize($parent, 'json', ['groups' => [MotiveNormalizer::GROUP_PARENT_TO_CHILDREN]]);
|
||||||
|
|
||||||
|
// children must be normalized by the injected normalizer and parent not exposed
|
||||||
|
self::assertArrayHasKey('children', $actual);
|
||||||
|
self::assertSame([
|
||||||
|
['motive' => 'normalized'],
|
||||||
|
['motive' => 'normalized'],
|
||||||
|
], $actual['children']);
|
||||||
|
self::assertArrayNotHasKey('parent', $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNormalizeChildrenToParent(): void
|
||||||
|
{
|
||||||
|
$parent = new Motive();
|
||||||
|
$parent->setLabel(['fr' => 'Parent']);
|
||||||
|
$child = new Motive();
|
||||||
|
$child->setLabel(['fr' => 'Enfant']);
|
||||||
|
$child->setParent($parent);
|
||||||
|
|
||||||
|
$normalizer = new MotiveNormalizer();
|
||||||
|
$normalizer->setNormalizer($this->buildDummyNormalizer());
|
||||||
|
|
||||||
|
$actual = $normalizer->normalize($child, 'json', ['groups' => ['read', MotiveNormalizer::GROUP_CHILDREN_TO_PARENT]]);
|
||||||
|
|
||||||
|
// parent must be normalized by the injected normalizer and children not exposed
|
||||||
|
self::assertArrayHasKey('parent', $actual);
|
||||||
|
self::assertSame(['motive' => 'normalized'], $actual['parent']);
|
||||||
|
self::assertArrayNotHasKey('children', $actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSupportsAndSupportedTypes(): void
|
||||||
|
{
|
||||||
|
$motive = new Motive();
|
||||||
|
$normalizer = new MotiveNormalizer();
|
||||||
|
|
||||||
|
self::assertTrue($normalizer->supportsNormalization($motive, 'json'));
|
||||||
|
self::assertFalse($normalizer->supportsNormalization(new \stdClass(), 'json'));
|
||||||
|
|
||||||
|
$supported = $normalizer->getSupportedTypes('json');
|
||||||
|
self::assertArrayHasKey(Motive::class, $supported);
|
||||||
|
self::assertTrue($supported[Motive::class]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildDummyNormalizer(): NormalizerInterface
|
||||||
|
{
|
||||||
|
return new class () implements NormalizerInterface {
|
||||||
|
public function normalize($object, ?string $format = null, array $context = []): array
|
||||||
|
{
|
||||||
|
if ($object instanceof StoredObject) {
|
||||||
|
return ['stored_object'];
|
||||||
|
}
|
||||||
|
if ($object instanceof Motive) {
|
||||||
|
return ['motive' => 'normalized'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['normalized'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsNormalization($data, ?string $format = null): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user