Merge remote-tracking branch 'origin/master' into issue519_filiation_create_person

This commit is contained in:
Julien Fastré 2022-03-24 11:12:56 +01:00
commit f9d87876f2
58 changed files with 958 additions and 170 deletions

View File

@ -11,6 +11,15 @@ and this project adheres to
## Unreleased
<!-- write down unreleased development here -->
* [ACL] fix allow to see the course, event if the scope'course does not contains the scope's user
* [search] enforce limit of results for fetching rsults by search api https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/576
* [activity] Fix delete button for document (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/554)
* [activity] Add return path the document generation (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/553)
* [person] add person ressource to person docgen normaliser (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/517)
* [person] AccompanyingCourseWorkEdit: fix deleting evaluation documents (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/546)
* [person] AccompanyingCourseWorkEdit: download existing documents (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/512)
* [person] AccompanyingCourseWorkEdit: replace document by a new one (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/511)
* [person] AccompanyingPeriodWork: add referrers to work, add doctrine event listener to add logged user to referrers collection and display a referrers list in work list (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/502)
* [person] AccompanyingPeriodWorkEvaluation: fix circular reference when serialising (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/495)
* [person] order accompanying period by opening date in search persons, person and household period lists (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/493)
* [parcours] autosave of the pinned comment for draft accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/477)
@ -46,9 +55,13 @@ and this project adheres to
* [contact] add contact button color changed plus the pipe at the side removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/506)
* [household] create-edit household composition placed in separate page to avoid confusion (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/505)
* [blur] Improved positioning of toggle icon (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/486)
* [parcours] List of parcours for a specific user so they can be reassigned in case of absence (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/509)
* [thirdparty] Thirdparty view page, english text translated (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/534)
* [social_action] Translation changed in evaluation section (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/512)
* [filiation] Possible to add person (or create onthefly) to add to filiation graph + add relation (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/519)
* [household] Within parcours listing page of household add create button (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/560)
* [person_resource] bugfix when adding thirdparty or freetext resource (no issue)
* [aside_activity] style correction + sticky-form create button (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/529)
## Test releases

View File

@ -250,11 +250,6 @@ parameters:
count: 1
path: src/Bundle/ChillMainBundle/Entity/Address.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1
path: src/Bundle/ChillMainBundle/Entity/Embeddable/CommentEmbeddable.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1

View File

@ -179,14 +179,15 @@ final class ActivityController extends AbstractController
{
$view = null;
[$person, $accompanyingPeriod] = $this->getEntity($request);
$entity = $this->activityRepository->find($id);
if (null === $entity) {
throw $this->createNotFoundException('Unable to find Activity entity.');
}
$accompanyingPeriod = $entity->getAccompanyingPeriod();
$person = $entity->getPerson();
if ($entity->getAccompanyingPeriod() instanceof AccompanyingPeriod) {
$view = 'ChillActivityBundle:Activity:editAccompanyingCourse.html.twig';
$accompanyingPeriod = $entity->getAccompanyingPeriod();
@ -220,6 +221,9 @@ final class ActivityController extends AbstractController
$this->entityManager->persist($entity);
$this->entityManager->flush();
$params = $this->buildParamsToUrl($person, $accompanyingPeriod);
$params['id'] = $entity->getId();
if ($form->has('gendocTemplateId') && null !== $form['gendocTemplateId']->getData()) {
return $this->redirectToRoute(
'chill_docgenerator_generate_from_template',
@ -227,15 +231,13 @@ final class ActivityController extends AbstractController
'template' => $form->get('gendocTemplateId')->getData(),
'entityClassName' => Activity::class,
'entityId' => $entity->getId(),
'returnPath' => $this->generateUrl('chill_activity_activity_edit', $params),
]
);
}
$this->addFlash('success', $this->get('translator')->trans('Success : activity updated!'));
$params = $this->buildParamsToUrl($person, $accompanyingPeriod);
$params['id'] = $entity->getId();
return $this->redirectToRoute('chill_activity_activity_show', $params);
}
@ -444,6 +446,9 @@ final class ActivityController extends AbstractController
'template' => $form->get('gendocTemplateId')->getData(),
'entityClassName' => Activity::class,
'entityId' => $entity->getId(),
'returnPath' => $this->generateUrl('chill_activity_activity_edit', [
'id' => $entity->getId(),
]),
]
);
}

View File

@ -305,6 +305,7 @@ class ActivityType extends AbstractType
'label' => $activityType->getLabel('documents'),
'required' => $activityType->isRequired('documents'),
'allow_add' => true,
'allow_delete' => true,
'button_add_label' => 'activity.Insert a document',
'button_remove_label' => 'activity.Remove a document',
'empty_collection_explain' => 'No documents',

View File

@ -0,0 +1,16 @@
div.chill-dropzone__below-zone {
a.btn-delete {
display: none;
}
}
// do it in js does not work
// document.addEventListener('DOMContentLoaded', e => {
// const dropzoneBelow = document.querySelectorAll('div.chill-dropzone__below-zone');
// dropzoneBelow.forEach(
// d => {
// const a = d.querySelector('a.btn-delete');
// d.removeChild(a);
// }
// )
// });

View File

@ -30,4 +30,5 @@
{{ parent() }}
{{ encore_entry_link_tags('mod_async_upload') }}
{{ encore_entry_link_tags('vue_activity') }}
{{ encore_entry_link_tags('page_edit_activity') }}
{% endblock %}

View File

@ -7,5 +7,7 @@ module.exports = function(encore, entries)
ChillActivityAssets: __dirname + '/Resources/public'
});
encore.addEntry('page_edit_activity', __dirname + '/Resources/public/page/edit_activity/index.scss');
encore.addEntry('vue_activity', __dirname + '/Resources/public/vuejs/Activity/index.js');
};

View File

@ -40,7 +40,7 @@
{% endif %}
</div>
</div>
<div class="item-col">
<div class="item-col" style="justify-content: flex-end;">
<div class="box">
<ul class="list-content fa-ul">
<li>
@ -79,7 +79,7 @@
{{ chill_pagination(paginator) }}
<ul class="record_actions">
<ul class="record_actions sticky-form-buttons">
<li>
<a href="{{ chill_path_add_return_path('chill_crud_aside_activity_new') }}" class="btn btn-create">
{{ 'Create'|trans }}

View File

@ -61,13 +61,13 @@ class StoredObject implements AsyncFileInterface, Document
/**
* @var int[]
* @ORM\Column(type="json", name="iv")
* @Serializer\Groups({"write"})
* @Serializer\Groups({"read", "write"})
*/
private array $iv = [];
/**
* @ORM\Column(type="json", name="key")
* @Serializer\Groups({"write"})
* @Serializer\Groups({"read", "write"})
*/
private array $keyInfos = [];

View File

@ -73,6 +73,7 @@ var download = (button) => {
button.type = mimeType;
button.textContent = labelReady;
if (hasFilename) {
button.download = filename;
if (extension !== false) {
button.download = button.download + '.' + extension;
@ -92,4 +93,4 @@ window.addEventListener('load', function(e) {
initializeButtons(e.target);
});
module.exports = initializeButtons;
export { initializeButtons, download };

View File

@ -1,6 +1,6 @@
var algo = 'AES-CBC';
import Dropzone from 'dropzone';
var initializeDownload = require('./downloader.js');
import { initializeButtons } from './downloader.js';
/**
@ -359,7 +359,7 @@ var insertDownloadButton = (zone, zoneData) => {
addBelowButton(newButton, zone, zoneData);
//zone.appendChild(newButton);
initializeDownload(zone);
initializeButtons(zone);
};
window.addEventListener('load', function(e) {

View File

@ -1,5 +1,5 @@
<template>
<a class="btn btn-create" :title="$t(buttonTitle)" @click="openModal">
<a :class="btnClasses" :title="$t(buttonTitle)" @click="openModal">
<span>{{ $t(buttonTitle) }}</span>
</a>
<teleport to="body">
@ -96,10 +96,27 @@ export default {
Modal
},
i18n,
props: [
'buttonTitle',
'options'
],
props: {
buttonTitle: {
type: String,
default: 'Ajouter un document',
},
options: {
type: Object,
default: {
maxFiles: 1,
maxPostSize: 262144000, // 250MB
required: false,
}
},
btnClasses: {
type: Object,
default: {
btn: true,
'btn-create': true
}
}
},
emits: ['addDocument'],
data() {
return {

View File

@ -0,0 +1,45 @@
<template>
<a
class="btn btn-download"
:title="$t(buttonTitle)"
:data-key=JSON.stringify(storedObject.keyInfos)
:data-iv=JSON.stringify(storedObject.iv)
:data-mime-type=storedObject.type
:data-label-preparing="$t('dataLabelPreparing')"
:data-label-ready="$t('dataLabelReady')"
:data-temp-url-get-generator="url"
@click.once="downloadDocument">
</a>
</template>
<script>
import { download } from '../../module/async_upload/downloader';
const i18n = {
messages: {
fr: {
dataLabelPreparing: "Chargement...",
dataLabelReady: "",
}
}
};
export default {
name: "AddAsyncUploadDownloader",
i18n,
props: [
'buttonTitle',
'storedObject'
],
computed: {
url() {
return `/asyncupload/temp_url/generate/GET?object_name=${this.storedObject.filename}`;
}
},
methods: {
downloadDocument(e) {
download(e.target);
}
}
}
</script>

View File

@ -26,6 +26,7 @@ use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use function count;
use function strlen;
class LoadPostalCodesCommand extends Command
{
@ -118,11 +119,11 @@ class LoadPostalCodesCommand extends Command
private function addPostalCode($row, OutputInterface $output)
{
if($row[2] == 'FR' && strlen($row[0]) == 4) {
if ('FR' === $row[2] && strlen($row[0]) === 4) {
// CP in FRANCE are on 5 digit
// For CP starting with a zero, the starting zero can be remove if stored as number in a csv
// add a zero if CP from FR and on 4 digit
$row[0] = '0'.$row[0];
$row[0] = '0' . $row[0];
}
if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {

View File

@ -114,6 +114,11 @@ class ChillMainExtension extends Extension implements
$config['available_countries']
);
$container->setParameter(
'chill_main.access_global_history',
$config['access_global_history']
);
$container->setParameter(
'chill_main.routing.resources',
$config['routing']['resources']

View File

@ -113,6 +113,9 @@ class Configuration implements ConfigurationInterface
->end()
->end()
->end()
->booleanNode('access_global_history')
->defaultTrue()
->end()
->arrayNode('redis')
->children()
->scalarNode('host')

View File

@ -61,7 +61,7 @@ class CommentEmbeddable
public function isEmpty()
{
return empty($this->getComment());
return null === $this->getComment() || '' === $this->getComment();
}
public function setComment(?string $comment)

View File

@ -27,7 +27,7 @@
</a>
{% for menu in menus %}
<a class="list-group-item list-group-item-action" href="{{ menu.uri }}">
{{ menu.label|upper }}
{{ menu.label|trans|upper }}
</a>
{% endfor %}
</div>

View File

@ -1,60 +1,62 @@
{% extends '@ChillMain/Admin/Permission/layout_crud_permission_index.html.twig' %}
{% block admin_content -%}
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block table_entities_thead_tr %}
<th>{{ 'crud.admin_user.index.is_active'|trans }}</th>
<th>{{ 'crud.admin_user.index.usernames'|trans }}</th>
<th>{{ 'crud.admin_user.index.mains'|trans }}</th>
<th>&nbsp;</th>
{% endblock %}
{% block table_entities_tbody %}
{% for entity in entities %}
<tr data-username="{{ entity.username|e('html_attr') }}">
<td>
<h1>{{"Users"|trans}}</h1>
{% for entity in entities %}
<div class="flex-table">
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
{{ entity.label }}
{% if entity.isEnabled %}
<i class="fa fa-check chill-green"></i>
{% else %}
<i class="fa fa-times chill-red"></i>
{% endif %}
</td>
<td>
{{ entity.username }}
<br/>
{{ entity.label }}
<br/>
{{ entity.email }}
</td>
<td>
</div>
<div class="item-col"><i>{{ entity.email }}</i></div>
</div>
<div class="item-row">
<div class="item-col">
login: {{ entity.username|e('html_attr') }}
</div>
<div class="item-col">
{% if entity.userJob %}
{{ entity.userJob.label|localize_translatable_string }}
<br/>
{{ entity.userJob.label|localize_translatable_string }}
{% endif %}
</div>
</div>
<div class="item-row">
<div class="item-col">
{% if entity.mainScope %}
{{ entity.mainScope.name|localize_translatable_string }}
<br/>
{{ entity.mainScope.name|localize_translatable_string }}
{% endif %}
{% if entity.mainCenter %}
{{ entity.mainCenter.name }}
, {{ entity.mainCenter.name }}
{% endif %}
</td>
<td>
<ul class="record_actions">
</div>
</div>
<div class="item-row">
<ul class="record_actions">
<li>
<a class="btn btn-edit" href="{{ path('chill_crud_admin_user_edit', { 'id': entity.id }) }}"></a>
</li>
<li>
<a class="btn btn-chill-red" href="{{ path('admin_user_edit_password', { 'id' : entity.id }) }}" title="{{ 'Edit password'|trans|e('html_attr') }}"><i class="fa fa-ellipsis-h"></i></a>
</li>
{% if is_granted('ROLE_ALLOWED_TO_SWITCH') %}
<li>
<a class="btn btn-edit" href="{{ path('chill_crud_admin_user_edit', { 'id': entity.id }) }}"></a>
<a class="btn btn-chill-blue" href="{{ path('chill_main_homepage', {'_switch_user': entity.username }) }}" title="{{ "Impersonate"|trans|e('html_attr') }}"><i class="fa fa-user-secret"></i></a>
</li>
<li>
<a class="btn btn-chill-red" href="{{ path('admin_user_edit_password', { 'id' : entity.id }) }}" title="{{ 'Edit password'|trans|e('html_attr') }}"><i class="fa fa-ellipsis-h"></i></a>
</li>
{% if is_granted('ROLE_ALLOWED_TO_SWITCH') %}
<li>
<a class="btn btn-chill-blue" href="{{ path('chill_main_homepage', {'_switch_user': entity.username }) }}" title="{{ "Impersonate"|trans|e('html_attr') }}"><i class="fa fa-user-secret"></i></a>
</li>
{% endif %}
</ul>
</td>
</tr>
{% endfor %}
{% endblock %}
{% endembed %}
{% endblock %}
{% endif %}
</ul>
</div>
</div>
</div>
{% endfor %}
{{ chill_pagination(paginator) }}
{% endblock %}

View File

@ -14,6 +14,7 @@ namespace Chill\MainBundle\Routing\MenuBuilder;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Knp\Menu\MenuItem;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@ -22,23 +23,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class SectionMenuBuilder implements LocalMenuBuilderInterface
{
/**
* @var AuthorizationCheckerInterface
*/
protected $authorizationChecker;
protected AuthorizationCheckerInterface $authorizationChecker;
/**
* @var TranslatorInterface
*/
protected $translator;
protected ParameterBagInterface $parameterBag;
protected TranslatorInterface $translator;
/**
* SectionMenuBuilder constructor.
*/
public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translator)
public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translator, ParameterBagInterface $parameterBag)
{
$this->authorizationChecker = $authorizationChecker;
$this->translator = $translator;
$this->parameterBag = $parameterBag;
}
/**
@ -54,14 +52,16 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
'order' => 0,
]);
$menu->addChild($this->translator->trans('Global timeline'), [
'route' => 'chill_center_timeline',
])
->setExtras(
[
'order' => 10,
]
);
if ($this->parameterBag->get('chill_main.access_global_history')) {
$menu->addChild($this->translator->trans('Global timeline'), [
'route' => 'chill_center_timeline',
])
->setExtras(
[
'order' => 10,
]
);
}
if ($this->authorizationChecker->isGranted(ChillExportVoter::EXPORT)) {
$menu->addChild($this->translator->trans('Export Menu'), [

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Search;
use Chill\MainBundle\Pagination\Paginator;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Serializer\Model\Collection;
use Doctrine\DBAL\Types\Types;
@ -102,9 +103,9 @@ class SearchApi
return $items;
}
private function buildUnionQuery(array $queries, $types, $parameters)
private function buildUnionQuery(array $queries, $types, $parameters, Paginator $paginator)
{
$query = '{unions} ORDER BY pertinence DESC';
$query = '{unions} ORDER BY pertinence DESC LIMIT ? OFFSET ?';
$unions = [];
$parameters = [];
@ -113,6 +114,10 @@ class SearchApi
$parameters = array_merge($parameters, $q->buildParameters());
}
// add pagination limit
$parameters[] = $paginator->getItemsPerPage();
$parameters[] = $paginator->getCurrentPageFirstItemNumber();
$union = implode(' UNION ', $unions);
return [
@ -132,9 +137,9 @@ class SearchApi
return (int) $countNq->getSingleScalarResult();
}
private function fetchRawResult($queries, $types, $parameters, $paginator): array
private function fetchRawResult($queries, $types, $parameters, Paginator $paginator): array
{
[$union, $parameters] = $this->buildUnionQuery($queries, $types, $parameters);
[$union, $parameters] = $this->buildUnionQuery($queries, $types, $parameters, $paginator);
$rsm = new ResultSetMappingBuilder($this->em);
$rsm->addScalarResult('key', 'key', Types::STRING)
->addScalarResult('metadata', 'metadata', Types::JSON)

View File

@ -39,7 +39,7 @@ class CommentEmbeddableDocGenNormalizer implements ContextAwareNormalizerInterfa
*/
public function normalize($object, ?string $format = null, array $context = []): array
{
if (null === $object) {
if (null === $object || ($object->isEmpty())) {
return [
'comment' => '',
'isNull' => true,
@ -52,7 +52,11 @@ class CommentEmbeddableDocGenNormalizer implements ContextAwareNormalizerInterfa
];
}
$user = $this->userRepository->find($object->getUserId());
if (null === $object->getUserId()) {
$user = null;
} else {
$user = $this->userRepository->find($object->getUserId());
}
return [
'comment' => $object->getComment(),

View File

@ -18,7 +18,6 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PhonenumberNormalizer implements ContextAwareNormalizerInterface, DenormalizerInterface
{
@ -54,7 +53,7 @@ class PhonenumberNormalizer implements ContextAwareNormalizerInterface, Denormal
public function normalize($object, ?string $format = null, array $context = []): string
{
if ($format === 'docgen' && null === $object) {
if ('docgen' === $format && null === $object) {
return '';
}
@ -68,13 +67,13 @@ class PhonenumberNormalizer implements ContextAwareNormalizerInterface, Denormal
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
{
if ($data instanceof PhoneNumber && $format === 'json') {
if ($data instanceof PhoneNumber && 'json' === $format) {
return true;
}
if ($format === 'docgen' && (
if ('docgen' === $format && (
$data instanceof PhoneNumber || PhoneNumber::class === ($context['docgen:expects'] ?? null)
)) {
)) {
return true;
}

View File

@ -100,10 +100,10 @@ final class UserControllerTest extends WebTestCase
$crawler = $this->client->followRedirect();
// Check data in the show view
$this->assertGreaterThan(
0,
$crawler->filter('td:contains("Test_user")')->count(),
'Missing element td:contains("Test user")'
$this->assertStringContainsString(
'Test_user',
$crawler->text(),
'page contains the name of the user'
);
//test the auth of the new client
@ -125,11 +125,7 @@ final class UserControllerTest extends WebTestCase
$this->client->submit($form);
$crawler = $this->client->followRedirect();
// Check the element contains an attribute with value equals "Foo"
$this->assertGreaterThan(
0,
$crawler->filter('[data-username="' . $username . '"]')->count(),
'Missing element [data-username="Foo bar"]'
);
$this->assertResponseIsSuccessful();
}
/**

View File

@ -13,9 +13,8 @@ services:
- { name: 'chill.menu_builder' }
Chill\MainBundle\Routing\MenuBuilder\SectionMenuBuilder:
arguments:
$authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
autowire: true
autoconfigure: true
tags:
- { name: 'chill.menu_builder' }

View File

@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Controller;
use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\AccompanyingCourseType;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
@ -279,6 +280,40 @@ class AccompanyingCourseController extends Controller
]);
}
/**
* @Route("/{_locale}/person/household/parcours/new", name="chill_household_accompanying_course_new")
*/
public function newHouseholdParcoursAction(Request $request): Response
{
$period = new AccompanyingPeriod();
$em = $this->getDoctrine()->getManager();
if ($request->query->has('household_id')) {
$householdId = $request->query->get('household_id');
$household = $em->getRepository(Household::class)->find($householdId);
$members = $household->getCurrentMembers();
if (null !== $members) {
foreach ($members as $m) {
$period->addPerson($m->getPerson());
}
}
}
$userLocation = $this->getUser()->getCurrentLocation();
$period->setAdministrativeLocation($userLocation);
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::CREATE, $period);
$em->persist($period);
$em->flush();
return $this->redirectToRoute('chill_person_accompanying_course_edit', [
'accompanying_period_id' => $period->getId(),
]);
}
/**
* @Route("/{_locale}/parcours/{accompanying_period_id}/open", name="chill_person_accompanying_course_reopen")
* @ParamConverter("accompanyingCourse", options={"id": "accompanying_period_id"})

View File

@ -0,0 +1,114 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
class ReassignAccompanyingPeriodController extends AbstractController
{
private AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository;
private EngineInterface $engine;
private FormFactoryInterface $formFactory;
private PaginatorFactory $paginatorFactory;
private Security $security;
private UserRender $userRender;
private UserRepository $userRepository;
public function __construct(AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository, UserRepository $userRepository, EngineInterface $engine, FormFactoryInterface $formFactory, PaginatorFactory $paginatorFactory, Security $security, UserRender $userRender)
{
$this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository;
$this->engine = $engine;
$this->formFactory = $formFactory;
$this->paginatorFactory = $paginatorFactory;
$this->security = $security;
$this->userRepository = $userRepository;
$this->userRender = $userRender;
}
/**
* @Route("/{_locale}/person/accompanying-periods/reassign", name="chill_course_list_reassign")
*/
public function listAction(Request $request): Response
{
if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) {
throw new AccessDeniedException();
}
$form = $this->buildFilterForm();
$form->handleRequest($request);
$total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod(
$form['user']->getData()
);
$paginator = $this->paginatorFactory->create($total);
$periods = $this->accompanyingPeriodACLAwareRepository
->findByUserOpenedAccompanyingPeriod(
$form['user']->getData(),
['openingDate' => 'ASC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return new Response(
$this->engine->render('@ChillPerson/AccompanyingPeriod/reassign_list.html.twig', [
'paginator' => $paginator,
'periods' => $periods,
'form' => $form->createView(),
])
);
}
private function buildFilterForm(): FormInterface
{
$data = [
'user' => null,
];
$builder = $this->formFactory->createBuilder(FormType::class, $data, [
'method' => 'get', 'csrf_protection' => false, ]);
$builder
->add('user', EntityType::class, [
'class' => User::class,
'choices' => $this->userRepository->findByActive(['username' => 'ASC']),
'choice_label' => function (User $u) {
return $this->userRender->renderString($u, []);
},
'multiple' => false,
'label' => 'User',
'required' => false,
]);
return $builder->getForm();
}
}

View File

@ -59,6 +59,16 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$config['allow_multiple_simultaneous_accompanying_periods']
);
$container->setParameter(
'chill_person.create_person_allowed',
$config['create_person_allowed']
);
$container->setParameter(
'chill_person.create_parcours_allowed',
$config['create_parcours_allowed']
);
// register all configuration in a unique parameter
$container->setParameter('chill_person', $config);

View File

@ -62,6 +62,12 @@ class Configuration implements ConfigurationInterface
->end() // children for 'validation', parent = validation
->end() //validation, parent = children of root
->end() // children of root, parent = root
->booleanNode('create_person_allowed')
->defaultTrue()
->end()
->booleanNode('create_parcours_allowed')
->defaultTrue()
->end()
->arrayNode('person_fields')
->canBeDisabled()
->children()

View File

@ -142,6 +142,15 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
*/
private Collection $persons;
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_person_accompanying_period_work_referrer")
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"accompanying_period_work:create"})
*/
private Collection $referrers;
/**
* @ORM\ManyToMany(targetEntity=Result::class, inversedBy="accompanyingPeriodWorks")
* @ORM\JoinTable(name="chill_person_accompanying_period_work_result")
@ -196,6 +205,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
$this->thirdParties = new ArrayCollection();
$this->persons = new ArrayCollection();
$this->accompanyingPeriodWorkEvaluations = new ArrayCollection();
$this->referrers = new ArrayCollection();
}
public function addAccompanyingPeriodWorkEvaluation(AccompanyingPeriodWorkEvaluation $evaluation): self
@ -227,6 +237,15 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this;
}
public function addReferrer(User $referrer): self
{
if (!$this->referrers->contains($referrer)) {
$this->referrers[] = $referrer;
}
return $this;
}
public function addResult(Result $result): self
{
if (!$this->results->contains($result)) {
@ -308,6 +327,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this->persons;
}
/**
* @return Collection|User[]
*/
public function getReferrers(): Collection
{
return $this->referrers;
}
/**
* @return Collection|Result[]
*/
@ -382,6 +409,13 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this;
}
public function removeReferrer(User $referrer): self
{
$this->referrers->removeElement($referrer);
return $this;
}
public function removeResult(Result $result): self
{
$this->results->removeElement($result);

View File

@ -26,6 +26,7 @@ use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
use Chill\PersonBundle\Entity\Person\PersonResource;
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
@ -451,6 +452,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
*/
private $proxyAccompanyingPeriodOpenState = false; //TO-DELETE ?
/**
* @ORM\OneToMany(targetEntity=PersonResource::class, mappedBy="personOwner")
*
* @var Collection|PersonResource[];
*/
private Collection $resources;
/**
* The person's spoken languages.
*
@ -493,6 +501,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
$this->maritalStatusComment = new CommentEmbeddable();
$this->periodLocatedOn = new ArrayCollection();
$this->accompanyingPeriodRequested = new ArrayCollection();
$this->resources = new ArrayCollection();
}
/**
@ -1301,6 +1310,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this->placeOfBirth;
}
/**
* @return PersonResource[]|Collection
*/
public function getResources()
{
return $this->resources;
}
/**
* Get spokenLanguages.
*

View File

@ -19,7 +19,7 @@ use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@ -27,7 +27,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @ORM\Entity
* @ORM\Table(name="chill_person_resource")
* @DiscriminatorMap(typeProperty="type", mapping={
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "personResource": personResource::class
* })
*/
@ -39,13 +39,13 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
/**
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\CommentEmbeddable", columnPrefix="comment_")
* @Groups({"read"})
* @Serializer\Groups({"read", "docgen:read"})
*/
private CommentEmbeddable $comment;
/**
* @ORM\Column(type="text", nullable=true)
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
*/
private ?string $freeText = null;
@ -53,25 +53,30 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Groups({"read", "docgen:read"})
*/
private ?int $id;
/**
* @ORM\ManyToOne(targetEntity=PersonResourceKind::class, inversedBy="personResources")
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
*/
private $kind;
private ?PersonResourceKind $kind = null;
/**
* @ORM\ManyToOne(targetEntity=Person::class, inversedBy="personResources")
* The person which host the owner of this resource.
*
* @ORM\ManyToOne(targetEntity=Person::class)
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
*/
private ?Person $person = null;
/**
* @ORM\ManyToOne(targetEntity=Person::class)
* The person linked with this resource.
*
* @ORM\ManyToOne(targetEntity=Person::class, inversedBy="resources")
* @ORM\JoinColumn(nullable=false)
* @Groups({"read"})
*/
@ -80,7 +85,7 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
/**
* @ORM\ManyToOne(targetEntity=ThirdParty::class, inversedBy="personResources")
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
*/
private ?ThirdParty $thirdParty = null;
@ -122,6 +127,26 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
return $this->personOwner;
}
/**
* @Groups({"read", "docgen:read"})
*/
public function getResourceKind(): string
{
if ($this->getPerson() instanceof Person) {
return 'person';
}
if ($this->getThirdParty() instanceof ThirdParty) {
return 'thirdparty';
}
if (null !== $this->getFreeText()) {
return 'freetext';
}
return 'none';
}
public function getThirdParty(): ?ThirdParty
{
return $this->thirdParty;
@ -205,5 +230,10 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
$context->buildViolation('You must associate at least one entity')
->addViolation();
}
if (null !== $this->person && $this->person === $this->personOwner) {
$context->buildViolation('You cannot associate a resource with the same person')
->addViolation();
}
}
}

View File

@ -12,10 +12,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* **About denormalization**: this operation is operated by @see{AccompanyingPeriodResourdeNormalizer}.
*
* @ORM\Entity
* @ORM\Table(name="chill_person_resource_kind")
*/
@ -25,8 +24,9 @@ class PersonResourceKind
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"docgen:read"})
*/
private int $id;
private ?int $id = null;
/**
* @ORM\Column(type="boolean")
@ -35,6 +35,8 @@ class PersonResourceKind
/**
* @ORM\Column(type="json", length=255)
* @Serializer\Groups({"docgen:read"})
* @Serializer\Context({"is-translatable": true}, groups={"docgen:read"})
*/
private array $title;

View File

@ -0,0 +1,33 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\EventListener;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Symfony\Component\Security\Core\Security;
class AccompanyingPeriodWorkEventListener
{
private Security $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function prePersistAccompanyingPeriodWork(AccompanyingPeriodWork $work): void
{
if ($this->security->getUser() instanceof User) {
$work->addReferrer($this->security->getUser());
}
}
}

View File

@ -75,7 +75,7 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
'accompanying_period_id' => $period->getId(),
], ])
->setExtras(['order' => 30]);
*/
*/
$menu->addChild($this->translator->trans('Accompanying Course Comment'), [
'route' => 'chill_person_accompanying_period_comment_list',

View File

@ -13,7 +13,9 @@ namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\Query\Parameter;
use Knp\Menu\MenuItem;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@ -22,23 +24,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class SectionMenuBuilder implements LocalMenuBuilderInterface
{
/**
* @var AuthorizationCheckerInterface
*/
protected $authorizationChecker;
protected AuthorizationCheckerInterface $authorizationChecker;
/**
* @var TranslatorInterface
*/
protected $translator;
protected TranslatorInterface $translator;
protected ParameterBagInterface $parameterBag;
/**
* SectionMenuBuilder constructor.
*/
public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translator)
public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translator, ParameterBagInterface $parameterBag)
{
$this->authorizationChecker = $authorizationChecker;
$this->translator = $translator;
$this->parameterBag = $parameterBag;
}
/**
@ -46,7 +45,7 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
*/
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
if ($this->authorizationChecker->isGranted(PersonVoter::CREATE)) {
if ($this->authorizationChecker->isGranted(PersonVoter::CREATE) && $this->parameterBag->get('chill_person.create_person_allowed')) {
$menu->addChild($this->translator->trans('Add a person'), [
'route' => 'chill_person_new',
])
@ -56,12 +55,22 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
]);
}
$menu->addChild($this->translator->trans('Create an accompanying course'), [
'route' => 'chill_person_accompanying_course_new',
if ($this->parameterBag->get('chill_person.create_parcours_allowed')) {
$menu->addChild($this->translator->trans('Create an accompanying course'), [
'route' => 'chill_person_accompanying_course_new',
])
->setExtras([
'order' => 11,
'icons' => ['plus'],
]);
}
$menu->addChild($this->translator->trans('Accompanying courses of users'), [
'route' => 'chill_course_list_reassign',
])
->setExtras([
'order' => 11,
'icons' => ['plus'],
'order' => 12,
'icons' => ['task'],
]);
}

View File

@ -87,9 +87,12 @@ class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
->join('work.accompanyingPeriod', 'period')
->where(
$qb->expr()->andX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->isNull('e.endDate'),
$qb->expr()->gte(':now', $qb->expr()->diff('e.maxDate', 'e.warningInterval'))
$qb->expr()->gte(':now', $qb->expr()->diff('e.maxDate', 'e.warningInterval')),
$qb->expr()->orX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->isMemberOf(':user', 'work.referrers')
)
)
)
->setParameters([

View File

@ -159,9 +159,12 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
->join('w.accompanyingPeriod', 'period')
->where(
$qb->expr()->andX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->gte('w.endDate', ':since'),
$qb->expr()->lte('w.startDate', ':until')
$qb->expr()->lte('w.startDate', ':until'),
$qb->expr()->orX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->isMemberOf(':user', 'w.referrers')
)
)
)
->setParameters([

View File

@ -11,12 +11,14 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Security;
use DateTime;
use Symfony\Component\Security\Core\Security;
use function count;
final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface
@ -41,6 +43,37 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function buildQueryOpenedAccompanyingCourseByUser(?User $user)
{
$qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap');
$qb->where($qb->expr()->eq('ap.user', ':user'))
->andWhere(
$qb->expr()->neq('ap.step', ':draft'),
$qb->expr()->orX(
$qb->expr()->isNull('ap.closingDate'),
$qb->expr()->gt('ap.closingDate', ':now')
)
)
->setParameter('user', $user)
->setParameter('now', new DateTime('now'))
->setParameter('draft', AccompanyingPeriod::STEP_DRAFT);
return $qb;
}
public function countByUserOpenedAccompanyingPeriod(?User $user): int
{
if (null === $user) {
return 0;
}
return $this->buildQueryOpenedAccompanyingCourseByUser($user)
->select('COUNT(ap)')
->getQuery()
->getSingleScalarResult();
}
public function findByPerson(
Person $person,
string $role,
@ -92,4 +125,25 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
return $qb->getQuery()->getResult();
}
/**
* @return array|AccompanyingPeriod[]
*/
public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array
{
if (null === $user) {
return [];
}
$qb = $this->buildQueryOpenedAccompanyingCourseByUser($user);
$qb->setFirstResult($offset)
->setMaxResults($limit);
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy('ap.' . $field, $direction);
}
return $qb->getQuery()->getResult();
}
}

View File

@ -11,10 +11,13 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\Person;
interface AccompanyingPeriodACLAwareRepositoryInterface
{
public function countByUserOpenedAccompanyingPeriod(?User $user): int;
public function findByPerson(
Person $person,
string $role,
@ -22,4 +25,6 @@ interface AccompanyingPeriodACLAwareRepositoryInterface
?int $limit = null,
?int $offset = null
): array;
public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array;
}

View File

@ -78,6 +78,16 @@ final class AccompanyingPeriodRepository implements ObjectRepository
->getResult();
}
public function findConfirmedByUser(User $user)
{
$qb = $this->createQueryBuilder('ap');
$qb->where($qb->expr()->eq('ap.user', ':user'))
->andWhere('ap.step', 'CONFIRMED')
->setParameter('user', $user);
return $qb;
}
public function findOneBy(array $criteria): ?AccompanyingPeriod
{
return $this->findOneBy($criteria);

View File

@ -127,7 +127,7 @@
</div>
<ul class="record_actions" v-if="evaluationsForAction.length > 0">
<li>
<button :title="$t('add_an_evaluation')" class="btn btn-create" @click="toggleAddEvaluation"></button>
<button :title="$t('add_an_evaluation')" class="btn btn-create" @click="toggleAddEvaluation">{{ $t('add_an_evaluation') }}</button>
</li>
</ul>
<div v-else>
@ -151,6 +151,37 @@
</ul>
</div>
<div id="referrers" class="action-row">
<h3>{{ $t('referrers') }}</h3>
<div v-if="!hasReferrers">
<p class="chill-no-data-statement">{{ $t('no_referrers') }}</p>
</div>
<div v-else>
<ul class="list-suggest remove-items inline">
<li v-for="u in referrers" :key="u.id" :title="$t('remove_referrer')" @click="removeReferrer(u)">
<span>
{{ u.text }}
</span>
</li>
</ul>
</div>
<ul class="record_actions">
<li class="add-persons">
<add-persons
ref="referrerPicker"
:key="referrerPicker.key"
:buttonTitle="$t('add_referrers')"
:modalTitle="$t('choose_referrers')"
:options="referrerPicker.options"
@addNewPersons="addReferrers">
</add-persons>
</li>
</ul>
</div>
<div id="handlingThirdParty" class="action-row">
<h3>{{ $t('handling_thirdparty') }}</h3>
@ -289,7 +320,6 @@ import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vu
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
const i18n = {
messages: {
fr: {
@ -322,6 +352,10 @@ const i18n = {
available_evaluations_text: "Documents disponibles pour ajout :",
no_evaluations_available: "Aucune évaluation disponible",
no_goals_available: "Aucun objectif disponible",
referrers: "Agents traitants",
no_referrers: "Aucun agent traitant",
choose_referrers: "Choisir des agents traitants",
remove_referrer: "Enlever l'agent"
}
}
};
@ -370,6 +404,17 @@ export default {
}
},
},
referrerPicker: {
key: 'referrer',
options: {
type: ['user'],
priority: null,
uniq: false,
button: {
display: false
}
},
},
};
},
computed: {
@ -381,6 +426,7 @@ export default {
'personsReachables',
'handlingThirdParty',
'thirdParties',
'referrers',
'isPosting',
'errors',
'templatesAvailablesForAction',
@ -389,6 +435,7 @@ export default {
'hasResultsForAction',
'hasHandlingThirdParty',
'hasThirdParties',
'hasReferrers'
]),
startDate: {
get() {
@ -465,6 +512,14 @@ export default {
removeThirdParty(t) {
this.$store.commit('removeThirdParty', t);
},
addReferrers({selected, modal}) {
this.$store.commit('addReferrers', selected.map(r => r.result));
this.$refs.referrerPicker.resetSearch();
modal.showModal = false;
},
removeReferrer(u) {
this.$store.commit('removeReferrer', u);
},
goToGenerateWorkflow({link}) {
console.log('save before leave to generate workflow')
const callback = (data) => {
@ -521,6 +576,7 @@ div#workEditor {
"objectives objectives"
"evaluations evaluations"
"persons persons"
"referrers referrers"
"handling handling"
"tparties tparties"
"errors errors";
@ -543,6 +599,8 @@ div#workEditor {
grid-area: handling; }
#thirdParties {
grid-area: tparties; }
#referrers {
grid-area: referrers; }
#errors {
grid-area: errors; }
@ -657,5 +715,4 @@ div#workEditor {
}
}
</style>

View File

@ -27,7 +27,7 @@
</li>
<li v-if="canDelete">
<a class="btn btn-delete" @click="modal.showModal = true" :title="$t('action.delete')"></a>
<a class="btn btn-delete" @click="modal.showModal = true" :title="$t('action.delete')">{{ $t('delete_evaluation')}}</a>
</li>
</ul>
</div>
@ -72,7 +72,8 @@ const i18n = {
sure: "Êtes-vous sûr?",
sure_description: "Cette évaluation sera supprimée de cette action d'accompagnement",
ok: "Supprimer"
}
},
delete_evaluation: "Supprimer l'évaluation",
}
}
};

View File

@ -65,7 +65,7 @@
<h5>{{ $t('Documents') }} :</h5>
<div class="flex-table">
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.key">
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.id">
<div class="item-row">
<div class="input-group input-group-lg mb-3">
<div>
@ -75,6 +75,7 @@
type="text"
:value="d.title"
:id="d.id"
:data-key="i"
@input="onInputDocumentTitle"/>
</div>
</div>
@ -84,6 +85,8 @@
<p v-if="d.createdBy" class="createdBy">Créé par {{ d.createdBy.text }}<br/>
Le {{ $d(ISOToDatetime(d.createdAt.datetime), 'long') }}</p>
</div>
</div>
<div class="item-row">
<div class="item-col">
<ul class="record_actions" >
<li v-if="d.workflows_availables.length > 0">
@ -98,6 +101,23 @@
@go-to-generate-workflow="goToGenerateWorkflowEvaluationDocument"
></list-workflow-modal>
</li>
<li>
<add-async-upload
:buttonTitle="$t('replace')"
:options="asyncUploadOptions"
:btnClasses="{'btn': true, 'btn-edit': true}"
@addDocument="(arg) => replaceDocument(d, arg)"
>
</add-async-upload>
</li>
<li>
<add-async-upload-downloader
:buttonTitle="$t('download')"
:storedObject="d.storedObject"
>
</add-async-upload-downloader>
</li>
<li>
<a :href="buildEditLink(d.storedObject)" class="btn btn-wopilink"></a>
</li>
@ -152,6 +172,7 @@ import { mapGetters, mapState } from 'vuex';
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
import AddAsyncUpload from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUpload.vue';
import AddAsyncUploadDownloader from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUploadDownloader.vue';
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
@ -176,7 +197,9 @@ const i18n = {
document_upload: "Téléverser un document",
document_title: "Titre du document",
template_title: "Nom du template",
browse: "Ajouter un document"
browse: "Ajouter un document",
replace: "Remplacer",
download: "Télécharger le fichier existant"
}
}
};
@ -188,6 +211,7 @@ export default {
ckeditor: CKEditor.component,
PickTemplate,
AddAsyncUpload,
AddAsyncUploadDownloader,
ListWorkflowModal,
},
i18n,
@ -274,8 +298,9 @@ export default {
},
onInputDocumentTitle(event) {
const id = Number(event.target.id);
const key = Number(event.target.dataset.key) + 1;
const title = event.target.value;
this.$store.commit('updateDocumentTitle', {id: id, evaluationKey: this.evaluation.key, title: title});
this.$store.commit('updateDocumentTitle', {id: id, key: key, evaluationKey: this.evaluation.key, title: title});
},
addDocument(storedObject) {
let document = {
@ -285,6 +310,14 @@ export default {
};
this.$store.commit('addDocument', {key: this.evaluation.key, document: document});
},
replaceDocument(oldDocument, storedObject) {
let document = {
type: 'accompanying_period_work_evaluation_document',
storedObject: storedObject,
title: oldDocument.title
};
this.$store.commit('replaceDocument', {key: this.evaluation.key, document: document, oldDocument: oldDocument});
},
removeDocument(document) {
if (window.confirm("Êtes-vous sûr·e de vouloir supprimer le document qui a pour titre \"" + document.title +"\" ?")) {
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});

View File

@ -31,6 +31,7 @@ const store = createStore({
.map(p => p.person),
handlingThirdParty: window.accompanyingCourseWork.handlingThierParty,
thirdParties: window.accompanyingCourseWork.thirdParties,
referrers: window.accompanyingCourseWork.referrers,
isPosting: false,
errors: [],
},
@ -54,6 +55,9 @@ const store = createStore({
hasHandlingThirdParty(state) {
return state.handlingThirdParty !== null;
},
hasReferrers(state) {
return state.referrers.length > 0;
},
hasThirdParties(state) {
return state.thirdParties.length > 0;
},
@ -82,6 +86,7 @@ const store = createStore({
},
results: state.resultsPicked.map(r => ({id: r.id, type: r.type})),
thirdParties: state.thirdParties.map(t => ({id: t.id, type: t.type})),
referrers: state.referrers.map(t => ({id: t.id, type: t.type})),
goals: state.goalsPicked.map(g => {
let o = {
type: g.type,
@ -131,9 +136,9 @@ const store = createStore({
endDate: e.endDate !== null ? ISOToDatetime(e.endDate.datetime) : null,
maxDate: e.maxDate !== null ? ISOToDatetime(e.maxDate.datetime) : null,
warningInterval: e.warningInterval !== null ? intervalISOToDays(e.warningInterval) : null,
documents: e.documents.map((d, dindex) => {
documents: e.documents.map((d, docIndex) => {
return Object.assign(d, {
key: index
key: docIndex
});
}),
});
@ -213,13 +218,26 @@ const store = createStore({
}));
},
removeDocument(state, {key, document}) {
let evaluations = state.evaluationsPicked.find(e => e.key === key);
if (evaluations === undefined) {
let evaluation = state.evaluationsPicked.find(e => e.key === key);
if (evaluation === undefined) {
return;
}
evaluation.documents = evaluation.documents.filter(d => d.key !== document.key);
},
replaceDocument(state, payload) {
let evaluation = state.evaluationsPicked.find(e => e.key === payload.key);
if (evaluation === undefined) {
return;
}
evaluations.documents = evaluations.documents.filter(d => d.key !== document.key);
let newDocument = Object.assign(
payload.document, {
key: evaluation.documents.length + 1,
workflows_availables: state.work.workflows_availables_evaluation_documents,
workflows: [],
}
);
evaluation.documents = evaluation.documents.map(d => d.id === payload.oldDocument.id ? newDocument : d);
},
addEvaluation(state, evaluation) {
let e = {
@ -302,6 +320,18 @@ const store = createStore({
state.thirdParties = state.thirdParties
.filter(t => t.id !== thirdParty.id);
},
addReferrers(state, referrers) {
let ids = state.referrers.map(t => t.id);
let unexistings = referrers.filter(t => !ids.includes(t.id));
for (let i in unexistings) {
state.referrers.push(unexistings[i]);
}
},
removeReferrer(state, user) {
state.referrers = state.referrers
.filter(u => u.id !== user.id);
},
setErrors(state, errors) {
state.errors = errors;
},
@ -309,13 +339,17 @@ const store = createStore({
state.isPosting = st;
},
updateDocumentTitle(state, payload) {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
if (payload.id === 0) {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
.documents.find(d => d.key === payload.key).title = payload.title;
} else {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
.documents.find(d => d.id === payload.id).title = payload.title;
}
}
},
actions: {
updateThirdParty({ commit }, payload) {
console.log(payload);
commit('updateThirdParty', payload);
},
getReachablesGoalsForAction({ getters, commit, dispatch }) {
@ -408,6 +442,9 @@ const store = createStore({
removeDocument({commit}, payload) {
commit('removeDocument', payload);
},
replaceDocument({commit}, payload) {
commit('replaceDocument', payload);
},
submit({ getters, state, commit }, callback) {
let
payload = getters.buildPayload,

View File

@ -28,11 +28,14 @@
{% if w.createdBy %}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Referrer'|trans }}</h3>
<h3>{{ 'Referrers'|trans }}</h3>
</div>
<div class="wl-col list">
<p class="wl-item">
{{ w.createdBy|chill_entity_render_box }}
{% for u in w.referrers %}
{{ u|chill_entity_render_box }}
{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
</div>
</div>

View File

@ -0,0 +1,82 @@
{% extends 'ChillMainBundle::layout.html.twig' %}
{% block title 'period_by_user_list.Period by user'|trans %}
{% block js %}
{{ encore_entry_script_tags('mod_set_referrer') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_set_referrer') }}
{% endblock %}
{% macro period_meta(period) %}
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE', period) %}
<div class="item-col item-meta">
{% set job_id = null %}
{% if period.job is defined %}
{% set job_id = period.job.id %}
{% endif %}
<span
data-set-referrer-app="data-set-referrer-app"
data-set-referrer-accompanying-period-id="{{ period.id }}"
data-set-referrer-job-id="{{ job_id }}"
></span>
</div>
{% endif %}
{% endmacro %}
{% macro period_actions(period) %}
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', period) %}
<li>
<a href="{{ chill_path_add_return_path('chill_person_accompanying_course_index', {'accompanying_period_id': period.id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% endmacro %}
{% import _self as m %}
{% block content %}
<div class="col-10">
<h1>{{ block('title') }}</h1>
{{ form_start(form) }}
<div class="row filter-box">
<div class="col-md-6">
{{ form_label(form.user ) }}
{{ form_widget(form.user, {'attr': {'class': 'select2'}}) }}
</div>
</div>
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-save change-icon">
<i class="fa fa-filter"></i> Filtrer
</button>
</li>
</ul>
{{ form_end(form) }}
{% if form.user.vars.value is empty %}
<p class="chill-no-data-statement">{{ 'period_by_user_list.Pick a user'|trans }}</p>
{% elseif periods|length == 0 and form.user.vars.value is not empty %}
<p class="chill-no-data-statement">{{ 'period_by_user_list.Any course or no authorization to see them'|trans }}</p>
{% else %}
<p><span class="badge rounded-pill bg-primary">{{ paginator.totalItems }}</span> parcours à réassigner (calculé ce jour à {{ null|format_time('medium') }})</p>
<div class="flex-table">
{% for period in periods %}
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period,
'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period) } %}
{% endfor %}
</div>
{% endif %}
{{ chill_pagination(paginator) }}
</div>
{% endblock %}

View File

@ -46,13 +46,13 @@
{{ 'Household summary'|trans }}
</a>
</li>
{#
{# TODO: add ACL to check if user is allowed to edit household? #}
<li>
<a href="{{ path ('chill_person_household_accompanying_course_new', {'household_id' : [ household.id ] } ) }}" class="btn btn-create">
{{ 'Create an accompanying period'|trans }}
<a class="btn btn-create"
href="{{ path ('chill_household_accompanying_course_new', {'household_id' : household.id } ) }}" role="button">
{{ 'Create an accompanying period'|trans }}
</a>
</li>
#}
</ul>
</div>

View File

@ -23,6 +23,7 @@ use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@ -30,6 +31,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
use function array_map;
use function implode;
use function in_array;
use function is_string;
class PersonDocGenNormalizer implements
ContextAwareNormalizerInterface,
@ -63,6 +66,15 @@ class PersonDocGenNormalizer implements
$dateContext = $context;
$dateContext['docgen:expects'] = DateTimeInterface::class;
$addressContext = array_merge($context, ['docgen:expects' => Address::class]);
$personResourceContext = array_merge($context, [
'docgen:expects' => Person\PersonResource::class,
// we simplify the list of attributes for the embedded persons
AbstractNormalizer::GROUPS => ['docgen:read'],
// when a person reference the same person... take care of circular references
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
return $this->normalizer->normalize(null, $format, $context);
},
]);
if (null === $person) {
return $this->normalizeNullValue($format, $context);
@ -104,6 +116,7 @@ class PersonDocGenNormalizer implements
'memo' => $person->getMemo(),
'numberOfChildren' => (string) $person->getNumberOfChildren(),
'address' => $this->normalizer->normalize($person->getCurrentPersonAddress(), $format, $addressContext),
//'resources' => $this->normalizer->normalize($person->getResources(), $format, $personResourceContext),
];
if ($context['docgen:person:with-household'] ?? false) {
@ -147,6 +160,17 @@ class PersonDocGenNormalizer implements
);
}
private function hasGroup($context, string $group): bool
{
$groups = $context[AbstractNormalizer::GROUPS] ?? [];
if (is_string($groups)) {
$groups = [$groups];
}
return in_array($group, $groups, true);
}
private function normalizeNullValue(string $format, array $context)
{
$normalizer = new NormalizeNullValueHelper($this->normalizer, 'type', 'person');
@ -169,6 +193,8 @@ class PersonDocGenNormalizer implements
$data = $normalizer->normalize($attributes, $format, $context);
//$data['resources'] = [];
if ($context['docgen:person:with-relations'] ?? false) {
$data['relations'] = [];
}

View File

@ -16,6 +16,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@ -25,17 +26,24 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
*/
final class HouseholdNormalizerTest extends KernelTestCase
{
private EntityManagerInterface $entityManager;
private ?NormalizerInterface $normalizer;
private array $toDelete;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
$this->entityManager = self::$container->get(EntityManagerInterface::class);
}
public function testNormalizationRecursive()
{
$person = new Person();
$person->setFirstName('ok')->setLastName('ok');
$this->entityManager->persist($person);
$member = new HouseholdMember();
$household = new Household();
$position = (new Position())
@ -44,7 +52,8 @@ final class HouseholdNormalizerTest extends KernelTestCase
$member->setPerson($person)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('1 month ago'));
->setEndDate(new DateTimeImmutable('1 month ago'))
->setPosition($position);
$household->addMember($member);

View File

@ -62,7 +62,7 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
'workflow.Doc for evaluation (n°%eval%)',
['%eval%' => $entityWorkflow->getRelatedEntityId()]
) . ' (' . $this->translatableStringHelper->localize($doc->getAccompanyingPeriodWorkEvaluation()
->getEvaluation()->getTitle()).') '.$doc->getTitle();
->getEvaluation()->getTitle()) . ') ' . $doc->getTitle();
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingPeriodWorkEvaluationDocument

View File

@ -41,6 +41,11 @@ services:
autowire: true
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\ReassignAccompanyingPeriodController:
autoconfigure: true
autowire: true
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\PersonApiController:
arguments:
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'

View File

@ -12,3 +12,13 @@ services:
event: 'prePersist'
entity: 'Chill\PersonBundle\Entity\PersonAltName'
method: 'prePersistAltName'
Chill\PersonBundle\EventListener\AccompanyingPeriodWorkEventListener:
autoconfigure: true
autowire: true
tags:
-
name: 'doctrine.orm.entity_listener'
event: 'prePersist'
entity: 'Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork'
method: 'prePersistAccompanyingPeriodWork'

View File

@ -0,0 +1,42 @@
<?php
/**
* 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.
*/
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add referrers to AccompanyingPeriodWork.
*/
final class Version20220310063629 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE chill_person_accompanying_period_work_referrer');
}
public function getDescription(): string
{
return 'Add referrers to AccompanyingPeriodWork';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE chill_person_accompanying_period_work_referrer (accompanyingperiodwork_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(accompanyingperiodwork_id, user_id))');
$this->addSql('CREATE INDEX IDX_3619F5EBB99F6060 ON chill_person_accompanying_period_work_referrer (accompanyingperiodwork_id)');
$this->addSql('CREATE INDEX IDX_3619F5EBA76ED395 ON chill_person_accompanying_period_work_referrer (user_id)');
$this->addSql('ALTER TABLE chill_person_accompanying_period_work_referrer ADD CONSTRAINT FK_3619F5EBB99F6060 FOREIGN KEY (accompanyingperiodwork_id) REFERENCES chill_person_accompanying_period_work (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_accompanying_period_work_referrer ADD CONSTRAINT FK_3619F5EBA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('INSERT INTO chill_person_accompanying_period_work_referrer (accompanyingperiodwork_id, user_id)
SELECT id, createdby_id FROM chill_person_accompanying_period_work');
}
}

View File

@ -194,6 +194,7 @@ No accompanying user: Aucun accompagnant
No data given: Pas d'information
Participants: Personnes impliquées
Create an accompanying course: Créer un parcours
Accompanying courses of users: Parcours des utilisateurs
This accompanying course is still a draft: Ce parcours est encore à l'état brouillon.
Associated peoples: Usagers concernés
Resources: Interlocuteurs privilégiés
@ -213,6 +214,7 @@ No requestor: Pas de demandeur
No resources: "Pas d'interlocuteurs privilégiés"
Persons associated: Usagers concernés
Referrer: Référent
Referrers: Référents
Some peoples does not belong to any household currently. Add them to an household soon: Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur ménage dès que possible.
Add to household now: Ajouter à un ménage
Any resource for this accompanying course: Aucun interlocuteur privilégié pour ce parcours
@ -494,6 +496,7 @@ Remove household composition: Supprimer composition familiale
Are you sure you want to remove this composition?: Etes-vous sûr de vouloir supprimer cette composition familiale ?
Concerns household n°%id%: Concerne le ménage n°%id%
Composition: Composition
The composition has been successfully removed.: La composition a été supprimée.
# accompanying course work
Accompanying Course Actions: Actions d'accompagnements
@ -577,3 +580,8 @@ My accompanying periods in draft: Mes parcours brouillons
workflow:
Doc for evaluation (n°%eval%): Document de l'évaluation n°%eval%
period_by_user_list:
Period by user: Parcours d'accompagnement par utilisateur
Pick a user: Choisissez un utilisateur pour obtenir la liste de ses parcours
Any course or no authorization to see them: Aucun parcours pour ce référent, ou aucun droit pour visualiser les parcours de ce référent.

View File

@ -149,7 +149,7 @@ class ThirdPartyType extends AbstractType
'by_reference' => false,
'button_add_label' => 'Add a contact',
'button_remove_label' => 'Remove a contact',
'empty_collection_explain' => 'Any contact',
'empty_collection_explain' => 'No contacts associated',
]);
}

View File

@ -60,7 +60,7 @@
{% if types|length > 0 %}
{{ types|join(', ') }}
{% else %}
<p class="chill-no-data-statement">{{ 'thirdparty.no_categories' }}</p>
<p class="chill-no-data-statement">{{ 'thirdparty.no_categories'|trans }}</p>
{% endif %}
</dd>

View File

@ -15,6 +15,7 @@ thirdparty.No_comment: Aucun commentaire
thirdparty.NameCompany: Service/Département
thirdparty.Acronym: Sigle
thirdparty.Categories: Catégories
thirdparty.no_categories: Aucune catégorie
thirdparty.Child: Personne de contact
thirdparty.child: Personne de contact
thirdparty.Children: Personnes de contact
@ -69,7 +70,6 @@ No nameCompany given: Aucune raison sociale renseignée
No acronym given: Aucun sigle renseigné
No phone given: Aucun téléphone renseigné
No email given: Aucune adresse courriel renseignée
thirdparty.no_categories: Aucune catégorie
The party is visible in those centers: Le tiers est visible dans ces centres
The party is not visible in any center: Le tiers n'est associé à aucun centre