Merge branch 'master' of gitlab.com:Chill-Projet/chill-bundles

This commit is contained in:
Mathieu Jaumotte 2021-10-11 14:42:57 +02:00
commit 0c5e5ac672
70 changed files with 2112 additions and 1001 deletions

View File

@ -21,6 +21,14 @@ and this project adheres to
* [AccompanyingCourse Resume page] badge-title for AccompanyingCourseWork and for Activities;
* Improve badges behaviour with small screens;
* [ThirdParty]:
* third party list
* create a kind contact/institution when create a new thirdparty, and set contact embedded as kind=child;
* filter thirdparties in list
* [FilterOrder]: add development kit for generating filter and ordering in list
## Test releases
### test release 2021-10-04

View File

@ -7,6 +7,7 @@ namespace Chill\AsideActivityBundle\Controller;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Chill\AsideActivityBundle\Repository\AsideActivityCategoryRepository;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
use Chill\MainBundle\Pagination\PaginatorInterface;
@ -22,9 +23,9 @@ final class AsideActivityController extends CRUDController
$this->categoryRepository = $categoryRepository;
}
protected function buildQueryEntities(string $action, Request $request)
protected function buildQueryEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null)
{
$qb = parent::buildQueryEntities($action, $request);
$qb = parent::buildQueryEntities($action, $request, $filterOrder);
if ('index' === $action) {
$qb->where($qb->expr()->eq('e.agent', ':user'));
@ -53,7 +54,7 @@ final class AsideActivityController extends CRUDController
$asideActivity = new AsideActivity();
$duration = $request->query->get('duration', '300');
$duration = \DateTime::createFromFormat('U', $duration);
$duration = \DateTime::createFromFormat('U', $duration);
$asideActivity->setDuration($duration);
$categoryId = $request->query->get('type', 7);

View File

@ -20,6 +20,8 @@
namespace Chill\MainBundle\CRUD\Controller;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@ -209,22 +211,24 @@ class CRUDController extends AbstractController
* This method:
*
* 1. Launch `onPreIndex`
* x. check acl. If it does return a response instance, return it
* x. launch `onPostCheckACL`. If it does return a response instance, return it
* 1. count the items, using `countEntities`
* 2. build a paginator element from the the number of entities ;
* 3. Launch `onPreIndexQuery`. If it does return a response instance, return it
* 3. build a query, using `queryEntities`
* x. fetch the results, using `getQueryResult`
* x. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it
* 4. create default parameters:
* 2. check acl. If it does return a response instance, return it
* 3. launch `onPostCheckACL`. If it does return a response instance, return it
* 4. count the items, using `countEntities`
* 5. build a paginator element from the the number of entities ;
* 6. Launch `onPreIndexQuery`. If it does return a response instance, return it
* 7. fetch the results, using `getQueryResult`
*
* Internally, this build a query, using `queryEntities`
*
* 8. Launch `onPostIndexFetchQuery`. If it does return a response instance, return it
* 9. create default parameters:
*
* The default parameters are:
*
* * entities: the list en entities ;
* * crud_name: the name of the crud ;
* * paginator: a paginator element ;
* 5. Launch rendering, the parameter is fetch using `getTemplateFor`
* 10. Launch rendering, the parameter is fetch using `getTemplateFor`
* The parameters may be personnalized using `generateTemplateParameter`.
*
* @param string $action
@ -249,7 +253,8 @@ class CRUDController extends AbstractController
return $response;
}
$totalItems = $this->countEntities($action, $request);
$filterOrder = $this->buildFilterOrderHelper($action, $request);
$totalItems = $this->countEntities($action, $request, $filterOrder);
$paginator = $this->getPaginatorFactory()->create($totalItems);
$response = $this->onPreIndexBuildQuery($action, $request, $totalItems,
@ -259,16 +264,7 @@ class CRUDController extends AbstractController
return $response;
}
$query = $this->queryEntities($action, $request, $paginator);
$response = $this->onPostIndexBuildQuery($action, $request, $totalItems,
$paginator, $query);
if ($response instanceof Response) {
return $response;
}
$entities = $this->getQueryResult($action, $request, $totalItems, $paginator, $query);
$entities = $this->getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
$response = $this->onPostIndexFetchQuery($action, $request, $totalItems,
$paginator, $entities);
@ -280,7 +276,8 @@ class CRUDController extends AbstractController
$defaultTemplateParameters = [
'entities' => $entities,
'crud_name' => $this->getCrudName(),
'paginator' => $paginator
'paginator' => $paginator,
'filter_order' => $filterOrder
];
return $this->render(
@ -289,6 +286,11 @@ class CRUDController extends AbstractController
);
}
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
{
return null;
}
/**
* @param string $action
* @param Request $request
@ -361,9 +363,9 @@ class CRUDController extends AbstractController
* @param PaginatorInterface $paginator
* @return type
*/
protected function queryEntities(string $action, Request $request, PaginatorInterface $paginator)
protected function queryEntities(string $action, Request $request, PaginatorInterface $paginator, ?FilterOrderHelper $filterOrder = null)
{
$query = $this->buildQueryEntities($action, $request)
$query = $this->buildQueryEntities($action, $request, $filterOrder)
->setFirstResult($paginator->getCurrentPage()->getFirstItemNumber())
->setMaxResults($paginator->getItemsPerPage());
@ -393,11 +395,13 @@ class CRUDController extends AbstractController
* @param Request $request
* @param int $totalItems
* @param PaginatorInterface $paginator
* @param mixed $query
* @return mixed
*/
protected function getQueryResult(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, $query)
protected function getQueryResult(string $action, Request $request, int $totalItems, PaginatorInterface $paginator,
?FilterOrderHelper $filterOrder = null)
{
$query = $this->queryEntities($action, $request, $paginator, $filterOrder);
return $query->getQuery()->getResult();
}
@ -408,9 +412,9 @@ class CRUDController extends AbstractController
* @param Request $request
* @return int
*/
protected function countEntities(string $action, Request $request): int
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
{
return $this->buildQueryEntities($action, $request)
return $this->buildQueryEntities($action, $request, $filterOrder)
->select('COUNT(e)')
->getQuery()
->getSingleScalarResult()
@ -1183,6 +1187,11 @@ class CRUDController extends AbstractController
return $this->get(Resolver::class);
}
protected function getFilterOrderHelperFactory(): FilterOrderHelperFactoryInterface
{
return $this->get(FilterOrderHelperFactoryInterface::class);
}
/**
* @return array
*/
@ -1197,6 +1206,7 @@ class CRUDController extends AbstractController
EventDispatcherInterface::class => EventDispatcherInterface::class,
Resolver::class => Resolver::class,
SerializerInterface::class => SerializerInterface::class,
FilterOrderHelperFactoryInterface::class => FilterOrderHelperFactoryInterface::class,
]
);
}

View File

@ -1,22 +1,17 @@
<?php
namespace Chill\ThirdPartyBundle\DataFixtures\ORM;
namespace Chill\MainBundle\DataFixtures\ORM;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCivility;
use Chill\MainBundle\Entity\Civility;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Persistence\ObjectManager;
/**
* Class LoadThirdPartyCivility
* @package Chill\ThirdPartyBundle\DataFixtures\ORM
* @author Mathieu Jaumotte mathieu.jaumotte@champs-libres.coop
*/
class LoadThirdPartyCivility extends Fixture implements FixtureGroupInterface
class LoadCivility extends Fixture implements FixtureGroupInterface
{
public static function getGroups(): array
{
return ['thirdparty_civilities'];
return ['civilities'];
}
public function load(ObjectManager $manager): void
@ -34,8 +29,7 @@ class LoadThirdPartyCivility extends Fixture implements FixtureGroupInterface
];
foreach ( $civilities as $val) {
print "Creating thirdparty civility : " . $val['name']['fr'] . "\n";
$civility = (new ThirdPartyCivility())
$civility = (new Civility())
->setName($val['name'])
->setActive(true);
$manager->persist($civility);

View File

@ -20,33 +20,41 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\ThirdPartyBundle\Entity;
namespace Chill\MainBundle\Entity;
use Chill\ThirdPartyBundle\Repository\ThirdPartyCivilityRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Table(name="chill_3party.party_civility")
* @ORM\Entity(repositoryClass=ThirdPartyCivilityRepository::class)
* @ORM\Table(name="chill_main_civility")
* @ORM\Entity
*/
class ThirdPartyCivility
class Civility
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/
private $id;
/**
* @ORM\Column(type="json")
* @Serializer\Groups({"read"})
*/
private $name = [];
private array $name = [];
/**
* @ORM\Column(type="json")
* @Serializer\Groups({"read"})
*/
private array $abbreviation = [];
/**
* @ORM\Column(type="boolean")
*/
private $active = true;
private bool $active = true;
public function getId(): ?int
{
@ -76,4 +84,22 @@ class ThirdPartyCivility
return $this;
}
/**
* @return array
*/
public function getAbbreviation(): array
{
return $this->abbreviation;
}
/**
* @param array $abbreviation
* @return Civility
*/
public function setAbbreviation(array $abbreviation): self
{
$this->abbreviation = $abbreviation;
return $this;
}
}

View File

@ -1,136 +0,0 @@
<?php
/*
* Copyright (C) 2015 Julien Fastré <julien.fastre@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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/>.
*/
namespace Chill\MainBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CenterType extends AbstractType
{
/**
* The user linked with this type.
*
* @var \Chill\MainBundle\Entity\User
*/
protected $user;
/**
* associative array where keys are center.id and
* value are center objects
*
* @var Center[]
*/
protected $reachableCenters = array();
/**
*
* @var CenterTransformer
*/
protected $transformer;
public function __construct(TokenStorageInterface $tokenStorage,
CenterTransformer $transformer)
{
$this->user = $tokenStorage->getToken()->getUser();
$this->transformer = $transformer;
$this->prepareReachableCenterByUser();
}
/**
* return a 'hidden' field if only one center is available.
*
* Return a 'choice' field if more than one center is available.
*
* @return string
* @throws \RuntimeException if the user is not associated with any center
*/
public function getParent()
{
$nbReachableCenters = count($this->reachableCenters);
if ($nbReachableCenters <= 1) {
return HiddenType::class;
} else {
return EntityType::class;
}
}
/**
* configure default options, i.e. add choices if user can reach multiple
* centers.
*
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
if (count($this->reachableCenters) > 1) {
$resolver->setDefault('class', Center::class)
->setDefault('choices', $this->reachableCenters)
->setDefault('placeholder', 'Pick a center')
;
}
}
/**
* add a data transformer if user can reach only one center
*
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if ($this->getParent() === HiddenType::class) {
$builder->addModelTransformer($this->transformer);
}
}
/**
* populate reachableCenters as an associative array where
* keys are center.id and value are center entities.
*
*/
private function prepareReachableCenterByUser()
{
$groupCenters = $this->user->getGroupCenters();
foreach ($groupCenters as $groupCenter) {
$center = $groupCenter->getCenter();
if (!array_key_exists($center->getId(),
$this->reachableCenters)) {
$this->reachableCenters[$center->getId()] = $center;
}
}
}
}

View File

@ -24,14 +24,14 @@ use Symfony\Component\Form\FormInterface;
/**
* Available options :
*
*
* - `button_add_label`
* - `button_remove_label`
* - `identifier`: an identifier to identify the kind of collecton. Useful if some
* javascript should be launched associated to `add_entry`, `remove_entry` events.
*
* - `empty_collection_explain`
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class ChillCollectionType extends AbstractType
{
@ -41,10 +41,11 @@ class ChillCollectionType extends AbstractType
->setDefaults([
'button_add_label' => 'Add an entry',
'button_remove_label' => 'Remove entry',
'identifier' => ''
'identifier' => '',
'empty_collection_explain' => '',
]);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['button_add_label'] = $options['button_add_label'];
@ -52,8 +53,9 @@ class ChillCollectionType extends AbstractType
$view->vars['allow_delete'] = (int) $options['allow_delete'];
$view->vars['allow_add'] = (int) $options['allow_add'];
$view->vars['identifier'] = $options['identifier'];
$view->vars['empty_collection_explain'] = $options['empty_collection_explain'];
}
public function getParent()
{
return \Symfony\Component\Form\Extension\Core\Type\CollectionType::class;

View File

@ -20,36 +20,57 @@
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Repository\CenterRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
class CenterTransformer implements DataTransformerInterface
{
private EntityManagerInterface $em;
private CenterRepository $centerRepository;
private bool $multiple = false;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
public function __construct(
CenterRepository $centerRepository,
bool $multiple = false
) {
$this->centerRepository = $centerRepository;
$this->multiple = $multiple;
}
public function reverseTransform($id)
{
if ($id === NULL) {
return NULL;
if ($this->multiple) {
return new ArrayCollection();
} else {
return NULL;
}
}
$center = $this
->em
->getRepository(Center::class)
->find($id);
if ($this->multiple) {
$ids = \explode(',', $id);
} else {
$ids[] = (int) $id;
}
if ($center === NULL) {
$centers = $this
->centerRepository
->findBy(['id' => $ids ]);
if ([] === $centers || count($ids) > count($centers)) {
throw new TransformationFailedException(sprintf(
'No center found with id %d', $id));
'No center found for one of those ids: %s', implode(',', $ids)));
}
return $center;
if ($this->multiple) {
return new ArrayCollection($centers);
} else {
return $centers[0];
}
}
public function transform($center)
@ -58,7 +79,21 @@ class CenterTransformer implements DataTransformerInterface
return '';
}
return $center->getId();
}
if ($this->multiple) {
if (!is_iterable($center)) {
throw new UnexpectedTypeException($center, \Traversable::class);
}
$ids = [];
foreach ($center as $c) {
$ids[] = $c->getId();
}
return implode(',', $ids);
} else {
if (!$center instanceof Center) {
throw new UnexpectedTypeException($center, Center::class);
}
return (string) $center->getId();
}
}
}

View File

@ -0,0 +1,55 @@
<?php
namespace Chill\MainBundle\Form\Type\Listing;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\HttpFoundation\RequestStack;
final class FilterOrderType extends \Symfony\Component\Form\AbstractType
{
private RequestStack $requestStack;
public function __construct(RequestStack $requestStack)
{
$this->requestStack = $requestStack;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
/** @var FilterOrderHelper $helper */
$helper = $options['helper'];
if ($helper->hasSearchBox()) {
$builder->add('q', SearchType::class, [
'label' => false,
'required' => false
]);
}
foreach ($this->requestStack->getCurrentRequest()->query->getIterator() as $key => $value) {
switch($key) {
case 'q':
continue;
case 'page':
$builder->add($key, HiddenType::class, [
'data' => 1
]);
break;
default:
$builder->add($key, HiddenType::class, [
'data' => $value
]);
break;
}
}
}
public function configureOptions(\Symfony\Component\OptionsResolver\OptionsResolver $resolver)
{
$resolver->setRequired('helper')
->setAllowedTypes('helper', FilterOrderHelper::class);
}
}

View File

@ -0,0 +1,174 @@
<?php
/*
* Copyright (C) 2015 Julien Fastré <julien.fastre@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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/>.
*/
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Repository\CenterRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Security\Core\Security;
/**
* Pick a center
*
* For a given role and, eventually, scopes, show a dropdown (if more than
* one reachable center) or a HiddenType (if one or zero center).
*
*
*/
class PickCenterType extends AbstractType
{
protected AuthorizationHelperInterface $authorizationHelper;
protected Security $security;
protected CenterRepository $centerRepository;
public function __construct(
AuthorizationHelperInterface $authorizationHelper,
Security $security,
CenterRepository $centerRepository
) {
$this->authorizationHelper = $authorizationHelper;
$this->security = $security;
$this->centerRepository = $centerRepository;
}
/**
* return a 'hidden' field if only one center is available.
*
* Return a 'choice' field if more than one center is available.
*
* @return string
* @throws \RuntimeException if the user is not associated with any center
*/
/*
public function getParent()
{
$nbReachableCenters = count($this->reachableCenters);
if ($nbReachableCenters <= 1) {
return HiddenType::class;
} else {
return EntityType::class;
}
}
*/
/**
* configure default options, i.e. add choices if user can reach multiple
* centers.
*
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('class', Center::class)
->setRequired('role')
->setAllowedTypes('role', [ 'string' ])
->setDefault('scopes', [])
->setAllowedTypes('scopes', ['iterable'])
->setDefault('choice_options', [])
;
/*
->setDefault('choices', $this->reachableCenters)
->setDefault('placeholder', 'Pick a center')
;
*/
}
/**
* add a data transformer if user can reach only one center
*
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$centers = $this->getReachableCenters($options['role'], $options['scopes']);
if (count($centers) <= 1) {
$multiple = $options['choice_options']['multiple'] ?? false;
$builder->add('center', HiddenType::class);
$builder->get('center')->addModelTransformer(
new CenterTransformer($this->centerRepository, $multiple)
);
} else {
$builder->add('center', EntityType::class,
\array_merge(
$options['choice_options'],
[
'class' => Center::class,
'choices' => $centers
]
)
);
}
$builder
->addModelTransformer(new CallbackTransformer(
function($data) {
if (NULL === $data) {
return ['center' => null];
}
return ['center' => $data];
},
function($data) {
return $data['center'];
}
));
}
private function getReachableCenters(string $role, iterable $scopes): array
{
if (0 < count($scopes)) {
$centers = [];
foreach($scopes as $scope) {
foreach ($this->authorizationHelper
->getReachableCenters($this->security->getUser(), $role, $scope) as $center) {
$centers[spl_object_hash($center)] = $center;
}
}
return \array_values($centers);
} else {
return $this->authorizationHelper
->getReachableCenters($this->security->getUser(), $role);
}
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['is_hidden'] = count($this->getReachableCenters($options['role'],
$options['scopes'])) <= 1;
}
}

View File

@ -20,23 +20,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\ThirdPartyBundle\Repository;
namespace Chill\MainBundle\Repository;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCivility;
use Chill\MainBundle\Entity\Civility;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method ThirdPartyCivility|null find($id, $lockMode = null, $lockVersion = null)
* @method ThirdPartyCivility|null findOneBy(array $criteria, array $orderBy = null)
* @method ThirdPartyCivility[] findAll()
* @method ThirdPartyCivility[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
* @method Civility|null find($id, $lockMode = null, $lockVersion = null)
* @method Civility|null findOneBy(array $criteria, array $orderBy = null)
* @method Civility[] findAll()
* @method Civility[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ThirdPartyCivilityRepository extends ServiceEntityRepository
class CivilityRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ThirdPartyCivility::class);
parent::__construct($registry, Civility::class);
}
}

View File

@ -1,27 +1,27 @@
/**
* Javascript file which handle ChillCollectionType
*
* Two events are emitted by this module, both on window and on collection / ul.
*
*
* Two events are emitted by this module, both on window and on collection / ul.
*
* Collection (an UL element) and entry (a li element) are associated with those
* events.
*
*
* ```
* window.addEventListener('collection-add-entry', function(e) {
* console.log(e.detail.collection);
* console.log(e.detail.entry);
* });
*
*
* window.addEventListener('collection-remove-entry', function(e) {
* console.log(e.detail.collection);
* console.log(e.detail.entry);
* });
*
*
* collection.addEventListener('collection-add-entry', function(e) {
* console.log(e.detail.collection);
* console.log(e.detail.entry);
* });
*
*
* collection.addEventListener('collection-remove-entry', function(e) {
* console.log(e.detail.collection);
* console.log(e.detail.entry);
@ -38,7 +38,7 @@ class CollectionEvent {
}
/**
*
*
* @param {type} button
* @returns {handleAdd}
*/
@ -47,6 +47,7 @@ var handleAdd = function(button) {
form_name = button.dataset.collectionAddTarget,
prototype = button.dataset.formPrototype,
collection = document.querySelector('ul[data-collection-name="'+form_name+'"]'),
empty_explain = collection.querySelector('li[data-collection-empty-explain]'),
entry = document.createElement('li'),
event = new CustomEvent('collection-add-entry', { detail: { collection: collection, entry: entry } }),
counter = collection.childNodes.length,
@ -56,8 +57,11 @@ var handleAdd = function(button) {
entry.innerHTML = content;
entry.classList.add('entry');
initializeRemove(collection, entry);
if (empty_explain !== null) {
empty_explain.remove();
}
collection.appendChild(entry);
collection.dispatchEvent(event);
window.dispatchEvent(event);
};
@ -70,30 +74,30 @@ var initializeRemove = function(collection, entry) {
allowDelete = collection.dataset.collectionAllowDelete,
event = new CustomEvent('collection-remove-entry', { detail: { collection: collection, entry: entry } })
;
if (allowDelete === '0' && isPersisted === '1') {
return;
}
button.classList.add('btn', 'btn-delete', 'remove-entry');
button.textContent = content;
button.addEventListener('click', function(e) {
e.preventDefault();
entry.remove();
collection.dispatchEvent(event);
window.dispatchEvent(event);
});
entry.appendChild(button);
};
window.addEventListener('load', function() {
var
var
addButtons = document.querySelectorAll("button[data-collection-add-target]"),
collections = document.querySelectorAll("ul[data-collection-name]")
;
for (let i = 0; i < addButtons.length; i ++) {
let addButton = addButtons[i];
addButton.addEventListener('click', function(e) {
@ -101,11 +105,15 @@ window.addEventListener('load', function() {
handleAdd(e.target);
});
}
for (let i = 0; i < collections.length; i ++) {
let entries = collections[i].querySelectorAll(':scope > li');
for (let j = 0; j < entries.length; j ++) {
console.log(entries[j].dataset);
if (entries[j].dataset.collectionEmptyExplain === "1") {
continue;
}
initializeRemove(collections[i], entries[j]);
}
}

View File

@ -74,7 +74,12 @@ export default {
case 'thirdparty':
let data = this.$refs.castThirdparty.$data.thirdparty;
data.name = data.text;
data.address = { id: data.address.address_id }
if (data.address !== undefined) {
data.address = { id: data.address.address_id }
} else {
data.address = null;
}
return data;
default:
throw Error('Invalid type of entity')

View File

@ -1,5 +1,5 @@
{% set formId = crudMainFormId|default('crud_main_form') %}
<div class="{% block crud_content_main_div_class %}{% endblock %}">
<div class="{% block crud_content_main_div_class %}col-10 centered{% endblock %}">
{% block crud_content_header %}
<h1>{{ ('crud.'~crud_name~'.title_edit')|trans }}</h1>
{% endblock crud_content_header %}

View File

@ -1,9 +1,15 @@
<div class="col-10 centered">
{% block index_header %}
<h1>{{ ('crud.' ~ crud_name ~ '.index.title')|trans({'%crud_name%': crud_name}) }}</h1>
{% endblock index_header %}
{% block filter_order %}
{% if filter_order is not null %}
{{ filter_order|chill_render_filter_order_helper }}
{% endif %}
{% endblock %}
{% if entities|length == 0 %}
{% block no_existing_entities %}
<p>{{ no_existing_entities_sentences|default('No entities')|trans }}</p>
@ -32,17 +38,20 @@
{% endif %}
<div class="crud_index__pagination">
{{ chill_pagination(paginator) }}
</div>
{% block pagination %}
<div class="crud_index__pagination">
{{ chill_pagination(paginator) }}
</div>
{% endblock %}
{% block list_actions %}
<ul class="record_actions sticky-form-buttons">
{% block add_new %}
<li>
<a href="{{ chill_path_add_return_path('chill_crud_' ~ crud_name ~ '_new') }}" class="btn btn-new">{{ ('crud.'~crud_name~'.index.add_new')|trans( {'%crud_name%': crud_name} ) }}</a>
</li>
{% endblock %}
{% block actions_before %}{% endblock %}
{% block add_new %}
<li>
<a href="{{ chill_path_add_return_path('chill_crud_' ~ crud_name ~ '_new') }}" class="btn btn-new">{{ ('crud.'~crud_name~'.index.add_new')|trans( {'%crud_name%': crud_name} ) }}</a>
</li>
{% endblock %}
</ul>
{% endblock list_actions %}
</div>

View File

@ -1,10 +1,11 @@
{% set formId = crudMainFormId|default('crud_main_form') %}
<div class="{% block crud_content_main_div_class %}col-10 centered{% endblock %}">
{% block crud_content_header %}
<h1>{{ ('crud.' ~ crud_name ~ '.title_new')|trans({'%crud_name%' : crud_name }) }}</h1>
{% endblock crud_content_header %}
{% block crud_content_form %}
{{ form_start(form) }}
{{ form_start(form, { 'attr' : { 'id': formId } }) }}
{% block crud_content_form_rows %}
{% for f in form %}{% if f.vars.name != 'submit' %}
@ -14,6 +15,8 @@
{% block crud_content_after_form %}{% endblock %}
{{ form_end(form) }}
{% block crud_content_form_actions %}
<ul class="record_actions sticky-form-buttons">
{% block content_form_actions_back %}
@ -25,21 +28,21 @@
{% endblock %}
{% block content_form_actions_save_and_close %}
<li class="">
<button type="submit" name="submit" value="save-and-close" class="btn btn-create">
<button type="submit" name="submit" value="save-and-close" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_close'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_save_and_show %}
<li class="">
<button type="submit" name="submit" value="save-and-show" class="btn btn-create">
<button type="submit" name="submit" value="save-and-show" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_show'|trans }}
</button>
</li>
{% endblock %}
{% block content_form_actions_save_and_new %}
<li class="">
<button type="submit" name="submit" value="save-and-new" class="btn btn-create">
<button type="submit" name="submit" value="save-and-new" class="btn btn-create" form="{{ formId }}">
{{ 'crud.new.save_and_new'|trans }}
</button>
</li>

View File

@ -0,0 +1,12 @@
{{ form_start(form) }}
<div class="chill_filter_order container">
<div class="row">
<div class="col-md-12">
<div class="input-group mb-3">
{{ form_widget(form.q)}}
<button type="submit" class="btn btn-chill-l-gray"><i class="fa fa-search"></i></button>
</div>
</div>
</div>
</div>
{{ form_end(form) }}

View File

@ -168,6 +168,10 @@
{{ form_widget(entry) }}
</div>
</li>
{% else %}
<li data-collection-empty-explain="1">
<span class="chill-no-data-statement">{{ form.vars.empty_collection_explain|default('No item')|trans }}</span>
</li>
{% endfor %}
</ul>
@ -198,3 +202,15 @@
{{ form_widget(entry) }}
{% endfor %}
{% endblock comment_widget %}
{% block pick_center_widget %}
{{ form_widget(form.center) }}
{% endblock pick_center_widget %}
{% block pick_center_row %}
{% if (not form.vars.is_hidden) %}
{{ block('form_row') }}
{% else %}
{{ form_widget(form.center) }}
{% endif %}
{% endblock %}

View File

@ -41,9 +41,8 @@ use Chill\MainBundle\Entity\RoleScope;
*
* Provides methods for user and entities information.
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class AuthorizationHelper
class AuthorizationHelper implements AuthorizationHelperInterface
{
protected RoleHierarchyInterface $roleHierarchy;
@ -203,9 +202,9 @@ class AuthorizationHelper
* @param User $user
* @param string|Role $role
* @param null|Scope $scope
* @return Center[]
* @return Center[]|array
*/
public function getReachableCenters(User $user, $role, Scope $scope = null)
public function getReachableCenters(User $user, string $role, ?Scope $scope = null): array
{
if ($role instanceof Role) {
$role = $role->getRole();
@ -267,9 +266,9 @@ class AuthorizationHelper
* @param User $user
* @param string role
* @param Center|Center[] $center
* @return Scope[]
* @return Scope[]|array
*/
public function getReachableScopes(User $user, $role, $center)
public function getReachableScopes(User $user, string $role, $center): array
{
if ($role instanceof Role) {
$role = $role->getRole();

View File

@ -0,0 +1,32 @@
<?php
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Symfony\Component\Security\Core\Role\Role;
interface AuthorizationHelperInterface
{
/**
* Get reachable Centers for the given user, role,
* and optionnaly Scope
*
* @param User $user
* @param string|Role $role
* @param null|Scope $scope
* @return Center[]
*/
public function getReachableCenters(User $user, string $role, ?Scope $scope = null): array;
/**
* @param User $user
* @param string $role
* @param Center|Center[]|array $center
* @return array
*/
public function getReachableScopes(User $user, string $role, $center): array;
}

View File

@ -0,0 +1,60 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
use Chill\MainBundle\Form\Type\Listing\FilterOrderType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class FilterOrderHelper
{
private FormFactoryInterface $formFactory;
private RequestStack $requestStack;
private ?array $searchBoxFields = null;
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
}
public function setSearchBox($searchBoxFields = null): self
{
$this->searchBoxFields = $searchBoxFields;
return $this;
}
public function hasSearchBox(): bool
{
return $this->searchBoxFields !== null;
}
private function getFormData(): array
{
return [
'q' => $this->getQueryString()
];
}
public function getQueryString(): ?string
{
$q = $this->requestStack->getCurrentRequest()
->query->get('q', null);
return empty($q) ? NULL : $q;
}
public function buildForm($name = null, string $type = FilterOrderType::class, array $options = []): FormInterface
{
return $this->formFactory
->createNamed($name, $type, $this->getFormData(), \array_merge([
'helper' => $this,
'method' => 'GET',
'csrf_protection' => false,
], $options));
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class FilterOrderHelperBuilder
{
private ?array $searchBoxFields = null;
private FormFactoryInterface $formFactory;
private RequestStack $requestStack;
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
}
public function addSearchBox(array $fields, ?array $options = []): self
{
$this->searchBoxFields = $fields;
return $this;
}
public function build(): FilterOrderHelper
{
$helper = new FilterOrderHelper(
$this->formFactory,
$this->requestStack
);
$helper->setSearchBox($this->searchBoxFields);
return $helper;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
use Symfony\Component\Form\FormFactoryBuilderInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class FilterOrderHelperFactory implements FilterOrderHelperFactoryInterface
{
private FormFactoryInterface $formFactory;
private RequestStack $requestStack;
public function __construct(
FormFactoryInterface $formFactory,
RequestStack $requestStack
) {
$this->formFactory = $formFactory;
$this->requestStack = $requestStack;
}
public function create(string $context, ?array $options = []): FilterOrderHelperBuilder
{
return new FilterOrderHelperBuilder($this->formFactory, $this->requestStack);
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
interface FilterOrderHelperFactoryInterface
{
public function create(string $context, ?array $options = []): FilterOrderHelperBuilder;
}

View File

@ -0,0 +1,34 @@
<?php
namespace Chill\MainBundle\Templating\Listing;
use Twig\Environment;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class Templating extends AbstractExtension
{
public function getFilters()
{
return [
new TwigFilter('chill_render_filter_order_helper', [$this, 'renderFilterOrderHelper'], [
'needs_environment' => true, 'is_safe' => ['html'],
])
];
}
public function renderFilterOrderHelper(
Environment $environment,
FilterOrderHelper $helper,
?string $template = '@ChillMain/FilterOrder/base.html.twig',
?array $options = []
) {
return $environment->render($template, [
'helper' => $helper,
'form' => $helper->buildForm()->createView(),
'options' => $options
]);
}
}

View File

@ -28,18 +28,19 @@ use Symfony\Bridge\Doctrine\Form\Type\EntityType;
/**
*
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CenterTypeTest extends TypeTestCase
{
/**
* Test that a user which can reach only one center
* Test that a user which can reach only one center
* render as an hidden field
*/
public function testUserCanReachSingleCenter()
{
$this->markTestSkipped();
//prepare user
$center = $this->prepareCenter(1, 'center');
$groupCenter = (new GroupCenter())
@ -47,18 +48,19 @@ class CenterTypeTest extends TypeTestCase
;
$user = (new User())
->addGroupCenter($groupCenter);
$type = $this->prepareType($user);
$this->assertEquals(HiddenType::class, $type->getParent());
}
/**
* Test that a user which can reach only one center
* Test that a user which can reach only one center
* render as an hidden field
*/
public function testUserCanReachMultipleSameCenter()
{
$this->markTestSkipped();
//prepare user
$center = $this->prepareCenter(1, 'center');
$groupCenterA = (new GroupCenter())
@ -70,18 +72,19 @@ class CenterTypeTest extends TypeTestCase
$user = (new User())
->addGroupCenter($groupCenterA)
->addGroupCenter($groupCenterB);
$type = $this->prepareType($user);
$this->assertEquals(HiddenType::class, $type->getParent());
}
/**
* Test that a user which can reach multiple center
* Test that a user which can reach multiple center
* make CenterType render as "entity" type.
*/
public function testUserCanReachMultipleCenters()
{
$this->markTestSkipped();
//prepare user
$centerA = $this->prepareCenter(1, 'centerA');
$centerB = $this->prepareCenter(2, 'centerB');
@ -95,61 +98,61 @@ class CenterTypeTest extends TypeTestCase
->addGroupCenter($groupCenterA)
->addGroupCenter($groupCenterB)
;
$type = $this->prepareType($user);
$this->assertEquals(EntityType::class, $type->getParent());
}
/**
* prepare a mocked center, with and id and name given
*
*
* @param int $id
* @param string $name
* @return \Chill\MainBundle\Entity\Center
* @return \Chill\MainBundle\Entity\Center
*/
private function prepareCenter($id, $name)
{
$prophet = new \Prophecy\Prophet;
$prophecyCenter = $prophet->prophesize();
$prophecyCenter->willExtend('\Chill\MainBundle\Entity\Center');
$prophecyCenter->getId()->willReturn($id);
$prophecyCenter->getName()->willReturn($name);
return $prophecyCenter->reveal();
}
/**
* prepare the type with mocked center transformer and token storage
*
*
* @param User $user the user for wich the form will be prepared
* @return CenterType
*/
private function prepareType(User $user)
{
$prophet = new \Prophecy\Prophet;
$prophet = new \Prophecy\Prophet;
//create a center transformer
$centerTransformerProphecy = $prophet->prophesize();
$centerTransformerProphecy
->willExtend('Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer');
$transformer = $centerTransformerProphecy->reveal();
$tokenProphecy = $prophet->prophesize();
$tokenProphecy
->willImplement('\Symfony\Component\Security\Core\Authentication\Token\TokenInterface');
$tokenProphecy->getUser()->willReturn($user);
$token = $tokenProphecy->reveal();
$tokenStorageProphecy = $prophet->prophesize();
$tokenStorageProphecy
->willExtend('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage');
$tokenStorageProphecy->getToken()->willReturn($token);
$tokenStorage = $tokenStorageProphecy->reveal();
return new CenterType($tokenStorage, $transformer);
}
}

View File

@ -60,11 +60,6 @@ services:
tags:
- { name: twig.extension }
chill.main.form.data_transformer.center_transformer:
class: Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer
arguments:
- "@doctrine.orm.entity_manager"
chill.main.validator.role_scope_scope_presence:
class: Chill\MainBundle\Validation\Validator\RoleScopeScopePresence
arguments:

View File

@ -36,13 +36,9 @@ services:
tags:
- { name: form.type, alias: select2_chill_language }
chill.main.form.type.center:
class: Chill\MainBundle\Form\Type\CenterType
arguments:
- "@security.token_storage"
- "@chill.main.form.data_transformer.center_transformer"
tags:
- { name: form.type, alias: center }
Chill\MainBundle\Form\Type\PickCenterType:
autowire: true
autoconfigure: true
chill.main.form.type.composed_role_scope:
class: Chill\MainBundle\Form\Type\ComposedRoleScopeType
@ -97,6 +93,10 @@ services:
arguments:
- '@Chill\MainBundle\Export\ExportManager'
Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer:
autowire: true
autoconfigure: true
chill.main.form.advanced_search_type:
class: Chill\MainBundle\Form\AdvancedSearchType
autowire: true

View File

@ -38,6 +38,7 @@ services:
autowire: true
autoconfigure: true
Chill\MainBundle\Security\Authorization\AuthorizationHelper: '@chill.main.security.authorization.helper'
Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface: '@chill.main.security.authorization.helper'
chill.main.role_provider:
class: Chill\MainBundle\Security\RoleProvider

View File

@ -36,7 +36,7 @@ services:
autowire: true
tags:
- { name: 'chill.render_entity' }
Chill\MainBundle\Templating\ChillMarkdownRenderExtension:
tags:
- { name: twig.extension }
@ -46,4 +46,10 @@ services:
- '@Symfony\Component\Templating\EngineInterface'
tags:
- { name: 'chill.render_entity' }
Chill\MainBundle\Templating\Listing\:
resource: './../../Templating/Listing'
autoconfigure: true
autowire: true
Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface: '@Chill\MainBundle\Templating\Listing\FilterOrderHelperFactory'

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Create civility table
*/
final class Version20211007150019 extends AbstractMigration
{
public function getDescription(): string
{
return 'create civility table';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_main_civility_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_main_civility (id INT NOT NULL, name JSON NOT NULL, abbreviation JSON NOT NULL, active BOOLEAN NOT NULL, PRIMARY KEY(id))');
}
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_main_civility_id_seq');
$this->addSql('DROP TABLE chill_main_civility');
}
}

View File

@ -56,6 +56,7 @@ centers: centres
Centers: Centres
comment: commentaire
Comment: Commentaire
Any comment: Aucun commentaire
# comment embeddable
No comment associated: Aucun commentaire

View File

@ -155,7 +155,7 @@ final class PersonController extends AbstractController
$person = $this->_getPerson($person_id);
if ($person === null) {
return $this->createNotFoundException();
throw $this->createNotFoundException();
}
$this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person,

View File

@ -22,7 +22,9 @@
namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Form\Event\CustomizeFormEvent;
use Chill\MainBundle\Repository\CenterRepository;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
@ -30,12 +32,11 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\CenterType;
use Chill\MainBundle\Form\Type\PickCenterType;
use Chill\PersonBundle\Form\Type\GenderType;
use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
final class CreationPersonType extends AbstractType
{
@ -43,11 +44,7 @@ final class CreationPersonType extends AbstractType
// TODO: See if this is still valid and update accordingly.
const NAME = 'chill_personbundle_person_creation';
/**
*
* @var CenterTransformer
*/
private $centerTransformer;
private CenterRepository $centerRepository;
/**
*
@ -58,11 +55,11 @@ final class CreationPersonType extends AbstractType
private EventDispatcherInterface $dispatcher;
public function __construct(
CenterTransformer $centerTransformer,
CenterRepository $centerRepository,
ConfigPersonAltNamesHelper $configPersonAltNamesHelper,
EventDispatcherInterface $dispatcher
) {
$this->centerTransformer = $centerTransformer;
$this->centerTransformer = $centerRepository;
$this->configPersonAltNamesHelper = $configPersonAltNamesHelper;
$this->dispatcher = $dispatcher;
}
@ -82,8 +79,9 @@ final class CreationPersonType extends AbstractType
->add('gender', GenderType::class, array(
'required' => true, 'placeholder' => null
))
->add('center', CenterType::class, [
'required' => false
->add('center', PickCenterType::class, [
'required' => false,
'role' => PersonVoter::CREATE,
])
;

View File

@ -1,37 +1,102 @@
<template>
<div class="container">
<span class="name">
{{ item.result.text }}
<div class="container tpartycontainer">
<div class="tparty-identification">
<span class="name">
{{ item.result.text }}
</span>
<span class="location">
<template v-if="hasAddress">
{{ getAddress.text }} -
{{ getAddress.postcode.name }}
</template>
</span>
</div>
<div class="tpartyparent" v-if="hasParent">
<span class="name">
{{ item.result.parent.text }}
</span>
</div>
</div>
<div class="right_actions">
<span class="badge bg-chill-red" v-if="item.result.kind == 'child'">
{{ $t('thirdparty.contact')}}
</span>
<span class="badge bg-info" v-else-if="item.result.kind == 'company'">
{{ $t('thirdparty.company')}}
</span>
<span class="location">
{{ item.result.address.text }} -
{{ item.result.address.postcode.name }}
<span class="badge bg-secondary" v-else="item.result.kind == 'contact'">
{{ $t('thirdparty.contact')}}
</span>
</div>
<div class="right_actions">
<span class="badge rounded-pill bg-secondary" :title="item.key">
{{ $t('item.type_thirdparty') }}
</span>
<on-the-fly
type="thirdparty"
v-bind:id="item.result.id"
action="show">
type="thirdparty"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
</div>
</div>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
const i18n = {
messages: {
fr: {
thirdparty: {
contact: "Contact",
company: "Institution",
child: "Personne de contact"
}
}
}
};
export default {
name: 'SuggestionThirdParty',
components: {
OnTheFly
},
props: ['item']
props: ['item'],
i18n,
computed: {
hasAddress() {
if (this.$props.item.result.address !== null) {
return true;
}
if (this.$props.item.result.parent !== null) {
this.$props.item.result.parent.address !== null;
}
},
hasParent() {
return this.$props.item.result.parent !== null;
},
getAddress() {
if (this.$props.item.result.address !== null) {
return this.$props.item.result.address;
}
if (this.$props.item.result.parent.address !== null) {
return this.$props.item.result.parent.address;
}
return null;
}
}
}
</script>
<style lang="scss" scoped>
.tpartycontainer {
.tpartyparent {
.name {
font-weight: bold;
font-variant: all-small-caps;
}
}
}
</style>

View File

@ -2,7 +2,18 @@
namespace Chill\ThirdPartyBundle\Controller;
use Chill\MainBundle\CRUD\Controller\AbstractCRUDController;
use Chill\MainBundle\CRUD\Controller\CRUDController;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
use Chill\ThirdPartyBundle\Repository\ThirdPartyACLAwareRepositoryInterface;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use http\Exception\RuntimeException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
@ -16,12 +27,7 @@ use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Security\Core\Role\Role;
use Chill\MainBundle\Pagination\PaginatorFactory;
/**
* Routes for operations on ThirdParties.
*
* @Route("/{_locale}/thirdparty/thirdparty")
*/
class ThirdPartyController extends Controller
final class ThirdPartyController extends CRUDController
{
/**
*
@ -41,145 +47,86 @@ class ThirdPartyController extends Controller
*/
protected $paginatorFactory;
protected ThirdPartyACLAwareRepositoryInterface $thirdPartyACLAwareRepository;
protected RequestStack $requestStack;
public function __construct(
AuthorizationHelper $authorizationHelper,
TranslatorInterface $translator,
PaginatorFactory $paginatorFactory
PaginatorFactory $paginatorFactory,
RequestStack $requestStack,
ThirdPartyACLAwareRepositoryInterface $thirdPartyACLAwareRepository
) {
$this->authorizationHelper = $authorizationHelper;
$this->translator = $translator;
$this->paginatorFactory = $paginatorFactory;
$this->requestStack = $requestStack;
$this->thirdPartyACLAwareRepository = $thirdPartyACLAwareRepository;
}
/**
* @Route("/index", name="chill_3party_3party_index")
*/
public function indexAction()
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::SHOW);
$repository = $this->getDoctrine()->getManager()
->getRepository(ThirdParty::class);
$nbThirdParties = $repository->count([]); //$repository->countByMemberOfCenters($centers);
$pagination = $this->paginatorFactory->create($nbThirdParties);
$thirdParties = $repository->findAll();
return $this->render('ChillThirdPartyBundle:ThirdParty:index.html.twig', array(
'third_parties' => $thirdParties,
'pagination' => $pagination
));
}
/**
* @Route("/new", name="chill_3party_3party_new")
*/
public function newAction(Request $request)
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE);
$centers = [];
$thirdParty = new ThirdParty();
$thirdParty->setCenters(new ArrayCollection($centers));
$form = $this->createForm(ThirdPartyType::class, $thirdParty, [
'usage' => 'create'
]);
$form->add('submit', SubmitType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($thirdParty);
$em->flush();
$this->addFlash('success',
$this->translator->trans("Third party created")
);
return $this->redirectToRoute('chill_3party_3party_show', [
'thirdparty_id' => $thirdParty->getId()
]);
} elseif ($form->isSubmitted()) {
$msg = $this->translator->trans('This form contains errors');
$this->addFlash('error', $msg);
if (NULL === $filterOrder){
throw new \LogicException('filterOrder should not be null');
}
return $this->render('@ChillThirdParty/ThirdParty/new.html.twig', [
'form' => $form->createView(),
'thirdParty' => $thirdParty
]);
return $this->thirdPartyACLAwareRepository->countThirdParties(ThirdPartyVoter::SHOW,
$filterOrder->getQueryString());
}
/**
* @Route("/{thirdparty_id}/update", name="chill_3party_3party_update")
* @ParamConverter("thirdParty", options={"id": "thirdparty_id"})
*/
public function updateAction(ThirdParty $thirdParty, Request $request)
protected function getQueryResult(string $action, Request $request, int $totalItems, PaginatorInterface $paginator, ?FilterOrderHelper $filterOrder = null)
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::CREATE);
return $this->thirdPartyACLAwareRepository
->listThirdParties(ThirdPartyVoter::SHOW, $filterOrder->getQueryString(), ['name' => 'ASC'], $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber());
}
$repository = $this->getDoctrine()->getManager()
->getRepository(ThirdParty::class);
$centers = $repository->findAll();
// we want to keep centers the users has no access to. So we will add them
// later if they are removed. (this is a ugly hack but it will works
$centersAssociatedNotForUsers = \array_diff(
$thirdParty->getCenters()->toArray(),
$centers);
$form = $this->createForm(ThirdPartyType::class, $thirdParty, [
'usage' => 'create'
]);
$form->add('submit', SubmitType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// re-add centers the user has no accesses:
foreach ($centersAssociatedNotForUsers as $c) {
$thirdParty->addCenter($c);
protected function onPostCheckACL($action, Request $request, $entity): ?Response
{
if ('edit' === $action || 'view' === $action) {
if ($entity->isChild()) {
throw $this->createAccessDeniedException();
}
$em = $this->getDoctrine()->getManager();
$em->flush();
$this->addFlash('success',
$this->translator->trans("Third party updated")
);
return $this->redirectToRoute('chill_3party_3party_show', [
'thirdparty_id' => $thirdParty->getId()
]);
} elseif ($form->isSubmitted()) {
$msg = $this->translator->trans('This form contains errors');
$this->addFlash('error', $msg);
}
return $this->render('@ChillThirdParty/ThirdParty/update.html.twig', [
'form' => $form->createView(),
'thirdParty' => $thirdParty
]);
if ('new' === $action) {
if (!$request->query->has('kind')) {
return $this->render('@ChillThirdParty/ThirdParty/new_pick_kind.html.twig');
} else {
$kind = $request->query->getAlpha('kind', '');
if (!(ThirdParty::KIND_COMPANY === $kind || ThirdParty::KIND_CONTACT === $kind)) {
throw new BadRequestHttpException('This kind is not supported: '.$kind);
}
$entity->setKind($kind);
}
}
return null;
}
/**
* @Route("/{thirdparty_id}/show", name="chill_3party_3party_show")
* @ParamConverter("thirdParty", options={"id": "thirdparty_id"})
*/
public function showAction(ThirdParty $thirdParty, Request $request)
protected function createFormFor(string $action, $entity, string $formClass = null, array $formOptions = []): FormInterface
{
$this->denyAccessUnlessGranted(ThirdPartyVoter::SHOW, $thirdParty);
if ('new' === $action) {
return parent::createFormFor($action, $entity, $formClass, \array_merge(
$formOptions, [ 'kind' => $this->requestStack->getCurrentRequest()->query->getAlpha('kind')]
));
} elseif ('edit' === $action) {
return parent::createFormFor($action, $entity, $formClass, \array_merge(
$formOptions, [ 'kind' => $entity->getKind()]
));
}
return $this->render('@ChillThirdParty/ThirdParty/show.html.twig', [
'thirdParty' => $thirdParty
]);
return parent::createFormFor($action, $entity, $formClass, $formOptions);
}
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
{
return $this->getFilterOrderHelperFactory()
->create(self::class)
->addSearchBox(['name', 'company_name', 'acronym'])
->build();
}
}

View File

@ -21,13 +21,14 @@ class LoadThirdParty extends Fixture Implements DependentFixtureInterface
$thirdParties = $this->getThirdParties()->getObjects();
foreach ($thirdParties as $name => $thirdParty) {
if ('a' === $name[0]) {
if ('a' === $name[0]) {
// this is an address
continue;
}
$thirdParty->setCreatedAt(new \DateTimeImmutable('today'));
foreach ($this->getCenters() as $center) {
$thirdParty->addCenter($center);
$thirdParty->addCenter($center);
}
$manager->persist($thirdParty);
@ -38,7 +39,7 @@ class LoadThirdParty extends Fixture Implements DependentFixtureInterface
private function getCenters(): \Iterator
{
$references = \array_map(function($a) { return $a['ref']; },
$references = \array_map(function($a) { return $a['ref']; },
LoadCenters::$centers);
$number = random_int(1, count($references));

View File

@ -2,6 +2,9 @@
namespace Chill\ThirdPartyBundle\DependencyInjection;
use Chill\ThirdPartyBundle\Controller\ThirdPartyController;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Form\ThirdPartyType;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@ -37,6 +40,7 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte
$loader->load('services/menu.yaml');
$loader->load('services/fixtures.yaml');
$loader->load('services/serializer.yaml');
$loader->load('services/repository.yaml');
}
public function prepend(ContainerBuilder $container)
@ -54,6 +58,34 @@ class ChillThirdPartyExtension extends Extension implements PrependExtensionInte
'@ChillThirdPartyBundle/config/routes.yaml'
)
],
'cruds' => [
[
'class' => ThirdParty::class,
'controller' => ThirdPartyController::class,
'name' => '3party_3party',
'base_path' => '/3party/3party',
'form_class' => ThirdPartyType::class,
'actions' => [
'index' => [
'template' => '@ChillThirdParty/ThirdParty/index.html.twig',
'role' => ThirdPartyVoter::SHOW,
],
'new' => [
'template' => '@ChillThirdParty/ThirdParty/new.html.twig',
'role' => ThirdPartyVoter::CREATE,
],
'edit' => [
'template' => '@ChillThirdParty/ThirdParty/update.html.twig',
'role' => ThirdPartyVoter::UPDATE,
],
'view' => [
'template' => '@ChillThirdParty/ThirdParty/view.html.twig',
'role' => ThirdPartyVoter::SHOW,
]
]
]
],
'apis' => [
[
'class' => \Chill\ThirdPartyBundle\Entity\ThirdParty::class,

View File

@ -22,6 +22,9 @@
namespace Chill\ThirdPartyBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
@ -31,6 +34,7 @@ use Symfony\Component\Validator\Constraints as Assert;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
/**
* ThirdParty is a party recorded in the database.
@ -39,14 +43,13 @@ use Symfony\Component\Serializer\Annotation\Groups;
* all users with the right 'CHILL_3PARTY_3PARTY_SEE', 'CHILL_3PARTY_3 to see, select and edit parties for this
* center.
*
* @ORM\Entity
* @ORM\Table(name="chill_3party.third_party")
* @ORM\Entity(repositoryClass="Chill\ThirdPartyBundle\Repository\ThirdPartyRepository")
* @DiscriminatorMap(typeProperty="type", mapping={
* "thirdparty"=ThirdParty::class
* })
* @ORM\HasLifecycleCallbacks()
*/
class ThirdParty
class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
{
/**
* @var int
@ -54,7 +57,17 @@ class ThirdParty
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
private ?int $id = null;
const KIND_CONTACT = 'contact';
const KIND_COMPANY = 'company';
const KIND_CHILD = 'child';
/**
* @ORM\Column(name="kind", type="string", length="20", options={"default":""})
* @Groups({"write"})
*/
private ?string $kind = "";
/**
* @var string
@ -62,7 +75,7 @@ class ThirdParty
* @Assert\Length(min="2")
* @Groups({"read", "write"})
*/
private $name;
private ?string $name = "";
/**
* [fr] Raison sociale
@ -71,7 +84,16 @@ class ThirdParty
* @Assert\Length(min="3")
* @Groups({"read", "write"})
*/
private $nameCompany;
private ?string $nameCompany = "";
/**
* Canonicalized form composed of name, company name and acronym.
*
* This field is read-only, and is generated on database side.
*
* @ORM\Column(name="canonicalized", type="text", options={"default":""})
*/
private ?string $canonicalized = "";
/**
* [fr] Sigle
@ -80,7 +102,7 @@ class ThirdParty
* @Assert\Length(min="2")
* @Groups({"read", "write"})
*/
private $acronym;
private ?string $acronym = "";
/**
* @var ThirdPartyCategory
@ -89,18 +111,20 @@ class ThirdParty
* joinColumns={@ORM\JoinColumn(name="thirdparty_id", referencedColumnName="id")},
* inverseJoinColumns={@ORM\JoinColumn(name="category_id", referencedColumnName="id")})
*/
private $categories;
private Collection $categories;
/**
* @var array|null
* @ORM\Column(name="types", type="json", nullable=true)
* @Assert\Count(min=1)
*/
private $types;
/**
* Contact Persons: One Institutional ThirdParty has Many Contact Persons
* @ORM\OneToMany(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty", mappedBy="parent")
* @ORM\OneToMany(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty", mappedBy="parent",
* cascade={"persist"}, orphanRemoval=true)
* @var ThirdParty[]|Collection
* @Assert\Valid(traverse=true)
*/
private Collection $children;
@ -108,23 +132,24 @@ class ThirdParty
* Institutional ThirdParty: Many Contact Persons have One Institutional ThirdParty
* @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", referencedColumnName="id")
* @Groups({"read"})
*/
private ?ThirdParty $parent;
private ?ThirdParty $parent = null;
/**
* @var ThirdPartyCivility
* @ORM\OneToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdPartyCivility")
* @ORM\JoinColumn(name="civility", referencedColumnName="id", nullable=true)
* @var Civility
* @ORM\ManyToOne(targetEntity=Civility::class)
* ORM\JoinColumn(name="civility", referencedColumnName="id", nullable=true)
*/
private $civility;
private ?Civility $civility = null;
/**
* [fr] Qualité
* @var ThirdPartyProfession
* @ORM\OneToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdPartyProfession")
* @ORM\JoinColumn(name="profession", referencedColumnName="id", nullable=true)
* @ORM\ManyToOne(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdPartyProfession")
* ORM\JoinColumn(name="profession", referencedColumnName="id", nullable=true)
*/
private $profession;
private ?ThirdPartyProfession $profession = null;
/**
* @var string|null
@ -132,9 +157,10 @@ class ThirdParty
* @Assert\Regex("/^([\+{1}])([0-9\s*]{4,20})$/",
* message="Invalid phone number: it should begin with the international prefix starting with ""+"", hold only digits and be smaller than 20 characters. Ex: +33123456789"
* )
* @PhonenumberConstraint(type="any")
* @Groups({"read", "write"})
*/
private $telephone;
private ?string $telephone = null;
/**
* @var string|null
@ -142,7 +168,13 @@ class ThirdParty
* @Assert\Email(checkMX=false)
* @Groups({"read", "write"})
*/
private $email;
private ?string $email = null;
/**
* @var bool
* @ORM\Column(name="contact_data_anonymous", type="boolean", options={"default":false})
*/
private bool $contactDataAnonymous = false;
/**
* @var Address|null
@ -151,27 +183,24 @@ class ThirdParty
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* @Groups({"read", "write"})
*/
private $address;
private ?Address $address = null;
/**
* Soft-delete flag
* @var boolean
* @ORM\Column(name="active", type="boolean", options={"defaut": true})
*/
private $active = true;
private bool $active = true;
/**
* @var string|null
* @ORM\Column(name="comment", type="text", nullable=true)
*/
private $comment;
private ?string $comment = null;
/**
* @var Collection
* @ORM\ManyToMany(targetEntity="\Chill\MainBundle\Entity\Center")
* @ORM\JoinTable(name="chill_3party.party_center")
*/
private $centers;
private Collection $centers;
/**
* @ORM\Column(name="created_at", type="datetime_immutable", nullable=false)
@ -179,33 +208,21 @@ class ThirdParty
private \DateTimeImmutable $createdAt;
/**
* @ORM\Column(name="updated_at", type="datetime", nullable=true)
* @ORM\Column(name="updated_at", type="datetime_immutable", nullable=true)
*/
private ?\DateTime $updatedAt;
private ?\DateTimeImmutable $updatedAt;
/**
* @var User
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @ORM\JoinColumn(name="updated_by", referencedColumnName="id")
*/
private $updatedBy;
private ?User $updatedBy;
/**
* @ORM\PrePersist()
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @ORM\JoinColumn(name="created_by", referencedColumnName="id")
*/
public function prePersist()
{
$this->createdAt = new \DateTimeImmutable();
}
/**
* @ORM\PreUpdate()
*/
public function preUpdate()
{
$this->updatedAt = new \DateTime();
}
private ?User $createdBy;
/**
@ -228,6 +245,17 @@ class ThirdParty
return $this->id;
}
public function getKind(): ?string
{
return $this->kind;
}
public function setKind(?string $kind): ThirdParty
{
$this->kind = $kind;
return $this;
}
/**
* Set name.
*
@ -297,6 +325,17 @@ class ThirdParty
return $this->email;
}
public function isContactDataAnonymous(): bool
{
return $this->contactDataAnonymous;
}
public function setContactDataAnonymous(bool $contactDataAnonymous): ThirdParty
{
$this->contactDataAnonymous = $contactDataAnonymous;
return $this;
}
/**
* Set comment.
*
@ -331,6 +370,10 @@ class ThirdParty
// remove all keys from the input data
$this->type = \array_values($type);
foreach ($this->children as $child) {
$child->setTypes($type);
}
return $this;
}
@ -367,6 +410,9 @@ class ThirdParty
public function setActive(bool $active)
{
$this->active = $active;
foreach ($this->children as $child) {
$child->setActive($active);
}
return $this;
}
@ -418,10 +464,10 @@ class ThirdParty
}
/**
* @param Address $address
* @param Address|null $address
* @return $this
*/
public function setAddress(Address $address)
public function setAddress(?Address $address = null)
{
$this->address = $address;
@ -448,9 +494,9 @@ class ThirdParty
* @param string $nameCompany
* @return ThirdParty
*/
public function setNameCompany(string $nameCompany): ThirdParty
public function setNameCompany(?string $nameCompany): ThirdParty
{
$this->nameCompany = $nameCompany;
$this->nameCompany = (string) $nameCompany;
return $this;
}
@ -486,7 +532,14 @@ class ThirdParty
*/
public function addCategory(ThirdPartyCategory $category): self
{
$this->categories[] = $category;
if (!$this->categories->contains($category)) {
$this->categories[] = $category;
}
foreach ($this->children as $child) {
$child->addCategory($child);
}
return $this;
}
@ -497,6 +550,11 @@ class ThirdParty
public function removeCategory(ThirdPartyCategory $category): self
{
$this->categories->removeElement($category);
foreach ($this->children as $child) {
$child->removeCategory($child);
}
return $this;
}
@ -509,19 +567,18 @@ class ThirdParty
}
/**
* isLeaf aliases
* @Groups({"read"})
*/
public function isChild():bool
{
return $this->isLeaf();
return $this->parent !== null;
}
public function isParent():bool
{
return !$this->isLeaf();
return !$this->isChild();
}
/**
* @return Collection
*/
@ -530,6 +587,47 @@ class ThirdParty
return $this->children;
}
/**
* Get the children where active = true
*
* @return Collection
*/
public function getActiveChildren(): Collection
{
return $this->children->filter(fn (ThirdParty $tp) => $tp->getActive());
}
/**
* Add a child and set the child as active
*
* Method used in conjonction with getActiveChildren in form.
*
* @internal use the method addChild
* @param ThirdParty $child
* @return $this
*/
public function addActiveChild(ThirdParty $child): self
{
$child->setActive(true);
return $this->addChild($child);
}
/**
* mark the child as unactive, but keep the child existing in the
* database. To effectively remove the child, use removeChild instead.
*
* @param ThirdParty $child
* @return $this
*/
public function removeActiveChild(ThirdParty $child): self
{
$child->setActive(false);
return $this;
}
/**
* @param ThirdParty $child
* @return $this
@ -537,17 +635,24 @@ class ThirdParty
public function addChild(ThirdParty $child): self
{
$this->children[] = $child;
$child->setParent($this)->setKind(ThirdParty::KIND_CHILD);;
return $this;
}
/**
* Remove the child from the database.
*
* If you want to keep the child into the database
* but desactivate it, use removeActiveChildren instead.
*
* @param ThirdParty $child
* @return $this
*/
public function removeChild(ThirdParty $child): self
{
$this->children->removeElement($child);
$this->active = false;
return $this;
}
@ -570,18 +675,18 @@ class ThirdParty
}
/**
* @return ThirdPartyCivility|null
* @return Civility|null
*/
public function getCivility(): ?ThirdPartyCivility
public function getCivility(): ?Civility
{
return $this->civility;
}
/**
* @param ThirdPartyCivility $civility
* @param Civility $civility
* @return $this
*/
public function setCivility(ThirdPartyCivility $civility): ThirdParty
public function setCivility(Civility $civility): ThirdParty
{
$this->civility = $civility;
return $this;
@ -617,7 +722,7 @@ class ThirdParty
* @param \DateTimeImmutable $createdAt
* @return $this
*/
public function setCreatedAt(\DateTimeImmutable $createdAt): ThirdParty
public function setCreatedAt(\DateTimeInterface $createdAt): ThirdParty
{
$this->createdAt = $createdAt;
return $this;
@ -626,16 +731,16 @@ class ThirdParty
/**
* @return \DateTime|null
*/
public function getUpdatedAt(): ?\DateTime
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
/**
* @param \DateTime $updatedAt
* @param \DateTimeImmutable $updatedAt
* @return $this
*/
public function setUpdatedAt(\DateTime $updatedAt): ThirdParty
public function setUpdatedAt(\DateTimeInterface $updatedAt): ThirdParty
{
$this->updatedAt = $updatedAt;
return $this;
@ -659,6 +764,12 @@ class ThirdParty
return $this;
}
public function setCreatedBy(User $user): TrackCreationInterface
{
$this->createdBy = $user;
return $this;
}
}

View File

@ -3,23 +3,26 @@
namespace Chill\ThirdPartyBundle\Form;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\PickCenterType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
use Chill\ThirdPartyBundle\Entity\ThirdPartyCivility;
use Chill\ThirdPartyBundle\Entity\ThirdPartyProfession;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Chill\ThirdPartyBundle\Security\Voter\ThirdPartyVoter;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Security\Core\Role\Role;
use Chill\ThirdPartyBundle\ThirdPartyType\ThirdPartyTypeManager;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
@ -58,58 +61,10 @@ class ThirdPartyType extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$types = [];
foreach ($this->typesManager->getProviders() as $key => $provider) {
$types['chill_3party.key_label.'.$key] = $key;
}
if (count($types) === 1) {
$builder
->add('types', HiddenType::class, [
'data' => array_values($types)
])
->get('types')
->addModelTransformer(new CallbackTransformer(
function (?array $typeArray): ?string {
if (null === $typeArray) {
return null;
}
return implode(',', $typeArray);
},
function (?string $typeStr): ?array {
if (null === $typeStr) {
return null;
}
return explode(',', $typeStr);
}
))
;
} else {
$builder->add('types', ChoiceType::class, [
'choices' => $types,
'expanded' => true,
'multiple' => true,
'label' => 'thirdparty.Type'
]);
}
$builder
->add('name', TextType::class, [
'required' => true
])
->add('categories', EntityType::class, [
'label' => 'thirdparty.Categories',
'class' => ThirdPartyCategory::class,
'choice_label' => function (ThirdPartyCategory $category): string {
return $this->translatableStringHelper->localize($category->getName());
},
'query_builder' => function (EntityRepository $er): QueryBuilder {
return $er->createQueryBuilder('c')
->where('c.active = true');
},
'required' => true,
'multiple' => true,
'attr' => ['class' => 'select2']
])
->add('telephone', TextType::class, [
'label' => 'Phonenumber',
'required' => false
@ -117,23 +72,16 @@ class ThirdPartyType extends AbstractType
->add('email', EmailType::class, [
'required' => false
])
->add('active', ChoiceType::class, [
'label' => 'thirdparty.Status',
'choices' => [
'Active, shown to users' => true,
'Inactive, not shown to users' => false
],
'expanded' => true,
'multiple' => false
])
->add('comment', ChillTextareaType::class, [
'required' => false
])
->add('centers', EntityType::class, [
'choices' => $this->getReachableCenters($options),
'class' => \Chill\MainBundle\Entity\Center::class,
'multiple' => true,
'attr' => ['class' => 'select2']
->add('centers', PickCenterType::class, [
'role' => (\array_key_exists('data', $options) && $this->om->contains($options['data'])) ?
ThirdPartyVoter::UPDATE : ThirdPartyVoter::CREATE,
'choice_options' => [
'multiple' => true,
'attr' => ['class' => 'select2']
]
])
;
@ -159,12 +107,12 @@ class ThirdPartyType extends AbstractType
;
// Contact Person ThirdParty (child)
if ($options['data']->isChild()) {
if (ThirdParty::KIND_CONTACT === $options['kind'] || ThirdParty::KIND_CHILD === $options['kind']) {
$builder
->add('civility', EntityType::class, [
'label' => 'thirdparty.Civility',
'class' => ThirdPartyCivility::class,
'choice_label' => function (ThirdPartyCivility $civility): string {
'class' => Civility::class,
'choice_label' => function (Civility $civility): string {
return $this->translatableStringHelper->localize($civility->getName());
},
'query_builder' => function (EntityRepository $er): QueryBuilder {
@ -172,7 +120,7 @@ class ThirdPartyType extends AbstractType
->where('c.active = true');
},
'placeholder' => 'thirdparty.choose civility',
'required' => true
'required' => false
])
->add('profession', EntityType::class, [
'label' => 'thirdparty.Profession',
@ -187,6 +135,10 @@ class ThirdPartyType extends AbstractType
'placeholder' => 'thirdparty.choose profession',
'required' => false
])
->add('contactDataAnonymous', CheckboxType::class, [
'required' => false,
'label' => 'thirdparty.Contact data are confidential'
])
;
// Institutional ThirdParty (parent)
@ -200,49 +152,99 @@ class ThirdPartyType extends AbstractType
'label' => 'thirdparty.Acronym',
'required' => false
])
->add('activeChildren', ChillCollectionType::class, [
'entry_type' => ThirdPartyType::class,
'entry_options' => [
'is_child' => true,
'block_name' => 'children',
'kind' => ThirdParty::KIND_CHILD,
],
'block_name' => 'active_children',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'button_add_label' => "Add a contact",
'button_remove_label' => "Remove a contact",
'empty_collection_explain' => "Any contact"
])
;
}
}
/**
*
* @param array $options
* @return \Chill\MainBundle\Entity\Center[]
*/
protected function getReachableCenters(array $options)
{
switch($options['usage']) {
case 'create': $role = new Role(ThirdPartyVoter::CREATE);
break;
case 'update': $role = new Role(ThirdPartyVoter::UPDATE);
break;
if (ThirdParty::KIND_CHILD !== $options['kind']) {
$builder
->add('categories', EntityType::class, [
'label' => 'thirdparty.Categories',
'class' => ThirdPartyCategory::class,
'choice_label' => function (ThirdPartyCategory $category): string {
return $this->translatableStringHelper->localize($category->getName());
},
'query_builder' => function (EntityRepository $er): QueryBuilder {
return $er->createQueryBuilder('c')
->where('c.active = true');
},
'required' => true,
'multiple' => true,
'attr' => ['class' => 'select2']
])
->add('active', ChoiceType::class, [
'label' => 'thirdparty.Status',
'choices' => [
'Active, shown to users' => true,
'Inactive, not shown to users' => false
],
'expanded' => true,
'multiple' => false
]);
// add the types
$types = [];
foreach ($this->typesManager->getProviders() as $key => $provider) {
$types['chill_3party.key_label.'.$key] = $key;
}
if (count($types) === 1) {
$builder
->add('types', HiddenType::class, [
'data' => array_values($types)
])
->get('types')
->addModelTransformer(new CallbackTransformer(
function (?array $typeArray): ?string {
if (null === $typeArray) {
return null;
}
return implode(',', $typeArray);
},
function (?string $typeStr): ?array {
if (null === $typeStr) {
return null;
}
return explode(',', $typeStr);
}
))
;
} else {
$builder
->add('types', ChoiceType::class, [
'choices' => $types,
'expanded' => true,
'multiple' => true,
'label' => 'thirdparty.Type'
]);
}
}
return $this->authorizationHelper->getReachableCenters(
$this->tokenStorage->getToken()->getUser(), $role);
}
/**
* {@inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Chill\ThirdPartyBundle\Entity\ThirdParty'
'data_class' => ThirdParty::class,
'is_child' => false,
'kind' => null
));
$resolver->setRequired('usage')
->setAllowedValues('usage', ['create', 'update'])
;
}
/**
* {@inheritdoc}
*/
public function getBlockPrefix()
{
return 'chill_thirdpartybundle_thirdparty';
}
}

View File

@ -37,13 +37,13 @@ class MenuBuilder implements LocalMenuBuilderInterface
* @var AuthorizationCheckerInterface
*/
protected $authorizationChecker;
/**
*
* @var TranslatorInterface
*/
protected $translator;
public function __construct(
AuthorizationCheckerInterface $authorizationChecker,
TranslatorInterface $translator
@ -52,15 +52,15 @@ class MenuBuilder implements LocalMenuBuilderInterface
$this->translator = $translator;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
if ($this->authorizationChecker->isGranted(ThirdPartyVoter::SHOW)) {
$menu
->addChild(
$this->translator->trans('Third parties'),
$this->translator->trans('Third parties'),
[
'route' => 'chill_3party_3party_index',
'route' => 'chill_crud_3party_3party_index',
])
->setExtras([
'order' => 112

View File

@ -2,22 +2,62 @@
namespace Chill\ThirdPartyBundle\Repository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
/**
* @Author Mathieu Jaumotte mathieu.jaumotte@champs-libres.coop
*/
class ThirdPartyACLAwareRepository implements ThirdPartyACLAwareRepositoryInterface
final class ThirdPartyACLAwareRepository implements ThirdPartyACLAwareRepositoryInterface
{
private Security $security;
private AuthorizationHelper $authorizationHelper;
private ThirdPartyRepository $thirdPartyRepository;
public function findByThirdparty(
ThirdParty $thirdparty,
string $role,
?array $orderBy = [],
int $limit = null,
int $offset = null
public function __construct(Security $security, AuthorizationHelper $authorizationHelper, ThirdPartyRepository $thirdPartyRepository)
{
$this->security = $security;
$this->authorizationHelper = $authorizationHelper;
$this->thirdPartyRepository = $thirdPartyRepository;
}
public function listThirdParties(
string $role,
?string $filterString,
?array $orderBy = [],
?int $limit = null,
?int $offset = null
): array {
$qb = $this->buildQuery($filterString);
// TODO: Implement findByThirdparty() method.
foreach ($orderBy as $sort => $direction) {
$qb->addOrderBy('tp.'.$sort, $direction);
}
$qb->setFirstResult($offset)
->setMaxResults($limit);
return $qb->getQuery()->getResult();
}
public function countThirdParties(
string $role,
?string $filterString
): int {
$qb = $this->buildQuery($filterString);
$qb->select('count(tp)');
return $qb->getQuery()->getSingleScalarResult();
}
public function buildQuery(?string $filterString = null): QueryBuilder
{
$qb = $this->thirdPartyRepository->createQueryBuilder('tp');
if (NULL !== $filterString) {
$qb->andWhere($qb->expr()->like('tp.canonicalized', 'LOWER(UNACCENT(:filterString))'))
->setParameter('filterString', '%'.$filterString.'%');
}
return $qb;
}
}

View File

@ -6,11 +6,20 @@ use Chill\ThirdPartyBundle\Entity\ThirdParty;
interface ThirdPartyACLAwareRepositoryInterface
{
public function findByThirdparty(
ThirdParty $thirdparty,
public function countThirdParties(string $role, ?string $filterString): int;
/**
* @param string $role
* @param array|null $orderBy
* @param int|null $limit
* @param int|null $offset
* @return array|ThirdParty[]
*/
public function listThirdParties(
string $role,
?string $filterString,
?array $orderBy = [],
int $limit = null,
int $offset = null
?int $limit = 0,
?int $offset = 50
): array;
}

View File

@ -2,23 +2,26 @@
namespace Chill\ThirdPartyBundle\Repository;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Query;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\Persistence\ObjectRepository;
class ThirdPartyRepository extends ServiceEntityRepository
final class ThirdPartyRepository implements ObjectRepository
{
public function __construct(ManagerRegistry $registry)
private EntityRepository $repository;
public function __construct(EntityManagerInterface $em)
{
parent::__construct($registry, ThirdParty::class);
$this->repository = $em->getRepository(ThirdParty::class);
}
/**
* count amongst parties associated to $centers, with $terms parameters
*
*
* @param array $centers
* @param type $terms
* @return int
@ -27,24 +30,24 @@ class ThirdPartyRepository extends ServiceEntityRepository
{
$qb = $this->buildQuery($centers, $terms);
$qb->select('COUNT(tp)');
return $qb->getQuery()->getSingleScalarResult();
}
/**
* Search amongst parties associated to $centers, with $terms parameters
*
*
* Different format for return:
* - ['entity']: return the entity hydrated as objects
* - ['array', [ DQL ]: return objects hydrated as entity, with
* - ['array', [ DQL ]: return objects hydrated as entity, with
* an array describing the fields as DQL.
*
*
* supported terms:
*
*
* - name or _default: containing the name (name LIKE %string%)
* - is_active: is active = true / false
* - types: an array of types
*
*
* @param array $centers
* @param int $firstResult
* @param int $maxResults
@ -74,43 +77,43 @@ class ThirdPartyRepository extends ServiceEntityRepository
break;
default:
throw new \DomainException("This return format is invalid");
}
}
$qb->setFirstResult($firstResult)
->setMaxResults($maxResults);
return $qb->getQuery()->getResult();
}
protected function createMemberOfCentersQuery($centers): QueryBuilder
{
$qb = $this->createQueryBuilder('tp');
$or = $qb->expr()->orX();
foreach ($centers as $center) {
$or->add($qb->expr()->isMemberOf(':center_'.$center->getId(), 'tp.centers'));
$qb->setParameter('center_'.$center->getId(), $center);
}
$qb->where($or);
return $qb;
}
protected function buildQuery($centers, $terms): QueryBuilder
{
$qb = $this->createMemberOfCentersQuery($centers);
$this->setNameCondition($qb, $terms);
$this->setTypesCondition($qb, $terms);
$this->setIsActiveCondition($qb, $terms);
return $qb;
}
/**
* Add parameters to filter by containing $terms["name"] or
* Add parameters to filter by containing $terms["name"] or
* $terms["_default"]
*
*
* @param QueryBuilder $qb
* @param array $terms
*/
@ -125,7 +128,7 @@ class ThirdPartyRepository extends ServiceEntityRepository
$qb->setParameter('name', '%'.$term.'%');
}
}
protected function setTypesCondition(QueryBuilder $qb, array $terms)
{
if (\array_key_exists('types', $terms)) {
@ -137,14 +140,55 @@ class ThirdPartyRepository extends ServiceEntityRepository
$qb->andWhere($orx);
}
}
protected function setIsActiveCondition(QueryBuilder $qb, array $terms)
{
if (\array_key_exists('is_active', $terms)) {
$qb->andWhere(
$terms['is_active'] ? $qb->expr()->eq('tp.active', "'TRUE'") :
$terms['is_active'] ? $qb->expr()->eq('tp.active', "'TRUE'") :
$qb->expr()->eq('tp.active', "'FALSE'")
);
}
}
public function find($id): ?ThirdParty
{
return $this->repository->find($id);
}
/**
* @return array|ThirdParty[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @param array $criteria
* @param array|null $orderBy
* @param null $limit
* @param null $offset
* @return array|ThirdParty[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?ThirdParty
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return ThirdParty::class;
}
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
}

View File

@ -20,6 +20,20 @@
</div>
<div v-else-if="action === 'edit' || action === 'create'">
<div class="form-floating mb-3">
<div class="form-check">
<input class="form-check-input mt-0" type="radio" v-model="kind" value="company" id="tpartyKindInstitution">
<label for="tpartyKindInstitution" class="required">
{{ $t('tparty.company')}}
</label>
</div>
<div class="form-check">
<input class="form-check-input mt-0" type="radio" v-model="kind" value="contact" id="tpartyKindContact">
<label for="tpartyKindContact" class="required">
{{ $t('tparty.contact')}}
</label>
</div>
</div>
<div class="form-floating mb-3">
<input class="form-control form-control-lg" id="name" v-model="thirdparty.text" v-bind:placeholder="$t('thirdparty.name')" />
<label for="name">{{ $t('thirdparty.name') }}</label>
@ -59,6 +73,17 @@ import ThirdPartyRenderBox from '../Entity/ThirdPartyRenderBox.vue';
import AddAddress from 'ChillMainAssets/vuejs/Address/components/AddAddress';
import { getThirdparty } from '../../_api/OnTheFly';
const i18n = {
messages: {
fr: {
tparty: {
contact: "Contact",
company: "Institution"
}
}
}
};
export default {
name: "OnTheFlyThirdParty",
props: ['id', 'type', 'action'],
@ -66,11 +91,12 @@ export default {
ThirdPartyRenderBox,
AddAddress
},
i18n,
data() {
return {
//context: {}, <--
thirdparty: {
type: 'thirdparty'
type: 'thirdparty',
},
addAddress: {
options: {
@ -88,6 +114,19 @@ export default {
}
},
computed: {
kind: {
get() {
// note: there are also default to 'institution' set in the "mounted" method
if (this.$data.thirdparty.kind !== undefined) {
return this.$data.thirdparty.kind;
} else {
return 'company';
}
},
set(v) {
this.$data.thirdparty.kind = v;
}
},
context() {
let context = {
target: {
@ -133,6 +172,8 @@ export default {
mounted() {
if (this.action !== 'create') {
this.loadData();
} else {
this.thirdparty.kind = 'company';
}
},
}

View File

@ -20,7 +20,7 @@
#}
{% macro raw(thirdparty, options) %}
<span class="name">{{ thirdparty.name }}</span>
<span class="name">{{ thirdparty|chill_entity_render_string }}</span>
{% endmacro raw %}
{% macro label(thirdparty, options) %}
@ -29,7 +29,7 @@
<div class="denomination {{ 'h' ~ options['hLevel'] }}">
{%- if options['addLink'] and is_granted('CHILL_3PARTY_3PARTY_SHOW', thirdparty) -%}
<a href="{{ chill_path_add_return_path('chill_3party_3party_show', { 'thirdparty_id': thirdparty.id }) }}">
<a href="{{ chill_path_add_return_path('chill_crud_3party_3party_view', { 'id': thirdparty.isChild ? thirdparty.parent.id : thirdparty.id }) }}">
{%- endif -%}
{% if options['customArea']['beforeLabel'] is defined %}
@ -41,7 +41,7 @@
{% if options['customArea']['afterLabel'] is defined %}
{{ options['customArea']['afterLabel'] }}
{% endif %}
{%- if options['addLink'] and is_granted('CHILL_3PARTY_3PARTY_SHOW', thirdparty) -%}
</a>
{%- endif -%}
@ -81,6 +81,13 @@
<div class="item-row entity-bloc">
<div class="item-col">
{{ _self.label(thirdparty, options) }}
{% if thirdparty.kind == 'company' %}
<span class="badge bg-info">{{ 'thirdparty.company'|trans }}</span>
{% elseif thirdparty.kind == 'child' %}
<span class="badge bg-chill-red">{{ 'thirdparty.Child'|trans }}</span>
{% elseif thirdparty.kind == 'contact' %}
<span class="badge bg-secondary">{{ 'thirdparty.contact'|trans }}</span>
{% endif %}
</div>
<div class="item-col">
<ul class="list-content fa-ul">
@ -110,10 +117,10 @@
{% if options['customButtons']['replace'] is defined %}
{{ options['customButtons']['replace'] }}
{% elseif is_granted('CHILL_3PARTY_3PARTY_SHOW', thirdparty) %}
{% elseif is_granted('CHILL_3PARTY_3PARTY_SHOW', thirdparty) and options['addLink'] %}
<li>
<a class="btn btn-sm btn-show" target="_blank" title="{{ 'Show thirdparty'|trans }}"
href="{{ path('chill_3party_3party_show', { thirdparty_id: thirdparty.id }) }}"></a>
href="{{ path('chill_crud_3party_3party_view', { id: thirdparty.isChild ? thirdparty.parent.id : thirdparty.id }) }}"></a>
</li>
{% else %}
{% endif %}
@ -124,4 +131,17 @@
</ul>
</div>
</div>
{% if options['showContacts'] and thirdparty.activeChildren|length > 0 %}
<div class="item-row">
{{ 'thirdparty.Children'|trans }}&nbsp;:
{% for c in thirdparty.activeChildren %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'thirdparty', id: c.id },
action: 'show',
displayBadge: true,
buttonText: c|chill_entity_render_string
} %}
{% endfor %}
</div>
{% endif %}
{%- endif -%}

View File

@ -0,0 +1,63 @@
{% if form.civility is defined %}
{{ form_row(form.civility) }}
{% endif %}
{{ form_row(form.name) }}
{% if form.nameCompany is defined %}
{{ form_row(form.nameCompany) }}
{{ form_row(form.acronym) }}
{% endif %}
{% if form.profession is defined %}
{{ form_row(form.profession) }}
{% endif %}
{{ form_row(form.types) }}
{{ form_row(form.categories) }}
{{ form_row(form.telephone) }}
{{ form_row(form.email) }}
{% if form.contactDataAnonymous is defined %}
{{ form_row(form.contactDataAnonymous) }}
{% endif %}
{% if form.activeChildren is defined %}
<h2>{{ 'Contacts'|trans }}</h2>
{{ form_widget(form.activeChildren) }}
{% endif %}
<div class="mb-3 row">
{{ form_label(form.address) }}
{{ form_widget(form.address) }}
<div class="col-sm-8">
{% if thirdParty.address %}
{# include vue_address component #}
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'thirdparty', id: thirdParty.id },
mode: 'edit',
addressId: thirdParty.address.id,
buttonSize: 'btn-sm',
} %}
{#
backUrl: path('chill_3party_3party_new'),
#}
{% else %}
{# include vue_address component #}
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'thirdparty', id: thirdParty.id },
mode: 'new',
buttonSize: 'btn-sm',
buttonText: 'Create a new address',
modalTitle: 'Create a new address',
} %}
{% endif %}
</div>
</div>
{{ form_row(form.comment) }}
{{ form_row(form.centers) }}
{{ form_row(form.active) }}

View File

@ -0,0 +1,46 @@
{% block _third_party_active_children_entry_widget %}
<div class="container">
<div class="row">
<div class="form-group col-md-3 mb-3">
{{ form_widget(form.civility) }}
{{ form_errors(form.civility) }}
{{ form_label(form.civility) }}
</div>
<div class="form-group col-md-5 mb-3">
{{ form_widget(form.name) }}
{{ form_errors(form.name) }}
{{ form_label(form.name) }}
</div>
<div class="form-group col-md-4 mb-3">
{{ form_widget(form.profession) }}
{{ form_errors(form.profession) }}
{{ form_label(form.profession) }}
</div>
</div>
<div class="row">
<div class="form-group col-md-5 mb-3">
{{ form_widget(form.telephone) }}
{{ form_errors(form.telephone) }}
{{ form_label(form.telephone) }}
</div>
<div class="form-group col-md-5 mb-3">
{{ form_widget(form.email) }}
{{ form_errors(form.email) }}
{{ form_label(form.email) }}
</div>
<div class="form-group col-md-2 mb-3">
{{ form_widget(form.contactDataAnonymous) }}
{{ form_label(form.contactDataAnonymous) }}
{{ form_errors(form.contactDataAnonymous) }}
</div>
</div>
<div class="row">
<div class="form-group col-md-12 mb-3">
{{ form_widget(form.comment) }}
{{ form_errors(form.comment) }}
</div>
</div>
</div>
{% endblock %}

View File

@ -2,108 +2,59 @@
{% block title 'List of third parties'|trans %}
{% set third_parties = entities %}
{% block content %}
<div class="thirdparty-list my-5">
<div class="row justify-content-center">
<div class="col-md-10 col-xxl">
{% embed '@ChillMain/CRUD/_index.html.twig' %}
{% block index_header %}
<h1>{{ 'List of third parties'|trans }}</h1>
{% endblock %}
<h1>{{ 'List of third parties'|trans }}</h1>
{% block table_entities %}
<div class="thirdparty-list my-5">
<div class="row justify-content-center">
<div>
{% if third_parties|length == 0 %}
<p class="chill-no-data-statement">{{ 'No third parties'|trans }}</p>
{% else %}
<label class="counter">
<span>{{ paginator.totalItems }}</span> {{ 'third parties'|trans }}
</label>
<nav class="filter-actions border border-secondary my-4 p-3">
<i>outils de filtrage</i>
</nav>
<div class="flex-table">
{% for tp in third_parties %}
<div class="item-bloc">
{{ tp|chill_entity_render_box({'render': 'bloc', 'addLink': false, 'showContacts': true }) }}
<div class="item-row separator">
<ul class="record_actions">
{% if is_granted('CHILL_3PARTY_3PARTY_UPDATE', tp) %}
<li>
<a href="{{ chill_path_add_return_path('chill_crud_3party_3party_edit',
{ 'id': (tp.isParent ? tp.id : tp.parent.id) }) }}" class="btn btn-sm btn-update"></a>
</li>
{% endif %}
{% if is_granted('CHILL_3PARTY_3PARTY_SHOW', tp) %}
<li>
<a href="{{ chill_path_add_return_path('chill_crud_3party_3party_view',
{ 'id': (tp.isParent ? tp.id : tp.parent.id) }) }}" class="btn btn-sm btn-show"></a>
</li>
{% endif %}
</ul>
</div>
</div>
<div class="row justify-content-center">
<div class="col-md-10 col-xxl">
<label class="counter">
<span>{{ pagination.totalItems }}</span> {{ 'third parties'|trans }}
</label>
<table class="table table-bordered border-dark table-striped align-middle">
<thead>
<tr>
<th class="chill-pink" style="width: 35px;"></th>
<th class="chill-pink">{{ 'Name'|trans }}
<i class="fa fa-fw fa-sort"></i>
</th>
<th class="chill-pink">{{ 'Category'|trans }}
<i class="fa fa-fw fa-sort"></i>
</th>
<th class="chill-pink">{{ 'Address'|trans }}
<i class="fa fa-fw fa-sort"></i>
</th>
<th class="chill-pink">{{ 'thirdparty.UpdatedAt.short'|trans }}
<i class="fa fa-fw fa-sort"></i>
</th>
<th class="chill-pink"></th>
</tr>
</thead>
<tbody>
{% for tp in third_parties %}
<tr>
<th>{{ (tp.active ? '<i class="fa fa-check chill-green">' : '<i class="fa fa-times chill-red">')|raw }}</th>
<td>{{ tp.name }}</td>
{% set types = [] %}
{% for t in tp.types %}
{% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %}
</div>
</div>
{% endfor %}
<td>{{ types|join(', ') }}</td>
<td>
{{ tp.address|chill_entity_render_box({'multiline': false, 'with_valid_from': false}) }}
</td>
<td>
{% if tp.updatedAt != null %}
{{ tp.updatedAt|format_date('short') }}
{% else %}
{{ tp.createdAt|format_date('short') }}
{% endif %}
</td>
<td>
<ul class="record_actions">
{% if is_granted('CHILL_3PARTY_3PARTY_UPDATE', tp) %}
<li>
<a href="{{ chill_path_add_return_path('chill_3party_3party_update', { 'thirdparty_id': tp.id }) }}" class="btn btn-sm btn-update"></a>
</li>
{% endif %}
{% if is_granted('CHILL_3PARTY_3PARTY_SHOW', tp) %}
<li>
<a href="{{ chill_path_add_return_path('chill_3party_3party_show', { 'thirdparty_id': tp.id }) }}" class="btn btn-sm btn-show"></a>
</li>
{% endif %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if third_parties|length < pagination.getTotalItems %}
{{ chill_pagination(pagination, 'long') }}
{% endif %}
{% endif %}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
{{ chill_items_per_page(pagination) }}
</li>
{% if is_granted('CHILL_3PARTY_3PARTY_CREATE') %}
<li>
<a href="{{ chill_path_add_return_path('chill_3party_3party_new') }}" class="btn btn-create">
{{ "New third party"|trans }}
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block actions_before %}
<li class="cancel">
{{ chill_items_per_page(paginator) }}
</li>
{% endblock %}
{% endembed %}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('page_3party_3party_index') }}
{% endblock %}

View File

@ -1,87 +1,21 @@
{% extends "@ChillMain/layout.html.twig" %}
{% set thirdParty = entity %}
{% form_theme form '@ChillThirdParty/ThirdParty/_form_thirdparty_children.html.twig' %}
{% block title 'Create third party'|trans %}
{% block content %}
<div class="thirdparty-new my-5">
<div class="row justify-content-center">
<div class="col-md-10 col-xxl">
<h1>{{ 'Create third party'|trans }}</h1>
{{ form_start(form) }}
{% if form.civility is defined %}
{{ form_row(form.civility) }}
{% endif %}
{{ form_row(form.name) }}
{% if form.nameCompany is defined %}
{{ form_row(form.nameCompany) }}
{{ form_row(form.acronym) }}
{% endif %}
{% if form.profession is defined %}
{{ form_row(form.profession) }}
{% endif %}
{{ form_row(form.types) }}
{{ form_row(form.categories) }}
{{ form_row(form.telephone) }}
{{ form_row(form.email) }}
<div class="mb-3 row">
{{ form_label(form.address) }}
{{ form_widget(form.address) }}
<div class="col-sm-8">
{% if thirdParty.address %}
{# include vue_address component #}
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'thirdparty', id: thirdParty.id },
mode: 'edit',
addressId: thirdParty.address.id,
buttonSize: 'btn-sm',
} %}
{#
backUrl: path('chill_3party_3party_new'),
#}
{% else %}
{# include vue_address component #}
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'thirdparty', id: thirdParty.id },
mode: 'new',
buttonSize: 'btn-sm',
buttonText: 'Create a new address',
modalTitle: 'Create a new address',
} %}
{% endif %}
</div>
</div>
{{ form_row(form.comment) }}
{{ form_row(form.centers) }}
{{ form_row(form.active) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_3party_3party_index') }}" class="btn btn-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
</li>
<li>
{{ form_widget(form.submit, {'label': 'Create', 'attr': {'class': 'btn btn-new' }}) }}
</li>
</ul>
{{ form_end(form) }}
</div>
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block crud_content_header %}
<h1>{{ 'Create third party'|trans }}</h1>
{% endblock %}
{% block crud_content_form_rows %}
{% include '@ChillThirdParty/ThirdParty/_form.html.twig' %}
{% endblock %}
{% endembed %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title 'thirdparty.Which kind of third party ?'|trans %}
{% block content %}
<div class="col-10 centered">
<h1>{{ block('title') }}</h1>
<div class="container" style="margin-top: 2rem;">
<div class="row">
<div class="col-md-4">
<a
href="{{ chill_path_forward_return_path('chill_crud_3party_3party_new', {'kind': 'company'}) }}"
class="btn btn-outline-chill-green-dark">
{{ 'thirdparty.A company'|trans }}
</a>
</div>
<div class="col-md-8">
<p>{{ 'thirdparty.a_company_explanation'|trans }}</p>
</div>
</div>
<div class="row">
<div class="col-md-4">
<a
href="{{ chill_path_forward_return_path('chill_crud_3party_3party_new', {'kind': 'contact'}) }}"
class="btn btn-outline-chill-green-dark">
{{ 'thirdparty.A contact'|trans }}
</a>
</div>
<div class="col-md-8">
<p>{{ 'thirdparty.a_contact_explanation'|trans }}</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,122 +0,0 @@
{% extends "@ChillMain/layout.html.twig" %}
{% set title_ = 'Show third party %name%'|trans({'%name%' : thirdParty.name }) %}
{% block title title_ %}
{% block content %}
<div class="thirdparty-show my-5">
<div class="row justify-content-center">
<div class="col-md-10 col-xxl">
<h1>
{{ title_ }}
<span class="badge bg-{{ thirdParty.active ? 'success' : 'danger' }}"
title="{{ (thirdParty.active ? 'shown to users' : 'not shown to users')|trans }}">
{{ (thirdParty.active ? 'Active' : 'Inactive')|trans }}
</span>
</h1>
<dl class="chill_view_data">
<dt>{{ 'Name'|trans }}</dt>
<dd>
{% if thirdParty.isLeaf == true %}{{ thirdParty.civility }}{% endif %}
{{ thirdParty.name }}
</dd>
{% if thirdParty.isLeaf == false %}
<dt>{{ 'thirdparty.NameCompany'|trans }}</dt>
<dd>
{% if thirdParty.nameCompany == null %}
<span class="chill-no-data-statement">{{ 'No nameCompany given'|trans }}</span>
{% else %}
{{ thirdParty.nameCompany }}
{% endif %}
</dd>
<dt>{{ 'thirdparty.Acronym'|trans }}</dt>
<dd>
{% if thirdParty.acronym == null %}
<span class="chill-no-data-statement">{{ 'No acronym given'|trans }}</span>
{% else %}
{{ thirdParty.acronym }}
{% endif %}
</dd>
{% endif %}
<dt>{{ 'Type'|trans }}</dt>
{% set types = [] %}
{% for t in thirdParty.types %}
{% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %}
{% endfor %}
<dd>
{{ types|join(', ') }}
</dd>
<dt>{{ 'Phonenumber'|trans }}</dt>
<dd>
{% if thirdParty.telephone == null %}
<span class="chill-no-data-statement">{{ 'No phone given'|trans }}</span>
{% else %}
<a href="{{ 'tel:' ~ thirdParty.telephone }}">
{{ thirdParty.telephone|chill_print_or_message("thirdparty.No_phonenumber") }}
</a>
{% endif %}
</dd>
<dt>{{ 'email'|trans }}<dt>
<dd>
{% if thirdParty.email == null %}
<span class="chill-no-data-statement">{{ 'No email given'|trans }}</span>
{% else %}
<a href="{{ 'mailto:' ~ thirdParty.email }}">
{{ thirdParty.email|chill_print_or_message("thirdparty.No_email") }}
</a>
{% endif %}
</dd>
<dt>{{ 'Address'|trans }}</dt>
<dd>
{% if thirdParty.address == null %}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{% else %}
{{ thirdParty.address|chill_entity_render_box({'with_valid_from': false, 'extended_infos': true }) }}
{% endif %}
</dd>
<dt>{{ 'Comment'|trans }}</dt>
<dd>
{% if thirdParty.comment is not empty %}
<blockquote class="chill-user-quote">
{{ thirdParty.comment|chill_markdown_to_html }}
</blockquote>
{% endif %}
</dd>
<dt>{{ 'Centers'|trans }}</dt>
<dd>{{ 'The party is visible in those centers'|trans }}&nbsp;: {{ thirdParty.centers|join(', ') }}</dd>
</dl>
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_3party_3party_index') }}">
{{ 'Back to the list'|trans }}
</a>
</li>
{% if is_granted('CHILL_3PARTY_3PARTY_UPDATE', thirdParty) %}
<li>
<a class="btn btn-update" href="{{ chill_path_forward_return_path('chill_3party_3party_update', { 'thirdparty_id': thirdParty.id }) }}">
{{ 'Update'|trans }}
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,30 +1,48 @@
{% extends "@ChillMain/layout.html.twig" %}
{% set thirdParty = entity %}
{% form_theme form '@ChillThirdParty/ThirdParty/_form_thirdparty_children.html.twig' %}
{% block title 'Update third party %name%'|trans({ '%name%': thirdParty.name }) %}
{% block content %}
<div class="thirdparty-new my-5">
<div class="row justify-content-center">
{% embed '@ChillMain/CRUD/_edit_content.html.twig' %}
{% block crud_content_header %}
<h1>
{{ 'Update third party %name%'|trans({ '%name%': thirdParty.name }) }}
<span class="badge bg-{{ thirdParty.active ? 'success' : 'danger' }}"
title="{{ (thirdParty.active ? 'shown to users' : 'not shown to users')|trans }}">
{{ (thirdParty.active ? 'Active' : 'Inactive')|trans }}
</span>
</h1>
{% endblock %}
{% block crud_content_form_rows %}
<div class="date-by">
{% if thirdParty.updatedAt != null %}
{{ 'thirdparty.UpdatedAt.short'|trans ~ thirdParty.updatedAt|format_date('short') }}
{% else %}
{{ 'thirdparty.CreatedAt.short'|trans ~ thirdParty.createdAt|format_date('short') }}
{% endif %}
{% if thirdParty.updatedBy != null %}
{{ 'thirdparty.UpdateBy.short'|trans ~ thirdParty.updatedBy.usernameCanonical }}
{% endif %}
</div>
{% include '@ChillThirdParty/ThirdParty/_form.html.twig' %}
{% endblock %}
{% endembed %}
</div>
</div>
{% endblock %}
{% block content_not %}
<div class="thirdparty-edit my-5">
<div class="row justify-content-center">
<div class="col-md-10">
<h1>
{{ 'Update third party %name%'|trans({ '%name%': thirdParty.name }) }}
<span class="badge bg-{{ thirdParty.active ? 'success' : 'danger' }}"
title="{{ (thirdParty.active ? 'shown to users' : 'not shown to users')|trans }}">
{{ (thirdParty.active ? 'Active' : 'Inactive')|trans }}
</span>
</h1>
<div class="date-by">
{% if thirdParty.updatedAt != null %}
{{ 'thirdparty.UpdatedAt.short'|trans ~ thirdParty.updatedAt|format_date('short') }}
{% else %}
{{ 'thirdparty.CreatedAt.short'|trans ~ thirdParty.createdAt|format_date('short') }}
{% endif %}
{% if thirdParty.updatedBy != null %}
{{ 'thirdparty.UpdateBy.short'|trans ~ thirdParty.updatedBy.usernameCanonical }}
{% endif %}
</div>
{{ form_start(form) }}

View File

@ -0,0 +1,139 @@
{% extends "@ChillMain/layout.html.twig" %}
{% set thirdParty = entity %}
{% set title_ = 'Show third party %name%'|trans({'%name%' : thirdParty.name }) %}
{% block title title_ %}
{% block content %}
<div class="thirdparty-show my-5">
<div class="row justify-content-center">
{% embed '@ChillMain/CRUD/_view_content.html.twig' %}
{% block crud_content_header %}
<h1>
{{ title_ }}
<span class="badge bg-{{ thirdParty.active ? 'success' : 'danger' }}"
title="{{ (thirdParty.active ? 'shown to users' : 'not shown to users')|trans }}">
{{ (thirdParty.active ? 'Active' : 'Inactive')|trans }}
</span>
</h1>
{% endblock %}
{% block crud_content_view_details %}
<dl class="chill_view_data">
<dt>{{ 'Name'|trans }}</dt>
<dd>
{% if thirdParty.isLeaf == true %}{{ thirdParty.civility }}{% endif %}
{{ thirdParty.name }}
</dd>
{% if thirdParty.kind == 'company' %}
<dt>{{ 'thirdparty.NameCompany'|trans }}</dt>
<dd>
{% if thirdParty.nameCompany == null %}
<span class="chill-no-data-statement">{{ 'No nameCompany given'|trans }}</span>
{% else %}
{{ thirdParty.nameCompany }}
{% endif %}
</dd>
<dt>{{ 'thirdparty.Acronym'|trans }}</dt>
<dd>
{% if thirdParty.acronym == null %}
<span class="chill-no-data-statement">{{ 'No acronym given'|trans }}</span>
{% else %}
{{ thirdParty.acronym }}
{% endif %}
</dd>
{% endif %}
<dt>{{ 'Type'|trans }}</dt>
{% set types = [] %}
{% for t in thirdParty.types %}
{% set types = types|merge( [ ('chill_3party.key_label.'~t)|trans ] ) %}
{% endfor %}
<dd>
{{ types|join(', ') }}
</dd>
<dt>{{ 'Phonenumber'|trans }}</dt>
<dd>
{% if thirdParty.telephone == null %}
<span class="chill-no-data-statement">{{ 'No phone given'|trans }}</span>
{% else %}
<a href="{{ 'tel:' ~ thirdParty.telephone }}">
{{ thirdParty.telephone|chill_print_or_message("thirdparty.No_phonenumber") }}
</a>
{% endif %}
</dd>
<dt>{{ 'email'|trans }}<dt>
<dd>
{% if thirdParty.email == null %}
<span class="chill-no-data-statement">{{ 'No email given'|trans }}</span>
{% else %}
<a href="{{ 'mailto:' ~ thirdParty.email }}">
{{ thirdParty.email|chill_print_or_message("thirdparty.No_email") }}
</a>
{% endif %}
</dd>
<dt>{{ 'Address'|trans }}</dt>
<dd>
{% if thirdParty.address == null %}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{% else %}
{{ thirdParty.address|chill_entity_render_box({'with_valid_from': false, 'extended_infos': true }) }}
{% endif %}
</dd>
<dt>{{ 'Comment'|trans }}</dt>
<dd>
{% if thirdParty.comment is not empty %}
<blockquote class="chill-user-quote">
{{ thirdParty.comment|chill_markdown_to_html }}
</blockquote>
{% else %}
<span class="chill-no-data-statement">{{ 'Any comment'|trans }}</span>
{% endif %}
</dd>
{% if thirdParty.kind == 'company' %}
<dt>{{ 'Contacts'|trans }}</dt>
<dd>
{% if thirdParty.activeChildren|length == 0 %}
<p class="chill-no-data-statement">{{ 'Any contacts associated'|trans }}</p>
{% else %}
<div class="flex-table">
{% for tp in thirdParty.activeChildren %}
<div class="item-bloc">
{{ tp|chill_entity_render_box({'render': 'bloc', 'addLink': false}) }}
</div>
{% endfor %}
</div>
{% endif %}
</dd>
{% endif %}
<dt>{{ 'Centers'|trans }}</dt>
<dd>
{% set centers = thirdParty|chill_resolve_center %}
{% if centers is iterable %}
{{ 'The party is visible in those centers'|trans }}&nbsp;:
{{ centers|join(', ') }}
{% elseif centers is null %}
{{ 'The party is not visible in any center'|trans }}
{% else %}
{{ 'The party is visible in those centers'|trans }}&nbsp;: {{ centers }}
{% endif %}
</dd>
</dl>
{% endblock %}
{% block content_form_actions_delete %}{% endblock %}
{% block content_view_actions_duplicate_link %}{% endblock %}
{% endembed %}
</div>
</div>
{% endblock %}

View File

@ -20,9 +20,14 @@ class ThirdPartyApiSearch implements SearchApiInterface
return (new SearchApiQuery)
->setSelectKey('tparty')
->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)")
->setSelectPertinence("SIMILARITY(?, LOWER(UNACCENT(tparty.name)))", [ $pattern ])
->setSelectPertinence("GREATEST(".
"STRICT_WORD_SIMILARITY(LOWER(UNACCENT(?)), tparty.canonicalized),".
"(tparty.canonicalized LIKE '%' || LOWER(UNACCENT(?)) || '%')::int".
")", [ $pattern, $pattern ])
->setFromClause('chill_3party.third_party AS tparty')
->setWhereClause('SIMILARITY(LOWER(UNACCENT(?)), LOWER(UNACCENT(tparty.name))) > 0.20', [ $pattern ])
->setWhereClause("tparty.active IS TRUE ".
"AND (LOWER(UNACCENT(?)) <<% tparty.canonicalized OR ".
"tparty.canonicalized LIKE '%' || LOWER(UNACCENT(?)) || '%')", [ $pattern, $pattern ])
;
}
@ -33,7 +38,7 @@ class ThirdPartyApiSearch implements SearchApiInterface
public function prepare(array $metadatas): void
{
}
public function supportsResult(string $key, array $metadatas): bool

View File

@ -3,6 +3,7 @@
namespace Chill\ThirdPartyBundle\Serializer\Normalizer;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@ -12,17 +13,27 @@ class ThirdPartyNormalizer implements NormalizerInterface, NormalizerAwareInterf
{
use NormalizerAwareTrait;
private ThirdPartyRender $thirdPartyRender;
public function __construct(ThirdPartyRender $thirdPartyRender)
{
$this->thirdPartyRender = $thirdPartyRender;
}
public function normalize($thirdParty, string $format = null, array $context = [])
{
/** @var $thirdParty ThirdParty */
$data['type'] = 'thirdparty';
// TODO should be replaced by a "render entity"
$data['text'] = $thirdParty->getName();
$data['text'] = $this->thirdPartyRender->renderString($thirdParty, []);
$data['id'] = $thirdParty->getId();
$data['kind'] = $thirdParty->getKind();
$data['address'] = $this->normalizer->normalize($thirdParty->getAddress(), $format,
[ 'address_rendering' => 'short' ]);
$data['phonenumber'] = $thirdParty->getTelephone();
$data['email'] = $thirdParty->getEmail();
$data['isChild'] = $thirdParty->isChild();
$data['parent'] = $this->normalizer->normalize($thirdParty->getParent(), $format, $context);
$data['civility'] = $this->normalizer->normalize($thirdParty->getCivility(), $format, $context);
return $data;
}

View File

@ -21,6 +21,7 @@
namespace Chill\ThirdPartyBundle\Templating\Entity;
use Chill\MainBundle\Templating\Entity\AbstractChillEntityRender;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Component\Templating\EngineInterface;
@ -32,10 +33,15 @@ class ThirdPartyRender extends AbstractChillEntityRender
{
protected EngineInterface $engine;
protected TranslatableStringHelper $translatableStringHelper;
public function __construct(EngineInterface $engine)
public function __construct(
EngineInterface $engine,
TranslatableStringHelper $translatableStringHelper
)
{
$this->engine = $engine;
$this->translatableStringHelper = $translatableStringHelper;
}
/**
@ -55,6 +61,7 @@ class ThirdPartyRender extends AbstractChillEntityRender
'hLevel' => $options['hLevel'] ?? 3,
'customButtons' => $options['customButtons'] ?? [],
'customArea' => $options['customArea'] ?? [],
'showContacts' => $options['showContacts'] ?? [],
];
return
@ -75,7 +82,18 @@ class ThirdPartyRender extends AbstractChillEntityRender
*/
public function renderString($entity, array $options): string
{
return $entity->getName();
if ($entity->getCivility() !== NULL) {
$civility = $this->translatableStringHelper
->localize($entity->getCivility()->getAbbreviation()).' ';
} else {
$civility = '';
}
if (!empty($entity->getAcronym())) {
$acronym = ' ('.$entity->getAcronym().')';
} else {
$acronym = '';
}
return $civility.$entity->getName().$acronym;
}
public function supports($entity, array $options): bool

View File

@ -1,9 +1,12 @@
module.exports = function(encore, entries)
{
entries.push(__dirname + '/Resources/public/chill/index.js');
// Aliases are used when webpack is trying to resolve modules path
encore.addAliases({
ChillThirdPartyAssets: __dirname + '/Resources/public'
});
encore.addEntry(
'page_3party_3party_index',
__dirname + '/Resources/public/page/index/index.js'
);
};

View File

@ -6,8 +6,3 @@ services:
tags:
- { name: 'serializer.normalizer', priority: 64 }
Chill\ThirdPartyBundle\Repository\:
autowire: true
resource: '../Repository/'
tags:
- { name: 'doctrine.repository_service' }

View File

@ -1,7 +1,5 @@
services:
Chill\ThirdPartyBundle\Controller\ThirdPartyController:
arguments:
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
$paginatorFactory: '@Chill\MainBundle\Pagination\PaginatorFactory'
tags: ['controller.service_arguments']
Chill\ThirdPartyBundle\Controller\:
resource: './../Controller'
autowire: true
autoconfigure: true

View File

@ -4,10 +4,6 @@ services:
tags:
- { 'name': doctrine.fixture.orm }
Chill\ThirdPartyBundle\DataFixtures\ORM\LoadThirdPartyCivility:
tags:
- { 'name': doctrine.fixture.orm }
Chill\ThirdPartyBundle\DataFixtures\ORM\LoadThirdPartyCategory:
tags:
- { 'name': doctrine.fixture.orm }

View File

@ -0,0 +1,8 @@
---
services:
Chill\ThirdPartyBundle\Repository\:
autowire: true
autoconfigure: true
resource: '../Repository/'
Chill\ThirdPartyBundle\Repository\ThirdPartyACLAwareRepositoryInterface: '@Chill\ThirdPartyBundle\Repository\ThirdPartyACLAwareRepository'

View File

@ -1,6 +1,6 @@
services:
Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender:
arguments:
$engine: '@Symfony\Component\Templating\EngineInterface'
autowire: true
autoconfigure: true
tags:
- 'chill.render_entity'

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\ThirdParty;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add created_by to third party
*/
final class Version20211006200924 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add link to creator (created_by) to thirdParty';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_3party.third_party ADD created_by INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_3party.third_party ALTER created_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
$this->addSql('ALTER TABLE chill_3party.third_party ALTER created_at DROP DEFAULT');
$this->addSql('ALTER TABLE chill_3party.third_party ALTER updated_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE');
$this->addSql('ALTER TABLE chill_3party.third_party ALTER updated_at DROP DEFAULT');
$this->addSql('COMMENT ON COLUMN chill_3party.third_party.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_3party.third_party.updated_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_3party.third_party ADD CONSTRAINT FK_D952467BDE12AB56 FOREIGN KEY (created_by) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_D952467BDE12AB56 ON chill_3party.third_party (created_by)');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_3party.third_party DROP created_by');
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\ThirdParty;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* migrate data from 3party.civility to chill_main_civility table
*/
final class Version20211007150459 extends AbstractMigration
{
public function getDescription(): string
{
return 'migrate data from 3party.civility to chill_main_civility table';
}
public function up(Schema $schema): void
{
$this->addSql('DROP INDEX chill_3party.uniq_d952467b384d4799');
$this->addSql('DROP INDEX chill_3party.uniq_d952467bba930d69');
$this->addSql('ALTER TABLE chill_3party.third_party ADD civility_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_3party.third_party ADD profession_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_3party.third_party ADD kind VARCHAR(20) NOT NULL DEFAULT \'\'');
$this->addSql('ALTER TABLE chill_3party.third_party ADD canonicalized TEXT NOT NULL DEFAULT \'\'');
$this->addSql('CREATE TEMPORARY TABLE civility_migration AS SELECT * FROM chill_3party.party_civility');
$this->addSql('ALTER TABLE civility_migration ADD COLUMN new_id INT DEFAULT NULL');
$this->addSql('UPDATE civility_migration SET new_id = nextval(\'chill_main_civility_id_seq\')');
$this->addSql('
INSERT INTO chill_main_civility (id, name, abbreviation, active)
SELECT new_id, name, \'{}\'::json, active from civility_migration
');
$this->addSql('UPDATE chill_3party.third_party SET civility_id = new_id
FROM civility_migration WHERE civility_migration.id = third_party.civility');
$this->addSql('ALTER TABLE chill_3party.third_party DROP CONSTRAINT fk_d952467b384d4799');
$this->addSql('ALTER TABLE chill_3party.third_party DROP CONSTRAINT fk_d952467bba930d69');
$this->addSql('ALTER TABLE chill_3party.third_party DROP civility');
$this->addSql('ALTER TABLE chill_3party.third_party DROP profession');
$this->addSql('DROP SEQUENCE chill_3party.party_civility_id_seq CASCADE');
$this->addSql('DROP TABLE chill_3party.party_civility');
$this->addSql('ALTER TABLE chill_3party.third_party ADD CONSTRAINT FK_D952467B23D6A298 FOREIGN KEY (civility_id) REFERENCES chill_main_civility (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_3party.third_party ADD CONSTRAINT FK_D952467BFDEF8996 FOREIGN KEY (profession_id) REFERENCES chill_3party.party_profession (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX IDX_D952467B23D6A298 ON chill_3party.third_party (civility_id)');
$this->addSql('CREATE INDEX IDX_D952467BFDEF8996 ON chill_3party.third_party (profession_id)');
}
public function down(Schema $schema): void
{
$this->throwIrreversibleMigrationException('Reversible migration not implemented');
// for reference:
$this->addSql('CREATE SEQUENCE chill_3party.party_civility_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_3party.party_civility (id INT NOT NULL, name JSON NOT NULL, active BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('ALTER TABLE chill_3party.third_party DROP CONSTRAINT FK_D952467B23D6A298');
$this->addSql('ALTER TABLE chill_3party.third_party DROP CONSTRAINT FK_D952467BFDEF8996');
$this->addSql('ALTER TABLE chill_3party.third_party ADD civility INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_3party.third_party ADD profession INT DEFAULT NULL');
$this->addSql('ALTER TABLE chill_3party.third_party DROP civility_id');
$this->addSql('ALTER TABLE chill_3party.third_party DROP profession_id');
$this->addSql('ALTER TABLE chill_3party.third_party DROP kind');
$this->addSql('ALTER TABLE chill_3party.third_party DROP canonicalized');
$this->addSql('ALTER TABLE chill_3party.third_party ADD CONSTRAINT fk_d952467b384d4799 FOREIGN KEY (civility) REFERENCES chill_3party.party_civility (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_3party.third_party ADD CONSTRAINT fk_d952467bba930d69 FOREIGN KEY (profession) REFERENCES chill_3party.party_profession (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE UNIQUE INDEX uniq_d952467b384d4799 ON chill_3party.third_party (civility)');
$this->addSql('CREATE UNIQUE INDEX uniq_d952467bba930d69 ON chill_3party.third_party (profession)');
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\ThirdParty;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Create trigger for canonicalisation on 3party + indexes
*/
final class Version20211007165001 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create trigger for canonicalisation on 3party + indexes';
}
public function up(Schema $schema): void
{
$this->addSql("
UPDATE chill_3party.third_party
SET canonicalized =
UNACCENT(
LOWER(
name ||
CASE WHEN COALESCE(name_company, '') <> '' THEN ' ' ELSE '' END ||
COALESCE(name_company, '') ||
CASE WHEN COALESCE(acronym, '') <> '' THEN ' ' ELSE '' END ||
COALESCE(acronym, '')
)
)
");
$this->addSql("
CREATE OR REPLACE FUNCTION chill_3party.canonicalize() RETURNS TRIGGER
LANGUAGE plpgsql
AS
$$
BEGIN
NEW.canonicalized =
UNACCENT(
LOWER(
NEW.name ||
CASE WHEN COALESCE(NEW.name_company, '') <> '' THEN ' ' ELSE '' END ||
COALESCE(NEW.name_company, '') ||
CASE WHEN COALESCE(NEW.acronym, '') <> '' THEN ' ' ELSE '' END ||
COALESCE(NEW.acronym, '')
)
)
;
return NEW;
END
$$
");
$this->addSql("
CREATE TRIGGER canonicalize_fullname_on_insert
BEFORE INSERT
ON chill_3party.third_party
FOR EACH ROW
EXECUTE procedure chill_3party.canonicalize();
");
$this->addSql("
CREATE TRIGGER canonicalize_fullname_on_update
BEFORE UPDATE
ON chill_3party.third_party
FOR EACH ROW
EXECUTE procedure chill_3party.canonicalize();
");
$this->addSql("
CREATE INDEX chill_custom_canonicalized_trgm_idx_gist
ON chill_3party.third_party USING GIST (canonicalized gist_trgm_ops) WHERE active IS TRUE
");
}
public function down(Schema $schema): void
{
$this->addSql('DROP TRIGGER canonicalize_fullname_on_update ON chill_3party.third_party');
$this->addSql('DROP TRIGGER canonicalize_fullname_on_insert ON chill_3party.third_party');
$this->addSql('DROP FUNCTION chill_3party.canonicalize()');
$this->addSql("
DROP INDEX chill_3party.chill_custom_canonicalized_trgm_idx_gist
");
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\ThirdParty;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add anonymous flag for contacts
*/
final class Version20211007194942 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add anonymous flag for contacts';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_3party.third_party ADD contact_data_anonymous BOOLEAN DEFAULT \'false\' NOT NULL;');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_3party.third_party DROP contact_data_anonymous');
}
}

View File

@ -29,6 +29,17 @@ thirdparty.UpdateBy.short: ' par '
thirdparty.CreatedAt.long: Date de création
thirdparty.UpdatedAt.long: Date de la dernière modification
thirdparty.UpdateBy.long: Utilisateur qui a effectué la dernière modification
thirdparty.A company: Une institution
thirdparty.company: Institution
thirdparty.A contact: Une personne physique
thirdparty.contact: Personne physique
thirdparty.a_company_explanation: >-
Les institutions peuvent compter un ou plusieurs contacts, interne à l'instution. Il est également possible de
leur associer un acronyme, et le nom d'un service.
thirdparty.a_contact_explanation: >-
Les personnes physiques ne disposent pas d'acronyme, de service, ou de contacts sous-jacents.
thirdparty.Which kind of third party ?: Quel type de tiers souhaitez-vous créer ?
thirdparty.Contact data are confidential: Données de contact confidentielles
New third party: Ajouter un nouveau tiers
Show third party %name%: Tiers "%name%"
@ -45,6 +56,10 @@ Inactive, not shown to users: Inactif, invisible pour les utilisateurs
Inactive: Inactif
not shown to users: invisible pour les utilisateurs
Show thirdparty: Voir le tiers
Add a contact: Ajouter un contact
Remove a contact: Supprimer
Contacts: Contacts
Any contact: Aucun contact
No nameCompany given: Aucune raison sociale renseignée
No acronym given: Aucun sigle renseigné
@ -52,9 +67,16 @@ No phone given: Aucun téléphone renseigné
No email given: Aucune adresse courriel renseignée
The party is visible in those centers: Le tiers est visible dans ces centres
The party is not visible in any center: Le tiers n'est associé à aucun centre
No third parties: Aucun tiers
# ROLES
CHILL_3PARTY_3PARTY_CREATE: Ajouter un Tiers
CHILL_3PARTY_3PARTY_SHOW: Voir un Tiers
CHILL_3PARTY_3PARTY_UPDATE: Modifier un Tiers
# crud:
crud:
3party_3party:
index:
add_new: Créer