Compare commits

..

9 Commits

Author SHA1 Message Date
6db36d5ab6 Remove ChillEventBundle and refactor framework.yaml configurations
This commit involves the deletion of ChillEventBundle from the bundles configuration. Additionally, test framework configurations are handled in a consolidated manner by moving assets configurations (json_manifest_path) from test/framework.yaml to framework.yaml. The obsolete test/framework.yaml has been deleted as it is no longer needed.
2024-06-04 21:55:53 +02:00
59f721934e Refactor export download script to use ES6 and webpack
The export download script was refactored to use ES6 syntax and webpack's modular system. This included separating out the download script into its own file for better organization, removing globally-scoped JavaScript, and adding the new download script as a webpack entry point. Also, the import method for the 'mime' library was adjusted to use ES6 syntax.
2024-06-04 21:53:32 +02:00
84ce8a93f3 Refactor ISOToDateTime to handle case when timezone's server is UTC
A condition is added to check if the timezone is set as '0000' (UTC timezone), if yes then a new Date is returned with the Date.UTC method. This ensures that the time returned correctly reflects the current timezone
2024-06-01 00:40:22 +02:00
ab5f2ffb65 add script to run php-cs-fixer 2024-06-01 00:35:36 +02:00
73bae8ccb9 fix indentation 2024-06-01 00:35:26 +02:00
dcfa569e3a Upgrade CKEditor and refactor configuration with use of typescript 2024-06-01 00:35:08 +02:00
4b07fe3622 Update address list import to latest compiled addresses
The import of the address list has been upgraded to use the latest version of the compiled addresses from Belgian-best-address. In the AddressReferenceBEFromBestAddress class, the RELEASE constant has been updated to point to the v1.1.1 tag.
2024-05-30 16:02:35 +02:00
48bf359d2e Restore feature to see chill assets style preview in dev environment
This commit introduces a new dev.assets.html.twig file and updates the chill.yaml file to add new paths for the SASS Assets tests.
2024-05-28 16:34:32 +02:00
60c7ea601c Update form builder parameter in SearchController
Changed the first argument in the `createNamedBuilder` method from `null` to an empty string. This adjustment ensures the form factory correctly creates the builder in the SearchController.
2024-05-28 16:00:03 +02:00
131 changed files with 706 additions and 6345 deletions

View File

@@ -0,0 +1,6 @@
kind: Feature
body: |
Upgrade import of address list to the last version of compiled addresses of belgian-best-address
time: 2024-05-30T16:00:03.440767606+02:00
custom:
Issue: ""

View File

@@ -0,0 +1,6 @@
kind: Feature
body: |
Upgrade CKEditor and refactor configuration with use of typescript
time: 2024-05-31T19:02:42.776662753+02:00
custom:
Issue: ""

View File

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

View File

@@ -119,7 +119,6 @@
"Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle", "Chill\\PersonBundle\\": "src/Bundle/ChillPersonBundle",
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle", "Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle", "Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
"Chill\\TicketBundle\\": "src/Bundle/ChillTicketBundle/src",
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle", "Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src", "Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src",
"Chill\\Utils\\Rector\\": "utils/rector/src" "Chill\\Utils\\Rector\\": "utils/rector/src"
@@ -127,9 +126,8 @@
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"App\\": "tests", "App\\": "tests/",
"Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", "Chill\\DocGeneratorBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
"Chill\\TicketBundle\\Tests\\": "src/Bundle/ChillTicketBundle/tests",
"Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests", "Chill\\WopiBundle\\Tests\\": "src/Bundle/ChillDocGeneratorBundle/tests",
"Chill\\Utils\\Rector\\Tests\\": "utils/rector/tests" "Chill\\Utils\\Rector\\Tests\\": "utils/rector/tests"
} }
@@ -151,6 +149,7 @@
"scripts": { "scripts": {
"auto-scripts": { "auto-scripts": {
"cache:clear": "symfony-cmd" "cache:clear": "symfony-cmd"
} },
"php-cs-fixer": "php-cs-fixer fix --config=./.php-cs-fixer.dist.php --show-progress=none"
} }
} }

View File

@@ -6,15 +6,16 @@
"@apidevtools/swagger-cli": "^4.0.4", "@apidevtools/swagger-cli": "^4.0.4",
"@babel/core": "^7.20.5", "@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2", "@babel/preset-env": "^7.20.2",
"@ckeditor/ckeditor5-build-classic": "^35.3.2", "@ckeditor/ckeditor5-build-classic": "^41.4.2",
"@ckeditor/ckeditor5-dev-utils": "^31.1.13", "@ckeditor/ckeditor5-dev-utils": "^40.2.0",
"@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13", "@ckeditor/ckeditor5-dev-webpack-plugin": "^31.1.13",
"@ckeditor/ckeditor5-markdown-gfm": "^35.3.2", "@ckeditor/ckeditor5-dev-translations": "^40.2.0",
"@ckeditor/ckeditor5-theme-lark": "^35.3.2", "@ckeditor/ckeditor5-markdown-gfm": "^41.4.2",
"@ckeditor/ckeditor5-vue": "^4.0.1", "@ckeditor/ckeditor5-theme-lark": "^41.4.2",
"@ckeditor/ckeditor5-vue": "^5.1.0",
"@symfony/webpack-encore": "^4.1.0", "@symfony/webpack-encore": "^4.1.0",
"@tsconfig/node14": "^1.0.1", "@tsconfig/node14": "^1.0.1",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"bindings": "^1.5.0", "bindings": "^1.5.0",
"bootstrap": "5.2.3", "bootstrap": "5.2.3",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
@@ -31,7 +32,7 @@
"select2-bootstrap-theme": "0.1.0-beta.10", "select2-bootstrap-theme": "0.1.0-beta.10",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"ts-loader": "^9.3.1", "ts-loader": "^9.3.1",
"typescript": "^4.7.2", "typescript": "^5.4.5",
"vue-loader": "^17.0.0", "vue-loader": "^17.0.0",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-cli": "^5.0.1" "webpack-cli": "^5.0.1"

View File

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

View File

@@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
class UserGroupApiController extends ApiController {}

View File

@@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\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)
;
}
}

View File

@@ -24,7 +24,6 @@ use Chill\MainBundle\Controller\LocationTypeController;
use Chill\MainBundle\Controller\NewsItemController; use Chill\MainBundle\Controller\NewsItemController;
use Chill\MainBundle\Controller\RegroupmentController; use Chill\MainBundle\Controller\RegroupmentController;
use Chill\MainBundle\Controller\UserController; use Chill\MainBundle\Controller\UserController;
use Chill\MainBundle\Controller\UserGroupApiController;
use Chill\MainBundle\Controller\UserJobApiController; use Chill\MainBundle\Controller\UserJobApiController;
use Chill\MainBundle\Controller\UserJobController; use Chill\MainBundle\Controller\UserJobController;
use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface; use Chill\MainBundle\DependencyInjection\Widget\Factory\WidgetFactoryInterface;
@@ -60,7 +59,6 @@ use Chill\MainBundle\Entity\LocationType;
use Chill\MainBundle\Entity\NewsItem; use Chill\MainBundle\Entity\NewsItem;
use Chill\MainBundle\Entity\Regroupment; use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Form\CenterType; use Chill\MainBundle\Form\CenterType;
use Chill\MainBundle\Form\CivilityType; use Chill\MainBundle\Form\CivilityType;
@@ -805,21 +803,6 @@ 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,
],
],
],
],
], ],
]); ]);
} }

View File

@@ -1,139 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\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;
}
}

View File

@@ -76,24 +76,6 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']); ->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. * Get type (mobile, landline, ...) for phone number.
*/ */

View File

@@ -59,6 +59,10 @@ export const ISOToDatetime = (str: string|null): Date|null => {
[hours, minutes, seconds] = time.split(':').map(s => parseInt(s)); [hours, minutes, seconds] = time.split(':').map(s => parseInt(s));
; ;
if ('0000' === timezone) {
return new Date(Date.UTC(year, month-1, date, hours, minutes, seconds));
}
return new Date(year, month-1, date, hours, minutes, seconds); return new Date(year, month-1, date, hours, minutes, seconds);
} }

View File

@@ -15,9 +15,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
var mime = require('mime') import mime from 'mime';
var download_report = (url, container) => { export const download_report = (url, container) => {
var download_text = container.dataset.downloadText, var download_text = container.dataset.downloadText,
alias = container.dataset.alias; alias = container.dataset.alias;
@@ -63,5 +63,3 @@ var download_report = (url, container) => {
.replaceChild(problem_text, container.firstChild); .replaceChild(problem_text, container.firstChild);
}); });
}; };
module.exports = download_report;

View File

@@ -39,23 +39,5 @@ ClassicEditor.defaultConfig = {
'redo' 'redo'
] ]
}, },
language: 'fr' language: 'fr',
}; };
let Fields = [];
Fields.push.apply(Fields, document.querySelectorAll('textarea[ckeditor]'));
// enable for custom fields
//Fields.push.apply(Fields, document.querySelectorAll('.cf-fields textarea'));
Fields.forEach(function(field) {
ClassicEditor
.create( field )
.then( editor => {
//console.log( 'CkEditor was initialized', editor );
})
.catch( error => {
console.error( error.stack );
})
;
});

View File

@@ -0,0 +1,15 @@
import ClassicEditor from "./editor_config";
const ckeditorFields: NodeListOf<HTMLTextAreaElement> = document.querySelectorAll('textarea[ckeditor]');
ckeditorFields.forEach((field: HTMLTextAreaElement): void => {
ClassicEditor
.create( field )
.then( editor => {
//console.log( 'CkEditor was initialized', editor );
})
.catch( error => {
console.error( error.stack );
})
;
});
//Fields.push.apply(Fields, document.querySelectorAll('.cf-fields textarea'));

View File

@@ -0,0 +1,16 @@
import {download_report} from "../../lib/download-report/download-report";
window.addEventListener("DOMContentLoaded", function(e) {
const export_generate_url = window.export_generate_url;
if (typeof export_generate_url === 'undefined') {
console.error('Alias not found!');
throw new Error('Alias not found!');
}
const query = window.location.search,
container = document.querySelector("#download_container")
;
download_report(export_generate_url + "?" + query.toString(), container);
});

View File

@@ -1,175 +1,164 @@
export interface DateTime { export interface DateTime {
datetime: string; datetime: string;
datetime8601: string; datetime8601: string
} }
export interface Civility { export interface Civility {
id: number; id: number;
// TODO // TODO
} }
export interface Job { export interface Job {
id: number; id: number;
type: "user_job"; type: "user_job";
label: { label: {
fr: string; // could have other key. How to do that in ts ? "fr": string; // could have other key. How to do that in ts ?
}; }
} }
export interface Center { export interface Center {
id: number; id: number;
type: "center"; type: "center";
name: string; name: string;
} }
export interface Scope { export interface Scope {
id: number; id: number;
type: "scope"; type: "scope";
name: { name: {
fr: string; "fr": string
}; }
} }
export interface User { export interface User {
type: "user"; type: "user";
id: number; id: number;
username: string; username: string;
text: string; text: string;
text_without_absence: string; text_without_absence: string;
email: string; email: string;
user_job: Job; user_job: Job;
label: string; label: string;
// todo: mainCenter; mainJob; etc.. // 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 { export interface UserAssociatedInterface {
type: "user"; type: "user";
id: number; id: number;
}
export type TranslatableString = {
fr?: string;
nl?: string;
}; };
export type TranslatableString = {
fr?: string;
nl?: string;
}
export interface Postcode { export interface Postcode {
id: number; id: number;
name: string; name: string;
code: string; code: string;
center: Point; center: Point;
} }
export type Point = { export type Point = {
type: "Point"; type: "Point";
coordinates: [lat: number, lon: number]; coordinates: [lat: number, lon: number];
};
export interface Country {
id: number;
name: TranslatableString;
code: string;
} }
export type AddressRefStatus = "match" | "to_review" | "reviewed"; export interface Country {
id: number;
name: TranslatableString;
code: string;
}
export type AddressRefStatus = 'match'|'to_review'|'reviewed';
export interface Address { export interface Address {
type: "address"; type: "address";
address_id: number; address_id: number;
text: string; text: string;
street: string; street: string;
streetNumber: string; streetNumber: string;
postcode: Postcode; postcode: Postcode;
country: Country; country: Country;
floor: string | null; floor: string | null;
corridor: string | null; corridor: string | null;
steps: string | null; steps: string | null;
flat: string | null; flat: string | null;
buildingName: string | null; buildingName: string | null;
distribution: string | null; distribution: string | null;
extra: string | null; extra: string | null;
confidential: boolean; confidential: boolean;
lines: string[]; lines: string[];
addressReference: AddressReference | null; addressReference: AddressReference | null;
validFrom: DateTime; validFrom: DateTime;
validTo: DateTime | null; validTo: DateTime | null;
point: Point | null; point: Point | null;
refStatus: AddressRefStatus; refStatus: AddressRefStatus;
isNoAddress: boolean; isNoAddress: boolean;
} }
export interface AddressWithPoint extends Address { export interface AddressWithPoint extends Address {
point: Point; point: Point
} }
export interface AddressReference { export interface AddressReference {
id: number; id: number;
createdAt: DateTime | null; createdAt: DateTime | null;
deletedAt: DateTime | null; deletedAt: DateTime | null;
municipalityCode: string; municipalityCode: string;
point: Point; point: Point;
postcode: Postcode; postcode: Postcode;
refId: string; refId: string;
source: string; source: string;
street: string; street: string;
streetNumber: string; streetNumber: string;
updatedAt: DateTime | null; updatedAt: DateTime | null;
} }
export interface SimpleGeographicalUnit { export interface SimpleGeographicalUnit {
id: number; id: number;
layerId: number; layerId: number;
unitName: string; unitName: string;
unitRefId: string; unitRefId: string;
} }
export interface GeographicalUnitLayer { export interface GeographicalUnitLayer {
id: number; id: number;
name: TranslatableString; name: TranslatableString;
refId: string; refId: string;
} }
export interface Location { export interface Location {
type: "location"; type: "location";
id: number; id: number;
active: boolean; active: boolean;
address: Address | null; address: Address | null;
availableForUsers: boolean; availableForUsers: boolean;
createdAt: DateTime | null; createdAt: DateTime | null;
createdBy: User | null; createdBy: User | null;
updatedAt: DateTime | null; updatedAt: DateTime | null;
updatedBy: User | null; updatedBy: User | null;
email: string | null; email: string | null
name: string; name: string;
phonenumber1: string | null; phonenumber1: string | null;
phonenumber2: string | null; phonenumber2: string | null;
locationType: LocationType; locationType: LocationType;
} }
export interface LocationAssociated { export interface LocationAssociated {
type: "location"; type: "location";
id: number; id: number;
} }
export interface LocationType { export interface LocationType {
type: "location-type"; type: "location-type";
id: number; id: number;
active: boolean; active: boolean;
addressRequired: "optional" | "required"; addressRequired: "optional" | "required";
availableForUsers: boolean; availableForUsers: boolean;
editableByUsers: boolean; editableByUsers: boolean;
contactData: "optional" | "required"; contactData: "optional" | "required";
title: TranslatableString; title: TranslatableString;
} }
export interface NewsItemType { export interface NewsItemType {

View File

@@ -1,24 +1,13 @@
<template> <template>
<span class="chill-entity entity-user"> <span class="chill-entity entity-user">
{{ user.label }} {{ user.label }}
<span class="user-job" v-if="user.user_job !== null" <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>
>({{ user.user_job.label.fr }})</span </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> </template>
<script> <script>
export default { export default {
name: "UserRenderBoxBadge", name: "UserRenderBoxBadge",
props: ["user"], props: ['user'],
}; }
</script> </script>

View File

@@ -0,0 +1,306 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title %}
SASS Assets Catalogue
{% endblock %}
{% block css %}
<style media="screen">
h2 { margin: 1.5em 0; }
div.flex-table ul, div.flex-bloc ul { padding-left: 1rem; }
div.flex-table div.item-bloc div.item-row div.item-col:first-child { flex-basis: 20%; }
div.flex-bloc div.item-bloc { flex-basis: 50%; }
</style>
{% endblock %}
{% block content %}
<div class="col-md-10">
<h1 class="display-4">{{ block('title') }}</h1>
<b>Voir aussi: </b>
<a href="{{ path('sass_assets_test1') }}">Test 1</a> |
<a href="{{ path('sass_assets_test2') }}">Test 2</a>
<h2>Flex-table et flex-bloc</h2>
<p>Base d'un placement flex alternatif à l'usage des tables.
Flex-table et flex-bloc utilisent la même structure html (seul la root class change).
Le placement est responsive.
La bordure utilise box-shadow pour simuler border-collapse (table).
</p>
<p>Une classe separator peut être appliquée sur item-row</p>
<xmp>
<div class="flex-table">
<div class="item-bloc">
<div class="item-row">
<div class="item-col"></div>
<div class="item-col"></div>
</div>
<div class="item-row separator">
<div class="item-col"></div>
<div class="item-col"></div>
</div>
</div>
<div class="item-bloc">
<div class="item-row">
<div class="item-col"></div>
<div class="item-col"></div>
</div>
<div class="item-row">
<div class="item-col"></div>
<div class="item-col"></div>
</div>
</div>
</div>
</xmp>
<h3>Flex-table</h3>
<p>On fixe manuellement la largeur de la première colonne :
<pre>div.flex-table div.item-bloc div.item-row div.item-col:first-child { flex-basis: 20%; }</pre>
</p>
<div class="flex-table debug">
<div class="item-bloc">
<div class="item-row">
<div class="item-col">Title row1</div>
<div class="item-col">
<ul class="list-content">
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
</ul>
</div>
</div>
<div class="item-row separator">
<div class="item-col">Title row2</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
<div class="item-row">
<div class="item-col">Title row3</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
</div>
<div class="item-bloc">
<div class="item-row">
<div class="item-col">Title row1</div>
<div class="item-col">
<ul class="list-content">
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
</ul>
</div>
</div>
<div class="item-row">
<div class="item-col">Title row2</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
</div>
</div>
<h3>Flex-bloc</h3>
<p>On fixe manuellement la largeur des blocs :
<pre>div.flex-bloc div.item-bloc { flex-basis: 50%; }</pre>
</p>
<div class="flex-bloc debug">
<div class="item-bloc">
<div class="item-row">
<div class="item-col">Title row1</div>
<div class="item-col">
<ul class="list-content">
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
</ul>
</div>
</div>
<div class="item-row separator">
<div class="item-col">Title row2</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
<div class="item-row">
<div class="item-col">Title row3</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
</div>
<div class="item-bloc">
<div class="item-row">
<div class="item-col">Title row1</div>
<div class="item-col">
<ul class="list-content">
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
<li>Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</li>
</ul>
</div>
</div>
<div class="item-row">
<div class="item-col">Title row2</div>
<div class="item-col">Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac viverra et a dui.</div>
</div>
</div>
</div>
<h2>Wrap-list</h2>
<p>Une liste inline qui s'aligne, puis glisse sous son titre.</p>
<div class="wrap-list debug">
<div class="wl-row">
<div class="wl-col title">Usagers concernés</div>
<div class="wl-col list">
<p class="wl-item"><a href="#">Gaston Bah</a></p>
<p class="wl-item"><a href="#">Alain Bah</a></p>
<p class="wl-item"><a href="#">Adèle Gaillot</a></p>
<p class="wl-item"><a href="#">Corentine Bah</a></p>
<p class="wl-item"><a href="#">Justin Bah</a></p>
<p class="wl-item"><a href="#">Michel Sardou</a></p>
<p class="wl-item"><a href="#">Carine Rousseau</a></p>
<p class="wl-item"><a href="#">Mohamed Martin</a></p>
</div>
</div>
<div class="wl-row">
<div class="wl-col title">Problématiques sociales</div>
<div class="wl-col list">
<p class="wl-item"><a href="#">Gaston Bah</a></p>
<p class="wl-item"><a href="#">Alain Bah</a></p>
<p class="wl-item"><a href="#">Adèle Gaillot</a></p>
</div>
</div>
</div>
<xmp>
<div class="wrap-list">
<div class="wl-row">
<div class="wl-col title">title</div>
<div class="wl-col list">
<p class="wl-item">item</p>
<p class="wl-item">item</p>
...
</div>
</div>
...
</div>
</xmp>
<h2>Wrap-header</h2>
<p>Réglage d'une zone de titre sur 2 lignes.</p>
<div class="wrap-header debug">
<div class="wh-row">
<div class="wh-col">
<span class="h3"><b>Title</b></span>
<span class="badge rounded-pill bg-danger">badge</span>
</div>
<div class="wh-col">
<span class="badge rounded-pill bg-primary">badge</span>
</div>
</div>
<div class="wh-row">
<div class="wh-col">from startdate to enddate</div>
<div class="wh-col">text</div>
</div>
</div>
<xmp>
<div class="wrap-header">
<div class="wh-row">
<div class="wh-col">line1 left</div>
<div class="wh-col">line1 right</div>
</div>
<div class="wh-row">
<div class="wh-col">line2 left</div>
<div class="wh-col">line2 right</div>
</div>
</div>
</xmp>
<h2>Float-button top</h2>
<p>Une zone de bouton flotte à droite d'un contenu. On peut voir en faisant varier la largeur que celui-ci vient s'adapter harmonieusement autour des boutons.</p>
<div class="float-button top debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac. Proin fermentum mauris quam, ut suscipit nisl auctor at. Ut vestibulum ligula eget ex congue, efficitur interdum ipsum tincidunt. Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
</div>
<xmp>
<div class="float-button top">
<div class="box">
<div class="action">
floating button
</div>
content ...
</div>
</div>
</xmp>
<h2>Float-button bottom</h2>
<p>Avec la même structure, on accroche la zone de bouton en bas, toujours à droite. Voir <a href="https://css-tricks.com/float-an-element-to-the-bottom-corner/">source</a>. </p>
<div class="float-button bottom debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac. Proin fermentum mauris quam, ut suscipit nisl auctor at. Ut vestibulum ligula eget ex congue, efficitur interdum ipsum tincidunt. Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
</div>
<xmp>
<div class="float-button bottom">
<div class="box">
<div class="action">
floating button
</div>
content ...
</div>
</div>
</xmp>
<h1>Buttons</h1>
<ul class="record_actions">
<li><a href="#" class="btn btn-submit">submit</a></li>
<li><a href="#" class="btn btn-save">save</a></li>
<li><a href="#" class="btn btn-create">create</a></li>
<li><a href="#" class="btn btn-new">new</a></li>
<li><a href="#" class="btn btn-duplicate">duplicate</a></li>
<li><a href="#" class="btn btn-not-duplicate">not-duplicate</a></li>
<li><a href="#" class="btn btn-reset">reset</a></li>
<li><a href="#" class="btn btn-delete">delete</a></li>
<li><a href="#" class="btn btn-danger">danger</a></li>
<li><a href="#" class="btn btn-remove">remove</a></li>
<li><a href="#" class="btn btn-unlink">unlink</a></li>
<li><a href="#" class="btn btn-action">action</a></li>
<li><a href="#" class="btn btn-edit">edit</a></li>
<li><a href="#" class="btn btn-update">update</a></li>
<li><a href="#" class="btn btn-show">show</a></li>
<li><a href="#" class="btn btn-view">view</a></li>
<li><a href="#" class="btn btn-misc">misc</a></li>
<li><a href="#" class="btn btn-cancel">cancel</a></li>
<li><a href="#" class="btn btn-choose">choose</a></li>
<li><a href="#" class="btn btn-notify">notify</a></li>
<li><a href="#" class="btn btn-tpchild">tpchild</a></li>
<li><a href="#" class="btn btn-chill-beige">my button</a></li>
</ul>
<h2>Variants of <pre>record_actions</pre></h2>
<h3><pre>small</pre></h3>
<ul class="record_actions small">
<li><a href="#" class="btn btn-create"></a></li>
</ul>
<h3><pre>inline</pre></h3>
<div>
This is inline and small
<ul class="record_actions small inline">
<li><a href="#" class="btn btn-create"></a></li>
</ul>
</div>
<xmp><a class="btn btn-submit">Text</a></xmp>
Toutes les classes btn-* de bootstrap sont fonctionnelles
</div>
{% endblock %}

View File

@@ -0,0 +1,84 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title %}
SASS Assets Tests - page 1
{% endblock %}
{% block css %}
<style media="screen">
</style>
{% endblock %}
{% block content %}
<div class="col-md-8">
<h1>CSS Tests - page 1 : float-button</h1>
<h2>1) avec des li</h2>
<div class="float-button bottom debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
<ul class="list-content fa-ul">
<li><i class="fa fa-li fa-file-text-o"></i>Sed efficitur magna vel massa efficitur venenatis. Sed odio massa, scelerisque sit amet mauris eu, tristique dictum arcu. Sed posuere, elit eget cursus rhoncus, arcu ligula blandit nisi, in vulputate eros massa non risus.</li>
<li><i class="fa fa-li fa-map-marker"></i>
<div class="chill-entity entity-address my-3" data-v-8b2170ec="">
<div class="address multiline" data-v-8b2170ec="">
<p class="street" data-v-8b2170ec="">97, chemin Franck Julien, </p>
<p class="postcode" data-v-8b2170ec="">1000 Bruxelles</p>
<p class="country" data-v-8b2170ec="">Belgique</p>
</div>
<div class="address-more" data-v-8b2170ec="">
<div data-v-8b2170ec="">
<span class="corridor" data-v-8b2170ec="">
<b data-v-8b2170ec="">Couloir</b>: 3
</span>
</div>
</div>
</div>
</li>
<li><i class="fa fa-li fa-mobile"></i><a href="tel: +33 8 27 17 12 19">+33 8 27 17 12 19</a></li>
<li><i class="fa fa-li fa-envelope-o"></i><a href="mailto: gusikowski.yesenia@hotmail.com">gusikowski.yesenia@hotmail.com</a></li>
</ul>
</div>
</div>
<h2>2) avec des p</h2>
<div class="float-button bottom debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
<p>Voir <a href="https://css-tricks.com/float-an-element-to-the-bottom-corner/">trick</a>.</p>
<p>Sed efficitur magna vel massa efficitur venenatis. Sed odio massa, scelerisque sit amet mauris eu, tristique dictum arcu. Sed posuere, elit eget cursus rhoncus, arcu ligula blandit nisi, in vulputate eros massa non risus. Proin lacinia, sapien in pharetra ultricies, justo urna fermentum lectus, non tempor ipsum leo a ante. Aenean porta, ipsum in fringilla hendrerit, nisi justo vestibulum ex, non lacinia risus felis vitae diam. Curabitur sem eros, consectetur a auctor vel, facilisis sit amet sem.</p>
<p>Aenean finibus a nisl a scelerisque. Donec bibendum facilisis odio id euismod. Pellentesque luctus justo ligula, eget dictum ligula ultrices quis. Pellentesque at nunc est. Aenean luctus, tortor in lacinia porta, ex nisl dignissim magna, non vehicula elit risus at elit. Suspendisse in velit non augue egestas laoreet. Etiam blandit lacus at semper aliquam. Integer leo nunc, condimentum sagittis accumsan sit amet, consectetur vel massa. Aenean convallis nibh vel augue ullamcorper tempus. Integer eu laoreet sapien.</p>
</div>
</div>
<h2>3) avec des div</h2>
<div class="float-button bottom debug">
<div class="box">
<div class="action">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
<div>Voir <a href="https://css-tricks.com/float-an-element-to-the-bottom-corner/">trick</a>.</div>
<div>Sed efficitur magna vel massa efficitur venenatis. Sed odio massa, scelerisque sit amet mauris eu, tristique dictum arcu. Sed posuere, elit eget cursus rhoncus, arcu ligula blandit nisi, in vulputate eros massa non risus. Proin lacinia, sapien in pharetra ultricies, justo urna fermentum lectus, non tempor ipsum leo a ante. Aenean porta, ipsum in fringilla hendrerit, nisi justo vestibulum ex, non lacinia risus felis vitae diam.
<a href="#">Curabitur</a> sem eros, consectetur a auctor vel, facilisis sit amet sem.</div>
<div>Aenean finibus a nisl a scelerisque. Donec bibendum facilisis odio id euismod. Pellentesque luctus justo ligula, eget dictum ligula ultrices quis. Pellentesque at nunc est. Aenean luctus, tortor in lacinia porta, ex nisl dignissim magna, non vehicula elit risus at elit. Suspendisse in velit non augue egestas laoreet. Etiam blandit lacus at semper aliquam. Integer leo nunc, condimentum sagittis accumsan sit amet, consectetur vel massa. Aenean convallis nibh vel augue ullamcorper tempus. Integer eu laoreet sapien.</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,78 @@
{% extends '@ChillMain/layout.html.twig' %}
{% block title %}
SASS Assets Tests - page 2
{% endblock %}
{% block css %}
{% endblock %}
{% block content %}
<div class="col-md-10">
<h1>CSS Tests - page 2: grid layout</h1>
<h2>1) mgrid 1-2: avec grid-column et grid-row</h2>
<div class="mgrid debug">
<div class="area1">
Nam rhoncus tristique ligula, tincidunt iaculis augue tincidunt ac. Proin fermentum mauris quam, ut suscipit nisl auctor at. Ut vestibulum ligula eget ex congue, efficitur interdum ipsum tincidunt. Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="area2">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
</div>
<h2>2) lgrid 3-4: avec grid-template-areas</h2>
<div class="lgrid debug">
<div class="area3">
<i>La zone qu'on crée avec les noms doit être rectangulaires. Actuellement, il n'existe pas de méthode pour créer une zone avec une forme de L (bien que la spécification indique qu'une prochaine version pourrait couvrir cette fonctionnalité).
[...] Si des zones ne sont pas rectangulaires, cela sera également considéré comme invalide.</i>
Voir sur MDN: <a target="_blank" href="https://developer.mozilla.org/fr/docs/Web/CSS/CSS_Grid_Layout/Grid_Template_Areas#occuper_plusieurs_cellules">Définir des zones sur une grille</a>
</div>
<div class="area4">
<ul class="record_actions">
<li><button type="button" name="button">Annuler</button></li>
<li><button type="button" name="button">Voir</button></li>
<li><button type="button" name="button">Enregistrer</button></li>
</ul>
</div>
</div>
<h2>3) cgrid 5-6-7-8: avec masonry</h2>
<p>Expérimental: dans FF <i>about:config</i>, il faut mettre <i>layout.css.grid-template-masonry-value.enabled = true</i></p>
<div class="cgrid debug">
<div class="item">
1 Integer id sapien et nibh tristique viverra et a dui. Ut blandit pharetra consectetur. Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="item">
2 Sed scelerisque eget purus at tempus. Etiam sit amet tellus et eros semper tempor. Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="item">
3 Curabitur suscipit pulvinar enim at lobortis. Ut nisl augue, cursus vel hendrerit sed, posuere vel sapien. Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="item">
4 Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan. In vitae tristique leo, a fringilla nisl.
</div>
<div class="item">
5 Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim a accumsan.
</div>
<div class="item">
6 Proin hendrerit arcu velit, eu ultrices dui interdum eget. Vestibulum consectetur sodales enim.
</div>
<div class="item">
7 Proin hendrerit arcu velit, eu ultrices dui interdum eget.
</div>
<div class="item">
8 Eu ultrices dui interdum eget.
</div>
</div>
</div>
{% endblock %}

View File

@@ -22,15 +22,13 @@
{% block js %} {% block js %}
<script type="text/javascript"> <script type="text/javascript">
window.addEventListener("DOMContentLoaded", function(e) { window.export_generate_url = "{{ path('chill_main_export_generate', { 'alias' : alias } ) }}";
var url = "{{ path('chill_main_export_generate', { 'alias' : alias } ) }}",
query = window.location.search,
container = document.querySelector("#download_container")
;
chill.download_report(url+query, container);
});
</script> </script>
{{ encore_entry_link_tags('page_download_exports') }}
{% endblock %}
{% block css %}
{{ encore_entry_script_tags('page_download_exports') }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

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

View File

@@ -18,7 +18,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
class AddressReferenceBEFromBestAddress class AddressReferenceBEFromBestAddress
{ {
private const RELEASE = 'https://gitea.champs-libres.be/api/v1/repos/Chill-project/belgian-bestaddresses-transform/releases/tags/v1.0.0'; private const RELEASE = 'https://gitea.champs-libres.be/api/v1/repos/Chill-project/belgian-bestaddresses-transform/releases/tags/v1.1.1';
public function __construct(private readonly HttpClientInterface $client, private readonly AddressReferenceBaseImporter $baseImporter, private readonly AddressToReferenceMatcher $addressToReferenceMatcher) {} public function __construct(private readonly HttpClientInterface $client, private readonly AddressReferenceBaseImporter $baseImporter, private readonly AddressToReferenceMatcher $addressToReferenceMatcher) {}

View File

@@ -12,7 +12,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Tests\Phonenumber; namespace Chill\MainBundle\Tests\Phonenumber;
use Chill\MainBundle\Phonenumber\PhonenumberHelper; use Chill\MainBundle\Phonenumber\PhonenumberHelper;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberUtil; use libphonenumber\PhoneNumberUtil;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@@ -53,36 +52,12 @@ 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 * @dataProvider formatPhonenumbers
*/ */
public function testFormatPhonenumbers(string $defaultCarrierCode, string $phoneNumber, string $expected) public function testFormatPhonenumbers(string $defaultCarrierCode, string $phoneNumber, string $expected)
{ {
$util = PhoneNumberUtil::getInstance(); $util = PhoneNumberUtil::getInstance();
$subject = new PhonenumberHelper( $subject = new PhonenumberHelper(
new ArrayAdapter(), new ArrayAdapter(),
new ParameterBag([ new ParameterBag([
@@ -95,24 +70,4 @@ final class PhonenumberHelperTest extends KernelTestCase
$this->assertEquals($expected, $subject->format($util->parse($phoneNumber))); $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));
}
} }

View File

@@ -1,91 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\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();
}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\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;
}
}

View File

@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\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();
}
}
}
}

View File

@@ -29,42 +29,6 @@ components:
type: string type: string
text: text:
type: string 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: Center:
type: object type: object
properties: properties:
@@ -944,19 +908,3 @@ paths:
$ref: '#/components/schemas/NewsItem' $ref: '#/components/schemas/NewsItem'
403: 403:
description: "Unauthorized" 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"

View File

@@ -1,10 +1,10 @@
const CKEditorWebpackPlugin = require( '@ckeditor/ckeditor5-dev-webpack-plugin' );
const { styles } = require( '@ckeditor/ckeditor5-dev-utils' ); const { styles } = require( '@ckeditor/ckeditor5-dev-utils' );
const {CKEditorTranslationsPlugin} = require("@ckeditor/ckeditor5-dev-translations");
buildCKEditor = function(encore) buildCKEditor = function(encore)
{ {
encore encore
.addPlugin( new CKEditorWebpackPlugin( { .addPlugin( new CKEditorTranslationsPlugin( {
language: 'fr', language: 'fr',
addMainLanguageTranslationsToAllAssets: true, addMainLanguageTranslationsToAllAssets: true,
verbose: !encore.isProduction(), verbose: !encore.isProduction(),
@@ -52,12 +52,14 @@ module.exports = function(encore, entries)
Tabs: __dirname + '/Resources/public/lib/tabs' Tabs: __dirname + '/Resources/public/lib/tabs'
}); });
// Page entrypoints // Page entrypoints
encore.addEntry('page_login', __dirname + '/Resources/public/page/login/index.js'); encore.addEntry('page_login', __dirname + '/Resources/public/page/login/index.js');
encore.addEntry('page_location', __dirname + '/Resources/public/page/location/index.js'); encore.addEntry('page_location', __dirname + '/Resources/public/page/location/index.js');
encore.addEntry('page_workflow_show', __dirname + '/Resources/public/page/workflow-show/index.js'); encore.addEntry('page_workflow_show', __dirname + '/Resources/public/page/workflow-show/index.js');
encore.addEntry('page_homepage_widget', __dirname + '/Resources/public/page/homepage_widget/index.js'); encore.addEntry('page_homepage_widget', __dirname + '/Resources/public/page/homepage_widget/index.js');
encore.addEntry('page_export', __dirname + '/Resources/public/page/export/index.js'); encore.addEntry('page_export', __dirname + '/Resources/public/page/export/index.js');
encore.addEntry('page_download_exports', __dirname + '/Resources/public/page/export/download-export.js');
buildCKEditor(encore); buildCKEditor(encore);
@@ -65,7 +67,7 @@ module.exports = function(encore, entries)
encore.addEntry('mod_collection', __dirname + '/Resources/public/module/collection/index.ts'); encore.addEntry('mod_collection', __dirname + '/Resources/public/module/collection/index.ts');
encore.addEntry('mod_forkawesome', __dirname + '/Resources/public/module/forkawesome/index.js'); encore.addEntry('mod_forkawesome', __dirname + '/Resources/public/module/forkawesome/index.js');
encore.addEntry('mod_bootstrap', __dirname + '/Resources/public/module/bootstrap/index.js'); encore.addEntry('mod_bootstrap', __dirname + '/Resources/public/module/bootstrap/index.js');
encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index.js'); encore.addEntry('mod_ckeditor5', __dirname + '/Resources/public/module/ckeditor5/index');
encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js'); encore.addEntry('mod_disablebuttons', __dirname + '/Resources/public/module/disable-buttons/index.js');
encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js'); encore.addEntry('mod_blur', __dirname + '/Resources/public/module/blur/index.js');
encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js'); encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js');

View File

@@ -3,9 +3,6 @@ services:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
Chill\MainBundle\Validation\:
resource: '../../Validation'
chill_main.validator_user_circle_consistency: chill_main.validator_user_circle_consistency:
class: Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistencyValidator class: Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistencyValidator
arguments: arguments:

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20240416145021 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create tables for user_group';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_main_user_group_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_main_user_group (id INT NOT NULL, label JSON DEFAULT \'[]\' NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE chill_main_user_group_user (usergroup_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(usergroup_id, user_id))');
$this->addSql('CREATE INDEX IDX_1E07F044D2112630 ON chill_main_user_group_user (usergroup_id)');
$this->addSql('CREATE INDEX IDX_1E07F044A76ED395 ON chill_main_user_group_user (user_id)');
$this->addSql('ALTER TABLE chill_main_user_group_user ADD CONSTRAINT FK_1E07F044D2112630 FOREIGN KEY (usergroup_id) REFERENCES chill_main_user_group (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_user_group_user ADD CONSTRAINT FK_1E07F044A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_main_user_group_id_seq');
$this->addSql('DROP TABLE chill_main_user_group_user');
$this->addSql('DROP TABLE chill_main_user_group');
}
}

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\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');
}
}

View File

@@ -28,11 +28,7 @@ use Symfony\Component\Form\FormBuilderInterface;
final readonly class GeographicalUnitStatAggregator implements AggregatorInterface final readonly class GeographicalUnitStatAggregator implements AggregatorInterface
{ {
public function __construct( public function __construct(private GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository, private TranslatableStringHelperInterface $translatableStringHelper, private RollingDateConverterInterface $rollingDateConverter) {}
private GeographicalUnitLayerRepositoryInterface $geographicalUnitLayerRepository,
private TranslatableStringHelperInterface $translatableStringHelper,
private RollingDateConverterInterface $rollingDateConverter
) {}
public function addRole(): ?string public function addRole(): ?string
{ {

View File

@@ -21,8 +21,6 @@ use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException; use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberFormat;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
final readonly class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface final readonly class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
@@ -300,27 +298,4 @@ final readonly class PersonACLAwareRepository implements PersonACLAwareRepositor
\array_map(static fn (Center $c) => $c->getId(), $authorizedCenters) \array_map(static fn (Center $c) => $c->getId(), $authorizedCenters)
); );
} }
public function findByPhone(PhoneNumber $phoneNumber, int $start = 0, int $limit = 20): array
{
$authorizedCenters = $this->authorizationHelper
->getReachableCenters($this->security->getUser(), PersonVoter::SEE);
if ([] === $authorizedCenters) {
return [];
}
$util = \libphonenumber\PhoneNumberUtil::getInstance();
return $this->em->createQuery(
'SELECT p FROM '.Person::class.' p LEFT JOIN p.otherPhoneNumbers opn JOIN p.centerCurrent pcc '.
'WHERE (p.phonenumber LIKE :phone OR p.mobilenumber LIKE :phone OR opn.phonenumber LIKE :phone) '.
'AND pcc.center IN (:centers)'
)
->setMaxResults($limit)
->setFirstResult($start)
->setParameter('phone', $util->format($phoneNumber, PhoneNumberFormat::E164))
->setParameter('centers', $authorizedCenters)
->getResult();
}
} }

View File

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

View File

@@ -12,12 +12,10 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository; namespace Chill\PersonBundle\Repository;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonPhone;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
use libphonenumber\PhoneNumber;
class PersonRepository implements ObjectRepository class PersonRepository implements ObjectRepository
{ {
@@ -31,8 +29,6 @@ class PersonRepository implements ObjectRepository
/** /**
* @throws \Doctrine\ORM\NoResultException * @throws \Doctrine\ORM\NoResultException
* @throws \Doctrine\ORM\NonUniqueResultException * @throws \Doctrine\ORM\NonUniqueResultException
*
* @deprecated
*/ */
public function countByPhone( public function countByPhone(
string $phonenumber, string $phonenumber,
@@ -75,8 +71,6 @@ class PersonRepository implements ObjectRepository
/** /**
* @throws \Exception * @throws \Exception
*
* @deprecated Use @see{self::findByPhoneNumber} or use a dedicated method in PersonACLAwareRepository
*/ */
public function findByPhone( public function findByPhone(
string $phonenumber, string $phonenumber,
@@ -97,25 +91,6 @@ class PersonRepository implements ObjectRepository
return $qb->getQuery()->getResult(); 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) public function findOneBy(array $criteria)
{ {
return $this->repository->findOneBy($criteria); return $this->repository->findOneBy($criteria);
@@ -134,20 +109,6 @@ 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 * @throws \Exception
*/ */

View File

@@ -52,7 +52,7 @@
<script> <script>
import CKEditor from '@ckeditor/ckeditor5-vue'; import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from "ChillMainAssets/module/ckeditor5"; import ClassicEditor from "../../../../../../ChillMainBundle/Resources/public/module/ckeditor5/editor_config";
import { mapState } from "vuex"; import { mapState } from "vuex";
export default { export default {

View File

@@ -3,10 +3,10 @@
<h2><a id="section-10"></a>{{ $t('persons_associated.title')}}</h2> <h2><a id="section-10"></a>{{ $t('persons_associated.title')}}</h2>
<div v-if="currentParticipations.length > 0"> <div v-if="currentParticipations.length > 0">
<label class="col-form-label">{{ $t('persons_associated.counter', { count: counter }) }}</label> <label class="col-form-label">{{ $tc('persons_associated.counter', counter) }}</label>
</div> </div>
<div v-else> <div v-else>
<label class="chill-no-data-statement">{{ $t('persons_associated.counter', { count: counter }) }}</label> <label class="chill-no-data-statement">{{ $tc('persons_associated.counter', counter) }}</label>
</div> </div>
<div v-if="participationWithoutHousehold.length > 0" class="alert alert-warning no-household"> <div v-if="participationWithoutHousehold.length > 0" class="alert alert-warning no-household">

View File

@@ -4,10 +4,10 @@
<h2><a id="section-90"></a>{{ $t('resources.title')}}</h2> <h2><a id="section-90"></a>{{ $t('resources.title')}}</h2>
<div v-if="resources.length > 0"> <div v-if="resources.length > 0">
<label class="col-form-label">{{ $t('resources.counter', { count: counter }) }}</label> <label class="col-form-label">{{ $tc('resources.counter', counter) }}</label>
</div> </div>
<div v-else> <div v-else>
<label class="chill-no-data-statement">{{ $t('resources.counter', { count: counter }) }}</label> <label class="chill-no-data-statement">{{ $tc('resources.counter', counter) }}</label>
</div> </div>
<div class="flex-table mb-3"> <div class="flex-table mb-3">

View File

@@ -41,7 +41,7 @@
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue'; import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods"; import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
import CKEditor from '@ckeditor/ckeditor5-vue'; import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from "ChillMainAssets/module/ckeditor5"; import ClassicEditor from "ChillMainAssets/module/ckeditor5/editor_config";
export default { export default {
name: "WriteComment", name: "WriteComment",

View File

@@ -331,7 +331,7 @@
import {mapState, mapGetters,} from 'vuex'; import {mapState, mapGetters,} from 'vuex';
import {dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date'; import {dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date';
import CKEditor from '@ckeditor/ckeditor5-vue'; import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js'; import ClassicEditor from 'ChillMainAssets/module/ckeditor5/editor_config';
import AddResult from './components/AddResult.vue'; import AddResult from './components/AddResult.vue';
import AddEvaluation from './components/AddEvaluation.vue'; import AddEvaluation from './components/AddEvaluation.vue';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'; import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';

View File

@@ -195,7 +195,7 @@
<script> <script>
import {dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date'; import {dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date';
import CKEditor from '@ckeditor/ckeditor5-vue'; import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js'; import ClassicEditor from 'ChillMainAssets/module/ckeditor5/editor_config';
import { mapGetters, mapState } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue'; import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator'; import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';

View File

@@ -23,7 +23,7 @@
data-bs-toggle="collapse" data-bs-toggle="collapse"
aria-expanded="false" aria-expanded="false"
@click="toggleHouseholdSuggestion"> @click="toggleHouseholdSuggestion">
{{ $t('household_members_editor.show_household_suggestion', { count: countHouseholdSuggestion }) }} {{ $tc('household_members_editor.show_household_suggestion', countHouseholdSuggestion) }}
</button> </button>
<button v-if="showHouseholdSuggestion" <button v-if="showHouseholdSuggestion"
class="accordion-button" class="accordion-button"

View File

@@ -75,7 +75,7 @@ div.participation-details {
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue'; import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import CKEditor from '@ckeditor/ckeditor5-vue'; import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js'; import ClassicEditor from 'ChillMainAssets/module/ckeditor5/editor_config';
export default { export default {
name: 'MemberDetails', name: 'MemberDetails',

View File

@@ -10,7 +10,7 @@
<script> <script>
import CKEditor from '@ckeditor/ckeditor5-vue'; import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from "ChillMainAssets/module/ckeditor5"; import ClassicEditor from "ChillMainAssets/module/ckeditor5/editor_config";
export default { export default {
name: "PersonComment.vue", name: "PersonComment.vue",

View File

@@ -17,7 +17,7 @@
<div class="search"> <div class="search">
<label class="col-form-label" style="float: right;"> <label class="col-form-label" style="float: right;">
{{ $t('add_persons.suggested_counter', { count: suggestedCounter }) }} {{ $tc('add_persons.suggested_counter', suggestedCounter) }}
</label> </label>
<input id="search-persons" <input id="search-persons"
@@ -42,7 +42,7 @@
</a> </a>
</span> </span>
<span v-if="selectedCounter > 0"> <span v-if="selectedCounter > 0">
{{ $t('add_persons.selected_counter', { count: selectedCounter }) }} {{ $tc('add_persons.selected_counter', selectedCounter) }}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -52,7 +52,9 @@
{{ $t('renderbox.deathdate') + ' ' + deathdate }} {{ $t('renderbox.deathdate') + ' ' + deathdate }}
</time> </time>
<span v-if="options.addAge && person.birthdate" class="age">{{ $t('renderbox.years_old', { n: person.age }) }}</span> <span v-if="options.addAge && person.birthdate" class="age">{{
$tc('renderbox.years_old', person.age)
}}</span>
</p> </p>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@
<span :class="'altname altname-' + altNameKey"> ({{ altNameLabel }})</span> <span :class="'altname altname-' + altNameKey"> ({{ altNameLabel }})</span>
</span> </span>
<span v-if="person.suffixText" class="suffixtext">&nbsp;{{ person.suffixText }}</span> <span v-if="person.suffixText" class="suffixtext">&nbsp;{{ person.suffixText }}</span>
<span class="age" v-if="this.addAge && person.birthdate !== null && person.deathdate === null">{{ $t('renderbox.years_old', { n: person.age }) }}</span> <span class="age" v-if="this.addAge && person.birthdate !== null && person.deathdate === null">{{ $tc('renderbox.years_old', person.age) }}</span>
<span v-else-if="this.addAge && person.deathdate !== null">&nbsp;()</span> <span v-else-if="this.addAge && person.deathdate !== null">&nbsp;()</span>
</span> </span>
</template> </template>

View File

@@ -15,7 +15,7 @@ const personMessages = {
person: { person: {
firstname: "Prénom", firstname: "Prénom",
lastname: "Nom", lastname: "Nom",
born: (ctx) => { born: (ctx: {gender: "man"|"woman"|"unknown"}) => {
if (ctx.gender === 'man') { if (ctx.gender === 'man') {
return 'Né le'; return 'Né le';
} else if (ctx.gender === 'woman') { } else if (ctx.gender === 'woman') {

View File

@@ -11,17 +11,14 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\Repository; namespace Chill\PersonBundle\Tests\Repository;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CenterRepositoryInterface; use Chill\MainBundle\Repository\CenterRepositoryInterface;
use Chill\MainBundle\Repository\CountryRepository; use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonPhone;
use Chill\PersonBundle\Repository\PersonACLAwareRepository; use Chill\PersonBundle\Repository\PersonACLAwareRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\DataProvider;
use Prophecy\Argument; use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
@@ -101,67 +98,4 @@ final class PersonACLAwareRepositoryTest extends KernelTestCase
$this->assertStringContainsString('diallo', strtolower($person->getFirstName().' '.$person->getLastName())); $this->assertStringContainsString('diallo', strtolower($person->getFirstName().' '.$person->getLastName()));
} }
} }
/**
* @dataProvider providePersonsWithPhoneNumbers
*/
public function testFindByPhonenumber(\libphonenumber\PhoneNumber $phoneNumber, ?int $expectedId): void
{
$user = new User();
$authorizationHelper = $this->prophesize(AuthorizationHelperInterface::class);
$authorizationHelper->getReachableCenters(Argument::exact($user), Argument::exact(PersonVoter::SEE))
->willReturn($this->centerRepository->findAll());
$security = $this->prophesize(Security::class);
$security->getUser()->willReturn($user);
$repository = new PersonACLAwareRepository(
$security->reveal(),
$this->entityManager,
$this->countryRepository,
$authorizationHelper->reveal()
);
$actual = $repository->findByPhone($phoneNumber, 0, 10);
if (null === $expectedId) {
self::assertCount(0, $actual);
} else {
$actualIds = array_map(fn (Person $person) => $person->getId(), $actual);
self::assertContains($expectedId, $actualIds);
}
}
public static function providePersonsWithPhoneNumbers(): iterable
{
self::bootKernel();
$em = self::getContainer()->get(EntityManagerInterface::class);
$center = $em->createQuery('SELECT c FROM '.Center::class.' c ')->setMaxResults(1)
->getSingleResult();
$util = \libphonenumber\PhoneNumberUtil::getInstance();
$mobile = $util->parse('+32486123456');
$fixed = $util->parse('+3281136917');
$anotherMobile = $util->parse('+32486123478');
$person = (new Person())->setFirstName('diallo')->setLastName('diallo')->setCenter($center);
$person->setMobilenumber($mobile)->setPhonenumber($fixed);
$otherPhone = new PersonPhone();
$otherPhone->setPerson($person);
$otherPhone->setPhonenumber($anotherMobile);
$otherPhone->setType('mobile');
$em->persist($person);
$em->persist($otherPhone);
$em->flush();
self::ensureKernelShutdown();
yield [$mobile, $person->getId()];
yield [$anotherMobile, $person->getId()];
yield [$fixed, $person->getId()];
yield [$util->parse('+331234567890'), null];
}
} }

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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
) {}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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,
) {}
}

View File

@@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Action\Ticket;
class AssociateByPhonenumberCommand
{
public function __construct(
public string $phonenumber,
) {}
}

View File

@@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Action\Ticket;
final readonly class CreateTicketCommand
{
public function __construct(
public string $externalReference = '',
) {}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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);
}
}

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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);
}
}
}

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Action\Ticket\Handler;
use Chill\TicketBundle\Action\Ticket\CreateTicketCommand;
use Chill\TicketBundle\Entity\Ticket;
class CreateTicketCommandHandler
{
public function __invoke(CreateTicketCommand $command): Ticket
{
$ticket = new Ticket();
$ticket->setExternalRef($command->externalReference);
return $ticket;
}
}

View File

@@ -1,55 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Action\Ticket\Handler;
use Chill\TicketBundle\Action\Ticket\ReplaceMotiveCommand;
use Chill\TicketBundle\Entity\MotiveHistory;
use Chill\TicketBundle\Entity\Ticket;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Clock\ClockInterface;
final readonly class ReplaceMotiveCommandHandler
{
public function __construct(
private ClockInterface $clock,
private EntityManagerInterface $entityManager,
) {}
public function handle(Ticket $ticket, ReplaceMotiveCommand $command): void
{
if (null === $command->motive) {
throw new \InvalidArgumentException('The new motive cannot be null');
}
// will add if there are no existing motive
$readyToAdd = 0 === count($ticket->getMotiveHistories());
foreach ($ticket->getMotiveHistories() as $history) {
if (null !== $history->getEndDate()) {
continue;
}
if ($history->getMotive() === $command->motive) {
// we apply the same motive, we do nothing
continue;
}
$history->setEndDate($this->clock->now());
$readyToAdd = true;
}
if ($readyToAdd) {
$history = new MotiveHistory($command->motive, $ticket, $this->clock->now());
$this->entityManager->persist($history);
}
}
}

View File

@@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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);
}
}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Action\Ticket;
use Chill\TicketBundle\Entity\Motive;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
final readonly class ReplaceMotiveCommand
{
public function __construct(
#[Assert\NotNull]
#[Groups(['write'])]
public ?Motive $motive,
) {}
}

View File

@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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(),
]);
}
}

View File

@@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ChillTicketBundle extends Bundle {}

View File

@@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Controller;
use 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
);
}
}

View File

@@ -1,70 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Controller;
use 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()])
);
}
}

View File

@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Controller;
use Chill\TicketBundle\Entity\Ticket;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
class EditTicketController
{
public function __construct(
private Environment $templating
) {}
#[Route('/{_locale}/ticket/ticket/{id}/edit', name: 'chill_ticket_ticket_edit')]
public function __invoke(
Ticket $ticket
): Response {
return new Response(
$this->templating->render(
'@ChillTicket/Ticket/edit.html.twig',
[
'ticket' => $ticket,
]
)
);
}
}

View File

@@ -1,58 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Controller;
use 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);
}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
final class MotiveApiController extends ApiController
{
protected function customizeQuery(string $action, Request $request, $query): void
{
/* @var $query QueryBuilder */
$query->andWhere('e.active = TRUE');
}
}

View File

@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Controller;
use 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
);
}
}

View File

@@ -1,85 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Controller;
use 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,
);
}
}

View File

@@ -1,82 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\DataFixtures\ORM;
use Chill\TicketBundle\Entity\Motive;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
final class LoadMotives extends Fixture implements FixtureGroupInterface
{
public static function getGroups(): array
{
return ['ticket'];
}
public function load(ObjectManager $manager)
{
foreach (explode("\n", self::MOTIVES) as $label) {
if ('' === trim($label)) {
continue;
}
$motive = new Motive();
$motive->setLabel(['fr' => trim($label)]);
$manager->persist($motive);
}
$manager->flush();
}
private const MOTIVES = <<<'TXT'
Coordonnées
Horaire de passage
Retard de livraison
Erreur de livraison
Colis incomplet
MATLOC
Retard DASRI
Planning d'astreintes
Planning des tournées
Contrôle pompe
Changement de rendez-vous
Renseignement facturation/prestation
Décès patient
Demande de prise en charge
Information absence
Demande bulletin de situation
Difficultés accès logement
Déplacement inutile
Problème de prélèvement/de commande
Parc auto
Demande d'admission
Retrait de matériel au domicile
Comptes-rendus
Démarchage commercial
Demande de transport
Demande laboratoire
Demande admission
Suivi de prise en charge
Mauvaise adresse
Patient absent
Annulation
Colis perdu
Changement de rendez-vous
Coordination interservices
Problème de substitution produits
Problème ordonnance
Réclamations facture
Préparation urgente
TXT;
}

View File

@@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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,
],
],
],
],
],
]);
}
}

View File

@@ -1,130 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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;
}
}

View File

@@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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;
}
}

View File

@@ -1,91 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Entity;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity()]
#[ORM\Table(name: 'input_history', schema: 'chill_ticket')]
class InputHistory
{
#[ORM\Id]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: Person::class)]
#[ORM\JoinColumn(nullable: true)]
private ?Person $person = null;
#[ORM\ManyToOne(targetEntity: ThirdParty::class)]
#[ORM\JoinColumn(nullable: true)]
private ?ThirdParty $thirdParty = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])]
private ?\DateTimeImmutable $endDate = null;
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: true)]
private ?User $removedBy = null;
public function __construct(
Person|ThirdParty $input,
#[ORM\ManyToOne(targetEntity: Ticket::class)]
#[ORM\JoinColumn(nullable: false)]
private Ticket $ticket,
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
private \DateTimeImmutable $startDate,
) {
if ($input instanceof Person) {
$this->person = $input;
} else {
$this->thirdParty = $input;
}
}
public function getId(): ?int
{
return $this->id;
}
public function getEndDate(): ?\DateTimeImmutable
{
return $this->endDate;
}
public function getRemovedBy(): ?User
{
return $this->removedBy;
}
public function getStartDate(): \DateTimeImmutable
{
return $this->startDate;
}
public function getTicket(): Ticket
{
return $this->ticket;
}
public function getInput(): Person|ThirdParty
{
if (null !== $this->person) {
return $this->person;
}
return $this->thirdParty;
}
}

View File

@@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
#[ORM\Entity()]
#[ORM\Table(name: 'motive', schema: 'chill_ticket')]
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_motive' => Motive::class])]
class Motive
{
#[ORM\Id]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
#[Serializer\Groups(['read'])]
private ?int $id = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
#[Serializer\Groups(['read'])]
private array $label = [];
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::BOOLEAN, nullable: false, options: ['default' => true])]
#[Serializer\Groups(['read'])]
private bool $active = true;
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): void
{
$this->active = $active;
}
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): array
{
return $this->label;
}
public function setLabel(array $label): void
{
$this->label = $label;
}
}

View File

@@ -1,80 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
#[ORM\Entity]
#[ORM\Table(name: 'motives_history', schema: 'chill_ticket')]
#[Serializer\DiscriminatorMap(typeProperty: 'type', mapping: ['ticket_motive_history' => MotiveHistory::class])]
class MotiveHistory implements TrackCreationInterface
{
use TrackCreationTrait;
#[ORM\Id]
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: false)]
#[ORM\GeneratedValue(strategy: 'AUTO')]
#[Serializer\Groups(['read'])]
private ?int $id = null;
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: true, options: ['default' => null])]
#[Serializer\Groups(['read'])]
private ?\DateTimeImmutable $endDate = null;
public function __construct(
#[ORM\ManyToOne(targetEntity: Motive::class)]
#[ORM\JoinColumn(nullable: false)]
#[Serializer\Groups(['read'])]
private Motive $motive,
#[ORM\ManyToOne(targetEntity: Ticket::class)]
#[ORM\JoinColumn(nullable: false)]
private Ticket $ticket,
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIME_IMMUTABLE, nullable: false)]
#[Serializer\Groups(['read'])]
private \DateTimeImmutable $startDate = new \DateTimeImmutable('now')
) {
$ticket->addMotiveHistory($this);
}
public function getEndDate(): ?\DateTimeImmutable
{
return $this->endDate;
}
public function getId(): ?int
{
return $this->id;
}
public function getMotive(): Motive
{
return $this->motive;
}
public function getStartDate(): \DateTimeImmutable
{
return $this->startDate;
}
public function getTicket(): Ticket
{
return $this->ticket;
}
public function setEndDate(?\DateTimeImmutable $endDate): void
{
$this->endDate = $endDate;
}
}

View File

@@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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;
}
}

View File

@@ -1,231 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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;
}
}

View File

@@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\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]);
}
}

View File

@@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\TicketBundle\Repository;
use Chill\TicketBundle\Entity\Ticket;
use Doctrine\Persistence\ObjectRepository;
/**
* @extends ObjectRepository<Ticket>
*/
interface TicketRepositoryInterface extends ObjectRepository
{
public function findOneByExternalRef(string $extId): ?Ticket;
}

View File

@@ -1,24 +0,0 @@
@import '~ChillMainAssets/module/bootstrap/shared';
div.banner {
div#header-ticket-main {
background: none repeat scroll 0 0 #ae986fFF;
color: $white;
padding-top: 1em;
padding-bottom: 1em;
}
div#header-ticket-details {
background: none repeat scroll 0 0 #d3c7b1FF;
color: $white;
padding-top: 1em;
padding-bottom: 1em;
div.contact {
display: flex;
align-content: center;
& > * {
margin-right: 1em;
}
}
}
}

View File

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

View File

@@ -1,86 +0,0 @@
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,
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
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;

Some files were not shown because too many files have changed in this diff Show More