Merge branch '139_demandeur' into _23_addresses_form_2

This commit is contained in:
Mathieu Jaumotte 2021-05-11 15:51:06 +02:00
commit 41254db72f
51 changed files with 1732 additions and 375 deletions

View File

@ -73,7 +73,8 @@
"symfony/web-profiler-bundle": "^5.0",
"symfony/var-dumper": "4.*",
"symfony/debug-bundle": "^5.1",
"symfony/phpunit-bridge": "^5.2"
"symfony/phpunit-bridge": "^5.2",
"nelmio/alice": "^3.8"
},
"scripts": {
"auto-scripts": {

View File

@ -450,4 +450,39 @@ Full configuration example
single-collection: single
base_role: null
Maintaining an OpenApi
======================
.. note::
This is experimental and may change. Keep reading this part.
Accessing swagger
-----------------
The swagger UI is accessible through `<http://localhost:8001/_dev/swagger>`_
This is possible only in development mode.
You must be authenticated with a valid user to access it.
Maintaining specs
-----------------
Each bundle should have an `open api 3.0 spec file <https://swagger.io/docs/specification/about/>`_ at the bundle's root, and name :code:`chill.api.specs.yaml`.
.. warning::
Update the command :code:`specs-build` into `package.json` when adding a new api file.
The specs may be compiled from the different bundles filed, and validated using docker node:
.. code-block:: bash
./docker-node.sh
# build the openapi
yarn specs-build
# validate
yarn specs-validate

View File

@ -82,7 +82,7 @@ Chill will be available at ``http://localhost:8001.`` Currently, there isn't any
.. code-block:: bash
docker-compose exec --user $(id -u) php bin/console doctrine:fixtures:load
docker-compose exec --user $(id -u) php bin/console doctrine:fixtures:load --purge-with-truncate
There are several users available:

View File

@ -25,12 +25,19 @@ class AbstractCRUDController extends AbstractController
*
* @param string $id
* @return object
* @throw Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the object is not found
*/
protected function getEntity($action, $id, Request $request): ?object
protected function getEntity($action, $id, Request $request): object
{
return $this->getDoctrine()
$e = $this->getDoctrine()
->getRepository($this->getEntityClass())
->find($id);
if (NULL === $e) {
throw $this->createNotFoundException(sprintf("The object %s for id %s is not found", $this->getEntityClass(), $id));
}
return $e;
}
/**

View File

@ -22,6 +22,7 @@
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Serializer\Model\Collection;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Search\UnknowSearchDomainException;
@ -34,6 +35,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Chill\MainBundle\Search\SearchProvider;
use Symfony\Contracts\Translation\TranslatorInterface;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\SearchApi;
/**
* Class SearchController
@ -42,32 +44,24 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
*/
class SearchController extends AbstractController
{
/**
*
* @var SearchProvider
*/
protected $searchProvider;
protected SearchProvider $searchProvider;
/**
*
* @var TranslatorInterface
*/
protected $translator;
protected TranslatorInterface $translator;
/**
*
* @var PaginatorFactory
*/
protected $paginatorFactory;
protected PaginatorFactory $paginatorFactory;
protected SearchApi $searchApi;
function __construct(
SearchProvider $searchProvider,
TranslatorInterface $translator,
PaginatorFactory $paginatorFactory
PaginatorFactory $paginatorFactory,
SearchApi $searchApi
) {
$this->searchProvider = $searchProvider;
$this->translator = $translator;
$this->paginatorFactory = $paginatorFactory;
$this->searchApi = $searchApi;
}
@ -152,6 +146,19 @@ class SearchController extends AbstractController
array('results' => $results, 'pattern' => $pattern)
);
}
public function searchApi(Request $request, $_format): JsonResponse
{
//TODO this is an incomplete implementation
$query = $request->query->get('q', '');
$results = $this->searchApi->getResults($query, 0, 150);
$paginator = $this->paginatorFactory->create(count($results));
$collection = new Collection($results, $paginator);
return $this->json($collection);
}
public function advancedSearchListAction(Request $request)
{

View File

@ -55,11 +55,11 @@ class LoadCenters extends AbstractFixture implements OrderedFixtureInterface
public function load(ObjectManager $manager)
{
foreach (static::$centers as $new) {
$centerA = new Center();
$centerA->setName($new['name']);
$center = new Center();
$center->setName($new['name']);
$manager->persist($centerA);
$this->addReference($new['ref'], $centerA);
$manager->persist($center);
$this->addReference($new['ref'], $center);
static::$refs[] = $new['ref'];
}

View File

@ -39,8 +39,17 @@ div.subheader {
height: 130px;
}
//// VUEJS ////
//// SCRATCH BUTTONS
.sc-button {
&.disabled {
cursor: default;
&.bt-remove {
background-color: #d9d9d9;
}
}
}
//// VUEJS ////
div.vue-component {
padding: 1.5em;
margin: 2em 0;
@ -95,33 +104,47 @@ div.vue-component {
}
//// AddPersons modal
div.modal-body.up {
margin: auto 4em;
div.search {
position: relative;
input {
padding: 1.2em 1.5em 1.2em 2.5em;
margin: 1em 0;
div.body-head {
overflow-y: unset;
div.modal-body:first-child {
margin: auto 4em;
div.search {
position: relative;
input {
padding: 1.2em 1.5em 1.2em 2.5em;
margin: 1em 0;
}
i {
position: absolute;
opacity: 0.5;
padding: 0.65em 0;
top: 50%;
}
i.fa-search {
left: 0.5em;
}
i.fa-times {
right: 1em;
padding: 0.75em 0;
cursor: pointer;
}
}
i {
position: absolute;
top: 50%;
left: 0.5em;
padding: 0.65em 0;
opacity: 0.5;
}
}
div.modal-body:last-child {
padding-bottom: 0;
}
}
div.results {
div.count {
margin: -0.5em 0 0.7em;
display: flex;
justify-content: space-between;
div.count {
margin: -0.5em 0 0.7em;
display: flex;
justify-content: space-between;
a {
cursor: pointer;
}
}
div.results {
div.list-item {
line-height: 26pt;
padding: 0.3em 0.8em;
padding: 0.4em 0.8em;
display: flex;
flex-direction: row;
&.checked {
@ -132,11 +155,20 @@ div.results {
& > input {
margin-right: 0.8em;
}
span:not(.name) {
margin-left: 0.5em;
opacity: 0.5;
font-size: 90%;
font-style: italic;
}
}
div.right_actions {
margin: 0 0 0 auto;
display: flex;
align-items: flex-end;
& > * {
margin-left: 0.5em;
align-self: baseline;
}
a.sc-button {
border: 1px solid lightgrey;
@ -146,7 +178,6 @@ div.results {
}
}
}
.discret {
color: grey;
margin-right: 1em;

View File

@ -9,8 +9,8 @@
<button class="close sc-button grey" @click="$emit('close')">
<i class="fa fa-times" aria-hidden="true"></i></button>
</div>
<div class="modal-body up" style="overflow-y: unset;">
<slot name="body-fixed"></slot>
<div class="body-head">
<slot name="body-head"></slot>
</div>
<div class="modal-body">
<slot name="body"></slot>

View File

@ -0,0 +1,36 @@
<?php
namespace Chill\MainBundle\Search\Model;
class Result
{
private float $relevance;
/**
* mixed an arbitrary result
*/
private $result;
/**
* @param float $relevance
* @param $result
*/
public function __construct(float $relevance, $result)
{
$this->relevance = $relevance;
$this->result = $result;
}
public function getRelevance(): float
{
return $this->relevance;
}
public function getResult()
{
return $this->result;
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Chill\MainBundle\Search;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\EntityManagerInterface;
use Chill\MainBundle\Search\SearchProvider;
use Symfony\Component\VarDumper\Resources\functions\dump;
/**
* ***Warning*** This is an incomplete implementation ***Warning***
*/
class SearchApi
{
private EntityManagerInterface $em;
private SearchProvider $search;
public function __construct(EntityManagerInterface $em, SearchProvider $search)
{
$this->em = $em;
$this->search = $search;
}
/**
* @return Model/Result[]
*/
public function getResults(string $query, int $offset, int $maxResult): array
{
// **warning again**: this is an incomplete implementation
$results = [];
foreach ($this->getPersons($query) as $p) {
$results[] = new Model\Result((float)\rand(0, 100) / 100, $p);
}
foreach ($this->getThirdParties($query) as $t) {
$results[] = new Model\Result((float)\rand(0, 100) / 100, $t);
}
\usort($results, function(Model\Result $a, Model\Result $b) {
return ($a->getRelevance() <=> $b->getRelevance()) * -1;
});
return $results;
}
public function countResults(string $query): int
{
return 0;
}
private function getThirdParties(string $query)
{
$thirdPartiesIds = $this->em->createQuery('SELECT t.id FROM '.ThirdParty::class.' t')
->getScalarResult();
$nbResults = rand(0, 15);
if ($nbResults === 1) {
$nbResults++;
} elseif ($nbResults === 0) {
return [];
}
$ids = \array_map(function ($e) use ($thirdPartiesIds) { return $thirdPartiesIds[$e]['id'];},
\array_rand($thirdPartiesIds, $nbResults));
$a = $this->em->getRepository(ThirdParty::class)
->findById($ids);
return $a;
}
private function getPersons(string $query)
{
$params = [
SearchInterface::SEARCH_PREVIEW_OPTION => false
];
$search = $this->search->getResultByName($query, 'person_regular', 0, 50, $params, 'json');
$ids = \array_map(function($r) { return $r['id']; }, $search['results']);
if (count($ids) === 0) {
return [];
}
return $this->em->getRepository(Person::class)
->findById($ids)
;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Chill\MainBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
{
use NormalizerAwareTrait;
public function normalize($address, string $format = null, array $context = [])
{
$data['address_id'] = $address->getId();
$data['text'] = $address->getStreet().', '.$address->getBuildingName();
$data['postcode']['name'] = $address->getPostCode()->getName();
return $data;
}
public function supportsNormalization($data, string $format = null)
{
return $data instanceof Address;
}
}

View File

@ -0,0 +1,59 @@
---
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Chill api"
description: "Api documentation for chill. Currently, work in progress"
servers:
- url: "/api"
description: "Your current dev server"
components:
parameters:
_format:
name: _format
in: path
required: true
schema:
type: string
enum:
- json
paths:
/1.0/search.json:
get:
summary: perform a search across multiple entities
tags:
- search
- person
- thirdparty
description: >
**Warning**: This is currently a stub (not really implemented
The search is performed across multiple entities. The entities must be listed into
`type` parameters.
The results are ordered by relevance, from the most to the lowest relevant.
parameters:
- name: q
in: query
required: true
description: the pattern to search
schema:
type: string
- name: type[]
in: query
required: true
description: the type entities amongst the search is performed
schema:
type: array
items:
type: string
enum:
- person
- thirdparty
responses:
200:
description: "OK"

View File

@ -69,6 +69,13 @@ chill_main_search:
requirements:
_format: html|json
chill_main_search_global:
path: '/api/1.0/search.{_format}'
controller: Chill\MainBundle\Controller\SearchController::searchApi
format: 'json'
requirements:
_format: 'json'
chill_main_advanced_search:
path: /{_locale}/search/advanced/{name}
controller: Chill\MainBundle\Controller\SearchController::advancedSearchAction

View File

@ -16,6 +16,7 @@ services:
$searchProvider: '@chill_main.search_provider'
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
$paginatorFactory: '@Chill\MainBundle\Pagination\PaginatorFactory'
$searchApi: '@Chill\MainBundle\Search\SearchApi'
tags: ['controller.service_arguments']
Chill\MainBundle\Controller\PermissionsGroupController:

View File

@ -1,3 +1,10 @@
services:
chill_main.search_provider:
class: Chill\MainBundle\Search\SearchProvider
class: Chill\MainBundle\Search\SearchProvider
Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
Chill\MainBundle\Search\SearchApi:
arguments:
$em: '@Doctrine\ORM\EntityManagerInterface'
$search: '@Chill\MainBundle\Search\SearchProvider'

View File

@ -4,6 +4,10 @@ services:
tags:
- { name: 'serializer.normalizer', priority: 64 }
Chill\MainBundle\Serializer\Normalizer\AddressNormalizer:
tags:
- { name: 'serializer.normalizer', priority: 64 }
Chill\MainBundle\Serializer\Normalizer\DateNormalizer:
tags:
- { name: 'serializer.normalizer', priority: 64 }

View File

@ -12,6 +12,8 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Component\Serializer\Exception\RuntimeException;
class AccompanyingCourseApiController extends ApiController
{
@ -64,6 +66,50 @@ class AccompanyingCourseApiController extends ApiController
return $this->json($participation);
}
public function requestorApi($id, Request $request, string $_format): Response
{
/** @var AccompanyingPeriod $accompanyingPeriod */
$action = 'requestor';
$accompanyingPeriod = $this->getEntity($action, $id, $request);
// a requestor may be a person or a thirdParty
$this->checkACL($action, $request, $_format, $accompanyingPeriod);
$this->onPostCheckACL($action, $request, $_format, $accompanyingPeriod);
if (Request::METHOD_DELETE === $request->getMethod()) {
$accompanyingPeriod->setRequestor(NULL);
} elseif (Request::METHOD_POST === $request->getMethod()) {
$requestor = null;
$exceptions = [];
foreach ([Person::class, ThirdParty::class] as $class) {
try {
$requestor = $this->getSerializer()
->deserialize($request->getContent(), $class, $_format, []);
} catch (RuntimeException $e) {
$exceptions[] = $e;
}
}
if ($requestor === null) {
throw new BadRequestException('Could not find any person or requestor', 0, $exceptions[0]);
}
$accompanyingPeriod->setRequestor($requestor);
} else {
throw new BadRequestException('method not supported');
}
$errors = $this->validator->validate($accompanyingPeriod);
if ($errors->count() > 0) {
// only format accepted
return $this->json($errors);
}
$this->getDoctrine()->getManager()->flush();
return $this->json($accompanyingPeriod->getRequestor());
}
protected function onPostCheckACL(string $action, Request $request, string $_format, $entity): ?Response
{
$this->eventDispatcher->dispatch(

View File

@ -106,6 +106,11 @@ class AccompanyingCourseController extends Controller
])
);
//$accompanyingCourse->getRequestorPerson();
//$accompanyingCourse->getRequestorThirdParty();
//$accompanyingCourse->isRequestorAnonymous();
//dump($accompanyingCourse); die;
switch ($_format) {
case 'json':
return $this->json($accompanyingCourse);

View File

@ -334,6 +334,18 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'requestor' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
]
]

View File

@ -515,9 +515,9 @@ class AccompanyingPeriod
return $this->requestorPerson;
}
public function setRequestorPerson(Person $requestorPerson): self
public function setRequestorPerson(Person $requestorPerson = null): self
{
$this->requestorPerson = ($this->requestorThirdParty === null) ? $requestorPerson : null;
$this->requestorPerson = $requestorPerson;
return $this;
}
@ -527,9 +527,9 @@ class AccompanyingPeriod
return $this->requestorThirdParty;
}
public function setRequestorThirdParty(ThirdParty $requestorThirdParty): self
public function setRequestorThirdParty(ThirdParty $requestorThirdParty = null): self
{
$this->requestorThirdParty = ($this->requestorPerson === null) ? $requestorThirdParty : null;
$this->requestorThirdParty = $requestorThirdParty;
return $this;
}
@ -542,6 +542,37 @@ class AccompanyingPeriod
return $this->requestorPerson ?? $this->requestorThirdParty;
}
/**
* Set a requestor
*
* The requestor is either an instance of ThirdParty, or an
* instance of Person
*
* @param $requestor Person|ThirdParty
* @return self
* @throw UnexpectedValueException if the requestor is not a Person or ThirdParty
*
*/
public function setRequestor($requestor): self
{
if ($requestor instanceof Person) {
$this->setRequestorThirdParty(NULL);
$this->setRequestorPerson($requestor);
} elseif ($requestor instanceof ThirdParty) {
$this->setRequestorThirdParty($requestor);
$this->setRequestorPerson(NULL);
} elseif (NULL === $requestor) {
$this->setRequestorPerson(NULL);
$this->setRequestorThirdParty(NULL);
} else {
throw new \UnexpectedValueException("requestor is not an instance of Person or ThirdParty");
}
return $this;
}
public function isRequestorAnonymous(): bool
{
return $this->requestorAnonymous;

View File

@ -2,6 +2,8 @@
<accompanying-course></accompanying-course>
<persons-associated></persons-associated>
<requestor></requestor>
<interlocutors></interlocutors>
<!--test></test-->
</template>
<script>
@ -10,13 +12,17 @@ import { mapState } from 'vuex'
import AccompanyingCourse from './components/AccompanyingCourse.vue';
import PersonsAssociated from './components/PersonsAssociated.vue';
import Requestor from './components/Requestor.vue';
import Interlocutors from './components/Interlocutors.vue';
//import Test from './components/Test.vue';
export default {
name: 'App',
components: {
AccompanyingCourse,
PersonsAssociated,
Requestor
Requestor,
Interlocutors,
//Test
},
computed: mapState([
'accompanyingCourse'

View File

@ -1,18 +1,11 @@
const
locale = 'fr',
format = 'json'
, accompanying_period_id = window.accompanyingCourseId //tmp
;
/*
* Endpoint chill_person_accompanying_course_api_show
* method GET, get AccompanyingCourse Object
* Endpoint v.2 chill_api_single_accompanying_course__entity
* method GET/HEAD, get AccompanyingCourse Instance
*
* @accompanying_period_id___ integer
* @TODO var is not used but necessary in method signature
* @id integer - id of accompanyingCourse
*/
let getAccompanyingCourse = (accompanying_period_id___) => { //tmp
const url = `/${locale}/person/api/1.0/accompanying-course/${accompanying_period_id}/show.${format}`;
const getAccompanyingCourse = (id) => {
const url = `/api/1.0/person/accompanying-course/${id}.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
@ -21,21 +14,50 @@ let getAccompanyingCourse = (accompanying_period_id___) => { //tmp
};
/*
* Endpoint chill_person_accompanying_course_api_add_participation,
* Endpoint v.2 chill_api_single_accompanying_course_participation,
* method POST/DELETE, add/close a participation to the accompanyingCourse
*
* @accompanying_period_id integer - id of accompanyingCourse
* @person_id integer - id of person
* @method string - POST or DELETE
* @id integer - id of accompanyingCourse
* @person_id integer - id of person
* @method string - POST or DELETE
*/
let postParticipation = (accompanying_period_id, person_id, method) => {
const url = `/${locale}/person/api/1.0/accompanying-course/${accompanying_period_id}/participation.${format}`
const postParticipation = (id, person_id, method) => {
const url = `/api/1.0/person/accompanying-course/${id}/participation.json`;
return fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({id: person_id})
body: JSON.stringify({person_id: person_id})
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint v.2 chill_api_single_accompanying_course_requestor,
* method POST/DELETE, add/close a requestor to the accompanyingCourse
*
* @id integer - id of accompanyingCourse
* @payload object of type person|thirdparty
* @method string - POST or DELETE
*/
const postRequestor = (id, payload, method) => {
const body = {};
if (payload !== null) {
const typeId = `${payload.result.type}_id`;
body[typeId] = payload.result[typeId];
};
console.log(body);
const url = `/api/1.0/person/accompanying-course/${id}/requestor.json`;
return fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
@ -45,5 +67,6 @@ let postParticipation = (accompanying_period_id, person_id, method) => {
export {
getAccompanyingCourse,
postParticipation
postParticipation,
postRequestor
};

View File

@ -4,14 +4,46 @@
<dl>
<dt>{{ $t('course.id') }}</dt>
<dd>{{ accompanyingCourse.id }}</dd>
<dt>{{ $t('course.user') }}</dt>
<dd v-if="accompanyingCourse.user">{{ accompanyingCourse.user.username }}</dd>
<dt>{{ $t('course.status') }}</dt>
<dd>
<div v-if="accompanyingCourse.step !== 'DRAFT'"
class="badge badge-pill badge-primary">
{{ $t('course.step.active') }}</div>
<div v-else class="badge badge-pill badge-secondary">
{{ $t('course.step.draft') }}</div>
</dd>
<dt>{{ $t('course.flags') }}</dt>
<dd>
<div v-if="accompanyingCourse.emergency === true"
class="badge badge-pill badge-primary">
{{ $t('course.emergency') }}</div>
<div v-else class="badge badge-pill badge-secondary">
{{ $t('course.emergency') }}</div>
<div v-if="accompanyingCourse.confidential === true"
class="badge badge-pill badge-primary">
{{ $t('course.confidential') }}</div>
<div v-else class="badge badge-pill badge-secondary">
{{ $t('course.confidential') }}</div>
</dd>
<dt>{{ $t('course.opening_date') }}</dt>
<dd>{{ $d(accompanyingCourse.openingDate.datetime, 'short') }}</dd>
<dt>{{ $t('course.closing_date') }}</dt>
<dd>{{ $d(accompanyingCourse.closingDate.datetime, 'short') }}</dd>
<dd v-if="accompanyingCourse.closingDate">{{ $d(accompanyingCourse.closingDate.datetime, 'short') }}</dd>
<dt>{{ $t('course.closing_motive') }}</dt>
<dd v-if="accompanyingCourse.closingMotive">{{ accompanyingCourse.closingMotive.name.fr }}</dd>
<dt>{{ $t('course.remark') }}</dt>
<dd>{{ accompanyingCourse.remark }}</dd>
<dt>{{ $t('course.closing_motive') }}</dt>
<dd>{{ accompanyingCourse.closing_motive }}</dd>
</dl>
</div>
</template>

View File

@ -0,0 +1,54 @@
<template>
<tr>
<td>{{ interlocutor.key }}</td>
<td>{{ interlocutor.result.text }}</td>
<td><span >
</span>
</td>
<td><span >
</span>
</td>
<td>
<ul class="record_actions">
<li>
<a class="sc-button bt-show" target="_blank"
:href="url.show"
:title="$t('action.show')">
</a>
</li>
<li>
<a class="sc-button bt-update" target="_blank"
:href="url.edit"
:title="$t('action.edit')">
</a>
</li>
<li>
<button
class="sc-button bt-remove"
:title="$t('action.remove')"
@click.prevent="$emit('remove', interlocutor)">
</button>
</li>
</ul>
</td>
</tr>
</template>
<script>
export default {
name: 'InterlocutorItem',
props: ['interlocutor'],
data() {
return {
url: {
show: 'show', //'/fr/person/' + this.interlocutor.person.id + '/general',
edit: 'edit' //'/fr/person/' + this.interlocutor.person.id + '/general/edit'
}
}
},
emits: ['remove']
}
</script>

View File

@ -0,0 +1,82 @@
<template>
<div class="vue-component">
<h3>{{ $t('interlocutors.title')}}</h3>
<label>{{ $tc('interlocutors.counter', counter) }}</label>
<table class="rounded">
<thead>
<tr>
<th class="chill-orange">{{ $t('interlocutors.firstname') }}</th>
<th class="chill-orange">{{ $t('interlocutors.lastname') }}</th>
<th>3</th>
<th>4</th>
<th class="chill-orange">{{ $t('action.actions') }}</th>
</tr>
</thead>
<tbody>
<interlocutor-item
v-for="interlocutor in interlocutors"
v-bind:interlocutor="interlocutor"
v-bind:key="interlocutor.id"
@remove="removeInterlocutor">
</interlocutor-item>
</tbody>
</table>
<add-persons
buttonTitle="interlocutors.add_interlocutors"
modalTitle="interlocutors.add_interlocutors"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons"> <!-- to cast child method -->
</add-persons>
</div>
</template>
<script>
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
import InterlocutorItem from './InterlocutorItem.vue'
export default {
name: 'Interlocutors',
components: {
AddPersons,
InterlocutorItem
},
data() {
return {
addPersons: {
key: 'interlocutors',
options: {
type: ['person', 'thirdparty'],
priority: null,
uniq: false,
}
}
}
},
computed: {
accompanyingCourse() {
return this.$store.state.accompanyingCourse
}
},
methods: {
removeInterlocutor() {
console.log('@@ CLICK remove interlocutor: item');
this.$store.dispatch('removeInterlocutor');
},
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach(function(item) {
this.$store.dispatch('addInterlocutor', item);
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
}
}
}
</script>

View File

@ -29,10 +29,12 @@
</button>
</li-->
<li>
<button class="sc-button bt-remove"
<button v-if="!participation.endDate"
class="sc-button bt-remove"
:title="$t('action.remove')"
@click.prevent="$emit('close', participation)">
</button>
<button v-else class="sc-button bt-remove disabled"></button>
</li>
</ul>
</td>

View File

@ -2,7 +2,7 @@
<div class="vue-component">
<h3>{{ $t('persons_associated.title')}}</h3>
<label>{{ $tc('persons_associated.counter', counter) }}</label>
<table class="rounded">
<table class="rounded">
<thead>
<tr>
<th class="chill-orange">{{ $t('persons_associated.firstname') }}</th>
@ -12,24 +12,26 @@
<th class="chill-orange">{{ $t('action.actions') }}</th>
</tr>
</thead>
<tbody>
<person-item
v-for="participation in participations"
v-bind:participation="participation"
v-bind:key="participation.id"
<tbody>
<person-item
v-for="participation in participations"
v-bind:participation="participation"
v-bind:key="participation.id"
@remove="removeParticipation"
@close="closeParticipation">
</person-item>
</tbody>
</table>
<add-persons></add-persons>
<ul class="record_actions">
<!--li>
<button class="sc-button orange" @click="savePersons">
{{ $t('action.save') }}
</button>
</li-->
</ul>
<add-persons
buttonTitle="persons_associated.add_persons"
modalTitle="add_persons.title"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons"> <!-- to cast child method -->
</add-persons>
</div>
</template>
@ -40,27 +42,44 @@ import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
export default {
name: 'PersonsAssociated',
components: {
components: {
PersonItem,
AddPersons
},
data() {
return {
addPersons: {
key: 'persons_associated',
options: {
type: ['person'],
priority: null,
uniq: false,
}
}
}
},
computed: mapState({
participations: state => state.accompanyingCourse.participations,
counter: state => state.accompanyingCourse.participations.length
}),
methods: {
removeParticipation(item) {
this.$store.dispatch('removeParticipation', item)
console.log('@@ CLICK remove participation: item', item);
this.$store.dispatch('removeParticipation', item);
},
closeParticipation(item) {
console.log('@@ CLICK close participation: item', item);
this.$store.dispatch('closeParticipation', item)
this.$store.dispatch('closeParticipation', item);
},
/*
savePersons() {
console.log('[wip] saving persons');
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach(function(item) {
this.$store.dispatch('addParticipation', item);
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
}
*/
}
}
</script>

View File

@ -1,85 +1,116 @@
<template>
<div class="vue-component">
<h3>{{ $t('requestor.title') }}</h3>
{{ accompanyingCourse.id }}
{{ accompanyingCourse.remark }}<br><br>
<!-- TESTS AREA -->
<ul class="record_actions">
<li>
<button class="sc-button bt-create" @click="modal1.showModal = true">
{{ $t('action.show_modal') }}
</button>
</li>
<li>
<button class="sc-button bt-create" @click="modal2.showModal = true">
Ouvrir une seconde modale
</button>
</li>
</ul>
<div v-if="accompanyingCourse.requestor">
<dt>{{ $t('requestor.type') }}</dt>
<dd>{{ accompanyingCourse.requestor.type }}</dd>
<dt>{{ $t('requestor.text') }}</dt>
<dd>{{ accompanyingCourse.requestor.text }}</dd>
<dt>{{ $t('requestor.anonymous') }}</dt>
<dd>{{ accompanyingCourse.requestorAnonymous }}</dd>
<div v-if="accompanyingCourse.requestor.type === 'person'">
<dt>{{ $t('requestor.person_id') }}</dt>
<dd>{{ accompanyingCourse.requestor.person_id }}</dd>
<dt>{{ $t('requestor.birthdate') }}</dt>
<dd>{{ $d(accompanyingCourse.requestor.birthdate.datetime, 'short') }}</dd>
<dt>{{ $t('requestor.center') }}</dt>
<dd>{{ accompanyingCourse.requestor.center.name }}</dd>
<dt>{{ $t('requestor.firstName') }}</dt>
<dd>{{ accompanyingCourse.requestor.firstName }}</dd>
<dt>{{ $t('requestor.lastName') }}</dt>
<dd>{{ accompanyingCourse.requestor.lastName }}</dd>
<dt>{{ $t('requestor.phonenumber') }}</dt>
<dd>{{ accompanyingCourse.requestor.phonenumber }}</dd>
<dt>{{ $t('requestor.mobilenumber') }}</dt>
<dd>{{ accompanyingCourse.requestor.mobilenumber }}</dd>
<dt>{{ $t('requestor.altNames') }}</dt>
<dd>{{ accompanyingCourse.requestor.altNames }}</dd>
</div>
<div v-if="accompanyingCourse.requestor.type === 'thirdparty'">
<dt>{{ $t('requestor.person_id') }}</dt>
<dd>{{ accompanyingCourse.requestor.thirdparty_id }}</dd>
<dt>{{ $t('requestor.address') }}</dt>
<dd>{{ accompanyingCourse.requestor.address.text }}</dd>
<dt>{{ $t('requestor.location') }}</dt>
<dd>{{ accompanyingCourse.requestor.address.postcode.name }}</dd>
</div>
</div>
<teleport to="body">
<modal v-if="modal1.showModal" :modalDialogClass="modal1.modalDialogClass" @close="modal1.showModal = false">
<template v-slot:header>
<h3 class="modal-title">Le titre de ma modale</h3>
</template>
<template v-slot:body>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
</template>
<template v-slot:footer>
<button class="sc-button green" @click="modal1.showModal = false; modal2.showModal = true">
{{ $t('action.next')}}</button>
</template>
</modal>
</teleport>
<label>
<input type="checkbox" v-model="isAnonymous" :value="value" />
{{ $t('requestor.is_anonymous') }}
</label>
<teleport to="body">
<modal v-if="modal2.showModal" :modalDialogClass="modal2.modalDialogClass" @close="modal2.showModal = false">
<template v-slot:header>
<h3 class="modal-title">Une autre modale</h3>
</template>
<template v-slot:body>
<p>modal 2</p>
</template>
<template v-slot:footer>
<button class="sc-button green" @click="modal2.showModal = false">
{{ $t('action.save')}}</button>
</template>
</modal>
</teleport>
<!-- END TESTS -->
<button v-if="accompanyingCourse.requestor !== null"
class="sc-button bt-remove"
:title="$t('action.remove')"
@click="removeRequestor">
</button>
<add-persons v-if="accompanyingCourse.requestor === null"
buttonTitle="requestor.add_requestor"
modalTitle="requestor.add_requestor"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons"> <!-- to cast child method -->
</add-persons>
</div>
</template>
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal'
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue'
export default {
name: 'Requestor',
components: {
Modal,
AddPersons,
},
data() {
return {
modal1: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl" // modal-lg modal-md modal-sm
},
modal2: {
showModal: false,
modalDialogClass: "modal-dialog-centered modal-sm" // modal-lg modal-md modal-sm
addPersons: {
key: 'requestor',
options: {
type: ['person', 'thirdparty'],
priority: null,
uniq: true,
}
}
}
},
computed: {
accompanyingCourse() {
return this.$store.state.accompanyingCourse
},
isAnonymous: {
set(value) {
console.log('requestorIsAnonymous value',value);
this.$store.dispatch('requestorIsAnonymous', value);
},
get() {
return this.$store.state.accompanyingCourse.requestorAnonymous;
}
}
},
methods: {
removeRequestor() {
console.log('@@ CLICK remove requestor: item');
this.$store.dispatch('removeRequestor');
},
addNewPersons({ selected, modal }) {
console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach(function(item) {
this.$store.dispatch('addRequestor', item);
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
}
}
}

View File

@ -0,0 +1,81 @@
<template>
<div class="vue-component">
<h3>Tests</h3>
<!-- Modal -->
<ul class="record_actions">
<li>
<button class="sc-button bt-create" @click="modal1.showModal = true">
{{ $t('action.show_modal') }}
</button>
</li>
<li>
<button class="sc-button bt-create" @click="modal2.showModal = true">
Ouvrir une seconde modale
</button>
</li>
</ul>
<teleport to="body">
<modal v-if="modal1.showModal" :modalDialogClass="modal1.modalDialogClass" @close="modal1.showModal = false">
<template v-slot:header>
<h3 class="modal-title">Le titre de ma modale</h3>
</template>
<template v-slot:body>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus luctus facilisis suscipit. Cras pulvinar, purus sagittis pulvinar porta, enim ex posuere lacus, in pulvinar lectus magna in odio. Nullam iaculis congue lorem ac suscipit. Proin ut rutrum augue. Ut vehicula risus nec hendrerit ullamcorper. Ut volutpat eu mi eget viverra. Morbi dictum placerat suscipit. </p>
<p>Quisque non erat tincidunt, lacinia justo ut, pulvinar nisl. Nunc id enim ut sem pretium interdum consectetur eu quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam posuere erat eget augue finibus luctus. Maecenas auctor, tortor non luctus ultrices, neque neque porttitor ex, nec lacinia lorem ligula et elit. Sed tempor nulla vitae lorem sollicitudin dictum. Vestibulum nec arcu eget elit pulvinar pretium. Phasellus facilisis metus sed diam luctus, feugiat scelerisque velit dignissim.</p>
</template>
<template v-slot:footer>
<button class="sc-button green" @click="modal1.showModal = false; modal2.showModal = true">
{{ $t('action.next')}}</button>
</template>
</modal>
</teleport>
<teleport to="body">
<modal v-if="modal2.showModal" :modalDialogClass="modal2.modalDialogClass" @close="modal2.showModal = false">
<template v-slot:header>
<h3 class="modal-title">Une autre modale</h3>
</template>
<template v-slot:body>
<p>modal 2</p>
</template>
<template v-slot:footer>
<button class="sc-button green" @click="modal2.showModal = false">
{{ $t('action.save')}}</button>
</template>
</modal>
</teleport>
<!-- END Modal -->
</div>
</template>
<script>
import Modal from 'ChillMainAssets/vuejs/_components/Modal'
export default {
name: 'Test',
components: {
Modal,
},
data() {
return {
modal1: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl" // modal-lg modal-md modal-sm
},
modal2: {
showModal: false,
modalDialogClass: "modal-dialog-centered modal-sm" // modal-lg modal-md modal-sm
}
}
},
computed: {
}
}
</script>

View File

@ -9,19 +9,50 @@ const appMessages = {
closing_date: "Date de clôture",
remark: "Commentaire",
closing_motive: "Motif de clôture",
user: "TMS",
flags: "Indicateurs",
status: "État",
step: {
draft: "Brouillon",
open: "Ouvert",
active: "En file active"
},
emergency: "urgent",
confidential: "confidentiel",
},
persons_associated: {
title: "Usagers concernés",
counter: "Pas d'usager | 1 usager | {count} usagers",
firstname: "Prénom",
lastname: "Nom",
startdate: "Date d'entrée",
firstname: "Prénom",
lastname: "Nom",
startdate: "Date d'entrée",
enddate: "Date de sortie",
addPerson: "Ajouter un usager",
add_persons: "Ajouter des usagers",
},
requestor: {
title: "Demandeur",
add_requestor: "Ajouter un demandeur",
is_anonymous: "Le demandeur est anonyme",
type: "Type",
person_id: "id",
text: "Dénomination",
firstName: "Prénom",
lastName: "Nom",
birthdate: "Date de naissance",
center: "Centre",
phonenumber: "Téléphone",
mobilenumber: "Mobile",
altNames: "Autres noms",
address: "Adresse",
location: "Localité",
},
interlocutors: {
title: "Interlocuteurs privilégiés",
counter: "Pas d'interlocuteur | 1 interlocuteur | {count} interlocuteurs",
firstname: "Prénom",
lastname: "Nom",
add_interlocutors: "Ajouter des interlocuteurs",
}
}
};

View File

@ -1,11 +1,9 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import addPersons from './modules/addPersons'
import { getAccompanyingCourse, postParticipation } from '../api';
import { getAccompanyingCourse, postParticipation, postRequestor } from '../api';
const debug = process.env.NODE_ENV !== 'production';
const id = window.accompanyingCourseId; //tmp
const id = window.accompanyingCourseId;
let initPromise = getAccompanyingCourse(id)
.then(accompanying_course => new Promise((resolve, reject) => {
@ -13,7 +11,6 @@ let initPromise = getAccompanyingCourse(id)
const store = createStore({
strict: debug,
modules: {
addPersons
},
state: {
accompanyingCourse: accompanying_course,
@ -23,30 +20,36 @@ let initPromise = getAccompanyingCourse(id)
},
mutations: {
removeParticipation(state, item) {
//console.log('mutation: remove item', item.id);
//console.log('### mutation: remove item', item.id);
state.accompanyingCourse.participations = state.accompanyingCourse.participations.filter(participation => participation !== item);
},
closeParticipation(state, { participation, payload }) {
console.log('### mutation: close item', { participation, payload });
// trouve dans le state le payload et le supprime du state
state.accompanyingCourse.participations = state.accompanyingCourse.participations.filter(participation => participation !== payload);
// pousse la participation
state.accompanyingCourse.participations.push(participation);
//console.log('### mutation: close item', { participation, payload });
// find row position and replace by closed participation
state.accompanyingCourse.participations.splice(
state.accompanyingCourse.participations.findIndex(element => element === payload), 1, participation
);
},
addParticipation(state, participation) {
//console.log('### mutation: add participation', participation);
state.accompanyingCourse.participations.push(participation);
},
removeRequestor(state) {
state.accompanyingCourse.requestor = null;
},
addRequestor(state, requestor) {
console.log(requestor);
state.accompanyingCourse.requestor = requestor;
}
},
actions: {
removeParticipation({ commit }, payload) {
commit('removeParticipation', payload);
},
closeParticipation({ commit }, payload) {
//console.log('## action: fetch delete participation: payload', payload.person.id);
postParticipation(id, payload.person.id, 'DELETE')
console.log('## action: fetch delete participation: payload', payload);
postParticipation(id, payload.person.person_id, 'DELETE')
.then(participation => new Promise((resolve, reject) => {
//console.log('payload', payload);
commit('closeParticipation', { participation, payload });
resolve();
}))
@ -54,22 +57,43 @@ let initPromise = getAccompanyingCourse(id)
state.errorMsg.push(error.message);
});
},
addParticipation(addPersons, payload) {
//console.log('## action: fetch post participation: payload', payload.id);
postParticipation(id, payload.id, 'POST')
addParticipation({ commit }, payload) {
console.log('## action: fetch post participation (select item): payload', payload);
postParticipation(id, payload.result.person_id, 'POST')
.then(participation => new Promise((resolve, reject) => {
//console.log(participation, payload);
addPersons.commit('addParticipation', participation);
addPersons.commit('resetState', payload);
commit('addParticipation', participation);
resolve();
}))
.catch((error) => {
state.errorMsg.push(error.message);
});
},
removeRequestor({ commit }) {
postRequestor(id, null, 'DELETE')
.then(requestor => new Promise((resolve, reject) => {
commit('removeRequestor');
resolve();
}))
.catch((error) => {
state.errorMsg.push(error.message);
});
},
addRequestor({ commit }, payload) {
console.log(payload);
postRequestor(id, payload, 'POST')
.then(requestor => new Promise((resolve, reject) => {
commit('addRequestor', requestor);
resolve();
}))
.catch((error) => {
state.errorMsg.push(error.message);
});
},
requestorIsAnonymous({ commit }, payload) {
console.log('payload', payload);
}
}
});
//console.log('store object', store.state.accompanyingCourse.id);
resolve(store);
}));

View File

@ -1,76 +0,0 @@
import { searchPersons } from 'ChillPersonAssets/vuejs/_api/AddPersons'
import { postParticipation } from '../../api';
// initial state
const state = {
query: "",
suggested: [],
selected: []
}
// getters
const getters = {
selectedAndSuggested: state => {
const uniqBy = (a, key) => [
...new Map(
a.map(x => [key(x), x])
).values()
];
let union = [...new Set([
...state.suggested.slice().reverse(),
...state.selected.slice().reverse(),
])];
return uniqBy(union, k => k.id);
}
}
// mutations
const mutations = {
setQuery(state, query) {
//console.log('q=', query);
state.query = query;
},
loadSuggestions(state, suggested) {
state.suggested = suggested;
},
updateSelected(state, value) {
state.selected = value;
},
resetState(state, selected) {
//console.log('avant', state.selected);
state.selected = state.selected.filter(value => value !== selected);
//console.log('après', state.selected);
state.query = "";
state.suggested = [];
}
}
// actions
const actions = {
setQuery({ commit }, payload) {
//console.log('## action: setquery: payload', payload);
commit('setQuery', payload.query);
if (payload.query.length >= 3) {
searchPersons(payload.query)
.then(suggested => new Promise((resolve, reject) => {
commit('loadSuggestions', suggested.results);
resolve();
}));
} else {
commit('loadSuggestions', []);
}
},
updateSelected({ commit }, payload) {
//console.log('## action: update selected values: payload', payload);
commit('updateSelected', payload);
}
}
export default {
//namespaced: true,
state,
getters,
actions,
mutations
}

View File

@ -1,15 +1,23 @@
const
locale = 'fr',
format = 'json'
;
/*
* Build query string with query and options
*/
const parametersToString = ({ query, options }) => {
let types ='';
options.type.forEach(function(type) {
types += '&type[]=' + type;
});
return 'q=' + query + types;
};
/*
* Endpoint chill_person_search, method GET, get a list of persons
* Endpoint chill_person_search
* method GET, get a list of persons
*
* @query string - the query to search for
*/
let searchPersons = (query) => {
let url = `/${locale}/search.${format}?name=person_regular&q=${query}`;
const searchPersons = ({ query, options }) => {
let queryStr = parametersToString({ query, options });
let url = `/fr/search.json?name=person_regular&${queryStr}`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
@ -17,4 +25,22 @@ let searchPersons = (query) => {
});
};
export { searchPersons };
/*
* Endpoint v.2 chill_main_search_global
* method GET, get a list of persons and thirdparty
*
* NOTE: this is a temporary WIP endpoint, return inconsistent random results
* @query string - the query to search for
*/
const searchPersons_2 = ({ query, options }) => {
let queryStr = parametersToString({ query, options });
let url = `/api/1.0/search.json?${queryStr}`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
export { searchPersons, searchPersons_2 };

View File

@ -1,73 +1,87 @@
<template>
<button class="sc-button bt-create centered mt-4" @click="openModal">
{{ $t('add_persons.search_add_others_persons') }}
{{ $t(buttonTitle) }}
</button>
<teleport to="body">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
<template v-slot:header>
<h3 class="modal-title">{{ $t('add_persons.title') }}</h3>
<h3 class="modal-title">{{ $t(modalTitle) }}</h3>
</template>
<template v-slot:body-fixed>
<div class="search">
<label style="float: right;">
{{ $tc('add_persons.suggested_counter', suggestedCounter) }}
</label>
<input id="search-persons"
name="query"
v-model="query"
:placeholder="$t('add_persons.search_some_persons')"
ref="search" />
<i class="fa fa-search fa-lg"></i>
<template v-slot:body-head>
<div class="modal-body">
<div class="search">
<label style="float: right;">
{{ $tc('add_persons.suggested_counter', suggestedCounter) }}
</label>
<input id="search-persons"
name="query"
v-model="query"
:placeholder="$t('add_persons.search_some_persons')"
ref="search" />
<i class="fa fa-search fa-lg"></i>
<i class="fa fa-times" v-if="queryLength >= 3" @click="resetSuggestion"></i>
</div>
</div>
</template>
<template v-slot:body>
<!--span class="discret">Selection: {{ selected }}</span-->
<div class="results">
<div class="modal-body">
<div class="count">
<span>
<a v-if="suggestedCounter > 0" href="#">
{{ $t('action.check_all')}}</a>
<a v-if="selectedCounter > 0" href="#">
{{ $t('action.reset')}}</a>
<a v-if="suggestedCounter > 2" @click="selectAll">
{{ $t('action.check_all')}}
</a>
<a v-if="selectedCounter > 0" @click="resetSelection">
<i v-if="suggestedCounter > 2"> </i>
{{ $t('action.reset')}}
</a>
</span>
<span v-if="selectedCounter > 0">
{{ $tc('add_persons.selected_counter', selectedCounter) }}
</span>
</div>
<person-suggestion
</div>
</template>
<template v-slot:body>
<!--span class="discret">Selection: {{ selected }}</span-->
<div class="results">
<person-suggestion
v-for="item in this.selectedAndSuggested.slice().reverse()"
v-bind:item="item"
v-bind:key="item.id">
v-bind:key="itemKey(item)"
v-bind:item="item"
v-bind:search="search"
@updateSelected="updateSelected">
</person-suggestion>
<button v-if="query.length >= 3" class="sc-button bt-create ml-5 mt-2" name="createPerson">
{{ $t('action.create') }} "{{ query }}"
</button>
</div>
</template>
<template v-slot:footer>
<button class="sc-button green" @click="addNewPersons">
<button class="sc-button green"
@click.prevent="$emit('addNewPersons', { selected, modal })">
<i class="fa fa-plus fa-fw"></i>{{ $t('action.add')}}
</button>
{{ $t(checkUniq) }}
</template>
</modal>
</teleport>
</template>
<script>
import { mapState } from 'vuex';
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import PersonSuggestion from 'ChillPersonAssets/vuejs/_components/PersonSuggestion';
import { searchPersons, searchPersons_2 } from 'ChillPersonAssets/vuejs/_api/AddPersons';
export default {
name: 'AddPersons',
@ -75,40 +89,69 @@ export default {
Modal,
PersonSuggestion,
},
props: [
'buttonTitle',
'modalTitle',
'options'
],
emits: ['addNewPersons'],
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
},
search: {
query: "",
suggested: [],
selected: []
}
}
},
computed: {
...mapState({
addPersons: state => state.addPersons
}),
query: {
set(query) {
this.$store.dispatch('setQuery', { query });
return this.setQuery(query);
},
get() {
return this.addPersons.query;
return this.search.query;
}
},
queryLength() {
return this.search.query.length;
},
suggested() {
return this.addPersons.suggested;
return this.search.suggested;
},
suggestedCounter() {
return this.addPersons.suggested.length;
},
return this.search.suggested.length;
},
selected() {
return this.addPersons.selected;
},
return this.search.selected;
},
selectedCounter() {
return this.addPersons.selected.length;
return this.search.selected.length;
},
selectedAndSuggested() {
return this.$store.getters.selectedAndSuggested;
const uniqBy = (a, key) => [
...new Map(
a.map(x => [key(x), x])
).values()
];
let union = [...new Set([
...this.suggested.slice().reverse(),
...this.selected.slice().reverse(),
])];
return uniqBy(union, k => k.key);
},
options() {
return this.options;
},
checkUniq() {
if (this.options.uniq === true && this.selectedCounter > 1) {
return "error_only_one_person";
}
return '';
}
},
methods: {
@ -118,15 +161,47 @@ export default {
this.$refs.search.focus();
})
},
addNewPersons() {
console.log('@@@ CLICK button addPersons')
this.selected.forEach(function(item) {
//console.log('# dispatch action for each item', item);
this.$store.dispatch('addParticipation', item);
}, this
);
this.modal.showModal = false;
setQuery(query) {
this.search.query = query;
if (query.length >= 3) {
searchPersons_2({ query, options: this.options })
.then(suggested => new Promise((resolve, reject) => {
this.loadSuggestions(suggested.results);
resolve();
}));
} else {
this.loadSuggestions([]);
}
},
loadSuggestions(suggested) {
this.search.suggested = suggested;
this.search.suggested.forEach(function(item) {
item.key = this.itemKey(item);
}, this);
},
updateSelected(value) {
this.search.selected = value;
},
resetSearch() {
this.resetSelection();
this.resetSuggestion();
},
resetSuggestion() {
this.search.query = "";
this.search.suggested = [];
},
resetSelection() {
this.search.selected = [];
},
selectAll() {
this.search.suggested.forEach(function(item) {
this.search.selected.push(item);
}, this);
},
itemKey(item) {
let type = item.result.type;
return type + item.result[type + '_id'];
}
}
},
}
</script>

View File

@ -1,53 +1,52 @@
<template>
<div class="list-item" :class="{ checked: isChecked }">
<div class="container">
<!--a class="discret" target="_blank" :href="url.show">{{ item.id }}</a-->
<input class=""
type="checkbox"
v-model="selected"
:value="item" />
<div class="container">
<input type="checkbox"
v-model="selected"
:value="item" />
</div>
{{ item.text }}
<suggestion-person
v-if="item.result.type === 'person'"
v-bind:item="item">
</suggestion-person>
</div>
<div class="right_actions">
<span class="badge badge-pill badge-secondary" :title="item.id">
{{ $t('item.type_person') }}
</span>
<a class="sc-button bt-show" target="_blank" :title="item.id" :href="url.show"></a>
</div>
<suggestion-third-party
v-if="item.result.type === 'thirdparty'"
v-bind:item="item">
</suggestion-third-party>
</div>
</template>
<script>
import { mapState } from 'vuex';
import SuggestionPerson from './PersonSuggestion/Person';
import SuggestionThirdParty from './PersonSuggestion/ThirdParty';
export default {
name: 'PersonSuggestion',
props: ['item'],
data() {
return {
url: {
show: '/fr/person/' + this.item.id + '/general',
edit: '/fr/person/' + this.item.id + '/general/edit'
}
}
components: {
SuggestionPerson,
SuggestionThirdParty,
},
props: [
'item',
'search'
],
emits: ['updateSelected'],
computed: {
selected: {
set(value) {
this.$store.dispatch('updateSelected', value);
this.$emit('updateSelected', value);
},
get() {
return this.$store.state.addPersons.selected;
return this.search.selected;
}
},
isChecked() {
return (this.selected.indexOf(this.item) === -1) ? false : true;
return (this.search.selected.indexOf(this.item) === -1) ? false : true;
}
}
},
};
</script>

View File

@ -0,0 +1,35 @@
<template>
<div class="container">
<span class="name">
{{ item.result.text }}
</span>
<span class="birthday">
{{ $d(item.result.birthdate.datetime, 'short') }}
</span>
</div>
<div class="right_actions">
<span class="badge badge-pill badge-secondary" :title="item.key">
{{ $t('item.type_person') }}
</span>
<a class="sc-button bt-show" target="_blank" :title="item.key" :href="url.show"></a>
</div>
</template>
<script>
export default {
name: 'SuggestionPerson',
props: ['item'],
data() {
return {
url: {
show: '/fr/person/' + this.item.result.person_id + '/general',
edit: '/fr/person/' + this.item.result.person_id + '/general/edit'
},
}
},
}
</script>

View File

@ -0,0 +1,36 @@
<template>
<div class="container">
<span class="name">
{{ item.result.text }}
</span>
<span class="location">
{{ item.result.address.text }} -
{{ item.result.address.postcode.name }}
</span>
</div>
<div class="right_actions">
<span class="badge badge-pill badge-secondary" :title="item.key">
{{ $t('item.type_thirdparty') }}
</span>
<a class="sc-button bt-show" target="_blank" :title="item.key" :href="url.show"></a>
</div>
</template>
<script>
export default {
name: 'SuggestionThirdParty',
props: ['item'],
data() {
return {
url: {
show: '/fr/thirdparty/thirdparty/' + this.item.result.thirdparty_id + '/show',
edit: '/fr/thirdparty/thirdparty/' + this.item.result.thirdparty_id + '/edit'
},
}
},
}
</script>

View File

@ -1,7 +1,6 @@
const personMessages = {
fr: {
add_persons: {
search_add_others_persons: "Rechercher et ajouter d'autres usagers",
title: "Ajouter des usagers",
suggested_counter: "Pas de résultats | 1 résultat | {count} résultats",
selected_counter: " 1 sélectionné | {count} sélectionnés",
@ -9,10 +8,11 @@ const personMessages = {
},
item: {
type_person: "Usager",
type_tms: "TMS",
type_3rdparty: "Tiers",
type_menage: "Ménage"
}
type_user: "TMS",
type_thirdparty: "Tiers",
type_household: "Ménage"
},
error_only_one_person: "Une seule personne peut être sélectionnée !"
}
};

View File

@ -38,6 +38,8 @@ class AccompanyingPeriodNormalizer implements NormalizerInterface, NormalizerAwa
'remark' => $period->getRemark(),
'participations' => $this->normalizer->normalize($period->getParticipations(), $format),
'closingMotive' => $this->normalizer->normalize($period->getClosingMotive(), $format),
'requestor' => $this->normalizer->normalize($period->getRequestor(), $format),
'requestorAnonymous' => $this->normalizer->normalize($period->isRequestorAnonymous(), $format),
'user' => $this->normalizer->normalize($period->getUser(), $format),
'step' => $period->getStep(),
'origin' => $this->normalizer->normalize($period->getOrigin(), $format),

View File

@ -25,7 +25,7 @@ use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Chill\PersonBundle\Repository\PersonRepository;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
/**
* Serialize a Person entity
@ -41,38 +41,58 @@ class PersonNormalizer implements
protected PersonRepository $repository;
private ChillEntityRenderExtension $render;
public const GET_PERSON = 'get_person';
public function __construct(PersonRepository $repository)
public function __construct(PersonRepository $repository, ChillEntityRenderExtension $render)
{
$this->repository = $repository;
$this->render = $render;
}
public function normalize($person, string $format = null, array $context = array())
{
/** @var Person $person */
return [
'id' => $person->getId(),
'type' => 'person',
'person_id' => $person->getId(),
'text' => $this->render->renderString($person),
'firstName' => $person->getFirstName(),
'lastName' => $person->getLastName(),
'birthdate' => $this->normalizer->normalize($person->getBirthdate()),
'center' => $this->normalizer->normalize($person->getCenter())
'center' => $this->normalizer->normalize($person->getCenter()),
'phonenumber' => $person->getPhonenumber(),
'mobilenumber' => $person->getMobilenumber(),
'altNames' => $this->normalizeAltNames($person->getAltNames())
];
}
protected function normalizeAltNames($altNames): array
{
$r = [];
foreach ($altNames as $n) {
$r[] = [ 'key' => $n->getKey(), 'label' => $n->getLabel() ];
}
return $r;
}
public function denormalize($data, string $type, string $format = null, array $context = []): Person
{
if ($context[self::GET_PERSON] ?? true) {
$id = $data['id'] ?? null;
$id = $data['person_id'] ?? null;
if (NULL === $id) {
throw new RuntimeException("missing id into person object");
throw new RuntimeException("missing person_id into person object");
}
}
/** var Person $person */
$person = $this->repository->findOneById($id);
if (NULL === $person) {
return UnexpectedValueException("person id not found");
throw new UnexpectedValueException("person id not found");
}
return $person;

View File

@ -27,6 +27,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Center;
use Doctrine\Common\Collections\Criteria;
@ -85,6 +86,116 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$this->assertEquals(404, $response->getStatusCode(), "Test that the response of rest api has a status code 'not found' (404)");
}
/**
* @dataProvider dataGenerateRandomRequestorValidData
*/
public function testRequestorWithValidData(AccompanyingPeriod $period, $personId, $thirdPartyId)
{
$em = self::$container->get(EntityManagerInterface::class);
// post a person
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'person_id' => $personId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode());
$this->assertArrayHasKey('person_id', $data);
$this->assertEquals($personId, $data['person_id']);
// check into database
$period = $em->getRepository(AccompanyingPeriod::class)
->find($period->getId());
$this->assertInstanceOf(Person::class, $period->getRequestor());
$this->assertEquals($personId, $period->getRequestor()->getId());
// post a third party
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'thirdparty_id' => $thirdPartyId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
$this->assertEquals(200, $response->getStatusCode());
$this->assertArrayHasKey('thirdparty_id', $data);
$this->assertEquals($thirdPartyId, $data['thirdparty_id']);
// check into database
$period = $em->getRepository(AccompanyingPeriod::class)
->find($period->getId());
$this->assertInstanceOf(ThirdParty::class, $period->getRequestor());
$this->assertEquals($thirdPartyId, $period->getRequestor()->getId());
// remove the requestor
$this->client->request(
Request::METHOD_DELETE,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId())
);
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
// check into database
$period = $em->getRepository(AccompanyingPeriod::class)
->find($period->getId());
$em->refresh($period);
$this->assertNull($period->getRequestor());
}
public function dataGenerateRandomRequestorValidData(): \Iterator
{
$dataLength = 2;
$maxResults = 100;
static::bootKernel();
$em = static::$container->get(EntityManagerInterface::class);
$center = $em->getRepository(Center::class)
->findOneBy(array('name' => 'Center A'));
$personIds = $em->createQuery("SELECT p.id FROM ".
Person::class." p ".
" WHERE p.center = :center")
->setParameter('center', $center)
->setMaxResults($maxResults)
->getScalarResult();
// create a random order
shuffle($personIds);
$thirdPartyIds = $em->createQuery("SELECT t.id FROM ".
ThirdParty::class." t ")
->setMaxResults($maxResults)
->getScalarResult();
// create a random order
shuffle($thirdPartyIds);
$i = 0;
while ($i <= $dataLength) {
$person = $em->getRepository(Person::class)
->find(\array_pop($personIds)['id']);
if (count($person->getAccompanyingPeriods()) === 0) {
continue;
}
$period = $person->getAccompanyingPeriods()[0];
yield [$period, \array_pop($personIds)['id'], \array_pop($thirdPartyIds)['id'] ];
$i++;
}
}
/**
*
* @dataProvider dataGenerateRandomAccompanyingCourse
@ -97,7 +208,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'id' => $personId ])
\json_encode([ 'person_id' => $personId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
@ -116,7 +227,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
// check that the person id is contained
$participationsPersonsIds = \array_map(
function($participation) { return $participation->person->id; },
function($participation) { return $participation->person->person_id; },
$data->participations);
$this->assertContains($personId, $participationsPersonsIds);
@ -128,7 +239,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
[], // parameters
[], // files
[], // server parameters
\json_encode([ 'id' => $personId ])
\json_encode([ 'person_id' => $personId ])
);
$response = $this->client->getResponse();
$data = \json_decode($response->getContent(), true);
@ -211,4 +322,5 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$nbGenerated++;
}
}
}

View File

@ -24,6 +24,7 @@ namespace Chill\PersonBundle\Tests\Entity;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
{
@ -104,4 +105,30 @@ class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
$participation = $period->getOpenParticipationContainsPerson($person);
$this->assertNull($participation);
}
public function testRequestor()
{
$period = new AccompanyingPeriod(new \DateTime());
$person = new Person();
$thirdParty = new ThirdParty();
$this->assertNull($period->getRequestorThirdParty());
$this->assertNull($period->getRequestorPerson());
$this->assertNull($period->getRequestor());
$period->setRequestor($person);
$this->assertNull($period->getRequestorThirdParty());
$this->assertSame($person, $period->getRequestorPerson());
$this->assertSame($person, $period->getRequestor());
$period->setRequestor($thirdParty);
$this->assertNull($period->getRequestorPerson());
$this->assertSame($thirdParty, $period->getRequestorThirdParty());
$this->assertSame($thirdParty, $period->getRequestor());
$period->setRequestor(NULL);
$this->assertNull($period->getRequestorThirdParty());
$this->assertNull($period->getRequestorPerson());
$this->assertNull($period->getRequestor());
}
}

View File

@ -0,0 +1,91 @@
components:
schemas:
PersonById:
type: object
properties:
person_id:
type: integer
required:
- person_id
ThirdPartyById:
type: object
properties:
thirdparty_id:
type: integer
required:
- thirdparty_id
paths:
/1.0/person/accompanying-course/{id}.json:
get:
tags:
- person
summary: "Return the description for an accompanying course (accompanying period)"
parameters:
- name: id
in: path
required: true
description: The accompanying period's id
schema:
type: integer
format: integer
minimum: 1
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
/1.0/person/accompanying-course/{id}/requestor.json:
post:
tags:
- person
summary: "Add a requestor to the accompanying course"
parameters:
- name: id
in: path
required: true
description: The accompanying period's id
schema:
type: integer
format: integer
minimum: 1
requestBody:
description: "A person or thirdparty"
required: true
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/PersonById'
- $ref: '#/components/schemas/ThirdPartyById'
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
delete:
tags:
- person
summary: "Remove the requestor for the accompanying course"
parameters:
- name: id
in: path
required: true
description: The accompanying period's id
schema:
type: integer
format: integer
minimum: 1
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"

View File

@ -3,6 +3,7 @@ services:
Chill\PersonBundle\Serializer\Normalizer\PersonNormalizer:
arguments:
$repository: '@Chill\PersonBundle\Repository\PersonRepository'
$render: '@Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension'
tags:
- { name: 'serializer.normalizer', priority: 64 }

View File

@ -0,0 +1,122 @@
<?php
namespace Chill\ThirdPartyBundle\DataFixtures\ORM;
use Chill\MainBundle\DataFixtures\ORM\LoadCenters;
use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes;
use Chill\MainBundle\Entity\PostalCode;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Nelmio\Alice\Loader\NativeLoader;
use Nelmio\Alice\ObjectSet;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\MainBundle\Entity\Address;
use Doctrine\ORM\EntityManagerInterface;
class LoadThirdParty extends Fixture Implements DependentFixtureInterface
{
public function load(ObjectManager $manager)
{
$thirdParties = $this->getThirdParties()->getObjects();
foreach ($thirdParties as $name => $thirdParty) {
if ('a' === $name[0]) {
// this is an address
continue;
}
foreach ($this->getCenters() as $center) {
$thirdParty->addCenter($center);
}
$manager->persist($thirdParty);
}
$manager->flush();
}
private function getCenters(): \Iterator
{
$references = \array_map(function($a) { return $a['ref']; },
LoadCenters::$centers);
$number = random_int(1, count($references));
if ($number === 1) {
yield $this->getReference($references[\array_rand($references)]);
} else {
foreach (array_rand($references, $number) as $index) {
yield $this->getReference($references[$index]);
}
}
}
public function getDependencies()
{
return [
LoadCenters::class,
LoadPostalCodes::class
];
}
private function getThirdParties(): ObjectSet
{
$loader = new NativeLoader();
$objectSet = $loader->loadData([
Address::class => [
'address{1..75}' => [
'street' => '<fr_FR:streetName()>',
'streetNumber' => '<fr_FR:buildingNumber()>',
'validFrom' => '<dateTimeBetween(\'-1 year\', \'now\')>',
'postCode' => $this->getPostalCode()
],
],
ThirdParty::class => [
'thirdparty{1..75}' => [
'name' => '<fr_FR:company()>',
'telephone' => '<fr_FR:phonenumber()>',
'email' => '<email()>',
'comment' => '<fr_FR:realTextBetween(10, 500)>',
'address' => '@address<current()>'
]
]
]);
return $objectSet;
}
private function getPostalCode(): PostalCode
{
$ref = LoadPostalCodes::$refs[\array_rand(LoadPostalCodes::$refs)];
return $this->getReference($ref);
if (count($this->postalCodesIds) === 0) {
// fill the postal codes
$this->em->createQuery('SELECT p.id FROM '.PostalCode::class)
->getScalarResult();
}
$id = $this->postalCodesIds[\array_rand($this->postalCodesIds)];
return $this->em->getRepository(PostalCode::class)
->find($id);
}
private function createAddress(): ObjectSet
{
$loader = new NativeLoader();
$objectSet = $loader->loadData([
Address::class => [
'address1' => [
'name' => '<fr_FR:company()>',
'telephone' => '<fr_FR:phonenumber()>',
'email' => '<email()>',
'comment' => '<fr_FR:realTextBetween(10, 500)>'
]
]
]);
return $objectSet;
}
}

View File

@ -33,6 +33,8 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte
$loader->load('services/search.yaml');
$loader->load('services/templating.yaml');
$loader->load('services/menu.yaml');
$loader->load('services/fixtures.yaml');
$loader->load('services/serializer.yaml');
}
public function prepend(ContainerBuilder $container)

View File

@ -4,13 +4,18 @@ namespace Chill\ThirdPartyBundle\Repository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Query;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
/**
* ThirdPartyRepository
*
*/
class ThirdPartyRepository extends \Doctrine\ORM\EntityRepository
class ThirdPartyRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ThirdParty::class);
}
/**
* count amongst parties associated to $centers, with $terms parameters
*

View File

@ -0,0 +1,70 @@
<?php
namespace Chill\ThirdPartyBundle\Serializer\Normalizer;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
class ThirdPartyNormalizer implements NormalizerInterface, DenormalizerInterface, NormalizerAwareInterface
{
private ThirdPartyRepository $repository;
use NormalizerAwareTrait;
public function __construct(ThirdPartyRepository $repository)
{
$this->repository= $repository;
}
public function normalize($thirdParty, string $format = null, array $context = [])
{
/** @var $thirdParty ThirdParty */
$data['type'] = 'thirdparty';
// TODO should be replaced by a "render entity"
$data['text'] = $thirdParty->getName();
$data['thirdparty_id'] = $thirdParty->getId();
$data['address'] = $this->normalizer->normalize($thirdParty->getAddress(), $format,
[ 'address_rendering' => 'short' ]);
return $data;
}
public function supportsNormalization($data, string $format = null)
{
return $data instanceof ThirdParty;
}
public function denormalize($data, string $type, string $format = null, array $context = [])
{
$thirdParty = $context[AbstractNormalizer::OBJECT_TO_POPULATE] ?? NULL;
if (NULL === $thirdParty) {
$id = $data['thirdparty_id'] ?? NULL;
if (NULL === $id) {
throw new RuntimeException("mission thirdparty_id into body");
}
$thirdParty = $this->repository->findOneById($id);
if (NULL === $thirdParty) {
throw new UnexpectedValueException("thirdparty not found");
}
}
return $thirdParty;
}
public function supportsDenormalization($data, string $type, string $format = null)
{
return ThirdParty::class === $type;
}
}

View File

@ -1 +1,13 @@
---
services:
Chill\ThirdPartyBundle\Serializer\Normalizer\:
autowire: true
resource: '../Serializer/Normalizer/'
tags:
- { name: 'serializer.normalizer', priority: 64 }
Chill\ThirdPartyBundle\Repository\:
autowire: true
resource: '../Repository/'
tags:
- { name: 'doctrine.repository_service' }

View File

@ -0,0 +1,5 @@
---
services:
Chill\ThirdPartyBundle\DataFixtures\ORM\LoadThirdParty:
tags:
- { 'name': doctrine.fixture.orm }

View File

@ -0,0 +1 @@
services: