Merge remote-tracking branch 'origin/master' into issue706_cc_in_workflow-2

This commit is contained in:
Julien Fastré 2023-04-13 09:53:23 +02:00
commit 0335986326
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
154 changed files with 3079 additions and 2666 deletions

View File

@ -69,7 +69,7 @@ phpstan_tests:
stage: Tests stage: Tests
image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82 image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82
script: script:
- bin/phpstan - bin/phpstan analyze --memory-limit=2G
cache: cache:
paths: paths:
- .cache/ - .cache/
@ -79,21 +79,37 @@ phpstan_tests:
- bin - bin
- tests/app/vendor/ - tests/app/vendor/
psalm_tests: rector_tests:
stage: Tests stage: Tests
image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82 image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82
script: script:
- bin/psalm - bin/rector --dry-run
allow_failure: true cache:
paths:
- .cache/
artifacts: artifacts:
expire_in: 30 min expire_in: 30 min
paths: paths:
- bin - bin
- tests/app/vendor/ - tests/app/vendor/
# psalm_tests:
# stage: Tests
# image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82
# script:
# - bin/psalm
# allow_failure: true
# artifacts:
# expire_in: 30 min
# paths:
# - bin
# - tests/app/vendor/
unit_tests: unit_tests:
stage: Tests stage: Tests
image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82 image: gitea.champs-libres.be/chill-project/chill-skeleton-basic/base-image:php82
# until we fix testes
allow_failure: true
script: script:
- php tests/app/bin/console doctrine:migrations:migrate -n - php tests/app/bin/console doctrine:migrations:migrate -n
- php -d memory_limit=2G tests/app/bin/console cache:clear --env=dev - php -d memory_limit=2G tests/app/bin/console cache:clear --env=dev

View File

@ -75,6 +75,7 @@
"phpunit/phpunit": ">= 7.5", "phpunit/phpunit": ">= 7.5",
"psalm/plugin-phpunit": "^0.18.4", "psalm/plugin-phpunit": "^0.18.4",
"psalm/plugin-symfony": "^4.0.2", "psalm/plugin-symfony": "^4.0.2",
"rector/rector": "^0.15.23",
"symfony/debug-bundle": "^5.1", "symfony/debug-bundle": "^5.1",
"symfony/dotenv": "^4.4", "symfony/dotenv": "^4.4",
"symfony/maker-bundle": "^1.20", "symfony/maker-bundle": "^1.20",

View File

@ -41,6 +41,7 @@
"@fullcalendar/timegrid": "^6.1.4", "@fullcalendar/timegrid": "^6.1.4",
"@fullcalendar/vue3": "^6.1.4", "@fullcalendar/vue3": "^6.1.4",
"@popperjs/core": "^2.9.2", "@popperjs/core": "^2.9.2",
"@types/leaflet": "^1.9.3",
"dropzone": "^5.7.6", "dropzone": "^5.7.6",
"es6-promise": "^4.2.8", "es6-promise": "^4.2.8",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",

View File

@ -442,11 +442,6 @@ parameters:
count: 2 count: 2
path: src/Bundle/ChillMainBundle/Workflow/Validator/StepDestValidValidator.php path: src/Bundle/ChillMainBundle/Workflow/Validator/StepDestValidValidator.php
-
message: "#^Return type \\(int\\|void\\|null\\) of method Chill\\\\PersonBundle\\\\Command\\\\ImportPeopleFromCSVCommand\\:\\:execute\\(\\) should be covariant with return type \\(int\\) of method Symfony\\\\Component\\\\Console\\\\Command\\\\Command\\:\\:execute\\(\\)$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
- -
message: "#^Parameter \\#3 \\$query \\(Doctrine\\\\ORM\\\\QueryBuilder\\) of method Chill\\\\PersonBundle\\\\Controller\\\\HouseholdCompositionTypeApiController\\:\\:customizeQuery\\(\\) should be contravariant with parameter \\$query \\(mixed\\) of method Chill\\\\MainBundle\\\\CRUD\\\\Controller\\\\AbstractCRUDController\\:\\:customizeQuery\\(\\)$#" message: "#^Parameter \\#3 \\$query \\(Doctrine\\\\ORM\\\\QueryBuilder\\) of method Chill\\\\PersonBundle\\\\Controller\\\\HouseholdCompositionTypeApiController\\:\\:customizeQuery\\(\\) should be contravariant with parameter \\$query \\(mixed\\) of method Chill\\\\MainBundle\\\\CRUD\\\\Controller\\\\AbstractCRUDController\\:\\:customizeQuery\\(\\)$#"
count: 1 count: 1
@ -788,16 +783,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillThirdPartyBundle/Security/Voter/ThirdPartyVoter.php path: src/Bundle/ChillThirdPartyBundle/Security/Voter/ThirdPartyVoter.php
-
message: "#^Parameter \\#1 \\$thirdParty \\(Chill\\\\ThirdPartyBundle\\\\Entity\\\\ThirdParty\\) of method Chill\\\\ThirdPartyBundle\\\\Serializer\\\\Normalizer\\\\ThirdPartyNormalizer\\:\\:normalize\\(\\) should be contravariant with parameter \\$object \\(mixed\\) of method Symfony\\\\Component\\\\Serializer\\\\Normalizer\\\\NormalizerInterface\\:\\:normalize\\(\\)$#"
count: 1
path: src/Bundle/ChillThirdPartyBundle/Serializer/Normalizer/ThirdPartyNormalizer.php
-
message: "#^Parameter \\#2 \\$format \\(string\\) of method Chill\\\\ThirdPartyBundle\\\\Serializer\\\\Normalizer\\\\ThirdPartyNormalizer\\:\\:supportsNormalization\\(\\) should be contravariant with parameter \\$format \\(string\\|null\\) of method Symfony\\\\Component\\\\Serializer\\\\Normalizer\\\\NormalizerInterface\\:\\:supportsNormalization\\(\\)$#"
count: 1
path: src/Bundle/ChillThirdPartyBundle/Serializer/Normalizer/ThirdPartyNormalizer.php
- -
message: "#^Parameter \\#1 \\$document \\(Chill\\\\DocStoreBundle\\\\Entity\\\\StoredObject\\) of method Chill\\\\WopiBundle\\\\Service\\\\Wopi\\\\ChillDocumentManager\\:\\:getBasename\\(\\) should be contravariant with parameter \\$document \\(ChampsLibres\\\\WopiLib\\\\Contract\\\\Entity\\\\Document\\) of method ChampsLibres\\\\WopiLib\\\\Contract\\\\Service\\\\DocumentManagerInterface\\:\\:getBasename\\(\\)$#" message: "#^Parameter \\#1 \\$document \\(Chill\\\\DocStoreBundle\\\\Entity\\\\StoredObject\\) of method Chill\\\\WopiBundle\\\\Service\\\\Wopi\\\\ChillDocumentManager\\:\\:getBasename\\(\\) should be contravariant with parameter \\$document \\(ChampsLibres\\\\WopiLib\\\\Contract\\\\Entity\\\\Document\\) of method ChampsLibres\\\\WopiLib\\\\Contract\\\\Service\\\\DocumentManagerInterface\\:\\:getBasename\\(\\)$#"
count: 1 count: 1

View File

@ -3026,111 +3026,6 @@ parameters:
count: 2 count: 2
path: src/Bundle/ChillPersonBundle/Command/ChillPersonMoveCommand.php path: src/Bundle/ChillPersonBundle/Command/ChillPersonMoveCommand.php
-
message: "#^Access to an undefined property Symfony\\\\Component\\\\EventDispatcher\\\\Event\\:\\:\\$force\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Access to an undefined property Symfony\\\\Component\\\\EventDispatcher\\\\Event\\:\\:\\$headers\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Access to an undefined property Symfony\\\\Component\\\\EventDispatcher\\\\Event\\:\\:\\$helperSet\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Access to an undefined property Symfony\\\\Component\\\\EventDispatcher\\\\Event\\:\\:\\$input\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Access to an undefined property Symfony\\\\Component\\\\EventDispatcher\\\\Event\\:\\:\\$output\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Access to an undefined property Symfony\\\\Component\\\\EventDispatcher\\\\Event\\:\\:\\$person\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Access to an undefined property Symfony\\\\Component\\\\EventDispatcher\\\\Event\\:\\:\\$rawHeaders\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Access to an undefined property Symfony\\\\Component\\\\EventDispatcher\\\\Event\\:\\:\\$row\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Access to an undefined property Symfony\\\\Component\\\\EventDispatcher\\\\Event\\:\\:\\$skipPerson\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Call to an undefined method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:createQuery\\(\\)\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Call to method buildForm\\(\\) on an unknown class Chill\\\\CustomFieldsBundle\\\\Service\\\\CustomFieldInterface\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Else branch is unreachable because previous condition is always true\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Elseif branch is unreachable because previous condition is always true\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Instanceof between \\*NEVER\\* and Symfony\\\\Component\\\\Form\\\\ChoiceList\\\\View\\\\ChoiceGroupView will always evaluate to false\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Instanceof between Symfony\\\\Component\\\\Form\\\\ChoiceList\\\\View\\\\ChoiceView and Symfony\\\\Component\\\\Form\\\\ChoiceList\\\\View\\\\ChoiceView will always evaluate to true\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Method Chill\\\\PersonBundle\\\\Command\\\\ImportPeopleFromCSVCommand\\:\\:processTextType\\(\\) has invalid return type Chill\\\\PersonBundle\\\\Command\\\\type\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Method Symfony\\\\Component\\\\Console\\\\Helper\\\\Table\\:\\:render\\(\\) invoked with 1 parameter, 0 required\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Method Symfony\\\\Contracts\\\\EventDispatcher\\\\EventDispatcherInterface\\:\\:dispatch\\(\\) invoked with 2 parameters, 1 required\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^PHPDoc tag @var above foreach loop does not specify variable name\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Parameter \\$value of method Chill\\\\PersonBundle\\\\Command\\\\ImportPeopleFromCSVCommand\\:\\:processTextType\\(\\) has invalid type Chill\\\\PersonBundle\\\\Command\\\\type\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Strict comparison using \\=\\=\\= between false and false will always evaluate to true\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
- -
message: "#^PHPDoc tag @var for property Chill\\\\PersonBundle\\\\Command\\\\ImportSocialWorkMetadata\\:\\:\\$importer with type Psr\\\\Log\\\\LoggerInterface is not subtype of native type Chill\\\\PersonBundle\\\\Service\\\\Import\\\\ChillImporter\\.$#" message: "#^PHPDoc tag @var for property Chill\\\\PersonBundle\\\\Command\\\\ImportSocialWorkMetadata\\:\\:\\$importer with type Psr\\\\Log\\\\LoggerInterface is not subtype of native type Chill\\\\PersonBundle\\\\Service\\\\Import\\\\ChillImporter\\.$#"
count: 1 count: 1

View File

@ -425,21 +425,6 @@ parameters:
count: 2 count: 2
path: src/Bundle/ChillPersonBundle/Actions/Remove/PersonMove.php path: src/Bundle/ChillPersonBundle/Actions/Remove/PersonMove.php
-
message: "#^Parameter \\#1 \\$event of method Symfony\\\\Contracts\\\\EventDispatcher\\\\EventDispatcherInterface\\:\\:dispatch\\(\\) expects object, string given\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Parameter \\#1 \\$mobilenumber of method Chill\\\\PersonBundle\\\\Entity\\\\Person\\:\\:setMobilenumber\\(\\) expects libphonenumber\\\\PhoneNumber\\|null, string given\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
-
message: "#^Parameter \\#1 \\$phonenumber of method Chill\\\\PersonBundle\\\\Entity\\\\Person\\:\\:setPhonenumber\\(\\) expects libphonenumber\\\\PhoneNumber\\|null, string given\\.$#"
count: 1
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
- -
message: "#^Parameter \\#1 \\$event of method Symfony\\\\Contracts\\\\EventDispatcher\\\\EventDispatcherInterface\\:\\:dispatch\\(\\) expects object, string given\\.$#" message: "#^Parameter \\#1 \\$event of method Symfony\\\\Contracts\\\\EventDispatcher\\\\EventDispatcherInterface\\:\\:dispatch\\(\\) expects object, string given\\.$#"
count: 1 count: 1

View File

@ -310,11 +310,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillPersonBundle/Command/ChillPersonMoveCommand.php path: src/Bundle/ChillPersonBundle/Command/ChillPersonMoveCommand.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 6
path: src/Bundle/ChillPersonBundle/Command/ImportPeopleFromCSVCommand.php
- -
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1 count: 1

View File

@ -3,7 +3,7 @@ parameters:
paths: paths:
- src/ - src/
tmpDir: .cache/ tmpDir: .cache/
reportUnmatchedIgnoredErrors: true reportUnmatchedIgnoredErrors: false
excludePaths: excludePaths:
- .php_cs* - .php_cs*
- docs/ - docs/

25
rector.php Normal file
View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([
__DIR__ . '/docs',
__DIR__ . '/src',
]);
$rectorConfig->cacheClass(\Rector\Caching\ValueObject\Storage\FileCacheStorage::class);
$rectorConfig->cacheDirectory(__DIR__.'/.cache/rector');
// register a single rule
$rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
// define sets of rules
// $rectorConfig->sets([
// LevelSetList::UP_TO_PHP_74
// ]);
};

View File

@ -195,7 +195,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @Groups({"docgen:read"}) * @Groups({"docgen:read"})
*/ */
private User $user; private ?User $user;
/** /**
* @ORM\ManyToMany(targetEntity="Chill\MainBundle\Entity\User") * @ORM\ManyToMany(targetEntity="Chill\MainBundle\Entity\User")
@ -494,7 +494,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
return $this->activityType; return $this->activityType;
} }
public function getUser(): User public function getUser(): ?User
{ {
return $this->user; return $this->user;
} }
@ -681,14 +681,14 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
return $this; return $this;
} }
public function setUser(UserInterface $user): self public function setUser(?User $user): self
{ {
$this->user = $user; $this->user = $user;
return $this; return $this;
} }
public function setUsers(?Collection $users): self public function setUsers(Collection $users): self
{ {
$this->users = $users; $this->users = $users;

View File

@ -20,13 +20,6 @@ use Symfony\Component\Form\FormBuilderInterface;
class LocationFilter implements FilterInterface class LocationFilter implements FilterInterface
{ {
private TranslatableStringHelper $translatableStringHelper;
public function __construct(TranslatableStringHelper $translatableStringHelper)
{
$this->translatableStringHelper = $translatableStringHelper;
}
public function addRole(): ?string public function addRole(): ?string
{ {
return null; return null;

View File

@ -225,6 +225,7 @@ class ActivityType extends AbstractType
$builder->add('user', PickUserDynamicType::class, [ $builder->add('user', PickUserDynamicType::class, [
'label' => $activityType->getLabel('user'), 'label' => $activityType->getLabel('user'),
'required' => $activityType->isRequired('user'), 'required' => $activityType->isRequired('user'),
'multiple' => false,
]); ]);
} }

View File

@ -48,7 +48,7 @@ final class PersonMenuBuilder implements LocalMenuBuilderInterface
if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) { if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) {
$menu->addChild( $menu->addChild(
$this->translator->trans('Activity list'), $this->translator->trans('Activities'),
[ [
'route' => 'chill_activity_activity_list', 'route' => 'chill_activity_activity_list',
'routeParameters' => ['person_id' => $person->getId()], 'routeParameters' => ['person_id' => $person->getId()],

View File

@ -63,7 +63,7 @@
</div> </div>
{% endif %} {% endif %}
{% if activity.user and t.userVisible %} {% if activity.user is not null and t.userVisible %}
<div class="wl-row"> <div class="wl-row">
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div> <div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
<div class="wl-col list"> <div class="wl-col list">

View File

@ -5,7 +5,7 @@
Maybe should we think about abstracting this file a bit more ? Moving it to PersonBundle ? Maybe should we think about abstracting this file a bit more ? Moving it to PersonBundle ?
#} #}
{% if context == 'calendar_accompanyingCourse' %} {% if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %}
{% import "@ChillCalendar/_invite.html.twig" as invite %} {% import "@ChillCalendar/_invite.html.twig" as invite %}
{% endif %} {% endif %}

View File

@ -34,10 +34,12 @@
<div class="item-row separator"> <div class="item-row separator">
<dl class="chill_view_data"> <dl class="chill_view_data">
{%- if entity.user is not null %}
<dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt> <dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt>
<dd> <dd>
<span class="badge-user">{{ entity.user|chill_entity_render_box }}</span> <span class="badge-user">{{ entity.user|chill_entity_render_box }}</span>
</dd> </dd>
{% endif %}
{%- if entity.scope -%} {%- if entity.scope -%}
<dt class="inline">{{ 'Scope'|trans }}</dt> <dt class="inline">{{ 'Scope'|trans }}</dt>

View File

@ -147,6 +147,32 @@ class ActivityContext implements
} }
} }
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$denormalized = [];
foreach (['mainPerson', 'person1', 'person2'] as $k) {
if (null !== ($id = ($data[$k] ?? null))) {
$denormalized[$k] = $this->personRepository->find($id);
} else {
$denormalized[$k] = null;
}
}
return $denormalized;
}
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$normalized = [];
foreach (['mainPerson', 'person1', 'person2'] as $k) {
$normalized[$k] = null === $data[$k] ? null : $data[$k]->getId();
}
return $normalized;
}
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
{ {
if (!$entity instanceof Activity) { if (!$entity instanceof Activity) {
@ -214,32 +240,6 @@ class ActivityContext implements
return $options['mainPerson'] || $options['person1'] || $options['person2']; return $options['mainPerson'] || $options['person1'] || $options['person2'];
} }
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$normalized = [];
foreach (['mainPerson', 'person1', 'person2'] as $k) {
$normalized[$k] = null === $data[$k] ? null : $data[$k]->getId();
}
return $normalized;
}
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$denormalized = [];
foreach (['mainPerson', 'person1', 'person2'] as $k) {
if (null !== ($id = ($data[$k] ?? null))) {
$denormalized[$k] = $this->personRepository->find($id);
} else {
$denormalized[$k] = null;
}
}
return $denormalized;
}
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
{ {
$storedObject->setTitle($this->translatableStringHelper->localize($template->getName())); $storedObject->setTitle($this->translatableStringHelper->localize($template->getName()));

View File

@ -34,10 +34,16 @@ use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository; use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
use DateTime; use DateTime;
use libphonenumber\PhoneNumber; use libphonenumber\PhoneNumber;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use function in_array;
/**
* @implements DocGeneratorContextWithPublicFormInterface<AccompanyingPeriod>
* @implements DocGeneratorContextWithAdminFormInterface<AccompanyingPeriod>
*/
class ListActivitiesByAccompanyingPeriodContext implements class ListActivitiesByAccompanyingPeriodContext implements
DocGeneratorContextWithAdminFormInterface, DocGeneratorContextWithAdminFormInterface,
DocGeneratorContextWithPublicFormInterface DocGeneratorContextWithPublicFormInterface
@ -69,7 +75,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
SocialIssueRepository $socialIssueRepository, SocialIssueRepository $socialIssueRepository,
ThirdPartyRepository $thirdPartyRepository, ThirdPartyRepository $thirdPartyRepository,
TranslatableStringHelperInterface $translatableStringHelper, TranslatableStringHelperInterface $translatableStringHelper,
UserRepository $userRepository UserRepository $userRepository,
) { ) {
$this->accompanyingPeriodContext = $accompanyingPeriodContext; $this->accompanyingPeriodContext = $accompanyingPeriodContext;
$this->activityACLAwareRepository = $activityACLAwareRepository; $this->activityACLAwareRepository = $activityACLAwareRepository;
@ -100,14 +106,86 @@ class ListActivitiesByAccompanyingPeriodContext implements
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
{ {
$this->accompanyingPeriodContext->buildPublicForm($builder, $template, $entity); $this->accompanyingPeriodContext->buildPublicForm($builder, $template, $entity);
$builder
->add('myActivitiesOnly', CheckboxType::class, [
'required' => false,
'label' => 'docgen.myActivitiesOnly',
])
->add('myWorksOnly', CheckboxType::class, [
'required' => false,
'label' => 'docgen.myWorksOnly',
]);
} }
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$denormalized = $this->accompanyingPeriodContext->contextGenerationDataDenormalize($template, $entity, $data);
foreach (['myActivitiesOnly', 'myWorksOnly'] as $k) {
$denormalized[$k] = $data[$k];
}
return $denormalized;
}
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
$normalized = $this->accompanyingPeriodContext->contextGenerationDataNormalize($template, $entity, $data);
foreach (['myActivitiesOnly', 'myWorksOnly'] as $k) {
$normalized[$k] = $data[$k] ?? false;
}
return $normalized;
}
private function filterActivitiesByUser(array $activities, User $user): array
{
return array_filter(
$activities,
function ($activity) use ($user) {
$activityUsernames = array_map(static function ($user) {
return $user['username'];
}, $activity['users'] ?? []);
return in_array($user->getUsername(), $activityUsernames, true);
}
);
}
private function filterWorksByUser(array $works, User $user): array
{
return array_filter(
$works,
function ($work) use ($user) {
$workUsernames = array_map(static function ($user) {
return $user['username'];
}, $work['referrers'] ?? []);
return in_array($user->getUsername(), $workUsernames, true);
}
);
}
public function getData(DocGeneratorTemplate $template, object $entity, array $contextGenerationData = []): array
{ {
$data = $this->accompanyingPeriodContext->getData($template, $entity, $contextGenerationData); $data = $this->accompanyingPeriodContext->getData($template, $entity, $contextGenerationData);
$data['activities'] = $this->getActivitiesSimplified($entity); $activities = $this->getActivitiesSimplified($entity);
$myActivitiesOnly = $contextGenerationData['myActivitiesOnly'];
if ($myActivitiesOnly && isset($contextGenerationData['creator'])) {
$activities = $this->filterActivitiesByUser($activities, $contextGenerationData['creator']);
}
$data['activities'] = $activities;
$myWorksOnly = $contextGenerationData['myWorksOnly'];
if ($myWorksOnly && isset($contextGenerationData['creator'])) {
$data['course']['works'] = $this->filterWorksByUser($data['course']['works'], $contextGenerationData['creator']);
}
return $data; return $data;
} }
@ -143,17 +221,7 @@ class ListActivitiesByAccompanyingPeriodContext implements
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool
{ {
return $this->accompanyingPeriodContext->hasPublicForm($template, $entity); return true;
}
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
return $this->accompanyingPeriodContext->contextGenerationDataNormalize($template, $entity, $data);
}
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array
{
return $this->accompanyingPeriodContext->contextGenerationDataDenormalize($template, $entity, $data);
} }
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void

View File

@ -121,14 +121,14 @@ final class ActivityControllerTest extends WebTestCase
$client->getResponse()->getStatusCode(), $client->getResponse()->getStatusCode(),
'Unexpected HTTP status code for GET /activity/' 'Unexpected HTTP status code for GET /activity/'
); );
$crawler = $client->click($crawler->selectLink('Ajouter une nouvelle activité') $crawler = $client->click($crawler->selectLink('Ajouter un nouvel échange')
->link()); ->link());
$reason1 = $this->getRandomActivityReason(); $reason1 = $this->getRandomActivityReason();
$reason2 = $this->getRandomActivityReason([$reason1->getId()]); $reason2 = $this->getRandomActivityReason([$reason1->getId()]);
// Fill in the form and submit it // Fill in the form and submit it
$form = $crawler->selectButton('Ajouter une nouvelle activité')->form([ $form = $crawler->selectButton('Ajouter un nouvel échange')->form([
'chill_activitybundle_activity' => [ 'chill_activitybundle_activity' => [
'date' => '15-01-2015', 'date' => '15-01-2015',
'durationTime' => 600, 'durationTime' => 600,
@ -152,9 +152,9 @@ final class ActivityControllerTest extends WebTestCase
); );
// Edit the entity // Edit the entity
$crawler = $client->click($crawler->selectLink("Modifier l'activité")->link()); $crawler = $client->click($crawler->selectLink("Modifier l'échange")->link());
$form = $crawler->selectButton("Sauver l'activité")->form([ $form = $crawler->selectButton("Sauver l'échange")->form([
'chill_activitybundle_activity' => [ 'chill_activitybundle_activity' => [
'date' => '25-01-2015', 'date' => '25-01-2015',
// 'remark' => 'Foo' // 'remark' => 'Foo'

View File

@ -112,9 +112,9 @@ final class ActivityTypeTest extends KernelTestCase
'attendee' => true, 'attendee' => true,
]]); ]]);
// var_dump($form->getErrors()->count()); var_dump($form->isValid()); // var_dump($form->getErrors()->count()); var_dump($form->isValid());
// foreach($form->getErrors() as $e) { fwrite(STDOUT, var_dump($e->getMessage())); } // foreach($form->getErrors() as $e) { fwrite(STDOUT, var_dump($e->getMessage())); }
// var_dump($form->getErrors()); // var_dump($form->getErrors());
$this->assertTrue($form->isSynchronized(), 'Test the form is synchronized'); $this->assertTrue($form->isSynchronized(), 'Test the form is synchronized');
$this->assertTrue($form->isValid(), 'test the form is valid'); $this->assertTrue($form->isValid(), 'test the form is valid');

View File

@ -1,41 +1,41 @@
#general #general
Show the activity: Voir l'activité Show the activity: Voir l'échange
Edit the activity: Modifier l'activité Edit the activity: Modifier l'échange
Activity: Activité Activity: Échange
Duration time: Durée Duration time: Durée
Duration Time: Durée Duration Time: Durée
durationTime: durée durationTime: durée
Travel time: Durée de déplacement Travel time: Durée de déplacement
Attendee: Présence de la personne Attendee: Présence de l'usager
attendee: présence de la personne attendee: présence de l'usager
list_reasons: liste des sujets list_reasons: liste des sujets
user_username: nom de l'utilisateur user_username: nom de l'utilisateur
circle_name: nom du cercle circle_name: nom du cercle
Remark: Commentaire Remark: Commentaire
No comments: Aucun commentaire No comments: Aucun commentaire
Add a new activity: Ajouter une nouvelle activité Add a new activity: Ajouter une nouvel échange
Activity list: Liste des activités Activity list: Liste des échanges
present: présent present: présent
not present: absent not present: absent
Delete: Supprimer Delete: Supprimer
Update: Mettre à jour Update: Mettre à jour
Update activity: Modifier l'activité Update activity: Modifier l'échange
Scope: Cercle Scope: Cercle
Activity data: Données de l'activité Activity data: Données de l'échange
Activity location: Localisation de l'activité Activity location: Localisation de l'échange
No reason associated: Aucun sujet No reason associated: Aucun sujet
No social issues associated: Aucune problématique sociale No social issues associated: Aucune problématique sociale
No social actions associated: Aucune action d'accompagnement No social actions associated: Aucune action d'accompagnement
There isn't any activities.: Aucune activité enregistrée. There isn't any activities.: Aucun échange enregistré.
type_name: type de l'activité type_name: type de l'échange
person_firstname: prénom person_firstname: prénom
person_lastname: nom de famille person_lastname: nom de famille
person_id: identifiant de la personne person_id: identifiant de l'usager
Type: Type Type: Type
Invisible: Invisible Invisible: Invisible
Optional: Optionnel Optional: Optionnel
Required: Obligatoire Required: Obligatoire
Persons: Personnes Persons: Usagers
Users: Utilisateurs Users: Utilisateurs
Emergency: Urgent Emergency: Urgent
Sent received: Entrant / Sortant Sent received: Entrant / Sortant
@ -50,10 +50,10 @@ received: Reçu
#forms #forms
Activity creation: Nouvelle activité Activity creation: Nouvel échange
Create: Créer Create: Créer
Back to the list: Retour à la liste Back to the list: Retour à la liste
Save activity: Sauver l'activité Save activity: Sauver l'échange
Reset form: Remise à zéro du formulaire Reset form: Remise à zéro du formulaire
Choose the duration: Choisir la durée Choose the duration: Choisir la durée
Choose a type: Choisir un type Choose a type: Choisir un type
@ -90,40 +90,40 @@ activity:
No documents: Aucun document No documents: Aucun document
#timeline #timeline
'%user% has done an %activity_type%': '%user% a effectué une activité de type "%activity_type%"' '%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"'
#controller #controller
'Success : activity created!': L'activité a été créée. 'Success : activity created!': L'échange a été créé.
'The form is not valid. The activity has not been created !': Le formulaire est invalide. L'activité n'a pas été créée. 'The form is not valid. The activity has not been created !': Le formulaire est invalide. L'échange n'a pas été créé.
'Success : activity updated!': L'activité a été mise à jour. 'Success : activity updated!': L'échange a été mis à jour.
'The form is not valid. The activity has not been updated !': Le formulaire est invalide. L'activité n'a pas été mise à jour. 'The form is not valid. The activity has not been updated !': Le formulaire est invalide. L'échange n'a pas été mise à jour.
# ROLES # ROLES
CHILL_ACTIVITY_CREATE: Créer une activité CHILL_ACTIVITY_CREATE: Créer un échange
CHILL_ACTIVITY_UPDATE: Modifier une activité CHILL_ACTIVITY_UPDATE: Modifier un échange
CHILL_ACTIVITY_SEE: Voir une activité CHILL_ACTIVITY_SEE: Voir un échange
CHILL_ACTIVITY_SEE_DETAILS: Voir le détail des activités CHILL_ACTIVITY_SEE_DETAILS: Voir le détail des échanges
CHILL_ACTIVITY_DELETE: Supprimer une activité CHILL_ACTIVITY_DELETE: Supprimer un échange
CHILL_ACTIVITY_STATS: Statistique des activités CHILL_ACTIVITY_STATS: Statistique des échanges
CHILL_ACTIVITY_LIST: Liste des activités CHILL_ACTIVITY_LIST: Liste des échanges
# admin # admin
Activities: Activités Activities: Échanges
Activity configuration: Configuration des activités Activity configuration: Configuration des échanges
Activity configuration menu: Configuration des activités Activity configuration menu: Configuration des échanges
Activity types: Types d'activité Activity types: Types d'échange
Activity type configuration: Configuration des categories d'activités Activity type configuration: Configuration des catégories d'échanges
Activity Reasons: Sujets d'une activité Activity Reasons: Sujets d'un échange
Activity Reasons Category: Catégories de sujet d'activités Activity Reasons Category: Catégories de sujet d'échanges
Activity Types Categories: Catégories des types d'activité Activity Types Categories: Catégories des types d'échange
Activity Presences: Presences aux activités Activity Presences: Presences aux échanges
Associated activity reason category is inactive: La catégorie de sujet attachée est inactive Associated activity reason category is inactive: La catégorie de sujet attachée est inactive
# Crud # Crud
crud: crud:
activity_type: activity_type:
title_new: Nouveau type d'activité title_new: Nouveau type d'échange
title_edit: Edition d'un type d'activité title_edit: Edition d'un type d'activité
activity_type_category: activity_type_category:
title_new: Nouvelle catégorie de type d'activité title_new: Nouvelle catégorie de type d'activité
@ -159,8 +159,8 @@ Create a new activity presence: Créer une nouvelle "Présence aux activités"
# activity type type admin # activity type type admin
ActivityType list: Types d'activités ActivityType list: Types d'activités
Create a new activity type: Créer un nouveau type d'activité Create a new activity type: Créer un nouveau type d'activité
Persons visible: Visibilité du champ Personnes Persons visible: Visibilité du champ Usagers
Persons label: Libellé du champ Personnes Persons label: Libellé du champ Usagers
User visible: Visibilité du champ Utilisateur User visible: Visibilité du champ Utilisateur
User label: Libellé du champ Utilisateur User label: Libellé du champ Utilisateur
Date visible: Visibilité du champ Date Date visible: Visibilité du champ Date
@ -183,8 +183,8 @@ Private comment visible: Visibilité du champ Commentaire Privé
Private comment label: Libellé du champ Commentaire Privé Private comment label: Libellé du champ Commentaire Privé
Emergency visible: Visibilité du champ Urgent Emergency visible: Visibilité du champ Urgent
Emergency label: Libellé du champ Urgent Emergency label: Libellé du champ Urgent
Accompanying period visible: Visibilité du champ Période d'accompagnement Accompanying period visible: Visibilité du champ parcours d'accompagnement
Accompanying period label: Libellé du champ Période d'accompagnement Accompanying period label: Libellé du champ parcours d'accompagnement
Social issues visible: Visibilité du champ Problématiques sociales Social issues visible: Visibilité du champ Problématiques sociales
Social issues label: Libellé du champ Problématiques sociales Social issues label: Libellé du champ Problématiques sociales
Social actions visible: Visibilité du champ Action sociale Social actions visible: Visibilité du champ Action sociale
@ -206,10 +206,10 @@ Are you sure you want to remove the activity about "%name%" ?: Êtes-vous sûr d
The activity has been successfully removed.: L'activité a été supprimée. The activity has been successfully removed.: L'activité a été supprimée.
# exports # exports
Exports of activities linked to a person: Exports des activités liées à une personne Exports of activities linked to a person: Exports des activités liées à un usager
Number of activities linked to a person: Nombre d'activités liées à une personne Number of activities linked to a person: Nombre d'activités liées à un usager
Count activities linked to a person: Nombre d'activités Count activities linked to a person: Nombre d'activités
Count activities linked to a person by various parameters.: Compte le nombre d'activités enregistrées et liées à une personne en fonction de différents paramètres. Count activities linked to a person by various parameters.: Compte le nombre d'activités enregistrées et liées à un usager en fonction de différents paramètres.
Sum activity linked to a person duration: Durée des activités Sum activity linked to a person duration: Durée des activités
Sum activities linked to a person duration: Durée des activités liés à un usager Sum activities linked to a person duration: Durée des activités liés à un usager
Sum activities linked to a person duration by various parameters.: Additionne la durée des activités en fonction de différents paramètres. Sum activities linked to a person duration by various parameters.: Additionne la durée des activités en fonction de différents paramètres.
@ -244,10 +244,10 @@ Activities before this date: Activités avant cette date
"Filtered by date of activity: only between %date_from% and %date_to%": "Filtré par date de l'activité: uniquement entre %date_from% et %date_to%" "Filtered by date of activity: only between %date_from% and %date_to%": "Filtré par date de l'activité: uniquement entre %date_from% et %date_to%"
This date should be after the date given in "Implied in an activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités après cette date" This date should be after the date given in "Implied in an activity after this date" field: Cette date devrait être postérieure à la date donnée dans le champ "activités après cette date"
Filtered by person having an activity in a period: Uniquement les personnes ayant eu une activité dans la période donnée Filtered by person having an activity in a period: Uniquement les usagers ayant eu une activité dans la période donnée
Implied in an activity after this date: Impliqué dans une activité après cette date Implied in an activity after this date: Impliqué dans une activité après cette date
Implied in an activity before this date: Impliqué dans une activité avant cette date Implied in an activity before this date: Impliqué dans une activité avant cette date
Filtered by person having an activity between %date_from% and %date_to% with reasons %reasons_name%: Filtré par personnes associées à une activité entre %date_from% et %date_to% avec les sujets %reasons_name% Filtered by person having an activity between %date_from% and %date_to% with reasons %reasons_name%: Filtré par usager associées à une activité entre %date_from% et %date_to% avec les sujets %reasons_name%
Activity reasons for those activities: Sujets de ces activités Activity reasons for those activities: Sujets de ces activités
Filter by activity type: Filtrer les activités par type Filter by activity type: Filtrer les activités par type
@ -326,9 +326,11 @@ This is the minimal activity data: Activité n°
docgen: docgen:
Activity basic: Echange Activity basic: Echange
A basic context for activity: Contexte pour les activités A basic context for activity: Contexte pour les échanges
Accompanying period with a list of activities: Parcours d'accompagnement avec liste des activités Accompanying period with a list of activities: Parcours d'accompagnement avec liste des échanges
Accompanying period with a list of activities description: Ce contexte reprend les informations du parcours, et tous les activités pour un parcours. Les activités ne sont pas filtrés. Accompanying period with a list of activities description: Ce contexte reprend les informations du parcours, et tous les échanges pour un parcours. Les échanges ne sont pas filtrés.
myActivitiesOnly: Prendre en compte uniquement les échanges dans lesquels je suis intervenu
myWorksOnly: Prendre en compte uniquement les actions d'accompagnement dont je suis référent
export: export:
list: list:
@ -336,8 +338,8 @@ export:
users name: Nom des utilisateurs users name: Nom des utilisateurs
users ids: Identifiant des utilisateurs users ids: Identifiant des utilisateurs
third parties ids: Identifiant des tiers third parties ids: Identifiant des tiers
persons ids: Identifiant des personnes persons ids: Identifiant des usagers
persons name: Nom des personnes persons name: Nom des usagers
thirds parties: Tiers thirds parties: Tiers
date: Date de l'activité date: Date de l'activité
locationName: Localisation locationName: Localisation

View File

@ -47,10 +47,10 @@ Reasons: Onderwerpen
#forms #forms
Activity creation: Nouvelle activité Activity creation: Nouvel échange
Create: Créer Create: Créer
Back to the list: Retour à la liste Back to the list: Retour à la liste
Save activity: Sauver l'activité Save activity: Sauver l'échange
Reset form: Remise à zéro du formulaire Reset form: Remise à zéro du formulaire
Choose the duration: Choisir la durée Choose the duration: Choisir la durée
Choose a type: Choisir un type Choose a type: Choisir un type
@ -79,43 +79,43 @@ activity:
No documents: Aucun document No documents: Aucun document
#timeline #timeline
'%user% has done an %activity_type%': '%user% a effectué une activité de type "%activity_type%"' '%user% has done an %activity_type%': '%user% a effectué un échange de type "%activity_type%"'
#controller #controller
'Success : activity created!': L'activité a été créée. 'Success : activity created!': L'échange a été créé.
'The form is not valid. The activity has not been created !': Le formulaire est invalide. L'activité n'a pas été créée. 'The form is not valid. The activity has not been created !': Le formulaire est invalide. L'échange n'a pas été créé.
'Success : activity updated!': L'activité a été mise à jour. 'Success : activity updated!': L'échange a été mis à jour.
'The form is not valid. The activity has not been updated !': Le formulaire est invalide. L'activité n'a pas été mise à jour. 'The form is not valid. The activity has not been updated !': Le formulaire est invalide. L'échange n'a pas été mis à jour.
# ROLES # ROLES
CHILL_ACTIVITY_CREATE: Créer une activité CHILL_ACTIVITY_CREATE: Créer un échange
CHILL_ACTIVITY_UPDATE: Modifier une activité CHILL_ACTIVITY_UPDATE: Modifier un échange
CHILL_ACTIVITY_SEE: Voir une activité CHILL_ACTIVITY_SEE: Voir un échange
CHILL_ACTIVITY_SEE_DETAILS: Voir le détail des activités CHILL_ACTIVITY_SEE_DETAILS: Voir le détail des échanges
CHILL_ACTIVITY_DELETE: Supprimer une activité CHILL_ACTIVITY_DELETE: Supprimer un échange
CHILL_ACTIVITY_STATS: Statistique des activités CHILL_ACTIVITY_STATS: Statistique des échanges
CHILL_ACTIVITY_LIST: Liste des activités CHILL_ACTIVITY_LIST: Liste des échanges
# admin # admin
Activities: Activités Activities: Échanges
Activity configuration: Configuration des activités Activity configuration: Configuration des échanges
Activity configuration menu: Configuration des activités Activity configuration menu: Configuration des échanges
Activity types: Types d'activité Activity types: Types d'échange
Activity type configuration: Configuration des categories d'activités Activity type configuration: Configuration des categories d'échanges
Activity Reasons: Sujets d'une activité Activity Reasons: Sujets d'un échange
Activity Reasons Category: Catégories de sujet d'activités Activity Reasons Category: Catégories de sujet d'échanges
Activity Types Categories: Catégories des types d'activité Activity Types Categories: Catégories des types d'échanges
Activity Presences: Presences des activités Activity Presences: Presences des échanges
# Crud # Crud
crud: crud:
activity_type: activity_type:
title_new: Nouveau type d'activité title_new: Nouveau type d'échange
title_edit: Edition d'un type d'activité title_edit: Edition d'un type d'échange
activity_type_category: activity_type_category:
title_new: Nouvelle catégorie de type d'activité title_new: Nouvelle catégorie de type d'échange
title_edit: Edition d'une catégorie de type d'activité title_edit: Edition d'une catégorie de type d'échange
# activity reason admin # activity reason admin
ActivityReason list: Liste des sujets ActivityReason list: Liste des sujets
@ -124,7 +124,7 @@ Active: Actif
Category: Catégorie Category: Catégorie
ActivityReason creation: Nouveau sujet ActivityReason creation: Nouveau sujet
ActivityReason edit: Modification d'un sujet ActivityReason edit: Modification d'un sujet
ActivityReason: Sujet d'activité ActivityReason: Sujet d'échange
The entity is inactive and won't be proposed: Le sujet est inactif et ne sera pas proposé The entity is inactive and won't be proposed: Le sujet est inactif et ne sera pas proposé
The entity is active and will be proposed: Le sujet est actif et sera proposé The entity is active and will be proposed: Le sujet est actif et sera proposé
@ -133,13 +133,13 @@ ActivityReasonCategory list: Catégories de sujets
Create a new activity category reason: Créer une nouvelle catégorie Create a new activity category reason: Créer une nouvelle catégorie
ActivityReasonCategory creation: Nouvelle catégorie de sujet ActivityReasonCategory creation: Nouvelle catégorie de sujet
ActivityReasonCategory edit: Modification d'une catégorie de sujet ActivityReasonCategory edit: Modification d'une catégorie de sujet
ActivityReasonCategory: Catégorie de sujet d'activité ActivityReasonCategory: Catégorie de sujet d'échange
ActivityReasonCategory is active and will be proposed: La catégorie est active et sera proposée ActivityReasonCategory is active and will be proposed: La catégorie est active et sera proposée
ActivityReasonCategory is inactive and won't be proposed: La catégorie est inactive et ne sera pas proposée ActivityReasonCategory is inactive and won't be proposed: La catégorie est inactive et ne sera pas proposée
# activity type type admin # activity type type admin
ActivityType list: Types d'activités ActivityType list: Types d'échanges
Create a new activity type: Créer un nouveau type d'activité Create a new activity type: Créer un nouveau type d'échange
Persons visible: Visibilité du champ Personnes Persons visible: Visibilité du champ Personnes
Persons label: Libellé du champ Personnes Persons label: Libellé du champ Personnes
User visible: Visibilité du champ Utilisateur User visible: Visibilité du champ Utilisateur
@ -177,20 +177,20 @@ Documents label: Libellé du champ Documents
# activity type category admin # activity type category admin
ActivityTypeCategory list: Liste des catégories des types d'activité ActivityTypeCategory list: Liste des catégories des types d'activité
Create a new activity type category: Créer une nouvelle catégorie de type d'activité Create a new activity type category: Créer une nouvelle catégorie de type d'échange
# activity delete # activity delete
Remove activity: Supprimer une activité Remove activity: Supprimer un échange
Are you sure you want to remove the activity about "%name%" ?: Êtes-vous sûr de vouloir supprimer une activité qui concerne "%name%" ? Are you sure you want to remove the activity about "%name%" ?: Êtes-vous sûr de vouloir supprimer un échange qui concerne "%name%" ?
The activity has been successfully removed.: L'activité a été supprimée. The activity has been successfully removed.: L'échange a été supprimée.
# exports # exports
Count activities: Nombre d'activités Count activities: Nombre d'échanges
Count activities by various parameters.: Compte le nombre d'activités enregistrées en fonction de différents paramètres. Count activities by various parameters.: Compte le nombre d'échanges enregistrées en fonction de différents paramètres.
Sum activity duration: Total de la durée des activités Sum activity duration: Total de la durée des échanges
Sum activities duration by various parameters.: Additionne la durée des activités en fonction de différents paramètres. Sum activities duration by various parameters.: Additionne la durée des échanges en fonction de différents paramètres.
List activities: Liste les activités List activities: Liste les échanges
Number of activities: Nombre d'activités Number of activities: Nombre d'échanges
#filters #filters
Filter by reason: Filtrer par sujet d'activité Filter by reason: Filtrer par sujet d'activité

View File

@ -1,22 +1,22 @@
The reasons's level should not be empty: Le niveau du sujet ne peut pas être vide The reasons's level should not be empty: Le niveau du sujet ne peut pas être vide
At least one reason must be choosen: Au moins un sujet doit être choisi At least one reason must be choosen: Au moins un sujet doit être choisi
For this type of activity, you must add at least one person: Pour ce type d'activité, vous devez ajouter au moins un usager For this type of activity, you must add at least one person: Pour ce type d'échange, vous devez ajouter au moins un usager
For this type of activity, you must add at least one user: Pour ce type d'activité, vous devez ajouter au moins un utilisateur For this type of activity, you must add at least one user: Pour ce type d'échange, vous devez ajouter au moins un utilisateur
For this type of activity, you must add at least one third party: Pour ce type d'activité, vous devez ajouter au moins un tiers For this type of activity, you must add at least one third party: Pour ce type d'échange, vous devez ajouter au moins un tiers
For this type of activity, user is required: Pour ce type d'activité, l'utilisateur est requis For this type of activity, user is required: Pour ce type d'échange, l'utilisateur est requis
For this type of activity, date is required: Pour ce type d'activité, la date est requise For this type of activity, date is required: Pour ce type d'échange, la date est requise
For this type of activity, location is required: Pour ce type d'activité, la localisation est requise For this type of activity, location is required: Pour ce type d'échange, la localisation est requise
For this type of activity, attendee is required: Pour ce type d'activité, le champ "Présence de la personne" est requis For this type of activity, attendee is required: Pour ce type d'échange, le champ "Présence de l'usager" est requis
For this type of activity, duration time is required: Pour ce type d'activité, la durée est requise For this type of activity, duration time is required: Pour ce type d'échange, la durée est requise
For this type of activity, travel time is required: Pour ce type d'activité, la durée du trajet est requise For this type of activity, travel time is required: Pour ce type d'échange, la durée du trajet est requise
For this type of activity, reasons is required: Pour ce type d'activité, le champ "sujet" est requis For this type of activity, reasons is required: Pour ce type d'échange, le champ "sujet" est requis
For this type of activity, comment is required: Pour ce type d'activité, un commentaire est requis For this type of activity, comment is required: Pour ce type d'échange, un commentaire est requis
For this type of activity, sent/received is required: Pour ce type d'activité, le champ Entrant/Sortant est requis For this type of activity, sent/received is required: Pour ce type d'échange, le champ Entrant/Sortant est requis
For this type of activity, document is required: Pour ce type d'activité, un document est requis For this type of activity, document is required: Pour ce type d'échange, un document est requis
For this type of activity, emergency is required: Pour ce type d'activité, le champ "Urgent" est requis For this type of activity, emergency is required: Pour ce type d'échange, le champ "Urgent" est requis
For this type of activity, accompanying period is required: Pour ce type d'activité, le parcours d'accompagnement est requis For this type of activity, accompanying period is required: Pour ce type d'échange, le parcours d'accompagnement est requis
For this type of activity, you must add at least one social issue: Pour ce type d'activité, vous devez ajouter au moins une problématique sociale For this type of activity, you must add at least one social issue: Pour ce type d'échange, vous devez ajouter au moins une problématique sociale
For this type of activity, you must add at least one social action: Pour ce type d'activité, vous devez indiquer au moins une action sociale For this type of activity, you must add at least one social action: Pour ce type d'échange, vous devez indiquer au moins une action sociale
# admin # admin
This parameter must be equal to social issue parameter: Ce paramètre doit être égal au paramètre "Visibilité du champs Problématiques sociales" This parameter must be equal to social issue parameter: Ce paramètre doit être égal au paramètre "Visibilité du champs Problématiques sociales"

View File

@ -160,8 +160,8 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
return 'export.aside_activity.main_center'; return 'export.aside_activity.main_center';
} }
/** @var Center $c */
if (null === $value || '' === $value || null === $c = $this->centerRepository->find($value)) { if (null === $value || '' === $value || null === $c = $this->centerRepository->find($value)) {
/** @var Center $c */
return ''; return '';
} }
@ -190,10 +190,6 @@ final class ListAsideActivity implements ListInterface, GroupedExportInterface
]; ];
} }
/**
* @param QueryBuilder $query
* @param array $data
*/
public function getResult($query, $data): array public function getResult($query, $data): array
{ {
return $query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY); return $query->getQuery()->getResult(AbstractQuery::HYDRATE_ARRAY);

View File

@ -28,22 +28,17 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use function in_array; use function in_array;
final class AsideActivityFormType extends AbstractType final class AsideActivityFormType extends AbstractType
{ {
private TokenStorageInterface $storage;
private array $timeChoices; private array $timeChoices;
public function __construct( public function __construct(
ParameterBagInterface $parameterBag, ParameterBagInterface $parameterBag,
TokenStorageInterface $storage
) { ) {
$this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration'); $this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration');
$this->storage = $storage;
} }
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)

View File

@ -44,17 +44,6 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
'order' => 11, 'order' => 11,
'icons' => ['plus'], 'icons' => ['plus'],
]); ]);
$menu->addChild($this->translator->trans('Phonecall'), [
'route' => 'chill_crud_aside_activity_new',
'routeParameters' => [
'type' => 1,
'duration' => 900,
],
])
->setExtras([
'order' => 12,
'icons' => ['plus'],
]);
} }
} }

View File

@ -21,7 +21,7 @@ Type: Type
Invisible: Invisible Invisible: Invisible
Optional: Optionnel Optional: Optionnel
Required: Obligatoire Required: Obligatoire
Persons: Personnes Persons: Usagers
Users: Utilisateurs Users: Utilisateurs
Emergency: Urgent Emergency: Urgent
by: "Par " by: "Par "
@ -50,7 +50,7 @@ For agent: Pour l'utilisateur
date: Date date: Date
Duration: Durée Duration: Durée
Note: Note Note: Note
Choose the agent for whom this activity is created: Choisissez l'agent pour qui l'activité est créée Choose the agent for whom this activity is created: Choisissez l'agent pour qui l'échange est créé
Choose the activity category: Choisir la catégorie Choose the activity category: Choisir la catégorie
#Duration #Duration

View File

@ -33,27 +33,15 @@ class ElementController extends AbstractController
{ {
private CalculatorManager $calculator; private CalculatorManager $calculator;
private LoggerInterface $chillMainLogger;
private EntityManagerInterface $em;
private ResourceRepository $resourceRepository; private ResourceRepository $resourceRepository;
private ChargeRepository $chargeRepository; private ChargeRepository $chargeRepository;
private TranslatorInterface $translator;
public function __construct( public function __construct(
EntityManagerInterface $em,
TranslatorInterface $translator,
LoggerInterface $chillMainLogger,
CalculatorManager $calculator, CalculatorManager $calculator,
ResourceRepository $resourceRepository, ResourceRepository $resourceRepository,
ChargeRepository $chargeRepository, ChargeRepository $chargeRepository,
) { ) {
$this->em = $em;
$this->translator = $translator;
$this->chillMainLogger = $chillMainLogger;
$this->calculator = $calculator; $this->calculator = $calculator;
$this->resourceRepository = $resourceRepository; $this->resourceRepository = $resourceRepository;
$this->chargeRepository = $chargeRepository; $this->chargeRepository = $chargeRepository;

View File

@ -30,7 +30,7 @@ final class ChargeKindRepository implements ChargeKindRepositoryInterface
} }
/** /**
* @return ChargeType[] * @return array<ChargeKind>
*/ */
public function findAll(): array public function findAll(): array
{ {
@ -38,7 +38,7 @@ final class ChargeKindRepository implements ChargeKindRepositoryInterface
} }
/** /**
* @return ChargeType[] * @return array<ChargeKind>
*/ */
public function findAllActive(): array public function findAllActive(): array
{ {
@ -53,7 +53,7 @@ final class ChargeKindRepository implements ChargeKindRepositoryInterface
} }
/** /**
* @return ChargeType[] * @return array<ChargeKind>
*/ */
public function findAllByType(string $type): array public function findAllByType(string $type): array
{ {
@ -64,7 +64,7 @@ final class ChargeKindRepository implements ChargeKindRepositoryInterface
* @param mixed|null $limit * @param mixed|null $limit
* @param mixed|null $offset * @param mixed|null $offset
* *
* @return ChargeType[] * @return array<ChargeKind>
*/ */
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{ {

View File

@ -19,25 +19,25 @@ interface ChargeKindRepositoryInterface extends ObjectRepository
public function find($id): ?ChargeKind; public function find($id): ?ChargeKind;
/** /**
* @return ChargeType[] * @return array<ChargeKind>
*/ */
public function findAll(): array; public function findAll(): array;
/** /**
* @return ChargeType[] * @return array<ChargeKind>
*/ */
public function findAllActive(): array; public function findAllActive(): array;
/** /**
* @return ChargeType[] * @return array<ChargeKind>
*/ */
public function findAllByType(string $type): array; public function findAllByType(string $type): array;
/** /**
* @param mixed|null $limit * @param int|null $limit
* @param mixed|null $offset * @param int|null $offset
* *
* @return ChargeType[] * @return array<ChargeKind>
*/ */
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array; public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;

View File

@ -30,7 +30,7 @@ final class ResourceKindRepository implements ResourceKindRepositoryInterface
} }
/** /**
* @return ResourceType[] * @return list<ResourceKind>
*/ */
public function findAll(): array public function findAll(): array
{ {
@ -38,7 +38,7 @@ final class ResourceKindRepository implements ResourceKindRepositoryInterface
} }
/** /**
* @return ResourceType[] * @return list<ResourceKind>
*/ */
public function findAllActive(): array public function findAllActive(): array
{ {
@ -58,7 +58,7 @@ final class ResourceKindRepository implements ResourceKindRepositoryInterface
} }
/** /**
* @return ResourceType[] * @return list<ResourceKind>
*/ */
public function findAllByType(string $type): array public function findAllByType(string $type): array
{ {
@ -69,7 +69,7 @@ final class ResourceKindRepository implements ResourceKindRepositoryInterface
* @param mixed|null $limit * @param mixed|null $limit
* @param mixed|null $offset * @param mixed|null $offset
* *
* @return ResourceType[] * @return list<ResourceKind>
*/ */
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
{ {

View File

@ -19,25 +19,25 @@ interface ResourceKindRepositoryInterface extends ObjectRepository
public function find($id): ?ResourceKind; public function find($id): ?ResourceKind;
/** /**
* @return ResourceType[] * @return list<ResourceKind>
*/ */
public function findAll(): array; public function findAll(): array;
/** /**
* @return ResourceType[] * @return list<ResourceKind>
*/ */
public function findAllActive(): array; public function findAllActive(): array;
/** /**
* @return ResourceType[] * @return list<ResourceKind>
*/ */
public function findAllByType(string $type): array; public function findAllByType(string $type): array;
/** /**
* @param mixed|null $limit * @param int|null $limit
* @param mixed|null $offset * @param int|null $offset
* *
* @return ResourceType[] * @return list<ResourceKind>
*/ */
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array; public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array;

View File

@ -29,7 +29,7 @@ End of validity period: Fin de la période de validité
Total: Total Total: Total
Create new resource: Créer une nouvelle ressource Create new resource: Créer une nouvelle ressource
Create new charge: Créer une nouvelle charge Create new charge: Créer une nouvelle charge
See person: Voir personne See person: Voir usagers
There isn't any element recorded: Aucun élément enregistré There isn't any element recorded: Aucun élément enregistré
No resources registered: Aucune ressource enregistrée No resources registered: Aucune ressource enregistrée

View File

@ -207,6 +207,7 @@ class CalendarController extends AbstractController
'entityClassName' => Calendar::class, 'entityClassName' => Calendar::class,
'entityId' => $entity->getId(), 'entityId' => $entity->getId(),
'template' => $template->getId(), 'template' => $template->getId(),
'returnPath' => $request->getRequestUri(),
]); ]);
} }
} }
@ -377,7 +378,9 @@ class CalendarController extends AbstractController
$this->addFlash('success', $this->translator->trans('Success : calendar item created!')); $this->addFlash('success', $this->translator->trans('Success : calendar item created!'));
if ($form->get('save_and_upload_doc')->isClicked()) { if ($form->get('save_and_upload_doc')->isClicked()) {
return $this->redirectToRoute('chill_calendar_calendardoc_new', ['id' => $entity->getId()]); return $this->redirectToRoute('chill_calendar_calendardoc_new', [
'id' => $entity->getId()
]);
} }
foreach ($templates as $template) { foreach ($templates as $template) {
@ -386,6 +389,7 @@ class CalendarController extends AbstractController
'entityClassName' => Calendar::class, 'entityClassName' => Calendar::class,
'entityId' => $entity->getId(), 'entityId' => $entity->getId(),
'template' => $template->getId(), 'template' => $template->getId(),
'returnPath' => $this->generateUrl('chill_calendar_calendar_edit', ['id' => $entity->getId()]),
]); ]);
} }
} }
@ -526,14 +530,20 @@ class CalendarController extends AbstractController
'comment' => $calendar->getComment()->getComment(), 'comment' => $calendar->getComment()->getComment(),
]; ];
return $this->redirectToRoute( $routeParams = [
'chill_activity_activity_new',
[
'accompanying_period_id' => $calendar->getAccompanyingPeriod()->getId(),
'activityData' => $activityData, 'activityData' => $activityData,
'returnPath' => $request->query->get('returnPath', null), 'returnPath' => $request->query->get('returnPath', null),
] ];
);
if ($calendar->getContext() === 'accompanying_period') {
$routeParams['accompanying_period_id'] = $calendar->getAccompanyingPeriod()->getId();
} elseif ($calendar->getContext() === 'person') {
$routeParams['person_id'] = $calendar->getPerson()->getId();
} else {
throw new RuntimeException('context not found for this calendar');
}
return $this->redirectToRoute('chill_activity_activity_new', $routeParams);
} }
private function buildListFilterOrder(): FilterOrderHelper private function buildListFilterOrder(): FilterOrderHelper
@ -544,21 +554,6 @@ class CalendarController extends AbstractController
return $filterOrder->build(); return $filterOrder->build();
} }
private function buildParamsToUrl(?User $user, ?AccompanyingPeriod $accompanyingPeriod): array
{
$params = [];
if (null !== $user) {
$params['user_id'] = $user->getId();
}
if (null !== $accompanyingPeriod) {
$params['id'] = $accompanyingPeriod->getId();
}
return $params;
}
/** /**
* Creates a form to delete a Calendar entity by id. * Creates a form to delete a Calendar entity by id.
*/ */

View File

@ -29,6 +29,7 @@ use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\ReadableCollection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use LogicException; use LogicException;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
@ -92,6 +93,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
/** /**
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod", inversedBy="calendars") * @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod", inversedBy="calendars")
* @Serializer\Groups({"calendar:read", "read"})
*/ */
private ?AccompanyingPeriod $accompanyingPeriod = null; private ?AccompanyingPeriod $accompanyingPeriod = null;
@ -507,10 +509,10 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
} }
/** /**
* @return Collection|User[] * @return ReadableCollection<(int|string), User>
* @Serializer\Groups({"calendar:read", "read"}) * @Serializer\Groups({"calendar:read", "read"})
*/ */
public function getUsers(): Collection public function getUsers(): ReadableCollection
{ {
return $this->getInvites()->map(static function (Invite $i) { return $this->getInvites()->map(static function (Invite $i) {
return $i->getUser(); return $i->getUser();

View File

@ -52,7 +52,7 @@ class CountCalendars implements ExportInterface, GroupedExportInterface
return 'Exports of calendar'; return 'Exports of calendar';
} }
public function getLabels($key, array $values, $data): Closure public function getLabels($key, array $values, $data)
{ {
if ('export_result' !== $key) { if ('export_result' !== $key) {
throw new LogicException("the key {$key} is not used by this export"); throw new LogicException("the key {$key} is not used by this export");

View File

@ -26,9 +26,6 @@ class CalendarDocEditType extends AbstractType
->add('title', TextType::class, [ ->add('title', TextType::class, [
'label' => 'chill_calendar.Document title', 'label' => 'chill_calendar.Document title',
'required' => true, 'required' => true,
])
->add('doc', StoredObjectType::class, [
'label' => 'chill_calendar.Document object',
]); ]);
} }

View File

@ -15,6 +15,7 @@
:picked="null !== this.$store.getters.getMainUser ? [this.$store.getters.getMainUser] : []" :picked="null !== this.$store.getters.getMainUser ? [this.$store.getters.getMainUser] : []"
:removableIfSet="false" :removableIfSet="false"
:displayPicked="false" :displayPicked="false"
:suggested="this.suggestedUsers"
@addNewEntity="setMainUser" @addNewEntity="setMainUser"
></pick-entity> ></pick-entity>
</div> </div>
@ -143,6 +144,7 @@ export default {
slotMinTime: '09:00:00', slotMinTime: '09:00:00',
slotMaxTime: '18:00:00', slotMaxTime: '18:00:00',
hideWeekEnds: true, hideWeekEnds: true,
previousUser: [],
} }
}, },
computed: { computed: {
@ -188,11 +190,23 @@ export default {
users.push(this.$store.getters.getUserDataById(id).user); users.push(this.$store.getters.getUserDataById(id).user);
} }
return users; return users;
},
suggestedUsers() {
const suggested = [];
this.$data.previousUser.forEach(u => {
if (u.id !== this.$store.getters.getMainUser.id) {
suggested.push(u)
} }
});
return suggested;
},
}, },
methods: { methods: {
setMainUser(user) { setMainUser({entity}) {
console.log('setMainUser APP', user); const user = entity;
console.log('setMainUser APP', entity);
if (user.id !== this.$store.getters.getMainUser && ( if (user.id !== this.$store.getters.getMainUser && (
this.$store.state.activity.calendarRange !== null this.$store.state.activity.calendarRange !== null
@ -205,6 +219,14 @@ export default {
} }
} }
// add the previous user, if any, in the previous user list (in use for suggestion)
if (null !== this.$store.getters.getMainUser) {
const suggestedUids = new Set(this.$data.previousUser.map(u => u.id));
if (!suggestedUids.has(this.$store.getters.getMainUser.id)){
this.$data.previousUser.push(this.$store.getters.getMainUser);
}
}
this.$store.dispatch('setMainUser', user); this.$store.dispatch('setMainUser', user);
this.$store.commit('showUserOnCalendar', {user, ranges: true, remotes: true}); this.$store.commit('showUserOnCalendar', {user, ranges: true, remotes: true});
}, },

View File

@ -202,6 +202,8 @@ export default {
return dispatch('associateCalendarToRange', { range: null }).then(() => { return dispatch('associateCalendarToRange', { range: null }).then(() => {
commit('setMainUser', mainUser); commit('setMainUser', mainUser);
return dispatch('fetchCalendarEvents');
}); });
}, },

View File

@ -54,7 +54,7 @@ export const mapEntity = (entity: EventInput): EventInput => {
export const createUserData = (user: User, colorIndex: number): UserData => { export const createUserData = (user: User, colorIndex: number): UserData => {
const colorId = colorIndex % COLORS.length; const colorId = colorIndex % COLORS.length;
console.log('colorId', colorId);
return { return {
user: user, user: user,
calendarRanges: [], calendarRanges: [],

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
<label class="form-label">Lieu des plages de disponibilités créées</label> <label class="form-label">{{ $t('created_availabilities') }}</label>
<vue-multiselect <vue-multiselect
v-model="pickedLocation" v-model="pickedLocation"
:options="locations" :options="locations"

View File

@ -1,5 +1,6 @@
const appMessages = { const appMessages = {
fr: { fr: {
created_availabilities: "Lieu des plages de disponibilités créées",
edit_your_calendar_range: "Planifiez vos plages de disponibilités", edit_your_calendar_range: "Planifiez vos plages de disponibilités",
show_my_calendar: "Afficher mon calendrier", show_my_calendar: "Afficher mon calendrier",
show_weekends: "Afficher les week-ends", show_weekends: "Afficher les week-ends",

View File

@ -20,15 +20,24 @@
<div class="row"> <div class="row">
<div class="col text-start"> <div class="col text-start">
{{ d.storedObject.title }} {{ d.storedObject.title }}
</div>
{% if d.dateTimeVersion < d.calendar.dateTimeVersion %} {% if d.dateTimeVersion < d.calendar.dateTimeVersion %}
<div class="col text-start">
<span class="badge bg-danger">{{ 'chill_calendar.Document outdated'|trans }}</span> <span class="badge bg-danger">{{ 'chill_calendar.Document outdated'|trans }}</span>
</div>
{% endif %} {% endif %}
</div>
<div class="col-md-auto text-center">
{{ mm.mimeIcon(d.storedObject.type) }}
</div>
<div class="col col-lg-4 text-end"> <div class="col col-lg-4 text-end">
<ul class="record_actions">
<li>
{{ d.storedObject|chill_document_button_group(d.storedObject.title, is_granted('CHILL_CALENDAR_DOC_EDIT', d), {'small': true}) }} {{ d.storedObject|chill_document_button_group(d.storedObject.title, is_granted('CHILL_CALENDAR_DOC_EDIT', d), {'small': true}) }}
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_edit', {id: d.id}) }}" class="btn btn-sm btn-edit"></a>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_calendar_calendardoc_delete', {id: d.id}) }}" class="btn btn-sm btn-delete"></a>
</li>
</ul>
</div> </div>
</div> </div>
</li> </li>

View File

@ -110,7 +110,46 @@
</div> </div>
</div> </div>
<div class="item-row"> {% if calendar.activity is not null %}
<div class="item-row separator">
<div class="item-col">
<div class="wrap-list">
<div class="wl-row">
<div class="wl-col title"><h3>{{ 'Activity'|trans }}</h3></div>
<div class="wl-col list activity-linked">
<h2 class="badge-title">
<span class="title_label"></span>
<span class="title_action">
{{ calendar.activity.type.name | localize_translatable_string }}
{% if calendar.activity.emergency %}
<span class="badge bg-danger rounded-pill fs-6 float-end">{{ 'Emergency'|trans|upper }}</span>
{% endif %}
</span>
</h2>
<ul class="record_actions">
<li class="cancel">
<span class="createdBy">
{{ 'Created by'|trans }}
<b>{{ calendar.activity.createdBy|chill_entity_render_string }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
</span>
</li>
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}
<li>
<a href="{{ chill_path_add_return_path('chill_activity_activity_show', {'id': calendar.activity.id}) }}" class="btn btn-sm btn-show" ></a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<div class="item-row separator">
<ul class="record_actions"> <ul class="record_actions">
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', calendar) %} {% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', calendar) %}
{% if templates|length == 0 %} {% if templates|length == 0 %}
@ -147,7 +186,12 @@
</li> </li>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if accompanyingCourse is defined and is_granted('CHILL_ACTIVITY_CREATE', accompanyingCourse) and calendar.activity is null %} {% if calendar.activity is null and (
(calendar.context == 'accompanying_period' and is_granted('CHILL_ACTIVITY_CREATE', calendar.accompanyingPeriod))
or
(calendar.context == 'person' and is_granted('CHILL_ACTIVITY_CREATE', calendar.person))
)
%}
<li> <li>
<a class="btn btn-create" <a class="btn btn-create"
href="{{ chill_path_add_return_path('chill_calendar_calendar_to_activity', { 'id': calendar.id }) }}"> href="{{ chill_path_add_return_path('chill_calendar_calendar_to_activity', { 'id': calendar.id }) }}">

View File

@ -9,13 +9,13 @@
{% block js %} {% block js %}
{{ parent() }} {{ parent() }}
{{ encore_entry_script_tags('mod_answer') }} {{ encore_entry_script_tags('mod_answer') }}
{{ encore_entry_script_tags('mod_async_upload') }} {{ encore_entry_script_tags('mod_document_action_buttons_group') }}
{% endblock %} {% endblock %}
{% block css %} {% block css %}
{{ parent() }} {{ parent() }}
{{ encore_entry_link_tags('mod_answer') }} {{ encore_entry_link_tags('mod_answer') }}
{{ encore_entry_link_tags('mod_async_upload') }} {{ encore_entry_link_tags('mod_document_action_buttons_group') }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}

View File

@ -24,7 +24,6 @@
{{ form_start(form) }} {{ form_start(form) }}
{{ form_row(form.title) }} {{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">

View File

@ -23,7 +23,6 @@
{{ form_start(form) }} {{ form_start(form) }}
{{ form_row(form.title) }} {{ form_row(form.title) }}
{{ form_row(form.doc) }}
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">

View File

@ -28,7 +28,7 @@
<ul class="record_actions sticky-form-buttons"> <ul class="record_actions sticky-form-buttons">
<li class="cancel"> <li class="cancel">
<a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_list_by_accompanying_period', {'id': accompanyingCourse.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a> <a class="btn btn-cancel" href="{{ chill_return_path_or('chill_calendar_calendar_edit', {'id': calendar_doc.calendar.id }) }}">{{ 'Cancel'|trans|chill_return_path_label }}</a>
</li> </li>
<li> <li>

View File

@ -31,8 +31,6 @@ use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use function count; use function count;
/**
*/
final class CalendarContext implements CalendarContextInterface final class CalendarContext implements CalendarContextInterface
{ {
private BaseContextData $baseContextData; private BaseContextData $baseContextData;
@ -150,10 +148,8 @@ final class CalendarContext implements CalendarContextInterface
} }
/** /**
* param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData
* @param mixed $entity
*/ */
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array public function getData(DocGeneratorTemplate $template, mixed $entity, array $contextGenerationData = []): array
{ {
$options = $this->getOptions($template); $options = $this->getOptions($template);
@ -274,9 +270,6 @@ final class CalendarContext implements CalendarContextInterface
return $denormalized; return $denormalized;
} }
/**
* param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData
*/
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
{ {
$options = $this->getOptions($template); $options = $this->getOptions($template);
@ -287,9 +280,6 @@ final class CalendarContext implements CalendarContextInterface
$this->entityManager->persist($doc); $this->entityManager->persist($doc);
} }
/**
* return array{askMainPerson: bool, mainPersonLabel: ?string, askThirdParty: bool, thirdPartyLabel: ?string, trackDateTime: bool} $options
*/
private function getOptions(DocGeneratorTemplate $template): array private function getOptions(DocGeneratorTemplate $template): array
{ {
return $template->getOptions(); return $template->getOptions();

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Service\DocGenerator; namespace Chill\CalendarBundle\Service\DocGenerator;
use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Entity\Calendar;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface; use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
@ -19,37 +20,9 @@ use Chill\DocStoreBundle\Entity\StoredObject;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
/** /**
* @template-extends DocGeneratorContextWithPublicFormInterface<Calendar> * @extends DocGeneratorContextWithPublicFormInterface<Calendar>
* @extends DocGeneratorContextWithAdminFormInterface<Calendar>
*/ */
interface CalendarContextInterface extends DocGeneratorContextWithPublicFormInterface, DocGeneratorContextWithAdminFormInterface interface CalendarContextInterface extends DocGeneratorContextWithPublicFormInterface, DocGeneratorContextWithAdminFormInterface
{ {
public function adminFormReverseTransform(array $data): array;
public function adminFormTransform(array $data): array;
public function buildAdminForm(FormBuilderInterface $builder): void;
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void;
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array;
public function getDescription(): string;
public function getEntityClass(): string;
public function getFormData(DocGeneratorTemplate $template, $entity): array;
public static function getKey(): string;
public function getName(): string;
public function hasAdminForm(): bool;
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool;
public function contextGenerationDataNormalize(DocGeneratorTemplate $template, $entity, array $data): array;
public function contextGenerationDataDenormalize(DocGeneratorTemplate $template, $entity, array $data): array;
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void;
} }

View File

@ -132,8 +132,8 @@ docgen:
Base context for calendar: 'Rendez-vous: contexte de base' Base context for calendar: 'Rendez-vous: contexte de base'
A base context for generating document on calendar: Contexte pour générer des documents à partir des rendez-vous A base context for generating document on calendar: Contexte pour générer des documents à partir des rendez-vous
Track changes on datetime and warn user if date time is updated after the doc generation: Suivre les changements sur le document et prévenir les utilisateurs que la date et l'heure ont été modifiée après la génération du document Track changes on datetime and warn user if date time is updated after the doc generation: Suivre les changements sur le document et prévenir les utilisateurs que la date et l'heure ont été modifiée après la génération du document
Ask main person: Demander de choisir une personne parmi les participants aux rendez-vous Ask main person: Demander de choisir un usager parmi les participants aux rendez-vous
Main person label: Label pour choisir la personne Main person label: Label pour choisir l'usager
Ask third party: Demander de choisir un tiers parmi les participants aux rendez-vous Ask third party: Demander de choisir un tiers parmi les participants aux rendez-vous
Third party label: Label pour choisir le tiers Third party label: Label pour choisir le tiers
Destinee: Destinataire Destinee: Destinataire

View File

@ -1,5 +1,5 @@
calendar: calendar:
At least {{ limit }} person is required.: Au moins {{ limit }} personne doit être associée à ce rendez-vous At least {{ limit }} person is required.: Au moins {{ limit }} usager doit être associée à ce rendez-vous
An end date is required: Indiquez une date et heure de fin An end date is required: Indiquez une date et heure de fin
A start date is required: Indiquez une date et heure de début A start date is required: Indiquez une date et heure de début
A location is required: Indiquez un lieu A location is required: Indiquez un lieu

View File

@ -13,6 +13,10 @@ namespace Chill\DocGeneratorBundle\Context;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
/**
* @template T of object
* @extends DocGeneratorContextInterface<T>
*/
interface DocGeneratorContextWithAdminFormInterface extends DocGeneratorContextInterface interface DocGeneratorContextWithAdminFormInterface extends DocGeneratorContextInterface
{ {
public function adminFormReverseTransform(array $data): array; public function adminFormReverseTransform(array $data): array;

View File

@ -15,7 +15,8 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
/** /**
* @template T * @template T of object
* @extends DocGeneratorContextInterface<T>
*/ */
interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContextInterface interface DocGeneratorContextWithPublicFormInterface extends DocGeneratorContextInterface
{ {

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\DocStoreBundle\Entity; namespace Chill\DocStoreBundle\Entity;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\MainBundle\Entity\HasScopesInterface; use Chill\MainBundle\Entity\HasScopesInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
@ -19,7 +20,7 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Entity * @ORM\Entity
* @ORM\Table("chill_doc.accompanyingcourse_document") * @ORM\Table("chill_doc.accompanyingcourse_document")
*/ */
class AccompanyingCourseDocument extends Document implements HasScopesInterface class AccompanyingCourseDocument extends Document implements HasScopesInterface, HasCentersInterface
{ {
/** /**
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class) * @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class)
@ -27,6 +28,11 @@ class AccompanyingCourseDocument extends Document implements HasScopesInterface
*/ */
private ?AccompanyingPeriod $course = null; private ?AccompanyingPeriod $course = null;
public function getCenters(): ?iterable
{
return $this->course->getCenters();
}
public function getCourse(): ?AccompanyingPeriod public function getCourse(): ?AccompanyingPeriod
{ {
return $this->course; return $this->course;

View File

@ -51,16 +51,9 @@
</div> </div>
<ul class="item-col record_actions flex-shrink-1"> <ul class="item-col record_actions flex-shrink-1">
{% if document.course is defined %} {% if document.course is defined %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
<li class="delete">
<a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
</li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
<li> <li>
<a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a> {{ chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) }}
</li> </li>
{% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %} {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_SEE_DETAILS', document) %}
<li> <li>
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }} {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document)) }}
@ -69,20 +62,17 @@
<a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a> <a href="{{ chill_path_add_return_path('accompanying_course_document_show', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li> </li>
{% endif %} {% endif %}
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
<li> <li>
{{ chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) }} <a href="{{ path('accompanying_course_document_edit', {'course': accompanyingCourse.id, 'id': document.id }) }}" class="btn btn-update"></a>
</li> </li>
{% else %} {% endif %}
{% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %} {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_DELETE', document) %}
<li class="delete"> <li class="delete">
<a href="{{ chill_return_path_or('chill_docstore_person_document_delete', {'person': person.id, 'id': document.id}) }}" class="btn btn-delete"></a> <a href="{{ chill_return_path_or('chill_docstore_accompanying_course_document_delete', {'course': accompanyingCourse.id, 'id': document.id}) }}" class="btn btn-delete"></a>
</li>
{% endif %}
{% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %}
<li>
<a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
</li> </li>
{% endif %} {% endif %}
{% else %}
{% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %} {% if is_granted('CHILL_PERSON_DOCUMENT_SEE_DETAILS', document) %}
<li> <li>
{{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }} {{ document.object|chill_document_button_group(document.title, is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document)) }}
@ -91,6 +81,16 @@
<a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a> <a href="{{ path('person_document_show', {'person': person.id, 'id': document.id}) }}" class="btn btn-show"></a>
</li> </li>
{% endif %} {% endif %}
{% if is_granted('CHILL_PERSON_DOCUMENT_UPDATE', document) %}
<li>
<a href="{{ path('person_document_edit', {'person': person.id, 'id': document.id}) }}" class="btn btn-update"></a>
</li>
{% endif %}
{% if is_granted('CHILL_PERSON_DOCUMENT_DELETE', document) %}
<li class="delete">
<a href="{{ chill_return_path_or('chill_docstore_person_document_delete', {'person': person.id, 'id': document.id}) }}" class="btn btn-delete"></a>
</li>
{% endif %}
{% endif %} {% endif %}
</ul> </ul>

View File

@ -78,12 +78,12 @@ class AccompanyingCourseDocumentVoter extends AbstractChillVoter implements Prov
return []; return [];
} }
protected function supports($attribute, $subject) protected function supports($attribute, $subject): bool
{ {
return $this->voterHelper->supports($attribute, $subject); return $this->voterHelper->supports($attribute, $subject);
} }
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{ {
if (!$token->getUser() instanceof User) { if (!$token->getUser() instanceof User) {
return false; return false;

View File

@ -113,8 +113,7 @@ class EventSearch extends AbstractSearch
] ]
); );
} }
// format is "json"
if ('json' === $format) {
$results = []; $results = [];
$search = $this->search($terms, $start, $limit, $options); $search = $this->search($terms, $start, $limit, $options);
@ -129,12 +128,9 @@ class EventSearch extends AbstractSearch
return [ return [
'results' => $results, 'results' => $results,
'pagination' => [
'more' => $paginator->hasNextPage(), 'more' => $paginator->hasNextPage(),
],
]; ];
} }
}
public function supports($domain, $format) public function supports($domain, $format)
{ {

View File

@ -34,7 +34,7 @@ Edit the participation: Modifier la participation
Participation Edit: Modifier une participation Participation Edit: Modifier une participation
Add a participation: Ajouter un participant Add a participation: Ajouter un participant
Participation creation: Ajouter une participation Participation creation: Ajouter une participation
Associated person: Personne associée Associated person: Usager associé
Associated event: Événement associé Associated event: Événement associé
Back to the event: Retour à l'événement Back to the event: Retour à l'événement
The participation was created: La participation a été créée The participation was created: La participation a été créée

View File

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

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

@ -46,6 +46,7 @@ use Chill\MainBundle\Doctrine\Type\NativeDateIntervalType;
use Chill\MainBundle\Doctrine\Type\PointType; use Chill\MainBundle\Doctrine\Type\PointType;
use Chill\MainBundle\Entity\Civility; use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Entity\Country; use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\GeographicalUnitLayer;
use Chill\MainBundle\Entity\Language; use Chill\MainBundle\Entity\Language;
use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\LocationType; 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

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

View File

@ -31,9 +31,6 @@ class NativeDateIntervalType extends DateIntervalType
{ {
public const FORMAT = '%rP%YY%MM%DDT%HH%IM%SS'; public const FORMAT = '%rP%YY%MM%DDT%HH%IM%SS';
/**
* @param DateInterval|null $value
*/
public function convertToDatabaseValue($value, AbstractPlatform $platform) public function convertToDatabaseValue($value, AbstractPlatform $platform)
{ {
if (null === $value) { if (null === $value) {

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Export; namespace Chill\MainBundle\Export;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
/** /**
@ -23,6 +24,7 @@ use Doctrine\ORM\QueryBuilder;
* aggregation, use `ListInterface`. * aggregation, use `ListInterface`.
* *
* @example Chill\PersonBundle\Export\CountPerson an example of implementation * @example Chill\PersonBundle\Export\CountPerson an example of implementation
* @template Q of QueryBuilder|NativeQuery
*/ */
interface ExportInterface extends ExportElementInterface interface ExportInterface extends ExportElementInterface
{ {
@ -84,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[] $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`) * @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); public function getLabels($key, array $values, $data);
@ -102,7 +104,7 @@ interface ExportInterface extends ExportElementInterface
/** /**
* Return the results of the query builder. * 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) * @param mixed[] $data the data from the export's fomr (added by self::buildForm)
* *
* @return mixed[] an array of results * @return mixed[] an array of results
@ -132,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 $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 * @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 = []); public function initiateQuery(array $requiredModifiers, array $acl, array $data = []);

View File

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

View File

@ -24,15 +24,7 @@ class ExportPickCenterDataMapper implements DataMapperInterface
{ {
protected RegroupmentRepository $regroupmentRepository; protected RegroupmentRepository $regroupmentRepository;
/** public function mapDataToForms($data, $forms): void
* @param array|Center[] $data
* @param $forms
*
* @throws Exception
*
* @return void
*/
public function mapDataToForms($data, $forms)
{ {
if (null === $data) { if (null === $data) {
return; return;
@ -44,7 +36,9 @@ class ExportPickCenterDataMapper implements DataMapperInterface
$pickedRegroupment = []; $pickedRegroupment = [];
foreach ($this->regroupmentRepository->findAll() as $regroupment) { 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)) { if (0 === count($notContained)) {
@ -56,13 +50,7 @@ class ExportPickCenterDataMapper implements DataMapperInterface
$form['centers']->setData($data); $form['centers']->setData($data);
} }
/** public function mapFormsToData($forms, &$data): void
* @param iterable $forms
* @param array $data
*
* @return void
*/
public function mapFormsToData($forms, &$data)
{ {
/** @var array<string, FormInterface> $forms */ /** @var array<string, FormInterface> $forms */
$forms = iterator_to_array($forms); $forms = iterator_to_array($forms);
@ -74,8 +62,8 @@ class ExportPickCenterDataMapper implements DataMapperInterface
} }
if (array_key_exists('regroupment', $forms)) { if (array_key_exists('regroupment', $forms)) {
foreach ($forms['regroupment']->getData() as $regroupment) {
/** @var Regroupment $regroupment */ /** @var Regroupment $regroupment */
foreach ($forms['regroupment']->getData() as $regroupment) {
foreach ($regroupment->getCenters() as $center) { foreach ($regroupment->getCenters() as $center) {
$centers[spl_object_hash($center)] = $center; $centers[spl_object_hash($center)] = $center;
} }

View File

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

View File

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

View File

@ -11,20 +11,58 @@ declare(strict_types=1);
namespace Chill\MainBundle\Repository; namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\GeographicalUnit; use Chill\MainBundle\Entity\GeographicalUnit;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; 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; private EntityRepository $repository;
public function __construct(EntityManagerInterface $em) public function __construct(EntityManagerInterface $em)
{ {
$this->repository = $em->getRepository($this->getClassName()); $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 public function find($id): ?GeographicalUnit

View File

@ -11,8 +11,23 @@ declare(strict_types=1);
namespace Chill\MainBundle\Repository; namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO;
use Doctrine\Persistence\ObjectRepository; use Doctrine\Persistence\ObjectRepository;
interface GeographicalUnitRepositoryInterface extends 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

@ -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') { if (body !== null && typeof body !== 'undefined') {
Object.assign(opts, {body: JSON.stringify(body)}) 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') { if (typeof options !== 'undefined') {
opts = Object.assign(opts, options); opts = Object.assign(opts, options);
} }
console.log('will fetch', url);
console.log('content for ' + url, opts);
return fetch(url, opts) return fetch(url, opts)
.then(response => { .then(response => {
if (response.ok) { 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,6 +18,7 @@ function loadDynamicPicker(element) {
isMultiple = parseInt(el.dataset.multiple) === 1, isMultiple = parseInt(el.dataset.multiple) === 1,
uniqId = el.dataset.uniqid, uniqId = el.dataset.uniqid,
input = element.querySelector('[data-input-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 ? picked = isMultiple ?
JSON.parse(input.value) : ( JSON.parse(input.value) : (
(input.value === '[]' || input.value === '') ? (input.value === '[]' || input.value === '') ?
@ -54,16 +55,12 @@ function loadDynamicPicker(element) {
}, },
computed: { computed: {
notPickedSuggested() { notPickedSuggested() {
if (this.multiple) {
const pickedIds = new Set(); const pickedIds = new Set();
for (const p of this.picked) { for (const p of this.picked) {
pickedIds.add(`${p.type}${p.id}`); pickedIds.add(`${p.type}${p.id}`);
} }
return this.suggested.filter(e => !pickedIds.has(`${e.type}${e.id}`)) return this.suggested.filter(e => !pickedIds.has(`${e.type}${e.id}`))
} }
return this.suggested.filter(e => e.type !== this.picked.type && e.id !== e.picked.id);
}
}, },
methods: { methods: {
addNewEntity({entity}) { addNewEntity({entity}) {
@ -90,7 +87,11 @@ function loadDynamicPicker(element) {
this.suggested.push(entity); this.suggested.push(entity);
} }
this.picked = this.picked.filter(e => !(e.type === entity.type && e.id === entity.id)); this.picked = this.picked.filter(e => !(e.type === entity.type && e.id === entity.id));
if (this.multiple) {
input.value = JSON.stringify(this.picked); input.value = JSON.stringify(this.picked);
} else {
input.value = "";
}
}, },
} }
}) })

View File

@ -70,6 +70,8 @@ export interface Country {
code: string; code: string;
} }
export type AddressRefStatus = 'match'|'to_review'|'reviewed';
export interface Address { export interface Address {
type: "address"; type: "address";
address_id: number; address_id: number;
@ -90,6 +92,13 @@ export interface Address {
addressReference: AddressReference | null; addressReference: AddressReference | null;
validFrom: DateTime; validFrom: DateTime;
validTo: DateTime | null; validTo: DateTime | null;
point: Point | null;
refStatus: AddressRefStatus;
isNoAddress: boolean;
}
export interface AddressWithPoint extends Address {
point: Point
} }
export interface AddressReference { export interface AddressReference {
@ -106,6 +115,19 @@ export interface AddressReference {
updatedAt: DateTime | null; 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 { export interface Location {
type: "location"; type: "location";
id: number; id: number;

View File

@ -1,3 +1,5 @@
import {getAddressById} from 'ChillMainAssets/lib/api/address';
/** /**
* Endpoint chill_api_single_country__index * Endpoint chill_api_single_country__index
* method GET, get Country Object * method GET, get Country Object
@ -188,13 +190,7 @@ const postPostalCode = (postalCode) => { //<--
* @returns {Promise} a promise containing a Address object * @returns {Promise} a promise containing a Address object
*/ */
const getAddress = (id) => { const getAddress = (id) => {
//console.log('<< get address'); return getAddressById(id);
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');
});
}; };
export { export {

View File

@ -580,15 +580,15 @@ export default {
this.entity.selected.city = this.context.edit ? this.entity.address.postcode : {}; this.entity.selected.city = this.context.edit ? this.entity.address.postcode : {};
this.entity.selected.address = {}; this.entity.selected.address = {};
this.entity.selected.address.street = this.context.edit ? this.entity.address.street: 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: null; this.entity.selected.address.streetNumber = this.context.edit ? this.entity.address.streetNumber: '';
this.entity.selected.address.floor = this.context.edit ? this.entity.address.floor: null; this.entity.selected.address.floor = this.context.edit ? this.entity.address.floor: '';
this.entity.selected.address.corridor = this.context.edit ? this.entity.address.corridor: null; this.entity.selected.address.corridor = this.context.edit ? this.entity.address.corridor: '';
this.entity.selected.address.steps = this.context.edit ? this.entity.address.steps: null; this.entity.selected.address.steps = this.context.edit ? this.entity.address.steps: '';
this.entity.selected.address.flat = this.context.edit ? this.entity.address.flat: null; this.entity.selected.address.flat = this.context.edit ? this.entity.address.flat: '';
this.entity.selected.address.buildingName = this.context.edit ? this.entity.address.buildingName: null; this.entity.selected.address.buildingName = this.context.edit ? this.entity.address.buildingName: '';
this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: null; this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: '';
this.entity.selected.address.extra = this.context.edit ? this.entity.address.extra: null; 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.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; this.entity.selected.writeNew.postcode = false // NB: this used to be this.context.edit, but think it was erroneous;

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

View File

@ -69,6 +69,7 @@
<i class="fa fa-li fa-map-marker"></i> <i class="fa fa-li fa-map-marker"></i>
{% endif %} {% endif %}
{{ _self.inline(address, options, streetLine, lines) }} {{ _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> </li>
{%- endif -%} {%- endif -%}
@ -78,6 +79,7 @@
<i class="fa fa-fw fa-map-marker"></i> <i class="fa fa-fw fa-map-marker"></i>
{% endif %} {% endif %}
{{ _self.inline(address, options, streetLine, lines) }} {{ _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> </span>
{%- endif -%} {%- endif -%}
@ -102,6 +104,7 @@
<div class="noaddress"> <div class="noaddress">
{{ 'address.consider homeless'|trans }} {{ 'address.consider homeless'|trans }}
</div> </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 %} {% else %}
<div class="address{% if options['multiline'] %} multiline{% endif %}{% if options['with_delimiter'] %} delimiter{% endif %}"> <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> <i class="fa fa-fw fa-map-marker"></i>
{% endif %} {% endif %}
{{ _self.raw(lines) }} {{ _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> </div>
{% endif %} {% endif %}
{{ _self.validity(address, options) }} {{ _self.validity(address, options) }}

View File

@ -68,6 +68,7 @@
{{ form_row(transition_form.future_cc_users) }} {{ form_row(transition_form.future_cc_users) }}
{{ form_row(transition_form.future_dest_emails) }} {{ form_row(transition_form.future_dest_emails) }}
{{ form_errors(transition_form.future_dest_users) }}
</div> </div>
<p>{{ form_label(transition_form.comment) }}</p> <p>{{ form_label(transition_form.comment) }}</p>

View File

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

View File

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

View File

@ -16,6 +16,18 @@ use function count;
use function implode; use function implode;
use function strtr; 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 class SearchApiQuery
{ {
private ?string $fromClause = null; private ?string $fromClause = null;

View File

@ -71,9 +71,9 @@ interface SearchInterface
* @param array $terms the string to search * @param array $terms the string to search
* @param int $start the first result (for pagination) * @param int $start the first result (for pagination)
* @param int $limit the number of 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'); public function renderResult(array $terms, $start = 0, $limit = 50, array $options = [], $format = 'html');

View File

@ -16,7 +16,7 @@ use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface; use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface;
use Chill\MainBundle\Security\ParentRoleHelper; use Chill\MainBundle\Security\ParentRoleHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher; use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\Role;
@ -34,7 +34,7 @@ use function get_class;
*/ */
class AuthorizationHelper implements AuthorizationHelperInterface class AuthorizationHelper implements AuthorizationHelperInterface
{ {
private CenterResolverDispatcherInterface $centerResolverDispatcher; private CenterResolverManagerInterface $centerResolverManager;
private LoggerInterface $logger; private LoggerInterface $logger;
@ -45,13 +45,13 @@ class AuthorizationHelper implements AuthorizationHelperInterface
private UserACLAwareRepositoryInterface $userACLAwareRepository; private UserACLAwareRepositoryInterface $userACLAwareRepository;
public function __construct( public function __construct(
CenterResolverDispatcherInterface $centerResolverDispatcher, CenterResolverManagerInterface $centerResolverManager,
LoggerInterface $logger, LoggerInterface $logger,
ScopeResolverDispatcher $scopeResolverDispatcher, ScopeResolverDispatcher $scopeResolverDispatcher,
UserACLAwareRepositoryInterface $userACLAwareRepository, UserACLAwareRepositoryInterface $userACLAwareRepository,
ParentRoleHelper $parentRoleHelper ParentRoleHelper $parentRoleHelper
) { ) {
$this->centerResolverDispatcher = $centerResolverDispatcher; $this->centerResolverManager = $centerResolverManager;
$this->logger = $logger; $this->logger = $logger;
$this->scopeResolverDispatcher = $scopeResolverDispatcher; $this->scopeResolverDispatcher = $scopeResolverDispatcher;
$this->userACLAwareRepository = $userACLAwareRepository; $this->userACLAwareRepository = $userACLAwareRepository;
@ -63,7 +63,7 @@ class AuthorizationHelper implements AuthorizationHelperInterface
* *
* @param User $user The user * @param User $user The user
* @param array $centers a list of centers which are going to be filtered * @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 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, * Get reachable Centers for the given user, role,
* and optionally Scope. * and optionally Scope.
* *
* @return array|Center[] * @return list<Center>
*/ */
public function getReachableCenters(UserInterface $user, string $role, ?Scope $scope = null): array public function getReachableCenters(UserInterface $user, string $role, ?Scope $scope = null): array
{ {
if ($role instanceof Role) { if ($role instanceof Role) {
$role = $role->getRole(); $role = $role->getRole();
} }
/** @var array<string, Center> $centers */
$centers = []; $centers = [];
foreach ($user->getGroupCenters() as $groupCenter) { foreach ($user->getGroupCenters() as $groupCenter) {
@ -129,13 +130,13 @@ class AuthorizationHelper implements AuthorizationHelperInterface
//check that the role is in the reachable roles //check that the role is in the reachable roles
if ($this->isRoleReached($role, $roleScope->getRole())) { if ($this->isRoleReached($role, $roleScope->getRole())) {
if (null === $scope) { if (null === $scope) {
$centers[] = $groupCenter->getCenter(); $centers[spl_object_hash($groupCenter->getCenter())] = $groupCenter->getCenter();
break; break;
} }
if ($scope->getId() === $roleScope->getScope()->getId()) { if ($scope->getId() === $roleScope->getScope()->getId()) {
$centers[] = $groupCenter->getCenter(); $centers[spl_object_hash($groupCenter->getCenter())] = $groupCenter->getCenter();
break; 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[] * @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); return $this->getReachableCircles($user, $role, $center);
} }
@ -252,10 +249,9 @@ class AuthorizationHelper implements AuthorizationHelperInterface
*/ */
public function userHasAccess(User $user, $entity, $attribute) public function userHasAccess(User $user, $entity, $attribute)
{ {
$center = $this->centerResolverDispatcher->resolveCenter($entity); $centers = $this->centerResolverManager->resolveCenters($entity);
if (is_iterable($center)) { foreach ($centers as $c) {
foreach ($center as $c) {
if ($this->userHasAccessForCenter($user, $c, $entity, $attribute)) { if ($this->userHasAccessForCenter($user, $c, $entity, $attribute)) {
return true; return true;
} }
@ -264,17 +260,6 @@ class AuthorizationHelper implements AuthorizationHelperInterface
return false; 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');
}
/** /**
* Test if a parent role may give access to a given child role. * Test if a parent role may give access to a given child role.
* *

View File

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

@ -97,6 +97,13 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
); );
$data['validFrom'] = $address->getValidFrom(); $data['validFrom'] = $address->getValidFrom();
$data['validTo'] = $address->getValidTo(); $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) { } elseif ('docgen' === $format) {
$dateContext = array_merge($context, ['docgen:expects' => DateTimeInterface::class]); $dateContext = array_merge($context, ['docgen:expects' => DateTimeInterface::class]);
$data['validFrom'] = $this->normalizer->normalize($address->getValidFrom(), $format, $dateContext); $data['validFrom'] = $this->normalizer->normalize($address->getValidFrom(), $format, $dateContext);

View File

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

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