mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-06 21:39:42 +00:00
Compare commits
10 Commits
#361-impro
...
405-aside-
Author | SHA1 | Date | |
---|---|---|---|
ac6336d197 | |||
a46b301e44 | |||
05f0443011 | |||
7f8d8f891e | |||
ddb932a4fa | |||
3a02f15bcd | |||
bc2fbee5c6 | |||
ebd10ca522 | |||
d3a31be412
|
|||
d159a82f88
|
6
.changes/unreleased/Fixed-20251003-224044.yaml
Normal file
6
.changes/unreleased/Fixed-20251003-224044.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Fixed
|
||||||
|
body: Fix the rendering of list of StoredObjectVersions, where there are kept version (before converting to pdf) and intermediate versions deleted
|
||||||
|
time: 2025-10-03T22:40:44.685474863+02:00
|
||||||
|
custom:
|
||||||
|
Issue: ""
|
||||||
|
SchemaChange: No schema change
|
6
.changes/unreleased/Fixed-20251006-121315.yaml
Normal file
6
.changes/unreleased/Fixed-20251006-121315.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
kind: Fixed
|
||||||
|
body: 'Notification: fix editing of sent notification by removing form.addressesEmails, a field that no longer exists'
|
||||||
|
time: 2025-10-06T12:13:15.45905994+02:00
|
||||||
|
custom:
|
||||||
|
Issue: "434"
|
||||||
|
SchemaChange: No schema change
|
@@ -120,3 +120,6 @@ chill_activity:
|
|||||||
-
|
-
|
||||||
label: '5 hours'
|
label: '5 hours'
|
||||||
seconds: 18000
|
seconds: 18000
|
||||||
|
|
||||||
|
chill_aside_activity:
|
||||||
|
show_concerned_persons_count: true
|
||||||
|
@@ -17,9 +17,3 @@ when@dev:
|
|||||||
defaults:
|
defaults:
|
||||||
template: '@ChillMain/Dev/dev.assets.test2.html.twig'
|
template: '@ChillMain/Dev/dev.assets.test2.html.twig'
|
||||||
|
|
||||||
|
|
||||||
sass_address_picker:
|
|
||||||
path: /_dev/address-picker
|
|
||||||
controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
|
|
||||||
defaults:
|
|
||||||
template: '@ChillMain/Dev/dev.address-picker.html.twig'
|
|
||||||
|
@@ -45,7 +45,6 @@
|
|||||||
"webpack-cli": "^5.0.1"
|
"webpack-cli": "^5.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fragaria/address-formatter": "^6.6.1",
|
|
||||||
"@fullcalendar/core": "^6.1.4",
|
"@fullcalendar/core": "^6.1.4",
|
||||||
"@fullcalendar/daygrid": "^6.1.4",
|
"@fullcalendar/daygrid": "^6.1.4",
|
||||||
"@fullcalendar/interaction": "^6.1.4",
|
"@fullcalendar/interaction": "^6.1.4",
|
||||||
@@ -67,12 +66,10 @@
|
|||||||
"mime": "^4.0.0",
|
"mime": "^4.0.0",
|
||||||
"pdfjs-dist": "^4.3.136",
|
"pdfjs-dist": "^4.3.136",
|
||||||
"vis-network": "^9.1.0",
|
"vis-network": "^9.1.0",
|
||||||
"vue": "^3.5.x",
|
"vue": "^3.5.6",
|
||||||
"vue-i18n": "^9.1.6",
|
"vue-i18n": "^9.1.6",
|
||||||
"vue-multiselect": "3.0.0-alpha.2",
|
"vue-multiselect": "3.0.0-alpha.2",
|
||||||
"vue-toast-notification": "^3.1.2",
|
"vue-toast-notification": "^3.1.2",
|
||||||
"vue-tsc": "^3.1.0",
|
|
||||||
"vue-use-leaflet": "^0.1.7",
|
|
||||||
"vuex": "^4.0.0"
|
"vuex": "^4.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
|
@@ -25,6 +25,7 @@ final class ChillAsideActivityExtension extends Extension implements PrependExte
|
|||||||
$config = $this->processConfiguration($configuration, $configs);
|
$config = $this->processConfiguration($configuration, $configs);
|
||||||
|
|
||||||
$container->setParameter('chill_aside_activity.form.time_duration', $config['form']['time_duration']);
|
$container->setParameter('chill_aside_activity.form.time_duration', $config['form']['time_duration']);
|
||||||
|
$container->setParameter('chill_aside_activity.show_concerned_persons_count', $config['show_concerned_persons_count']);
|
||||||
|
|
||||||
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
|
||||||
$loader->load('services.yaml');
|
$loader->load('services.yaml');
|
||||||
|
@@ -141,6 +141,11 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
->end()
|
||||||
|
->booleanNode('show_concerned_persons_count')
|
||||||
|
->defaultTrue()
|
||||||
|
->info('Show the concerned persons count field in aside activity forms and views')
|
||||||
|
->end()
|
||||||
->end();
|
->end();
|
||||||
|
|
||||||
return $treeBuilder;
|
return $treeBuilder;
|
||||||
|
@@ -62,6 +62,10 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
private User $updatedBy;
|
private User $updatedBy;
|
||||||
|
|
||||||
|
#[Assert\GreaterThanOrEqual(0)]
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true)]
|
||||||
|
private ?int $concernedPersonsCount = null;
|
||||||
|
|
||||||
public function getAgent(): ?User
|
public function getAgent(): ?User
|
||||||
{
|
{
|
||||||
return $this->agent;
|
return $this->agent;
|
||||||
@@ -186,4 +190,16 @@ class AsideActivity implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getConcernedPersonsCount(): ?int
|
||||||
|
{
|
||||||
|
return $this->concernedPersonsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setConcernedPersonsCount(?int $concernedPersonsCount): self
|
||||||
|
{
|
||||||
|
$this->concernedPersonsCount = $concernedPersonsCount;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,86 @@
|
|||||||
|
<?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\AsideActivityBundle\Export\Aggregator;
|
||||||
|
|
||||||
|
use Chill\AsideActivityBundle\Export\Declarations;
|
||||||
|
use Chill\MainBundle\Export\AggregatorInterface;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
class ByConcernedPersonsCountAggregator implements AggregatorInterface
|
||||||
|
{
|
||||||
|
public function addRole(): ?string
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
||||||
|
{
|
||||||
|
$qb->addSelect('aside.concernedPersonsCount AS by_concerned_persons_count_aggregator')
|
||||||
|
->addGroupBy('by_concerned_persons_count_aggregator');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyOn(): string
|
||||||
|
{
|
||||||
|
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder): void
|
||||||
|
{
|
||||||
|
// No form needed
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNormalizationVersion(): int
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeFormData(array $formData): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormDefaultData(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels($key, array $values, $data): callable
|
||||||
|
{
|
||||||
|
return function ($value): string {
|
||||||
|
if ('_header' === $value) {
|
||||||
|
return 'export.aggregator.Concerned persons count';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $value) {
|
||||||
|
return 'export.aggregator.No concerned persons count specified';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryKeys($data): array
|
||||||
|
{
|
||||||
|
return ['by_concerned_persons_count_aggregator'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'export.aggregator.Group by concerned persons count';
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,116 @@
|
|||||||
|
<?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\AsideActivityBundle\Export\Export;
|
||||||
|
|
||||||
|
use Chill\AsideActivityBundle\Export\Declarations;
|
||||||
|
use Chill\AsideActivityBundle\Repository\AsideActivityRepository;
|
||||||
|
use Chill\AsideActivityBundle\Security\AsideActivityVoter;
|
||||||
|
use Chill\MainBundle\Export\ExportInterface;
|
||||||
|
use Chill\MainBundle\Export\FormatterInterface;
|
||||||
|
use Chill\MainBundle\Export\GroupedExportInterface;
|
||||||
|
use Doctrine\ORM\Query;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
|
class SumConcernedPersonsCountAsideActivity implements ExportInterface, GroupedExportInterface
|
||||||
|
{
|
||||||
|
public function __construct(private readonly AsideActivityRepository $repository) {}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder) {}
|
||||||
|
|
||||||
|
public function getNormalizationVersion(): int
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function normalizeFormData(array $formData): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function denormalizeFormData(array $formData, int $fromVersion): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormDefaultData(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAllowedFormattersTypes(): array
|
||||||
|
{
|
||||||
|
return [FormatterInterface::TYPE_TABULAR];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'export.Sum concerned persons count for aside activities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getGroup(): string
|
||||||
|
{
|
||||||
|
return 'export.Exports of aside activities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabels($key, array $values, $data)
|
||||||
|
{
|
||||||
|
if ('export_sum_concerned_persons_count' !== $key) {
|
||||||
|
throw new \LogicException("the key {$key} is not used by this export");
|
||||||
|
}
|
||||||
|
|
||||||
|
$labels = array_combine($values, $values);
|
||||||
|
$labels['_header'] = $this->getTitle();
|
||||||
|
|
||||||
|
return static fn ($value) => $labels[$value];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryKeys($data): array
|
||||||
|
{
|
||||||
|
return ['export_sum_concerned_persons_count'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResult($query, $data, \Chill\MainBundle\Export\ExportGenerationContext $context): array
|
||||||
|
{
|
||||||
|
return $query->getQuery()->getResult(Query::HYDRATE_SCALAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'export.Sum concerned persons count for aside activities';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): string
|
||||||
|
{
|
||||||
|
return Declarations::ASIDE_ACTIVITY_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function initiateQuery(array $requiredModifiers, array $acl, array $data, \Chill\MainBundle\Export\ExportGenerationContext $context): \Doctrine\ORM\QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->repository->createQueryBuilder('aside');
|
||||||
|
|
||||||
|
$qb->select('SUM(COALESCE(aside.concernedPersonsCount, 0)) as export_sum_concerned_persons_count');
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requiredRole(): string
|
||||||
|
{
|
||||||
|
return AsideActivityVoter::STATS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsModifiers(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Declarations::ASIDE_ACTIVITY_TYPE,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -21,6 +21,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
|||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
|
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
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;
|
||||||
@@ -29,11 +30,13 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
final class AsideActivityFormType extends AbstractType
|
final class AsideActivityFormType extends AbstractType
|
||||||
{
|
{
|
||||||
private readonly array $timeChoices;
|
private readonly array $timeChoices;
|
||||||
|
private readonly bool $showConcernedPersonsCount;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ParameterBagInterface $parameterBag,
|
ParameterBagInterface $parameterBag,
|
||||||
) {
|
) {
|
||||||
$this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration');
|
$this->timeChoices = $parameterBag->get('chill_aside_activity.form.time_duration');
|
||||||
|
$this->showConcernedPersonsCount = $parameterBag->get('chill_aside_activity.show_concerned_persons_count');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
@@ -76,6 +79,16 @@ final class AsideActivityFormType extends AbstractType
|
|||||||
->add('location', PickUserLocationType::class)
|
->add('location', PickUserLocationType::class)
|
||||||
;
|
;
|
||||||
|
|
||||||
|
if ($this->showConcernedPersonsCount) {
|
||||||
|
$builder->add('concernedPersonsCount', IntegerType::class, [
|
||||||
|
'label' => 'Concerned persons count',
|
||||||
|
'required' => false,
|
||||||
|
'attr' => [
|
||||||
|
'min' => 0,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (['duration'] as $fieldName) {
|
foreach (['duration'] as $fieldName) {
|
||||||
$builder->get($fieldName)
|
$builder->get($fieldName)
|
||||||
->addModelTransformer($durationTimeTransformer);
|
->addModelTransformer($durationTimeTransformer);
|
||||||
|
@@ -42,6 +42,11 @@
|
|||||||
{%- if entity.location.name is defined -%}
|
{%- if entity.location.name is defined -%}
|
||||||
<div><i class="fa fa-fw fa-map-marker"></i>{{ entity.location.name }}</div>
|
<div><i class="fa fa-fw fa-map-marker"></i>{{ entity.location.name }}</div>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
|
|
||||||
|
{%- if entity.concernedPersonsCount > 0 -%}
|
||||||
|
<div><i class="fa fa-fw fa-user"></i>{{ entity.concernedPersonsCount }}</div>
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="item-col" style="justify-content: flex-end;">
|
<div class="item-col" style="justify-content: flex-end;">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
@@ -38,6 +38,9 @@
|
|||||||
<dt class="inline">{{ 'Duration'|trans }}</dt>
|
<dt class="inline">{{ 'Duration'|trans }}</dt>
|
||||||
<dd>{{ entity.duration|date('H:i') }}</dd>
|
<dd>{{ entity.duration|date('H:i') }}</dd>
|
||||||
|
|
||||||
|
<dt class="inline">{{ 'Concerned persons count'|trans }}</dt>
|
||||||
|
<dd>{{ entity.concernedPersonsCount }}</dd>
|
||||||
|
|
||||||
<dt class="inline">{{ 'Remark'|trans }}</dt>
|
<dt class="inline">{{ 'Remark'|trans }}</dt>
|
||||||
{%- if entity.note is empty -%}
|
{%- if entity.note is empty -%}
|
||||||
<dd>
|
<dd>
|
||||||
|
@@ -20,6 +20,10 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: chill.export, alias: 'avg_aside_activity_duration' }
|
- { name: chill.export, alias: 'avg_aside_activity_duration' }
|
||||||
|
|
||||||
|
Chill\AsideActivityBundle\Export\Export\SumConcernedPersonsCountAsideActivity:
|
||||||
|
tags:
|
||||||
|
- { name: chill.export, alias: 'sum_aside_activity_concerned_persons_count' }
|
||||||
|
|
||||||
## Filters
|
## Filters
|
||||||
chill.aside_activity.export.date_filter:
|
chill.aside_activity.export.date_filter:
|
||||||
class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
|
class: Chill\AsideActivityBundle\Export\Filter\ByDateFilter
|
||||||
@@ -70,3 +74,7 @@ services:
|
|||||||
Chill\AsideActivityBundle\Export\Aggregator\ByLocationAggregator:
|
Chill\AsideActivityBundle\Export\Aggregator\ByLocationAggregator:
|
||||||
tags:
|
tags:
|
||||||
- { name: chill.export_aggregator, alias: 'aside_activity_location_aggregator' }
|
- { name: chill.export_aggregator, alias: 'aside_activity_location_aggregator' }
|
||||||
|
|
||||||
|
Chill\AsideActivityBundle\Export\Aggregator\ByConcernedPersonsCountAggregator:
|
||||||
|
tags:
|
||||||
|
- { name: chill.export_aggregator, alias: 'aside_activity_concerned_persons_count_aggregator' }
|
||||||
|
@@ -0,0 +1,33 @@
|
|||||||
|
<?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\Migrations\AsideActivity;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20251006113048 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add concernedPersonsCount property to AsideActivity entity';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_asideactivity.asideactivity ADD concernedPersonsCount INT DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE chill_asideactivity.AsideActivity DROP concernedPersonsCount');
|
||||||
|
}
|
||||||
|
}
|
@@ -27,6 +27,7 @@ Emergency: Urgent
|
|||||||
by: "Par "
|
by: "Par "
|
||||||
location: Lieu
|
location: Lieu
|
||||||
Asideactivity location: Localisation de l'activité
|
Asideactivity location: Localisation de l'activité
|
||||||
|
Concerned persons count: Nombre d'usager concernés
|
||||||
|
|
||||||
# Crud
|
# Crud
|
||||||
crud:
|
crud:
|
||||||
@@ -190,6 +191,7 @@ export:
|
|||||||
Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
|
Count aside activities by various parameters.: Compte le nombre d'activités annexes selon divers critères
|
||||||
Average aside activities duration: Durée moyenne des activités annexes
|
Average aside activities duration: Durée moyenne des activités annexes
|
||||||
Sum aside activities duration: Durée des activités annexes
|
Sum aside activities duration: Durée des activités annexes
|
||||||
|
Sum concerned persons count for aside activities: Nombre d'usager concernés par les activités annexes
|
||||||
filter:
|
filter:
|
||||||
Filter by aside activity date: Filtrer les activités annexes par date
|
Filter by aside activity date: Filtrer les activités annexes par date
|
||||||
Filter by aside activity type: Filtrer les activités annexes par type d'activité
|
Filter by aside activity type: Filtrer les activités annexes par type d'activité
|
||||||
@@ -210,6 +212,8 @@ export:
|
|||||||
'Filtered by aside activity location: only %location%': "Filtré par localisation: uniquement %location%"
|
'Filtered by aside activity location: only %location%': "Filtré par localisation: uniquement %location%"
|
||||||
aggregator:
|
aggregator:
|
||||||
Group by aside activity type: Grouper les activités annexes par type d'activité
|
Group by aside activity type: Grouper les activités annexes par type d'activité
|
||||||
|
Group by concerned persons count: Grouper les activités annexes par nombre d'usagers conernés
|
||||||
|
Concerned persons count: Nombre d'usagers concernés
|
||||||
Aside activity type: Type d'activité annexe
|
Aside activity type: Type d'activité annexe
|
||||||
by_user_job:
|
by_user_job:
|
||||||
Aggregate by user job: Grouper les activités annexes par métier des utilisateurs
|
Aggregate by user job: Grouper les activités annexes par métier des utilisateurs
|
||||||
|
@@ -59,7 +59,7 @@ final readonly class StoredObjectVersionApiController
|
|||||||
|
|
||||||
return new JsonResponse(
|
return new JsonResponse(
|
||||||
$this->serializer->serialize(
|
$this->serializer->serialize(
|
||||||
new Collection($items, $paginator),
|
new Collection(array_values($items->toArray()), $paginator),
|
||||||
'json',
|
'json',
|
||||||
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
||||||
),
|
),
|
||||||
|
@@ -3,9 +3,9 @@ import {
|
|||||||
StoredObject,
|
StoredObject,
|
||||||
StoredObjectPointInTime,
|
StoredObjectPointInTime,
|
||||||
StoredObjectVersionWithPointInTime,
|
StoredObjectVersionWithPointInTime,
|
||||||
} from "./../../../types";
|
} from "ChillDocStoreAssets/types";
|
||||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||||
import { ISOToDatetime } from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
|
||||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
||||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
||||||
|
@@ -40,6 +40,10 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
|||||||
$storedObject->registerVersion();
|
$storedObject->registerVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove one version in the history
|
||||||
|
$v5 = $storedObject->getVersions()->get(5);
|
||||||
|
$storedObject->removeVersion($v5);
|
||||||
|
|
||||||
$security = $this->prophesize(Security::class);
|
$security = $this->prophesize(Security::class);
|
||||||
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
||||||
->willReturn(true)
|
->willReturn(true)
|
||||||
@@ -53,6 +57,7 @@ class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
|||||||
self::assertEquals($response->getStatusCode(), 200);
|
self::assertEquals($response->getStatusCode(), 200);
|
||||||
self::assertIsArray($body);
|
self::assertIsArray($body);
|
||||||
self::assertArrayHasKey('results', $body);
|
self::assertArrayHasKey('results', $body);
|
||||||
|
self::assertIsList($body['results']);
|
||||||
self::assertCount(10, $body['results']);
|
self::assertCount(10, $body['results']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,50 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Repository\AddressReferenceRepositoryInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
final readonly class AddressReferenceAggregatedApiController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private Security $security,
|
|
||||||
private AddressReferenceRepositoryInterface $addressReferenceRepository,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route(path: '/api/1.0/main/address-reference/aggregated/search')]
|
|
||||||
public function search(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
if (!$this->security->isGranted('IS_AUTHENTICATED')) {
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$request->query->has('q')) {
|
|
||||||
throw new BadRequestHttpException('Parameter "q" is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$q = trim($request->query->get('q'));
|
|
||||||
|
|
||||||
if ('' === $q) {
|
|
||||||
throw new BadRequestHttpException('Parameter "q" is required and cannot be empty.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->addressReferenceRepository->findAggregatedBySearchString($q);
|
|
||||||
|
|
||||||
return new JsonResponse(iterator_to_array($result));
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Controller;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Repository\PostalCodeForAddressReferenceRepositoryInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
final readonly class PostalCodeForAddressReferenceApiController
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private PostalCodeForAddressReferenceRepositoryInterface $postalCodeForAddressReferenceRepository,
|
|
||||||
private Security $security,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
#[Route('/api/1.0/main/address-reference/postal-code/search')]
|
|
||||||
public function findPostalCodeBySearch(Request $request): JsonResponse
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!$this->security->isGranted('IS_AUTHENTICATED')) {
|
|
||||||
throw new AccessDeniedHttpException();
|
|
||||||
}
|
|
||||||
|
|
||||||
$search = $request->query->get('q');
|
|
||||||
|
|
||||||
if (null === $search || '' === trim($search)) {
|
|
||||||
throw new BadRequestHttpException('No search query provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
$postalCodes = iterator_to_array($this->postalCodeForAddressReferenceRepository->findPostalCode($search));
|
|
||||||
|
|
||||||
return new JsonResponse($postalCodes, json: false);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -14,14 +14,13 @@ namespace Chill\MainBundle\Repository;
|
|||||||
use Chill\MainBundle\Entity\AddressReference;
|
use Chill\MainBundle\Entity\AddressReference;
|
||||||
use Chill\MainBundle\Entity\PostalCode;
|
use Chill\MainBundle\Entity\PostalCode;
|
||||||
use Chill\MainBundle\Search\SearchApiQuery;
|
use Chill\MainBundle\Search\SearchApiQuery;
|
||||||
use Doctrine\DBAL\Types\Types;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
use Doctrine\ORM\NativeQuery;
|
|
||||||
use Doctrine\ORM\Query\ResultSetMapping;
|
use Doctrine\ORM\Query\ResultSetMapping;
|
||||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
final readonly class AddressReferenceRepository implements AddressReferenceRepositoryInterface
|
final readonly class AddressReferenceRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private EntityManagerInterface $entityManager;
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
@@ -66,121 +65,6 @@ final readonly class AddressReferenceRepository implements AddressReferenceRepos
|
|||||||
return $this->repository->findAll();
|
return $this->repository->findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
|
|
||||||
{
|
|
||||||
$terms = $this->buildTermsFromSearchString($search);
|
|
||||||
if ([] === $terms) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$connection = $this->entityManager->getConnection();
|
|
||||||
$qb = $connection->createQueryBuilder();
|
|
||||||
|
|
||||||
$qb->select('row_number() OVER () AS row_number', 'var.street AS street', 'cmpc.id AS postcode_id', 'cmpc.code AS code', 'cmpc.label AS label', 'jsonb_object_agg(var.address_id, var.streetnumber ORDER BY var.row_number) AS positions')
|
|
||||||
->from('view_chill_main_address_reference', 'var')
|
|
||||||
->innerJoin('var', 'chill_main_postal_code', 'cmpc', 'cmpc.id = var.postcode_id')
|
|
||||||
->groupBy('cmpc.id', 'var.street')
|
|
||||||
->setFirstResult($firstResult)
|
|
||||||
->setMaxResults($maxResults);
|
|
||||||
|
|
||||||
$paramId = 0;
|
|
||||||
|
|
||||||
foreach ($terms as $term) {
|
|
||||||
$qb->andWhere('var.address like UNACCENT(LOWER(?))');
|
|
||||||
$qb->setParameter(++$paramId, "%{$term}%");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $postalCode) {
|
|
||||||
$qb->andWhere('var.postcode_id = ?');
|
|
||||||
$qb->setParameter(++$paramId, $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $qb->executeQuery();
|
|
||||||
|
|
||||||
foreach ($result->iterateAssociative() as $row) {
|
|
||||||
yield [...$row, 'positions' => json_decode($row['positions'], true, 512, JSON_THROW_ON_ERROR)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return iterable<AddressReference>
|
|
||||||
*/
|
|
||||||
public function findBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
|
|
||||||
{
|
|
||||||
$terms = $this->buildTermsFromSearchString($search);
|
|
||||||
if ([] === $terms) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
|
||||||
$rsm->addRootEntityFromClassMetadata(AddressReference::class, 'ar');
|
|
||||||
$baseSql = 'SELECT '.$rsm->generateSelectClause(['ar' => 'ar']).' FROM chill_main_address_reference ar JOIN
|
|
||||||
view_chill_main_address_reference var ON var.address_id = ar.id';
|
|
||||||
$nql = $this->buildQueryBySearchString($rsm, $baseSql, $terms, $postalCode);
|
|
||||||
|
|
||||||
$orderBy = [];
|
|
||||||
$pertinence = [];
|
|
||||||
foreach ($terms as $k => $term) {
|
|
||||||
$pertinence[] =
|
|
||||||
"(EXISTS (SELECT 1 FROM unnest(string_to_array(address, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(:order{$k})))))::int";
|
|
||||||
$pertinence[] = "(address LIKE UNACCENT(LOWER(:order{$k})))::int";
|
|
||||||
$nql->setParameter('order'.$k, $term);
|
|
||||||
}
|
|
||||||
$orderBy[] = implode(' + ', $pertinence).' ASC';
|
|
||||||
$orderBy[] = implode('row_number ASC', $orderBy);
|
|
||||||
|
|
||||||
$nql->setSQL($nql->getSQL().' ORDER BY '.implode(', ', $orderBy));
|
|
||||||
|
|
||||||
return $nql->toIterable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function countBySearchString(string $search, PostalCode|int|null $postalCode = null): int
|
|
||||||
{
|
|
||||||
$terms = $this->buildTermsFromSearchString($search);
|
|
||||||
if ([] === $terms) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
$rsm = new ResultSetMappingBuilder($this->entityManager);
|
|
||||||
$rsm->addScalarResult('c', 'c', Types::INTEGER);
|
|
||||||
$nql = $this->buildQueryBySearchString($rsm, 'SELECT COUNT(var.*) AS c FROM view_chill_main_address_reference var', $terms, $postalCode);
|
|
||||||
|
|
||||||
return $nql->getSingleScalarResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildTermsFromSearchString(string $search): array
|
|
||||||
{
|
|
||||||
return array_filter(
|
|
||||||
array_map(
|
|
||||||
static fn (string $term) => trim($term),
|
|
||||||
explode(' ', $search)
|
|
||||||
),
|
|
||||||
static fn (string $term) => '' !== $term
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildQueryBySearchString(ResultSetMapping $rsm, string $select, array $terms, PostalCode|int|null $postalCode = null): NativeQuery
|
|
||||||
{
|
|
||||||
$nql = $this->entityManager->createNativeQuery('', $rsm);
|
|
||||||
|
|
||||||
$sql = $select.' WHERE ';
|
|
||||||
|
|
||||||
$wheres = [];
|
|
||||||
foreach ($terms as $k => $term) {
|
|
||||||
$wheres[] = "var.address like :w{$k}";
|
|
||||||
$nql->setParameter("w{$k}", '%'.$term.'%', Types::STRING);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null !== $postalCode) {
|
|
||||||
$wheres[] = 'var.postcode_id = :postalCode';
|
|
||||||
$nql->setParameter('postalCode', $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
$nql->setSQL($sql.implode(' AND ', $wheres));
|
|
||||||
|
|
||||||
return $nql;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param mixed|null $limit
|
* @param mixed|null $limit
|
||||||
* @param mixed|null $offset
|
* @param mixed|null $offset
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Repository;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\PostalCode;
|
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
|
||||||
|
|
||||||
interface AddressReferenceRepositoryInterface extends ObjectRepository
|
|
||||||
{
|
|
||||||
public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable;
|
|
||||||
}
|
|
@@ -1,69 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Repository;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Connection;
|
|
||||||
|
|
||||||
final readonly class PostalCodeForAddressReferenceRepository implements PostalCodeForAddressReferenceRepositoryInterface
|
|
||||||
{
|
|
||||||
public function __construct(private Connection $connection) {}
|
|
||||||
|
|
||||||
public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable
|
|
||||||
{
|
|
||||||
$terms = $this->buildTermsFromSearchString($search);
|
|
||||||
if ([] === $terms) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$qb = $this->connection->createQueryBuilder();
|
|
||||||
|
|
||||||
$qb->from('chill_main_postal_code', 'cmpc')
|
|
||||||
->join('cmpc', 'view_chill_main_address_reference', 'vcmar', 'vcmar.postcode_id = cmpc.id')
|
|
||||||
->join('vcmar', 'country', 'country', condition: 'cmpc.country_id = country.id')
|
|
||||||
->setFirstResult($firstResult)
|
|
||||||
->setMaxResults($maxResults)
|
|
||||||
;
|
|
||||||
|
|
||||||
$qb->select(
|
|
||||||
'DISTINCT ON (cmpc.code, cmpc.label) cmpc.id AS postcode_id',
|
|
||||||
'cmpc.code AS code',
|
|
||||||
'cmpc.label AS label',
|
|
||||||
'country.id AS country_id',
|
|
||||||
'country.countrycode AS country_code',
|
|
||||||
'country.name AS country_name'
|
|
||||||
);
|
|
||||||
|
|
||||||
$paramId = 0;
|
|
||||||
|
|
||||||
foreach ($terms as $term) {
|
|
||||||
$qb->andWhere('vcmar.address like ?');
|
|
||||||
$qb->setParameter(++$paramId, "%{$term}%");
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $qb->executeQuery();
|
|
||||||
|
|
||||||
foreach ($result->iterateAssociative() as $row) {
|
|
||||||
yield [...$row, 'country_name' => json_decode($row['country_name'], true, 512, JSON_THROW_ON_ERROR)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function buildTermsFromSearchString(string $search): array
|
|
||||||
{
|
|
||||||
return array_filter(
|
|
||||||
array_map(
|
|
||||||
static fn (string $term) => trim($term),
|
|
||||||
explode(' ', $search)
|
|
||||||
),
|
|
||||||
static fn (string $term) => '' !== $term
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Repository;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for postal code using optimized materialized view.
|
|
||||||
*/
|
|
||||||
interface PostalCodeForAddressReferenceRepositoryInterface
|
|
||||||
{
|
|
||||||
public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable;
|
|
||||||
}
|
|
@@ -75,7 +75,6 @@ export interface Postcode {
|
|||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
center: Point;
|
center: Point;
|
||||||
country: Country;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Point {
|
export interface Point {
|
||||||
@@ -91,28 +90,6 @@ export interface Country {
|
|||||||
|
|
||||||
export type AddressRefStatus = "match" | "to_review" | "reviewed";
|
export type AddressRefStatus = "match" | "to_review" | "reviewed";
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface to create an address
|
|
||||||
*/
|
|
||||||
export interface AddressCreation {
|
|
||||||
confidential: boolean;
|
|
||||||
isNoAddress: boolean;
|
|
||||||
street: string;
|
|
||||||
streetNumber: string;
|
|
||||||
postcode: Postcode;
|
|
||||||
point: Point; // [number, number]; // [longitude, latitude]
|
|
||||||
addressReference: AddressReference;
|
|
||||||
validFrom: DateTime|null;
|
|
||||||
floor: string;
|
|
||||||
corridor: string;
|
|
||||||
steps: string;
|
|
||||||
flat: string;
|
|
||||||
buildingName: string;
|
|
||||||
distribution: string;
|
|
||||||
extra: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface Address {
|
export interface Address {
|
||||||
type: "address";
|
type: "address";
|
||||||
address_id: number;
|
address_id: number;
|
||||||
@@ -131,7 +108,7 @@ export interface Address {
|
|||||||
confidential: boolean;
|
confidential: boolean;
|
||||||
lines: string[];
|
lines: string[];
|
||||||
addressReference: AddressReference | null;
|
addressReference: AddressReference | null;
|
||||||
validFrom: DateTime | null; // TODO there is no null for validFrom
|
validFrom: DateTime;
|
||||||
validTo: DateTime | null;
|
validTo: DateTime | null;
|
||||||
point: Point | null;
|
point: Point | null;
|
||||||
refStatus: AddressRefStatus;
|
refStatus: AddressRefStatus;
|
||||||
|
@@ -1,33 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
|
|
||||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
|
||||||
import AddressPicker from "ChillMainAssets/vuejs/AddressPicker/AddressPicker.vue";
|
|
||||||
import {Ref, ref} from "vue";
|
|
||||||
|
|
||||||
const showModal: Ref<boolean> = ref(false);
|
|
||||||
|
|
||||||
const modalDialogClasses = {"modal-dialog": true, "modal-dialog-scrollable": true, "modal-xl": true};
|
|
||||||
|
|
||||||
const clickButton = () => {
|
|
||||||
showModal.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
showModal.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<modal v-if="showModal" :hide-footer="false" :modal-dialog-class="modalDialogClasses" @close="closeModal">
|
|
||||||
<template v-slot:header>TODO</template>
|
|
||||||
<template v-slot:body>
|
|
||||||
<AddressPicker></AddressPicker>
|
|
||||||
</template>
|
|
||||||
</modal>
|
|
||||||
<button class="btn btn-submit" type="button" @click="clickButton">SEARCH ADDRESS</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,170 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {Address, AddressReference} from "ChillMainAssets/types";
|
|
||||||
import SearchBar from "ChillMainAssets/vuejs/AddressPicker/Component/SearchBar.vue";
|
|
||||||
import {
|
|
||||||
AddressAggregated,
|
|
||||||
AssociatedPostalCode, fetchAddressReference,
|
|
||||||
getAddressesAggregated,
|
|
||||||
getPostalCodes,
|
|
||||||
} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
|
||||||
import {computed, Ref, ref} from "vue";
|
|
||||||
import AddressAggregatedList from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedList.vue";
|
|
||||||
import AddressDetailsForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressDetailsForm.vue";
|
|
||||||
import AddressForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressForm.vue";
|
|
||||||
import {trans, SAVE} from "translator";
|
|
||||||
|
|
||||||
interface AddressPickerProps {
|
|
||||||
suggestions?: Address[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<AddressPickerProps>(), {
|
|
||||||
suggestions: () => [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const addresses: Ref<AddressAggregated[]> = ref([]);
|
|
||||||
const postalCodes: Ref<AssociatedPostalCode[]> = ref([]);
|
|
||||||
const searchTokens: Ref<string[]> = ref([]);
|
|
||||||
const addressReference: Ref<AddressReference|null> = ref(null);
|
|
||||||
|
|
||||||
let abortControllerSearchAddress: null | AbortController = null;
|
|
||||||
let abortControllerSearchPostalCode: null | AbortController = null;
|
|
||||||
|
|
||||||
const searchResultsClasses = computed(() => ({
|
|
||||||
"mid-size": addressReference !== null,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const floor = ref<string>("");
|
|
||||||
const corridor = ref<string>("");
|
|
||||||
const steps = ref<string>("");
|
|
||||||
const flat = ref<string>("");
|
|
||||||
const buildingName = ref<string>("");
|
|
||||||
const extra = ref<string>("");
|
|
||||||
const distribution = ref<string>("");
|
|
||||||
|
|
||||||
|
|
||||||
const onSearch = async function (search: string): Promise<void> {
|
|
||||||
performSearchForAddress(search);
|
|
||||||
performSearchForPostalCode(search);
|
|
||||||
searchTokens.value = [search];
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPickPosition = async (id: string) => {
|
|
||||||
console.log('Pick Position', id);
|
|
||||||
addressReference.value = await fetchAddressReference(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const performSearchForAddress = async (search: string): Promise<void> => {
|
|
||||||
if (null !== abortControllerSearchAddress) {
|
|
||||||
abortControllerSearchAddress.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("" === search) {
|
|
||||||
addresses.value = [];
|
|
||||||
abortControllerSearchAddress = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
abortControllerSearchAddress = new AbortController();
|
|
||||||
|
|
||||||
console.log("onSearch", search);
|
|
||||||
try {
|
|
||||||
addresses.value = await getAddressesAggregated(
|
|
||||||
search,
|
|
||||||
abortControllerSearchAddress,
|
|
||||||
);
|
|
||||||
abortControllerSearchAddress = null;
|
|
||||||
|
|
||||||
// check if there is only one result
|
|
||||||
if (addresses.value.length === 1 && Object.keys(addresses.value[0].positions).length === 1) {
|
|
||||||
onPickPosition(Object.keys(addresses.value[0].positions)[0]);
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
if (e instanceof DOMException && e.name === "AbortError") {
|
|
||||||
console.log("search aborted for:", search);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const performSearchForPostalCode = async (search: string): Promise<void> => {
|
|
||||||
if (null !== abortControllerSearchPostalCode) {
|
|
||||||
abortControllerSearchPostalCode.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("" === search) {
|
|
||||||
addresses.value = [];
|
|
||||||
abortControllerSearchPostalCode = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
abortControllerSearchPostalCode = new AbortController();
|
|
||||||
|
|
||||||
console.log("onSearch", search);
|
|
||||||
try {
|
|
||||||
postalCodes.value = await getPostalCodes(
|
|
||||||
search,
|
|
||||||
abortControllerSearchPostalCode,
|
|
||||||
);
|
|
||||||
abortControllerSearchPostalCode = null;
|
|
||||||
} catch (e: unknown) {
|
|
||||||
if (e instanceof DOMException && e.name === "AbortError") {
|
|
||||||
console.log("search postal code aborted for:", search);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const save = async(): Promise<void> => {
|
|
||||||
console.log("save");
|
|
||||||
console.log("content", floor, corridor, steps, flat, buildingName, extra, distribution);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<search-bar @search="onSearch"></search-bar>
|
|
||||||
<div class="address-pick-content">
|
|
||||||
<div class="search-results" :class="searchResultsClasses">
|
|
||||||
<address-aggregated-list :addresses="addresses" :search-tokens="searchTokens" @pick-position="(id) => onPickPosition(id)"></address-aggregated-list>
|
|
||||||
</div>
|
|
||||||
<div v-if="addressReference !== null" class="address-details-form">
|
|
||||||
<address-details-form :address="addressReference"
|
|
||||||
v-model:floor="floor"
|
|
||||||
v-model:corridor="corridor"
|
|
||||||
v-model:steps="steps"
|
|
||||||
v-model:flat="flat"
|
|
||||||
v-model:building-name="buildingName"
|
|
||||||
v-model:extra="extra"
|
|
||||||
v-model:distribution="distribution"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ul class="record_actions">
|
|
||||||
<li><button class="btn btn-save">{{ trans(SAVE) }}</button></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.address-pick-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 1rem;
|
|
||||||
|
|
||||||
.search-results {
|
|
||||||
&.mid-size {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.address-details-form {
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,25 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
|
||||||
import AddressAggregatedListItem from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedListItem.vue";
|
|
||||||
|
|
||||||
interface AddressAggregatedListProps {
|
|
||||||
addresses: AddressAggregated[];
|
|
||||||
searchTokens: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<AddressAggregatedListProps>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
pickPosition: [id: string]
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<template v-for="a in props.addresses" :key="a.row_number">
|
|
||||||
<address-aggregated-list-item :address="a" :search-tokens="props.searchTokens" @pick-position="(id) => emit('pickPosition', id)"></address-aggregated-list-item>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,82 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
|
|
||||||
import {computed, ref} from "vue";
|
|
||||||
|
|
||||||
interface AddressAggregatedListItemProps {
|
|
||||||
address: AddressAggregated;
|
|
||||||
searchTokens: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<AddressAggregatedListItemProps>();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
pickPosition: [id: string]
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const showAllPositions = ref<boolean>(false);
|
|
||||||
const positionsToShow = computed((): Record<string, string> => {
|
|
||||||
const obj: Record<string, any> = {};
|
|
||||||
let count = 0;
|
|
||||||
for (const [id, position] of Object.entries(props.address.positions)) {
|
|
||||||
obj[id] = position;
|
|
||||||
count++;
|
|
||||||
if (count >= 10 && !showAllPositions.value) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
})
|
|
||||||
const needToShowMorePosition = computed(() => {
|
|
||||||
return Object.keys(props.address.positions).length > 10;
|
|
||||||
})
|
|
||||||
|
|
||||||
const onClickButton = (id: string) => {
|
|
||||||
console.log('onClickButton', id);
|
|
||||||
emit('pickPosition', id);
|
|
||||||
}
|
|
||||||
const displayAllPositions = () => {
|
|
||||||
showAllPositions.value = true;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="street">
|
|
||||||
<span>{{ props.address.street }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="postcode">
|
|
||||||
<span>{{ props.address.code }}</span> <span>{{ address.label }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="positions">
|
|
||||||
<ul>
|
|
||||||
<li v-for="(position, id) in positionsToShow" :key="id" >
|
|
||||||
<button type="button" @click="onClickButton(id)" >
|
|
||||||
{{ position }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li v-if="needToShowMorePosition">
|
|
||||||
<button @click="displayAllPositions">show all</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.street {
|
|
||||||
font-variant: small-caps;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.postcode {
|
|
||||||
font-variant: small-caps;
|
|
||||||
}
|
|
||||||
.positions ul {
|
|
||||||
list-style-type: none;
|
|
||||||
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,44 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {AddressReference} from "ChillMainAssets/types";
|
|
||||||
import {computed, ref} from "vue";
|
|
||||||
import {addressReferenceToAddress} from "ChillMainAssets/vuejs/AddressPicker/helper";
|
|
||||||
import AddressDetailsContent from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsContent.vue";
|
|
||||||
import AddressForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressForm.vue";
|
|
||||||
|
|
||||||
export interface AddressDetailsFormProps {
|
|
||||||
address: AddressReference;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<AddressDetailsFormProps>();
|
|
||||||
|
|
||||||
const floor = ref<string>("");
|
|
||||||
const corridor = ref<string>("");
|
|
||||||
const steps = ref<string>("");
|
|
||||||
const flat = ref<string>("");
|
|
||||||
const buildingName = ref<string>("");
|
|
||||||
const extra = ref<string>("");
|
|
||||||
const distribution = ref<string>("");
|
|
||||||
|
|
||||||
const address = computed(() => addressReferenceToAddress(props.address));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<address-form
|
|
||||||
@update:floor="val => (floor = val)"
|
|
||||||
@update:corridor="val => (corridor = val)"
|
|
||||||
@update:steps="val => (steps = val)"
|
|
||||||
@update:flat="val => (flat = val)"
|
|
||||||
@update:building-name="val => (buildingName = val)"
|
|
||||||
@update:extra="val => (extra = val)"
|
|
||||||
@update:distribution="val => (distribution = val)"
|
|
||||||
></address-form>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<address-details-content :address="address"></address-details-content>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,112 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import {
|
|
||||||
ADDRESS_STREET,
|
|
||||||
ADDRESS_STREET_NUMBER,
|
|
||||||
ADDRESS_FLOOR,
|
|
||||||
ADDRESS_CORRIDOR,
|
|
||||||
ADDRESS_STEPS,
|
|
||||||
ADDRESS_FLAT,
|
|
||||||
ADDRESS_BUILDING_NAME,
|
|
||||||
ADDRESS_DISTRIBUTION,
|
|
||||||
ADDRESS_EXTRA,
|
|
||||||
ADDRESS_FILL_AN_ADDRESS,
|
|
||||||
trans,
|
|
||||||
} from "translator";
|
|
||||||
import {ref} from "vue";
|
|
||||||
|
|
||||||
const isNoAddress = ref(false);
|
|
||||||
|
|
||||||
const floor = defineModel("floor");
|
|
||||||
const corridor = defineModel("corridor");
|
|
||||||
const steps = defineModel("steps");
|
|
||||||
const flat = defineModel("flat");
|
|
||||||
const buildingName = defineModel("buildingName");
|
|
||||||
const extra = defineModel("extra");
|
|
||||||
const distribution = defineModel("distribution");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="floor"
|
|
||||||
:placeholder="trans(ADDRESS_FLOOR)"
|
|
||||||
v-model="floor"
|
|
||||||
/>
|
|
||||||
<label for="floor">{{ trans(ADDRESS_FLOOR) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="corridor"
|
|
||||||
:placeholder="trans(ADDRESS_CORRIDOR)"
|
|
||||||
v-model="corridor"
|
|
||||||
/>
|
|
||||||
<label for="corridor">{{ trans(ADDRESS_CORRIDOR) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="steps"
|
|
||||||
:placeholder="trans(ADDRESS_STEPS)"
|
|
||||||
v-model="steps"
|
|
||||||
/>
|
|
||||||
<label for="steps">{{ trans(ADDRESS_STEPS) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="flat"
|
|
||||||
:placeholder="trans(ADDRESS_FLAT)"
|
|
||||||
v-model="flat"
|
|
||||||
/>
|
|
||||||
<label for="flat">{{ trans(ADDRESS_FLAT) }}</label>
|
|
||||||
</div>
|
|
||||||
<div :class="isNoAddress ? 'col-lg-12' : 'col-lg-6'">
|
|
||||||
<div class="form-floating my-1" v-if="!isNoAddress">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="buildingName"
|
|
||||||
maxlength="255"
|
|
||||||
:placeholder="trans(ADDRESS_BUILDING_NAME)"
|
|
||||||
v-model="buildingName"
|
|
||||||
/>
|
|
||||||
<label for="buildingName">{{
|
|
||||||
trans(ADDRESS_BUILDING_NAME)
|
|
||||||
}}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="extra"
|
|
||||||
maxlength="255"
|
|
||||||
:placeholder="trans(ADDRESS_EXTRA)"
|
|
||||||
v-model="extra"
|
|
||||||
/>
|
|
||||||
<label for="extra">{{ trans(ADDRESS_EXTRA) }}</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-floating my-1" v-if="!isNoAddress">
|
|
||||||
<input
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
name="distribution"
|
|
||||||
maxlength="255"
|
|
||||||
:placeholder="trans(ADDRESS_DISTRIBUTION)"
|
|
||||||
v-model="distribution"
|
|
||||||
/>
|
|
||||||
<label for="distribution">{{
|
|
||||||
trans(ADDRESS_DISTRIBUTION)
|
|
||||||
}}</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
|
|
||||||
</style>
|
|
@@ -1,39 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ADDRESS_PICKER_SEARCH_FOR_ADDRESSES, trans } from 'translator';
|
|
||||||
const emits = defineEmits<{
|
|
||||||
search: [search: string];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let searchTimer = 0;
|
|
||||||
let searchString: string;
|
|
||||||
|
|
||||||
const onInput = function (event: InputEvent) {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
const value = target.value;
|
|
||||||
searchString = value;
|
|
||||||
|
|
||||||
if (0 === searchTimer) {
|
|
||||||
window.clearTimeout(searchTimer);
|
|
||||||
searchTimer = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTimer = window.setTimeout(() => {
|
|
||||||
if (value === searchString) {
|
|
||||||
emits("search", value);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="input-group mb-3">
|
|
||||||
<span class="input-group-text">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
|
|
||||||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<input type="search" class="form-control" @input="onInput" :placeholder="trans(ADDRESS_PICKER_SEARCH_FOR_ADDRESSES)" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
@@ -1,69 +0,0 @@
|
|||||||
import {AddressReference, TranslatableString} from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
export interface AddressAggregated {
|
|
||||||
row_number: number;
|
|
||||||
street: string;
|
|
||||||
postcode_id: number;
|
|
||||||
code: string;
|
|
||||||
label: string;
|
|
||||||
positions: Record<string, string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssociatedPostalCode {
|
|
||||||
postcode_id: number;
|
|
||||||
code: string;
|
|
||||||
label: string;
|
|
||||||
country_id: number;
|
|
||||||
country_code: string;
|
|
||||||
country_name: TranslatableString;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @throws {DOMException} when fetch is aborted, the property name is always equals to 'AbortError'
|
|
||||||
*/
|
|
||||||
export const getAddressesAggregated = async (
|
|
||||||
search: string,
|
|
||||||
abortController: AbortController,
|
|
||||||
): Promise<AddressAggregated[]> => {
|
|
||||||
const params = new URLSearchParams({ q: search.trim() });
|
|
||||||
let response = null;
|
|
||||||
|
|
||||||
response = await fetch(
|
|
||||||
`/api/1.0/main/address-reference/aggregated/search?${params}`,
|
|
||||||
{ signal: abortController.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPostalCodes = async (
|
|
||||||
search: string,
|
|
||||||
abortController: AbortController,
|
|
||||||
): Promise<AssociatedPostalCode[]> => {
|
|
||||||
const params = new URLSearchParams({ q: search.trim() });
|
|
||||||
let response = null;
|
|
||||||
|
|
||||||
response = await fetch(
|
|
||||||
`/api/1.0/main/address-reference/postal-code/search?${params}`,
|
|
||||||
{ signal: abortController.signal },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchAddressReference = async (id: string): Promise<AddressReference> => {
|
|
||||||
const response = await fetch(`/api/1.0/main/address-reference/${id}.json`);
|
|
||||||
if (response.ok) {
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(response.statusText);
|
|
||||||
}
|
|
@@ -1,21 +0,0 @@
|
|||||||
import {Address, AddressCreation, AddressReference} from "ChillMainAssets/types";
|
|
||||||
|
|
||||||
export const addressReferenceToAddress = (reference: AddressReference): AddressCreation => {
|
|
||||||
return {
|
|
||||||
street: reference.street,
|
|
||||||
streetNumber: reference.streetNumber,
|
|
||||||
postcode: reference.postcode,
|
|
||||||
floor: "",
|
|
||||||
corridor: "",
|
|
||||||
steps: "",
|
|
||||||
flat: "",
|
|
||||||
buildingName: "",
|
|
||||||
distribution: "",
|
|
||||||
extra: "",
|
|
||||||
confidential: false,
|
|
||||||
addressReference: reference,
|
|
||||||
point: reference.point,
|
|
||||||
isNoAddress: false,
|
|
||||||
validFrom: null,
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,12 +0,0 @@
|
|||||||
import { createApp } from "vue";
|
|
||||||
import AddressButton from "ChillMainAssets/vuejs/AddressPicker/AddressButton.vue";
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
|
||||||
document
|
|
||||||
.querySelectorAll<HTMLDivElement>("div[data-address-picker]")
|
|
||||||
.forEach((elem): void => {
|
|
||||||
const app = createApp(AddressButton);
|
|
||||||
|
|
||||||
app.mount(elem);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -4,27 +4,24 @@
|
|||||||
:show-button-details="false"
|
:show-button-details="false"
|
||||||
></address-render-box>
|
></address-render-box>
|
||||||
<address-details-ref-matching
|
<address-details-ref-matching
|
||||||
v-if="isAddress(props.address)"
|
|
||||||
:address="props.address"
|
:address="props.address"
|
||||||
@update-address="onUpdateAddress"
|
@update-address="onUpdateAddress"
|
||||||
></address-details-ref-matching>
|
></address-details-ref-matching>
|
||||||
<address-details-map :address="props.address"></address-details-map>
|
<address-details-map :address="props.address"></address-details-map>
|
||||||
<address-details-geographical-layers
|
<address-details-geographical-layers
|
||||||
v-if="isAddress(props.address)"
|
|
||||||
:address="props.address"
|
:address="props.address"
|
||||||
></address-details-geographical-layers>
|
></address-details-geographical-layers>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {Address, AddressCreation} from "../../../types";
|
import { Address } from "../../../types";
|
||||||
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
|
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
|
||||||
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
|
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
|
||||||
import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue";
|
import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue";
|
||||||
import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue";
|
import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue";
|
||||||
import {isAddress} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
|
||||||
|
|
||||||
interface AddressModalContentProps {
|
interface AddressModalContentProps {
|
||||||
address: Address|AddressCreation;
|
address: Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<AddressModalContentProps>();
|
const props = defineProps<AddressModalContentProps>();
|
||||||
|
@@ -12,91 +12,90 @@
|
|||||||
Voir sur
|
Voir sur
|
||||||
<a :href="makeUrlGoogleMap(props.address)" target="_blank"
|
<a :href="makeUrlGoogleMap(props.address)" target="_blank"
|
||||||
>Google Maps</a
|
>Google Maps</a
|
||||||
> <a
|
>
|
||||||
:href="makeUrlOsm(props.address)" target="_blank"
|
<a :href="makeUrlOsm(props.address)" target="_blank">OSM</a>
|
||||||
>OSM</a>
|
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {computed, onMounted, ref} from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import markerIconPng from "leaflet/dist/images/marker-icon.png";
|
import markerIconPng from "leaflet/dist/images/marker-icon.png";
|
||||||
import L, { LatLngExpression, LatLngTuple } from "leaflet";
|
import L, { LatLngExpression, LatLngTuple } from "leaflet";
|
||||||
import {Address, AddressCreation, Point} from "../../../../types";
|
import { Address, Point } from "../../../../types";
|
||||||
import {buildAddressLines, getAddressPoint} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
|
||||||
import {useLeafletDisplayLayer, useLeafletMap, useLeafletMarker, useLeafletTileLayer} from "vue-use-leaflet";
|
|
||||||
|
|
||||||
const lonLatForLeaflet = (point: Point): LatLngTuple => {
|
const lonLatForLeaflet = (point: Point): LatLngTuple => {
|
||||||
return [point.coordinates[1], point.coordinates[0]];
|
return [point.coordinates[1], point.coordinates[0]];
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface MapProps {
|
export interface MapProps {
|
||||||
address: Address|AddressCreation;
|
address: Address;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = defineProps<MapProps>();
|
const props = defineProps<MapProps>();
|
||||||
|
|
||||||
const markerIcon = L.icon({
|
|
||||||
iconUrl: markerIconPng,
|
|
||||||
iconAnchor: [12, 41],
|
|
||||||
});
|
|
||||||
const latLngMarker = computed((): LatLngExpression => {
|
|
||||||
if (props.address === null || props.address.point === null) {
|
|
||||||
return [0, 0, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [props.address.point.coordinates[1], props.address.point.coordinates[0], 0]
|
|
||||||
});
|
|
||||||
|
|
||||||
const map_div = ref<HTMLDivElement | null>(null);
|
const map_div = ref<HTMLDivElement | null>(null);
|
||||||
const map = useLeafletMap(map_div, {zoom: 18, center: latLngMarker});
|
let map: L.Map | null = null;
|
||||||
const tileLayer = useLeafletTileLayer(
|
let marker: L.Marker | null = null;
|
||||||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
||||||
attribution:
|
onMounted(() => {
|
||||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
if (map_div.value === null) {
|
||||||
|
// there is no map div when the address does not have any Point
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
useLeafletDisplayLayer(map, tileLayer);
|
|
||||||
|
|
||||||
|
if (props.address.point !== null) {
|
||||||
|
map = L.map(map_div.value);
|
||||||
|
map.setView(lonLatForLeaflet(props.address.point), 18);
|
||||||
|
|
||||||
const marker = useLeafletMarker(latLngMarker, {icon: markerIcon});
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
useLeafletDisplayLayer(map, marker);
|
attribution:
|
||||||
|
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
const markerIcon = L.icon({
|
||||||
|
iconUrl: markerIconPng,
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
});
|
||||||
|
|
||||||
const makeUrlGoogleMap = (address: Address|AddressCreation): string => {
|
marker = L.marker(lonLatForLeaflet(props.address.point), {
|
||||||
|
icon: markerIcon,
|
||||||
|
});
|
||||||
|
marker.addTo(map);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeUrlGoogleMap = (address: Address): string => {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("api", "1");
|
params.append("api", "1");
|
||||||
const point = getAddressPoint(address);
|
if (address.point !== null && address.addressReference !== null) {
|
||||||
if (point !== null && address.addressReference !== null) {
|
|
||||||
params.append(
|
params.append(
|
||||||
"query",
|
"query",
|
||||||
`${point.coordinates[1]} ${point.coordinates[0]}`,
|
`${address.point.coordinates[1]} ${address.point.coordinates[0]}`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
params.append("query", buildAddressLines(address).join(", "));
|
params.append("query", address.lines.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
return `https://www.google.com/maps/search/?${params.toString()}`;
|
return `https://www.google.com/maps/search/?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const makeUrlOsm = (address: Address|AddressCreation): string => {
|
const makeUrlOsm = (address: Address): string => {
|
||||||
const point = getAddressPoint(address);
|
if (address.point !== null && address.addressReference !== null) {
|
||||||
if (point !== null && address.addressReference !== null) {
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("mlat", `${point.coordinates[1]}`);
|
params.append("mlat", `${address.point.coordinates[1]}`);
|
||||||
params.append("mlon", `${point.coordinates[0]}`);
|
params.append("mlon", `${address.point.coordinates[0]}`);
|
||||||
const hashParams = new URLSearchParams();
|
const hashParams = new URLSearchParams();
|
||||||
hashParams.append(
|
hashParams.append(
|
||||||
"map",
|
"map",
|
||||||
`18/${point.coordinates[1]}/${point.coordinates[0]}`,
|
`18/${address.point.coordinates[1]}/${address.point.coordinates[0]}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
|
return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.append("query", buildAddressLines(address).join(", "));
|
params.append("query", address.lines.join(", "));
|
||||||
|
|
||||||
return `https://www.openstreetmap.org/search?${params.toString()}`;
|
return `https://www.openstreetmap.org/search?${params.toString()}`;
|
||||||
};
|
};
|
||||||
|
@@ -1,46 +0,0 @@
|
|||||||
import {Address, AddressCreation, Point} from "ChillMainAssets/types";
|
|
||||||
import addressFormatter, {Input} from "@fragaria/address-formatter";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given object is of type Address by verifying the existence
|
|
||||||
* of the `lines` property and confirming that it is an array of strings.
|
|
||||||
*
|
|
||||||
* @param {AddressCreation | Address} obj - The object to check.
|
|
||||||
* @return {boolean} Returns true if the object is of type Address, otherwise false.
|
|
||||||
*/
|
|
||||||
export function isAddress(obj: AddressCreation | Address): obj is Address {
|
|
||||||
return (obj as any).lines !== undefined && Array.isArray((obj as any).lines);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAddressFormatterObject(address: AddressCreation): Input {
|
|
||||||
return {
|
|
||||||
city: address.postcode.name,
|
|
||||||
postcode: address.postcode.code,
|
|
||||||
countryCode: address.postcode.country.code,
|
|
||||||
street: address.street,
|
|
||||||
houseNumber: address.streetNumber,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const buildAddressLines = (address: AddressCreation|Address): string[] => {
|
|
||||||
if (isAddress(address)) {
|
|
||||||
return address.lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lines = addressFormatter.format(buildAddressFormatterObject(address), {output: 'array', countryCode: address.addressReference.postcode.country.code });
|
|
||||||
console.log('lines:', lines);
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const buildAddressText = (address: AddressCreation|Address): string => {
|
|
||||||
return buildAddressLines(address).join(' - ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getAddressPoint = (address: AddressCreation|Address): Point|null => {
|
|
||||||
if (isAddress(address)) {
|
|
||||||
return address.point;
|
|
||||||
}
|
|
||||||
|
|
||||||
return address.addressReference?.point;
|
|
||||||
}
|
|
@@ -4,14 +4,14 @@
|
|||||||
<div v-if="isConfidential">
|
<div v-if="isConfidential">
|
||||||
<confidential :position-btn-far="true">
|
<confidential :position-btn-far="true">
|
||||||
<template #confidential-content>
|
<template #confidential-content>
|
||||||
<div v-if="isMultiline">
|
<div v-if="isMultiline === true">
|
||||||
<p
|
<p
|
||||||
v-for="(l, i) in buildAddressLines(address)"
|
v-for="(l, i) in address.lines"
|
||||||
:key="`line-${i}`"
|
:key="`line-${i}`"
|
||||||
>
|
>
|
||||||
{{ l }}
|
{{ l }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="showButtonDetails && isAddress(address) ">
|
<p v-if="showButtonDetails">
|
||||||
<address-details-button
|
<address-details-button
|
||||||
:address_id="address.address_id"
|
:address_id="address.address_id"
|
||||||
:address_ref_status="address.refStatus"
|
:address_ref_status="address.refStatus"
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p v-if="'' !== buildAddressText(address)" class="street">
|
<p v-if="'' !== address.text" class="street">
|
||||||
{{ buildAddressText(address) }}
|
{{ address.text }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="null !== address.postcode"
|
v-if="null !== address.postcode"
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
{{ address.postcode.code }}
|
{{ address.postcode.code }}
|
||||||
{{ address.postcode.name }}
|
{{ address.postcode.name }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="null !== address.postcode" class="country">
|
<p v-if="null !== address.country" class="country">
|
||||||
{{ localizeString(address.postcode.country.name) }}
|
{{ localizeString(address.country.name) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -38,11 +38,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isConfidential">
|
<div v-if="!isConfidential">
|
||||||
<div v-if="isMultiline">
|
<div v-if="isMultiline === true">
|
||||||
<p v-for="(l, i) in buildAddressLines(address)" :key="`line-${i}`">
|
<p v-for="(l, i) in address.lines" :key="`line-${i}`">
|
||||||
{{ l }}
|
{{ l }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="showButtonDetails && isAddress(address) ">
|
<p v-if="showButtonDetails">
|
||||||
<address-details-button
|
<address-details-button
|
||||||
:address_id="address.address_id"
|
:address_id="address.address_id"
|
||||||
:address_ref_status="address.refStatus"
|
:address_ref_status="address.refStatus"
|
||||||
@@ -50,9 +50,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<p v-if="'' !== buildAddressText(address)" class="street">
|
<p v-if="address.text" class="street">
|
||||||
{{ buildAddressText(address)}}
|
{{ address.text }}
|
||||||
<template v-if="showButtonDetails && isAddress(address) ">
|
<template v-if="showButtonDetails">
|
||||||
<address-details-button
|
<address-details-button
|
||||||
:address_id="address.address_id"
|
:address_id="address.address_id"
|
||||||
:address_ref_status="address.refStatus"
|
:address_ref_status="address.refStatus"
|
||||||
@@ -63,49 +63,68 @@
|
|||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
|
|
||||||
<div v-if="useDatePane" class="address-more">
|
<div v-if="useDatePane === true" class="address-more">
|
||||||
<div v-if="address.validFrom">
|
<div v-if="address.validFrom">
|
||||||
<span class="validFrom">
|
<span class="validFrom">
|
||||||
<b>{{ trans(ADDRESS_VALID_FROM) }}</b
|
<b>{{ trans(ADDRESS_VALID_FROM) }}</b
|
||||||
>: {{ address.validFrom?.datetime8601 }}
|
>: {{ $d(address.validFrom.date) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isAddress(address) && address.validTo !== null">
|
<div v-if="address.validTo">
|
||||||
<span class="validTo">
|
<span class="validTo">
|
||||||
<b>{{ trans(ADDRESS_VALID_TO) }}</b
|
<b>{{ trans(ADDRESS_VALID_TO) }}</b
|
||||||
>: {{ address.validTo?.datetime8601 }}
|
>: {{ $d(address.validTo.date) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</component>
|
</component>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script>
|
||||||
import { computed } from "vue";
|
|
||||||
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";
|
import AddressDetailsButton from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsButton.vue";
|
||||||
import { trans, ADDRESS_VALID_FROM, ADDRESS_VALID_TO } from "translator";
|
import { trans, ADDRESS_VALID_FROM, ADDRESS_VALID_TO } from "translator";
|
||||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||||
import {Address, AddressCreation} from "ChillMainAssets/types";
|
|
||||||
import {isAddress, buildAddressLines, buildAddressText} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
|
||||||
|
|
||||||
const props = withDefaults(
|
export default {
|
||||||
defineProps<{
|
name: "AddressRenderBox",
|
||||||
address: Address|AddressCreation;
|
methods: { localizeString },
|
||||||
isMultiline?: boolean;
|
components: {
|
||||||
useDatePane?: boolean;
|
Confidential,
|
||||||
showButtonDetails?: boolean;
|
AddressDetailsButton,
|
||||||
}>(),
|
},
|
||||||
{
|
props: {
|
||||||
isMultiline: true,
|
address: {
|
||||||
useDatePane: false,
|
type: Object,
|
||||||
showButtonDetails: true,
|
},
|
||||||
}
|
isMultiline: {
|
||||||
);
|
default: true,
|
||||||
|
type: Boolean,
|
||||||
const component = computed(() => (props.isMultiline ? "div" : "span"));
|
},
|
||||||
const multiline = computed(() => (props.isMultiline ? "multiline" : ""));
|
useDatePane: {
|
||||||
const isConfidential = computed(() => Boolean(props.address?.confidential));
|
default: false,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
showButtonDetails: {
|
||||||
|
default: true,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return { trans, ADDRESS_VALID_FROM, ADDRESS_VALID_TO };
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
component() {
|
||||||
|
return this.isMultiline === true ? "div" : "span";
|
||||||
|
},
|
||||||
|
multiline() {
|
||||||
|
return this.isMultiline === true ? "multiline" : "";
|
||||||
|
},
|
||||||
|
isConfidential() {
|
||||||
|
return this.address.confidential;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
{% extends '@ChillMain/layout.html.twig' %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
{{ encore_entry_link_tags('address_picker') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
{{ encore_entry_script_tags('address_picker') }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div data-address-picker="data-address-picker"></div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@@ -21,8 +21,6 @@
|
|||||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||||
|
|
||||||
{{ form_row(form.addressesEmails) }}
|
|
||||||
|
|
||||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||||
|
|
||||||
<div class="mb-3 row">
|
<div class="mb-3 row">
|
||||||
|
@@ -66,7 +66,6 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
|
|||||||
'name' => $address->getPostCode()->getName(),
|
'name' => $address->getPostCode()->getName(),
|
||||||
'code' => $address->getPostCode()->getCode(),
|
'code' => $address->getPostCode()->getCode(),
|
||||||
'center' => $address->getPostcode()->getCenter(),
|
'center' => $address->getPostcode()->getCenter(),
|
||||||
'country' => $this->normalizer->normalize($address->getPostCode()->getCountry(), $format, [AbstractNormalizer::GROUPS => ['read']]),
|
|
||||||
],
|
],
|
||||||
'country' => [
|
'country' => [
|
||||||
'id' => $address->getPostCode()->getCountry()->getId(),
|
'id' => $address->getPostCode()->getCountry()->getId(),
|
||||||
|
@@ -1,118 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Tests\Controller;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Controller\AddressReferenceAggregatedApiController;
|
|
||||||
use Chill\MainBundle\Repository\AddressReferenceRepositoryInterface;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
||||||
use Symfony\Component\Security\Core\Security;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @covers \Chill\MainBundle\Controller\AddressReferenceAggregatedApiController
|
|
||||||
*/
|
|
||||||
final class AddressReferenceAggregatedApiControllerTest extends TestCase
|
|
||||||
{
|
|
||||||
use ProphecyTrait;
|
|
||||||
|
|
||||||
public function testAccessDeniedWhenNotAuthenticated(): void
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('IS_AUTHENTICATED')->willReturn(false);
|
|
||||||
|
|
||||||
$repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
|
|
||||||
|
|
||||||
$controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
|
|
||||||
|
|
||||||
$request = new Request(query: ['q' => 'anything']);
|
|
||||||
|
|
||||||
$this->expectException(AccessDeniedHttpException::class);
|
|
||||||
|
|
||||||
$controller->search($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testBadRequestWhenQueryIsMissing(): void
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('IS_AUTHENTICATED')->willReturn(true);
|
|
||||||
|
|
||||||
$repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
|
|
||||||
|
|
||||||
$controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
|
|
||||||
|
|
||||||
$request = new Request();
|
|
||||||
|
|
||||||
$this->expectException(BadRequestHttpException::class);
|
|
||||||
$this->expectExceptionMessage('Parameter "q" is required.');
|
|
||||||
|
|
||||||
$controller->search($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testBadRequestWhenQueryIsEmpty(): void
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('IS_AUTHENTICATED')->willReturn(true);
|
|
||||||
|
|
||||||
$repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
|
|
||||||
|
|
||||||
$controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
|
|
||||||
|
|
||||||
$request = new Request(query: ['q' => ' ']);
|
|
||||||
|
|
||||||
$this->expectException(BadRequestHttpException::class);
|
|
||||||
$this->expectExceptionMessage('Parameter "q" is required and cannot be empty.');
|
|
||||||
|
|
||||||
$controller->search($request);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testSuccessfulSearchReturnsJsonAndCallsRepositoryWithTrimmedQuery(): void
|
|
||||||
{
|
|
||||||
$security = $this->prophesize(Security::class);
|
|
||||||
$security->isGranted('IS_AUTHENTICATED')->willReturn(true);
|
|
||||||
|
|
||||||
$expectedQuery = 'foo';
|
|
||||||
$iterableResult = new \ArrayIterator([
|
|
||||||
[
|
|
||||||
'street' => 'Main Street',
|
|
||||||
'postcode_id' => 123,
|
|
||||||
'code' => '1000',
|
|
||||||
'label' => 'Brussels',
|
|
||||||
'positions' => ['1' => '12', '2' => '14'],
|
|
||||||
'row_number' => 1,
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
|
|
||||||
$repo->findAggregatedBySearchString($expectedQuery)->willReturn($iterableResult)->shouldBeCalledOnce();
|
|
||||||
|
|
||||||
$controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
|
|
||||||
|
|
||||||
// Provide spaces around to ensure trimming is applied
|
|
||||||
$request = new Request(query: ['q' => " {$expectedQuery} "]);
|
|
||||||
|
|
||||||
$response = $controller->search($request);
|
|
||||||
|
|
||||||
self::assertSame(200, $response->getStatusCode());
|
|
||||||
$data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
self::assertIsArray($data);
|
|
||||||
self::assertCount(1, $data);
|
|
||||||
self::assertSame('Main Street', $data[0]['street']);
|
|
||||||
self::assertSame(123, $data[0]['postcode_id']);
|
|
||||||
self::assertSame('1000', $data[0]['code']);
|
|
||||||
self::assertSame('Brussels', $data[0]['label']);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,88 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Tests\Repository;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\AddressReference;
|
|
||||||
use Chill\MainBundle\Entity\PostalCode;
|
|
||||||
use Chill\MainBundle\Repository\AddressReferenceRepository;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
class AddressReferenceRepositoryTest extends KernelTestCase
|
|
||||||
{
|
|
||||||
private static AddressReferenceRepository $repository;
|
|
||||||
|
|
||||||
public static function setUpBeforeClass(): void
|
|
||||||
{
|
|
||||||
static::bootKernel();
|
|
||||||
static::$repository = static::getContainer()->get(AddressReferenceRepository::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider generateSearchString
|
|
||||||
*/
|
|
||||||
public function testFindBySearchString(string $search, int|PostalCode|null $postalCode, string $text, ?array $expected = null): void
|
|
||||||
{
|
|
||||||
$actual = static::$repository->findBySearchString($search, $postalCode);
|
|
||||||
|
|
||||||
self::assertIsIterable($actual, $text);
|
|
||||||
|
|
||||||
if (null !== $expected) {
|
|
||||||
self::assertEquals($expected, iterator_to_array($actual));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider generateSearchString
|
|
||||||
*/
|
|
||||||
public function testCountBySearchString(string $search, int|PostalCode|null $postalCode, string $text, ?array $expected = null): void
|
|
||||||
{
|
|
||||||
$actual = static::$repository->countBySearchString($search, $postalCode);
|
|
||||||
|
|
||||||
self::assertIsInt($actual, $text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @dataProvider generateSearchString
|
|
||||||
*/
|
|
||||||
public function testFindAggreggateBySearchString(string $search, int|PostalCode|null $postalCode, string $text, ?array $expected = null): void
|
|
||||||
{
|
|
||||||
$actual = static::$repository->findAggregatedBySearchString($search, $postalCode);
|
|
||||||
|
|
||||||
self::assertIsIterable($actual, $text);
|
|
||||||
|
|
||||||
if (null !== $expected) {
|
|
||||||
self::assertEquals($expected, iterator_to_array($actual));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function generateSearchString(): iterable
|
|
||||||
{
|
|
||||||
self::bootKernel();
|
|
||||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
||||||
/** @var AddressReference $ar */
|
|
||||||
$ar = $em->createQuery('SELECT ar FROM '.AddressReference::class.' ar')
|
|
||||||
->setMaxResults(1)
|
|
||||||
->getSingleResult();
|
|
||||||
|
|
||||||
yield ['', null, 'search with empty string', []];
|
|
||||||
yield [' ', null, 'search with spaces only', []];
|
|
||||||
yield ['rue des moulins', null, 'search contains an empty string'];
|
|
||||||
yield ['rue des moulins', $ar->getPostcode()->getId(), 'search with postal code as an id'];
|
|
||||||
yield ['rue des moulins', $ar->getPostcode(), 'search with postal code instance'];
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Tests\Repository;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Repository\PostalCodeForAddressReferenceRepository;
|
|
||||||
use Doctrine\DBAL\Connection;
|
|
||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*
|
|
||||||
* @coversNothing
|
|
||||||
*/
|
|
||||||
final class PostalCodeForAddressReferenceRepositoryTest extends KernelTestCase
|
|
||||||
{
|
|
||||||
private Connection $connection;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
self::bootKernel();
|
|
||||||
$this->connection = self::getContainer()->get(Connection::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return iterable<string[]>
|
|
||||||
*/
|
|
||||||
public static function provideSearches(): iterable
|
|
||||||
{
|
|
||||||
yield [''];
|
|
||||||
yield [' '];
|
|
||||||
yield ['hugo'];
|
|
||||||
yield [' hugo'];
|
|
||||||
yield ['hugo '];
|
|
||||||
yield ['rue victor hugo'];
|
|
||||||
yield ['rue victor hugo'];
|
|
||||||
}
|
|
||||||
|
|
||||||
#[DataProvider('provideSearches')]
|
|
||||||
public function testFindPostalCodeDoesNotErrorAndIsIterable(string $search): void
|
|
||||||
{
|
|
||||||
$repository = new PostalCodeForAddressReferenceRepository($this->connection);
|
|
||||||
|
|
||||||
$result = $repository->findPostalCode($search);
|
|
||||||
|
|
||||||
self::assertIsIterable($result);
|
|
||||||
|
|
||||||
// Ensure it can be converted to an array (and iterate without error)
|
|
||||||
$rows = \is_array($result) ? $result : iterator_to_array($result, false);
|
|
||||||
self::assertIsArray($rows);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -595,44 +595,6 @@ paths:
|
|||||||
401:
|
401:
|
||||||
description: "Unauthorized"
|
description: "Unauthorized"
|
||||||
|
|
||||||
/1.0/main/address-reference/aggregated/search:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- address
|
|
||||||
summary: Search for address reference aggregated
|
|
||||||
parameters:
|
|
||||||
- name: q
|
|
||||||
in: query
|
|
||||||
required: true
|
|
||||||
description: The search pattern
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "ok"
|
|
||||||
401:
|
|
||||||
description: "Unauthorized"
|
|
||||||
400:
|
|
||||||
description: "Bad Request"
|
|
||||||
/1.0/main/address-reference/postal-code/search:
|
|
||||||
get:
|
|
||||||
tags:
|
|
||||||
- address
|
|
||||||
summary: Search for postal code that can contains the search query
|
|
||||||
parameters:
|
|
||||||
- name: q
|
|
||||||
in: query
|
|
||||||
required: true
|
|
||||||
description: The search pattern
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: "ok"
|
|
||||||
401:
|
|
||||||
description: "Unauthorized"
|
|
||||||
400:
|
|
||||||
description: "Bad Request"
|
|
||||||
/1.0/main/postal-code/search.json:
|
/1.0/main/postal-code/search.json:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@@ -120,11 +120,6 @@ module.exports = function (encore, entries) {
|
|||||||
"vue_onthefly",
|
"vue_onthefly",
|
||||||
__dirname + "/Resources/public/vuejs/OnTheFly/index.js",
|
__dirname + "/Resources/public/vuejs/OnTheFly/index.js",
|
||||||
);
|
);
|
||||||
encore.addEntry(
|
|
||||||
'address_picker',
|
|
||||||
__dirname + "/Resources/public/vuejs/AddressPicker/index.ts",
|
|
||||||
)
|
|
||||||
|
|
||||||
encore.addEntry(
|
encore.addEntry(
|
||||||
"page_workflow_waiting_post_process",
|
"page_workflow_waiting_post_process",
|
||||||
__dirname + "/Resources/public/vuejs/WaitPostProcessWorkflow/index.ts"
|
__dirname + "/Resources/public/vuejs/WaitPostProcessWorkflow/index.ts"
|
||||||
|
@@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\Migrations\Main;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20250214154310 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Create view for searching address reference';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql(<<<'SQL'
|
|
||||||
create materialized view public.view_chill_main_address_reference as
|
|
||||||
SELECT row_number() OVER () AS row_number,
|
|
||||||
cmar.street AS street,
|
|
||||||
cmar.streetnumber AS streetnumber,
|
|
||||||
cmar.id AS address_id,
|
|
||||||
lower(unaccent(
|
|
||||||
(((((cmar.street || ' '::text) || cmar.streetnumber) || ' '::text) || cmpc.code::text) || ' '::text) ||
|
|
||||||
cmpc.label::text)) AS address,
|
|
||||||
cmpc.id AS postcode_id
|
|
||||||
FROM chill_main_address_reference cmar
|
|
||||||
JOIN chill_main_postal_code cmpc ON cmar.postcode_id = cmpc.id
|
|
||||||
WHERE cmar.deletedat IS NULL
|
|
||||||
ORDER BY ((cmpc.code::text || ' '::text) || cmpc.label::text), cmar.street, (lpad(cmar.streetnumber, 10, '0'::text));
|
|
||||||
SQL);
|
|
||||||
|
|
||||||
$this->addSql(<<<'SQL'
|
|
||||||
create index if not exists view_chill_internal_address_reference_trgm
|
|
||||||
on view_chill_main_address_reference using gist (postcode_id, address public.gist_trgm_ops);
|
|
||||||
SQL);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('DROP MATERIALIZED VIEW view_chill_main_address_reference');
|
|
||||||
}
|
|
||||||
}
|
|
@@ -181,11 +181,6 @@ address more:
|
|||||||
buildingName: résidence
|
buildingName: résidence
|
||||||
extra: ""
|
extra: ""
|
||||||
distribution: cedex
|
distribution: cedex
|
||||||
|
|
||||||
address_picker:
|
|
||||||
# placeholder
|
|
||||||
Search for addresses: Chercher des adresses
|
|
||||||
|
|
||||||
Create a new address: Créer une nouvelle adresse
|
Create a new address: Créer une nouvelle adresse
|
||||||
Create an address: Créer une adresse
|
Create an address: Créer une adresse
|
||||||
Update address: Modifier l'adresse
|
Update address: Modifier l'adresse
|
||||||
|
Reference in New Issue
Block a user