Feature: Allow to filter periods to reassign by postal code

This commit is contained in:
2022-10-06 20:46:57 +02:00
parent 49731777b4
commit f82bc02f8b
18 changed files with 553 additions and 19 deletions

View File

@@ -14,7 +14,7 @@ namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Repository\PostalCodeRepository;
use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -30,11 +30,11 @@ final class PostalCodeAPIController extends ApiController
private PaginatorFactory $paginatorFactory;
private PostalCodeRepository $postalCodeRepository;
private PostalCodeRepositoryInterface $postalCodeRepository;
public function __construct(
CountryRepository $countryRepository,
PostalCodeRepository $postalCodeRepository,
PostalCodeRepositoryInterface $postalCodeRepository,
PaginatorFactory $paginatorFactory
) {
$this->countryRepository = $countryRepository;

View File

@@ -0,0 +1,55 @@
<?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\Form\Type\DataTransformer;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use function gettype;
use function is_int;
class PostalCodeToIdTransformer implements DataTransformerInterface
{
private PostalCodeRepositoryInterface $postalCodeRepository;
public function __construct(PostalCodeRepositoryInterface $postalCodeRepository)
{
$this->postalCodeRepository = $postalCodeRepository;
}
public function reverseTransform($value)
{
if (null === $value || trim('') === $value) {
return null;
}
if (!is_int((int) $value)) {
throw new TransformationFailedException('Cannot transform ' . gettype($value));
}
return $this->postalCodeRepository->find((int) $value);
}
public function transform($value)
{
if (null === $value) {
return null;
}
if ($value instanceof PostalCode) {
return $value->getId();
}
throw new TransformationFailedException('Could not reverseTransform ' . gettype($value));
}
}

View File

@@ -0,0 +1,49 @@
<?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\Form\Type;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Form\Type\DataTransformer\PostalCodeToIdTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PickPostalCodeType extends AbstractType
{
private PostalCodeToIdTransformer $postalCodeToIdTransformer;
public function __construct(PostalCodeToIdTransformer $postalCodeToIdTransformer)
{
$this->postalCodeToIdTransformer = $postalCodeToIdTransformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer($this->postalCodeToIdTransformer);
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['uniqid'] = $view->vars['attr']['data-input-postal-code'] = uniqid('input_pick_postal_code_');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('class', PostalCode::class)
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false);
}
}

View File

@@ -18,10 +18,9 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\ObjectRepository;
use RuntimeException;
final class PostalCodeRepository implements ObjectRepository
final class PostalCodeRepository implements PostalCodeRepositoryInterface
{
private EntityManagerInterface $entityManager;
@@ -29,7 +28,7 @@ final class PostalCodeRepository implements ObjectRepository
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(PostalCode::class);
$this->repository = $entityManager->getRepository($this->getClassName());
$this->entityManager = $entityManager;
}
@@ -51,20 +50,11 @@ final class PostalCodeRepository implements ObjectRepository
return $this->repository->find($id, $lockMode, $lockVersion);
}
/**
* @return PostalCode[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return PostalCode[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
@@ -95,7 +85,7 @@ final class PostalCodeRepository implements ObjectRepository
return $this->repository->findOneBy($criteria, $orderBy);
}
public function getClassName()
public function getClassName(): string
{
return PostalCode::class;
}

View File

@@ -0,0 +1,42 @@
<?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\Country;
use Chill\MainBundle\Entity\PostalCode;
use Doctrine\Persistence\ObjectRepository;
interface PostalCodeRepositoryInterface extends ObjectRepository
{
public function countByPattern(string $pattern, ?Country $country): int;
public function find($id, $lockMode = null, $lockVersion = null): ?PostalCode;
/**
* @return PostalCode[]
*/
public function findAll(): array;
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return PostalCode[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array;
public function findByPattern(string $pattern, ?Country $country, ?int $start = 0, ?int $limit = 50): array;
public function findOneBy(array $criteria, ?array $orderBy = null): ?PostalCode;
public function getClassName(): string;
}

View File

@@ -0,0 +1,66 @@
import { createApp } from 'vue';
import PickPostalCode from 'ChillMainAssets/vuejs/PickPostalCode/PickPostalCode';
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n';
import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
const i18n = _createI18n(appMessages);
function loadOnePicker(el, input, uniqId, city) {
const app = createApp({
template: '<pick-postal-code @select-city="onCitySelected" @removeCity="onCityRemoved" :picked="city"></pick-postal-code>',
components: {
PickPostalCode,
},
data() {
return {
city: city,
}
},
methods: {
onCitySelected(city) {
this.city = city;
input.value = city.id;
},
onCityRemoved(city) {
this.city = null;
input.value = '';
}
}
})
.use(i18n)
.mount(el);
}
function loadDynamicPickers(element) {
let apps = element.querySelectorAll('[data-module="pick-postal-code"]');
apps.forEach(function(el) {
console.log('el', el);
const
uniqId = el.dataset.uniqid,
input = document.querySelector(`input[data-input-uniqid="${uniqId}"]`),
cityIdValue = input.value === '' ? null : input.value
;
console.log('uniqid', uniqId);
console.log('input', input);
console.log('input value', input.value);
console.log('cityIdValue', cityIdValue);
if (cityIdValue !== null) {
makeFetch('GET', `/api/1.0/main/postal-code/${cityIdValue}.json`).then(city => {
loadOnePicker(el, input, uniqId, city);
})
} else {
loadOnePicker(el, input, uniqId, null);
}
});
}
document.addEventListener('DOMContentLoaded', function(e) {
loadDynamicPickers(document)
})

View File

@@ -0,0 +1,4 @@
# Pickpostalcode
## Usage
<PickPostalCode />

View File

@@ -0,0 +1,108 @@
<template>
<div class="PickPostalCode">
<vue-multiselect
id="citySelector"
@search-change="listenInputSearch"
ref="citySelector"
v-model="internalPicked"
@select="selectCity"
@remove="remove"
name=""
track-by="id"
label="value"
:custom-label="transName"
:placeholder="$t('select_city')"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
:taggable="true"
:multiple="false"
:internal-search="false"
:loading="isLoading"
:options="cities"></vue-multiselect>
</div>
</template>
<script lang="js">
import VueMultiselect from "vue-multiselect";
import {reactive, defineProps, onMounted} from "vue";
import {fetchCities, searchCities} from "./api";
export default {
components: {
VueMultiselect,
},
data() {
return {
cities: [],
internalPicked: null,
isLoading: false,
abortControllers: [],
}
},
emits: ['pickCity', 'removeCity'],
props: {
picked: {
type: Object,
required: false,
default: null
},
country: {
type: Object,
required: false,
default: null
}
},
mounted() {
if (this.picked !== null) {
this.internalPicked = this.picked;
this.cities.push(this.picked);
}
},
methods: {
transName(value) {
return (value.code && value.name) ? `${value.name} (${value.code})` : '';
},
selectCity(city) {
console.log('city', city);
this.$emit('selectCity', city);
},
listenInputSearch(query) {
if (query.length <= 2) {
return;
}
let c = this.abortControllers.pop();
while (typeof c !== 'undefined') {
c.abort();
c = this.abortControllers.pop();
}
this.isLoading = true;
let controller = new AbortController();
this.abortControllers.push(controller);
searchCities(query, this.country, controller).then(
newCities => {
this.cities = this.cities.filter(city => city.id === this.picked);
newCities.forEach(item => {
this.cities.push(item);
})
this.isLoading = false;
return Promise.resolve();
})
.catch((error) => {
console.log(error); //TODO better error handling
this.isLoading = false;
});
},
remove(item) {
this.$emit('removeCity', item);
}
},
}
</script>

View File

@@ -0,0 +1,3 @@
.PickPostalCode {
}

View File

@@ -0,0 +1,43 @@
import {makeFetch, fetchResults} from 'ChillMainAssets/lib/api/apiMethods';
/**
* Endpoint chill_api_single_postal_code__index
* method GET, get Cities Object
* @params {object} a country object
* @returns {Promise} a promise containing all Postal Code objects filtered with country
*/
const fetchCities = (country) => {
// warning: do not use fetchResults (in apiMethods): we need only a **part** of the results in the db
const params = new URLSearchParams({item_per_page: 100});
if (country !== null) {
params.append('country', country.id);
}
return makeFetch('GET', `/api/1.0/main/postal-code.json?${params.toString()}`).then(r => Promise.resolve(r.results));
};
/**
* Endpoint chill_main_postalcodeapi_search
* method GET, get Cities Object
* @params {string} search a search string
* @params {object} country a country object
* @params {AbortController} an abort controller
* @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string
*/
const searchCities = (search, country, controller) => {
const url = '/api/1.0/main/postal-code/search.json?';
const params = new URLSearchParams({q: search});
if (country !== null) {
Object.assign('country', country.id);
}
return makeFetch('GET', url + params, null, {signal: controller.signal})
.then(result => Promise.resolve(result.results));
};
export {
fetchCities,
searchCities,
};

View File

@@ -238,3 +238,9 @@
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %}
{% block pick_postal_code_widget %}
{{ form_help(form)}}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-postal-code" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %}

View File

@@ -0,0 +1,67 @@
<?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 Form\Type;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Form\Type\DataTransformer\PostalCodeToIdTransformer;
use Chill\MainBundle\Form\Type\PickPostalCodeType;
use Chill\MainBundle\Repository\PostalCodeRepositoryInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use ReflectionClass;
use Symfony\Component\Form\PreloadedExtension;
use Symfony\Component\Form\Test\TypeTestCase;
/**
* @internal
* @coversNothing
*/
final class PickPostalCodeTypeTest extends TypeTestCase
{
use ProphecyTrait;
public function testSubmitValidData(): void
{
$form = $this->factory->create(PickPostalCodeType::class, null);
$form->submit(['1']);
$this->assertTrue($form->isSynchronized());
$this->assertEquals(1, $form->getData()->getId());
}
protected function getExtensions()
{
$postalCodeRepository = $this->prophesize(PostalCodeRepositoryInterface::class);
$postalCodeRepository->find(Argument::type('string'))
->will(static function ($args) {
$postalCode = new PostalCode();
$reflectionClass = new ReflectionClass($postalCode);
$id = $reflectionClass->getProperty('id');
$id->setAccessible(true);
$id->setValue($postalCode, (int) $args[0]);
return $postalCode;
});
$type = new PickPostalCodeType(
new PostalCodeToIdTransformer(
$postalCodeRepository->reveal()
)
);
return [
new PreloadedExtension([$type], []),
];
}
}

View File

@@ -70,6 +70,7 @@ module.exports = function(encore, entries)
encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js');
encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js');
encore.addEntry('mod_wopi_link', __dirname + '/Resources/public/module/wopi-link/index.js');
encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js');
// Vue entrypoints
encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js');