Merge branch 'master' into admin-permissionsgroup-templates

This commit is contained in:
2023-04-14 12:22:32 +02:00
375 changed files with 12036 additions and 4094 deletions

View File

@@ -34,6 +34,29 @@ abstract class AbstractCRUDController extends AbstractController
*/
protected array $crudConfig = [];
/**
* get the role given from the config.
*
* @param mixed $entity
* @param mixed $_format
*/
protected function getRoleFor(string $action, Request $request, $entity, $_format): string
{
$actionConfig = $this->getActionConfig($action);
if (null !== $actionConfig['roles'][$request->getMethod()]) {
return $actionConfig['roles'][$request->getMethod()];
}
if ($this->crudConfig['base_role']) {
return $this->crudConfig['base_role'];
}
throw new \RuntimeException(sprintf('the config does not have any role for the ' .
'method %s nor a global role for the whole action. Add those to your ' .
'configuration or override the required method', $request->getMethod()));
}
public static function getSubscribedServices(): array
{
return array_merge(

View File

@@ -280,11 +280,13 @@ class ApiController extends AbstractCRUDController
switch ($request->getMethod()) {
case Request::METHOD_DELETE:
// oups... how to use property accessor to remove element ?
/* @phpstan-ignore-next-line as we do not find a simpler way to do this */
$entity->{'remove' . ucfirst($property)}($postedData);
break;
case Request::METHOD_POST:
/* @phpstan-ignore-next-line as we do not find a simpler way to do this */
$entity->{'add' . ucfirst($property)}($postedData);
break;
@@ -499,28 +501,6 @@ class ApiController extends AbstractCRUDController
return ['groups' => ['read']];
}
/**
* get the role given from the config.
*
* @param mixed $entity
* @param mixed $_format
*/
protected function getRoleFor(string $action, Request $request, $entity, $_format): string
{
$actionConfig = $this->getActionConfig($action);
if (null !== $actionConfig['roles'][$request->getMethod()]) {
return $actionConfig['roles'][$request->getMethod()];
}
if ($this->crudConfig['base_role']) {
return $this->crudConfig['base_role'];
}
throw new RuntimeException(sprintf('the config does not have any role for the ' .
'method %s nor a global role for the whole action. Add those to your ' .
'configuration or override the required method', $request->getMethod()));
}
protected function getSerializer(): SerializerInterface
{

View File

@@ -101,7 +101,7 @@ class CRUDRoutesLoader extends Loader
$singleCollection = $action['single_collection'] ?? '_entity' === $name ? 'single' : null;
if ('collection' === $singleCollection) {
// continue;
// continue;
}
// compute default action

View File

@@ -72,7 +72,6 @@ class ChillMainBundle extends Bundle
$container->addCompilerPass(new NotificationCounterCompilerPass());
$container->addCompilerPass(new MenuCompilerPass());
$container->addCompilerPass(new ACLFlagsCompilerPass());
$container->addCompilerPass(new GroupingCenterCompilerPass());
$container->addCompilerPass(new CRUDControllerCompilerPass());
$container->addCompilerPass(new ShortMessageCompilerPass());
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\Address;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
class AddressToReferenceMatcherController
{
private Security $security;
private EntityManagerInterface $entityManager;
private SerializerInterface $serializer;
public function __construct(
Security $security,
EntityManagerInterface $entityManager,
SerializerInterface $serializer
) {
$this->security = $security;
$this->entityManager = $entityManager;
$this->serializer = $serializer;
}
/**
* @Route("/api/1.0/main/address/reference-match/{id}/set/reviewed", methods={"POST"})
*/
public function markAddressAsReviewed(Address $address): JsonResponse
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
$address->setRefStatus(Address::ADDR_REFERENCE_STATUS_REVIEWED);
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($address, 'json', [AbstractNormalizer::GROUPS => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
/**
* Set an address back to "to review". Only if the address is in "reviewed" state.
*
* @Route("/api/1.0/main/address/reference-match/{id}/set/to_review", methods={"POST"})
*/
public function markAddressAsToReview(Address $address): JsonResponse
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
if (Address::ADDR_REFERENCE_STATUS_REVIEWED !== $address->getRefStatus()) {
throw new AccessDeniedHttpException("forbidden to mark a matching address to 'to review'");
}
$address->setRefStatus(Address::ADDR_REFERENCE_STATUS_TO_REVIEW);
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($address, 'json', [AbstractNormalizer::GROUPS => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
/**
* @Route("/api/1.0/main/address/reference-match/{id}/sync-with-reference", methods={"POST"})
*/
public function syncAddressWithReference(Address $address): JsonResponse
{
if (null === $address->getAddressReference()) {
throw new BadRequestHttpException('this address does not have any address reference');
}
$address->syncWithReference($address->getAddressReference());
$this->entityManager->flush();
return new JsonResponse(
$this->serializer->serialize($address, 'json', [AbstractNormalizer::GROUPS => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\GeographicalUnitRepositoryInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
class GeographicalUnitByAddressApiController
{
private PaginatorFactory $paginatorFactory;
private GeographicalUnitRepositoryInterface $geographicalUnitRepository;
private Security $security;
private SerializerInterface $serializer;
/**
* @param PaginatorFactory $paginatorFactory
* @param GeographicalUnitRepositoryInterface $geographicalUnitRepository
* @param Security $security
* @param SerializerInterface $serializer
*/
public function __construct(
PaginatorFactory $paginatorFactory,
GeographicalUnitRepositoryInterface $geographicalUnitRepository,
Security $security,
SerializerInterface $serializer
) {
$this->paginatorFactory = $paginatorFactory;
$this->geographicalUnitRepository = $geographicalUnitRepository;
$this->security = $security;
$this->serializer = $serializer;
}
/**
* @Route("/api/1.0/main/geographical-unit/by-address/{id}.{_format}", requirements={"_format": "json"})
*/
public function getGeographicalUnitCoveringAddress(Address $address): JsonResponse
{
if (!$this->security->isGranted('ROLE_USER')) {
throw new AccessDeniedHttpException();
}
$count = $this->geographicalUnitRepository->countGeographicalUnitContainingAddress($address);
$pagination = $this->paginatorFactory->create($count);
$units = $this->geographicalUnitRepository->findGeographicalUnitContainingAddress($address, $pagination->getCurrentPageFirstItemNumber(), $pagination->getItemsPerPage());
$collection = new Collection($units, $pagination);
return new JsonResponse(
$this->serializer->serialize($collection, 'json', [AbstractNormalizer::GROUPS => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
}

View File

@@ -28,7 +28,7 @@ class LocationController extends CRUDController
protected function customizeQuery(string $action, Request $request, $query): void
{
$query->where('e.availableForUsers = "TRUE"');
$query->where('e.availableForUsers = TRUE');
}
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)

View File

@@ -93,6 +93,45 @@ class WorkflowApiController
);
}
/**
* Return a list of workflow which are waiting an action for the user.
*
* @Route("/api/1.0/main/workflow/my-cc", methods={"GET"})
*/
public function myWorkflowCc(Request $request): JsonResponse
{
if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) {
throw new AccessDeniedException();
}
$total = $this->entityWorkflowRepository->countByCc($this->security->getUser());
if ($request->query->getBoolean('countOnly', false)) {
return new JsonResponse(
$this->serializer->serialize(new Counter($total), 'json'),
JsonResponse::HTTP_OK,
[],
true
);
}
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByCc(
$this->security->getUser(),
['id' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return new JsonResponse(
$this->serializer->serialize(new Collection($workflows, $paginator), 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
/**
* @Route("/api/1.0/main/workflow/{id}/subscribe", methods={"POST"})
*/

View File

@@ -30,6 +30,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\TransitionBlocker;
@@ -48,11 +49,13 @@ class WorkflowController extends AbstractController
private Registry $registry;
private Security $security;
private TranslatorInterface $translator;
private ValidatorInterface $validator;
public function __construct(EntityWorkflowManager $entityWorkflowManager, EntityWorkflowRepository $entityWorkflowRepository, ValidatorInterface $validator, PaginatorFactory $paginatorFactory, Registry $registry, EntityManagerInterface $entityManager, TranslatorInterface $translator)
public function __construct(EntityWorkflowManager $entityWorkflowManager, EntityWorkflowRepository $entityWorkflowRepository, ValidatorInterface $validator, PaginatorFactory $paginatorFactory, Registry $registry, EntityManagerInterface $entityManager, TranslatorInterface $translator, Security $security)
{
$this->entityWorkflowManager = $entityWorkflowManager;
$this->entityWorkflowRepository = $entityWorkflowRepository;
@@ -61,6 +64,7 @@ class WorkflowController extends AbstractController
$this->registry = $registry;
$this->entityManager = $entityManager;
$this->translator = $translator;
$this->security = $security;
}
/**
@@ -224,6 +228,33 @@ class WorkflowController extends AbstractController
);
}
/**
* @Route("/{_locale}/main/workflow/list/cc", name="chill_main_workflow_list_cc")
*/
public function myWorkflowsCc(Request $request): Response
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
$total = $this->entityWorkflowRepository->countByDest($this->getUser());
$paginator = $this->paginatorFactory->create($total);
$workflows = $this->entityWorkflowRepository->findByCc(
$this->getUser(),
['createdAt' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return $this->render(
'@ChillMain/Workflow/list.html.twig',
[
'workflows' => $this->buildHandler($workflows),
'paginator' => $paginator,
'step' => 'cc',
]
);
}
/**
* @Route("/{_locale}/main/workflow/list/dest", name="chill_main_workflow_list_dest")
*/
@@ -291,10 +322,22 @@ class WorkflowController extends AbstractController
if (count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
// possible transition
$usersInvolved = $entityWorkflow->getUsersInvolved();
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
if (false !== $currentUserFound) {
unset($usersInvolved[$currentUserFound]);
}
$transitionForm = $this->createForm(
WorkflowStepType::class,
$entityWorkflow->getCurrentStep(),
['transition' => true, 'entity_workflow' => $entityWorkflow]
[
'transition' => true,
'entity_workflow' => $entityWorkflow,
'suggested_users' => $usersInvolved
]
);
$transitionForm->handleRequest($request);
@@ -318,6 +361,7 @@ class WorkflowController extends AbstractController
}
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData();
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData();
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData();

View File

@@ -50,8 +50,8 @@ class LoadLanguages extends AbstractFixture implements ContainerAwareInterface,
foreach (Intl::getLanguageBundle()->getLanguageNames() as $code => $language) {
if (
!in_array($code, $this->regionalVersionToInclude, true)
&& !in_array($code, $this->ancientToExclude, true)
!in_array($code, $this->regionalVersionToInclude, true)
&& !in_array($code, $this->ancientToExclude, true)
) {
$lang = (new Language())
->setId($code)

View File

@@ -46,6 +46,7 @@ use Chill\MainBundle\Doctrine\Type\NativeDateIntervalType;
use Chill\MainBundle\Doctrine\Type\PointType;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\GeographicalUnitLayer;
use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\LocationType;
@@ -732,6 +733,20 @@ class ChillMainExtension extends Extension implements
],
],
],
[
'class' => GeographicalUnitLayer::class,
'name' => 'geographical-unit-layer',
'base_path' => '/api/1.0/main/geographical-unit-layer',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
],
]
],
]);
}

View File

@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\DependencyInjection\CompilerPass;
use LogicException;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class GroupingCenterCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (false === $container->hasDefinition('chill.main.form.pick_centers_type')) {
throw new LogicException('The service chill.main.form.pick_centers_type does '
. 'not exists in container');
}
$pickCenterType = $container->getDefinition('chill.main.form.pick_centers_type');
foreach ($container->findTaggedServiceIds('chill.grouping_center') as $serviceId => $tagged) {
$pickCenterType->addMethodCall(
'addGroupingCenter',
[new Reference($serviceId)]
);
}
}
}

View File

@@ -274,7 +274,7 @@ class Configuration implements ConfigurationInterface
->end()
->end() // end of root/children
->end() // end of root
;
;
$rootNode->children()
->arrayNode('add_address')->addDefaultsIfNotSet()->children()

View File

@@ -211,7 +211,7 @@ trait AddWidgetConfigurationTrait
*
* @throws InvalidConfigurationException if a service's tag does not have the "alias" key
*
* @return type
* @return array
*/
protected function getWidgetAliasesbyPlace($place, ContainerBuilder $containerBuilder)
{

View File

@@ -12,6 +12,10 @@ declare(strict_types=1);
namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Doctrine\Model\Point;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateTime;
use DateTimeInterface;
@@ -28,8 +32,28 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
* @ORM\Table(name="chill_main_address")
* @ORM\HasLifecycleCallbacks
*/
class Address
class Address implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
/**
* When an Address does match with the AddressReference
*/
public const ADDR_REFERENCE_STATUS_MATCH = 'match';
/**
* When an Address does not match with the AddressReference, and
* is pending for a review
*/
public const ADDR_REFERENCE_STATUS_TO_REVIEW = 'to_review';
/**
* When an Address does not match with the AddressReference, but
* is reviewed
*/
public const ADDR_REFERENCE_STATUS_REVIEWED = 'reviewed';
/**
* @ORM\ManyToOne(targetEntity=AddressReference::class)
* @Groups({"write"})
@@ -37,67 +61,48 @@ class Address
private ?AddressReference $addressReference = null;
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Groups({"write"})
*/
private $buildingName;
private string $buildingName = '';
/**
* @ORM\Column(type="boolean")
* @ORM\Column(type="boolean", options={"default": false})
* @Groups({"write"})
*/
private bool $confidential = false;
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Groups({"write"})
*/
private $corridor;
private string $corridor = '';
/**
* A list of metadata, added by customizable fields.
*
* @var array
*/
private $customs = [];
/**
* @var string|null
*
* used for the CEDEX information
*
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Groups({"write"})
*/
private $distribution;
private string $distribution = '';
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Groups({"write"})
*/
private $extra;
private string $extra = '';
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Groups({"write"})
*/
private $flat;
private string $flat = '';
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Groups({"write"})
*/
private $floor;
private string $floor = '';
/**
* List of geographical units and addresses.
@@ -131,11 +136,9 @@ class Address
* True if the address is a "no address", aka homeless person, ...
*
* @Groups({"write"})
* @ORM\Column(type="boolean")
*
* @var bool
* @ORM\Column(type="boolean", options={"default": false})
*/
private $isNoAddress = false;
private bool $isNoAddress = false;
/**
* A ThirdParty reference for person's addresses that are linked to a third party.
@@ -146,7 +149,7 @@ class Address
* @Groups({"write"})
* @ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $linkedToThirdParty;
private ?ThirdParty $linkedToThirdParty;
/**
* A geospatial field storing the coordinates of the Address.
@@ -156,7 +159,7 @@ class Address
* @ORM\Column(type="point", nullable=true)
* @Groups({"write"})
*/
private $point;
private ?Point $point = null;
/**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
@@ -166,28 +169,36 @@ class Address
private ?PostalCode $postcode = null;
/**
* @var string|null
*
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"write"})
* @var self::ADDR_REFERENCE_STATUS_*
* @ORM\Column(type="text", nullable=false, options={"default": self::ADDR_REFERENCE_STATUS_MATCH})
*/
private $steps;
private string $refStatus = self::ADDR_REFERENCE_STATUS_MATCH;
/**
* @var string
*
* @ORM\Column(type="string", length=255)
* @Groups({"write"})
* @ORM\Column(type="datetime_immutable", nullable=false, options={"default": "CURRENT_TIMESTAMP"})
*/
private $street = '';
private \DateTimeImmutable $refStatusLastUpdate;
/**
* @var string
*
* @ORM\Column(type="string", length=255)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Groups({"write"})
*/
private $streetNumber = '';
private string $steps = '';
/**
*
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Groups({"write"})
*/
private string $street = '';
/**
*
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Groups({"write"})
*/
private string $streetNumber = '';
/**
* Indicates when the address starts validation. Used to build an history
@@ -210,6 +221,7 @@ class Address
public function __construct()
{
$this->validFrom = new DateTime();
$this->refStatusLastUpdate = new \DateTimeImmutable('now');
$this->geographicalUnits = new ArrayCollection();
}
@@ -220,7 +232,6 @@ class Address
->setBuildingName($original->getBuildingName())
->setConfidential($original->getConfidential())
->setCorridor($original->getCorridor())
->setCustoms($original->getCustoms())
->setDistribution($original->getDistribution())
->setExtra($original->getExtra())
->setFlat($original->getFlat())
@@ -239,11 +250,20 @@ class Address
public static function createFromAddressReference(AddressReference $original): Address
{
return (new Address())
->setPoint($original->getPoint())
->setPostcode($original->getPostcode())
->setStreet($original->getStreet())
->setStreetNumber($original->getStreetNumber())
->setAddressReference($original);
->syncWithReference($original);
}
public function syncWithReference(AddressReference $addressReference): Address
{
$this
->setPoint($addressReference->getPoint())
->setPostcode($addressReference->getPostcode())
->setStreet($addressReference->getStreet())
->setStreetNumber($addressReference->getStreetNumber())
->setRefStatus(self::ADDR_REFERENCE_STATUS_MATCH)
->setAddressReference($addressReference);
return $this;
}
public function getAddressReference(): ?AddressReference
@@ -251,7 +271,7 @@ class Address
return $this->addressReference;
}
public function getBuildingName(): ?string
public function getBuildingName(): string
{
return $this->buildingName;
}
@@ -261,35 +281,27 @@ class Address
return $this->confidential;
}
public function getCorridor(): ?string
public function getCorridor(): string
{
return $this->corridor;
}
/**
* Get customs informations in the address.
*/
public function getCustoms(): array
{
return $this->customs;
}
public function getDistribution(): ?string
public function getDistribution(): string
{
return $this->distribution;
}
public function getExtra(): ?string
public function getExtra(): string
{
return $this->extra;
}
public function getFlat(): ?string
public function getFlat(): string
{
return $this->flat;
}
public function getFloor(): ?string
public function getFloor(): string
{
return $this->floor;
}
@@ -340,12 +352,22 @@ class Address
return $this->postcode;
}
public function getSteps(): ?string
public function getRefStatus(): string
{
return $this->refStatus;
}
public function getRefStatusLastUpdate(): \DateTimeImmutable
{
return $this->refStatusLastUpdate;
}
public function getSteps(): string
{
return $this->steps;
}
public function getStreet(): ?string
public function getStreet(): string
{
return $this->street;
}
@@ -354,6 +376,7 @@ class Address
* Get streetAddress1 (legacy function).
*
* @return string
* @deprecated
*/
public function getStreetAddress1()
{
@@ -364,13 +387,14 @@ class Address
* Get streetAddress2 (legacy function).
*
* @return string
* @deprecated
*/
public function getStreetAddress2()
{
return $this->streetNumber;
}
public function getStreetNumber(): ?string
public function getStreetNumber(): string
{
return $this->streetNumber;
}
@@ -378,7 +402,7 @@ class Address
/**
* @return DateTime
*/
public function getValidFrom()
public function getValidFrom(): DateTime
{
return $this->validFrom;
}
@@ -407,7 +431,7 @@ class Address
public function setBuildingName(?string $buildingName): self
{
$this->buildingName = $buildingName;
$this->buildingName = (string) $buildingName;
return $this;
}
@@ -421,47 +445,35 @@ class Address
public function setCorridor(?string $corridor): self
{
$this->corridor = $corridor;
return $this;
}
/**
* Store custom informations in the address.
*
* @return $this
*/
public function setCustoms(array $customs): self
{
$this->customs = $customs;
$this->corridor = (string) $corridor;
return $this;
}
public function setDistribution(?string $distribution): self
{
$this->distribution = $distribution;
$this->distribution = (string) $distribution;
return $this;
}
public function setExtra(?string $extra): self
{
$this->extra = $extra;
$this->extra = (string) $extra;
return $this;
}
public function setFlat(?string $flat): self
{
$this->flat = $flat;
$this->flat = (string) $flat;
return $this;
}
public function setFloor(?string $floor): self
{
$this->floor = $floor;
$this->floor = (string) $floor;
return $this;
}
@@ -508,19 +520,44 @@ class Address
return $this;
}
/**
* Update the ref status
*
* <<<<<<< HEAD
* @param Address::ADDR_REFERENCE_STATUS_* $refStatus
* @param bool|null $updateLastUpdate Also update the "refStatusLastUpdate"
* =======
* The refstatuslast update is also updated
* >>>>>>> 31152616d (Feature: Provide api endpoint for reviewing addresses)
*/
public function setRefStatus(string $refStatus, ?bool $updateLastUpdate = true): self
{
$this->refStatus = $refStatus;
if ($updateLastUpdate) {
$this->setRefStatusLastUpdate(new \DateTimeImmutable('now'));
}
return $this;
}
public function setRefStatusLastUpdate(\DateTimeImmutable $refStatusLastUpdate): self
{
$this->refStatusLastUpdate = $refStatusLastUpdate;
return $this;
}
public function setSteps(?string $steps): self
{
$this->steps = $steps;
$this->steps = (string) $steps;
return $this;
}
public function setStreet(?string $street): self
{
if (null === $street) {
$street = '';
}
$this->street = $street;
$this->street = (string) $street;
return $this;
}
@@ -531,6 +568,7 @@ class Address
* @param string $streetAddress1
*
* @return Address
* @deprecated
*/
public function setStreetAddress1($streetAddress1)
{
@@ -543,7 +581,7 @@ class Address
* Set streetAddress2 (legacy function).
*
* @param string $streetAddress2
*
* @deprecated
* @return Address
*/
public function setStreetAddress2($streetAddress2)
@@ -555,10 +593,7 @@ class Address
public function setStreetNumber(?string $streetNumber): self
{
if (null === $streetNumber) {
$streetNumber = '';
}
$this->streetNumber = $streetNumber;
$this->streetNumber = (string) $streetNumber;
return $this;
}
@@ -605,7 +640,7 @@ class Address
return;
}
if (empty($this->getStreetAddress1())) {
if ('' === $this->getStreet()) {
$context
->buildViolation('address.street1-should-be-set')
->atPath('streetAddress1')

View File

@@ -55,13 +55,13 @@ class AddressReference
* @ORM\Column(type="integer")
* @groups({"read"})
*/
private $id;
private ?int $id;
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @groups({"read"})
*/
private $municipalityCode;
private string $municipalityCode = '';
/**
* A geospatial field storing the coordinates of the Address.
@@ -71,7 +71,7 @@ class AddressReference
* @ORM\Column(type="point")
* @groups({"read"})
*/
private $point;
private ?Point $point = null;
/**
* @var PostalCode
@@ -79,31 +79,31 @@ class AddressReference
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode")
* @groups({"read"})
*/
private $postcode;
private ?PostalCode $postcode;
/**
* @ORM\Column(type="string", length=255)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @groups({"read"})
*/
private $refId;
private string $refId = '';
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @groups({"read"})
*/
private $source;
private string $source = '';
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @groups({"read"})
*/
private $street;
private string $street = '';
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @groups({"read"})
*/
private $streetNumber;
private string $streetNumber = '';
/**
* @ORM\Column(type="datetime_immutable", nullable=true)
@@ -126,7 +126,7 @@ class AddressReference
return $this->id;
}
public function getMunicipalityCode(): ?string
public function getMunicipalityCode(): string
{
return $this->municipalityCode;
}
@@ -141,27 +141,27 @@ class AddressReference
*
* @return PostalCode
*/
public function getPostcode()
public function getPostcode(): ?PostalCode
{
return $this->postcode;
}
public function getRefId(): ?string
public function getRefId(): string
{
return $this->refId;
}
public function getSource(): ?string
public function getSource(): string
{
return $this->source;
}
public function getStreet(): ?string
public function getStreet(): string
{
return $this->street;
}
public function getStreetNumber(): ?string
public function getStreetNumber(): string
{
return $this->streetNumber;
}
@@ -192,7 +192,7 @@ class AddressReference
public function setMunicipalityCode(?string $municipalityCode): self
{
$this->municipalityCode = $municipalityCode;
$this->municipalityCode = (string) $municipalityCode;
return $this;
}
@@ -227,21 +227,21 @@ class AddressReference
public function setSource(?string $source): self
{
$this->source = $source;
$this->source = (string) $source;
return $this;
}
public function setStreet(?string $street): self
{
$this->street = $street;
$this->street = (string) $street;
return $this;
}
public function setStreetNumber(?string $streetNumber): self
{
$this->streetNumber = $streetNumber;
$this->streetNumber = (string) $streetNumber;
return $this;
}

View File

@@ -43,36 +43,20 @@ class Country
private ?int $id = null;
/**
* @var string
* @var array<string, string>
*
* @ORM\Column(type="json")
* @groups({"read", "docgen:read"})
* @Context({"is-translatable": true}, groups={"docgen:read"})
*/
private $name;
private array $name = [];
/**
* @return string
*/
public function __toString()
{
return $this->getName();
}
/**
* @return the string
*/
public function getCountryCode()
public function getCountryCode(): string
{
return $this->countryCode;
}
/**
* Get id.
*
* @return int
*/
public function getId()
public function getId(): ?int
{
return $this->id;
}
@@ -80,31 +64,23 @@ class Country
/**
* Get name.
*
* @return string
*/
public function getName()
public function getName(): array
{
return $this->name;
}
/**
* @param string $countryCode
*/
public function setCountryCode($countryCode)
public function setCountryCode(?string $countryCode): self
{
$this->countryCode = $countryCode;
$this->countryCode = (string) $countryCode;
return $this;
}
/**
* Set name.
*
* @param string $name
*
* @return Country
* @param array<string, string> $name
*/
public function setName($name)
public function setName(array $name): self
{
$this->name = $name;

View File

@@ -51,10 +51,7 @@ class CommentEmbeddable
return $this->date;
}
/**
* @return interger $userId
*/
public function getUserId()
public function getUserId(): ?int
{
return $this->userId;
}

View File

@@ -11,6 +11,8 @@ declare(strict_types=1);
namespace Chill\MainBundle\Entity\GeographicalUnit;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* Simple GeographialUnit Data Transfer Object.
*
@@ -21,24 +23,28 @@ class SimpleGeographicalUnitDTO
/**
* @readonly
* @psalm-readonly
* @Serializer\Groups({"read"})
*/
public int $id;
/**
* @readonly
* @psalm-readonly
* @Serializer\Groups({"read"})
*/
public int $layerId;
/**
* @readonly
* @psalm-readonly
* @Serializer\Groups({"read"})
*/
public string $unitName;
/**
* @readonly
* @psalm-readonly
* @Serializer\Groups({"read"})
*/
public string $unitRefId;

View File

@@ -14,6 +14,7 @@ namespace Chill\MainBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Table(name="chill_main_geographical_unit_layer", uniqueConstraints={
@@ -27,16 +28,19 @@ class GeographicalUnitLayer
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
*/
private ?int $id = null;
/**
* @ORM\Column(type="json", nullable=false, options={"default": "[]"})
* @Serializer\Groups({"read"})
*/
private array $name = [];
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Serializer\Groups({"read"})
*/
private string $refId = '';

View File

@@ -17,6 +17,7 @@ use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Doctrine\ORM\Event\PrePersistEventArgs;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
@@ -133,7 +134,7 @@ class NotificationComment implements TrackCreationInterface, TrackUpdateInterfac
/**
* @ORM\PrePersist
*/
public function onPrePersist(LifecycleEventArgs $eventArgs): void
public function onPrePersist(PrePersistEventArgs $eventArgs): void
{
$this->recentlyPersisted = true;
}

View File

@@ -16,6 +16,7 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Validator\Constraints\Entity\WorkflowStepUsersOnTransition;
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
@@ -24,6 +25,7 @@ use Doctrine\ORM\Mapping as ORM;
use Iterator;
use RuntimeException;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
use function count;
use function is_array;
@@ -41,6 +43,13 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
use TrackUpdateTrait;
/**
* a list of future cc users for the next steps.
*
* @var array|User[]
*/
public array $futureCcUsers = [];
/**
* a list of future dest emails for the next steps.
*
@@ -90,7 +99,7 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
/**
* @ORM\OneToMany(targetEntity=EntityWorkflowStep::class, mappedBy="entityWorkflow", orphanRemoval=true, cascade={"persist"})
* @ORM\OrderBy({"transitionAt": "ASC", "id": "ASC"})
*
* @Assert\Valid(traverse=true)
* @var Collection|EntityWorkflowStep[]
*/
private Collection $steps;
@@ -348,6 +357,23 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
return $this->transitionningStep;
}
/**
* @return User[]
*/
public function getUsersInvolved(): array
{
$usersInvolved = [];
$usersInvolved[spl_object_hash($this->getCreatedBy())] = $this->getCreatedBy();
foreach ($this->steps as $step) {
foreach ($step->getDestUser() as $u) {
$usersInvolved[spl_object_hash($u)] = $u;
}
}
return $usersInvolved;
}
public function getWorkflowName(): string
{
return $this->workflowName;

View File

@@ -32,6 +32,12 @@ class EntityWorkflowStep
*/
private string $accessKey;
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_workflow_entity_step_cc_user")
*/
private Collection $ccUser;
/**
* @ORM\Column(type="text", options={"default": ""})
*/
@@ -114,11 +120,21 @@ class EntityWorkflowStep
public function __construct()
{
$this->ccUser = new ArrayCollection();
$this->destUser = new ArrayCollection();
$this->destUserByAccessKey = new ArrayCollection();
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
}
public function addCcUser(User $user): self
{
if (!$this->ccUser->contains($user)) {
$this->ccUser[] = $user;
}
return $this;
}
public function addDestEmail(string $email): self
{
if (!in_array($email, $this->destEmail, true)) {
@@ -167,6 +183,11 @@ class EntityWorkflowStep
);
}
public function getCcUser(): Collection
{
return $this->ccUser;
}
public function getComment(): string
{
return $this->comment;
@@ -261,6 +282,13 @@ class EntityWorkflowStep
return true;
}
public function removeCcUser(User $user): self
{
$this->ccUser->removeElement($user);
return $this;
}
public function removeDestEmail(string $email): self
{
$this->destEmail = array_filter($this->destEmail, static function (string $existing) use ($email) {

View File

@@ -11,7 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export;
use Closure;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\QueryBuilder;
/**
@@ -24,6 +24,7 @@ use Doctrine\ORM\QueryBuilder;
* aggregation, use `ListInterface`.
*
* @example Chill\PersonBundle\Export\CountPerson an example of implementation
* @template Q of QueryBuilder|NativeQuery
*/
interface ExportInterface extends ExportElementInterface
{
@@ -85,7 +86,7 @@ interface ExportInterface extends ExportElementInterface
* @param mixed[] $values The values from the result. if there are duplicates, those might be given twice. Example: array('FR', 'BE', 'CZ', 'FR', 'BE', 'FR')
* @param mixed $data The data from the export's form (as defined in `buildForm`)
*
* @return pure-callable(null|string|int|float|'_header' $value):string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
* @return callable(null|string|int|float|'_header' $value): string|int|\DateTimeInterface where the first argument is the value, and the function should return the label to show in the formatted file. Example : `function($countryCode) use ($countries) { return $countries[$countryCode]->getName(); }`
*/
public function getLabels($key, array $values, $data);
@@ -103,7 +104,7 @@ interface ExportInterface extends ExportElementInterface
/**
* Return the results of the query builder.
*
* @param \Doctrine\ORM\NativeQuery|QueryBuilder $query
* @param Q $query
* @param mixed[] $data the data from the export's fomr (added by self::buildForm)
*
* @return mixed[] an array of results
@@ -133,7 +134,7 @@ interface ExportInterface extends ExportElementInterface
* @param array $acl an array where each row has a `center` key containing the Chill\MainBundle\Entity\Center, and `circles` keys containing the reachable circles. Example: `array( array('center' => $centerA, 'circles' => array($circleA, $circleB) ) )`
* @param array $data the data from the form, if any
*
* @return \Doctrine\ORM\NativeQuery|QueryBuilder the query to execute.
* @return Q the query to execute.
*/
public function initiateQuery(array $requiredModifiers, array $acl, array $data = []);

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export\Helper;
use DateTime;
use DateTimeInterface;
use Exception;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -35,7 +36,7 @@ class DateTimeHelper
return '';
}
if ($value instanceof \DateTimeInterface) {
if ($value instanceof DateTimeInterface) {
return $value;
}

View File

@@ -43,11 +43,13 @@ class AddressDataMapper implements DataMapperInterface
/** @var FormInterface $form */
switch ($key) {
case 'streetAddress1':
/** @phpstan-ignore-next-line */
$form->setData($address->getStreetAddress1());
break;
case 'streetAddress2':
/** @phpstan-ignore-next-line */
$form->setData($address->getStreetAddress2());
break;
@@ -110,11 +112,13 @@ class AddressDataMapper implements DataMapperInterface
return;
}
/** @phpstan-ignore-next-line */
$address->setStreetAddress1($form->getData());
break;
case 'streetAddress2':
/** @phpstan-ignore-next-line */
$address->setStreetAddress2($form->getData());
break;

View File

@@ -17,21 +17,14 @@ use Chill\MainBundle\Repository\RegroupmentRepository;
use Exception;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\FormInterface;
use function array_key_exists;
use function count;
class ExportPickCenterDataMapper implements DataMapperInterface
{
protected RegroupmentRepository $regroupmentRepository;
/**
* @param array|Center[] $data
* @param $forms
*
* @throws Exception
*
* @return mixed
*/
public function mapDataToForms($data, $forms)
public function mapDataToForms($data, $forms): void
{
if (null === $data) {
return;
@@ -43,7 +36,9 @@ class ExportPickCenterDataMapper implements DataMapperInterface
$pickedRegroupment = [];
foreach ($this->regroupmentRepository->findAll() as $regroupment) {
[$contained, $notContained] = $regroupment->getCenters()->partition(static function (Center $center) {
/** @phpstan-ignore-next-line */
[$contained, $notContained] = $regroupment->getCenters()->partition(static function (Center $center): bool {
return false;
});
if (0 === count($notContained)) {
@@ -55,13 +50,7 @@ class ExportPickCenterDataMapper implements DataMapperInterface
$form['centers']->setData($data);
}
/**
* @param iterable $forms
* @param array $data
*
* @return void
*/
public function mapFormsToData($forms, &$data)
public function mapFormsToData($forms, &$data): void
{
/** @var array<string, FormInterface> $forms */
$forms = iterator_to_array($forms);
@@ -72,10 +61,12 @@ class ExportPickCenterDataMapper implements DataMapperInterface
$centers[spl_object_hash($center)] = $center;
}
foreach ($forms['regroupment']->getData() as $regroupment) {
if (array_key_exists('regroupment', $forms)) {
/** @var Regroupment $regroupment */
foreach ($regroupment->getCenters() as $center) {
$centers[spl_object_hash($center)] = $center;
foreach ($forms['regroupment']->getData() as $regroupment) {
foreach ($regroupment->getCenters() as $center) {
$centers[spl_object_hash($center)] = $center;
}
}
}

View File

@@ -38,7 +38,7 @@ class RollingDateDataMapper implements DataMapperInterface
$forms = iterator_to_array($forms);
$viewData = new RollingDate(
$forms['roll']->getData(),
($forms['roll']->getData() ?? RollingDate::T_TODAY),
$forms['fixedDate']->getData()
);
}

View File

@@ -42,7 +42,9 @@ class IdToEntityDataTransformer implements DataTransformerInterface
{
$this->repository = $repository;
$this->multiple = $multiple;
$this->getId = $getId ?? static function (object $o) { return $o->getId(); };
$this->getId = $getId ?? static function (object $o) {
return $o->getId();
};
}
/**

View File

@@ -56,12 +56,7 @@ class CommentType extends AbstractType
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace(
$view->vars,
[
'fullWidth' => true,
]
);
$view->vars['fullWidth'] = true;
}
public function configureOptions(OptionsResolver $resolver)

View File

@@ -31,8 +31,6 @@ class MultipleObjectsToIdTransformer implements DataTransformerInterface
* Transforms a string (id) to an object (item).
*
* @param mixed $array
*
* @return ArrayCollection
*/
public function reverseTransform($array)
{
@@ -53,10 +51,8 @@ class MultipleObjectsToIdTransformer implements DataTransformerInterface
* Transforms an object (use) to a string (id).
*
* @param array $array
*
* @return ArrayCollection
*/
public function transform($array)
public function transform($array): array
{
$ret = [];

View File

@@ -11,7 +11,6 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\Export;
use Chill\MainBundle\Center\GroupingCenterInterface;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Regroupment;
use Chill\MainBundle\Export\ExportManager;
@@ -72,8 +71,10 @@ final class PickCenterType extends AbstractType
return $c->getName();
},
'data' => $centers,
])
->add('regroupment', EntityType::class, [
]);
if (count($this->regroupmentRepository->findAllActive()) > 0) {
$builder->add('regroupment', EntityType::class, [
'class' => Regroupment::class,
'label' => 'regroupment',
'multiple' => true,
@@ -83,6 +84,7 @@ final class PickCenterType extends AbstractType
return $r->getName();
},
]);
}
$builder->setDataMapper(new ExportPickCenterDataMapper());
}

View File

@@ -19,6 +19,7 @@ use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
@@ -28,12 +29,15 @@ class PickUserDynamicType extends AbstractType
{
private DenormalizerInterface $denormalizer;
private NormalizerInterface $normalizer;
private SerializerInterface $serializer;
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer)
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer, NormalizerInterface $normalizer)
{
$this->denormalizer = $denormalizer;
$this->serializer = $serializer;
$this->normalizer = $normalizer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
@@ -46,6 +50,11 @@ class PickUserDynamicType extends AbstractType
$view->vars['multiple'] = $options['multiple'];
$view->vars['types'] = ['user'];
$view->vars['uniqid'] = uniqid('pick_user_dyn');
$view->vars['suggested'] = [];
foreach ($options['suggested'] as $user) {
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
}
}
public function configureOptions(OptionsResolver $resolver)
@@ -53,7 +62,8 @@ class PickUserDynamicType extends AbstractType
$resolver
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false);
->setDefault('compound', false)
->setDefault('suggested', []);
}
public function getBlockPrefix()

View File

@@ -39,7 +39,7 @@ class PrivateCommentType extends AbstractType
$builder
->add('comments', ChillTextareaType::class, [
'disable_editor' => $options['disable_editor'],
'label' => false,
'label' => $options['label'],
])
->setDataMapper($this->dataMapper);
}

View File

@@ -95,12 +95,7 @@ class ScopePickerType extends AbstractType
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace(
$view->vars,
[
'fullWidth' => true,
]
);
$view->vars['fullWidth'] = true;
}
public function configureOptions(OptionsResolver $resolver)

View File

@@ -137,8 +137,8 @@ class WorkflowStepType extends AbstractType
$meta = $workflow->getMetadataStore()->getPlaceMetadata($to);
if (
!array_key_exists('isFinal', $meta) || false === $meta['isFinal']
) {
!array_key_exists('isFinal', $meta) || false === $meta['isFinal']
) {
$toFinal = false;
}
}
@@ -154,6 +154,14 @@ class WorkflowStepType extends AbstractType
'label' => 'workflow.dest for next steps',
'multiple' => true,
'mapped' => false,
'suggested' => $options['suggested_users'],
])
->add('future_cc_users', PickUserDynamicType::class, [
'label' => 'workflow.cc for next steps',
'multiple' => true,
'mapped' => false,
'required' => false,
'suggested' => $options['suggested_users'],
])
->add('future_dest_emails', ChillCollectionType::class, [
'label' => 'workflow.dest by email',
@@ -200,6 +208,7 @@ class WorkflowStepType extends AbstractType
->setAllowedTypes('transition', 'bool')
->setRequired('entity_workflow')
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
->setDefault('suggested_users', [])
->setDefault('constraints', [
new Callback(
function ($step, ExecutionContextInterface $context, $payload) {
@@ -234,6 +243,20 @@ class WorkflowStepType extends AbstractType
}
}
),
new Callback(
function ($step, ExecutionContextInterface $context, $payload) {
$form = $context->getObject();
foreach($form->get('future_dest_users')->getData() as $u) {
if (in_array($u, $form->get('future_cc_users')->getData(), true)) {
$context
->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
->atPath('ccUsers')
->addViolation();
}
}
}
)
]);
}
}

View File

@@ -17,6 +17,8 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Templating\UI\NotificationCounterInterface;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Doctrine\ORM\Event\PreFlushEventArgs;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Security\Core\User\UserInterface;
@@ -68,7 +70,7 @@ final class NotificationByUserCounter implements NotificationCounterInterface
return 'chill_main_notif_unread_by_' . $user->getId();
}
public function onEditNotificationComment(NotificationComment $notificationComment, LifecycleEventArgs $eventArgs): void
public function onEditNotificationComment(NotificationComment $notificationComment, PostPersistEventArgs $eventArgs): void
{
$this->resetCacheForNotification($notificationComment->getNotification());
}

View File

@@ -14,6 +14,8 @@ namespace Chill\MainBundle\Notification\Email;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\NotificationComment;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Event\PostPersistEventArgs;
use Doctrine\ORM\Event\PostUpdateEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
@@ -36,7 +38,7 @@ class NotificationMailer
$this->translator = $translator;
}
public function postPersistComment(NotificationComment $comment, LifecycleEventArgs $eventArgs): void
public function postPersistComment(NotificationComment $comment, PostPersistEventArgs $eventArgs): void
{
foreach (
array_merge(
@@ -72,13 +74,13 @@ class NotificationMailer
/**
* Send a email after a notification is persisted.
*/
public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
public function postPersistNotification(Notification $notification, PostPersistEventArgs $eventArgs): void
{
$this->sendNotificationEmailsToAddresses($notification);
$this->sendNotificationEmailsToAddressesEmails($notification);
}
public function postUpdateNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
public function postUpdateNotification(Notification $notification, PostUpdateEventArgs $eventArgs): void
{
$this->sendNotificationEmailsToAddressesEmails($notification);
}

View File

@@ -74,7 +74,8 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
}
return $environment->render('@ChillMain/Notification/extension_list_notifications_for.html.twig', [
'notifications' => $notifications, 'appendCommentForms' => $appendCommentForms,
'notifications' => $notifications,
'appendCommentForms' => $appendCommentForms,
]);
}
}

View File

@@ -21,37 +21,36 @@ class Page implements PageInterface
/**
* the number of item per page.
*
* @var int
*/
protected $itemPerPage;
protected int $itemPerPage;
/**
* the number of the current page.
*
* @var int
*/
protected $number;
protected int $number;
/**
* The route for the current page.
*
* @var string
*/
protected $route;
protected string $route;
/**
* Parameters for the route to the current page.
*
* @var array
*/
protected $routeParameters;
protected array $routeParameters;
/**
* The number of items in the whole iteration.
*
* @var int
*/
protected $totalItems;
protected int $totalItems;
/**
* @var UrlGeneratorInterface
@@ -59,12 +58,12 @@ class Page implements PageInterface
protected $urlGenerator;
public function __construct(
$number,
$itemPerPage,
int $number,
int $itemPerPage,
UrlGeneratorInterface $urlGenerator,
$route,
string $route,
array $routeParameters,
$totalItems
int $totalItems
) {
$this->urlGenerator = $urlGenerator;
$this->number = $number;
@@ -74,24 +73,24 @@ class Page implements PageInterface
$this->totalItems = $totalItems;
}
public function generateUrl()
public function generateUrl(): string
{
return $this->urlGenerator->generate($this->route, $this->routeParameters);
}
public function getFirstItemNumber()
public function getFirstItemNumber(): int
{
return ($this->number - 1) * $this->itemPerPage;
}
public function getLastItemNumber()
public function getLastItemNumber(): int
{
$last = $this->number * $this->itemPerPage - 1;
return $last < $this->totalItems ? $last : $this->totalItems;
}
public function getNumber()
public function getNumber(): int
{
return $this->number;
}

View File

@@ -27,27 +27,27 @@ class PageGenerator implements Iterator
$this->paginator = $paginator;
}
public function current()
public function current(): Page
{
return $this->paginator->getPage($current);
return $this->paginator->getPage($this->current);
}
public function key()
public function key(): int
{
return $this->current;
}
public function next()
public function next(): void
{
++$this->current;
}
public function rewind()
public function rewind(): void
{
$this->current = 1;
}
public function valid()
public function valid(): bool
{
return 0 < $this->current
&& $this->paginator->countPages() >= $this->current;

View File

@@ -26,21 +26,21 @@ class Paginator implements PaginatorInterface
*
* @var int
*/
protected $currentPageNumber;
protected int $currentPageNumber;
/**
* the number of items on a single page.
*
* @var int
*/
protected $itemPerPage;
protected int $itemPerPage;
/**
* the key in the GET parameter to indicate the number of item per page.
*
* @var string
*/
protected $itemPerPageKey;
protected string $itemPerPageKey;
/**
* the key in the GET parameter to indicate the page number in
@@ -48,45 +48,45 @@ class Paginator implements PaginatorInterface
*
* @var string
*/
protected $pageKey;
protected string $pageKey;
/**
* the route of the pages.
*
* @var string
*/
protected $route;
protected string $route;
/**
* the parameters of the route.
*
* @var string[]
*/
protected $routeParameters;
protected array $routeParameters;
/**
* The number of total items.
*
* @var int
*/
protected $totalItems;
protected int $totalItems;
/**
* the generator for url.
*
* @var UrlGeneratorInterface
*/
protected $urlGenerator;
protected UrlGeneratorInterface $urlGenerator;
public function __construct(
$totalItems,
$itemPerPage,
$currentPageNumber,
$route,
int $totalItems,
int $itemPerPage,
int $currentPageNumber,
string $route,
array $routeParameters,
UrlGeneratorInterface $urlGenerator,
$pageKey,
$itemPerPageKey
string $pageKey,
string $itemPerPageKey
) {
$this->totalItems = $totalItems;
$this->itemPerPage = $itemPerPage;
@@ -98,12 +98,12 @@ class Paginator implements PaginatorInterface
$this->itemPerPageKey = $itemPerPageKey;
}
public function count()
public function count(): int
{
return $this->countPages();
}
public function countPages()
public function countPages(): int
{
if (0 === $this->itemPerPage) {
return 1;
@@ -122,20 +122,17 @@ class Paginator implements PaginatorInterface
return 0 === $nb ? 1 : (int) $nb;
}
/**
* @return \Chill\MainBundle\Pagination\Page
*/
public function getCurrentPage()
public function getCurrentPage(): Page
{
return $this->getPage($this->currentPageNumber);
}
public function getCurrentPageFirstItemNumber()
public function getCurrentPageFirstItemNumber(): int
{
return $this->getCurrentPage()->getFirstItemNumber();
}
public function getItemsPerPage()
public function getItemsPerPage(): int
{
return $this->itemPerPage;
}
@@ -145,7 +142,7 @@ class Paginator implements PaginatorInterface
*
* @return \Chill\MainBundle\Pagination\Page
*/
public function getNextPage()
public function getNextPage(): Page
{
if (!$this->hasNextPage()) {
throw new RuntimeException('this page has no next page');
@@ -155,11 +152,10 @@ class Paginator implements PaginatorInterface
}
/**
* @param type $number
*
* @return \Chill\MainBundle\Pagination\Page
*/
public function getPage($number)
public function getPage(int $number): Page
{
if (!$this->hasPage($number)) {
throw new RuntimeException("The page with number {$number} does not "
@@ -179,7 +175,7 @@ class Paginator implements PaginatorInterface
);
}
public function getPagesGenerator()
public function getPagesGenerator(): iterable
{
for ($i = 1; $this->countPages() >= $i; ++$i) {
yield $this->getPage($i);
@@ -191,7 +187,7 @@ class Paginator implements PaginatorInterface
*
* @return \Chill\MainBundle\Pagination\Page
*/
public function getPreviousPage()
public function getPreviousPage(): PageInterface
{
if (!$this->hasPreviousPage()) {
throw new RuntimeException('this page has no previous page');
@@ -200,7 +196,7 @@ class Paginator implements PaginatorInterface
return $this->getPage($this->currentPageNumber - 1);
}
public function getTotalItems()
public function getTotalItems(): int
{
return $this->totalItems;
}
@@ -208,12 +204,12 @@ class Paginator implements PaginatorInterface
/**
* @return bool
*/
public function hasNextPage()
public function hasNextPage(): bool
{
return $this->hasPage($this->currentPageNumber + 1);
}
public function hasPage($number)
public function hasPage($number): bool
{
if (0 === $this->totalItems) {
return 1 === $number;
@@ -226,18 +222,18 @@ class Paginator implements PaginatorInterface
/**
* @return bool
*/
public function hasPreviousPage()
public function hasPreviousPage(): bool
{
return $this->hasPage($this->currentPageNumber - 1);
}
public function isCurrentPage(PageInterface $page)
public function isCurrentPage(PageInterface $page): bool
{
return $page->getNumber() === $this->currentPageNumber;
}
public function setItemsPerPage($itemPerPage)
public function setItemsPerPage(int $itemsPerPage)
{
$this->itemPerPage = $itemPerPage;
$this->itemPerPage = $itemsPerPage;
}
}

View File

@@ -124,12 +124,12 @@ class PaginatorFactory
return array_merge(
$this->router->getContext()->getParameters(),
// get the route parameters
$this->requestStack
->getCurrentRequest()
->attributes->get('_route_params'),
$this->requestStack
->getCurrentRequest()
->attributes->get('_route_params'),
// get the query parameters
$this->requestStack
->getCurrentRequest()->query->all()
$this->requestStack
->getCurrentRequest()->query->all()
);
}
}

View File

@@ -32,26 +32,26 @@ interface PaginatorInterface extends Countable
*
* @return int
*/
public function countPages();
public function countPages(): int;
/**
* get the current page.
*
* @return PageInterface
*/
public function getCurrentPage();
public function getCurrentPage(): PageInterface;
/**
* get the first result for the current page.
*
* @return int
*/
public function getCurrentPageFirstItemNumber();
public function getCurrentPageFirstItemNumber(): int;
/*
* get the number of items per page
*/
public function getItemsPerPage();
public function getItemsPerPage(): int;
/**
* get the next page.
@@ -60,7 +60,7 @@ interface PaginatorInterface extends Countable
*
* @return PageInterface
*/
public function getNextPage();
public function getNextPage(): PageInterface;
/**
* get page by his number.
@@ -69,14 +69,14 @@ interface PaginatorInterface extends Countable
*
* @throws RuntimeException if the pagination has no page with specified number
*/
public function getPage($number);
public function getPage(int $number): PageInterface;
/**
* get a generator to generate pages.
*
* @return Generator which return PageInterface elements
*/
public function getPagesGenerator();
public function getPagesGenerator(): iterable;
/**
* get the previous page.
@@ -85,35 +85,35 @@ interface PaginatorInterface extends Countable
*
* @return PageInterface
*/
public function getPreviousPage();
public function getPreviousPage(): PageInterface;
/**
* get the number of results for this paginator.
*
* @return int
*/
public function getTotalItems();
public function getTotalItems(): int;
/**
* check if the current page has a next page.
*
* @return bool
*/
public function hasNextPage();
public function hasNextPage(): bool;
/**
* check if the page with the given number exists.
*
* @param int $number
* @param mixed $number
*/
public function hasPage($number);
public function hasPage($number): bool;
/**
* check if the current page has a page before.
*
* @return bool
*/
public function hasPreviousPage();
public function hasPreviousPage(): bool;
/**
* check if the given page is the current page.
@@ -122,10 +122,10 @@ interface PaginatorInterface extends Countable
*
* @return bool
*/
public function isCurrentPage(PageInterface $page);
public function isCurrentPage(PageInterface $page): bool;
/*
* set the number of items per page
*/
public function setItemsPerPage($itemsPerPage);
public function setItemsPerPage(int $itemsPerPage);
}

View File

@@ -187,7 +187,7 @@ final class PhonenumberHelper implements PhoneNumberHelperInterface
}
// filter only number
$filtered = preg_replace('/[^0-9]/', '', $phonenumber);
$filtered = preg_replace('/[^0-9]/', '', (string) $phonenumber);
$item = $this->cachePool->getItem('pnum_' . $filtered);

View File

@@ -11,20 +11,58 @@ declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\GeographicalUnit;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
class GeographicalUnitRepository implements GeographicalUnitRepositoryInterface
final class GeographicalUnitRepository implements GeographicalUnitRepositoryInterface
{
private EntityManagerInterface $em;
private EntityRepository $repository;
public function __construct(EntityManagerInterface $em)
{
$this->repository = $em->getRepository($this->getClassName());
$this->em = $em;
}
public function countGeographicalUnitContainingAddress(Address $address): int
{
$qb = $this->buildQueryGeographicalUnitContainingAddress($address);
return $qb
->select('COUNT(gu)')
->getQuery()
->getSingleScalarResult();
}
public function findGeographicalUnitContainingAddress(Address $address, int $offset = 0, int $limit = 50): array
{
$qb = $this->buildQueryGeographicalUnitContainingAddress($address);
return $qb
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class))
->addOrderBy('IDENTITY(gu.layer)')
->addOrderBy(('gu.unitName'))
->getQuery()
->setFirstResult($offset)
->setMaxResults($limit)
->getResult();
}
private function buildQueryGeographicalUnitContainingAddress(Address $address): QueryBuilder
{
$qb = $this->repository
->createQueryBuilder('gu')
;
return $qb
->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class))
->innerJoin(Address::class, 'address', Join::WITH, 'ST_CONTAINS(gu.geom, address.point) = TRUE')
->where($qb->expr()->eq('address', ':address'))
->setParameter('address', $address)
;
}
public function find($id): ?GeographicalUnit

View File

@@ -11,8 +11,23 @@ declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO;
use Doctrine\Persistence\ObjectRepository;
interface GeographicalUnitRepositoryInterface extends ObjectRepository
{
/**
* Return the geographical units as @link{SimpleGeographicalUnitDTO} whithin the address is contained.
*
* This query is executed in real time (without the refresh of the materialized view which load the addresses).
*
* @param Address $address
* @param int $offset
* @param int $limit
* @return SimpleGeographicalUnitDTO[]
*/
public function findGeographicalUnitContainingAddress(Address $address, int $offset = 0, int $limit = 50): array;
public function countGeographicalUnitContainingAddress(Address $address): int;
}

View File

@@ -27,6 +27,13 @@ class EntityWorkflowRepository implements ObjectRepository
$this->repository = $entityManager->getRepository(EntityWorkflow::class);
}
public function countByCc(User $user): int
{
$qb = $this->buildQueryByCc($user)->select('count(ew)');
return (int) $qb->getQuery()->getSingleScalarResult();
}
public function countByDest(User $user): int
{
$qb = $this->buildQueryByDest($user)->select('count(ew)');
@@ -103,6 +110,19 @@ class EntityWorkflowRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findByCc(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->buildQueryByCc($user)->select('ew');
foreach ($orderBy as $key => $sort) {
$qb->addOrderBy('ew.' . $key, $sort);
}
$qb->setMaxResults($limit)->setFirstResult($offset);
return $qb->getQuery()->getResult();
}
public function findByDest(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
{
$qb = $this->buildQueryByDest($user)->select('ew');
@@ -165,6 +185,25 @@ class EntityWorkflowRepository implements ObjectRepository
return EntityWorkflow::class;
}
private function buildQueryByCc(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('ew');
$qb->join('ew.steps', 'step');
$qb->where(
$qb->expr()->andX(
$qb->expr()->isMemberOf(':user', 'step.ccUser'),
$qb->expr()->isNull('step.transitionAfter'),
$qb->expr()->eq('step.isFinal', "'FALSE'")
)
);
$qb->setParameter('user', $user);
return $qb;
}
private function buildQueryByDest(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('ew');

View File

@@ -0,0 +1,35 @@
import {Address, GeographicalUnitLayer, SimpleGeographicalUnit} from "../../types";
import {fetchResults, makeFetch} from "./apiMethods";
export const getAddressById = async (address_id: number): Promise<Address> =>
{
const url = `/api/1.0/main/address/${address_id}.json`;
const response = await fetch(url);
if (response.ok) {
return response.json();
}
throw Error('Error with request resource response');
};
export const getGeographicalUnitsByAddress = async (address: Address): Promise<SimpleGeographicalUnit[]> => {
return fetchResults<SimpleGeographicalUnit>(`/api/1.0/main/geographical-unit/by-address/${address.address_id}.json`);
}
export const getAllGeographicalUnitLayers = async (): Promise<GeographicalUnitLayer[]> => {
return fetchResults<GeographicalUnitLayer>(`/api/1.0/main/geographical-unit-layer.json`);
}
export const syncAddressWithReference = async (address: Address): Promise<Address> => {
return makeFetch<null, Address>("POST", `/api/1.0/main/address/reference-match/${address.address_id}/sync-with-reference`);
}
export const markAddressReviewed = async (address: Address): Promise<Address> => {
return makeFetch<null, Address>("POST", `/api/1.0/main/address/reference-match/${address.address_id}/set/reviewed`);
}
export const markAddressToReview = async (address: Address): Promise<Address> => {
return makeFetch<null, Address>("POST", `/api/1.0/main/address/reference-match/${address.address_id}/set/to_review`);
}

View File

@@ -67,9 +67,6 @@ export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL
},
};
console.log('for url '+url, body);
console.log('for url '+url, body !== null);
if (body !== null && typeof body !== 'undefined') {
Object.assign(opts, {body: JSON.stringify(body)})
}
@@ -77,9 +74,6 @@ export const makeFetch = <Input, Output>(method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL
if (typeof options !== 'undefined') {
opts = Object.assign(opts, options);
}
console.log('will fetch', url);
console.log('content for ' + url, opts);
return fetch(url, opts)
.then(response => {
if (response.ok) {

View File

@@ -0,0 +1,39 @@
import AddressDetailsButton from "../../vuejs/_components/AddressDetails/AddressDetailsButton.vue";
import {createApp} from "vue";
import {createI18n} from "vue-i18n";
import {_createI18n} from "../../vuejs/_js/i18n";
import {Address} from "../../types";
const i18n = _createI18n({});
document.querySelectorAll<HTMLSpanElement>('span[data-address-details]').forEach((el) => {
const dataset = el.dataset as {
addressId: string,
addressRefStatus: string,
};
const app = createApp({
components: {AddressDetailsButton},
data() {
return {
addressId: Number.parseInt(dataset.addressId),
addressRefStatus: dataset.addressRefStatus,
}
},
template: '<address-details-button :address_id="addressId" :address_ref_status="addressRefStatus" @update-address="onUpdateAddress"></address-details-button>',
methods: {
onUpdateAddress: (address: Address): void => {
if (address.refStatus === 'to_review' || address.refStatus === 'reviewed') {
// in this two case, the address content do not change
return;
}
if (window.confirm("L'adresse a été modifiée. Vous pouvez continuer votre travail. Cependant, pour afficher les données immédiatement, veuillez recharger la page. \n\n Voulez-vous recharger la page immédiatement ?")) {
window.location.reload();
}
}
}
});
app.use(i18n);
app.mount(el);
});

View File

@@ -18,12 +18,13 @@ function loadDynamicPicker(element) {
isMultiple = parseInt(el.dataset.multiple) === 1,
uniqId = el.dataset.uniqid,
input = element.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
// the "picked" will always be an array, even if multiple is false
picked = isMultiple ?
JSON.parse(input.value) : (
(input.value === '[]' || input.value === '') ?
null : [ JSON.parse(input.value) ]
)
;
suggested = JSON.parse(el.dataset.suggested)
if (!isMultiple) {
if (input.value === '[]'){
@@ -37,6 +38,7 @@ function loadDynamicPicker(element) {
':types="types" ' +
':picked="picked" ' +
':uniqid="uniqid" ' +
':suggested="notPickedSuggested" ' +
'@addNewEntity="addNewEntity" ' +
'@removeEntity="removeEntity"></pick-entity>',
components: {
@@ -48,16 +50,27 @@ function loadDynamicPicker(element) {
types: JSON.parse(el.dataset.types),
picked: picked === null ? [] : picked,
uniqid: el.dataset.uniqid,
suggested: suggested
}
},
computed: {
notPickedSuggested() {
const pickedIds = new Set();
for (const p of this.picked) {
pickedIds.add(`${p.type}${p.id}`);
}
return this.suggested.filter(e => !pickedIds.has(`${e.type}${e.id}`))
}
},
methods: {
addNewEntity(entity) {
addNewEntity({entity}) {
if (this.multiple) {
if (!this.picked.some(el => {
return el.type === entity.type && el.id === entity.id;
})) {
this.picked.push(entity);
input.value = JSON.stringify(this.picked);
console.log(entity)
}
} else {
if (!this.picked.some(el => {
@@ -69,9 +82,16 @@ function loadDynamicPicker(element) {
}
}
},
removeEntity(entity) {
removeEntity({entity}) {
if (-1 === this.suggested.findIndex(e => e.type === entity.type && e.id === entity.id)) {
this.suggested.push(entity);
}
this.picked = this.picked.filter(e => !(e.type === entity.type && e.id === entity.id));
input.value = JSON.stringify(this.picked);
if (this.multiple) {
input.value = JSON.stringify(this.picked);
} else {
input.value = "";
}
},
}
})

View File

@@ -70,6 +70,8 @@ export interface Country {
code: string;
}
export type AddressRefStatus = 'match'|'to_review'|'reviewed';
export interface Address {
type: "address";
address_id: number;
@@ -90,6 +92,13 @@ export interface Address {
addressReference: AddressReference | null;
validFrom: DateTime;
validTo: DateTime | null;
point: Point | null;
refStatus: AddressRefStatus;
isNoAddress: boolean;
}
export interface AddressWithPoint extends Address {
point: Point
}
export interface AddressReference {
@@ -106,6 +115,19 @@ export interface AddressReference {
updatedAt: DateTime | null;
}
export interface SimpleGeographicalUnit {
id: number;
layerId: number;
unitName: string;
unitRefId: string;
}
export interface GeographicalUnitLayer {
id: number;
name: TranslatableString;
refId: string;
}
export interface Location {
type: "location";
id: number;

View File

@@ -1,3 +1,5 @@
import {getAddressById} from 'ChillMainAssets/lib/api/address';
/**
* Endpoint chill_api_single_country__index
* method GET, get Country Object
@@ -188,13 +190,7 @@ const postPostalCode = (postalCode) => { //<--
* @returns {Promise} a promise containing a Address object
*/
const getAddress = (id) => {
//console.log('<< get address');
const url = `/api/1.0/main/address/${id}.json`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
return getAddressById(id);
};
export {

View File

@@ -59,7 +59,7 @@
v-bind:insideModal="false"
@pick-address="this.pickAddress"
ref="suggestAddress">
<template v-slot:before v-if="!bypassFirstStep">
<a class="btn btn-cancel" @click="resetPane">
{{ $t('action.cancel') }}
@@ -73,7 +73,7 @@
</button>
</li>
</template>
</suggest-pane>
</div>
</template>
@@ -133,7 +133,7 @@
v-bind:insideModal="false"
@getCities="getCities"
@getReferenceAddresses="getReferenceAddresses">
<template v-slot:before>
<a class="btn btn-cancel" @click="resetPane">
{{ $t('action.cancel') }}
@@ -152,7 +152,7 @@
</button>
</li>
</template>
</edit-pane>
</div>
</template>
@@ -206,7 +206,7 @@
v-bind:flag="this.flag"
v-bind:insideModal="false"
ref="dateAddress">
<template v-slot:before>
<button class="btn btn-misc" @click="openEditPane">
<i class="fa fa-fw fa-arrow-left"></i>
@@ -220,7 +220,7 @@
</button>
</li>
</template>
</date-pane>
</div>
</template>
@@ -580,15 +580,15 @@ export default {
this.entity.selected.city = this.context.edit ? this.entity.address.postcode : {};
this.entity.selected.address = {};
this.entity.selected.address.street = this.context.edit ? this.entity.address.street: null;
this.entity.selected.address.streetNumber = this.context.edit ? this.entity.address.streetNumber: null;
this.entity.selected.address.floor = this.context.edit ? this.entity.address.floor: null;
this.entity.selected.address.corridor = this.context.edit ? this.entity.address.corridor: null;
this.entity.selected.address.steps = this.context.edit ? this.entity.address.steps: null;
this.entity.selected.address.flat = this.context.edit ? this.entity.address.flat: null;
this.entity.selected.address.buildingName = this.context.edit ? this.entity.address.buildingName: null;
this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: null;
this.entity.selected.address.extra = this.context.edit ? this.entity.address.extra: null;
this.entity.selected.address.street = this.context.edit ? this.entity.address.street: '';
this.entity.selected.address.streetNumber = this.context.edit ? this.entity.address.streetNumber: '';
this.entity.selected.address.floor = this.context.edit ? this.entity.address.floor: '';
this.entity.selected.address.corridor = this.context.edit ? this.entity.address.corridor: '';
this.entity.selected.address.steps = this.context.edit ? this.entity.address.steps: '';
this.entity.selected.address.flat = this.context.edit ? this.entity.address.flat: '';
this.entity.selected.address.buildingName = this.context.edit ? this.entity.address.buildingName: '';
this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: '';
this.entity.selected.address.extra = this.context.edit ? this.entity.address.extra: '';
this.entity.selected.writeNew.address = this.context.edit && this.entity.address.addressReference === null && this.entity.address.street.length > 0
this.entity.selected.writeNew.postcode = false // NB: this used to be this.context.edit, but think it was erroneous;

View File

@@ -46,8 +46,7 @@
:class="{'active': activeTab === 'MyTasks'}"
@click="selectTab('MyTasks')">
{{ $t('my_tasks.tab') }}
<tab-counter :count="state.tasks.warning.count"></tab-counter>
<tab-counter :count="state.tasks.alert.count"></tab-counter>
<tab-counter :count="state.tasks.warning.count + state.tasks.alert.count"></tab-counter>
</a>
</li>
<li class="nav-item">
@@ -55,7 +54,7 @@
:class="{'active': activeTab === 'MyWorkflows'}"
@click="selectTab('MyWorkflows')">
{{ $t('my_workflows.tab') }}
<tab-counter :count="state.workflows.count"></tab-counter>
<tab-counter :count="state.workflows.count + state.workflowsCc.count"></tab-counter>
</a>
</li>
<li class="nav-item loading ms-auto py-2" v-if="loading">
@@ -150,4 +149,4 @@ export default {
a.nav-link {
cursor: pointer;
}
</style>
</style>

View File

@@ -1,88 +1,25 @@
<template>
<div class="alert alert-light">{{ $t('my_workflows.description') }}</div>
<my-workflows-table :workflows="workflows" />
<div class="alert alert-light">{{ $t('my_workflows.description') }}</div>
<span v-if="noResults" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">{{ $t('Object_workflow') }}</th>
<th scope="col">{{ $t('Step') }}</th>
<th scope="col">{{ $t('concerned_users') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(w, i) in workflows.results" :key="`workflow-${i}`">
<td>{{ w.title }}</td>
<td>
<div class="workflow">
<div class="breadcrumb">
<i class="fa fa-circle me-1 text-chill-yellow mx-2"></i>
<span class="mx-2">{{ getStep(w) }}</span>
</div>
</div>
</td>
<td v-if="w.datas.persons !== null">
<span v-for="p in w.datas.persons" class="me-1" :key="p.id">
<on-the-fly
:type="p.type"
:id="p.id"
:buttonText="p.textAge"
:displayBadge="'true' === 'true'"
action="show">
</on-the-fly>
</span>
</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(w)">
{{ $t('show_entity', { entity: $t('the_workflow') }) }}
</a>
</td>
</tr>
</template>
</tab-table>
<div class="alert alert-light">{{ $t('my_workflows.description_cc') }}</div>
<my-workflows-table :workflows="workflowsCc" />
</template>
<script>
import { mapState, mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
import { mapState } from "vuex";
import MyWorkflowsTable from './MyWorkflowsTable.vue';
export default {
name: "MyWorkflows",
components: {
TabTable,
OnTheFly
MyWorkflowsTable
},
computed: {
...mapState([
'workflows',
'workflowsCc',
]),
...mapGetters([
'isWorkflowsLoaded',
]),
noResults() {
if (!this.isWorkflowsLoaded) {
return false;
} else {
return this.workflows.count === 0;
}
},
},
methods: {
getUrl(w) {
return `/fr/main/workflow/${w.id}/show`;
},
getStep(w) {
const lastStep = w.steps.length - 1
return w.steps[lastStep].currentStep.text;
}
},
}
</script>
<style scoped>
span.outdated {
font-weight: bold;
color: var(--bs-warning);
}
</style>
</script>

View File

@@ -0,0 +1,83 @@
<template>
<span v-if="hasNoResults(workflows)" class="chill-no-data-statement">{{ $t('no_data') }}</span>
<tab-table v-else>
<template v-slot:thead>
<th scope="col">{{ $t('Object_workflow') }}</th>
<th scope="col">{{ $t('Step') }}</th>
<th scope="col">{{ $t('concerned_users') }}</th>
<th scope="col"></th>
</template>
<template v-slot:tbody>
<tr v-for="(w, i) in workflows.results" :key="`workflow-${i}`">
<td>{{ w.title }}</td>
<td>
<div class="workflow">
<div class="breadcrumb">
<i class="fa fa-circle me-1 text-chill-yellow mx-2"></i>
<span class="mx-2">{{ getStep(w) }}</span>
</div>
</div>
</td>
<td v-if="w.datas.persons !== null">
<span v-for="p in w.datas.persons" class="me-1" :key="p.id">
<on-the-fly
:type="p.type"
:id="p.id"
:buttonText="p.textAge"
:displayBadge="'true' === 'true'"
action="show">
</on-the-fly>
</span>
</td>
<td>
<a class="btn btn-sm btn-show" :href="getUrl(w)">
{{ $t('show_entity', { entity: $t('the_workflow') }) }}
</a>
</td>
</tr>
</template>
</tab-table>
</template>
<script>
import { mapGetters } from "vuex";
import TabTable from "./TabTable";
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly';
export default {
name: "MyWorkflows",
components: {
TabTable,
OnTheFly
},
props: ['workflows'],
computed: {
...mapGetters([
'isWorkflowsLoaded',
]),
},
methods: {
hasNoResults(workflows) {
if (!this.isWorkflowsLoaded) {
return false;
} else {
return workflows.count === 0;
}
},
getUrl(w) {
return `/fr/main/workflow/${w.id}/show`;
},
getStep(w) {
const lastStep = w.steps.length - 1
return w.steps[lastStep].currentStep.text;
}
},
}
</script>
<style scoped>
span.outdated {
font-weight: bold;
color: var(--bs-warning);
}
</style>

View File

@@ -24,7 +24,8 @@ const appMessages = {
},
my_workflows: {
tab: "Mes workflows",
description: "Liste des workflows en attente d'une action."
description: "Liste des workflows en attente d'une action.",
description_cc: "Liste des workflows dont je suis en copie."
},
opening_date: "Date d'ouverture",
social_issues: "Problématiques sociales",

View File

@@ -22,6 +22,7 @@ const store = createStore({
accompanyingCourses: {},
notifications: {},
workflows: {},
workflowsCc: {},
errorMsg: [],
loading: false
},
@@ -87,6 +88,9 @@ const store = createStore({
addWorkflows(state, workflows) {
state.workflows = workflows;
},
addWorkflowsCc(state, workflows) {
state.workflowsCc = workflows;
},
setLoading(state, bool) {
state.loading = bool;
},
@@ -195,17 +199,23 @@ const store = createStore({
case 'MyWorkflows':
if (!getters.isWorflowsLoaded) {
commit('setLoading', true);
const url = '/api/1.0/main/workflow/my';
makeFetch('GET', url)
.then((response) => {
console.log('workflows', response)
commit('addWorkflows', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
});
makeFetch('GET', '/api/1.0/main/workflow/my')
.then((response) => {
commit('addWorkflows', response);
makeFetch('GET', '/api/1.0/main/workflow/my-cc')
.then((response) => {
commit('addWorkflowsCc', response);
commit('setLoading', false);
})
.catch((error) => {
commit('catchError', error);
throw error;
});
})
.catch((error) => {
commit('catchError', error);
throw error;
});
}
break;
default:

View File

@@ -233,7 +233,7 @@ export default {
// console.log('data original', data);
data.parent = {type: "thirdparty", id: this.parent.id};
data.civility = data.civility !== null ? {type: 'chill_main_civility', id: data.civility.id} : null;
data.profession = data.profession !== null ? {type: 'third_party_profession', id: data.profession.id} : null;
data.profession = data.profession !== '' ? data.profession : '';
} else {
type = this.$refs.castNew.radioType;
data = this.$refs.castNew.castDataByType();
@@ -241,8 +241,8 @@ export default {
if (typeof data.civility !== 'undefined' && null !== data.civility) {
data.civility = data.civility !== null ? {type: 'chill_main_civility', id: data.civility.id} : null;
}
if (typeof data.profession !== 'undefined' && null !== data.profession) {
data.profession = data.profession !== null ? {type: 'third_party_profession', id: data.profession.id} : null;
if (typeof data.profession !== 'undefined' && '' !== data.profession) {
data.profession = data.profession !== '' ? data.profession : '';
}
// console.log('onthefly data', data);
}

View File

@@ -17,6 +17,9 @@
</add-persons>
</li>
</ul>
<ul class="list-suggest add-items inline">
<li v-for="s in suggested" :key="s.id" @click="addNewSuggested(s)"><span>{{ s.text }}</span></li>
</ul>
</template>
<script>
@@ -49,6 +52,10 @@ export default {
// display picked entities.
type: Boolean,
default: true,
},
suggested: {
type: Array,
default: []
}
},
emits: ['addNewEntity', 'removeEntity'],
@@ -61,55 +68,58 @@ export default {
};
},
computed: {
addPersonsOptions() {
return {
uniq: !this.multiple,
type: this.types,
priority: null,
button: {
size: 'btn-sm',
class: 'btn-submit',
},
};
},
translatedListOfTypes() {
let trans = [];
this.types.forEach(t => {
if (this.$props.multiple) {
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
} else {
trans.push(appMessages.fr.pick_entity[t + '_one'].toLowerCase());
}
})
addPersonsOptions() {
return {
uniq: !this.multiple,
type: this.types,
priority: null,
button: {
size: 'btn-sm',
class: 'btn-submit',
},
};
},
translatedListOfTypes() {
let trans = [];
this.types.forEach(t => {
if (this.$props.multiple) {
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
} else {
trans.push(appMessages.fr.pick_entity[t + '_one'].toLowerCase());
}
})
if (this.$props.multiple) {
return appMessages.fr.pick_entity.modal_title + trans.join(', ');
} else {
return appMessages.fr.pick_entity.modal_title_one + trans.join(', ');
}
},
listClasses() {
return {
'list-suggest': true,
'remove-items': this.$props.removableIfSet,
};
},
if (this.$props.multiple) {
return appMessages.fr.pick_entity.modal_title + trans.join(', ');
} else {
return appMessages.fr.pick_entity.modal_title_one + trans.join(', ');
}
},
listClasses() {
return {
'list-suggest': true,
'remove-items': this.$props.removableIfSet,
};
},
},
methods: {
addNewEntity({ selected, modal }) {
selected.forEach((item) => {
this.$emit('addNewEntity', item.result);
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
},
removeEntity(entity) {
if (!this.$props.removableIfSet) {
return;
}
this.$emit('removeEntity', entity);
}
addNewSuggested(entity) {
this.$emit('addNewEntity', {entity: entity});
},
addNewEntity({ selected, modal }) {
selected.forEach((item) => {
this.$emit('addNewEntity', { entity: item.result});
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
},
removeEntity(entity) {
if (!this.$props.removableIfSet) {
return;
}
this.$emit('removeEntity',{ entity: entity });
}
},
}
</script>

View File

@@ -0,0 +1,68 @@
<template>
<span v-if="data.working_ref_status === 'to_review'" class="badge bg-danger address-details-button-warning">L'adresse de référence a été modifiée</span>
<a v-if="data.loading === false" @click.prevent="clickOrOpen" class="btn btn-sm btn-misc">
<span class="fa fa-map address-details-button"></span>
</a>
<span v-if="data.loading" class="fa fa-spin fa-spinner "></span>
<AddressModal :address="data.working_address" @update-address="onUpdateAddress" ref="address_modal"></AddressModal>
</template>
<script lang="ts" setup>
import {Address, AddressRefStatus} from "../../../types";
import {onMounted, reactive, ref} from "vue";
import {getAddressById} from "../../../lib/api/address";
import AddressModal from "./AddressModal.vue";
export interface AddressModalContentProps {
address_id: number;
address_ref_status: AddressRefStatus | null;
}
const data = reactive<{
loading: boolean,
working_address: Address | null,
working_ref_status: AddressRefStatus | null,
}>({
loading: false,
working_address: null,
working_ref_status: null,
});
const props = defineProps<AddressModalContentProps>();
const emit = defineEmits<{
(e: 'update-address', value: Address): void
}>();
const address_modal = ref<InstanceType<typeof AddressModal> | null>(null);
onMounted(() => {
data.working_ref_status = props.address_ref_status;
});
async function clickOrOpen(): Promise<void> {
if (data.working_address === null) {
data.loading = true;
data.working_address = await getAddressById(props.address_id);
data.working_ref_status = data.working_address.refStatus;
data.loading = false;
}
// open the modal
address_modal.value?.open();
}
const onUpdateAddress = (address: Address): void => {
data.working_address = address;
data.working_ref_status = address.refStatus;
emit('update-address', address);
}
</script>
<style scoped lang="scss">
.address-details-button-warning {
display: inline-block;
margin-right: 0.3rem;
}
</style>

View File

@@ -0,0 +1,33 @@
<template>
<address-render-box :address="props.address" :show-button-details="false"></address-render-box>
<address-details-ref-matching :address="props.address" @update-address="onUpdateAddress"></address-details-ref-matching>
<address-details-map :address="props.address"></address-details-map>
<address-details-geographical-layers :address="props.address"></address-details-geographical-layers>
</template>
<script lang="ts" setup>
import {Address} from "../../../types";
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue";
import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue";
interface AddressModalContentProps {
address: Address,
}
const props = defineProps<AddressModalContentProps>();
const emit = defineEmits<{
(e: 'update-address', value: Address): void
}>();
const onUpdateAddress = (address: Address): void => {
emit('update-address', address);
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,57 @@
<template>
<teleport to="body">
<modal v-if="state.show_modal" @close="close">
<template v-slot:header>
<h2>Détails d'une adresse</h2>
</template>
<template v-slot:body>
<address-details-content :address="props.address" @update-address="onUpdateAddress"></address-details-content>
</template>
</modal>
</teleport>
</template>
<script lang="ts" setup>
import {reactive, ref} from "vue";
import Modal from 'ChillMainAssets/vuejs/_components/Modal.vue';
import {Address} from "../../../types";
import AddressDetailsContent from "./AddressDetailsContent.vue";
interface AddressModalProps {
address: Address
}
interface AddressModalState {
show_modal: boolean,
}
const props = defineProps<AddressModalProps>();
const emit = defineEmits<{
(e: 'update-address', value: Address): void
}>();
const state: AddressModalState = reactive({show_modal: false});
const open = (): void => {
state.show_modal = true;
}
const close = (): void => {
state.show_modal = false;
}
const onUpdateAddress = (address: Address): void => {
emit('update-address', address);
}
defineExpose({
close,
open,
});
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,55 @@
<template>
<template v-for="container in data.containers">
<h4>{{ container.layer.name.fr }}</h4>
<ul>
<li v-for="unit in container.units">{{ unit.unitName }} ({{ unit.unitRefId }})</li>
</ul>
</template>
</template>
<script lang="ts" setup>
import {Address, GeographicalUnitLayer, SimpleGeographicalUnit} from "../../../../types";
import {getGeographicalUnitsByAddress, getAllGeographicalUnitLayers} from "../../../../lib/api/address";
import {onMounted, reactive} from "vue";
export interface AddressDetailsGeographicalLayersProp {
address: Address
};
interface GeographicalUnitContainer {
layer: GeographicalUnitLayer;
units: SimpleGeographicalUnit[];
}
const props = defineProps<AddressDetailsGeographicalLayersProp>();
const data: {
containers: GeographicalUnitContainer[]
} = reactive({
containers: []
});
onMounted(async () => {
const [units, layers] = await Promise.all([
getGeographicalUnitsByAddress(props.address),
getAllGeographicalUnitLayers()
]) as [SimpleGeographicalUnit[], GeographicalUnitLayer[]];
for (let layer of layers) {
let us = units.filter((u) => u.layerId === layer.id);
if (us.length > 0) {
data.containers.push({
layer,
units: us
});
}
}
})
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,90 @@
<template>
<div v-if="props.address.isNoAddress" class="alert alert-info">
Cette adresse est incomplète. La position géographique est approximative.
</div>
<div v-if="props.address.point !== null" class="address_details_map" ref="map_div"></div>
<p>Voir sur <a :href="makeUrlGoogleMap(props.address)" target="_blank">Google Maps</a> <a :href="makeUrlOsm(props.address)" target="_blank">OSM</a></p>
</template>
<script lang="ts" setup>
import {onMounted, ref} from "vue";
import 'leaflet/dist/leaflet.css';
import markerIconPng from "leaflet/dist/images/marker-icon.png";
import L, {LatLngExpression, LatLngTuple} from "leaflet";
import {Address, Point} from "../../../../types";
const lonLatForLeaflet = (point: Point): LatLngTuple => {
return [point.coordinates[1], point.coordinates[0]];
}
export interface MapProps {
address: Address,
}
const props = defineProps<MapProps>();
const map_div = ref<HTMLDivElement | null>(null)
let map: L.Map|null = null;
let marker: L.Marker|null = null;
onMounted(() => {
if (map_div.value === null) {
// there is no map div when the address does not have any Point
return;
}
if (props.address.point !== null) {
map = L.map(map_div.value);
map.setView(lonLatForLeaflet(props.address.point), 18);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
const markerIcon = L.icon({
iconUrl: markerIconPng,
iconAnchor: [12, 41],
});
marker = L.marker(lonLatForLeaflet(props.address.point), {icon: markerIcon});
marker.addTo(map);
}
});
const makeUrlGoogleMap = (address: Address): string => {
const params = new URLSearchParams();
params.append('api', '1');
if (address.point !== null && address.addressReference !== null) {
params.append('query', `${address.point.coordinates[1]} ${address.point.coordinates[0]}`);
} else {
params.append('query', address.lines.join(', '));
}
return `https://www.google.com/maps/search/?${params.toString()}`;
}
const makeUrlOsm = (address: Address): string => {
if (address.point !== null && address.addressReference !== null) {
const params = new URLSearchParams();
params.append('mlat', `${address.point.coordinates[1]}`);
params.append('mlon', `${address.point.coordinates[0]}`);
const hashParams = new URLSearchParams();
hashParams.append('map', `18/${address.point.coordinates[1]}/${address.point.coordinates[0]}`);
return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
}
const params = new URLSearchParams();
params.append('query', address.lines.join(', '));
return `https://www.openstreetmap.org/search?${params.toString()}`;
}
</script>
<style scoped>
div.address_details_map {
height: 250px;
}
</style>

View File

@@ -0,0 +1,96 @@
<template>
<template v-if="props.address.refStatus !== 'match'">
<div v-if="props.address.refStatus === 'to_review' || props.address.refStatus === 'reviewed'" :class="{alert: true, 'alert-danger': props.address.refStatus === 'to_review', 'alert-warning': props.address.refStatus === 'reviewed'}">
<p v-if="props.address.refStatus === 'to_review'"><i class="fa fa-warning"></i> L'adresse de référence a été modifiée.</p>
<p v-if="props.address.refStatus === 'reviewed'">L'adresse est conservée, mais diffère de l'adresse de référence.</p>
<template v-if="props.address.addressReference.street !== props.address.street || props.address.addressReference.streetNumber !== props.address.streetNumber">
<template v-if="props.address.country.code === 'BE'">
<div class="difference">
<span class="old">{{ props.address.street }} {{props.address.streetNumber}}</span>
<span class="new">{{ props.address.addressReference.street }} {{ props.address.addressReference.streetNumber }}</span>
</div>
</template>
<template v-else>
<div class="difference">
<span class="old">{{props.address.streetNumber}} {{ props.address.street }}</span>
<span class="new">{{ props.address.addressReference.streetNumber }} {{ props.address.addressReference.street }}</span>
</div>
</template>
</template>
<template v-if="props.address.addressReference.postcode.id !== props.address.postcode.id">
<div class="difference">
<span class="old">{{ props.address.postcode.code }} {{props.address.postcode.name }}</span>
<span class="new">{{ props.address.addressReference.postcode.code }} {{ props.address.addressReference.postcode.name }}</span>
</div>
</template>
<template v-if="props.address.point !== null && (props.address.point.coordinates[0] !== props.address.addressReference.point.coordinates[0] || props.address.point.coordinates[1] !== props.address.addressReference.point.coordinates[1])">
<div class="difference">
<span class="old">{{ props.address.point.coordinates[0] }} {{ props.address.point.coordinates[1]}}</span>
<span class="new">{{ props.address.addressReference.point.coordinates[0] }} {{ props.address.addressReference.point.coordinates[1]}}</span>
</div>
</template>
<ul class="record_actions">
<li v-if="props.address.refStatus === 'to_review'"><button class="btn btn-sm btn-update" @click="applyUpdate">Appliquer les modifications</button></li>
<li v-if="props.address.refStatus === 'to_review'"><button class="btn btn-sm btn-primary" @click="keepCurrentAddress">Conserver</button></li>
<li v-if="props.address.refStatus === 'reviewed'"><button class="btn btn-sm btn-primary" @click="backToReview">-examiner</button></li>
</ul>
</div>
</template>
</template>
<script lang="ts" setup>
import {Address} from "../../../../types";
import {markAddressReviewed, markAddressToReview, syncAddressWithReference} from "../../../../lib/api/address";
export interface AddressDetailsRefMatchingProps {
address: Address;
}
const props = defineProps<AddressDetailsRefMatchingProps>();
const emit = defineEmits<{
(e: 'update-address', value: Address): void
}>();
const applyUpdate = async () => {
const new_address = await syncAddressWithReference(props.address);
emit('update-address', new_address);
}
const keepCurrentAddress = async () => {
const new_address = await markAddressReviewed(props.address);
emit("update-address", new_address);
}
const backToReview = async () => {
const new_address = await markAddressToReview(props.address);
emit("update-address", new_address);
}
</script>
<style scoped lang="scss">
.difference {
margin-bottom: 0.5rem;
span {
display: block;
}
.old {
text-decoration: red line-through;
}
.new {
font-weight: bold;
color: green;
}
}
</style>

View File

@@ -11,17 +11,18 @@
<p v-for="(l, i) in address.lines" :key="`line-${i}`">
{{ l }}
</p>
<p v-if="showButtonDetails"><address-details-button :address_id="address.address_id" :address_ref_status="address.refStatus"></address-details-button></p>
</div>
<div v-else>
<p v-if="address.text"
<p v-if="'' !== address.text"
class="street">
{{ address.text }}
</p>
<p v-if="address.postcode"
<p v-if="null !== address.postcode"
class="postcode">
{{ address.postcode.code }} {{ address.postcode.name }}
</p>
<p v-if="address.country"
<p v-if="null !== address.country"
class="country">
{{ address.country.name.fr }}
</p>
@@ -35,11 +36,12 @@
<p v-for="(l, i) in address.lines" :key="`line-${i}`">
{{ l }}
</p>
<p v-if="showButtonDetails"><address-details-button :address_id="address.address_id" :address_ref_status="address.refStatus"></address-details-button></p>
</div>
<div v-else>
<p v-if="address.text"
class="street">
{{ address.text }}
{{ address.text }} <template v-if="showButtonDetails"><address-details-button :address_id="address.address_id" :address_ref_status="address.refStatus"></address-details-button></template>
</p>
</div>
</div>
@@ -65,11 +67,13 @@
<script>
import Confidential from 'ChillMainAssets/vuejs/_components/Confidential.vue';
import AddressDetailsButton from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsButton.vue";
export default {
name: 'AddressRenderBox',
components: {
Confidential
Confidential,
AddressDetailsButton,
},
props: {
address: {
@@ -82,6 +86,10 @@ export default {
useDatePane: {
default: false,
type: Boolean
},
showButtonDetails: {
default: true,
type: Boolean
}
},
computed: {

View File

@@ -69,6 +69,7 @@
<i class="fa fa-li fa-map-marker"></i>
{% endif %}
{{ _self.inline(address, options, streetLine, lines) }}
<span data-address-details="1" data-address-id="{{ address.id|escape('html_attr') }}" data-address-ref-status="{{ address.refStatus|escape('html_attr') }}" ></span>
</li>
{%- endif -%}
@@ -78,6 +79,7 @@
<i class="fa fa-fw fa-map-marker"></i>
{% endif %}
{{ _self.inline(address, options, streetLine, lines) }}
<span data-address-details="1" data-address-id="{{ address.id|escape('html_attr') }}" data-address-ref-status="{{ address.refStatus|escape('html_attr') }}"></span>
</span>
{%- endif -%}
@@ -102,6 +104,7 @@
<div class="noaddress">
{{ 'address.consider homeless'|trans }}
</div>
<p><span data-address-details="1" data-address-id="{{ address.id|escape('html_attr') }}" data-address-ref-status="{{ address.refStatus|escape('html_attr') }}" ></span></p>
{% else %}
<div class="address{% if options['multiline'] %} multiline{% endif %}{% if options['with_delimiter'] %} delimiter{% endif %}">
@@ -109,6 +112,7 @@
<i class="fa fa-fw fa-map-marker"></i>
{% endif %}
{{ _self.raw(lines) }}
<p><span data-address-details="1" data-address-id="{{ address.id|escape('html_attr') }}" data-address-ref-status="{{ address.refStatus|escape('html_attr') }}"></span></p>
</div>
{% endif %}
{{ _self.validity(address, options) }}

View File

@@ -41,9 +41,10 @@
<h3 class="m-3">{{ 'Center'|trans }}</h3>
{{ form_widget(form.centers.center) }}
<h3 class="m-3">{{ 'Pick aggregated centers'|trans }}</h3>
{{ form_widget(form.centers.regroupment) }}
{% if form.centers.regroupment is defined %}
<h3 class="m-3">{{ 'Pick aggregated centers'|trans }}</h3>
{{ form_widget(form.centers.regroupment) }}
{% endif %}
</section>
<p>{{ form_widget(form.submit, { 'attr' : { 'class' : 'btn btn-action btn-create' }, 'label' : 'Go to export options' } ) }}</p>

View File

@@ -68,7 +68,8 @@
{{- form_errors(form) -}}
</div>
{% else %}
<div class="col-sm">
<div class="col-12 clear">{{- form_label(form) -}}</div>
<div class="col-sm-12">
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
{{- form_errors(form) -}}

View File

@@ -18,43 +18,48 @@
{% block form_row %}
{% apply spaceless %}
{% if form.vars.fullWidth is not defined or form.vars.fullWidth == false %}
<div class="mb-2">
<div class="row">
<div class="{% apply spaceless %}
{% if attr.class is defined and ('cf-title' in attr.class or 'cf-fields' in attr.class ) %}
col-sm-12
{% elseif attr.class is defined and 'multiple-cf-inline' in attr.class %}
col-sm-2 col-md-4 clear
{% else %}
col-sm-4 clear
{% if form.vars.fullWidth is not defined or form.vars.fullWidth == false %}
<div class="{% apply spaceless %}
{% if attr.class is defined and ('cf-title' in attr.class or 'cf-fields' in attr.class ) %}
col-sm-12
{% elseif attr.class is defined and 'multiple-cf-inline' in attr.class %}
col-sm-2 col-md-4 clear
{% else %}
col-sm-4 clear
{% endif %}
{% endapply %}">
{% if attr.class is not defined or ('cf-title' not in attr.class and 'cf-fields' not in attr.class ) %}
{{ form_label(form) }}
{% endif %}
{% endapply %}">
{% if attr.class is not defined or ('cf-title' not in attr.class and 'cf-fields' not in attr.class ) %}
{{ form_label(form) }}
</div>
<div class="{% apply spaceless %}
{% if attr.class is defined and 'cf-title' in attr.class %}
col-sm-12
{% elseif attr.class is defined and 'cf-fields' in attr.class %}
col-sm-12 parent
{% elseif attr.class is defined and 'multiple-cf-inline' in attr.class %}
col-sm-2 col-md-8 multiple-cf-inline
{% else %}
col-sm-8
{% endif %}
{% endapply %}">
{{ form_widget(form) }}
{{ form_errors(form) }}
</div>
{% else %}
<div class="col-12 clear">{{ form_label(form) }}</div>
<div class="col-12">{{ form_widget(form) }}</div>
{% endif %}
</div>
<div class="{% apply spaceless %}
{% if attr.class is defined and 'cf-title' in attr.class %}
col-sm-12
{% elseif attr.class is defined and 'cf-fields' in attr.class %}
col-sm-12 parent
{% elseif attr.class is defined and 'multiple-cf-inline' in attr.class %}
col-sm-2 col-md-8 multiple-cf-inline
{% else %}
col-sm-8
{% endif %}
{% endapply %}">
{{ form_widget(form) }}
{{ form_errors(form) }}
</div>
</div>
</div>
{% else %}
{{ form_widget(form) }}
{% endif %}
{% endapply %}
{% endblock form_row %}
{#
The block 'form_row' above may be removed !
Read this note: https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/502#note_1311993084
#}
{% block choice_widget_expanded %}
{% apply spaceless %}
@@ -200,7 +205,6 @@
{% block private_comment_row %}
{{ form_label(form) }}
{{ form_row(form) }}
{% endblock %}
@@ -211,7 +215,6 @@
{% endblock %}
{% block comment_row %}
{{ form_label(form) }}
{{ form_row(form) }}
{% endblock %}
@@ -249,7 +252,11 @@
{% block pick_entity_dynamic_widget %}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
<div data-module="pick-dynamic"
data-types="{{ form.vars['types']|json_encode }}"
data-multiple="{{ form.vars['multiple'] }}"
data-uniqid="{{ form.vars['uniqid'] }}"
data-suggested="{{ form.vars['suggested']|json_encode|escape('html_attr') }}"></div>
{% endblock %}
{% block pick_postal_code_widget %}
@@ -269,4 +276,4 @@
{{ form_errors(form.fixedDate) }}
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -30,11 +30,27 @@
{% endif %}
{% if c.notification.addressees|length > 0 %}
<li class="notification-to">
<span class="item-key">
{% if c.notification_cc is defined %}
{% if c.notification_cc %}
<span class="item-key">
<abbr title="{{ 'notification.sent_cc'|trans }}">
{{ 'notification.cc'|trans }} :
</abbr>
</span>
{% else %}
<span class="item-key">
<abbr title="{{ 'notification.sent_to'|trans }}">
{{ 'notification.to'|trans }} :
</abbr>
</span>
{% endif %}
{% else %}
<span class="item-key">
<abbr title="{{ 'notification.sent_to'|trans }}">
{{ 'notification.to'|trans }} :
</abbr>
</span>
{% endif %}
{% for a in c.notification.addressees %}
<span class="badge-user">
{{ a|chill_entity_render_string }}

View File

@@ -50,7 +50,8 @@
{% for data in datas %}
{% set notification = data.notification %}
{% include 'ChillMainBundle:Notification:_list_item.html.twig' with {
'fold_item': true
'fold_item': true,
'notification_cc': data.template_data.notificationCc is defined ? data.template_data.notificationCc : false
} %}
{% endfor %}
</div>

View File

@@ -27,7 +27,8 @@
},
'action_button': false,
'full_content': true,
'fold_item': false
'fold_item': false,
'notification_cc': handler.getTemplateData(notification).notificationCc is defined ? handler.getTemplateData(notification).notificationCc : false
} %}
</div>

View File

@@ -72,7 +72,7 @@
</div>
</div>
<div class="item-row column">
<table class="obj-res-eval my-3">
<table class="obj-res-eval smallfont my-3">
<thead>
<tr><th class="obj"><h4 class="title_label">Objectif - motif - dispositif</h4></th>
<th class="res"><h4 class="title_label">Résultats - orientations</h4></th>

View File

@@ -65,7 +65,10 @@
<div id="futureDests">
{{ form_row(transition_form.future_dest_users) }}
{{ form_row(transition_form.future_cc_users) }}
{{ form_row(transition_form.future_dest_emails) }}
{{ form_errors(transition_form.future_dest_users) }}
</div>
<p>{{ form_label(transition_form.comment) }}</p>

View File

@@ -81,6 +81,15 @@
</ul>
{% endif %}
{% if step.ccUser|length > 0 %}
<p><b>{{ 'workflow.Users put in Cc'|trans }}&nbsp;: </b></p>
<ul>
{% for u in step.ccUser %}
<li>{{ u|chill_entity_render_box }}</li>
{% endfor %}
</ul>
{% endif %}
{% if entity_workflow.currentStep.destEmail|length > 0 %}
<p><b>{{ 'workflow.An access key was also sent to those addresses'|trans }}&nbsp;:</b></p>
<ul>

View File

@@ -7,7 +7,7 @@
{% endblock %}
{% block content %}
<div class="col-10 workflow">
<div class="col-12 workflow">
<h1 class="mb-5">{{ block('title') }}</h1>
@@ -25,6 +25,12 @@
{{ 'workflow.dest'|trans }}
</a>
</li>
<li class="nav-item">
<a href="{{ path('chill_main_workflow_list_cc') }}"
class="nav-link {% if step == 'cc' %}active{% endif %}">
{{ 'workflow.cc'|trans }}
</a>
</li>
<li class="nav-item">
<a href="{{ path('chill_main_workflow_list_previous_without_reaction') }}"
class="nav-link {% if step == 'previous_without_reaction' %}active{% endif %}">

View File

@@ -15,6 +15,12 @@
{% for d in step.destUser %}{{ d|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
</b>
</li>
<li>
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
<b>
{% for u in step.ccUser %}{{ u|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
</b>
</li>
{% else %}
<li>
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>

View File

@@ -20,6 +20,7 @@
{{ encore_entry_link_tags('chill') }}
{{ encore_entry_link_tags('mod_blur') }}
{{ encore_entry_link_tags('vue_onthefly') }}
{{ encore_entry_link_tags('mod_address_details') }}
{% block css %}<!-- nothing added to css -->{% endblock %}
</head>
@@ -112,6 +113,7 @@
{{ encore_entry_script_tags('mod_blur') }}
{{ encore_entry_script_tags('chill') }}
{{ encore_entry_script_tags('vue_onthefly') }}
{{ encore_entry_script_tags('mod_address_details') }}
<script type="text/javascript">
window.addEventListener('DOMContentLoaded', function(e) {

View File

@@ -13,8 +13,18 @@ namespace Chill\MainBundle\Routing;
use Knp\Menu\MenuItem;
/**
* Implements a builder for menu
*
* @template TParams of array
*/
interface LocalMenuBuilderInterface
{
/**
* @param $menuId
* @param MenuItem $menu
* @param TParams $parameters
*/
public function buildMenu($menuId, MenuItem $menu, array $parameters);
/**

View File

@@ -11,8 +11,8 @@ declare(strict_types=1);
namespace Chill\MainBundle\Search;
use Chill\MainBundle\Pagination\Paginator;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
@@ -30,7 +30,7 @@ class SearchApi
private PaginatorFactory $paginator;
private iterable $providers = [];
private iterable $providers;
public function __construct(
EntityManagerInterface $em,
@@ -42,9 +42,6 @@ class SearchApi
$this->paginator = $paginator;
}
/**
* @return Model/Result[]
*/
public function getResults(string $pattern, array $types, array $parameters): Collection
{
$queries = $this->findQueries($pattern, $types, $parameters);
@@ -53,10 +50,10 @@ class SearchApi
throw new SearchApiNoQueryException($pattern, $types, $parameters);
}
$total = $this->countItems($queries, $types, $parameters);
$total = $this->countItems($queries);
$paginator = $this->paginator->create($total);
$rawResults = $this->fetchRawResult($queries, $types, $parameters, $paginator);
$rawResults = $this->fetchRawResult($queries, $types, $paginator);
$this->prepareProviders($rawResults);
$results = $this->buildResults($rawResults);
@@ -64,7 +61,7 @@ class SearchApi
return new Collection($results, $paginator);
}
private function buildCountQuery(array $queries, $types, $parameters)
private function buildCountQuery(array $queries): array
{
$query = 'SELECT SUM(c) AS count FROM ({union_unordered}) AS sq';
$unions = [];
@@ -88,7 +85,7 @@ class SearchApi
$items = [];
foreach ($rawResults as $r) {
foreach ($this->providers as $k => $p) {
foreach ($this->providers as $p) {
if ($p->supportsResult($r['key'], $r['metadata'])) {
$items[] = (new SearchApiResult($r['pertinence']))
->setResult(
@@ -103,7 +100,7 @@ class SearchApi
return $items;
}
private function buildUnionQuery(array $queries, $types, $parameters, Paginator $paginator)
private function buildUnionQuery(array $queries, PaginatorInterface $paginator): array
{
$query = '{unions} ORDER BY pertinence DESC LIMIT ? OFFSET ?';
$unions = [];
@@ -126,9 +123,9 @@ class SearchApi
];
}
private function countItems($providers, $types, $parameters): int
private function countItems($providers): int
{
[$countQuery, $parameters] = $this->buildCountQuery($providers, $types, $parameters);
[$countQuery, $parameters] = $this->buildCountQuery($providers);
$rsmCount = new ResultSetMappingBuilder($this->em);
$rsmCount->addScalarResult('count', 'count');
$countNq = $this->em->createNativeQuery($countQuery, $rsmCount);
@@ -137,9 +134,9 @@ class SearchApi
return (int) $countNq->getSingleScalarResult();
}
private function fetchRawResult($queries, $types, $parameters, Paginator $paginator): array
private function fetchRawResult($queries, $types, PaginatorInterface $paginator): array
{
[$union, $parameters] = $this->buildUnionQuery($queries, $types, $parameters, $paginator);
[$union, $parameters] = $this->buildUnionQuery($queries, $paginator);
$rsm = new ResultSetMappingBuilder($this->em);
$rsm->addScalarResult('key', 'key', Types::STRING)
->addScalarResult('metadata', 'metadata', Types::JSON)
@@ -172,7 +169,7 @@ class SearchApi
);
}
private function prepareProviders(array $rawResults)
private function prepareProviders(array $rawResults): void
{
$metadatas = [];
$providers = [];

View File

@@ -16,6 +16,18 @@ use function count;
use function implode;
use function strtr;
/**
* This create a query optimized for searching for the api response.
*
* When build, this class generate a SQL string and a list of a parameters which is suitable for running
* a native SQL query. This have usually the form of
*
* `SELECT '<key>' as key, <metadata> as metadata, <pertinence> as pertinence FROM <from clause> WHERE <where clause>`.
*
* The clause between `<>` are provided through the dedicated method in this class (@link{self::setSelectKey},
* @link{self::setFromClause}), etc.).
*
*/
class SearchApiQuery
{
private ?string $fromClause = null;

View File

@@ -71,9 +71,9 @@ interface SearchInterface
* @param array $terms the string to search
* @param int $start the first result (for pagination)
* @param int $limit the number of result (for pagination)
* @param string $format The format for result
* @param "html"|"json" $format The format for result
*
* @return string, an HTML string
* @return string|array a string if format is html, an array if format is json
*/
public function renderResult(array $terms, $start = 0, $limit = 50, array $options = [], $format = 'html');

View File

@@ -26,28 +26,4 @@ use const E_USER_DEPRECATED;
*/
abstract class AbstractChillVoter extends Voter implements ChillVoterInterface
{
protected function supports($attribute, $subject)
{
@trigger_error(
'This voter should implements the new `supports` '
. 'methods introduced by Symfony 3.0, and do not rely on '
. 'getSupportedAttributes and getSupportedClasses methods.',
E_USER_DEPRECATED
);
// @TODO: getSupportedAttributes() should be created in here and made abstract or in ChillVoterInterface.
// @TODO: getSupportedClasses() should be created in here and made abstract or in ChillVoterInterface.
return in_array($attribute, $this->getSupportedAttributes($attribute), true)
&& in_array(get_class($subject), $this->getSupportedClasses(), true);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
@trigger_error('This voter should implements the new `voteOnAttribute` '
. 'methods introduced by Symfony 3.0, and do not rely on '
. 'isGranted method', E_USER_DEPRECATED);
// @TODO: isGranted() should be created in here and made abstract or in ChillVoterInterface.
return $this->isGranted($attribute, $subject, $token->getUser());
}
}

View File

@@ -16,7 +16,7 @@ use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface;
use Chill\MainBundle\Security\ParentRoleHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Role\Role;
@@ -34,7 +34,7 @@ use function get_class;
*/
class AuthorizationHelper implements AuthorizationHelperInterface
{
private CenterResolverDispatcherInterface $centerResolverDispatcher;
private CenterResolverManagerInterface $centerResolverManager;
private LoggerInterface $logger;
@@ -45,13 +45,13 @@ class AuthorizationHelper implements AuthorizationHelperInterface
private UserACLAwareRepositoryInterface $userACLAwareRepository;
public function __construct(
CenterResolverDispatcherInterface $centerResolverDispatcher,
CenterResolverManagerInterface $centerResolverManager,
LoggerInterface $logger,
ScopeResolverDispatcher $scopeResolverDispatcher,
UserACLAwareRepositoryInterface $userACLAwareRepository,
ParentRoleHelper $parentRoleHelper
) {
$this->centerResolverDispatcher = $centerResolverDispatcher;
$this->centerResolverManager = $centerResolverManager;
$this->logger = $logger;
$this->scopeResolverDispatcher = $scopeResolverDispatcher;
$this->userACLAwareRepository = $userACLAwareRepository;
@@ -63,7 +63,7 @@ class AuthorizationHelper implements AuthorizationHelperInterface
*
* @param User $user The user
* @param array $centers a list of centers which are going to be filtered
* @param Center|string $role
* @param mixed $role
*/
public function filterReachableCenters(User $user, array $centers, $role): array
{
@@ -113,13 +113,14 @@ class AuthorizationHelper implements AuthorizationHelperInterface
* Get reachable Centers for the given user, role,
* and optionally Scope.
*
* @return array|Center[]
* @return list<Center>
*/
public function getReachableCenters(UserInterface $user, string $role, ?Scope $scope = null): array
{
if ($role instanceof Role) {
$role = $role->getRole();
}
/** @var array<string, Center> $centers */
$centers = [];
foreach ($user->getGroupCenters() as $groupCenter) {
@@ -129,13 +130,13 @@ class AuthorizationHelper implements AuthorizationHelperInterface
//check that the role is in the reachable roles
if ($this->isRoleReached($role, $roleScope->getRole())) {
if (null === $scope) {
$centers[] = $groupCenter->getCenter();
$centers[spl_object_hash($groupCenter->getCenter())] = $groupCenter->getCenter();
break;
}
if ($scope->getId() === $roleScope->getScope()->getId()) {
$centers[] = $groupCenter->getCenter();
$centers[spl_object_hash($groupCenter->getCenter())] = $groupCenter->getCenter();
break;
}
@@ -143,7 +144,7 @@ class AuthorizationHelper implements AuthorizationHelperInterface
}
}
return $centers;
return array_values($centers);
}
/**
@@ -194,12 +195,8 @@ class AuthorizationHelper implements AuthorizationHelperInterface
*
* @return array|Scope[]
*/
public function getReachableScopes(UserInterface $user, string $role, $center): array
public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array
{
if ($role instanceof Role) {
$role = $role->getRole();
}
return $this->getReachableCircles($user, $role, $center);
}
@@ -252,27 +249,15 @@ class AuthorizationHelper implements AuthorizationHelperInterface
*/
public function userHasAccess(User $user, $entity, $attribute)
{
$center = $this->centerResolverDispatcher->resolveCenter($entity);
$centers = $this->centerResolverManager->resolveCenters($entity);
if (is_iterable($center)) {
foreach ($center as $c) {
if ($this->userHasAccessForCenter($user, $c, $entity, $attribute)) {
return true;
}
foreach ($centers as $c) {
if ($this->userHasAccessForCenter($user, $c, $entity, $attribute)) {
return true;
}
return false;
}
if ($center instanceof Center) {
return $this->userHasAccessForCenter($user, $center, $entity, $attribute);
}
if (null === $center) {
return false;
}
throw new UnexpectedValueException('could not resolver a center');
return false;
}
/**

View File

@@ -21,12 +21,12 @@ interface AuthorizationHelperInterface
* Get reachable Centers for the given user, role,
* and optionnaly Scope.
*
* @return Center[]
* @return list<Center>
*/
public function getReachableCenters(UserInterface $user, string $role, ?Scope $scope = null): array;
/**
* @param array|Center|Center[] $center
* @param Center|list<Center> $center
*/
public function getReachableScopes(UserInterface $user, string $role, $center): array;
public function getReachableScopes(UserInterface $user, string $role, Center|array $center): array;
}

View File

@@ -27,7 +27,7 @@ class Counter implements JsonSerializable
return $this->counter;
}
public function jsonSerialize()
public function jsonSerialize(): array
{
return ['count' => $this->counter];
}

View File

@@ -97,6 +97,13 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
);
$data['validFrom'] = $address->getValidFrom();
$data['validTo'] = $address->getValidTo();
$data['refStatus'] = $address->getRefStatus();
$data['point'] = $this->normalizer->normalize(
$address->getPoint(),
$format,
[AbstractNormalizer::GROUPS => ['read']]
);
$data['isNoAddress'] = $address->isNoAddress();
} elseif ('docgen' === $format) {
$dateContext = array_merge($context, ['docgen:expects' => DateTimeInterface::class]);
$data['validFrom'] = $this->normalizer->normalize($address->getValidFrom(), $format, $dateContext);

View File

@@ -24,12 +24,18 @@ class AddressReferenceBEFromBestAddress
private AddressReferenceBaseImporter $baseImporter;
private AddressToReferenceMatcher $addressToReferenceMatcher;
private HttpClientInterface $client;
public function __construct(HttpClientInterface $client, AddressReferenceBaseImporter $baseImporter)
{
public function __construct(
HttpClientInterface $client,
AddressReferenceBaseImporter $baseImporter,
AddressToReferenceMatcher $addressToReferenceMatcher
) {
$this->client = $client;
$this->baseImporter = $baseImporter;
$this->addressToReferenceMatcher = $addressToReferenceMatcher;
}
public function import(string $lang, array $lists): void
@@ -89,16 +95,18 @@ class AddressReferenceBEFromBestAddress
$record['municipality_objectid'],
$record['postal_info_objectid'],
$record['streetname'],
$record['housenumber'] . $record['boxnumber'],
$record['housenumber'] .($record['boxnumber'] !== '' ? ' bte '. $record['boxnumber'] : ''),
'bestaddress.' . $list,
(float) $record['X'],
(float) $record['Y'],
(float) $record['X'],
3812
);
}
$this->baseImporter->finalize();
$this->addressToReferenceMatcher->checkAddressesMatchingReferences();
gzclose($uncompressedStream);
}
}

View File

@@ -22,12 +22,15 @@ class AddressReferenceFromBano
{
private AddressReferenceBaseImporter $baseImporter;
private AddressToReferenceMatcher $addressToReferenceMatcher;
private HttpClientInterface $client;
public function __construct(HttpClientInterface $client, AddressReferenceBaseImporter $baseImporter)
public function __construct(HttpClientInterface $client, AddressReferenceBaseImporter $baseImporter, AddressToReferenceMatcher $addressToReferenceMatcher)
{
$this->client = $client;
$this->baseImporter = $baseImporter;
$this->addressToReferenceMatcher = $addressToReferenceMatcher;
}
public function import(string $departementNo): void
@@ -82,6 +85,8 @@ class AddressReferenceFromBano
$this->baseImporter->finalize();
$this->addressToReferenceMatcher->checkAddressesMatchingReferences();
fclose($file);
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Service\Import;
use Chill\MainBundle\Entity\Address;
use Doctrine\DBAL\Connection;
use Psr\Log\LoggerInterface;
/**
* Mark existing addresses as to be reviewed regarding the
* address reference
*/
final class AddressToReferenceMatcher
{
private Connection $connection;
private LoggerInterface $logger;
private const LOG_PREFIX = '[address_to_reference_matcher] ';
private const SQL_MARK_TO_REVIEW_ADDRESS_UNMATCHING = <<<'SQL'
UPDATE chill_main_address a SET refstatus = '{{ to_review }}', refstatuslastupdate = NOW()
FROM chill_main_address_reference ar
WHERE
a.addressreference_id = ar.id
-- restrict only on active addresses
AND (a.validto IS NULL OR a.validto >= NOW())
-- only addresses that are marked matching or "to review", but before the update
AND
(a.refstatus LIKE '{{ matching }}'
OR (a.refstatus LIKE '{{ reviewed }}' AND a.refstatuslastupdate < ar.updatedat))
AND (
a.postcode_id != ar.postcode_id
OR a.street != ar.street
OR a.streetnumber != ar.streetnumber
OR ROUND(ST_X(a.point) * 1000000) <> ROUND(ST_X(ar.point) * 1000000)
OR ROUND(ST_Y(a.point) * 1000000) <> ROUND(ST_Y(ar.point) * 1000000)
)
SQL;
private const SQL_MARK_MATCHING_ADDRESSES_REVIEWED_OR_TO_REVIEW = <<<'SQL'
UPDATE chill_main_address a SET refstatus = '{{ matching }}', refstatuslastupdate = NOW()
FROM chill_main_address_reference ar
WHERE
a.addressreference_id = ar.id
-- restrict only on active addresses
AND (a.validto IS NULL OR a.validto >= NOW())
AND a.refstatus IN ('{{ to_review }}', '{{ reviewed }}')
AND a.postcode_id = ar.postcode_id
AND a.street = ar.street
AND a.streetnumber = ar.streetnumber
AND ROUND(ST_X(a.point) * 1000000) = ROUND(ST_X(ar.point) * 1000000)
AND ROUND(ST_Y(a.point) * 1000000) = ROUND(ST_Y(ar.point) * 1000000)
SQL;
private const SUBSTITUTES = [
'{{ to_review }}' => Address::ADDR_REFERENCE_STATUS_TO_REVIEW,
'{{ matching }}' => Address::ADDR_REFERENCE_STATUS_MATCH,
'{{ reviewed }}' => Address::ADDR_REFERENCE_STATUS_REVIEWED
];
public function __construct(Connection $connection, LoggerInterface $logger)
{
$this->connection = $connection;
$this->logger = $logger;
}
public function checkAddressesMatchingReferences(): void
{
$this->logger->notice(self::LOG_PREFIX.'Starting addresses matching');
$this->connection->transactional(function () {
$markedAsMatching = $this->connection->executeStatement(
strtr(self::SQL_MARK_MATCHING_ADDRESSES_REVIEWED_OR_TO_REVIEW, self::SUBSTITUTES)
);
$markedAsToReview = $this->connection->executeStatement(
strtr(self::SQL_MARK_TO_REVIEW_ADDRESS_UNMATCHING, self::SUBSTITUTES)
);
$this->logger->info(self::LOG_PREFIX.'Executed address matching', [
'marked_as_matching' => $markedAsMatching,
'marked_as_to_review' => $markedAsToReview,
]);
});
}
}

View File

@@ -90,7 +90,7 @@ class ChillTwigHelper extends AbstractExtension
return $twig->render($t, array_merge([
'value' => $value,
'message' => $message ?? 'No value',
'message' => $message,
], $options));
}
}

View File

@@ -11,15 +11,10 @@ declare(strict_types=1);
namespace Chill\MainBundle\Templating\Entity;
/**
* @deprecated load @link{BoxUtilsChillEntityRenderTrait} in the render
*/
abstract class AbstractChillEntityRender implements ChillEntityRenderInterface
{
protected function getDefaultClosingBox(): string
{
return '</section>';
}
protected function getDefaultOpeningBox($classSuffix): string
{
return '<section class="chill-entity entity-' . $classSuffix . '">';
}
use BoxUtilsChillEntityRenderTrait;
}

View File

@@ -13,11 +13,16 @@ namespace Chill\MainBundle\Templating\Entity;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Templating\EngineInterface;
use function array_merge;
use function strtr;
/**
* @implements ChillEntityRenderInterface<Address>
*/
class AddressRender implements ChillEntityRenderInterface
{
public const DEFAULT_OPTIONS = [
@@ -35,16 +40,14 @@ class AddressRender implements ChillEntityRenderInterface
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(EngineInterface $templating, TranslatableStringHelperInterface $translatableStringHelper)
{
public function __construct(
EngineInterface $templating,
TranslatableStringHelperInterface $translatableStringHelper
) {
$this->templating = $templating;
$this->translatableStringHelper = $translatableStringHelper;
}
/**
* @param Address addr
* @param mixed $addr
*/
public function renderBox($addr, array $options): string
{
$options = array_merge(self::DEFAULT_OPTIONS, $options);
@@ -60,17 +63,14 @@ class AddressRender implements ChillEntityRenderInterface
}
/**
* @param Address addr
* @param mixed $addr
*
* @return string[]
*/
public function renderLines(Address $addr, bool $includeCityLine = true, bool $includeCountry = true): array
{
$lines = [];
if (null !== $addr->getPostCode()) {
if ($addr->getPostCode()->getCountry()->getCountryCode() === 'FR') {
if (null !== $addr->getPostcode()) {
if ($addr->getPostcode()->getCountry()->getCountryCode() === 'FR') {
$lines[] = $this->renderIntraBuildingLine($addr);
$lines[] = $this->renderBuildingLine($addr);
$lines[] = $this->renderStreetLine($addr);
@@ -98,18 +98,18 @@ class AddressRender implements ChillEntityRenderInterface
}
}
return array_values(array_filter($lines, static fn ($l) => null !== $l));
return array_values(array_filter($lines, static fn ($l) => '' !== (string) $l));
}
public function renderStreetLine(Address $addr): ?string
{
if (null !== $addr->getStreet() && $addr->getStreet() !== '') {
if ('' !== $addr->getStreet()) {
$street = $addr->getStreet();
} else {
$street = '';
}
if (null !== $addr->getStreetNumber() && $addr->getStreetNumber() !== '') {
if ('' !== $addr->getStreetNumber()) {
$streetNumber = $addr->getStreetNumber();
} else {
$streetNumber = '';
@@ -117,10 +117,8 @@ class AddressRender implements ChillEntityRenderInterface
$res = trim($street . ', ' . $streetNumber, ', ');
if (null !== $addr->getPostCode()->getCountry()->getCountryCode()) {
if ($addr->getPostCode()->getCountry()->getCountryCode() === 'FR') {
$res = trim($streetNumber . ', ' . $street, ', ');
}
if ($addr->getPostcode()->getCountry()->getCountryCode() === 'FR') {
$res = trim($streetNumber . ', ' . $street, ', ');
}
if ((',' === $res) || ('' === $res)) {
@@ -130,10 +128,6 @@ class AddressRender implements ChillEntityRenderInterface
return $res;
}
/**
* @param Address addr
* @param mixed $addr
*/
public function renderString($addr, array $options): string
{
return implode(' — ', $this->renderLines($addr));
@@ -146,7 +140,7 @@ class AddressRender implements ChillEntityRenderInterface
private function renderBuildingLine(Address $addr): ?string
{
if (null !== $addr->getBuildingName() && $addr->getBuildingName() !== '') {
if ($addr->getBuildingName() !== '') {
$building = $addr->getBuildingName();
} else {
$building = '';
@@ -163,16 +157,14 @@ class AddressRender implements ChillEntityRenderInterface
$res = null;
}
if (null !== $addr->getPostCode()->getCountry()->getCountryCode()) {
if ($addr->getPostCode()->getCountry()->getCountryCode() === 'FR') {
$res = $addr->getBuildingName();
}
if ($addr->getPostcode()->getCountry()->getCountryCode() === 'FR') {
$res = $addr->getBuildingName();
}
return $res;
}
private function renderCityLine($addr): string
private function renderCityLine(Address $addr): string
{
if (null !== $addr->getPostcode()) {
$res = strtr('{postcode} {label}', [
@@ -180,11 +172,9 @@ class AddressRender implements ChillEntityRenderInterface
'{label}' => $addr->getPostcode()->getName(),
]);
if (null !== $addr->getPostCode()->getCountry()->getCountryCode()) {
if ($addr->getPostCode()->getCountry()->getCountryCode() === 'FR') {
if ($addr->getDistribution()) {
$res = $res . ' ' . $addr->getDistribution();
}
if ($addr->getPostcode()->getCountry()->getCountryCode() === 'FR') {
if ('' !== $addr->getDistribution()) {
$res = $res . ' ' . $addr->getDistribution();
}
}
}
@@ -192,35 +182,35 @@ class AddressRender implements ChillEntityRenderInterface
return $res ?? '';
}
private function renderCountryLine($addr): ?string
private function renderCountryLine(Address $addr): ?string
{
return $this->translatableStringHelper->localize(
$addr->getPostCode()->getCountry()->getName()
$addr->getPostcode()->getCountry()->getName()
);
}
private function renderDeliveryLine($addr): ?string
private function renderDeliveryLine(Address $addr): string
{
return $addr->getExtra();
}
private function renderIntraBuildingLine($addr): ?string
private function renderIntraBuildingLine(Address $addr): ?string
{
$arr = [];
if ($addr->getFlat()) {
if ('' !== $addr->getFlat()) {
$arr[] = 'appart ' . $addr->getFlat();
}
if ($addr->getFloor()) {
if ('' !== $addr->getFloor()) {
$arr[] = 'ét ' . $addr->getFloor();
}
if ($addr->getCorridor()) {
if ('' !== $addr->getCorridor()) {
$arr[] = 'coul ' . $addr->getCorridor();
}
if ($addr->getSteps()) {
if ('' !== $addr->getSteps()) {
$arr[] = 'esc ' . $addr->getSteps();
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Templating\Entity;
trait BoxUtilsChillEntityRenderTrait
{
protected function getDefaultClosingBox(): string
{
return '</section>';
}
protected function getDefaultOpeningBox($classSuffix): string
{
return '<section class="chill-entity entity-' . $classSuffix . '">';
}
}

View File

@@ -14,8 +14,10 @@ namespace Chill\MainBundle\Templating\Entity;
/**
* Render an entity using `__toString()`.
*/
class ChillEntityRender extends AbstractChillEntityRender
class ChillEntityRender implements ChillEntityRenderInterface
{
use BoxUtilsChillEntityRenderTrait;
public function renderBox($entity, array $options): string
{
return $this->getDefaultOpeningBox('default') . $entity

View File

@@ -14,6 +14,8 @@ namespace Chill\MainBundle\Templating\Entity;
/**
* Interface to implement which will render an entity in template on a custom
* manner.
*
* @template T
*/
interface ChillEntityRenderInterface
{
@@ -29,7 +31,7 @@ interface ChillEntityRenderInterface
* </span>
* ```
*
* @param type $entity
* @param T $entity
*/
public function renderBox($entity, array $options): string;
@@ -38,14 +40,12 @@ interface ChillEntityRenderInterface
*
* Example: returning the name of a person.
*
* @param object $entity
* @param T $entity
*/
public function renderString($entity, array $options): string;
/**
* Return true if the class support this object for the given options.
*
* @param type $entity
*/
public function supports($entity, array $options): bool;
public function supports(object $entity, array $options): bool;
}

Some files were not shown because too many files have changed in this diff Show More