Compare commits

...

54 Commits

Author SHA1 Message Date
96d302ccf7 quick fixes: birthdate make accompaying course form dans les choux 2021-07-01 09:13:00 +02:00
nobohan
0b5daa6c33 quick fix: address: add backurl after posting the address 2021-06-30 18:26:44 +02:00
662e5f967c Merge branch 'frontend/address-to-add-persons-widget' into 'master'
Add address to widget add persons

See merge request Chill-Projet/chill-bundles!105
2021-06-30 07:12:42 +00:00
802125edc6 Add address to widget add persons 2021-06-30 07:50:43 +02:00
028c3e0c31 widget addPersons: fix persons without birthdate 2021-06-29 22:40:23 +02:00
e4adafafc8 remove household search 2021-06-29 22:38:01 +02:00
54316d148c Merge branch 'features/improve-activity' into 'master'
Features/improve activity

See merge request Chill-Projet/chill-bundles!103
2021-06-29 19:18:51 +00:00
3c5a2a65df Merge branch 'features/improve-accompanying-course-summary' into 'master'
Improve accompanying course summary page

See merge request Chill-Projet/chill-bundles!102
2021-06-29 19:16:18 +00:00
741ef7b40d Merge branch 'features/add-suggestion-household-member-editor' into 'master'
Add suggestion household member editor

See merge request Chill-Projet/chill-bundles!97
2021-06-29 19:13:38 +00:00
e7384343b2 Merge remote-tracking branch 'origin/master' into features/improve-accompanying-course-summary 2021-06-29 21:12:21 +02:00
1e2a920200 clean menu builder for activity 2021-06-29 21:03:38 +02:00
b5a78fa341 Merge remote-tracking branch 'origin/master' into features/improve-activity 2021-06-29 20:52:06 +02:00
d9c1f52894 improve activity 2021-06-29 20:51:08 +02:00
nobohan
f05f7415bd household: add household accompanying periods 2021-06-29 19:41:00 +02:00
nobohan
5553c7314c household address: fix conflict on branch merge 2021-06-29 17:47:40 +02:00
7dc70baf57 permet de surcharger le label 'commentaire' des actviités 2021-06-29 17:39:19 +02:00
nobohan
34af401a98 accompanying period: add items in the summary page 2021-06-29 17:37:58 +02:00
5bae8dc5bf Import also scripts tags & css for launching banner 2021-06-29 17:13:57 +02:00
6e277e99ae fix in accompanying period list: thirdparty as requestor + link 2021-06-29 17:09:55 +02:00
2688c0f0ce fix missing div in accompanying course work list 2021-06-29 17:09:38 +02:00
nobohan
0135f5184e accompanying period: wording + minor changes in twig 2021-06-29 17:09:24 +02:00
fd698419e8 Merge branch 'fix-prototypage-details' into 'master'
Fix prototypage details

See merge request Chill-Projet/chill-bundles!94
2021-06-29 15:06:20 +00:00
4a864a0b14 Merge remote-tracking branch 'origin/master' into features/improve-accompanying-course-summary 2021-06-29 16:59:05 +02:00
55dc0d3f9f Ajout informations manquantes dans la page "summary" des
AccompanyingCourse
2021-06-29 16:57:28 +02:00
85835c8b0d Merge branch 'features/api-search' into 'master'
Features/api search

See merge request Chill-Projet/chill-bundles!98
2021-06-29 14:50:28 +00:00
nobohan
ab1edee9fb person menu: add style for submenus 2021-06-29 16:22:58 +02:00
6154e0912b fix in accompanying period list: thirdparty as requestor + link 2021-06-29 16:14:50 +02:00
0388428c6f fix missing div in accompanying course work list 2021-06-29 16:14:30 +02:00
7ac55275c2 Merge remote-tracking branch 'origin/fix_issues_juminet' into features/improve-accompanying-course-summary 2021-06-29 16:06:57 +02:00
1119e20a49 Add minor fixes to summary page:
* add messages when no resources
* add address to person
* replace household link label with household number
2021-06-29 16:03:59 +02:00
ea2870eef8 Merge branch 'features/create-user-endpoint' into 'master'
Feature: create user endpoint

See merge request Chill-Projet/chill-bundles!96
2021-06-29 14:02:16 +00:00
nobohan
93a44d8303 address: automatic return when saving address 2021-06-29 15:06:35 +02:00
nobohan
8109415e05 address: wording button 2021-06-29 14:39:43 +02:00
nobohan
514fe6163a address: format address as the French format 2021-06-29 14:33:51 +02:00
nobohan
4cf3cc8353 address: add focus on selectAddress field 2021-06-29 14:25:26 +02:00
dd991e3572 add test 2021-06-28 23:02:15 +02:00
0640631821 implementation on 3party + quote keys in SearchApiQuery 2021-06-28 22:44:42 +02:00
e845d9ba90 replace search api by a first workin implementation 2021-06-28 22:01:33 +02:00
nobohan
6f8231f6f6 accompanying course: clean controller 2021-06-28 22:01:05 +02:00
nobohan
048161e300 accompanying course summary (WIP) 2021-06-28 22:00:07 +02:00
nobohan
4f49292178 address: fix saving of isNoAddress 2021-06-28 17:18:57 +02:00
6eaffcae49 fix redirection after changing household member 2021-06-28 16:22:17 +02:00
nobohan
f92d710a26 accompanying course summary: add social actions 2021-06-28 16:16:55 +02:00
nobohan
bc6ba88acd menu: fix code indentation 2021-06-28 16:04:24 +02:00
nobohan
b72d45d9db accompanying period: hide activity entries from the menu if accompanyin period is DRAFT 2021-06-28 13:50:31 +02:00
2a1f5cbad1 add test for suggestion household by accompanying period 2021-06-28 13:18:00 +02:00
e8566fd006 change label 2021-06-28 12:36:19 +02:00
cd6b5c9a39 Add context for household member editor form:
* add context for accompanying period, if indicated in a query
parameter;
* expand automatically household suggestion, if indicated in a query
parameter
2021-06-28 11:23:30 +02:00
9aa3974071 show household suggestion in household members editor 2021-06-28 00:35:40 +02:00
a8bf478ee8 add endpoint for getting suggestion on household by accompanying period 2021-06-27 11:10:17 +02:00
a35f3363b2 Merge branch 'features/edit-accompanying-period-social-work' into 'master'
Features/edit accompanying period social work

See merge request Chill-Projet/chill-bundles!91
2021-06-26 13:15:34 +00:00
a43e6c12a0 add tests for UserApiController 2021-06-26 11:08:26 +02:00
abe012de60 add endpoint whoami 2021-06-26 11:08:10 +02:00
aaa9a1ec7b create user _index and _entity endpoint 2021-06-26 11:07:03 +02:00
73 changed files with 1699 additions and 484 deletions

View File

@@ -72,6 +72,7 @@ class ActivityController extends AbstractController
{ {
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
$view = null; $view = null;
// TODO: add pagination
[$person, $accompanyingPeriod] = $this->getEntity($request); [$person, $accompanyingPeriod] = $this->getEntity($request);
@@ -80,10 +81,9 @@ class ActivityController extends AbstractController
->getReachableCircles($this->getUser(), new Role('CHILL_ACTIVITY_SEE'), ->getReachableCircles($this->getUser(), new Role('CHILL_ACTIVITY_SEE'),
$person->getCenter()); $person->getCenter());
$activities = $em->getRepository('ChillActivityBundle:Activity')->findBy( $activities = $em->getRepository(Activity::class)
['person' => $person, 'scope' => $reachableScopes], ->findByPersonImplied($person, $reachableScopes)
['date' => 'DESC'], ;
);
$event = new PrivacyEvent($person, array( $event = new PrivacyEvent($person, array(
'element_class' => Activity::class, 'element_class' => Activity::class,

View File

@@ -224,7 +224,8 @@ class ActivityType extends AbstractType
if ($activityType->isVisible('comment')) { if ($activityType->isVisible('comment')) {
$builder->add('comment', CommentType::class, [ $builder->add('comment', CommentType::class, [
'label' => $activityType->getLabel('comment'), 'label' => empty($activityType->getLabel('comment'))
? 'activity.comment' : $activityType->getLabel('comment'),
'required' => $activityType->isRequired('comment'), 'required' => $activityType->isRequired('comment'),
]); ]);
} }

View File

@@ -4,6 +4,7 @@ namespace Chill\ActivityBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@@ -34,21 +35,13 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
{ {
$period = $parameters['accompanyingCourse']; $period = $parameters['accompanyingCourse'];
$menu->addChild($this->translator->trans('Activity list'), [ if (AccompanyingPeriod::STEP_DRAFT !== $period->getStep()) {
'route' => 'chill_activity_activity_list', $menu->addChild($this->translator->trans('Activity list'), [
'routeParameters' => [ 'route' => 'chill_activity_activity_list',
'accompanying_period_id' => $period->getId(), 'routeParameters' => [
]]) 'accompanying_period_id' => $period->getId(),
->setExtras(['order' => 40]); ]])
->setExtras(['order' => 40]);
$menu->addChild($this->translator->trans('Add a new activity'), [ }
'route' => 'chill_activity_activity_select_type',
'routeParameters' => [
'accompanying_period_id' => $period->getId(),
]])
->setExtras(['order' => 41]);
} }
} }

View File

@@ -1,88 +0,0 @@
<?php
/*
*
*/
namespace Chill\ActivityBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Role\Role;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class MenuBuilder implements LocalMenuBuilderInterface
{
/**
*
* @var TokenStorageInterface
*/
protected $tokenStorage;
/**
*
* @var TranslatorInterface
*/
protected $translator;
/**
*
* @var AuthorizationHelper
*/
protected $authorizationHelper;
public function __construct(
TokenStorageInterface $tokenStorage,
TranslatorInterface $translator,
AuthorizationHelper $authorizationHelper
) {
$this->tokenStorage = $tokenStorage;
$this->translator = $translator;
$this->authorizationHelper = $authorizationHelper;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
/* @var $person \Chill\PersonBundle\Entity\Person */
$person = $parameters['person'];
$user = $this->tokenStorage->getToken()->getUser();
$roleSee = new Role(ActivityVoter::SEE);
$roleAdd = new Role(ActivityVoter::CREATE);
if ($this->authorizationHelper->userHasAccess($user, $person, $roleSee)) {
$menu->addChild($this->translator->trans('Activity list'), [
'route' => 'chill_activity_activity_list',
'routeParameters' => [
'person_id' => $person->getId()
]
])
->setExtras([
'order' => 201
]);
}
if ($this->authorizationHelper->userHasAccess($user, $person, $roleAdd)) {
$menu->addChild($this->translator->trans('Add a new activity'), [
'route' => 'chill_activity_activity_new',
'routeParameters' => [
'person_id' => $person->getId()
]
])
->setExtras([
'order' => 200
]);
}
}
public static function getMenuIds(): array
{
return [ 'person' ];
}
}

View File

@@ -15,8 +15,7 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Chill\ActivityBundle\Menu; namespace Chill\ActivityBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
@@ -65,15 +64,6 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
->setExtra('order', 201) ->setExtra('order', 201)
; ;
} }
if ($this->authorizationChecker->isGranted(ActivityVoter::CREATE, $person)) {
$menu->addChild(
$this->translator->trans('Add a new activity'), [
'route' => 'chill_activity_activity_new',
'routeParameters' => [ 'person_id' => $person->getId() ],
])
->setExtra('order', 200)
;
}
} }
public static function getMenuIds(): array public static function getMenuIds(): array

View File

@@ -38,5 +38,30 @@ class ActivityRepository extends ServiceEntityRepository
{ {
parent::__construct($registry, Activity::class); parent::__construct($registry, Activity::class);
} }
public function findByPersonImplied($person, array $scopes, $orderBy = [ 'date' => 'DESC'], $limit = 100, $offset = 0)
{
$qb = $this->createQueryBuilder('a');
$qb->select('a');
$qb
// TODO add acl
//->where($qb->expr()->in('a.scope', ':scopes'))
//->setParameter('scopes', $scopes)
->andWhere(
$qb->expr()->orX(
$qb->expr()->eq('a.person', ':person'),
':person MEMBER OF a.persons'
)
)
->setParameter('person', $person)
;
foreach ($orderBy as $k => $dir) {
$qb->addOrderBy('a.'.$k, $dir);
}
return $qb->getQuery()
->getResult();
}
} }

View File

@@ -99,7 +99,7 @@
</a> </a>
</li> </li>
<li> <li>
<button class="sc-button bt-update" type="submit">{{ 'Save activity'|trans }}</button> <button class="sc-button bt-update" type="submit">{{ 'Save'|trans }}</button>
</li> </li>
</ul> </ul>
{{ form_end(edit_form) }} {{ form_end(edit_form) }}

View File

@@ -10,6 +10,7 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ parent() }}
{{ encore_entry_link_tags('async_upload') }} {{ encore_entry_link_tags('async_upload') }}
<script type="text/javascript"> <script type="text/javascript">
window.addEventListener('DOMContentLoaded', function (e) { window.addEventListener('DOMContentLoaded', function (e) {
@@ -22,6 +23,7 @@
{% endblock %} {% endblock %}
{% block css %} {% block css %}
{{ parent() }}
{{ encore_entry_link_tags('async_upload') }} {{ encore_entry_link_tags('async_upload') }}
{{ encore_entry_link_tags('vue_activity') }} {{ encore_entry_link_tags('vue_activity') }}
{% endblock %} {% endblock %}

View File

@@ -17,7 +17,7 @@
</p> </p>
{% else %} {% else %}
<div class="flex-table list-records {{ context }}"> <div class="flex-table list-records context-{{ context }}">
<!-- <!--
<thead> <thead>
<tr> <tr>
@@ -54,6 +54,20 @@
</p> </p>
{% endif %} {% endif %}
</div> </div>
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
<div class="accompanyingPeriodLink" style="margin-top: 1rem">
<a
href="{{ chill_path_add_return_path(
"chill_person_accompanying_course_index",
{ 'accompanying_period_id': activity.accompanyingPeriod.id }
) }}"
>
<i class="fa fa-random"></i>
{{ activity.accompanyingPeriod.id }}
</a>
</div>
{% endif %}
</div> </div>
<div class="item-col"> <div class="item-col">
@@ -147,13 +161,14 @@
</div> </div>
{% {%
if activity.comment.comment is not empty if activity.comment.comment is not empty
or activity.persons|length > 0 or activity.persons|length > 0
or activity.thirdParties|length > 0 or activity.thirdParties|length > 0
or activity.users|length > 0 or activity.users|length > 0
%} %}
<div class="item-row details"> <div class="item-row details">
<div class="item-col"> <div class="item-col">
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'row', 'entity': activity } %} {% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {'context': context, 'with_display': 'row', 'entity': activity } %}
</div> </div>
@@ -170,10 +185,13 @@
</div> </div>
{% endif %} {% endif %}
<ul class="record_actions"> {% if context != 'person' %}
<li> {# TODO set this condition in configuration #}
<a href="{{ path('chill_activity_activity_new', {'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="sc-button bt-create"> <ul class="record_actions">
{{ 'Add a new activity' | trans }} <li>
</a> <a href="{{ path('chill_activity_activity_new', {'person_id': person_id, 'accompanying_period_id': accompanying_course_id}) }}" class="sc-button bt-create">
</li> {{ 'Add a new activity' | trans }}
</ul> </a>
</li>
</ul>
{% endif %}

View File

@@ -98,7 +98,7 @@
</li> </li>
<li> <li>
<button class="sc-button bt-create" type="submit"> <button class="sc-button bt-create" type="submit">
{{ 'Add a new activity'|trans }} {{ 'Create'|trans }}
</button> </button>
</li> </li>
</ul> </ul>

View File

@@ -10,6 +10,7 @@
{% endblock %} {% endblock %}
{% block js %} {% block js %}
{{ parent() }}
{{ encore_entry_script_tags('async_upload') }} {{ encore_entry_script_tags('async_upload') }}
<script type="text/javascript"> <script type="text/javascript">
window.addEventListener('DOMContentLoaded', function (e) { window.addEventListener('DOMContentLoaded', function (e) {
@@ -22,6 +23,7 @@
{% endblock %} {% endblock %}
{% block css %} {% block css %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('build/async_upload.css') }}"/> <link rel="stylesheet" href="{{ asset('build/async_upload.css') }}"/>
{{ encore_entry_link_tags('vue_activity') }} {{ encore_entry_link_tags('vue_activity') }}
{% endblock %} {% endblock %}

View File

@@ -117,7 +117,7 @@
</li> </li>
<li> <li>
<a class="sc-button bt-update" href="{{ path('chill_activity_activity_edit', { 'id': entity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}"> <a class="sc-button bt-update" href="{{ path('chill_activity_activity_edit', { 'id': entity.id, 'person_id': person_id, 'accompanying_period_id': accompanying_course_id }) }}">
{{ 'Edit the activity'|trans }} {{ 'Edit'|trans }}
</a> </a>
</li> </li>
{# TODO {# TODO

View File

@@ -74,6 +74,7 @@ Users concerned: T(M)S
activity: activity:
Insert a document: Insérer un document Insert a document: Insérer un document
Remove a document: Supprimer le document Remove a document: Supprimer le document
comment: Commentaire
#timeline #timeline

View File

@@ -36,6 +36,7 @@ use Chill\MainBundle\Search\SearchProvider;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\SearchApi; use Chill\MainBundle\Search\SearchApi;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
/** /**
* Class SearchController * Class SearchController
@@ -151,11 +152,14 @@ class SearchController extends AbstractController
{ {
//TODO this is an incomplete implementation //TODO this is an incomplete implementation
$query = $request->query->get('q', ''); $query = $request->query->get('q', '');
$types = $request->query->get('type', []);
$results = $this->searchApi->getResults($query, 0, 150); if (count($types) === 0) {
$paginator = $this->paginatorFactory->create(count($results)); throw new BadRequestException("The request must contains at "
." one type");
}
$collection = new Collection($results, $paginator); $collection = $this->searchApi->getResults($query, $types, []);
return $this->json($collection); return $this->json($collection);
} }

View File

@@ -0,0 +1,27 @@
<?php
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
class UserApiController extends ApiController
{
/**
* @Route(
* "/api/1.0/main/whoami.{_format}",
* name="chill_main_user_whoami",
* requirements={
* "_format": "json"
* }
* )
*/
public function whoami($_format): JsonResponse
{
return $this->json($this->getUser(), JsonResponse::HTTP_OK, [],
[ "groups" => [ "read" ] ]);
}
}

View File

@@ -347,7 +347,28 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
] ]
], ],
] ]
] ],
[
'class' => \Chill\MainBundle\Entity\User::class,
'controller' => \Chill\MainBundle\Controller\UserApiController::class,
'name' => 'user',
'base_path' => '/api/1.0/main/user',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
]
],
]
],
] ]
]); ]);
} }

View File

@@ -132,6 +132,7 @@ class Address
/** /**
* True if the address is a "no address", aka homeless person, ... * True if the address is a "no address", aka homeless person, ...
* @groups({"write"}) * @groups({"write"})
* @ORM\Column(type="boolean")
* *
* @var bool * @var bool
*/ */
@@ -298,7 +299,7 @@ class Address
* @param bool $isNoAddress * @param bool $isNoAddress
* @return $this * @return $this
*/ */
public function setIsNoAddress(bool $isNoAddress) public function setIsNoAddress(bool $isNoAddress): self
{ {
$this->isNoAddress = $isNoAddress; $this->isNoAddress = $isNoAddress;
return $this; return $this;

View File

@@ -45,7 +45,8 @@ class CommentType extends AbstractType
{ {
$builder $builder
->add('comment', ChillTextareaType::class, [ ->add('comment', ChillTextareaType::class, [
'disable_editor' => $options['disable_editor'] 'disable_editor' => $options['disable_editor'],
'label' => $options['label'],
]) ])
; ;

View File

@@ -35,6 +35,7 @@ $table-body-td-text-align: left;
// Tabs // Tabs
$tabs-nav-margin-bottom: 0.2em; $tabs-nav-margin-bottom: 0.2em;
$tabs-nav-bg-color: $yellow; $tabs-nav-bg-color: $yellow;
$tabs-nav-bg-color-light: lighten($yellow, 10%);
$tabs-nav-text-color: $blue; $tabs-nav-text-color: $blue;
$tabs-new-border: none; $tabs-new-border: none;
$tabs-nav-hover-border: none; $tabs-nav-hover-border: none;

View File

@@ -26,7 +26,13 @@ $global-bg-color: $yellow;
color: $tabs-nav-title-text-color; color: $tabs-nav-title-text-color;
} }
> a { &.sub-menu {
padding-left: 20px;
> a {
background-color: $tabs-nav-bg-color-light;
}
}
> a {
display: block; display: block;
width: auto; width: auto;
padding: $tabs-nav-padding; padding: $tabs-nav-padding;
@@ -38,11 +44,11 @@ $global-bg-color: $yellow;
@include border-top-radius($base-border-radius); @include border-top-radius($base-border-radius);
@include border-bottom-radius($base-border-radius); @include border-bottom-radius($base-border-radius);
&:hover, &:active { &:hover, &:active {
border: $tabs-nav-hover-border; border: $tabs-nav-hover-border;
color: $tabs-nav-hover-text-color; color: $tabs-nav-hover-text-color;
text-decoration: none; text-decoration: none;
} }
} }
} }
} }

View File

@@ -46,6 +46,7 @@
<city-selection <city-selection
v-bind:address="address" v-bind:address="address"
v-bind:focusOnAddress="focusOnAddress"
v-bind:getReferenceAddresses="getReferenceAddresses"> v-bind:getReferenceAddresses="getReferenceAddresses">
</city-selection> </city-selection>
@@ -173,6 +174,14 @@ export default {
// this.$refs.search.focus(); // positionner le curseur à l'ouverture de la modale // this.$refs.search.focus(); // positionner le curseur à l'ouverture de la modale
//}) //})
}, },
focusOnCity() {
const citySelector = document.getElementById('citySelector');
citySelector.focus();
},
focusOnAddress() {
const addressSelector = document.getElementById('addressSelector');
addressSelector.focus();
},
getCountries() { getCountries() {
console.log('getCountries'); console.log('getCountries');
this.loading = true; this.loading = true;

View File

@@ -1,6 +1,7 @@
<template> <template>
<div class="container"> <div class="container">
<VueMultiselect <VueMultiselect
id="addressSelector"
v-model="value" v-model="value"
@select="selectAddress" @select="selectAddress"
name="field" name="field"
@@ -70,7 +71,7 @@ export default {
}, },
methods: { methods: {
transName(value) { transName(value) {
return value.streetNumber === undefined ? value.street : `${value.street}, ${value.streetNumber}` return value.streetNumber === undefined ? value.street : `${value.streetNumber}, ${value.street}`
}, },
selectAddress(value) { selectAddress(value) {
this.address.selected.address = value; this.address.selected.address = value;

View File

@@ -1,6 +1,7 @@
<template> <template>
<div class="container"> <div class="container">
<VueMultiselect <VueMultiselect
id="citySelector"
v-model="value" v-model="value"
@select="selectCity" @select="selectCity"
name="field" name="field"
@@ -35,7 +36,7 @@ import VueMultiselect from 'vue-multiselect';
export default { export default {
name: 'CitySelection', name: 'CitySelection',
components: { VueMultiselect }, components: { VueMultiselect },
props: ['address', 'getReferenceAddresses'], props: ['address', 'getReferenceAddresses', 'focusOnAddress'],
data() { data() {
return { return {
value: null value: null
@@ -74,6 +75,7 @@ export default {
this.address.newPostalCode.name = value.name; this.address.newPostalCode.name = value.name;
this.address.newPostalCode.code = value.code; this.address.newPostalCode.code = value.code;
this.getReferenceAddresses(value); this.getReferenceAddresses(value);
this.focusOnAddress();
}, },
addPostalCode() { addPostalCode() {
this.address.writeNewPostalCode = true; this.address.writeNewPostalCode = true;

View File

@@ -40,7 +40,6 @@ export default {
return name.fr //TODO multilang return name.fr //TODO multilang
}, },
selectCountry(value) { selectCountry(value) {
console.log(value);
this.address.selected.country = value; this.address.selected.country = value;
this.getCities(value); this.getCities(value);
}, },

View File

@@ -2,88 +2,169 @@
namespace Chill\MainBundle\Search; namespace Chill\MainBundle\Search;
use Chill\PersonBundle\Entity\Person; use Chill\MainBundle\Serializer\Model\Collection;
use Chill\ThirdPartyBundle\Entity\ThirdParty; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\PersonBundle\Search\SearchPersonApiProvider;
use Chill\ThirdPartyBundle\Search\ThirdPartyApiSearch;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Chill\MainBundle\Search\SearchProvider; use Chill\MainBundle\Search\SearchProvider;
use Symfony\Component\VarDumper\Resources\functions\dump; use Symfony\Component\VarDumper\Resources\functions\dump;
/** /**
* ***Warning*** This is an incomplete implementation ***Warning***
*/ */
class SearchApi class SearchApi
{ {
private EntityManagerInterface $em; private EntityManagerInterface $em;
private SearchProvider $search; private PaginatorFactory $paginator;
public function __construct(EntityManagerInterface $em, SearchProvider $search) private array $providers = [];
public function __construct(
EntityManagerInterface $em,
SearchPersonApiProvider $searchPerson,
ThirdPartyApiSearch $thirdPartyApiSearch,
PaginatorFactory $paginator
)
{ {
$this->em = $em; $this->em = $em;
$this->search = $search; $this->providers[] = $searchPerson;
$this->providers[] = $thirdPartyApiSearch;
$this->paginator = $paginator;
} }
/** /**
* @return Model/Result[] * @return Model/Result[]
*/ */
public function getResults(string $query, int $offset, int $maxResult): array public function getResults(string $pattern, array $types, array $parameters): Collection
{ {
// **warning again**: this is an incomplete implementation $queries = $this->findQueries($pattern, $types, $parameters);
$results = [];
foreach ($this->getPersons($query) as $p) { $total = $this->countItems($queries, $types, $parameters);
$results[] = new Model\Result((float)\rand(0, 100) / 100, $p); $paginator = $this->paginator->create($total);
}
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) { $rawResults = $this->fetchRawResult($queries, $types, $parameters, $paginator);
return ($a->getRelevance() <=> $b->getRelevance()) * -1;
});
return $results; $this->prepareProviders($rawResults);
$results = $this->buildResults($rawResults);
$collection = new Collection($results, $paginator);
return $collection;
} }
public function countResults(string $query): int private function findQueries($pattern, array $types, array $parameters): array
{ {
return 0; return \array_map(
fn($p) => $p->provideQuery($pattern, $parameters),
$this->findProviders($pattern, $types, $parameters),
);
} }
private function getThirdParties(string $query) private function findProviders(string $pattern, array $types, array $parameters): array
{ {
$thirdPartiesIds = $this->em->createQuery('SELECT t.id FROM '.ThirdParty::class.' t') return \array_filter(
->getScalarResult(); $this->providers,
$nbResults = rand(0, 15); fn($p) => $p->supportsTypes($pattern, $types, $parameters)
);
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) private function countItems($providers, $types, $parameters): int
{ {
$params = [ list($countQuery, $parameters) = $this->buildCountQuery($providers, $types, $parameters);
SearchInterface::SEARCH_PREVIEW_OPTION => false $rsmCount = new ResultSetMappingBuilder($this->em);
]; $rsmCount->addScalarResult('count', 'count');
$search = $this->search->getResultByName($query, 'person_regular', 0, 50, $params, 'json'); $countNq = $this->em->createNativeQuery($countQuery, $rsmCount);
$ids = \array_map(function($r) { return $r['id']; }, $search['results']); $countNq->setParameters($parameters);
return $countNq->getSingleScalarResult();
}
private function buildCountQuery(array $queries, $types, $parameters)
{
$query = "SELECT COUNT(sq.key) AS count FROM ({union_unordered}) AS sq";
$unions = [];
$parameters = [];
if (count($ids) === 0) { foreach ($queries as $q) {
return []; $unions[] = $q->buildQuery();
$parameters = \array_merge($parameters, $q->buildParameters());
} }
return $this->em->getRepository(Person::class) $unionUnordered = \implode(" UNION ", $unions);
->findById($ids)
return [
\strtr($query, [ '{union_unordered}' => $unionUnordered ]),
$parameters
];
}
private function buildUnionQuery(array $queries, $types, $parameters)
{
$query = "{unions} ORDER BY pertinence DESC";
$unions = [];
$parameters = [];
foreach ($queries as $q) {
$unions[] = $q->buildQuery();
$parameters = \array_merge($parameters, $q->buildParameters());
}
$union = \implode(" UNION ", $unions);
return [
\strtr($query, [ '{unions}' => $union]),
$parameters
];
}
private function fetchRawResult($queries, $types, $parameters, $paginator): array
{
list($union, $parameters) = $this->buildUnionQuery($queries, $types, $parameters, $paginator);
$rsm = new ResultSetMappingBuilder($this->em);
$rsm->addScalarResult('key', 'key', Types::STRING)
->addScalarResult('metadata', 'metadata', Types::JSON)
->addScalarResult('pertinence', 'pertinence', Types::FLOAT)
; ;
$nq = $this->em->createNativeQuery($union, $rsm);
$nq->setParameters($parameters);
return $nq->getResult();
}
private function prepareProviders($rawResults)
{
$metadatas = [];
foreach ($rawResults as $r) {
foreach ($this->providers as $k => $p) {
if ($p->supportsResult($r['key'], $r['metadata'])) {
$metadatas[$k][] = $r['metadata'];
break;
}
}
}
foreach ($metadatas as $k => $m) {
$this->providers[$k]->prepare($m);
}
}
private function buildResults($rawResults)
{
foreach ($rawResults as $r) {
foreach ($this->providers as $k => $p) {
if ($p->supportsResult($r['key'], $r['metadata'])) {
$items[] = (new SearchApiResult($r['pertinence']))
->setResult(
$p->getResult($r['key'], $r['metadata'], $r['pertinence'])
);
break;
}
}
}
return $items ?? [];
} }
} }

View File

@@ -0,0 +1,18 @@
<?php
namespace Chill\MainBundle\Search;
use Chill\MainBundle\Search\SearchApiQuery;
interface SearchApiInterface
{
public function provideQuery(string $pattern, array $parameters): SearchApiQuery;
public function supportsTypes(string $pattern, array $types, array $parameters): bool;
public function prepare(array $metadatas): void;
public function supportsResult(string $key, array $metadatas): bool;
public function getResult(string $key, array $metadata, float $pertinence);
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Chill\MainBundle\Search;
class SearchApiQuery
{
private ?string $selectKey;
private array $selectKeyParams = [];
private ?string $jsonbMetadata;
private array $jsonbMetadataParams = [];
private ?string $pertinence;
private array $pertinenceParams = [];
private ?string $fromClause;
private array $fromClauseParams = [];
private ?string $whereClause;
private array $whereClauseParams = [];
public function setSelectKey(string $selectKey, array $params = []): self
{
$this->selectKey = $selectKey;
$this->selectKeyParams = $params;
return $this;
}
public function setSelectJsonbMetadata(string $jsonbMetadata, array $params = []): self
{
$this->jsonbMetadata = $jsonbMetadata;
$this->jsonbMetadataParams = $params;
return $this;
}
public function setSelectPertinence(string $pertinence, array $params = []): self
{
$this->pertinence = $pertinence;
$this->pertinenceParams = $params;
return $this;
}
public function setFromClause(string $fromClause, array $params = []): self
{
$this->fromClause = $fromClause;
$this->fromClauseParams = $params;
return $this;
}
public function setWhereClause(string $whereClause, array $params = []): self
{
$this->whereClause = $whereClause;
$this->whereClauseParams = $params;
return $this;
}
public function buildQuery(): string
{
return \strtr("SELECT
'{key}' AS key,
{metadata} AS metadata,
{pertinence} AS pertinence
FROM {from}
WHERE {where}
", [
'{key}' => $this->selectKey,
'{metadata}' => $this->jsonbMetadata,
'{pertinence}' => $this->pertinence,
'{from}' => $this->fromClause,
'{where}' => $this->whereClause
]);
}
public function buildParameters(): array
{
return \array_merge(
$this->selectKeyParams,
$this->jsonbMetadataParams,
$this->pertinenceParams,
$this->fromClauseParams,
$this->whereClauseParams
);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Chill\MainBundle\Search;
use Symfony\Component\Serializer\Annotation as Serializer;
class SearchApiResult
{
private float $pertinence;
private $result;
public function __construct(float $relevance)
{
$this->relevance = $relevance;
}
public function setResult($result): self
{
$this->result = $result;
return $this;
}
/**
* @Serializer\Groups({"read"})
*/
public function getResult()
{
return $this->result;
}
/**
* @Serializer\Groups({"read"})
*/
public function getRelevance(): float
{
return $this->relevance;
}
}

View File

@@ -14,7 +14,7 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
public function normalize($address, string $format = null, array $context = []) public function normalize($address, string $format = null, array $context = [])
{ {
$data['address_id'] = $address->getId(); $data['address_id'] = $address->getId();
$data['text'] = $address->getStreet().', '.$address->getStreetNumber(); $data['text'] = $address->isNoAddress() ? '' : $address->getStreetNumber().', '.$address->getStreet();
$data['street'] = $address->getStreet(); $data['street'] = $address->getStreet();
$data['streetNumber'] = $address->getStreetNumber(); $data['streetNumber'] = $address->getStreetNumber();
$data['postcode']['name'] = $address->getPostCode()->getName(); $data['postcode']['name'] = $address->getPostCode()->getName();

View File

@@ -0,0 +1,36 @@
<?php
namespace Bundle\ChillMainBundle\Tests\Controller;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\HttpFoundation\Request;
class SearchApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
/**
* @dataProvider generateSearchData
*/
public function testSearch(string $pattern, array $types)
{
$client = $this->getClientAuthenticated();
$client->request(
Request::METHOD_GET,
'/api/1.0/search.json',
[ 'q' => $pattern, 'type' => $types ]
);
$this->assertResponseIsSuccessful();
}
public function generateSearchData()
{
yield ['per', ['person', 'thirdparty'] ];
yield ['per', ['thirdparty'] ];
yield ['per', ['person'] ];
yield ['fjklmeqjfkdqjklrmefdqjklm', ['person', 'thirdparty'] ];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Chill\MainBundle\Tests\Controller;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class UserApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
public function testIndex()
{
$client = $this->getClientAuthenticated();
$client->request(Request::METHOD_GET, '/api/1.0/main/user.json');
$this->assertResponseIsSuccessful();
$data = \json_decode($client->getResponse()->getContent(), true);
$this->assertTrue(\array_key_exists('count', $data));
$this->assertGreaterThan(0, $data['count']);
$this->assertTrue(\array_key_exists('results', $data));
return $data['results'][0];
}
/**
* @depends testIndex
*/
public function testEntity($existingUser)
{
$client = $this->getClientAuthenticated();
$client->request(Request::METHOD_GET, '/api/1.0/main/user/'.$existingUser['id'].'.json');
$this->assertResponseIsSuccessful();
}
public function testWhoami()
{
$client = $this->getClientAuthenticated();
$client->request(Request::METHOD_GET, '/api/1.0/main/whoami.json');
$this->assertResponseIsSuccessful();
}
}

View File

@@ -10,6 +10,19 @@ servers:
components: components:
schemas: schemas:
User:
type: object
properties:
id:
type: integer
type:
type: string
enum:
- user
username:
type: string
text:
type: string
Center: Center:
type: object type: object
properties: properties:
@@ -425,3 +438,46 @@ paths:
description: "not found" description: "not found"
401: 401:
description: "Unauthorized" description: "Unauthorized"
/1.0/main/user.json:
get:
tags:
- user
summary: Return a list of all user
responses:
200:
description: "ok"
/1.0/main/whoami.json:
get:
tags:
- user
summary: Return the currently authenticated user
responses:
200:
description: "ok"
/1.0/main/user/{id}.json:
get:
tags:
- user
summary: Return a user by id
parameters:
- name: id
in: path
required: true
description: The user id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/User'
404:
description: "not found"
401:
description: "Unauthorized"

View File

@@ -5,6 +5,5 @@ services:
Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider' Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
Chill\MainBundle\Search\SearchApi: Chill\MainBundle\Search\SearchApi:
arguments: autowire: true
$em: '@Doctrine\ORM\EntityManagerInterface' autoconfigure: true
$search: '@Chill\MainBundle\Search\SearchProvider'

View File

@@ -7,6 +7,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent; use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
@@ -35,14 +36,18 @@ class AccompanyingCourseController extends Controller
protected ValidatorInterface $validator; protected ValidatorInterface $validator;
private AccompanyingPeriodWorkRepository $workRepository;
public function __construct( public function __construct(
SerializerInterface $serializer, SerializerInterface $serializer,
EventDispatcherInterface $dispatcher, EventDispatcherInterface $dispatcher,
ValidatorInterface $validator ValidatorInterface $validator,
AccompanyingPeriodWorkRepository $workRepository
) { ) {
$this->serializer = $serializer; $this->serializer = $serializer;
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
$this->validator = $validator; $this->validator = $validator;
$this->workRepository = $workRepository;
} }
/** /**
@@ -102,9 +107,16 @@ class AccompanyingCourseController extends Controller
['date' => 'DESC'], ['date' => 'DESC'],
); );
$works = $this->workRepository->findByAccompanyingPeriod(
$accompanyingCourse,
['startDate' => 'DESC', 'endDate' => 'DESC'],
3
);
return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [ return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [
'accompanyingCourse' => $accompanyingCourse, 'accompanyingCourse' => $accompanyingCourse,
'withoutHousehold' => $withoutHousehold, 'withoutHousehold' => $withoutHousehold,
'works' => $works,
'activities' => $activities 'activities' => $activities
]); ]);
} }

View File

@@ -47,12 +47,12 @@ class AccompanyingPeriodController extends AbstractController
* @var EventDispatcherInterface * @var EventDispatcherInterface
*/ */
protected $eventDispatcher; protected $eventDispatcher;
/** /**
* @var ValidatorInterface * @var ValidatorInterface
*/ */
protected $validator; protected $validator;
/** /**
* AccompanyingPeriodController constructor. * AccompanyingPeriodController constructor.
* *
@@ -64,23 +64,25 @@ class AccompanyingPeriodController extends AbstractController
$this->eventDispatcher = $eventDispatcher; $this->eventDispatcher = $eventDispatcher;
$this->validator = $validator; $this->validator = $validator;
} }
public function listAction(int $person_id): Response public function listAction(int $person_id): Response
{ {
$person = $this->_getPerson($person_id); $person = $this->_getPerson($person_id);
$event = new PrivacyEvent($person, [ $event = new PrivacyEvent($person, [
'element_class' => AccompanyingPeriod::class, 'element_class' => AccompanyingPeriod::class,
'action' => 'list' 'action' => 'list'
]); ]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
$accompanyingPeriods = $person->getAccompanyingPeriodsOrdered();
return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [ return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [
'accompanying_periods' => $person->getAccompanyingPeriodsOrdered(), 'accompanying_periods' => $accompanyingPeriods,
'person' => $person 'person' => $person
]); ]);
} }
public function createAction(int $person_id, Request $request): Response public function createAction(int $person_id, Request $request): Response
{ {
$person = $this->_getPerson($person_id); $person = $this->_getPerson($person_id);
@@ -90,17 +92,17 @@ class AccompanyingPeriodController extends AbstractController
$accompanyingPeriod = new AccompanyingPeriod(new \DateTime('now')); $accompanyingPeriod = new AccompanyingPeriod(new \DateTime('now'));
$accompanyingPeriod->setClosingDate(new \DateTime('now')); $accompanyingPeriod->setClosingDate(new \DateTime('now'));
$accompanyingPeriod->addPerson($person); $accompanyingPeriod->addPerson($person);
//or $person->addAccompanyingPeriod($accompanyingPeriod); //or $person->addAccompanyingPeriod($accompanyingPeriod);
$form = $this->createForm( $form = $this->createForm(
AccompanyingPeriodType::class, AccompanyingPeriodType::class,
$accompanyingPeriod, [ $accompanyingPeriod, [
'period_action' => 'create', 'period_action' => 'create',
'center' => $person->getCenter() 'center' => $person->getCenter()
]); ]);
if ($request->getMethod() === 'POST') { if ($request->getMethod() === 'POST') {
$form->handleRequest($request); $form->handleRequest($request);
$errors = $this->_validatePerson($person); $errors = $this->_validatePerson($person);
@@ -120,7 +122,7 @@ class AccompanyingPeriodController extends AbstractController
$this->generateUrl('chill_person_accompanying_period_list', [ $this->generateUrl('chill_person_accompanying_period_list', [
'person_id' => $person->getId() 'person_id' => $person->getId()
])); ]));
} else { } else {
$flashBag->add('error', $this->get('translator') $flashBag->add('error', $this->get('translator')
->trans('Error! Period not created!')); ->trans('Error! Period not created!'));
@@ -137,7 +139,7 @@ class AccompanyingPeriodController extends AbstractController
'accompanying_period' => $accompanyingPeriod 'accompanying_period' => $accompanyingPeriod
]); ]);
} }
/** /**
* @throws Exception * @throws Exception
*/ */
@@ -154,7 +156,7 @@ class AccompanyingPeriodController extends AbstractController
/** @var Person $person */ /** @var Person $person */
$person = $this->_getPerson($person_id); $person = $this->_getPerson($person_id);
// CHECK // CHECK
if (! $accompanyingPeriod->containsPerson($person)) { if (! $accompanyingPeriod->containsPerson($person)) {
throw new Exception("Accompanying period " . $period_id . " does not contain person " . $person_id); throw new Exception("Accompanying period " . $period_id . " does not contain person " . $person_id);
@@ -176,7 +178,7 @@ class AccompanyingPeriodController extends AbstractController
if ($form->isValid(['Default', 'closed']) if ($form->isValid(['Default', 'closed'])
&& count($errors) === 0) { && count($errors) === 0) {
$em->flush(); $em->flush();
$flashBag->add('success', $flashBag->add('success',
@@ -186,9 +188,9 @@ class AccompanyingPeriodController extends AbstractController
$this->generateUrl('chill_person_accompanying_period_list', [ $this->generateUrl('chill_person_accompanying_period_list', [
'person_id' => $person->getId() 'person_id' => $person->getId()
])); ]));
} else { } else {
$flashBag->add('error', $this->get('translator') $flashBag->add('error', $this->get('translator')
->trans('Error when updating the period')); ->trans('Error when updating the period'));
@@ -204,19 +206,19 @@ class AccompanyingPeriodController extends AbstractController
'accompanying_period' => $accompanyingPeriod 'accompanying_period' => $accompanyingPeriod
]); ]);
} }
/** /**
* @throws \Exception * @throws \Exception
*/ */
public function closeAction(int $person_id, Request $request): Response public function closeAction(int $person_id, Request $request): Response
{ {
$person = $this->_getPerson($person_id); $person = $this->_getPerson($person_id);
$this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person, 'You are not allowed to update this person'); $this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person, 'You are not allowed to update this person');
if ($person->isOpen() === false) { if ($person->isOpen() === false) {
$this->get('session')->getFlashBag() $this->get('session')->getFlashBag()
->add('error', $this->get('translator') ->add('error', $this->get('translator')
->trans('Beware period is closed', ['%name%' => $person->__toString()] ->trans('Beware period is closed', ['%name%' => $person->__toString()]
@@ -229,7 +231,7 @@ class AccompanyingPeriodController extends AbstractController
} }
$current = $person->getCurrentAccompanyingPeriod(); $current = $person->getCurrentAccompanyingPeriod();
$form = $this->createForm(AccompanyingPeriodType::class, $current, [ $form = $this->createForm(AccompanyingPeriodType::class, $current, [
'period_action' => 'close', 'period_action' => 'close',
'center' => $person->getCenter() 'center' => $person->getCenter()
@@ -256,7 +258,7 @@ class AccompanyingPeriodController extends AbstractController
'person_id' => $person->getId() 'person_id' => $person->getId()
]) ])
); );
} else { } else {
$this->get('session')->getFlashBag() $this->get('session')->getFlashBag()
->add('error', $this->get('translator') ->add('error', $this->get('translator')
@@ -267,7 +269,7 @@ class AccompanyingPeriodController extends AbstractController
->add('info', $error->getMessage()); ->add('info', $error->getMessage());
} }
} }
} else { //if form is not valid } else { //if form is not valid
$this->get('session')->getFlashBag() $this->get('session')->getFlashBag()
->add('error', ->add('error',
@@ -288,7 +290,7 @@ class AccompanyingPeriodController extends AbstractController
'accompanying_period' => $current 'accompanying_period' => $current
]); ]);
} }
private function _validatePerson(Person $person): ConstraintViolationListInterface private function _validatePerson(Person $person): ConstraintViolationListInterface
{ {
$errors = $this->validator->validate($person, null, $errors = $this->validator->validate($person, null,
@@ -296,10 +298,10 @@ class AccompanyingPeriodController extends AbstractController
// Can be disabled with config // Can be disabled with config
if (false === $this->container->getParameter('chill_person.allow_multiple_simultaneous_accompanying_periods')) { if (false === $this->container->getParameter('chill_person.allow_multiple_simultaneous_accompanying_periods')) {
$errors_accompanying_period = $this->validator->validate($person, null, $errors_accompanying_period = $this->validator->validate($person, null,
['accompanying_period_consistent']); ['accompanying_period_consistent']);
foreach($errors_accompanying_period as $error ) { foreach($errors_accompanying_period as $error ) {
$errors->add($error); $errors->add($error);
} }
@@ -307,7 +309,7 @@ class AccompanyingPeriodController extends AbstractController
return $errors; return $errors;
} }
public function openAction(int $person_id, Request $request): Response public function openAction(int $person_id, Request $request): Response
{ {
$person = $this->_getPerson($person_id); $person = $this->_getPerson($person_id);
@@ -384,7 +386,7 @@ class AccompanyingPeriodController extends AbstractController
'accompanying_period' => $accompanyingPeriod 'accompanying_period' => $accompanyingPeriod
]); ]);
} }
public function reOpenAction(int $person_id, int $period_id, Request $request): Response public function reOpenAction(int $person_id, int $period_id, Request $request): Response
{ {
/** @var Person $person */ /** @var Person $person */
@@ -392,7 +394,7 @@ class AccompanyingPeriodController extends AbstractController
/* @var $period AccompanyingPeriod */ /* @var $period AccompanyingPeriod */
$period = \array_filter( $period = \array_filter(
$person->getAccompanyingPeriods(), $person->getAccompanyingPeriods(),
function (AccompanyingPeriod $p) use ($period_id) { function (AccompanyingPeriod $p) use ($period_id) {
return $p->getId() === ($period_id); return $p->getId() === ($period_id);
} }
@@ -417,13 +419,13 @@ class AccompanyingPeriodController extends AbstractController
return $this->redirectToRoute('chill_person_accompanying_period_list', [ return $this->redirectToRoute('chill_person_accompanying_period_list', [
'person_id' => $person->getId() 'person_id' => $person->getId()
]); ]);
} elseif ($confirm === false && $period->canBeReOpened($person)) { } elseif ($confirm === false && $period->canBeReOpened($person)) {
return $this->render('ChillPersonBundle:AccompanyingPeriod:re_open.html.twig', [ return $this->render('ChillPersonBundle:AccompanyingPeriod:re_open.html.twig', [
'period' => $period, 'period' => $period,
'person' => $person 'person' => $person
]); ]);
} else { } else {
return (new Response()) return (new Response())
->setStatusCode(Response::HTTP_BAD_REQUEST) ->setStatusCode(Response::HTTP_BAD_REQUEST)

View File

@@ -4,15 +4,50 @@ namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class HouseholdApiController extends ApiController class HouseholdApiController extends ApiController
{ {
private HouseholdRepository $householdRepository;
public function __construct(HouseholdRepository $householdRepository)
{
$this->householdRepository = $householdRepository;
}
public function householdAddressApi($id, Request $request, string $_format): Response public function householdAddressApi($id, Request $request, string $_format): Response
{ {
return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]); return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]);
} }
/**
* Find Household of people participating to the same AccompanyingPeriod
*
* @ParamConverter("person", options={"id" = "person_id"})
*/
public function suggestHouseholdByAccompanyingPeriodParticipationApi(Person $person, string $_format)
{
// TODO add acl
$count = $this->householdRepository->countByAccompanyingPeriodParticipation($person);
$paginator = $this->getPaginatorFactory()->create($count);
if ($count === 0) {
$households = [];
} else {
$households = $this->householdRepository->findByAccompanyingPeriodParticipation($person,
$paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
}
$collection = new Collection($households, $paginator);
return $this->json($collection, Response::HTTP_OK, [],
[ "groups" => ["read"]]);
}
} }

View File

@@ -30,7 +30,7 @@ class HouseholdController extends AbstractController
$this->translator = $translator; $this->translator = $translator;
$this->positionRepository = $positionRepository; $this->positionRepository = $positionRepository;
} }
/** /**
* @Route( * @Route(
* "/{household_id}/summary", * "/{household_id}/summary",
@@ -47,7 +47,7 @@ class HouseholdController extends AbstractController
->findByActiveOrdered() ->findByActiveOrdered()
; ;
// little performance improvement: // little performance improvement:
// initialize members collection, which will avoid // initialize members collection, which will avoid
// some queries // some queries
$household->getMembers()->initialize(); $household->getMembers()->initialize();
@@ -67,6 +67,31 @@ class HouseholdController extends AbstractController
); );
} }
/**
* @Route(
* "/{household_id}/accompanying-period",
* name="chill_person_household_accompanying_period",
* methods={"GET", "HEAD"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function accompanyingPeriod(Request $request, Household $household)
{
// TODO ACL
$members = $household->getMembers();
foreach($members as $m) {
$accompanyingPeriods = $m->getPerson()->getAccompanyingPeriods();
}
return $this->render('@ChillPerson/Household/accompanying_period.html.twig',
[
'household' => $household,
'accompanying_periods' => $accompanyingPeriods
]
);
}
/** /**
* @Route( * @Route(
* "/{household_id}/addresses", * "/{household_id}/addresses",

View File

@@ -3,6 +3,7 @@
namespace Chill\PersonBundle\Controller; namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Entity\Household\Position; use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\Household;
@@ -25,11 +26,17 @@ class HouseholdMemberController extends ApiController
private TranslatorInterface $translator; private TranslatorInterface $translator;
private AccompanyingPeriodRepository $periodRepository;
public function __construct(UrlGeneratorInterface $generator, TranslatorInterface $translator) public function __construct(
UrlGeneratorInterface $generator,
TranslatorInterface $translator,
AccompanyingPeriodRepository $periodRepository
)
{ {
$this->generator = $generator; $this->generator = $generator;
$this->translator = $translator; $this->translator = $translator;
$this->periodRepository = $periodRepository;
} }
/** /**
@@ -144,8 +151,23 @@ class HouseholdMemberController extends ApiController
'allowLeaveWithoutHousehold' => $allowLeaveWithoutHousehold ?? $request->query->has('allow_leave_without_household'), 'allowLeaveWithoutHousehold' => $allowLeaveWithoutHousehold ?? $request->query->has('allow_leave_without_household'),
]; ];
// context
if ($request->query->has('accompanying_period_id')) {
$period = $this->periodRepository->find(
$request->query->getInt('accompanying_period_id')
);
if ($period === null) {
throw $this->createNotFoundException('period not found');
}
// TODO add acl on accompanying Course
}
return $this->render('@ChillPerson/Household/members_editor.html.twig', [ return $this->render('@ChillPerson/Household/members_editor.html.twig', [
'data' => $data 'data' => $data,
'expandSuggestions' => (int) $request->query->getBoolean('expand_suggestions', false),
'accompanyingCourse' => $period ?? null,
]); ]);
} }

View File

@@ -565,6 +565,13 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
], ],
'controller_action' => 'householdAddressApi' 'controller_action' => 'householdAddressApi'
], ],
'suggestHouseholdByAccompanyingPeriodParticipation' => [
'path' => '/suggest/by-person/{person_id}/through-accompanying-period-participation.{_format}',
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
]
]
] ]
], ],
[ [

View File

@@ -9,22 +9,22 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class HouseholdMenuBuilder implements LocalMenuBuilderInterface class HouseholdMenuBuilder implements LocalMenuBuilderInterface
{ {
/** /**
* @var TranslatorInterface * @var TranslatorInterface
*/ */
protected $translator; protected $translator;
public function __construct(TranslatorInterface $translator) public function __construct(TranslatorInterface $translator)
{ {
$this->translator = $translator; $this->translator = $translator;
} }
public static function getMenuIds(): array public static function getMenuIds(): array
{ {
return [ 'household' ]; return [ 'household' ];
} }
public function buildMenu($menuId, MenuItem $menu, array $parameters): void public function buildMenu($menuId, MenuItem $menu, array $parameters): void
{ {
$household = $parameters['household']; $household = $parameters['household'];
@@ -35,15 +35,22 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface
'household_id' => $household->getId() 'household_id' => $household->getId()
]]) ]])
->setExtras(['order' => 10]); ->setExtras(['order' => 10]);
$menu->addChild($this->translator->trans('household.Accompanying period'), [
'route' => 'chill_person_household_accompanying_period',
'routeParameters' => [
'household_id' => $household->getId()
]])
->setExtras(['order' => 20]);
$menu->addChild($this->translator->trans('household.Addresses'), [ $menu->addChild($this->translator->trans('household.Addresses'), [
'route' => 'chill_person_household_addresses', 'route' => 'chill_person_household_addresses',
'routeParameters' => [ 'routeParameters' => [
'household_id' => $household->getId() 'household_id' => $household->getId()
]]) ]])
->setExtras(['order' => 30]); ->setExtras(['order' => 30]);
} }
} }

View File

@@ -25,8 +25,9 @@ namespace Chill\PersonBundle\Repository;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class AccompanyingPeriodRepository final class AccompanyingPeriodRepository implements ObjectRepository
{ {
private EntityRepository $repository; private EntityRepository $repository;
@@ -34,4 +35,32 @@ final class AccompanyingPeriodRepository
{ {
$this->repository = $entityManager->getRepository(AccompanyingPeriod::class); $this->repository = $entityManager->getRepository(AccompanyingPeriod::class);
} }
public function find($id): ?AccompanyingPeriod
{
return $this->repository->find($id);
}
/**
* @return AccompanyingPeriod[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): ?AccompanyingPeriod
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?AccompanyingPeriod
{
return $this->findOneBy($criteria);
}
public function getClassName()
{
return AccompanyingPeriod::class;
}
} }

View File

@@ -3,15 +3,102 @@
namespace Chill\PersonBundle\Repository\Household; namespace Chill\PersonBundle\Repository\Household;
use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\ObjectRepository;
final class HouseholdRepository final class HouseholdRepository implements ObjectRepository
{ {
private EntityRepository $repository; private EntityRepository $repository;
private EntityManagerInterface $em;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {
$this->repository = $entityManager->getRepository(Household::class); $this->repository = $entityManager->getRepository(Household::class);
$this->em = $entityManager;
} }
public function find($id)
{
return $this->repository->find($id);
}
public function findAll()
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria)
{
return $this->findOneBy($criteria);
}
public function getClassName()
{
return Household::class;
}
public function countByAccompanyingPeriodParticipation(Person $person)
{
return $this->buildQueryByAccompanyingPeriodParticipation($person, true);
}
public function findByAccompanyingPeriodParticipation(Person $person, int $limit, int $offset)
{
return $this->buildQueryByAccompanyingPeriodParticipation($person, false, $limit, $offset);
}
private function buildQueryByAccompanyingPeriodParticipation(Person $person, bool $isCount = false, int $limit = 50, int $offset = 0)
{
$rsm = new ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata(Household::class, 'h');
if ($isCount) {
$rsm->addScalarResult('count', 'count');
$sql = \strtr(self::SQL_BY_ACCOMPANYING_PERIOD_PARTICIPATION, [
'{select}' => 'COUNT(households.*) AS count',
'{limits}' => ''
]);
} else {
$sql = \strtr(self::SQL_BY_ACCOMPANYING_PERIOD_PARTICIPATION, [
'{select}' => $rsm->generateSelectClause(['h' => 'households']),
'{limits}' => "OFFSET {$offset} LIMIT {$limit}"
]);
}
$native = $this->em->createNativeQuery($sql, $rsm);
$native->setParameters([0 => $person->getId(), 1 => $person->getId()]);
if ($isCount) {
return $native->getSingleScalarResult();
} else {
return $native->getResult();
}
}
private CONST SQL_BY_ACCOMPANYING_PERIOD_PARTICIPATION = <<<SQL
WITH participations AS (
SELECT DISTINCT part.accompanyingperiod_id
FROM chill_person_accompanying_period_participation AS part
WHERE person_id = ?),
other_participants AS (
SELECT person_id, startDate, endDate
FROM chill_person_accompanying_period_participation
JOIN participations USING (accompanyingperiod_id)
WHERE person_id != ?
),
households AS (SELECT DISTINCT household.*
FROM chill_person_household_members AS hmembers
JOIN other_participants AS op USING (person_id)
JOIN chill_person_household AS household ON hmembers.household_id = household.id
WHERE daterange(op.startDate, op.endDate) && daterange(hmembers.startDate, hmembers.endDate)
)
SELECT {select} FROM households {limits}
SQL;
} }

View File

@@ -37,6 +37,11 @@ final class PersonRepository
return $this->repository->find($id, $lockMode, $lockVersion); return $this->repository->find($id, $lockMode, $lockVersion);
} }
public function findByIds($ids): array
{
return $this->repository->findBy(['id' => $ids]);
}
/** /**
* @param $centers * @param $centers
* @param $firstResult * @param $firstResult

View File

@@ -1,3 +1,4 @@
require('./sass/chillperson.scss'); require('./sass/chillperson.scss');
require('./sass/person_with_period.scss'); require('./sass/person_with_period.scss');
require('./sass/household_banner.scss'); require('./sass/household_banner.scss');
require('./sass/accompanying_period_work.scss');

View File

@@ -46,6 +46,15 @@
} }
} }
&.short {
.item {
padding-bottom: 0;
ul.record_actions {
margin-bottom: 0;
}
}
}
.updatedBy { .updatedBy {
margin-top: 1rem; margin-top: 1rem;
text-align: right; text-align: right;
@@ -94,3 +103,4 @@ ul.timeline {
} }
} }
} }

View File

@@ -0,0 +1,6 @@
.chill-entity__social-action {
.badge-primary {
background-color: var(--chill-green);
}
}

View File

@@ -18,9 +18,6 @@
<dl class="content-bloc" v-if="accompanyingCourse.requestor.type === 'person'"> <dl class="content-bloc" v-if="accompanyingCourse.requestor.type === 'person'">
<dt>{{ $t('requestor.birthdate') }}</dt>
<dd>{{ $d(accompanyingCourse.requestor.birthdate.datetime, 'short') }}</dd>
<dt>{{ $t('requestor.center') }}</dt> <dt>{{ $t('requestor.center') }}</dt>
<dd>{{ accompanyingCourse.requestor.center.name }}</dd> <dd>{{ accompanyingCourse.requestor.center.name }}</dd>
@@ -118,7 +115,12 @@ export default {
get() { get() {
return this.$store.state.accompanyingCourse.requestorAnonymous; return this.$store.state.accompanyingCourse.requestorAnonymous;
} }
} },
hasRequestorPersonBirthdate() {
console.log('hasRequestorBirthdate');
console.log(this.$store.state.accompanyingCourse.requestor);
return this.$store.state.accompanyingCourse.requestor.birthdate !== null;
},
}, },
methods: { methods: {
removeRequestor() { removeRequestor() {

View File

@@ -9,11 +9,11 @@
</span> </span>
{{ resource.resource.text }} {{ resource.resource.text }}
</td> </td>
<td v-if="resource.resource.type === 'person'"> <td v-if="resource.resource.type === 'person'">
{{ $tc('person.born') }}{{ $d(resource.resource.birthdate.datetime, 'short') }} &nbsp;
</td> </td>
<td v-else-if="resource.resource.type === 'thirdparty'"> <td v-if="resource.resource.type === 'thirdparty'">
{{ resource.resource.address.text }}<br> {{ resource.resource.address.text }}<br>
{{ resource.resource.address.postcode.name }} {{ resource.resource.address.postcode.name }}
</td> </td>

View File

@@ -118,11 +118,12 @@ export default {
modal.showModal = false; modal.showModal = false;
}, },
addToHousehold() { addToHousehold() {
this.$store.dispatch('addDateToAddressAndAddressToHousehold', { this.$store.dispatch('addDateToAddressAndAddressToHousehold', {
householdId: this.householdId, householdId: this.householdId,
addressId: this.$store.state.newAddress.address_id, addressId: this.$store.state.newAddress.address_id,
body: { validFrom: {datetime: `${this.validFrom}T00:00:00+0100`}} body: { validFrom: {datetime: `${this.validFrom}T00:00:00+0100`}},
}) backUrl: this.backUrl
})
} }
} }
}; };

View File

@@ -4,7 +4,7 @@ const appMessages = {
fr: { fr: {
select_a_existing_address: 'Sélectionner une adresse existante', select_a_existing_address: 'Sélectionner une adresse existante',
create_a_new_address: 'Créer une nouvelle adresse', create_a_new_address: 'Créer une nouvelle adresse',
add_an_address_to_household: 'Déménager le ménage', add_an_address_to_household: 'Enregistrer',
validFrom: 'Date du déménagement', validFrom: 'Date du déménagement',
move_date: 'Date du déménagement', move_date: 'Date du déménagement',
back_to_the_list: 'Retour à la liste', back_to_the_list: 'Retour à la liste',

View File

@@ -88,6 +88,7 @@ const store = createStore({
commit('addAddressToHousehold', household); commit('addAddressToHousehold', household);
commit('setSuccess', true); commit('setSuccess', true);
commit('setLoading', false); commit('setLoading', false);
window.location.assign(payload.backUrl);
resolve(); resolve();
})) }))
.catch((error) => { .catch((error) => {

View File

@@ -23,6 +23,21 @@ const householdMove = (payload) => {
}); });
}; };
const fetchHouseholdSuggestionByAccompanyingPeriod = (personId) => {
const url = `/api/1.0/person/household/suggest/by-person/${personId}/through-accompanying-period-participation.json`;
return window.fetch(url)
.then(response => {
if (response.ok) {
return response.json();
}
throw Error ({m: 'Error while fetching household suggestion', status: response.status});
}).then(data => Promise.resolve(data.results))
.catch(e => console.err(e));
;
};
export { export {
householdMove, householdMove,
fetchHouseholdSuggestionByAccompanyingPeriod,
}; };

View File

@@ -14,6 +14,23 @@
</div> </div>
<ul v-if="allowChangeHousehold" class="record_actions"> <ul v-if="allowChangeHousehold" class="record_actions">
<li v-if="!showHouseholdSuggestion">
<button
class="sc-button"
@click="toggleHouseholdSuggestion"
>
{{ $tc('household_members_editor.show_household_suggestion',
countHouseholdSuggestion) }}
</button>
</li>
<li v-if="showHouseholdSuggestion">
<button
class="sc-button"
@click="toggleHouseholdSuggestion"
>
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
</li>
<li v-if="allowHouseholdCreate"> <li v-if="allowHouseholdCreate">
<button class="sc-button bt-create" @click="createHousehold"> <button class="sc-button bt-create" @click="createHousehold">
{{ $t('household_members_editor.household.create_household') }} {{ $t('household_members_editor.household.create_household') }}
@@ -30,12 +47,56 @@
</button> </button>
</li> </li>
</ul> </ul>
<div class="householdSuggestions">
<div v-if="showHouseholdSuggestion">
<p>{{ $t('household_members_editor.household_for_participants_accompanying_period') }}:</p>
<div class="householdSuggestionList">
<div
v-for="h in filterHouseholdSuggestionByAccompanyingPeriod"
class="item"
>
<household-viewer :household="h"></household-viewer>
<ul class="record_actions">
<li>
<button class="sc-button" @click="selectHousehold(h)">
{{ $t('household_members_editor.select_household') }}
</button>
</li>
</ul>
</div >
</div>
</div>
</div>
</template> </template>
<style lang="scss">
.householdSuggestionList {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
& > .item {
margin-bottom: 0.8rem;
width: calc(50% - 1rem);
border: 1px solid var(--chill-light-gray);
padding: 0.5rem 0.5rem 0 0.5rem;
ul.record_actions {
margin-bottom: 0;
}
}
}
</style>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import HouseholdViewer from 'ChillPersonAssets/vuejs/_components/Household/Household.vue'; import HouseholdViewer from 'ChillPersonAssets/vuejs/_components/Household/Household.vue';
export default { export default {
@@ -47,6 +108,12 @@ export default {
...mapGetters([ ...mapGetters([
'hasHousehold', 'hasHousehold',
'isHouseholdNew', 'isHouseholdNew',
'hasHouseholdSuggestion',
'countHouseholdSuggestion',
'filterHouseholdSuggestionByAccompanyingPeriod',
]),
...mapState([
'showHouseholdSuggestion',
]), ]),
household() { household() {
return this.$store.state.household; return this.$store.state.household;
@@ -55,6 +122,7 @@ export default {
return this.$store.state.allowHouseholdCreate; return this.$store.state.allowHouseholdCreate;
}, },
allowHouseholdSearch() { allowHouseholdSearch() {
return false;
return this.$store.state.allowHouseholdSearch; return this.$store.state.allowHouseholdSearch;
}, },
allowLeaveWithoutHousehold() { allowLeaveWithoutHousehold() {
@@ -74,6 +142,12 @@ export default {
}, },
forceLeaveWithoutHousehold() { forceLeaveWithoutHousehold() {
this.$store.dispatch('forceLeaveWithoutHousehold'); this.$store.dispatch('forceLeaveWithoutHousehold');
},
toggleHouseholdSuggestion() {
this.$store.commit('toggleHouseholdSuggestion');
},
selectHousehold(h) {
this.$store.dispatch('selectHousehold', h);
} }
}, },
}; };

View File

@@ -5,7 +5,7 @@ const appMessages = {
fr: { fr: {
household_members_editor: { household_members_editor: {
household: { household: {
no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage.", no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage. Les usagers concernés par la modification apparaitront ensuite.",
new_household: "Nouveau ménage", new_household: "Nouveau ménage",
create_household: "Créer un ménage", create_household: "Créer un ménage",
search_household: "Chercher un ménage", search_household: "Chercher un ménage",
@@ -13,7 +13,7 @@ const appMessages = {
leave_without_household: "Sans nouveau ménage" leave_without_household: "Sans nouveau ménage"
}, },
concerned: { concerned: {
title: "Usagers concernés", title: "Nouveaux membres du ménage",
add_persons: "Ajouter d'autres usagers", add_persons: "Ajouter d'autres usagers",
search: "Rechercher des usagers", search: "Rechercher des usagers",
move_to: "Déplacer vers", move_to: "Déplacer vers",
@@ -29,6 +29,10 @@ const appMessages = {
remove_position: "Retirer des {position}", remove_position: "Retirer des {position}",
remove_concerned: "Ne plus transférer", remove_concerned: "Ne plus transférer",
household_part: "Ménage de destination", household_part: "Ménage de destination",
hide_household_suggestion: "Masquer les suggestions",
show_household_suggestion: 'Aucune suggestion | Afficher une suggestion | Afficher {count} suggestions',
household_for_participants_accompanying_period: "Ces ménages partagent le même parcours",
select_household: "Choisir ce ménage",
dates_title: "Période de validité", dates_title: "Période de validité",
dates: { dates: {
start_date: "Début de validité", start_date: "Début de validité",

View File

@@ -1,5 +1,5 @@
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import { householdMove, householdMoveTest } from './../api.js'; import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod } from './../api.js';
import { datetimeToISO } from 'ChillMainAssets/js/date.js'; import { datetimeToISO } from 'ChillMainAssets/js/date.js';
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production';
@@ -14,6 +14,8 @@ const concerned = window.household_members_editor_data.persons.map(p => {
}; };
}); });
console.log('expand suggestions', window.household_members_editor_expand_suggestions === 1);
const store = createStore({ const store = createStore({
strict: debug, strict: debug,
state: { state: {
@@ -33,11 +35,16 @@ const store = createStore({
allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch, allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch,
allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold, allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold,
forceLeaveWithoutHousehold: false, forceLeaveWithoutHousehold: false,
householdSuggestionByAccompanyingPeriod: [],
showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1,
warnings: [], warnings: [],
errors: [] errors: []
}, },
getters: { getters: {
isHouseholdNew(state) { isHouseholdNew(state) {
if (state.household === null) {
return false;
}
return !Number.isInteger(state.household.id); return !Number.isInteger(state.household.id);
}, },
hasHousehold(state) { hasHousehold(state) {
@@ -46,6 +53,21 @@ const store = createStore({
hasHouseholdOrLeave(state) { hasHouseholdOrLeave(state) {
return state.household !== null || state.forceLeaveWithoutHousehold; return state.household !== null || state.forceLeaveWithoutHousehold;
}, },
hasHouseholdSuggestion(state, getters) {
return getters.filterHouseholdSuggestionByAccompanyingPeriod.length > 0;
},
countHouseholdSuggestion(state, getters) {
return getters.filterHouseholdSuggestionByAccompanyingPeriod.length;
},
filterHouseholdSuggestionByAccompanyingPeriod(state) {
if (state.household === null) {
return state.householdSuggestionByAccompanyingPeriod;
}
return state.householdSuggestionByAccompanyingPeriod
.filter(h => h.id !== state.household.id)
;
},
hasPersonsWellPositionnated(state, getters) { hasPersonsWellPositionnated(state, getters) {
return getters.needsPositionning === false return getters.needsPositionning === false
|| (getters.persons.length > 0 && getters.concUnpositionned.length === 0); || (getters.persons.length > 0 && getters.concUnpositionned.length === 0);
@@ -172,9 +194,25 @@ const store = createStore({
state.household = null; state.household = null;
state.forceLeaveWithoutHousehold = true; state.forceLeaveWithoutHousehold = true;
}, },
selectHousehold(state, household) {
state.household = household;
state.forceLeaveWithoutHousehold = false;
},
setHouseholdSuggestionByAccompanyingPeriod(state, households) {
let existingIds = state.householdSuggestionByAccompanyingPeriod
.map(h => h.id);
for (let i in households) {
if (!existingIds.includes(households[i].id)) {
state.householdSuggestionByAccompanyingPeriod.push(households[i]);
}
}
},
setStartDate(state, dateI) { setStartDate(state, dateI) {
state.startDate = dateI; state.startDate = dateI;
}, },
toggleHouseholdSuggestion(state) {
state.showHouseholdSuggestion = !state.showHouseholdSuggestion;
},
setWarnings(state, warnings) { setWarnings(state, warnings) {
state.warnings = warnings; state.warnings = warnings;
// reset errors, which should come from servers // reset errors, which should come from servers
@@ -213,6 +251,10 @@ const store = createStore({
commit('forceLeaveWithoutHousehold'); commit('forceLeaveWithoutHousehold');
dispatch('computeWarnings'); dispatch('computeWarnings');
}, },
selectHousehold({ commit }, h) {
commit('selectHousehold', h);
dispatch('computeWarnings');
},
setStartDate({ commit, dispatch }, date) { setStartDate({ commit, dispatch }, date) {
commit('setStartDate', date); commit('setStartDate', date);
dispatch('computeWarnings'); dispatch('computeWarnings');
@@ -220,6 +262,12 @@ const store = createStore({
setComment({ commit }, payload) { setComment({ commit }, payload) {
commit('setComment', payload); commit('setComment', payload);
}, },
fetchHouseholdSuggestionForConcerned({ commit, state }, person) {
fetchHouseholdSuggestionByAccompanyingPeriod(person.id)
.then(households => {
commit('setHouseholdSuggestionByAccompanyingPeriod', households);
});
},
computeWarnings({ commit, state, getters }) { computeWarnings({ commit, state, getters }) {
let warnings = [], let warnings = [],
payload; payload;
@@ -255,7 +303,7 @@ const store = createStore({
if (household.type === 'household') { if (household.type === 'household') {
household_id = household.id; household_id = household.id;
// nothing to do anymore here, bye-bye ! // nothing to do anymore here, bye-bye !
window.location.replace(`/fr/person/household/${household_id}/members`); window.location.replace(`/fr/person/household/${household_id}/summary`);
} else { } else {
// we assume the answer was 422... // we assume the answer was 422...
error = household; error = household;
@@ -274,4 +322,10 @@ const store = createStore({
store.dispatch('computeWarnings'); store.dispatch('computeWarnings');
if (concerned.length > 0) {
concerned.forEach(c => {
store.dispatch('fetchHouseholdSuggestionForConcerned', c.person);
});
}
export { store }; export { store };

View File

@@ -4,8 +4,12 @@
<span class="name"> <span class="name">
{{ item.result.text }} {{ item.result.text }}
</span> </span>
<span class="birthday"> <span class="birthday" v-if="hasBirthdate">
{{ $d(item.result.birthdate.datetime, 'short') }} {{ $d(item.result.birthdate.datetime, 'short') }}
</span>
<span class="location" v-if="hasAddress">
{{ item.result.current_household_address.text }} -
{{ item.result.current_household_address.postcode.name }}
</span> </span>
</div> </div>
@@ -29,8 +33,16 @@ import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
export default { export default {
name: 'SuggestionPerson', name: 'SuggestionPerson',
components: { components: {
OnTheFly OnTheFly,
}, },
props: ['item'] props: ['item'],
computed: {
hasBirthdate() {
return this.item.result.birthdate !== null;
},
hasAddress() {
return this.item.result.current_household_address !== null;
}
}
} }
</script> </script>

View File

@@ -39,12 +39,15 @@
<ul> <ul>
{% for p in withoutHousehold %} {% for p in withoutHousehold %}
<li> <li>
<input type="checkbox" name="persons[]" value="{{ p.id }}" /> <input type="checkbox" name="persons[]" value="{{ p.id }}" checked />
{{ p|chill_entity_render_box }} {{ p|chill_entity_render_box }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<input type="hidden" name="expand_suggestions" value="true" />
<input type="hidden" name="accompanying_period_id", value="{{ accompanyingCourse.id }}" />
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<button type="submit" class="sc-button bt-edit"> <button type="submit" class="sc-button bt-edit">
@@ -91,7 +94,7 @@
<li> <li>
<i class="fa fa-li fa-map-marker"></i> <i class="fa fa-li fa-map-marker"></i>
{%- if p.person.lastAddress is not empty -%} {%- if p.person.lastAddress is not empty -%}
{{ p.person.getLastAddress|chill_entity_render_box({'with_valid_from': false}) }} {{ p.person.currentHouseholdAddress|chill_entity_render_box({'with_valid_from': false}) }}
{%- else -%} {%- else -%}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span> <span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{%- endif -%} {%- endif -%}
@@ -110,7 +113,7 @@
) }}" ) }}"
class="sc-button"> class="sc-button">
<i class="fa fa-home"></i> <i class="fa fa-home"></i>
{{ 'household.Household file'|trans }} n°&nbsp;{{ p.person.getCurrentHousehold.id }}
</a> </a>
</li> </li>
{% endif %} {% endif %}
@@ -122,111 +125,240 @@
{% endfor %} {% endfor %}
</div> </div>
<h2>{{ 'Requestor'|trans }}</h2>
{% if accompanyingCourse.requestor == null %}
<p>{{ 'Any requestor to this accompanying course'|trans }}</p>
{% elseif accompanyingCourse.requestorPerson is not empty %}
{% set r = accompanyingCourse.requestorPerson %}
<div class="item-row">
<div class="item-col">
<h3>
{{ r.firstname ~ ' ' ~ r.lastname }}
<span class="badge badge-pill badge-secondary">{{ 'Usager' }}</span>
</h3>
<p>
{% set born = (r.gender == 'woman') ? 'née': 'né' %}
{% set gender = (r.gender == 'woman') ? 'fa-venus' :
(r.gender == 'man') ? 'fa-mars' : 'fa-neuter' %}
{% set genderTitle = (r.gender == 'woman') ? 'femme' :
(r.gender == 'homme') ? 'fa-mars' : 'neutre' %}
<i class="fa fa-fw {{ gender }}" title="{{ genderTitle }}"></i>{{ born ~ ' le ' ~ r.birthdate|format_date('short') }}
</p>
</div>
<div class="item-col">
<ul class="list-content fa-ul">
<li>
{% if r.mobilenumber %}
<i class="fa fa-li fa-mobile"></i><a href="{{ 'tel:' ~ r.mobilenumber }}">{{ r.mobilenumber }}</a>
{% else %}
<i class="fa fa-li fa-phone"></i>
{% if r.phonenumber %}
<a href="{{ 'tel:' ~ r.phonenumber }}">{{ r.phonenumber }}</a>
{% else %}
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
{% endif %}
{% endif %}
</li>
<li>
<i class="fa fa-li fa-map-marker"></i>
{%- if r.lastAddress is not empty -%}
{{ r.getLastAddress|chill_entity_render_box({'with_valid_from': false}) }}
{%- else -%}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{%- endif -%}
</li>
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_person_view', { person_id: r.id }) }}" class="sc-button bt-show" target="_blank" title="Voir"></a>
</li>
</ul>
</div>
</div>
{% elseif accompanyingCourse.requestorThirdParty is not empty %}
{% set r = accompanyingCourse.requestorThirdParty %}
<div class="item-row">
<div class="item-col">
<h3>
{{ r.name }}
<span class="badge badge-pill badge-secondary">{{ 'Tiers' }}</span>
</h3>
</div>
<div class="item-col">
<ul class="list-content fa-ul">
<li><i class="fa fa-li fa-envelope-o"></i>
<a href="{{ 'mailto:' ~ r.email }}">
{{ r.email|chill_print_or_message("thirdparty.No_email") }}
</a>
</li>
<li><i class="fa fa-li fa-phone"></i>
{% if r.telephone %}
<a href="{{ 'tel:' ~ r.telephone }}">{{ r.telephone }}</a>
{% else %}
<span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span>
{% endif %}
</li>
<li><i class="fa fa-li fa-map-marker"></i>
{%- if r.address is not empty -%}
{{ r.getAddress|chill_entity_render_box({'with_valid_from': false}) }}
{%- else -%}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{%- endif -%}
</li>
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_3party_3party_show', { thirdparty_id: r.id }) }}" class="sc-button bt-show" target="_blank" title="Voir"></a>
</li>
</ul>
</div>
</div>
{% endif %}
<h2>{{ 'Resources'|trans }}</h2> <h2>{{ 'Resources'|trans }}</h2>
<div class="flex-bloc">
{% for r in accompanyingCourse.resources %}
<div class="item-bloc">
{% if r.person %}
<div class="item-row"> {% if accompanyingCourse.resources|length == 0 %}
<div class="item-col"> <p class="chill-no-data-statement">{{ 'Any resource for this accompanying course'|trans }}</p>
<h3> {% else %}
{{ r.person.firstname ~ ' ' ~ r.person.lastname }} <div class="flex-bloc">
<span class="badge badge-pill badge-secondary">{{ 'Usager' }}</span> {% for r in accompanyingCourse.resources %}
</h3> <div class="item-bloc">
<p> {% if r.person %}
{% set born = (r.person.gender == 'woman') ? 'née': 'né' %}
{% set gender = (r.person.gender == 'woman') ? 'fa-venus' :
(r.person.gender == 'man') ? 'fa-mars' : 'fa-neuter' %}
{% set genderTitle = (r.person.gender == 'woman') ? 'femme' :
(r.person.gender == 'homme') ? 'fa-mars' : 'neutre' %}
<i class="fa fa-fw {{ gender }}" title="{{ genderTitle }}"></i>{{ born ~ ' le ' ~ r.person.birthdate|format_date('short') }}
</p>
</div>
<div class="item-col">
<ul class="list-content fa-ul">
<li>
{% if r.person.mobilenumber %}
<i class="fa fa-li fa-mobile"></i><a href="{{ 'tel:' ~ r.person.mobilenumber }}">{{ r.person.mobilenumber }}</a>
{% else %}
<i class="fa fa-li fa-phone"></i>
{% if r.person.phonenumber %}
<a href="{{ 'tel:' ~ r.person.phonenumber }}">{{ r.person.phonenumber }}</a>
{% else %}
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
{% endif %}
{% endif %}
</li>
<li>
<i class="fa fa-li fa-map-marker"></i>
{%- if r.person.lastAddress is not empty -%}
{{ r.person.getLastAddress|chill_entity_render_box({'with_valid_from': false}) }}
{%- else -%}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{%- endif -%}
</li>
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_person_view', { person_id: r.person.id }) }}" class="sc-button bt-show" target="_blank" title="Voir"></a>
</li>
</ul>
</div>
</div>
{% endif %} <div class="item-row">
{% if r.thirdParty %} <div class="item-col">
<h3>
{{ r.person.firstname ~ ' ' ~ r.person.lastname }}
<span class="badge badge-pill badge-secondary">{{ 'Usager' }}</span>
</h3>
<p>
{% set born = (r.person.gender == 'woman') ? 'née': 'né' %}
{% set gender = (r.person.gender == 'woman') ? 'fa-venus' :
(r.person.gender == 'man') ? 'fa-mars' : 'fa-neuter' %}
{% set genderTitle = (r.person.gender == 'woman') ? 'femme' :
(r.person.gender == 'homme') ? 'fa-mars' : 'neutre' %}
<i class="fa fa-fw {{ gender }}" title="{{ genderTitle }}"></i>{{ born ~ ' le ' ~ r.person.birthdate|format_date('short') }}
</p>
</div>
<div class="item-col">
<ul class="list-content fa-ul">
<li>
{% if r.person.mobilenumber %}
<i class="fa fa-li fa-mobile"></i><a href="{{ 'tel:' ~ r.person.mobilenumber }}">{{ r.person.mobilenumber }}</a>
{% else %}
<i class="fa fa-li fa-phone"></i>
{% if r.person.phonenumber %}
<a href="{{ 'tel:' ~ r.person.phonenumber }}">{{ r.person.phonenumber }}</a>
{% else %}
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
{% endif %}
{% endif %}
</li>
<li>
<i class="fa fa-li fa-map-marker"></i>
{%- if r.person.lastAddress is not empty -%}
{{ r.person.getLastAddress|chill_entity_render_box({'with_valid_from': false}) }}
{%- else -%}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{%- endif -%}
</li>
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_person_view', { person_id: r.person.id }) }}" class="sc-button bt-show" target="_blank" title="Voir"></a>
</li>
</ul>
</div>
</div>
<div class="item-row"> <<<<<<< HEAD
<div class="item-col"> {% endif %}
<h3> {% if r.thirdParty %}
{{ r.thirdParty.name }}
<span class="badge badge-pill badge-secondary">{{ 'Tiers' }}</span>
</h3>
</div>
<div class="item-col">
<ul class="list-content fa-ul">
<li><i class="fa fa-li fa-envelope-o"></i>
<a href="{{ 'mailto:' ~ r.thirdParty.email }}">
{{ r.thirdParty.email|chill_print_or_message("thirdparty.No_email") }}
</a>
</li>
<li><i class="fa fa-li fa-phone"></i>
{% if r.thirdParty.telephone %}
<a href="{{ 'tel:' ~ r.thirdParty.telephone }}">{{ r.thirdParty.telephone }}</a>
{% else %}
<span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span>
{% endif %}
</li>
<li><i class="fa fa-li fa-map-marker"></i>
{%- if r.thirdParty.address is not empty -%}
{{ r.thirdParty.getAddress|chill_entity_render_box({'with_valid_from': false}) }}
{%- else -%}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{%- endif -%}
</li>
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_3party_3party_show', { thirdparty_id: r.thirdParty.id }) }}" class="sc-button bt-show" target="_blank" title="Voir"></a>
</li>
</ul>
</div>
</div>
{% endif %} <div class="item-row">
</div> <div class="item-col">
{% endfor %} <h3>
</div> {{ r.thirdParty.name }}
<span class="badge badge-pill badge-secondary">{{ 'Tiers' }}</span>
</h3>
</div>
<div class="item-col">
<ul class="list-content fa-ul">
<li><i class="fa fa-li fa-envelope-o"></i>
<a href="{{ 'mailto:' ~ r.thirdParty.email }}">
{{ r.thirdParty.email|chill_print_or_message("thirdparty.No_email") }}
</a>
</li>
<li><i class="fa fa-li fa-phone"></i>
{% if r.thirdParty.telephone %}
<a href="{{ 'tel:' ~ r.thirdParty.telephone }}">{{ r.thirdParty.telephone }}</a>
{% else %}
<span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span>
{% endif %}
</li>
<li><i class="fa fa-li fa-map-marker"></i>
{%- if r.thirdParty.address is not empty -%}
{{ r.thirdParty.getAddress|chill_entity_render_box({'with_valid_from': false}) }}
{%- else -%}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{%- endif -%}
</li>
</ul>
<ul class="record_actions">
<li>
<a href="{{ path('chill_3party_3party_show', { thirdparty_id: r.thirdParty.id }) }}" class="sc-button bt-show" target="_blank" title="Voir"></a>
</li>
</ul>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
<h2>{{ 'Social actions'|trans }}</h2> <h2>{{ 'Social actions'|trans }}</h2>
{% set person = null %} <div id="accompanying_course_work_list" class="short">
{% for w in works %}
<div class="item">
<div class="title">
<p class="title_label">{{ 'accompanying_course_work.action'|trans }}</p >
<h2 class="action_title">
{{ w.socialAction|chill_entity_render_box({ 'no-badge': false }) }}
</h2>
</div>
<div class="actions">
<ul class="record_actions">
<li>
<a
class="sc-button bt-edit"
href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}">{{ 'Edit'|trans }}</a>
</li>
</ul>
</div>
</div>
{% else %}
<p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}</p>
{% endfor %}
</div>
{% block contentActivity %} {% block contentActivity %}
{% include 'ChillActivityBundle:Activity:list.html.twig' with {'context': 'accompanyingCourse', 'context': 'person'} %} {% set person = null %}
{% include 'ChillActivityBundle:Activity:list.html.twig' with {'context': 'accompanyingCourse', 'context': 'accompanyingCourse'} %}
{% endblock %} {% endblock %}
{# ==> insert accompanyingCourse vue component #} {# ==> insert accompanyingCourse vue component #}
<div id="accompanying-course"></div> <div id="accompanying-course"></div>
{% endblock %} {% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('accompanying_course_work_list') }}
{% endblock %}

View File

@@ -12,7 +12,7 @@
<div class="title"> <div class="title">
<p class="title_label">{{ 'accompanying_course_work.action'|trans }}</p > <p class="title_label">{{ 'accompanying_course_work.action'|trans }}</p >
<h2 class="action_title"> <h2 class="action_title">
{{ w.socialAction|chill_entity_render_box({ 'no-badge': true }) }} {{ w.socialAction|chill_entity_render_box }}
</h2> </h2>
</div> </div>
@@ -67,7 +67,7 @@
<div class="objective_results objective_results--with-objectives"> <div class="objective_results objective_results--with-objectives">
<div class="objective obj"> <div class="objective obj">
<p class="title_label">{{ 'accompanying_course_work.goal'|trans }}</p> <p class="title_label">{{ 'accompanying_course_work.goal'|trans }}</p>
<h3 class="goal_title">{{ g.goal.title|localize_translatable_string }}</h3> <h4 class="goal_title">{{ g.goal.title|localize_translatable_string }}</h4>
</div> </div>
<div class="results res"> <div class="results res">
{% if g.results|length == 0 %} {% if g.results|length == 0 %}
@@ -96,13 +96,13 @@
<li> <li>
<a <a
class="sc-button bt-edit" class="sc-button bt-edit"
href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}">{{ 'Update'|trans }}</a> href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}">{{ 'Edit'|trans }}</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
{% else %} {% else %}
<p class="chill-no-data-statement">{{ 'accompanying_course_work.No work'|trans }}</p> <p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}</p>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -0,0 +1,136 @@
{% block content %}
{% for accompanying_period in accompanying_periods %}
<div class="flex-table">
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
{{'period'|trans}} <b>#{{ accompanying_period.id }}</b>
{% if accompanying_period.emergency %}
&nbsp;
<span class="badge badge-primary">
{{- 'Emergency'|trans|upper -}}
</span>
{% endif %}
{% if accompanying_period.confidential %}
&nbsp;
<span class="badge badge-primary">
{{- 'Confidential'|trans|upper -}}
</span>
{% endif %}
{% if accompanying_period.step == 'DRAFT' %}
&nbsp;
<span class="badge badge-primary">
{{- 'Draft'|trans|upper -}}
</span>
{% else %}
<span class="badge badge-primary">
{{- 'Confirmed'|trans|upper -}}
</span>
{% endif %}
</div>
<div class="item-col">
{% if chill_accompanying_periods.fields.user == 'visible' %}
{% if accompanying_period.user %}
{{ accompanying_period.user.username }}
{% else %}
<span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
{% endif %}
{% endif %}
</div>
</div>
<div class="item-row">
{% if accompanying_period.closingDate == null %}
{{ 'accompanying_period.dates_from_%opening_date%'|trans({ '%opening_date%': accompanying_period.openingDate|format_date('long') } ) }}
{% else %}
{{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({
'%opening_date%': accompanying_period.openingDate|format_date('long'),
'%closing_date%': accompanying_period.closingDate|format_date('long')}
) }}
{% if accompanying_period.isOpen == false %}
<dl class="chill_view_data">
<dt>{{ 'Closing motive'|trans }}&nbsp;:</dt>
<dd>{{ accompanying_period.closingMotive|chill_entity_render_box }}</dd>
</dl>
{% endif %}
{% endif %}
</div>
<div class="item-col">
<h3>{{ 'Participants'|trans }}</h3>
{% if accompanying_period.participations.count > 0 %}
{% for p in accompanying_period.participations %}
<p>
<a href="{{ path('chill_person_accompanying_period_list', { person_id: p.person.id }) }}">
{{ p.person.firstname ~ ' ' ~ p.person.lastname }}
</a>
</p>
{% endfor %}
{% else %}
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
{% endif %}
</div>
<div class="item-col">
<h3>{{ 'Requestor'|trans }}</h3>
{% if accompanying_period.requestorPerson is not null or accompanying_period.requestorThirdParty is not null %}
{% if accompanying_period.requestorPerson is not null %}
<p>{{ accompanying_period.requestorPerson.firstname ~ ' ' ~ accompanying_period.requestorPerson.lastname }}</p>
{% endif %}
{% if accompanying_period.requestorThirdParty is not null %}
<p>{{ accompanying_period.requestorThirdParty.name }}</p>
{% endif %}
{% else %}
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
{% endif %}
</div>
<div class="item-col">
<h3>{{ 'Social issues'|trans }}</h3>
{% if accompanying_period.socialIssues.count > 0 %}
{% for si in accompanying_period.socialIssues %}
<p>{{ si.title|localize_translatable_string }}</p>
{% endfor %}
{% else %}
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
{% endif %}
</div>
<div class="item-row">
<ul class="record_actions">
{# TODO if enable_accompanying_course_with_multiple_persons is true ... #}
<li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': accompanying_period.id }) }}" class="sc-button bt-show"></a>
</li>
{#
<li>
<a href="{{ path('chill_person_accompanying_period_update', {'person_id' : person.id, 'period_id' : accompanying_period.id } ) }}" class="sc-button bt-update no-content"></a>
</li>
{% if accompanying_period.isOpen == true %}
<li>
<a href="{{ path('chill_person_accompanying_period_close', {'person_id' : person.id}) }}" class="sc-button bt-update has-hidden change-icon">
<i class="fa fa-lock" aria-hidden="true"></i><span class="show-on-hover">{{'Close accompanying period'|trans }}</span>
</a>
</li>
{% endif %}
{% if accompanying_period.canBeReOpened(person) == true %}
<li>
<a href="{{ path('chill_person_accompanying_period_re_open', {'person_id' : person.id, 'period_id' : accompanying_period.id } ) }}" class="sc-button bt-create change-icon has-hidden">
<i class="fa fa-unlock" aria-hidden="true"></i><span class="show-on-hover">{{'Re-open accompanying period'|trans }}</span>
</a>
</li>
{% endif %}
#}
</ul>
</div>
</div>
</div>
<p></p>
{% endfor %}
{% endblock content %}

View File

@@ -5,114 +5,36 @@
{% block title %}{{ 'Person accompanying period - %name%'|trans({ '%name%' : person.__toString}) }}{% endblock title %} {% block title %}{{ 'Person accompanying period - %name%'|trans({ '%name%' : person.__toString}) }}{% endblock title %}
{% block personcontent %} {% block personcontent %}
<h1>{{ 'Accompanying period list'|trans }}</h1> <h1>{{ 'Accompanying period list'|trans }}</h1>
<table class="rounded"> {% include 'ChillPersonBundle:AccompanyingPeriod:_list.html.twig' %}
<thead>
<tr>
<th class="chill-red">{{ 'accompanying_period.dates'|trans }}</th>
{% if chill_accompanying_periods.fields.user == 'visible' %}
<th class="chill-blue">{{ 'Accompanying user'|trans }}</th>
{% endif %}
<th class="chill-orange">{{ 'Remark'|trans }}</th>
<th>&nbsp;</th>
</tr>
</thead> <ul class="record_actions sticky-form-buttons">
<tbody> <li class="cancel">
{% for accompanying_period in accompanying_periods %} <a href="{{ path ('chill_person_view', {'person_id' : person.id } ) }}" class="sc-button bt-cancel">
<tr> {{ 'Person details'|trans }}
<td> </a>
{% if accompanying_period.closingDate == null %} </li>
{{ 'accompanying_period.dates_from_%opening_date%'|trans({ '%opening_date%': accompanying_period.openingDate|format_date('long') } ) }} <li>
{% else %} <a href="{{ path ('chill_person_accompanying_course_new', {'person_id' : [ person.id ] } ) }}" class="sc-button bt-create">
{{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({ {{ 'Create an accompanying period'|trans }}
'%opening_date%': accompanying_period.openingDate|format_date('long'), </a>
'%closing_date%': accompanying_period.closingDate|format_date('long')} </li>
) }} {#
<li>
<a href="{{ path ('chill_person_accompanying_period_create', {'person_id' : person.id } ) }}" class="sc-button bt-create has-hidden">
<span class="show-on-hover">{{ 'Add an accompanying period in the past'|trans }}</span>
</a>
</li>
{% if person.isOpen == false %}
<li>
<a href="{{ path('chill_person_accompanying_period_open', {'person_id' : person.id} ) }}" class="sc-button bt-create change-icon has-hidden">
<i class="fa fa-unlock" aria-hidden="true"></i>
<span class="show-on-hover">{{'Begin a new accompanying period'|trans }}</span>
</a>
</li>
{% endif %}
#}
</ul>
{% if accompanying_period.isOpen == false %} {% endblock %}
<dl class="chill_view_data">
<dt>{{ 'Closing motive'|trans }}&nbsp;:</dt>
<dd>{{ accompanying_period.closingMotive|chill_entity_render_box }}</dd>
</dl>
{% endif %}
{% endif %}
</td>
{% if chill_accompanying_periods.fields.user == 'visible' %}
<td>
{% if accompanying_period.user %}
{{ accompanying_period.user.username }}
{% else %}
<span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
{% endif %}
</td>
{% endif %}
<td>
{% if accompanying_period is not empty %}
<blockquote class="chill-user-quote">
{{ accompanying_period.remark|chill_markdown_to_html }}
</blockquote>
{% else %}
{{ null|chill_print_or_message('No remark', 'blockquote') }}
{% endif %}
</td>
<td>
<ul class="record_actions">
{# TODO if enable_accompanying_course_with_multiple_persons is true ... #}
<li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': accompanying_period.id }) }}" class="sc-button bt-show"></a>
</li>
<li>
<a href="{{ path('chill_person_accompanying_period_update', {'person_id' : person.id, 'period_id' : accompanying_period.id } ) }}" class="sc-button bt-update no-content"></a>
</li>
{% if accompanying_period.isOpen == true %}
<li>
<a href="{{ path('chill_person_accompanying_period_close', {'person_id' : person.id}) }}" class="sc-button bt-update has-hidden change-icon">
<i class="fa fa-lock" aria-hidden="true"></i><span class="show-on-hover">{{'Close accompanying period'|trans }}</span>
</a>
</li>
{% endif %}
{% if accompanying_period.canBeReOpened(person) == true %}
<li>
<a href="{{ path('chill_person_accompanying_period_re_open', {'person_id' : person.id, 'period_id' : accompanying_period.id } ) }}" class="sc-button bt-create change-icon has-hidden">
<i class="fa fa-unlock" aria-hidden="true"></i><span class="show-on-hover">{{'Re-open accompanying period'|trans }}</span>
</a>
</li>
{% endif %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="form_control">
<ul class="record_actions">
<li class="cancel">
<a href="{{ path ('chill_person_view', {'person_id' : person.id } ) }}" class="sc-button bt-cancel">
{{ 'Person details'|trans }}
</a>
</li>
<li>
<a href="{{ path ('chill_person_accompanying_period_create', {'person_id' : person.id } ) }}" class="sc-button bt-create has-hidden">
<span class="show-on-hover">{{ 'Add an accompanying period in the past'|trans }}</span>
</a>
</li>
{% if person.isOpen == false %}
<li>
<a href="{{ path('chill_person_accompanying_period_open', {'person_id' : person.id} ) }}" class="sc-button bt-create change-icon has-hidden">
<i class="fa fa-unlock" aria-hidden="true"></i>
<span class="show-on-hover">{{'Begin a new accompanying period'|trans }}</span>
</a>
</li>
{% endif %}
</ul>
</div>
{% endblock personcontent %}

View File

@@ -0,0 +1,25 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Household accompanying period'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
{% include 'ChillPersonBundle:AccompanyingPeriod:_list.html.twig' %}
<div class="form_control">
<ul class="record_actions">
<li class="cancel">
<a href="{{ path ('chill_person_household_summary', {'household_id' : household.id } ) }}" class="sc-button bt-cancel">
{{ 'Household summary'|trans }}
</a>
</li>
{#<li>
<a href="{{ path ('chill_person_household_accompanying_course_new', {'household_id' : [ household.id ] } ) }}" class="sc-button bt-create">
{{ 'Create an accompanying period'|trans }}
</a>
</li>#}
</ul>
</div>
{% endblock %}

View File

@@ -12,11 +12,11 @@
{% else %} {% else %}
<div class="top"><i class="fa fa-caret-up fa-3x"></i></div> <div class="top"><i class="fa fa-caret-up fa-3x"></i></div>
{% endif %} {% endif %}
{% for address in household.addresses %} {% for address in household.addresses %}
<div class="col-b"></div> <div class="col-b"></div>
<div class="col-c content{{ ' row' ~ loop.index }}"> <div class="col-c content{{ ' row' ~ loop.index }}">
<div class="address"> <div class="address">
{% if address.isNoAddress == true %} {% if address.isNoAddress == true %}
@@ -39,22 +39,22 @@
{% endif %} {% endif %}
</div> </div>
<ul class="record_actions"> {#<ul class="record_actions">
<li> <li>
<a href="{{ path('chill_person_household_address_edit', { 'household_id': household.id, 'address_id' : address.id } ) }}" class="sc-button bt-edit"></a> <a href="{{ path('chill_person_household_address_edit', { 'household_id': household.id, 'address_id' : address.id } ) }}" class="sc-button bt-edit"></a>
</li> </li>
</ul> </ul> #}
</div> </div>
<div class="date"> <div class="date">
{% if address.validFrom is not empty %} {% if address.validFrom is not empty %}
{{ address.validFrom|format_date('long') }} {{ address.validFrom|format_date('long') }}
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<ul class="record_actions"> <ul class="record_actions">

View File

@@ -1,4 +1,5 @@
{% extends '@ChillMain/layout.html.twig' %} {% extends accompanyingCourse != null ? '@ChillPerson/AccompanyingCourse/layout.html.twig'
: '@ChillMain/layout.html.twig' %}
{% block title 'household.Edit household members'|trans %} {% block title 'household.Edit household members'|trans %}
@@ -14,6 +15,7 @@
{% block js %} {% block js %}
<script type="text/javascript"> <script type="text/javascript">
window.household_members_editor_data = {{ data|json_encode|raw }}; window.household_members_editor_data = {{ data|json_encode|raw }};
window.household_members_editor_expand_suggestions = {{ expandSuggestions }};
</script> </script>
{{ encore_entry_script_tags('household_members_editor') }} {{ encore_entry_script_tags('household_members_editor') }}
{% endblock %} {% endblock %}

View File

@@ -16,8 +16,8 @@
#} #}
<ul class="tab-nav"> <ul class="tab-nav">
{% for menu in menus %} {% for menu in menus %}
<li class=""> <li class="{{ menu.extra('class') }}">
<a href="{{ menu.uri }}" >{{ menu.label|upper }}</a> <a href="{{ menu.uri }}" >{{ menu.label|upper }}</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -0,0 +1,53 @@
<?php
namespace Chill\PersonBundle\Search;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\MainBundle\Search\SearchApiInterface;
class SearchPersonApiProvider implements SearchApiInterface
{
private PersonRepository $personRepository;
public function __construct(PersonRepository $personRepository)
{
$this->personRepository = $personRepository;
}
public function provideQuery(string $pattern, array $parameters): SearchApiQuery
{
$query = new SearchApiQuery();
$query
->setSelectKey("person")
->setSelectJsonbMetadata("jsonb_build_object('id', person.id)")
->setSelectPertinence("SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical)", [ $pattern ])
->setFromClause("chill_person_person AS person")
->setWhereClause("SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) > 0.20", [ $pattern ])
;
return $query;
}
public function supportsTypes(string $pattern, array $types, array $parameters): bool
{
return \in_array('person', $types);
}
public function prepare(array $metadatas): void
{
$ids = \array_map(fn($m) => $m['id'], $metadatas);
$this->personRepository->findByIds($ids);
}
public function supportsResult(string $key, array $metadatas): bool
{
return $key === 'person';
}
public function getResult(string $key, array $metadata, float $pertinence)
{
return $this->personRepository->find($metadata['id']);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Chill\PersonBundle\Tests\Controller;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Test\PrepareClientTrait;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class HouseholdApiControllerTest extends WebTestCase
{
use PrepareClientTrait;
/**
* @dataProvider generatePersonId
*/
public function testSuggestByAccompanyingPeriodParticipation(int $personId)
{
$client = $this->getClientAuthenticated();
$client->request(
Request::METHOD_GET,
"/api/1.0/person/household/suggest/by-person/{$personId}/through-accompanying-period-participation.json"
);
$this->assertResponseIsSuccessful();
}
public function generatePersonId()
{
self::bootKernel();
$qb = self::$container->get(EntityManagerInterface::class)
->createQueryBuilder();
$period = $qb
->select('ap')
->from(AccompanyingPeriod::class, 'ap')
->where(
$qb->expr()->gte('SIZE(ap.participations)', 2)
)
->getQuery()
->setMaxResults(1)
->getSingleResult()
;
$person = $period->getParticipations()
->first()->getPerson();
yield [ $person->getId() ];
}
}

View File

@@ -967,6 +967,36 @@ paths:
401: 401:
description: "Unauthorized" description: "Unauthorized"
/1.0/person/household/suggest/by-person/{person_id}/through-accompanying-period-participation.json:
get:
tags:
- household
summary: Return households associated with the given person through accompanying periods
description: |
Return households associated with the given person throught accompanying periods participation.
The current household of the given person is excluded.
parameters:
- name: person_id
in: path
required: true
description: The person's id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/Household'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/person/household/members/move.json: /1.0/person/household/members/move.json:
post: post:
tags: tags:

View File

@@ -46,6 +46,7 @@ services:
$serializer: '@Symfony\Component\Serializer\SerializerInterface' $serializer: '@Symfony\Component\Serializer\SerializerInterface'
$dispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' $dispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
$workRepository: '@Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository'
tags: ['controller.service_arguments'] tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\AccompanyingCourseApiController: Chill\PersonBundle\Controller\AccompanyingCourseApiController:

View File

@@ -28,3 +28,7 @@ services:
$em: '@Doctrine\ORM\EntityManagerInterface' $em: '@Doctrine\ORM\EntityManagerInterface'
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
Chill\PersonBundle\Search\SearchPersonApiProvider:
autowire: true
autoconfigure: true

View File

@@ -38,12 +38,13 @@ household:
Edit member household: Modifier l'appartenance au ménage Edit member household: Modifier l'appartenance au ménage
Current household members: Membres actuels Current household members: Membres actuels
Household summary: Résumé Household summary: Résumé
Accompanying period: Parcours d'accompagnement
Addresses: Adresses Addresses: Adresses
Current address: Adresse actuelle Current address: Adresse actuelle
Household does not have any address currently: Le ménage n'a pas d'adresse renseignée actuellement Household does not have any address currently: Le ménage n'a pas d'adresse renseignée actuellement
Edit household members: Modifier l'appartenance au ménage Edit household members: Modifier l'appartenance au ménage
and x other persons: >- and x other persons: >-
{x, plural, {x, plural,
one {et une autre personne} one {et une autre personne}
many {et # autres personnes} many {et # autres personnes}
other {et # autres personnes} other {et # autres personnes}
@@ -59,7 +60,7 @@ household:
data_saved: Données enregistrées data_saved: Données enregistrées
Household history for %name%: Historique des ménages pour {name} Household history for %name%: Historique des ménages pour {name}
Household shared: Ménages domiciliés Household shared: Ménages domiciliés
Household not shared: Ménage non domiciliés Household not shared: Ménage non domiciliés
Never in any household: Membre d'aucun ménage Never in any household: Membre d'aucun ménage
Membership currently running: En cours Membership currently running: En cours
from: Depuis from: Depuis

View File

@@ -165,6 +165,8 @@ An accompanying period starts: Une période d'accompagnement est ouverte
Any accompanying periods are open: Aucune période d'accompagnement ouverte Any accompanying periods are open: Aucune période d'accompagnement ouverte
An accompanying period is open: Une période d'accompagnement est ouverte An accompanying period is open: Une période d'accompagnement est ouverte
Accompanying period list: Périodes d'accompagnement Accompanying period list: Périodes d'accompagnement
Accompanying period: Parcours d'accompagnement
period: Parcours
New accompanying course: Nouveau parcours d'accompagnement New accompanying course: Nouveau parcours d'accompagnement
Choose a motive: Motif de fermeture Choose a motive: Motif de fermeture
Re-open accompanying period: Ré-ouvrir Re-open accompanying period: Ré-ouvrir
@@ -180,6 +182,8 @@ Create an accompanying course: Créer un parcours
This accompanying course is still a draft: Ce parcours est à l'état brouillon This accompanying course is still a draft: Ce parcours est à l'état brouillon
Associated peoples: Usagers concernés Associated peoples: Usagers concernés
Resources: Interlocuteurs privilégiés Resources: Interlocuteurs privilégiés
Requestor: Demandeur
Any requestor to this accompanying course: Aucun demandeur pour ce parcours
Social actions: Actions d'accompagnement Social actions: Actions d'accompagnement
Social issues: Problématiques sociales Social issues: Problématiques sociales
Last events on accompanying course: Dernières actions de suivi Last events on accompanying course: Dernières actions de suivi
@@ -189,6 +193,7 @@ See accompanying period: Voir cette période d'accompagnement
Referrer: Référent Referrer: Référent
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 appartenance à un ménage dès que possible. 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 appartenance à un ménage dès que possible.
Add to household now: Ajouter à un ménage Add to household now: Ajouter à un ménage
Any resource for this accompanying course: Aucun interlocuteur privilégié pour ce parcours
# pickAPersonType # pickAPersonType
Pick a person: Choisir une personne Pick a person: Choisir une personne
@@ -320,6 +325,9 @@ accompanying_period:
dates_from_%opening_date%_to_%closing_date%: Ouvert du %opening_date% au %closing_date% dates_from_%opening_date%_to_%closing_date%: Ouvert du %opening_date% au %closing_date%
occasional: ponctuel occasional: ponctuel
regular: régulier regular: régulier
Confidential: confidentiel
Draft: brouillon
Confirmed: en file active
# Accompanying Course # Accompanying Course
Accompanying Course: Parcours d'accompagnement Accompanying Course: Parcours d'accompagnement
@@ -337,6 +345,8 @@ Members: Membres
Addresses: Adresses Addresses: Adresses
Move household: Nouveau déménagement Move household: Nouveau déménagement
Addresses history for household: Historique des adresses Addresses history for household: Historique des adresses
Household accompanying period: Parcours d'accompagnement du ménage
Household summary: Résumé du ménage
# accompanying course work # accompanying course work
Accompanying Course Actions: Actions d'accompagnements Accompanying Course Actions: Actions d'accompagnements
@@ -353,4 +363,5 @@ accompanying_course_work:
no_results: Aucun résultat - orientation no_results: Aucun résultat - orientation
results: Résultats - orientations results: Résultats - orientations
goal: Objectif - motif - dispositif goal: Objectif - motif - dispositif
Any work: Aucune action d'accompagnement

View File

@@ -0,0 +1,48 @@
<?php
namespace Chill\ThirdPartyBundle\Search;
use Chill\MainBundle\Search\SearchApiInterface;
use Chill\MainBundle\Search\SearchApiQuery;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
class ThirdPartyApiSearch implements SearchApiInterface
{
private ThirdPartyRepository $thirdPartyRepository;
public function __construct(ThirdPartyRepository $thirdPartyRepository)
{
$this->thirdPartyRepository = $thirdPartyRepository;
}
public function provideQuery(string $pattern, array $parameters): SearchApiQuery
{
return (new SearchApiQuery)
->setSelectKey('tparty')
->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)")
->setSelectPertinence("SIMILARITY(?, LOWER(UNACCENT(tparty.name)))", [ $pattern ])
->setFromClause('chill_3party.third_party AS tparty')
->setWhereClause('SIMILARITY(LOWER(UNACCENT(?)), LOWER(UNACCENT(tparty.name))) > 0.20', [ $pattern ])
;
}
public function supportsTypes(string $pattern, array $types, array $parameters): bool
{
return \in_array('thirdparty', $types);
}
public function prepare(array $metadatas): void
{
}
public function supportsResult(string $key, array $metadatas): bool
{
return $key === 'tparty';
}
public function getResult(string $key, array $metadata, float $pertinence)
{
return $this->thirdPartyRepository->find($metadata['id']);
}
}

View File

@@ -7,3 +7,7 @@ services:
$paginatorFactory: '@Chill\MainBundle\Pagination\PaginatorFactory' $paginatorFactory: '@Chill\MainBundle\Pagination\PaginatorFactory'
tags: tags:
- { name: 'chill.search', alias: '3party' } - { name: 'chill.search', alias: '3party' }
Chill\ThirdPartyBundle\Search\ThirdPartyApiSearch:
autowire: true
autoconfigure: true