diff --git a/CHANGELOG.md b/CHANGELOG.md index ec27da83d..65b9864c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,18 @@ and this project adheres to ## Unreleased +* [main] address: use search API end points for getting postal code and reference address (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316) +* [main] address: in edit mode, select the encoded values in multiselect for address reference and city (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316) * [person search] fix bug when using birthdate after and birthdate before * [person search] increase pertinence when lastname begins with search pattern +* [activity/actions] Améliore la cohérence du design entre + * la page résumé d'un parcours (liste d'actions récentes et liste d'activités récentes) + * la page liste des actions + * la page liste des activités (contexte personne / contexte parcours) +* [household] field to edit wheter person is titulaire of household or not removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/322) +* [activity] create work if a work with same social action is not associated to the activity +* [visgraph] improve and fix bugs on vis-network relationship graph +* [bugfix] posting of birth- and deathdate through api fixed. ## Test releases @@ -23,6 +33,7 @@ and this project adheres to * [activity] layout for issues / actions * [activity][bugfix] in edit mode, the form will now load the social action list + ### Test release 2021-11-29 * [person] suggest entities (person | thirdparty) when creating/editing the accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/119) @@ -50,6 +61,9 @@ and this project adheres to * [activity] for a new activity: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties * [calendar] for a new rdv: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties + +## Test releases + ### Test release 2021-11-22 * [activity] delete admin_user_show in twig template because this route is not defined and should be defined @@ -164,7 +178,7 @@ and this project adheres to * fast creation buttons * add ordering for types -* [AccompanyingCourse Resume page] badge-title for AccompanyingCourseWork and for Activities; +* [AccompanyingCourse Resume page] dashboard for AccompanyingCourseWork and for Activities; * Improve badges behaviour with small screens; * [ThirdParty]: diff --git a/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php b/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php index e1c657947..39c7eab36 100644 --- a/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php +++ b/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php @@ -41,6 +41,7 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf $loader->load('services/form.yaml'); $loader->load('services/templating.yaml'); $loader->load('services/accompanyingPeriodConsistency.yaml'); + $loader->load('services/doctrine.entitylistener.yaml'); } public function prepend(ContainerBuilder $container) diff --git a/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php b/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php new file mode 100644 index 000000000..ab370e89e --- /dev/null +++ b/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php @@ -0,0 +1,77 @@ +em = $em; + $this->workRepository = $workRepository; + } + + public function persistActionToCourse(Activity $activity) + { + if ($activity->getAccompanyingPeriod() instanceof AccompanyingPeriod) { + $period = $activity->getAccompanyingPeriod(); + + $accompanyingCourseWorks = $this->workRepository->findByAccompanyingPeriod($period); + $periodActions = []; + $now = new DateTimeImmutable(); + + foreach ($accompanyingCourseWorks as $key => $work) { + // take only the actions which are still opened + if ($work->getEndDate() === null || $work->getEndDate() > ($activity->getDate() ?? $now)) { + $periodActions[$key] = spl_object_hash($work->getSocialAction()); + } + } + + $associatedPersons = $activity->getPersonsAssociated(); + $associatedThirdparties = $activity->getThirdParties(); + + foreach ($activity->getSocialActions() as $action) { + if (in_array(spl_object_hash($action), $periodActions, true)) { + continue; + } + $newAction = new AccompanyingPeriodWork(); + $newAction->setSocialAction($action); + $period->addWork($newAction); + + $date = DateTimeImmutable::createFromMutable($activity->getDate()); + $newAction->setStartDate($date); + + foreach ($associatedPersons as $person) { + $newAction->addPerson($person); + } + + foreach ($associatedThirdparties as $thirdparty) { + $newAction->setHandlingThierparty($thirdparty); + } + $this->em->persist($newAction); + $this->em->flush(); + } + } + } +} diff --git a/src/Bundle/ChillActivityBundle/Form/ActivityType.php b/src/Bundle/ChillActivityBundle/Form/ActivityType.php index d332acdcb..7ed80ab00 100644 --- a/src/Bundle/ChillActivityBundle/Form/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Form/ActivityType.php @@ -143,7 +143,7 @@ class ActivityType extends AbstractType return array_map( fn (string $id): ?SocialIssue => $this->om->getRepository(SocialIssue::class)->findOneBy(['id' => (int) $id]), - explode(',', $socialIssuesAsString) + explode(',', (string) $socialIssuesAsString) ); } )); @@ -169,7 +169,7 @@ class ActivityType extends AbstractType return array_map( fn (string $id): ?SocialAction => $this->om->getRepository(SocialAction::class)->findOneBy(['id' => (int) $id]), - explode(',', $socialActionsAsString) + explode(',', (string) $socialActionsAsString) ); } )); @@ -262,7 +262,7 @@ class ActivityType extends AbstractType function (?string $personsAsString): array { return array_map( fn (string $id): ?Person => $this->om->getRepository(Person::class)->findOneBy(['id' => (int) $id]), - explode(',', $personsAsString) + explode(',', (string) $personsAsString) ); } )); @@ -284,7 +284,7 @@ class ActivityType extends AbstractType function (?string $thirdpartyAsString): array { return array_map( fn (string $id): ?ThirdParty => $this->om->getRepository(ThirdParty::class)->findOneBy(['id' => (int) $id]), - explode(',', $thirdpartyAsString) + explode(',', (string) $thirdpartyAsString) ); } )); @@ -317,7 +317,7 @@ class ActivityType extends AbstractType function (?string $usersAsString): array { return array_map( fn (string $id): ?User => $this->om->getRepository(User::class)->findOneBy(['id' => (int) $id]), - explode(',', $usersAsString) + explode(',', (string) $usersAsString) ); } )); @@ -332,7 +332,7 @@ class ActivityType extends AbstractType return ''; } - return $location->getId(); + return (string) $location->getId(); }, function (?string $id): ?Location { return $this->om->getRepository(Location::class)->findOneBy(['id' => (int) $id]); @@ -379,7 +379,7 @@ class ActivityType extends AbstractType $timezoneUTC = new DateTimeZone('GMT'); /** @var DateTime $data */ $data = $formEvent->getData() === null ? - DateTime::createFromFormat('U', 300) : + DateTime::createFromFormat('U', '300') : $formEvent->getData(); $seconds = $data->getTimezone()->getOffset($data); $data->setTimeZone($timezoneUTC); diff --git a/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss b/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss index 28c02e23e..275f67950 100644 --- a/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss +++ b/src/Bundle/ChillActivityBundle/Resources/public/chill/chillactivity.scss @@ -24,14 +24,16 @@ div.new-activity-select-type { } //// ACTIVITY LIST PAGE -// precise badge-title specific details +// precise dashboard specific details +p.date-label { + display: inline-block; + margin: 0 0.5em 0 0; + font-weight: 700; + font-size: 18pt; +} +div.dashboard, h2.badge-title { - div.duration { - font-size: smaller; - padding-left: 1em; - margin-top: 1em; - } ul.list-content { font-size: 70%; list-style-type: none; @@ -39,16 +41,13 @@ h2.badge-title { margin: 0; li { margin-bottom: 0.2em; - // exception: change bg color for action badges above badge-title + // exception: change bg color for action badges above dashboard .bg-light { background-color: $chill-light-gray !important; } } } } -div.main { - padding: 1em; -} //// ACTIVITY SHOW AND FORM PAGES // Exceptions for flex-bloc in concerned-groups diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/activity-badge-title.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/activity-badge-title.html.twig deleted file mode 100644 index f51854163..000000000 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/activity-badge-title.html.twig +++ /dev/null @@ -1,110 +0,0 @@ -

- - - {% if activity.date %} -

{{ activity.date|format_date('short') }}

- {% endif %} - -
- {% if activity.durationTime and t.durationTimeVisible %} -

- - {{ activity.durationTime|date('H:i') }} -

- {% endif %} - - {% if activity.travelTime and t.travelTimeVisible %} -

- - {{ activity.travelTime|date('H:i') }} -

- {% endif %} -
- -
- - - {{ activity.type.name | localize_translatable_string }} - - {% if activity.emergency %} - {{ 'Emergency'|trans|upper }} - {% endif %} - - - - - -

- -{% if context == 'person' and activity.accompanyingPeriod is not empty %} -
- - -
-{% endif %} - diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig index f6b052265..2808cca60 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig @@ -126,7 +126,7 @@
{% if bloc.items|length > 0 %}
-

{{ bloc.title }}

+

{{ bloc.title }}

{% for item in bloc.items %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig index 7c75f5bf0..83d8cbcdb 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig @@ -10,49 +10,174 @@ {% for activity in activities %} {% set t = activity.type %}
+
- {% include '@ChillActivity/Activity/activity-badge-title.html.twig' %} +
+
+
+ + {% if activity.date %} +

+ {{ activity.date|format_date('short') }} +

+ {% endif %} + +
+
+ +

+ + + {{ activity.type.name | localize_translatable_string }} + + {% if activity.emergency %} + {{ 'Emergency'|trans|upper }} + {% endif %} + +

+ +
+
+
- {% if activity.comment.comment is not empty - or activity.persons|length > 0 - or activity.thirdParties|length > 0 - or activity.users|length > 0 - %} -
- {% if activity.comment.comment is not empty %} - {{ activity.comment|chill_entity_render_box({ - 'disable_markdown': false, - 'limit_lines': 3, - 'metadata': false, - }) }} - {% endif %} +
+
+ {% if activity.location and t.locationVisible %} +
+

{{ 'location'|trans }}

+
+

+ {{ activity.location.locationType.title|localize_translatable_string }} + {{ activity.location.name }} +

+
+
+ {% endif %} + + {% if activity.sentReceived is not empty and t.sentReceivedVisible %} +
+

{{ 'Sent received'|trans }}

+
+

+ {{ activity.sentReceived|capitalize|trans }} +

+
+
+ {% endif %} + + {% if activity.user and t.userVisible %} +
+

{{ 'Referrer'|trans }}

+
+

+ {{ activity.user.usernameCanonical|chill_entity_render_string|capitalize }} +

+
+
+ {% endif %} +
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with { 'context': context, - 'with_display': 'row', + 'with_display': 'wrap-list', 'entity': activity, 'badge_person': true } %} + +
+ {%- if activity.reasons is not empty and t.reasonsVisible -%} +
+
+

{{ 'Reasons'|trans }}

+
+
+ {% for r in activity.reasons %} +

+ {{ r|chill_entity_render_box }} +

+ {% endfor %} +
+
+ {% endif %} + + {%- if activity.socialIssues is not empty and t.socialIssuesVisible -%} +
+
+

{{ 'Social issues'|trans }}

+
+
+ {% for r in activity.socialIssues %} + + {% endfor %} +
+
+ {% endif %} + + {%- if activity.socialActions is not empty and t.socialActionsVisible -%} +
+
+

{{ 'Social actions'|trans }}

+
+
+ {% for r in activity.socialActions %} + + {% endfor %} +
+
+ {% endif %} + + {# SEULEMENT SI DÉTAILLÉ + {% if activity.comment.comment is not empty %} +
+
+

{{ 'Comment'|trans }}

+
+
+ {{ activity.comment|chill_entity_render_box({ + 'disable_markdown': false, + 'limit_lines': 3, + 'metadata': false + }) }} +
+
+ {% endif %} + #} +
- {% endif %}
-
diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig index c5de72308..431a06ba6 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/listAccompanyingCourse.html.twig @@ -23,8 +23,8 @@ {% if is_granted('CHILL_ACTIVITY_CREATE', accompanyingCourse) %} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig index 2829d8153..0f426c5a9 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list_recent.html.twig @@ -3,10 +3,88 @@ {% set t = activity.type %} + class="dashboard-link" title="{{ 'Show the activity'|trans }}"> - {% include '@ChillActivity/Activity/activity-badge-title.html.twig' %} +
+ + + + {%- if activity.date -%} +

{{ activity.date|format_date('short') }}

+ {%- endif -%} + + + + {% if activity.emergency %} + {{ 'Emergency'|trans|upper }} + {% endif %} + +
    + + {% if activity.sentReceived is not empty and t.sentReceivedVisible %} +
  • + {{ 'Sent received'|trans ~ ' : ' }} + {{ activity.sentReceived|capitalize|trans }} +
  • + {% endif %} + + {% if activity.location and t.locationVisible %} +
  • + {{ 'location'|trans ~ ': ' }} + + {{ activity.location.locationType.title|localize_translatable_string }} + {{ activity.location.name }} + +
  • + {% endif %} + + {% if activity.user and t.userVisible %} +
  • + {{ 'Referrer'|trans ~ ': ' }} + {{ activity.user.usernameCanonical }} +
  • + {% endif %} + +
  • + {{ 'Participants'|trans ~ ' : ' }} + {% for p in activity.personsAssociated %} + {{ p|chill_entity_render_box }} + {% endfor %} +
  • +
+ +
    + {%- if t.reasonsVisible -%} + {%- if activity.reasons is not empty -%} +
  • + {% for r in activity.reasons %} + {{ r|chill_entity_render_box }} + {% endfor %} +
  • + {%- endif -%} + {% endif %} + {%- if t.socialIssuesVisible %} + {%- if activity.socialIssues is not empty -%} + + {%- endif -%} + {% endif %} + {%- if t.socialActionsVisible -%} + {%- if activity.socialActions is not empty -%} + + {%- endif -%} + {% endif %} +
+ +
+
- {% endfor %}
diff --git a/src/Bundle/ChillActivityBundle/config/services/doctrine.entitylistener.yaml b/src/Bundle/ChillActivityBundle/config/services/doctrine.entitylistener.yaml new file mode 100644 index 000000000..3c439153b --- /dev/null +++ b/src/Bundle/ChillActivityBundle/config/services/doctrine.entitylistener.yaml @@ -0,0 +1,15 @@ +services: + Chill\ActivityBundle\EntityListener\ActivityEntityListener: + autowire: true + autoconfigure: true + tags: + - + name: 'doctrine.orm.entity_listener' + event: 'postPersist' + entity: 'Chill\ActivityBundle\Entity\Activity' + method: 'persistActionToCourse' + - + name: 'doctrine.orm.entity_listener' + event: 'postUpdate' + entity: 'Chill\ActivityBundle\Entity\Activity' + method: 'persistActionToCourse' diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 098a6f6df..3de1cc600 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -222,3 +222,5 @@ Aggregate by activity type: Aggréger par type d'activité Aggregate by activity reason: Aggréger par sujet de l'activité Last activities: Les dernières activités + +See activity in accompanying course context: Voir l'activité dans le contexte du parcours d'accompagnement diff --git a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php index 1230cd0f6..498c77c68 100644 --- a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php +++ b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php @@ -12,14 +12,66 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; use Chill\MainBundle\CRUD\Controller\ApiController; +use Chill\MainBundle\Entity\PostalCode; +use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorInterface; +use Chill\MainBundle\Repository\AddressReferenceRepository; +use Chill\MainBundle\Serializer\Model\Collection; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use function trim; -/** - * Class AddressReferenceAPIController. - */ -class AddressReferenceAPIController extends ApiController +final class AddressReferenceAPIController extends ApiController { + private AddressReferenceRepository $addressReferenceRepository; + + private PaginatorFactory $paginatorFactory; + + public function __construct(AddressReferenceRepository $addressReferenceRepository, PaginatorFactory $paginatorFactory) + { + $this->addressReferenceRepository = $addressReferenceRepository; + $this->paginatorFactory = $paginatorFactory; + } + + /** + * @Route("/api/1.0/main/address-reference/by-postal-code/{id}/search.json") + */ + public function search(PostalCode $postalCode, Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + if (!$request->query->has('q')) { + throw new BadRequestHttpException('You must supply a "q" parameter'); + } + + $pattern = $request->query->get('q'); + + if ('' === trim($pattern)) { + throw new BadRequestHttpException('the search pattern is empty'); + } + + $nb = $this->addressReferenceRepository->countByPostalCodePattern($postalCode, $pattern); + $paginator = $this->paginatorFactory->create($nb); + $addresses = $this->addressReferenceRepository->findByPostalCodePattern( + $postalCode, + $pattern, + false, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage() + ); + + return $this->json( + new Collection($addresses, $paginator), + Response::HTTP_OK, + [], + [AbstractNormalizer::GROUPS => ['read']] + ); + } + protected function customizeQuery(string $action, Request $request, $qb): void { if ($request->query->has('postal_code')) { diff --git a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php index fb926733b..fa1a29296 100644 --- a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php +++ b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php @@ -12,13 +12,80 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; use Chill\MainBundle\CRUD\Controller\ApiController; +use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\MainBundle\Repository\CountryRepository; +use Chill\MainBundle\Repository\PostalCodeRepository; +use Chill\MainBundle\Serializer\Model\Collection; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -/** - * Class PostalCodeAPIController. - */ -class PostalCodeAPIController extends ApiController +final class PostalCodeAPIController extends ApiController { + private CountryRepository $countryRepository; + + private PaginatorFactory $paginatorFactory; + + private PostalCodeRepository $postalCodeRepository; + + public function __construct( + CountryRepository $countryRepository, + PostalCodeRepository $postalCodeRepository, + PaginatorFactory $paginatorFactory + ) { + $this->countryRepository = $countryRepository; + $this->postalCodeRepository = $postalCodeRepository; + $this->paginatorFactory = $paginatorFactory; + } + + /** + * @Route("/api/1.0/main/postal-code/search.json") + */ + public function search(Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + if (!$request->query->has('q')) { + throw new BadRequestHttpException('You must supply a "q" parameter'); + } + + $pattern = $request->query->get('q'); + + if ('' === trim($pattern)) { + throw new BadRequestHttpException('the search pattern is empty'); + } + + if ($request->query->has('country')) { + $country = $this->countryRepository->find($request->query->getInt('country')); + + if (null === $country) { + throw new NotFoundHttpException('country not found'); + } + } else { + $country = null; + } + + $nb = $this->postalCodeRepository->countByPattern($pattern, $country); + $paginator = $this->paginatorFactory->create($nb); + $codes = $this->postalCodeRepository->findByPattern( + $pattern, + $country, + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage() + ); + + return $this->json( + new Collection($codes, $paginator), + Response::HTTP_OK, + [], + [AbstractNormalizer::GROUPS => ['read']] + ); + } + protected function customizeQuery(string $action, Request $request, $qb): void { if ($request->query->has('country')) { diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php index e793167f6..99efd391d 100644 --- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -22,6 +22,15 @@ use Symfony\Component\Serializer\Annotation\Groups; */ class AddressReference { + /** + * This is an internal column which is populated by database. + * + * This column will ease the search operations + * + * @ORM\Column(type="text", options={"default": ""}) + */ + private string $addressCanonical = ''; + /** * @ORM\Id * @ORM\GeneratedValue diff --git a/src/Bundle/ChillMainBundle/Entity/PostalCode.php b/src/Bundle/ChillMainBundle/Entity/PostalCode.php index 866ad04db..484a9e322 100644 --- a/src/Bundle/ChillMainBundle/Entity/PostalCode.php +++ b/src/Bundle/ChillMainBundle/Entity/PostalCode.php @@ -29,6 +29,15 @@ use Symfony\Component\Serializer\Annotation\Groups; */ class PostalCode { + /** + * This is an internal column which is populated by database. + * + * This column will ease the search operations + * + * @ORM\Column(type="text", options={"default": ""}) + */ + private string $canonical = ''; + /** * @var Point * diff --git a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php index 4344964af..6be52ec85 100644 --- a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php @@ -12,17 +12,29 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\AddressReference; +use Chill\MainBundle\Entity\PostalCode; +use Chill\MainBundle\Search\SearchApiQuery; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\Persistence\ObjectRepository; +use RuntimeException; +use function explode; +use function implode; +use function strtr; +use function trim; final class AddressReferenceRepository implements ObjectRepository { + private EntityManagerInterface $entityManager; + private EntityRepository $repository; public function __construct(EntityManagerInterface $entityManager) { $this->repository = $entityManager->getRepository(AddressReference::class); + $this->entityManager = $entityManager; } public function countAll(): int @@ -33,6 +45,18 @@ final class AddressReferenceRepository implements ObjectRepository return $qb->getQuery()->getSingleScalarResult(); } + public function countByPostalCodePattern(PostalCode $postalCode, string $pattern): int + { + $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern); + $sql = $query->buildQuery(true); + $rsm = new ResultSetMapping(); + $rsm->addScalarResult('c', 'c'); + + $nq = $this->entityManager->createNativeQuery($sql, $rsm)->setParameters($query->buildParameters(true)); + + return (int) $nq->getSingleResult()['c']; + } + public function find($id, $lockMode = null, $lockVersion = null): ?AddressReference { return $this->repository->find($id, $lockMode, $lockVersion); @@ -57,6 +81,33 @@ final class AddressReferenceRepository implements ObjectRepository return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } + /** + * @return AddressReference[]|array + */ + public function findByPostalCodePattern(PostalCode $postalCode, string $pattern, bool $simplify = false, int $start = 0, int $limit = 50): array + { + $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern); + + if (!$simplify) { + $rsm = new ResultSetMappingBuilder($this->entityManager); + $rsm->addRootEntityFromClassMetadata(AddressReference::class, 'cma'); + $query->addSelectClause($rsm->generateSelectClause()); + } else { + throw new RuntimeException('not implemented'); + } + + $sql = strtr( + $query->buildQuery() . 'ORDER BY pertinence DESC, lpad(streetnumber, 10, \'0\') ASC OFFSET ? LIMIT ? ', + // little hack for adding sql method to point + ['cma.point AS point' => 'ST_AsGeojson(cma.point) AS point'] + ); + $parameters = [...$query->buildParameters(), $start, $limit]; + + return $this->entityManager->createNativeQuery($sql, $rsm) + ->setParameters($parameters) + ->getResult(); + } + public function findOneBy(array $criteria, ?array $orderBy = null): ?AddressReference { return $this->repository->findOneBy($criteria, $orderBy); @@ -66,4 +117,44 @@ final class AddressReferenceRepository implements ObjectRepository { return AddressReference::class; } + + private function buildQueryByPostalCodePattern(PostalCode $postalCode, string $pattern): SearchApiQuery + { + $pattern = trim($pattern); + + if ('' === $pattern) { + throw new RuntimeException('the search pattern must not be empty'); + } + $query = new SearchApiQuery(); + + $query + ->setFromClause('chill_main_address_reference cma') + ->andWhereClause('postcode_id = ?', [$postalCode->getId()]); + + $pertinenceClause = ['STRICT_WORD_SIMILARITY(addresscanonical, UNACCENT(?))']; + $pertinenceArgs = [$pattern]; + $orWhere = ['addresscanonical %>> UNACCENT(?)']; + $orWhereArgs = [$pattern]; + + foreach (explode(' ', $pattern) as $part) { + $part = trim($part); + + if ('' === $part) { + continue; + } + + $orWhere[] = "addresscanonical LIKE '%' || UNACCENT(LOWER(?)) || '%'"; + $orWhereArgs[] = $part; + $pertinenceClause[] = + "(EXISTS (SELECT 1 FROM unnest(string_to_array(addresscanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"; + $pertinenceClause[] = + '(addresscanonical LIKE UNACCENT(LOWER(?)))::int'; + array_push($pertinenceArgs, $part, $part); + } + $query + ->setSelectPertinence(implode(' + ', $pertinenceClause), $pertinenceArgs) + ->andWhereClause(implode(' OR ', $orWhere), $orWhereArgs); + + return $query; + } } diff --git a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php index 697c8f90d..02e63771b 100644 --- a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php @@ -11,18 +11,39 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\Country; use Chill\MainBundle\Entity\PostalCode; +use Chill\MainBundle\Search\SearchApiQuery; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\Persistence\ObjectRepository; +use RuntimeException; final class PostalCodeRepository implements ObjectRepository { + private EntityManagerInterface $entityManager; + private EntityRepository $repository; public function __construct(EntityManagerInterface $entityManager) { $this->repository = $entityManager->getRepository(PostalCode::class); + $this->entityManager = $entityManager; + } + + public function countByPattern(string $pattern, ?Country $country): int + { + $query = $this->buildQueryByPattern($pattern, $country); + $sql = $query->buildQuery(true); + $rsm = new ResultSetMapping(); + $rsm->addScalarResult('c', 'c'); + + $nq = $this->entityManager->createNativeQuery($sql, $rsm) + ->setParameters($query->buildParameters(true)); + + return (int) $nq->getSingleResult()['c']; } public function find($id, $lockMode = null, $lockVersion = null): ?PostalCode @@ -49,6 +70,26 @@ final class PostalCodeRepository implements ObjectRepository return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } + public function findByPattern(string $pattern, ?Country $country, ?int $start = 0, ?int $limit = 50): array + { + $query = $this->buildQueryByPattern($pattern, $country); + + $rsm = new ResultSetMappingBuilder($this->entityManager); + $rsm->addRootEntityFromClassMetadata(PostalCode::class, 'cmpc'); + $query->addSelectClause($rsm->generateSelectClause()); + + $sql = strtr( + $query->buildQuery() . 'ORDER BY pertinence DESC, canonical ASC OFFSET ? LIMIT ? ', + // little hack for adding sql method to point + ['cmpc.center AS center' => 'ST_AsGeojson(cmpc.center) AS center'] + ); + $parameters = [...$query->buildParameters(), $start, $limit]; + + return $this->entityManager->createNativeQuery($sql, $rsm) + ->setParameters($parameters) + ->getResult(); + } + public function findOneBy(array $criteria, ?array $orderBy = null): ?PostalCode { return $this->repository->findOneBy($criteria, $orderBy); @@ -58,4 +99,48 @@ final class PostalCodeRepository implements ObjectRepository { return PostalCode::class; } + + private function buildQueryByPattern(string $pattern, ?Country $country): SearchApiQuery + { + $pattern = trim($pattern); + + if ('' === $pattern) { + throw new RuntimeException('the search pattern must not be empty'); + } + $query = new SearchApiQuery(); + + $query + ->setFromClause('chill_main_postal_code cmpc') + ->andWhereClause('cmpc.origin = 0'); + + if (null !== $country) { + $query->andWhereClause('cmpc.country_id = ?', [$country->getId()]); + } + + $pertinenceClause = ['STRICT_WORD_SIMILARITY(canonical, UNACCENT(?))']; + $pertinenceArgs = [$pattern]; + $orWhere = ['canonical %>> UNACCENT(?)']; + $orWhereArgs = [$pattern]; + + foreach (explode(' ', $pattern) as $part) { + $part = trim($part); + + if ('' === $part) { + continue; + } + + $orWhere[] = "canonical LIKE '%' || UNACCENT(LOWER(?)) || '%'"; + $orWhereArgs[] = $part; + $pertinenceClause[] = + "(EXISTS (SELECT 1 FROM unnest(string_to_array(canonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"; + $pertinenceClause[] = + '(canonical LIKE UNACCENT(LOWER(?)))::int'; + array_push($pertinenceArgs, $part, $part); + } + $query + ->setSelectPertinence(implode(' + ', $pertinenceClause), $pertinenceArgs) + ->andWhereClause(implode(' OR ', $orWhere), $orWhereArgs); + + return $query; + } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss index 8e3a9121b..6b22302b2 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss @@ -101,6 +101,10 @@ div.flex-table { div.item-row { flex-direction: row; + &.column { // exception + flex-direction: column + } + div.item-col { &:first-child { flex-grow: 0; flex-shrink: 0; flex-basis: auto; @@ -160,6 +164,12 @@ div.wrap-list { & > * { padding-right: 1em; } + + h3, h4 { + font-weight: 700; + font-size: 100%; + font-family: 'Open Sans'; + } } div.wl-col.list { @@ -171,6 +181,10 @@ div.wrap-list { padding: 0em; display: inline-block; } + + blockquote.chill-user-quote { + margin: 0.7em 0; + } } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js index 62065b3ad..1dbc85dee 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js @@ -16,7 +16,8 @@ const fetchCountries = () => { /** * Endpoint chill_api_single_postal_code__index -* method GET, get Country Object +* method GET, get Cities Object +* @params {object} a country object * @returns {Promise} a promise containing all Postal Code objects filtered with country */ const fetchCities = (country) => { @@ -29,6 +30,40 @@ const fetchCities = (country) => { }); }; + +/** +* Endpoint chill_main_postalcodeapi_search +* method GET, get Cities Object +* @params {string} search a search string +* @params {object} country a country object +* @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string +*/ +const searchCities = (search, country) => { + const url = `/api/1.0/main/postal-code/search.json?q=${search}&country=${country.id}`; + return fetch(url) + .then(response => { + if (response.ok) { return response.json(); } + throw Error('Error with request resource response'); + }); +}; + +/** +* Endpoint chill_main_addressreferenceapi_search +* method GET, get AddressReference Object +* @params {string} search a search string +* @params {object} postalCode a postalCode object +* @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string +*/ +const searchReferenceAddresses = (search, postalCode) => { + const url = `/api/1.0/main/address-reference/by-postal-code/${postalCode.id}/search.json?q=${search}`; + return fetch(url) + .then(response => { + if (response.ok) { return response.json(); } + throw Error('Error with request resource response'); + }); +}; + + /** * Endpoint chill_api_single_address_reference__index * method GET, get AddressReference Object @@ -170,5 +205,7 @@ export { postAddress, patchAddress, postPostalCode, - getAddress + getAddress, + searchCities, + searchReferenceAddresses }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue index 87ecff7fb..ed1d14633 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue @@ -556,8 +556,8 @@ export default { this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: null; this.entity.selected.address.extra = this.context.edit ? this.entity.address.extra: null; - this.entity.selected.writeNew.address = this.context.edit; - this.entity.selected.writeNew.postcode = this.context.edit; + this.entity.selected.writeNew.address = this.context.edit && this.entity.address.addressReference === null && this.entity.address.street.length > 0 + this.entity.selected.writeNew.postcode = false // NB: this used to be this.context.edit, but think it was erroneous; console.log('!! just set writeNew.postcode to', this.entity.selected.writeNew.postcode); }, @@ -569,7 +569,6 @@ export default { applyChanges() { console.log('apply changes'); - let newAddress = { 'isNoAddress': this.entity.selected.isNoAddress, 'street': this.entity.selected.isNoAddress ? '' : this.entity.selected.address.street, @@ -633,7 +632,6 @@ export default { if (!this.context.edit) { this.addNewAddress(newAddress) .then(payload => this.addressChangedCallback(payload)); - } else { this.updateAddress({ addressId: this.context.addressId, @@ -697,8 +695,7 @@ export default { * Async PATCH transactions, * then update existing address with backend datas when promise is resolved */ - updateAddress(payload) - { + updateAddress(payload) { this.flag.loading = true; // TODO change the condition because it writes new postal code in edit mode now: !writeNewPostalCode diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue index 2409dca53..c333961db 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue @@ -18,6 +18,7 @@ :taggable="true" :multiple="false" @tag="addAddress" + :loading="isLoading" :options="addresses">
@@ -48,14 +49,17 @@ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js index c2ee09960..8047aea11 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js @@ -24,6 +24,7 @@ const visMessages = { refresh: "Rafraîchir", screenshot: "Prendre une photo", choose_relation: "Choisissez le lien de parenté", + relationship_household: "Filiation du ménage", }, edit: 'Éditer', del: 'Supprimer', diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js index ca76f283b..5e8989e49 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js @@ -16,7 +16,12 @@ persons.forEach(person => { }) const app = createApp({ - template: `` + template: ``, + data() { + return { + household_id: JSON.parse(container.dataset.householdId) + } + } }) .use(store) .use(i18n) diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js index 119a7b29c..10269c6c8 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/store.js @@ -112,7 +112,7 @@ const store = createStore({ } }) //console.log('array', array.map(item => item.person.id)) - console.log('get persons group', group.map(f => f.id)) + //console.log('get persons group', group.map(f => f.id)) return group }, @@ -120,13 +120,17 @@ const store = createStore({ }, mutations: { addPerson(state, [person, options]) { + let age = getAge(person) + age = (age === '')? '' : ' - ' + age + let debug = '' /// Debug mode: uncomment to display person_id on visgraph //debug = `\nid ${person.id}` + person.group = person.type person._id = person.id person.id = `person_${person.id}` - person.label = `*${person.text}*\n_${getGender(person.gender)} - ${getAge(person.birthdate)}_${debug}` // + person.label = `*${person.text}*\n_${getGender(person.gender)}${age}_${debug}` person.folded = false // folded is used for missing persons if (options.folded) { @@ -161,7 +165,7 @@ const store = createStore({ state.links.push(link) }, updateLink(state, link) { - console.log('updateLink', link) + //console.log('updateLink', link) let link_ = { from: `person_${link.fromPerson.id}`, to: `person_${link.toPerson.id}`, @@ -264,7 +268,7 @@ const store = createStore({ fetchInfoForPerson({ dispatch }, person) { // TODO enfants hors ménages // example: household 61 - // console.log(person.text, 'household', person.current_household_id) + //console.log(person.text, 'household', person.current_household_id) if (null !== person.current_household_id) { dispatch('fetchHouseholdForPerson', person) } @@ -305,15 +309,16 @@ const store = createStore({ */ addLinkFromPersonsToHousehold({ commit, getters, dispatch }, household) { let members = getters.getMembersByHousehold(household.id) - console.log('add link for', members.length, 'members') + //console.log('add link for', members.length, 'members') members.forEach(m => { commit('addLink', { from: `${m.person.type}_${m.person.id}`, - to: `household_${m.person.current_household_id}`, - id: `household_${m.person.current_household_id}-person_${m.person.id}`, + to: `${household.id}`, + id: `${household.id}-person_${m.person.id}`, arrows: 'from', color: 'pink', font: { color: '#D04A60' }, + dashes: (getHouseholdWidth(m) === 1)? [0,4] : false, //edge style: [dash, gap, dash, gap] label: getHouseholdLabel(m), width: getHouseholdWidth(m), }) @@ -362,7 +367,7 @@ const store = createStore({ */ addLinkFromPersonsToCourse({ commit, getters, dispatch }, course) { const participations = getters.getParticipationsByCourse(course.id) - console.log('add link for', participations.length, 'participations') + //console.log('add link for', participations.length, 'participations') participations.forEach(p => { //console.log(p.person.id) commit('addLink', { @@ -445,7 +450,7 @@ const store = createStore({ * @param array */ addMissingPerson({ commit, getters, dispatch }, [person, parent]) { - console.log('! add missing Person', person.id) + //console.log('! add missing Person', person.id) commit('markPersonLoaded', person.id) commit('addPerson', [person, { folded: true }]) if (getters.isExcludedNode(parent.id)) { @@ -467,7 +472,7 @@ const store = createStore({ getters.getPersonsGroup(participations) .forEach(person => { if (person.folded === true) { - console.log('-=. unfold and expand person', person.id) + //console.log('-=. unfold and expand person', person.id) commit('unfoldPerson', person) dispatch('fetchInfoForPerson', person) } @@ -485,7 +490,7 @@ const store = createStore({ getters.getPersonsGroup(members) .forEach(person => { if (person.folded === true) { - console.log('-=. unfold and expand person', person.id) + //console.log('-=. unfold and expand person', person.id) commit('unfoldPerson', person) dispatch('fetchInfoForPerson', person) } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js index e95bc0d0b..3e00db883 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/vis-network.js @@ -13,13 +13,12 @@ window.options = { locale: 'fr', locales: visMessages, /* + */ configure: { enabled: true, - filter: 'nodes,edges', - //container: undefined, + filter: 'physics', showButton: true }, - */ physics: { enabled: true, barnesHut: { @@ -37,8 +36,8 @@ window.options = { centralGravity: 0.01, springLength: 100, springConstant: 0.08, - damping: 0.4, - avoidOverlap: 0 + damping: 0.75, + avoidOverlap: 0.00 }, repulsion: { centralGravity: 0.2, @@ -159,17 +158,21 @@ const getGender = (gender) => { } /** - * TODO Repeat getAge() in PersonRenderBox.vue - * @param birthdate + * TODO only one abstract function (-> getAge() is repeated in PersonRenderBox.vue) + * @param person * @returns {string|null} */ -const getAge = (birthdate) => { - if (null === birthdate) { - return null +const getAge = (person) => { + if (person.birthdate) { + let birthdate = new Date(person.birthdate.datetime) + if (person.deathdate) { + let deathdate = new Date(person.deathdate.datetime) + return (deathdate.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years + } + let now = new Date() + return (now.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years } - const birthday = new Date(birthdate.datetime) - const now = new Date() - return (now.getFullYear() - birthday.getFullYear()) + ' '+ visMessages.fr.visgraph.years + return '' } /** diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue index ffb9ef3d4..ab7074ffc 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Entity/PersonRenderBox.vue @@ -192,6 +192,7 @@ export default { return `/fr/person/${this.person.id}/general`; }, getAge: function() { + // TODO only one abstract function if(this.person.birthdate && !this.person.deathdate){ const birthday = new Date(this.person.birthdate.datetime) const now = new Date() diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig index 96e2cb534..cdd0363b4 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig @@ -103,7 +103,7 @@
-

{{ 'Last social actions'|trans }}

+

{{ 'Last social actions'|trans }}

{% include 'ChillPersonBundle:AccompanyingCourseWork:list_recent_by_accompanying_period.html.twig' with {'buttonText': false } %}
@@ -121,7 +121,7 @@ {% set accompanying_course_id = accompanyingCourse.id %} {% endif %} -

{{ 'Last activities' |trans }}

+

{{ 'Last activities' |trans }}

{% include 'ChillActivityBundle:Activity:list_recent.html.twig' with { 'context': 'accompanyingCourse', 'no_action': true } %}
{% endblock %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/delete.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/delete.html.twig index c66e5aa3d..37a6812bb 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/delete.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/delete.html.twig @@ -8,7 +8,7 @@

- {{ 'accompanying_course_work.action'|trans }} + {{ work.socialAction|chill_entity_render_string }}

diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig index 92b1919af..844b2d611 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig @@ -7,7 +7,217 @@

{{ block('title') }}

- {% include 'ChillPersonBundle:AccompanyingCourseWork:list_by_accompanying_period.html.twig' %} + {% if works|length == 0 %} +

{{ 'accompanying_course_work.Any work'|trans }} + +

+ + {% else %} +
+ {% for w in works %} +
+ +
+

+ + {{ w.socialAction|chill_entity_render_string }} + +
    +
  • + {{ 'accompanying_course_work.start_date'|trans ~ ' : ' }} + {{ w.startDate|format_date('short') }} +
  • + {% if w.endDate %} +
  • + {{ 'accompanying_course_work.end_date'|trans ~ ' : ' }} + {{ w.endDate|format_date('short') }} +
  • + {% endif %} +
+ +
+

+
+ +
+
+ + {% if w.createdBy %} +
+
+

{{ 'Referrer'|trans }}

+
+
+

+ {{ w.createdBy.usernameCanonical|chill_entity_render_string|capitalize }} +

+
+
+ {% endif %} + + {%- if w.persons -%} +
+
+

{{ 'Persons in accompanying course'|trans }}

+
+
+ {% for p in w.persons %} + + {{ p|chill_entity_render_box({ + 'render': 'raw', + 'addAltNames': false + }) }} + + {% endfor %} +
+
+ {% endif %} + + {%- if w.handlingThierParty -%} +
+
+

{{ 'Thirdparty handling'|trans }}

+
+
+ + {{ w.handlingThierParty|chill_entity_render_box({ + 'render': 'raw', + 'addAltNames': false + }) }} + +
+
+ {% endif %} + + {%- if w.socialAction.issue -%} +
+
+

{{ 'Social issue'|trans }}

+
+
+ +
+
+ {% endif %} + + {% if w.accompanyingPeriodWorkEvaluations|length > 0 %} +
+
+

{{ 'accompanying_course_work.evaluations'|trans }}

+
+
+
    + {% for e in w.accompanyingPeriodWorkEvaluations %} +
  • + {{ e.evaluation.title|localize_translatable_string }} + +
      +
    • + {{ 'accompanying_course_work.start_date'|trans ~ ' : ' }} + {{ e.startDate|format_date('short') }} +
    • + {% if e.endDate %} +
    • + {{ 'accompanying_course_work.end_date'|trans ~ ' : ' }} + {{ e.endDate|format_date('short') }} +
    • + {% endif %} + {% if e.maxDate %} +
    • + {{ 'accompanying_course_work.max_date'|trans ~ ' : ' }} + {{ e.maxDate|format_date('short') }} +
    • + {% endif %} + {% if e.warningInterval and e.warningInterval.d > 0 %} +
    • + {% set days = (e.warningInterval.d + e.warningInterval.m * 30) %} + {{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }} + {{ 'accompanying_course_work.%days% days before max_date'|trans({'%days%': days }) }} +
    • + {% endif %} +
    + +
  • + {% endfor %} +
+
+
+ {% endif %} + +
+
+ +
+ {# SEULEMENT SI DÉTAILLÉ + {% if w.results|length > 0 %} +
+
+

{{ 'accompanying_course_work.goal'|trans }}

+

{{ 'accompanying_course_work.results without objective'|trans }}

+
+
+

{{ 'accompanying_course_work.results'|trans }}

+
    + {% for r in w.results %} +
  • {{ r.title|localize_translatable_string }}
  • + {% endfor %} +
+
+
+ {% endif %} + {% if w.goals|length > 0 %} + {% for g in w.goals %} +
+
+

{{ 'accompanying_course_work.goal'|trans }}

+
  • {{ g.goal.title|localize_translatable_string }}
+
+
+

{{ 'accompanying_course_work.results'|trans }}

+ {% if g.results|length == 0 %} +

{{ 'accompanying_course_work.no_results'|trans }}

+ {% else %} +
    + {% for r in g.results %} +
  • {{ r.title|localize_translatable_string }}
  • + {% endfor %} +
+ {% endif %} +
+
+ {% endfor %} + {% endif %} + #} +
+ +
+
+ {{ 'Last updated by'|trans}} {{ w.updatedBy|chill_entity_render_box }},
+ {{ 'le ' ~ w.updatedAt|format_datetime('long', 'short') }} +
+
    +
  • + +
  • +
  • + +
  • +
+
+ +
+ {% endfor %} +
+ {% endif %}
{# {{ dump(w) }} #} {% endfor %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig index 56fcce85c..acaeebb96 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Household/relationship.html.twig @@ -17,7 +17,8 @@
+ data-persons="{{ persons|e('html_attr') }}" + data-household-id="{{ household.id|e('html_attr') }}">
{% endblock %} diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php index c0ba0201a..7049b7c00 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/PersonJsonNormalizer.php @@ -18,6 +18,7 @@ use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Repository\PersonRepository; use DateTime; +use DateTimeImmutable; use LogicException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; @@ -114,18 +115,14 @@ class PersonJsonNormalizer implements case 'birthdate': $object = $this->denormalizer->denormalize($data[$item], DateTime::class, $format, $context); - if ($object instanceof DateTime) { - $person->setBirthdate($object); - } + $person->setBirthdate($object); break; case 'deathdate': - $object = $this->denormalizer->denormalize($data[$item], DateTime::class, $format, $context); + $object = $this->denormalizer->denormalize($data[$item], DateTimeImmutable::class, $format, $context); - if ($object instanceof DateTime) { - $person->setDeathdate($object); - } + $person->setDeathdate($object); break; diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index c67884a19..3c49cc264 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -353,6 +353,19 @@ paths: application/json: schema: $ref: "#/components/schemas/Person" + examples: + Update a person: + value: + type: "person" + firstName: "string" + lastName: "string" + birthdate: + datetime: "2016-06-01T00:00:00+02:00" + deathdate: + datetime: "2021-06-01T00:00:00+02:00" + phonenumber: "string" + mobilenumber: "string" + gender: "male" responses: 401: description: "Unauthorized" @@ -375,6 +388,19 @@ paths: application/json: schema: $ref: "#/components/schemas/Person" + examples: + Create a new person: + value: + type: "person" + firstName: "string" + lastName: "string" + birthdate: + datetime: "2016-06-01T00:00:00+02:00" + deathdate: + datetime: "2021-06-01T00:00:00+02:00" + phonenumber: "string" + mobilenumber: "string" + gender: "male" responses: 200: description: "OK" diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index f2125d5c6..af2009f3a 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -198,6 +198,7 @@ Resources: Interlocuteurs privilégiés Any requestor to this accompanying course: Aucun demandeur pour ce parcours Social actions: Actions d'accompagnement Last social actions: Les dernières actions d'accompagnement +Social issue: Problématique sociale Social issues: Problématiques sociales Last events on accompanying course: Dernières actions de suivi Edit & activate accompanying course: Modifier et valider @@ -434,10 +435,14 @@ accompanying_course_work: create_date: Date de création start_date: Date de début end_date: Date de fin + max_date: Date d'échéance + warning_interval: Rappel + '%days% days before max_date': "%days% jour(s) avant l'échéance" results without objective: Aucun objectif - motif - dispositif no_results: Aucun résultat - orientation results: Résultats - orientations goal: Objectif - motif - dispositif + evaluations: Évaluations Any work: Aucune action d'accompagnement remove: Supprimer une action d'accompagnement social_evaluation: Évaluation