mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-15 03:04:58 +00:00
Compare commits
14 Commits
move-docum
...
#361-impro
Author | SHA1 | Date | |
---|---|---|---|
e55a7f2993
|
|||
8cade9ac77
|
|||
eb25f1408e
|
|||
279901ac93
|
|||
b1a7680b5e
|
|||
b1703a4187
|
|||
b16e6c4517
|
|||
d4eddaf671
|
|||
a126f2f06d
|
|||
845aa040cc
|
|||
5a284fe6cf
|
|||
a2b8e0e6ae
|
|||
189a26e0e8
|
|||
2e32b8946b
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: Allow the merge of two accompanying period works
|
||||
time: 2025-02-11T14:22:43.134106669+01:00
|
||||
custom:
|
||||
Issue: "359"
|
||||
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: Duplication of a document to another accompanying period work evaluation
|
||||
time: 2025-04-03T10:03:11.796736107+02:00
|
||||
custom:
|
||||
Issue: "369"
|
||||
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: Fusion of two accompanying period works
|
||||
time: 2025-04-03T10:08:57.25079018+02:00
|
||||
custom:
|
||||
Issue: "359"
|
||||
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: Add filter to social actions list to filter out actions where current user intervenes
|
||||
time: 2025-07-17T11:08:50.128269232+02:00
|
||||
custom:
|
||||
Issue: "400"
|
||||
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
||||
kind: Feature
|
||||
body: Show filters on list pages unfolded by default
|
||||
time: 2025-07-22T15:50:39.338057044+02:00
|
||||
custom:
|
||||
Issue: "399"
|
||||
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: adjust display logic for accompanying period dates, include closing date if period is closed.
|
||||
time: 2025-08-06T13:46:09.241584292+02:00
|
||||
custom:
|
||||
Issue: "382"
|
||||
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
||||
kind: Fixed
|
||||
body: add min and step attributes to integer field in DateIntervalType
|
||||
time: 2025-08-06T17:35:27.413787704+02:00
|
||||
custom:
|
||||
Issue: "384"
|
||||
SchemaChange: No schema change
|
@@ -1,6 +0,0 @@
|
||||
kind: UX
|
||||
body: Limit display of participations in event list
|
||||
time: 2025-07-22T13:26:37.500656935+02:00
|
||||
custom:
|
||||
Issue: ""
|
||||
SchemaChange: No schema change
|
@@ -17,3 +17,9 @@ when@dev:
|
||||
defaults:
|
||||
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,6 +45,7 @@
|
||||
"webpack-cli": "^5.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fragaria/address-formatter": "^6.6.1",
|
||||
"@fullcalendar/core": "^6.1.4",
|
||||
"@fullcalendar/daygrid": "^6.1.4",
|
||||
"@fullcalendar/interaction": "^6.1.4",
|
||||
@@ -69,6 +70,7 @@
|
||||
"vue-i18n": "^9.1.6",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-toast-notification": "^3.1.2",
|
||||
"vue-use-leaflet": "^0.1.7",
|
||||
"vuex": "^4.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
|
@@ -4,7 +4,6 @@ import { StoredObject, StoredObjectVersion } from "../../types";
|
||||
import DropFileWidget from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileWidget.vue";
|
||||
import { computed, reactive } from "vue";
|
||||
import { useToast } from "vue-toast-notification";
|
||||
import { DOCUMENT_REPLACE, DOCUMENT_ADD, trans } from "translator";
|
||||
|
||||
interface DropFileConfig {
|
||||
allowRemove: boolean;
|
||||
@@ -76,10 +75,10 @@ function closeModal(): void {
|
||||
@click="openModal"
|
||||
class="btn btn-create"
|
||||
>
|
||||
{{ trans(DOCUMENT_ADD) }}
|
||||
Ajouter un document
|
||||
</button>
|
||||
<button v-else @click="openModal" class="dropdown-item">
|
||||
{{ trans(DOCUMENT_REPLACE) }}
|
||||
<button v-else @click="openModal" class="btn btn-edit">
|
||||
Remplacer le document
|
||||
</button>
|
||||
<modal
|
||||
v-if="state.showModal"
|
||||
|
@@ -23,8 +23,6 @@ See the document: Voir le document
|
||||
|
||||
document:
|
||||
Any title: Aucun titre
|
||||
replace: Remplacer
|
||||
Add: Ajouter un document
|
||||
|
||||
generic_doc:
|
||||
filter:
|
||||
|
@@ -54,14 +54,14 @@ block js %}
|
||||
{% if e.participations|length > 0 %}
|
||||
<div class="item-row separator">
|
||||
<strong>{{ "Participations" | trans }} : </strong>
|
||||
{% for part in e.participations|slice(0, 5) %} {% include
|
||||
{% for part in e.participations|slice(0, 20) %} {% include
|
||||
'@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
|
||||
targetEntity: { name: 'person', id: part.person.id }, action:
|
||||
'show', displayBadge: true, buttonText:
|
||||
part.person|chill_entity_render_string, isDead:
|
||||
part.person.deathdate is not null } %} {% endfor %}
|
||||
{% if e.participations|length > 5 %}
|
||||
{{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 5}) }}
|
||||
part.person.deathdate is not null } %} {% endfor %} {% if
|
||||
e.participations|length > 20 %}
|
||||
{{ 'events.and_other_count_participants'|trans({'count': e.participations|length - 20}) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@@ -0,0 +1,50 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
@@ -55,10 +55,6 @@ class DateIntervalType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('n', IntegerType::class, [
|
||||
'attr' => [
|
||||
'min' => 0,
|
||||
'step' => 1,
|
||||
],
|
||||
'constraints' => [
|
||||
new GreaterThan([
|
||||
'value' => 0,
|
||||
|
@@ -14,13 +14,14 @@ namespace Chill\MainBundle\Repository;
|
||||
use Chill\MainBundle\Entity\AddressReference;
|
||||
use Chill\MainBundle\Entity\PostalCode;
|
||||
use Chill\MainBundle\Search\SearchApiQuery;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\NativeQuery;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
final readonly class AddressReferenceRepository implements ObjectRepository
|
||||
final readonly class AddressReferenceRepository implements AddressReferenceRepositoryInterface
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
@@ -65,6 +66,121 @@ final readonly class AddressReferenceRepository implements ObjectRepository
|
||||
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 $offset
|
||||
|
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
<?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
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
<?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;
|
||||
}
|
@@ -74,6 +74,7 @@ export interface Postcode {
|
||||
name: string;
|
||||
code: string;
|
||||
center: Point;
|
||||
country: Country;
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
@@ -89,6 +90,28 @@ export interface Country {
|
||||
|
||||
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 {
|
||||
type: "address";
|
||||
address_id: number;
|
||||
@@ -107,7 +130,7 @@ export interface Address {
|
||||
confidential: boolean;
|
||||
lines: string[];
|
||||
addressReference: AddressReference | null;
|
||||
validFrom: DateTime;
|
||||
validFrom: DateTime | null; // TODO there is no null for validFrom
|
||||
validTo: DateTime | null;
|
||||
point: Point | null;
|
||||
refStatus: AddressRefStatus;
|
||||
|
@@ -0,0 +1,33 @@
|
||||
<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>
|
@@ -0,0 +1,142 @@
|
||||
<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";
|
||||
|
||||
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 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;
|
||||
}
|
||||
};
|
||||
</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"></address-details-form>
|
||||
</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>
|
@@ -0,0 +1,25 @@
|
||||
<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>
|
@@ -0,0 +1,82 @@
|
||||
<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>
|
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import {AddressReference} from "ChillMainAssets/types";
|
||||
import {computed} from "vue";
|
||||
import {addressReferenceToAddress} from "ChillMainAssets/vuejs/AddressPicker/helper";
|
||||
import AddressDetailsContent from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsContent.vue";
|
||||
|
||||
export interface AddressDetailsFormProps {
|
||||
address: AddressReference;
|
||||
}
|
||||
|
||||
const props = defineProps<AddressDetailsFormProps>();
|
||||
|
||||
const address = computed(() => addressReferenceToAddress(props.address));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
FORM
|
||||
</div>
|
||||
<div>
|
||||
<address-details-content :address="address"></address-details-content>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@@ -0,0 +1,39 @@
|
||||
<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>
|
@@ -0,0 +1,69 @@
|
||||
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);
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
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,
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
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,24 +4,27 @@
|
||||
:show-button-details="false"
|
||||
></address-render-box>
|
||||
<address-details-ref-matching
|
||||
v-if="isAddress(props.address)"
|
||||
:address="props.address"
|
||||
@update-address="onUpdateAddress"
|
||||
></address-details-ref-matching>
|
||||
<address-details-map :address="props.address"></address-details-map>
|
||||
<address-details-geographical-layers
|
||||
v-if="isAddress(props.address)"
|
||||
:address="props.address"
|
||||
></address-details-geographical-layers>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Address } from "../../../types";
|
||||
import {Address, AddressCreation} from "../../../types";
|
||||
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
|
||||
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
|
||||
import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue";
|
||||
import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue";
|
||||
import {isAddress} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
||||
|
||||
interface AddressModalContentProps {
|
||||
address: Address;
|
||||
address: Address|AddressCreation;
|
||||
}
|
||||
|
||||
const props = defineProps<AddressModalContentProps>();
|
||||
|
@@ -12,90 +12,91 @@
|
||||
Voir sur
|
||||
<a :href="makeUrlGoogleMap(props.address)" target="_blank"
|
||||
>Google Maps</a
|
||||
>
|
||||
<a :href="makeUrlOsm(props.address)" target="_blank">OSM</a>
|
||||
> <a
|
||||
:href="makeUrlOsm(props.address)" target="_blank"
|
||||
>OSM</a>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import markerIconPng from "leaflet/dist/images/marker-icon.png";
|
||||
import L, { LatLngExpression, LatLngTuple } from "leaflet";
|
||||
import { Address, Point } from "../../../../types";
|
||||
import {Address, AddressCreation, 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 => {
|
||||
return [point.coordinates[1], point.coordinates[0]];
|
||||
};
|
||||
|
||||
export interface MapProps {
|
||||
address: Address;
|
||||
address: Address|AddressCreation;
|
||||
}
|
||||
|
||||
const props = defineProps<MapProps>();
|
||||
|
||||
const map_div = ref<HTMLDivElement | null>(null);
|
||||
let map: L.Map | null = null;
|
||||
let marker: L.Marker | null = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (map_div.value === null) {
|
||||
// there is no map div when the address does not have any Point
|
||||
return;
|
||||
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];
|
||||
}
|
||||
|
||||
if (props.address.point !== null) {
|
||||
map = L.map(map_div.value);
|
||||
map.setView(lonLatForLeaflet(props.address.point), 18);
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
}).addTo(map);
|
||||
|
||||
const markerIcon = L.icon({
|
||||
iconUrl: markerIconPng,
|
||||
iconAnchor: [12, 41],
|
||||
});
|
||||
|
||||
marker = L.marker(lonLatForLeaflet(props.address.point), {
|
||||
icon: markerIcon,
|
||||
});
|
||||
marker.addTo(map);
|
||||
}
|
||||
return [props.address.point.coordinates[1], props.address.point.coordinates[0], 0]
|
||||
});
|
||||
|
||||
const makeUrlGoogleMap = (address: Address): string => {
|
||||
const map_div = ref<HTMLDivElement | null>(null);
|
||||
const map = useLeafletMap(map_div, {zoom: 18, center: latLngMarker});
|
||||
const tileLayer = useLeafletTileLayer(
|
||||
"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
}
|
||||
);
|
||||
useLeafletDisplayLayer(map, tileLayer);
|
||||
|
||||
|
||||
const marker = useLeafletMarker(latLngMarker, {icon: markerIcon});
|
||||
useLeafletDisplayLayer(map, marker);
|
||||
|
||||
|
||||
const makeUrlGoogleMap = (address: Address|AddressCreation): string => {
|
||||
const params = new URLSearchParams();
|
||||
params.append("api", "1");
|
||||
if (address.point !== null && address.addressReference !== null) {
|
||||
const point = getAddressPoint(address);
|
||||
if (point !== null && address.addressReference !== null) {
|
||||
params.append(
|
||||
"query",
|
||||
`${address.point.coordinates[1]} ${address.point.coordinates[0]}`,
|
||||
`${point.coordinates[1]} ${point.coordinates[0]}`,
|
||||
);
|
||||
} else {
|
||||
params.append("query", address.lines.join(", "));
|
||||
params.append("query", buildAddressLines(address).join(", "));
|
||||
}
|
||||
|
||||
return `https://www.google.com/maps/search/?${params.toString()}`;
|
||||
};
|
||||
|
||||
const makeUrlOsm = (address: Address): string => {
|
||||
if (address.point !== null && address.addressReference !== null) {
|
||||
const makeUrlOsm = (address: Address|AddressCreation): string => {
|
||||
const point = getAddressPoint(address);
|
||||
if (point !== null && address.addressReference !== null) {
|
||||
const params = new URLSearchParams();
|
||||
params.append("mlat", `${address.point.coordinates[1]}`);
|
||||
params.append("mlon", `${address.point.coordinates[0]}`);
|
||||
params.append("mlat", `${point.coordinates[1]}`);
|
||||
params.append("mlon", `${point.coordinates[0]}`);
|
||||
const hashParams = new URLSearchParams();
|
||||
hashParams.append(
|
||||
"map",
|
||||
`18/${address.point.coordinates[1]}/${address.point.coordinates[0]}`,
|
||||
`18/${point.coordinates[1]}/${point.coordinates[0]}`,
|
||||
);
|
||||
|
||||
return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append("query", address.lines.join(", "));
|
||||
params.append("query", buildAddressLines(address).join(", "));
|
||||
|
||||
return `https://www.openstreetmap.org/search?${params.toString()}`;
|
||||
};
|
||||
|
@@ -0,0 +1,46 @@
|
||||
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;
|
||||
}
|
@@ -64,5 +64,3 @@ const props = defineProps({
|
||||
entity: Object,
|
||||
});
|
||||
</script>
|
||||
|
||||
thirdparty_duplicate: merge: Fussioner find: 'Désigner un tiers doublon'
|
||||
|
@@ -4,14 +4,14 @@
|
||||
<div v-if="isConfidential">
|
||||
<confidential :position-btn-far="true">
|
||||
<template #confidential-content>
|
||||
<div v-if="isMultiline === true">
|
||||
<div v-if="isMultiline">
|
||||
<p
|
||||
v-for="(l, i) in address.lines"
|
||||
v-for="(l, i) in buildAddressLines(address)"
|
||||
:key="`line-${i}`"
|
||||
>
|
||||
{{ l }}
|
||||
</p>
|
||||
<p v-if="showButtonDetails">
|
||||
<p v-if="showButtonDetails && isAddress(address) ">
|
||||
<address-details-button
|
||||
:address_id="address.address_id"
|
||||
:address_ref_status="address.refStatus"
|
||||
@@ -19,8 +19,8 @@
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p v-if="'' !== address.text" class="street">
|
||||
{{ address.text }}
|
||||
<p v-if="'' !== buildAddressText(address)" class="street">
|
||||
{{ buildAddressText(address) }}
|
||||
</p>
|
||||
<p
|
||||
v-if="null !== address.postcode"
|
||||
@@ -29,8 +29,8 @@
|
||||
{{ address.postcode.code }}
|
||||
{{ address.postcode.name }}
|
||||
</p>
|
||||
<p v-if="null !== address.country" class="country">
|
||||
{{ localizeString(address.country.name) }}
|
||||
<p v-if="null !== address.postcode" class="country">
|
||||
{{ localizeString(address.postcode.country.name) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -38,11 +38,11 @@
|
||||
</div>
|
||||
|
||||
<div v-if="!isConfidential">
|
||||
<div v-if="isMultiline === true">
|
||||
<p v-for="(l, i) in address.lines" :key="`line-${i}`">
|
||||
<div v-if="isMultiline">
|
||||
<p v-for="(l, i) in buildAddressLines(address)" :key="`line-${i}`">
|
||||
{{ l }}
|
||||
</p>
|
||||
<p v-if="showButtonDetails">
|
||||
<p v-if="showButtonDetails && isAddress(address) ">
|
||||
<address-details-button
|
||||
:address_id="address.address_id"
|
||||
:address_ref_status="address.refStatus"
|
||||
@@ -50,9 +50,9 @@
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p v-if="address.text" class="street">
|
||||
{{ address.text }}
|
||||
<template v-if="showButtonDetails">
|
||||
<p v-if="'' !== buildAddressText(address)" class="street">
|
||||
{{ buildAddressText(address)}}
|
||||
<template v-if="showButtonDetails && isAddress(address) ">
|
||||
<address-details-button
|
||||
:address_id="address.address_id"
|
||||
:address_ref_status="address.refStatus"
|
||||
@@ -63,68 +63,49 @@
|
||||
</div>
|
||||
</component>
|
||||
|
||||
<div v-if="useDatePane === true" class="address-more">
|
||||
<div v-if="useDatePane" class="address-more">
|
||||
<div v-if="address.validFrom">
|
||||
<span class="validFrom">
|
||||
<b>{{ trans(ADDRESS_VALID_FROM) }}</b
|
||||
>: {{ $d(address.validFrom.date) }}
|
||||
>: {{ address.validFrom?.datetime8601 }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="address.validTo">
|
||||
<div v-if="isAddress(address) && address.validTo !== null">
|
||||
<span class="validTo">
|
||||
<b>{{ trans(ADDRESS_VALID_TO) }}</b
|
||||
>: {{ $d(address.validTo.date) }}
|
||||
>: {{ address.validTo?.datetime8601 }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import Confidential from "ChillMainAssets/vuejs/_components/Confidential.vue";
|
||||
import AddressDetailsButton from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsButton.vue";
|
||||
import { trans, ADDRESS_VALID_FROM, ADDRESS_VALID_TO } from "translator";
|
||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||
import {Address, AddressCreation} from "ChillMainAssets/types";
|
||||
import {isAddress, buildAddressLines, buildAddressText} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
|
||||
|
||||
export default {
|
||||
name: "AddressRenderBox",
|
||||
methods: { localizeString },
|
||||
components: {
|
||||
Confidential,
|
||||
AddressDetailsButton,
|
||||
},
|
||||
props: {
|
||||
address: {
|
||||
type: Object,
|
||||
},
|
||||
isMultiline: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
useDatePane: {
|
||||
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;
|
||||
},
|
||||
},
|
||||
};
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
address: Address|AddressCreation;
|
||||
isMultiline?: boolean;
|
||||
useDatePane?: boolean;
|
||||
showButtonDetails?: boolean;
|
||||
}>(),
|
||||
{
|
||||
isMultiline: true,
|
||||
useDatePane: false,
|
||||
showButtonDetails: true,
|
||||
}
|
||||
);
|
||||
|
||||
const component = computed(() => (props.isMultiline ? "div" : "span"));
|
||||
const multiline = computed(() => (props.isMultiline ? "multiline" : ""));
|
||||
const isConfidential = computed(() => Boolean(props.address?.confidential));
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@@ -4,7 +4,7 @@
|
||||
{% endblock crud_content_header %}
|
||||
|
||||
{% block crud_content_view %}
|
||||
|
||||
|
||||
{% block crud_content_view_details %}
|
||||
<dl class="chill_view_data">
|
||||
<dt>id</dt>
|
||||
@@ -20,7 +20,7 @@
|
||||
{{ 'Cancel'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% block content_view_actions_before %}{% endblock %}
|
||||
{% block content_form_actions_delete %}
|
||||
{% if chill_crud_action_exists(crud_name, 'delete') %}
|
||||
@@ -32,7 +32,7 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock content_form_actions_delete %}
|
||||
{% endblock content_form_actions_delete %}
|
||||
{% block content_view_actions_duplicate_link %}
|
||||
{% if chill_crud_action_exists(crud_name, 'new') %}
|
||||
{% if is_granted(chill_crud_config('role', crud_name, 'new'), entity) %}
|
||||
@@ -44,17 +44,6 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock content_view_actions_duplicate_link %}
|
||||
{% block content_view_actions_merge %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_thirdparty_find_duplicate',
|
||||
{ 'thirdparty_id': entity.id }) }}"
|
||||
title="{{ 'Merge'|trans }}"
|
||||
class="btn btn-misc">
|
||||
<i class="bi bi-chevron-contract"></i>
|
||||
{{ 'Merge'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block content_view_actions_edit_link %}
|
||||
{% if chill_crud_action_exists(crud_name, 'edit') %}
|
||||
{% if is_granted(chill_crud_config('role', crud_name, 'edit'), entity) %}
|
||||
|
@@ -0,0 +1,15 @@
|
||||
{% 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 %}
|
@@ -5,7 +5,7 @@
|
||||
<strong><i class="fa fa-fw fa-filter"></i>Filtrer la liste</strong>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="filterOrderCollapse" aria-labelledby="filterOrderHeading" data-bs-parent="#filterOrderAccordion">
|
||||
<div class="accordion-collapse collapse" id="filterOrderCollapse" aria-labelledby="filterOrderHeading" data-bs-parent="#filterOrderAccordion">
|
||||
{% set btnSubmit = 0 %}
|
||||
<div class="accordion-body chill_filter_order container-xxl p-5 py-2">
|
||||
<div class="row my-2">
|
||||
|
@@ -280,17 +280,11 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block pick_linked_entities_widget %}
|
||||
<input type="hidden" {{ block('widget_attributes') }}
|
||||
{% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %}
|
||||
data-input-uniqid="{{ form.vars['uniqid'] }}"/>
|
||||
<div
|
||||
data-input-uniqid="{{ form.vars['uniqid'] }}"
|
||||
data-module="pick-linked-entities"
|
||||
data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
|
||||
data-suggested="{{ form.vars['suggested']|json_encode|escape('html_attr') }}"
|
||||
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value|escape('html_attr') }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}" />
|
||||
<div data-input-uniqid="{{ form.vars['uniqid'] }}" data-module="pick-linked-entities" data-pick-entities-type="{{ form.vars['pick-entities-type'] }}"
|
||||
></div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block pick_postal_code_widget %}
|
||||
|
@@ -66,6 +66,7 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
|
||||
'name' => $address->getPostCode()->getName(),
|
||||
'code' => $address->getPostCode()->getCode(),
|
||||
'center' => $address->getPostcode()->getCenter(),
|
||||
'country' => $this->normalizer->normalize($address->getPostCode()->getCountry(), $format, [AbstractNormalizer::GROUPS => ['read']]),
|
||||
],
|
||||
'country' => [
|
||||
'id' => $address->getPostCode()->getCountry()->getId(),
|
||||
|
@@ -0,0 +1,118 @@
|
||||
<?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']);
|
||||
}
|
||||
}
|
@@ -0,0 +1,88 @@
|
||||
<?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'];
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
<?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,6 +595,44 @@ paths:
|
||||
401:
|
||||
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:
|
||||
get:
|
||||
tags:
|
||||
|
@@ -120,5 +120,9 @@ module.exports = function (encore, entries) {
|
||||
"vue_onthefly",
|
||||
__dirname + "/Resources/public/vuejs/OnTheFly/index.js",
|
||||
);
|
||||
encore.addEntry(
|
||||
'address_picker',
|
||||
__dirname + "/Resources/public/vuejs/AddressPicker/index.ts",
|
||||
)
|
||||
|
||||
};
|
||||
|
@@ -0,0 +1,52 @@
|
||||
<?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,6 +181,11 @@ address more:
|
||||
buildingName: résidence
|
||||
extra: ""
|
||||
distribution: cedex
|
||||
|
||||
address_picker:
|
||||
# placeholder
|
||||
Search for addresses: Chercher des adresses
|
||||
|
||||
Create a new address: Créer une nouvelle adresse
|
||||
Create an address: Créer une adresse
|
||||
Update address: Modifier l'adresse
|
||||
|
@@ -11,7 +11,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||
use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface;
|
||||
@@ -131,7 +130,6 @@ final class AccompanyingCourseWorkController extends AbstractController
|
||||
$this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::SEE, $period);
|
||||
|
||||
$filter = $this->buildFilterOrder($period);
|
||||
$currentUser = $this->getUser();
|
||||
|
||||
$filterData = [
|
||||
'types' => $filter->hasEntityChoice('typesFilter') ? $filter->getEntityChoiceData('typesFilter') : [],
|
||||
@@ -140,10 +138,6 @@ final class AccompanyingCourseWorkController extends AbstractController
|
||||
'user' => $filter->getUserPickerData('userFilter'),
|
||||
];
|
||||
|
||||
if ($filter->getSingleCheckboxData('currentUserFilter') && $currentUser instanceof User) {
|
||||
$filterData['currentUser'] = $currentUser;
|
||||
}
|
||||
|
||||
$totalItems = $this->workRepository->countByAccompanyingPeriod($period);
|
||||
$paginator = $this->paginator->create($totalItems);
|
||||
|
||||
@@ -207,8 +201,6 @@ final class AccompanyingCourseWorkController extends AbstractController
|
||||
->addUserPicker('userFilter', 'accompanying_course_work.user_filter', ['required' => false])
|
||||
;
|
||||
|
||||
$filterBuilder->addSingleCheckbox('currentUserFilter', 'accompanying_course_work.my_actions_filter');
|
||||
|
||||
return $filterBuilder->build();
|
||||
}
|
||||
}
|
||||
|
@@ -18,7 +18,6 @@ use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepos
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriodWork\AccompanyingPeriodWorkMergeService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
@@ -33,7 +32,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
|
||||
* @ParamConverter("accompanyingPeriodWork", options={"id": "acpw_id"})
|
||||
*/
|
||||
#[Route(path: '{_locale}/person/accompanying-period/work/{id}/assign-duplicate', name: 'chill_person_accompanying_period_work_assign_duplicate')]
|
||||
public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request): Response
|
||||
public function assignDuplicate(AccompanyingPeriodWork $acpw, Request $request)
|
||||
{
|
||||
$accompanyingPeriod = $acpw->getAccompanyingPeriod();
|
||||
|
||||
@@ -80,7 +79,7 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
|
||||
* @ParamConverter("acpw2", options={"id": "acpw2_id"})
|
||||
*/
|
||||
#[Route(path: '/{_locale}/person/{acpw1_id}/duplicate/{acpw2_id}/confirm', name: 'chill_person_acpw_duplicate_confirm')]
|
||||
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request): Response
|
||||
public function confirmAction(AccompanyingPeriodWork $acpw1, AccompanyingPeriodWork $acpw2, Request $request)
|
||||
{
|
||||
$accompanyingPeriod = $acpw1->getAccompanyingPeriod();
|
||||
|
||||
@@ -99,7 +98,6 @@ class AccompanyingPeriodWorkDuplicateController extends AbstractController
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('acpw_duplicate.Successfully merged'));
|
||||
}
|
||||
|
||||
|
||||
return $this->redirectToRoute('chill_person_accompanying_period_work_show', ['id' => $acpw1->getId()]);
|
||||
}
|
||||
|
||||
|
@@ -12,7 +12,6 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Routing\ChillUrlGeneratorInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
|
||||
use Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument\AccompanyingPeriodWorkEvaluationDocumentDuplicator;
|
||||
@@ -25,16 +24,15 @@ use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
|
||||
readonly class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
|
||||
class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
|
||||
{
|
||||
public function __construct(
|
||||
private AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
|
||||
private Security $security,
|
||||
private SerializerInterface $serializer,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private ChillUrlGeneratorInterface $urlGenerator,
|
||||
private readonly AccompanyingPeriodWorkEvaluationDocumentDuplicator $duplicator,
|
||||
private readonly Security $security,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly ChillUrlGeneratorInterface $urlGenerator,
|
||||
) {}
|
||||
|
||||
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{id}/duplicate', methods: ['POST'])]
|
||||
@@ -58,32 +56,6 @@ readonly class AccompanyingPeriodWorkEvaluationDocumentDuplicateController
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ParamConverter("document", options={"id": "document_id"})
|
||||
* @ParamConverter("evaluation", options={"id": "evaluation_id"})
|
||||
*/
|
||||
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate', methods: ['POST'])]
|
||||
public function duplicateToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
|
||||
{
|
||||
$work = $evaluation->getAccompanyingPeriodWork();
|
||||
|
||||
if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
|
||||
throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
|
||||
}
|
||||
|
||||
$duplicatedDocument = $this->duplicator->duplicateToEvaluation($document, $evaluation);
|
||||
|
||||
$this->entityManager->persist($duplicatedDocument);
|
||||
$this->entityManager->persist($duplicatedDocument->getStoredObject());
|
||||
$this->entityManager->persist($evaluation);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($duplicatedDocument, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
|
||||
#[Route('/{_locale}/person/accompanying-course-work-evaluation-document/{id}/duplicate', name: 'chill_person_accompanying_period_work_evaluation_document_duplicate', methods: ['POST'])]
|
||||
public function duplicate(AccompanyingPeriodWorkEvaluationDocument $document): Response
|
||||
{
|
||||
|
@@ -1,58 +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\PersonBundle\Controller;
|
||||
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
|
||||
readonly class AccompanyingPeriodWorkEvaluationDocumentMoveController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private SerializerInterface $serializer,
|
||||
private EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @ParamConverter("document", options={"id": "document_id"})
|
||||
* @ParamConverter("evaluation", options={"id": "evaluation_id"})
|
||||
*/
|
||||
#[Route('/api/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/move', methods: ['POST'])]
|
||||
public function moveToEvaluationApi(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): Response
|
||||
{
|
||||
$work = $evaluation->getAccompanyingPeriodWork();
|
||||
|
||||
if (!$this->security->isGranted(AccompanyingPeriodWorkVoter::UPDATE, $work)) {
|
||||
throw new AccessDeniedHttpException('not allowed to edit this accompanying period work');
|
||||
}
|
||||
|
||||
$document->setAccompanyingPeriodWorkEvaluation($evaluation);
|
||||
|
||||
$this->entityManager->persist($document);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($document, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@@ -98,6 +98,16 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
|
||||
|
||||
public function setAccompanyingPeriodWorkEvaluation(?AccompanyingPeriodWorkEvaluation $accompanyingPeriodWorkEvaluation): AccompanyingPeriodWorkEvaluationDocument
|
||||
{
|
||||
// if an evaluation is already associated, we cannot change the association (removing the association,
|
||||
// by setting a null value, is allowed.
|
||||
if (
|
||||
$this->accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
|
||||
&& $accompanyingPeriodWorkEvaluation instanceof AccompanyingPeriodWorkEvaluation
|
||||
) {
|
||||
if ($this->accompanyingPeriodWorkEvaluation !== $accompanyingPeriodWorkEvaluation) {
|
||||
throw new \RuntimeException('It is not allowed to change the evaluation for a document');
|
||||
}
|
||||
}
|
||||
$this->accompanyingPeriodWorkEvaluation = $accompanyingPeriodWorkEvaluation;
|
||||
|
||||
return $this;
|
||||
|
@@ -24,7 +24,7 @@ class FindAccompanyingPeriodWorkType extends AbstractType
|
||||
{
|
||||
$builder
|
||||
->add('acpw', PickLinkedAccompanyingPeriodWorkType::class, [
|
||||
'label' => 'Accompanying period work',
|
||||
'label' => 'Social action',
|
||||
'multiple' => false,
|
||||
'accompanyingPeriod' => $options['accompanyingPeriod'],
|
||||
])
|
||||
|
@@ -16,26 +16,18 @@ use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class PickLinkedAccompanyingPeriodWorkType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly NormalizerInterface $normalizer) {}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['multiple'] = $options['multiple'];
|
||||
$view->vars['types'] = ['acpw'];
|
||||
$view->vars['uniqid'] = uniqid('pick_acpw_dyn');
|
||||
$view->vars['suggested'] = [];
|
||||
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
|
||||
$view->vars['submit_on_adding_new_entity'] = false;
|
||||
$view->vars['pick-entities-type'] = 'acpw';
|
||||
$view->vars['attr']['data-accompanying-period-id'] = $options['accompanyingPeriod']->getId();
|
||||
|
||||
foreach ($options['suggested'] as $suggestion) {
|
||||
$view->vars['suggested'][] = $this->normalizer->normalize($suggestion, 'json', ['groups' => 'read']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
@@ -46,7 +38,6 @@ class PickLinkedAccompanyingPeriodWorkType extends AbstractType
|
||||
->setDefault('multiple', false)
|
||||
->setAllowedTypes('multiple', ['bool'])
|
||||
->setDefault('compound', false)
|
||||
->setDefault('suggested', [])
|
||||
->setDefault('as_id', false)
|
||||
->setAllowedTypes('as_id', ['bool'])
|
||||
->setDefault('submit_on_adding_new_entity', false)
|
||||
|
@@ -90,7 +90,7 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
|
||||
* * first, opened works
|
||||
* * then, closed works
|
||||
*
|
||||
* @param array{types?: list<SocialAction>, user?: list<User>, currentUser?: User, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
|
||||
* @param array{types?: list<SocialAction>, user?: list<User>, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
|
||||
*
|
||||
* @return AccompanyingPeriodWork[]
|
||||
*/
|
||||
@@ -101,7 +101,6 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
|
||||
|
||||
$sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
|
||||
LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id
|
||||
AND (rw.enddate IS NULL OR rw.enddate > CURRENT_DATE)
|
||||
WHERE accompanyingPeriod_id = :periodId";
|
||||
|
||||
// implement filters
|
||||
@@ -120,10 +119,6 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
|
||||
.')';
|
||||
}
|
||||
|
||||
if (isset($filters['currentUser'])) {
|
||||
$sql .= ' AND rw.user_id = :currentUser';
|
||||
}
|
||||
|
||||
$sql .= " AND daterange(:after::date, :before::date) && daterange(w.startDate, w.endDate, '[]')";
|
||||
|
||||
// if the start and end date were inversed, we inverse the order to avoid an error
|
||||
@@ -157,11 +152,6 @@ class AccompanyingPeriodWorkRepository implements ObjectRepository
|
||||
->setParameter('limit', $limit, Types::INTEGER)
|
||||
->setParameter('offset', $offset, Types::INTEGER);
|
||||
|
||||
if (isset($filters['currentUser'])) {
|
||||
$nq->setParameter('currentUser', $filters['currentUser']->getId());
|
||||
}
|
||||
|
||||
|
||||
foreach ($filters['user'] as $key => $user) {
|
||||
$nq->setParameter('user_'.$key, $user);
|
||||
}
|
||||
|
@@ -41,8 +41,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
methods: {
|
||||
pickWork: function (payload: { work: AccompanyingPeriodWork }) {
|
||||
console.log("payload", payload);
|
||||
|
||||
input.value = payload.work.id?.toString() ?? "";
|
||||
input.value = payload.work.id.toString();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@@ -84,7 +84,7 @@ export interface AccompanyingPeriodWorkEvaluationDocument {
|
||||
}
|
||||
|
||||
export interface AccompanyingPeriodWork {
|
||||
id?: number;
|
||||
id: number;
|
||||
accompanyingPeriod?: AccompanyingPeriod;
|
||||
accompanyingPeriodWorkEvaluations: AccompanyingPeriodWorkEvaluation[];
|
||||
createdAt?: string;
|
||||
|
@@ -972,7 +972,7 @@ div#workEditor {
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
& > i.fa {
|
||||
i.fa {
|
||||
padding: 0.25rem;
|
||||
color: $white;
|
||||
|
||||
|
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<label class="col-sm-4 col-form-label visually-hidden">{{
|
||||
trans(EVALUATION_PUBLIC_COMMENT)
|
||||
}}</label>
|
||||
<div class="col-sm-12">
|
||||
<ckeditor
|
||||
:editor="ClassicEditor"
|
||||
:config="classicEditorConfig"
|
||||
:placeholder="trans(EVALUATION_COMMENT_PLACEHOLDER)"
|
||||
:value="comment"
|
||||
@input="$emit('update:comment', $event)"
|
||||
tag-name="textarea"
|
||||
></ckeditor>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Ckeditor } from "@ckeditor/ckeditor5-vue";
|
||||
import { ClassicEditor } from "ckeditor5";
|
||||
import classicEditorConfig from "ChillMainAssets/module/ckeditor5/editor_config";
|
||||
import {
|
||||
EVALUATION_PUBLIC_COMMENT,
|
||||
EVALUATION_COMMENT_PLACEHOLDER,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
defineProps(["comment"]);
|
||||
defineEmits(["update:comment"]);
|
||||
</script>
|
@@ -1,71 +0,0 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_STARTDATE) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
type="date"
|
||||
:value="startDate"
|
||||
@input="$emit('update:startDate', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_ENDDATE) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
type="date"
|
||||
:value="endDate"
|
||||
@input="$emit('update:endDate', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_MAXDATE) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
type="date"
|
||||
:value="maxDate"
|
||||
@input="$emit('update:maxDate', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_WARNING_INTERVAL) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<input
|
||||
class="form-control form-control-sm"
|
||||
type="number"
|
||||
:value="warningInterval"
|
||||
@input="$emit('update:warningInterval', $event.target.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
EVALUATION_STARTDATE,
|
||||
EVALUATION_ENDDATE,
|
||||
EVALUATION_MAXDATE,
|
||||
EVALUATION_WARNING_INTERVAL,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
defineProps(["startDate", "endDate", "maxDate", "warningInterval"]);
|
||||
defineEmits([
|
||||
"update:startDate",
|
||||
"update:endDate",
|
||||
"update:maxDate",
|
||||
"update:warningInterval",
|
||||
]);
|
||||
</script>
|
@@ -1,51 +0,0 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<h6>{{ trans(EVALUATION_DOCUMENT_ADD) }} :</h6>
|
||||
<pick-template
|
||||
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
|
||||
:id="evaluation.id"
|
||||
:templates="templates"
|
||||
:preventDefaultMoveToGenerate="true"
|
||||
@go-to-generate-document="$emit('submitBeforeGenerate', $event)"
|
||||
>
|
||||
<template v-slot:title>
|
||||
<label class="col-form-label">{{
|
||||
trans(EVALUATION_GENERATE_A_DOCUMENT)
|
||||
}}</label>
|
||||
</template>
|
||||
</pick-template>
|
||||
<div>
|
||||
<label class="col-form-label">{{
|
||||
trans(EVALUATION_DOCUMENT_UPLOAD)
|
||||
}}</label>
|
||||
<ul class="record_actions document-upload">
|
||||
<li>
|
||||
<drop-file-modal
|
||||
:allow-remove="false"
|
||||
@add-document="$emit('addDocument', $event)"
|
||||
></drop-file-modal>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import PickTemplate from "ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue";
|
||||
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
|
||||
import {
|
||||
EVALUATION_DOCUMENT_ADD,
|
||||
EVALUATION_DOCUMENT_UPLOAD,
|
||||
EVALUATION_GENERATE_A_DOCUMENT,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
defineProps(["evaluation", "templates"]);
|
||||
defineEmits(["addDocument", "submitBeforeGenerate"]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
ul.document-upload {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
</style>
|
@@ -1,345 +0,0 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<h5>{{ trans(EVALUATION_DOCUMENTS) }} :</h5>
|
||||
<div class="flex-table">
|
||||
<div
|
||||
class="item-bloc"
|
||||
v-for="(d, i) in documents"
|
||||
:key="d.id"
|
||||
:class="[
|
||||
parseInt(docAnchorId) === d.id ? 'bg-blink' : 'nothing',
|
||||
]"
|
||||
>
|
||||
<div :id="'document_' + d.id" class="item-row">
|
||||
<div class="input-group input-group-lg mb-3 row">
|
||||
<label class="col-sm-3 col-form-label"
|
||||
>Titre du document:</label
|
||||
>
|
||||
<div class="col-sm-9">
|
||||
<input
|
||||
class="form-control document-title"
|
||||
type="text"
|
||||
:value="d.title"
|
||||
:id="d.id"
|
||||
:data-key="i"
|
||||
@input="$emit('inputDocumentTitle', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<div class="item-col item-meta">
|
||||
<p v-if="d.createdBy" class="createdBy">
|
||||
Créé par {{ d.createdBy.text }}<br />
|
||||
Le
|
||||
{{
|
||||
$d(ISOToDatetime(d.createdAt.datetime), "long")
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<div class="item-col">
|
||||
<ul class="record_actions">
|
||||
<li
|
||||
v-if="
|
||||
d.workflows_availables.length > 0 ||
|
||||
d.workflows.length > 0
|
||||
"
|
||||
>
|
||||
<list-workflow-modal
|
||||
:workflows="d.workflows"
|
||||
:allowCreate="true"
|
||||
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument"
|
||||
:relatedEntityId="d.id"
|
||||
:workflowsAvailables="
|
||||
d.workflows_availables
|
||||
"
|
||||
:preventDefaultMoveToGenerate="true"
|
||||
:goToGenerateWorkflowPayload="{ doc: d }"
|
||||
@go-to-generate-workflow="
|
||||
$emit('goToGenerateWorkflow', $event)
|
||||
"
|
||||
></list-workflow-modal>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
v-if="AmIRefferer"
|
||||
class="btn btn-notify"
|
||||
@click="
|
||||
$emit(
|
||||
'goToGenerateNotification',
|
||||
d,
|
||||
false,
|
||||
)
|
||||
"
|
||||
></button>
|
||||
<template v-else>
|
||||
<button
|
||||
id="btnGroupNotifyButtons"
|
||||
type="button"
|
||||
class="btn btn-notify dropdown-toggle"
|
||||
:title="
|
||||
trans(EVALUATION_NOTIFICATION_SEND)
|
||||
"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu"
|
||||
aria-labelledby="btnGroupNotifyButtons"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
$emit(
|
||||
'goToGenerateNotification',
|
||||
d,
|
||||
true,
|
||||
)
|
||||
"
|
||||
>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
|
||||
)
|
||||
}}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
$emit(
|
||||
'goToGenerateNotification',
|
||||
d,
|
||||
false,
|
||||
)
|
||||
"
|
||||
>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_NOTIFICATION_NOTIFY_ANY,
|
||||
)
|
||||
}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</li>
|
||||
<li>
|
||||
<document-action-buttons-group
|
||||
:stored-object="d.storedObject"
|
||||
:filename="d.title"
|
||||
:can-edit="true"
|
||||
:execute-before-leave="
|
||||
submitBeforeLeaveToEditor
|
||||
"
|
||||
:davLink="
|
||||
d.storedObject._links?.dav_link.href
|
||||
"
|
||||
:davLinkExpiration="
|
||||
d.storedObject._links?.dav_link
|
||||
.expiration
|
||||
"
|
||||
@on-stored-object-status-change="
|
||||
$emit('statusDocumentChanged', $event)
|
||||
"
|
||||
></document-action-buttons-group>
|
||||
</li>
|
||||
<li v-if="Number.isInteger(d.id)">
|
||||
<div class="duplicate-dropdown">
|
||||
<button
|
||||
class="btn btn-edit dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{{ trans(EVALUATION_DOCUMENT_EDIT) }}
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<!--delete-->
|
||||
<li v-if="d.workflows.length === 0">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
$emit('removeDocument', d)
|
||||
"
|
||||
>
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_DOCUMENT_DELETE,
|
||||
)
|
||||
}}
|
||||
</a>
|
||||
</li>
|
||||
<!--replace document-->
|
||||
<li
|
||||
v-if="
|
||||
d.storedObject._permissions
|
||||
.canEdit
|
||||
"
|
||||
>
|
||||
<drop-file-modal
|
||||
:existing-doc="d.storedObject"
|
||||
:allow-remove="false"
|
||||
@add-document="
|
||||
(arg) =>
|
||||
$emit(
|
||||
'replaceDocument',
|
||||
d,
|
||||
arg.stored_object,
|
||||
arg.stored_object_version,
|
||||
)
|
||||
"
|
||||
></drop-file-modal>
|
||||
</li>
|
||||
<!--duplicate document-->
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
$emit(
|
||||
'duplicateDocument',
|
||||
d,
|
||||
)
|
||||
"
|
||||
>
|
||||
<i class="fa fa-copy" aria-hidden="true"></i>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_DOCUMENT_DUPLICATE_HERE,
|
||||
)
|
||||
}}
|
||||
</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
prepareDocumentDuplicationToWork(
|
||||
d,
|
||||
)
|
||||
"
|
||||
>
|
||||
<i class="fa fa-copy" aria-hidden="true"></i>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
|
||||
)
|
||||
}}</a
|
||||
>
|
||||
</li>
|
||||
<!--move document-->
|
||||
<li
|
||||
v-if="
|
||||
d.storedObject._permissions
|
||||
.canEdit
|
||||
"
|
||||
>
|
||||
<a
|
||||
class="dropdown-item"
|
||||
@click="
|
||||
prepareDocumentMoveToWork(d)
|
||||
"
|
||||
>
|
||||
<i class="fa fa-arrows" aria-hidden="true"></i>
|
||||
{{
|
||||
trans(
|
||||
EVALUATION_DOCUMENT_MOVE,
|
||||
)
|
||||
}}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AccompanyingPeriodWorkSelectorModal
|
||||
v-if="showAccompanyingPeriodSelector"
|
||||
v-model:selectedAcpw="selectedAcpw"
|
||||
:accompanying-period-id="accompanyingPeriodId"
|
||||
:is-evaluation-selector="true"
|
||||
:ignore-accompanying-period-work-ids="[]"
|
||||
@close-modal="showAccompanyingPeriodSelector = false"
|
||||
@update:selectedEvaluation="selectedEvaluation = $event"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ISOToDatetime } from "ChillMainAssets/chill/js/date";
|
||||
import ListWorkflowModal from "ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue";
|
||||
import DocumentActionButtonsGroup from "ChillDocStoreAssets/vuejs/DocumentActionButtonsGroup.vue";
|
||||
import DropFileModal from "ChillDocStoreAssets/vuejs/DropFileWidget/DropFileModal.vue";
|
||||
import {
|
||||
EVALUATION_NOTIFICATION_NOTIFY_REFERRER,
|
||||
EVALUATION_NOTIFICATION_NOTIFY_ANY,
|
||||
EVALUATION_NOTIFICATION_SEND,
|
||||
EVALUATION_DOCUMENTS,
|
||||
EVALUATION_DOCUMENT_MOVE,
|
||||
EVALUATION_DOCUMENT_DELETE,
|
||||
EVALUATION_DOCUMENT_EDIT,
|
||||
EVALUATION_DOCUMENT_DUPLICATE_HERE,
|
||||
EVALUATION_DOCUMENT_DUPLICATE_TO_OTHER_EVALUATION,
|
||||
trans,
|
||||
} from "translator";
|
||||
import { ref, watch } from "vue";
|
||||
import AccompanyingPeriodWorkSelectorModal from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkSelectorModal.vue";
|
||||
|
||||
defineProps(["documents", "docAnchorId", "accompanyingPeriodId", "accompanyingPeriodWorkId"]);
|
||||
const emit = defineEmits([
|
||||
"inputDocumentTitle",
|
||||
"removeDocument",
|
||||
"duplicateDocument",
|
||||
"statusDocumentChanged",
|
||||
"goToGenerateWorkflow",
|
||||
"goToGenerateNotification",
|
||||
"duplicateDocumentToWork",
|
||||
]);
|
||||
|
||||
const showAccompanyingPeriodSelector = ref(false);
|
||||
const selectedEvaluation = ref(null);
|
||||
const selectedDocumentToDuplicate = ref(null);
|
||||
const selectedDocumentToMove = ref(null);
|
||||
|
||||
const prepareDocumentDuplicationToWork = (d) => {
|
||||
selectedDocumentToDuplicate.value = d;
|
||||
/** ensure selectedDocumentToMove is null */
|
||||
selectedDocumentToMove.value = null;
|
||||
|
||||
showAccompanyingPeriodSelector.value = true;
|
||||
};
|
||||
|
||||
const prepareDocumentMoveToWork = (d) => {
|
||||
selectedDocumentToMove.value = d;
|
||||
/** ensure selectedDocumentToDuplicate is null */
|
||||
selectedDocumentToDuplicate.value = null;
|
||||
|
||||
showAccompanyingPeriodSelector.value = true;
|
||||
};
|
||||
|
||||
watch(selectedEvaluation, (val) => {
|
||||
if (selectedDocumentToDuplicate.value) {
|
||||
emit("duplicateDocumentToEvaluation", {
|
||||
evaluation: val,
|
||||
document: selectedDocumentToDuplicate.value,
|
||||
});
|
||||
} else {
|
||||
emit("moveDocumentToEvaluation", {
|
||||
evaluationDest: val,
|
||||
document: selectedDocumentToMove.value,
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
File diff suppressed because it is too large
Load Diff
@@ -1,32 +0,0 @@
|
||||
<template>
|
||||
<div class="row mb-3">
|
||||
<label class="col-4 col-sm-2 col-md-4 col-lg-2 col-form-label">
|
||||
{{ trans(EVALUATION_TIME_SPENT) }}
|
||||
</label>
|
||||
<div class="col-8 col-sm-4 col-md-8 col-lg-4">
|
||||
<select
|
||||
class="form-control form-control-sm"
|
||||
:value="timeSpent"
|
||||
@input="$emit('update:timeSpent', $event.target.value)"
|
||||
>
|
||||
<option disabled value="">
|
||||
{{ trans(EVALUATION_TIME_SPENT) }}
|
||||
</option>
|
||||
<option
|
||||
v-for="time in timeSpentChoices"
|
||||
:value="time.value"
|
||||
:key="time.value"
|
||||
>
|
||||
{{ time.text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { EVALUATION_TIME_SPENT, trans } from "translator";
|
||||
|
||||
defineProps(["timeSpent", "timeSpentChoices"]);
|
||||
defineEmits(["update:timeSpent"]);
|
||||
</script>
|
@@ -11,13 +11,12 @@ import { findSocialActionsBySocialIssue } from "ChillPersonAssets/vuejs/_api/Soc
|
||||
import { create } from "ChillPersonAssets/vuejs/_api/AccompanyingCourseWork.js";
|
||||
import { fetchResults, makeFetch } from "ChillMainAssets/lib/api/apiMethods.ts";
|
||||
import { fetchTemplates } from "ChillDocGeneratorAssets/api/pickTemplate.js";
|
||||
import {
|
||||
duplicate,
|
||||
duplicateDocumentToEvaluation,
|
||||
moveDocumentToEvaluation,
|
||||
} from "../_api/accompanyingCourseWorkEvaluationDocument";
|
||||
import { duplicate } from "../_api/accompanyingCourseWorkEvaluationDocument";
|
||||
|
||||
const debug = process.env.NODE_ENV !== "production";
|
||||
const evalFQDN = encodeURIComponent(
|
||||
"Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation",
|
||||
);
|
||||
|
||||
const store = createStore({
|
||||
strict: debug,
|
||||
@@ -299,47 +298,15 @@ const store = createStore({
|
||||
);
|
||||
},
|
||||
addDuplicatedDocument(state, { document, evaluation_key }) {
|
||||
console.log("add duplicated document", document);
|
||||
console.log("add duplicated dcuemnt - evaluation key", evaluation_key);
|
||||
|
||||
let evaluation = state.evaluationsPicked.find(
|
||||
(e) => e.key === evaluation_key,
|
||||
);
|
||||
document.key = evaluation.documents.length + 1;
|
||||
evaluation.documents.splice(0, 0, document);
|
||||
},
|
||||
addDuplicatedDocumentToEvaluation(state, { document, evaluation }) {
|
||||
let evaluationDest = state.evaluationsPicked.find(
|
||||
(e) => e.id === evaluation.id,
|
||||
);
|
||||
if (evaluationDest) {
|
||||
console.log("add duplicated document to evaluation", evaluationDest);
|
||||
document.key = evaluationDest.documents.length + 1;
|
||||
evaluationDest.documents.splice(0, 0, document);
|
||||
}
|
||||
},
|
||||
moveDocumentToEvaluation(
|
||||
state,
|
||||
{ evaluationInitial, evaluationDest, document },
|
||||
) {
|
||||
let evaluationA = state.evaluationsPicked.find(
|
||||
(e) => e.id === evaluationInitial.id,
|
||||
);
|
||||
let evaluationB = state.evaluationsPicked.find(
|
||||
(e) => e.id === evaluationDest.id,
|
||||
);
|
||||
|
||||
if (evaluationB) {
|
||||
// add document to chosen evaluation if evaluation is part of the same social work
|
||||
document.key = evaluationB.documents.length + 1;
|
||||
evaluationB.documents.splice(0, 0, document);
|
||||
}
|
||||
|
||||
// remove document from original evaluation
|
||||
const indexToRemove = evaluationA.documents.findIndex(
|
||||
(doc) => doc.id === document.id,
|
||||
);
|
||||
if (indexToRemove !== -1) {
|
||||
evaluationA.documents.splice(indexToRemove, 1);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Replaces a document in the state with a new document.
|
||||
*
|
||||
@@ -636,44 +603,6 @@ const store = createStore({
|
||||
const newDoc = await duplicate(document.id);
|
||||
commit("addDuplicatedDocument", { document: newDoc, evaluation_key });
|
||||
},
|
||||
async duplicateDocumentToEvaluation({ commit }, { document, evaluation }) {
|
||||
try {
|
||||
const newDoc = await duplicateDocumentToEvaluation(
|
||||
document.id,
|
||||
evaluation.id,
|
||||
);
|
||||
commit("addDuplicatedDocumentToEvaluation", {
|
||||
document: newDoc,
|
||||
evaluation,
|
||||
});
|
||||
|
||||
return newDoc;
|
||||
} catch (error) {
|
||||
console.error("Failed to move document:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async moveDocumentToEvaluation(
|
||||
{ commit },
|
||||
{ evaluationInitial, evaluationDest, document },
|
||||
) {
|
||||
try {
|
||||
const response = await moveDocumentToEvaluation(
|
||||
document.id,
|
||||
evaluationDest.id,
|
||||
);
|
||||
commit("moveDocumentToEvaluation", {
|
||||
evaluationInitial,
|
||||
evaluationDest,
|
||||
document,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Failed to move document:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
removeDocument({ commit }, payload) {
|
||||
commit("removeDocument", payload);
|
||||
},
|
||||
|
@@ -9,23 +9,3 @@ export const duplicate = async (
|
||||
`/api/1.0/person/accompanying-course-work-evaluation-document/${id}/duplicate`,
|
||||
);
|
||||
};
|
||||
|
||||
export const duplicateDocumentToEvaluation = async (
|
||||
document_id: number,
|
||||
evaluation_id: number,
|
||||
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
|
||||
return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
|
||||
"POST",
|
||||
`/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/duplicate`,
|
||||
);
|
||||
};
|
||||
|
||||
export const moveDocumentToEvaluation = async (
|
||||
document_id: number,
|
||||
evaluation_id: number,
|
||||
): Promise<AccompanyingPeriodWorkEvaluationDocument> => {
|
||||
return makeFetch<null, AccompanyingPeriodWorkEvaluationDocument>(
|
||||
"POST",
|
||||
`/api/1.0/person/accompanying-course-work-evaluation-document/${document_id}/evaluation/${evaluation_id}/move`,
|
||||
);
|
||||
};
|
||||
|
@@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<h2 class="badge-title">
|
||||
<span class="title_label"></span>
|
||||
<span class="title_action">
|
||||
<span>
|
||||
{{ trans(EVALUATION) }}:
|
||||
<span class="badge bg-light text-dark">
|
||||
{{ eval?.evaluation?.title.fr }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<ul class="small_in_title columns mt-1">
|
||||
<li>
|
||||
<span class="item-key">
|
||||
{{
|
||||
trans(
|
||||
ACCOMPANYING_COURSE_WORK_START_DATE,
|
||||
)
|
||||
}}
|
||||
:
|
||||
</span>
|
||||
<b>{{ formatDate(eval.startDate) }}</b>
|
||||
</li>
|
||||
|
||||
<li v-if="eval.endDate">
|
||||
<span class="item-key">
|
||||
{{
|
||||
trans(ACCOMPANYING_COURSE_WORK_END_DATE)
|
||||
}}
|
||||
:
|
||||
</span>
|
||||
<b>{{ formatDate(eval.endDate) }}</b>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ACCOMPANYING_COURSE_WORK_END_DATE,
|
||||
ACCOMPANYING_COURSE_WORK_START_DATE,
|
||||
EVALUATION,
|
||||
trans,
|
||||
} from "translator";
|
||||
import { ISOToDate } from "ChillMainAssets/chill/js/date";
|
||||
import { DateTime } from "ChillMainAssets/types";
|
||||
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const props = defineProps<{ eval: AccompanyingPeriodWorkEvaluation }>();
|
||||
const formatDate = (dateObject: DateTime) => {
|
||||
if (dateObject) {
|
||||
const parsedDate = ISOToDate(dateObject.datetime);
|
||||
if (parsedDate) {
|
||||
return new Intl.DateTimeFormat("default", {
|
||||
dateStyle: "short",
|
||||
}).format(parsedDate);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<div class="results">
|
||||
<div
|
||||
v-for="evaluation in evaluations"
|
||||
:key="evaluation.id"
|
||||
class="list-item"
|
||||
>
|
||||
<label class="acpw-item">
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
:value="evaluation"
|
||||
v-model="selectedEvaluation"
|
||||
name="item"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<accompanying-period-work-evaluation-item :eval="evaluation" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
|
||||
import { defineProps, ref, watch } from "vue";
|
||||
import AccompanyingPeriodWorkEvaluationItem from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationItem.vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const props = defineProps<{
|
||||
evaluations: AccompanyingPeriodWorkEvaluation[];
|
||||
}>();
|
||||
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
|
||||
|
||||
// eslint-disable-next-line vue/valid-define-emits
|
||||
const emit = defineEmits();
|
||||
|
||||
watch(selectedEvaluation, (newValue) => {
|
||||
emit("update:selectedEvaluation", newValue);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.acpw-item {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
@@ -26,24 +26,14 @@ import AccompanyingPeriodWorkItem from "./AccompanyingPeriodWorkItem.vue";
|
||||
import { AccompanyingPeriodWork } from "../../../types";
|
||||
import { defineProps, ref, watch } from "vue";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const props = defineProps<{
|
||||
accompanyingPeriodWorks: AccompanyingPeriodWork[];
|
||||
selectedAcpw?: AccompanyingPeriodWork | null;
|
||||
}>();
|
||||
const selectedAcpw = ref<AccompanyingPeriodWork | null>(
|
||||
props.selectedAcpw ?? null,
|
||||
);
|
||||
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:selectedAcpw": [value: AccompanyingPeriodWork | null];
|
||||
}>();
|
||||
|
||||
watch(
|
||||
() => props.selectedAcpw,
|
||||
(val) => {
|
||||
selectedAcpw.value = val ?? null;
|
||||
},
|
||||
);
|
||||
// eslint-disable-next-line vue/valid-define-emits
|
||||
const emit = defineEmits();
|
||||
|
||||
watch(selectedAcpw, (newValue) => {
|
||||
emit("update:selectedAcpw", newValue);
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row justify-content-end" v-if="!isEvaluationSelector">
|
||||
<div class="row justify-content-end">
|
||||
<div class="col-md-6 col-sm-10" v-if="selectedAcpw">
|
||||
<ul class="list-suggest remove-items">
|
||||
<li>
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul v-if="!showModal" class="record_actions">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-sm btn-create mt-3" @click="openModal">
|
||||
{{ trans(ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK) }}
|
||||
@@ -40,15 +40,9 @@
|
||||
|
||||
<template #body>
|
||||
<accompanying-period-work-list
|
||||
v-if="evaluations.length === 0"
|
||||
:accompanying-period-works="accompanyingPeriodWorks"
|
||||
v-model:selectedAcpw="selectedAcpw"
|
||||
/>
|
||||
<accompanying-period-work-evaluation-list
|
||||
v-if="evaluations.length > 0"
|
||||
:evaluations="evaluations"
|
||||
v-model:selectedEvaluation="selectedEvaluation"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
@@ -66,107 +60,58 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import AccompanyingPeriodWorkList from "./AccompanyingPeriodWorkList.vue";
|
||||
import { AccompanyingPeriodWork } from "../../../types";
|
||||
import {
|
||||
trans,
|
||||
ACPW_DUPLICATE_SELECT_ACCOMPANYING_PERIOD_WORK,
|
||||
CONFIRM,
|
||||
trans,
|
||||
} from "translator";
|
||||
import {fetchResults, makeFetch} from "ChillMainAssets/lib/api/apiMethods";
|
||||
import AccompanyingPeriodWorkEvaluationList from "ChillPersonAssets/vuejs/_components/AccompanyingPeriodWorkSelector/AccompanyingPeriodWorkEvaluationList.vue";
|
||||
import { AccompanyingPeriodWorkEvaluation } from "../../../types";
|
||||
import { fetchResults } from "ChillMainAssets/lib/api/apiMethods";
|
||||
|
||||
interface AccompanyingPeriodWorkSelectorModalProps {
|
||||
accompanyingPeriodId: number;
|
||||
}
|
||||
|
||||
const selectedAcpw = ref<AccompanyingPeriodWork | null>(null);
|
||||
const selectedEvaluation = ref<AccompanyingPeriodWorkEvaluation | null>(null);
|
||||
const showModal = ref(false);
|
||||
const accompanyingPeriodWorks = ref<AccompanyingPeriodWork[]>([]);
|
||||
const evaluations = ref<AccompanyingPeriodWorkEvaluation[]>([]);
|
||||
|
||||
const props = defineProps<{
|
||||
accompanyingPeriodId: string,
|
||||
isEvaluationSelector: boolean,
|
||||
ignoreAccompanyingPeriodWorkIds: number[],
|
||||
}>();
|
||||
const props = defineProps<AccompanyingPeriodWorkSelectorModalProps>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
pickWork: [payload: { work: AccompanyingPeriodWork | null }];
|
||||
closeModal: [];
|
||||
"update:selectedEvaluation": [evaluation: AccompanyingPeriodWorkEvaluation];
|
||||
}>();
|
||||
|
||||
onMounted(() => {
|
||||
if (props.accompanyingPeriodId) {
|
||||
getAccompanyingPeriodWorks(parseInt(props.accompanyingPeriodId));
|
||||
getAccompanyingPeriodWorks(props.accompanyingPeriodId);
|
||||
} else {
|
||||
console.error("No accompanyingperiod id was given");
|
||||
}
|
||||
|
||||
showModal.value = true;
|
||||
});
|
||||
const getAccompanyingPeriodWorks = async (periodId: number) => {
|
||||
const url = `/api/1.0/person/accompanying-course/${periodId}/works.json`;
|
||||
|
||||
const accompanyingPeriodWorksFetched = await fetchResults<AccompanyingPeriodWork>(url);
|
||||
if (props.isEvaluationSelector) {
|
||||
accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched.filter(
|
||||
(acpw: AccompanyingPeriodWork) => acpw.accompanyingPeriodWorkEvaluations.length > 0
|
||||
&& typeof acpw.id !== "undefined"
|
||||
&& !props.ignoreAccompanyingPeriodWorkIds.includes(acpw.id),
|
||||
);
|
||||
} else {
|
||||
accompanyingPeriodWorks.value = accompanyingPeriodWorksFetched
|
||||
try {
|
||||
accompanyingPeriodWorks.value = await fetchResults(url);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
/* makeFetch<number, AccompanyingPeriodWork[]>("GET", url)
|
||||
.then((response) => {
|
||||
accompanyingPeriodWorks.value = response;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});*/
|
||||
};
|
||||
|
||||
watch(selectedAcpw, (newValue) => {
|
||||
const inputField = document.getElementById(
|
||||
"find_accompanying_period_work_acpw",
|
||||
) as HTMLInputElement;
|
||||
if (inputField) {
|
||||
inputField.value = String(newValue?.id || "");
|
||||
}
|
||||
|
||||
/* if (!props.isEvaluationSelector) {
|
||||
console.log("Emitting from watch:", { work: newValue });
|
||||
emit("pickWork", { work: newValue });
|
||||
}*/
|
||||
});
|
||||
|
||||
const openModal = () => {
|
||||
showModal.value = true;
|
||||
};
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
selectedEvaluation.value = null;
|
||||
// selectedAcpw.value = null;
|
||||
emit("closeModal");
|
||||
};
|
||||
const openModal = () => (showModal.value = true);
|
||||
const closeModal = () => (showModal.value = false);
|
||||
const confirmSelection = () => {
|
||||
selectedAcpw.value = selectedAcpw.value;
|
||||
console.log("selectedAcpw", selectedAcpw.value);
|
||||
|
||||
if (!props.isEvaluationSelector) {
|
||||
if (selectedAcpw.value) {
|
||||
// only emit if something is actually selected!
|
||||
emit("pickWork", { work: selectedAcpw.value });
|
||||
closeModal();
|
||||
}
|
||||
// optionally show some error or warning if not selected
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedAcpw.value && props.isEvaluationSelector) {
|
||||
evaluations.value =
|
||||
selectedAcpw.value.accompanyingPeriodWorkEvaluations;
|
||||
}
|
||||
|
||||
if (selectedEvaluation.value && props.isEvaluationSelector) {
|
||||
// console.log('evaluation log in modal', selectedEvaluation.value)
|
||||
emit("update:selectedEvaluation", selectedEvaluation.value);
|
||||
closeModal();
|
||||
}
|
||||
emit("pickWork", { work: selectedAcpw.value });
|
||||
closeModal();
|
||||
};
|
||||
</script>
|
||||
|
@@ -1,9 +1,9 @@
|
||||
{%- macro details(w, accompanyingCourse, options) -%}
|
||||
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with {
|
||||
'displayAction': true,
|
||||
'displayAction': false,
|
||||
'displayContent': 'short',
|
||||
'displayFontSmall': true,
|
||||
'itemBlocClass': '',
|
||||
'displayNotification': true
|
||||
'displayNotification': false
|
||||
} %}
|
||||
{% endmacro %}
|
||||
|
@@ -2,10 +2,10 @@
|
||||
|
||||
{% set activeRouteKey = 'chill_person_accompanying_period_work_assign_duplicate' %}
|
||||
|
||||
{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %}
|
||||
|
||||
{% block title %}{{ 'Assign an accompanying period work duplicate' }}{% endblock %}
|
||||
|
||||
{% import '@ChillPerson/AccompanyingPeriodWorkDuplicate/_details.html.twig' as details %}
|
||||
|
||||
{% block content %}
|
||||
<div class="person-duplicate">
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h1>
|
||||
<h3>{{ 'acpw_duplicate.Assign duplicate'|trans }}</h3>
|
||||
{{ form_start(form) }}
|
||||
{%- if form.acpw is defined -%}
|
||||
{{ form_row(form.acpw) }}
|
||||
|
@@ -32,16 +32,9 @@
|
||||
<div class="wl-col list">
|
||||
<div class="d-flex flex-column justify-content-center">
|
||||
{% if app != null %}
|
||||
{% if acp.closingDate != null %}
|
||||
{{ 'accompanying_period.dates_from_%opening_date%_to_%closing_date%'|trans({
|
||||
'%opening_date%': acp.openingDate|format_date('long'),
|
||||
'%closing_date%': acp.closingDate|format_date('long')}
|
||||
) }}
|
||||
{% else %}
|
||||
<div class="date">
|
||||
{{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="date">
|
||||
{{ 'Since %date%'|trans({'%date%': app.startDate|format_date('medium') }) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) %}
|
||||
@@ -77,20 +70,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if acp.step == 'CLOSED' and acp.closingMotive is not null %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3 class="closingMotive">{{ 'Closing motive'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
<div>
|
||||
{{ acp.closingMotive.name|localize_translatable_string }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if acp.user is not null %}
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
|
@@ -12,7 +12,6 @@ declare(strict_types=1);
|
||||
namespace Chill\PersonBundle\Service\AccompanyingPeriodWorkEvaluationDocument;
|
||||
|
||||
use Chill\DocStoreBundle\Service\StoredObjectDuplicate;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||
use Symfony\Component\Clock\ClockInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
@@ -37,17 +36,4 @@ class AccompanyingPeriodWorkEvaluationDocumentDuplicator
|
||||
|
||||
return $newDocument;
|
||||
}
|
||||
|
||||
public function duplicateToEvaluation(AccompanyingPeriodWorkEvaluationDocument $document, AccompanyingPeriodWorkEvaluation $evaluation): AccompanyingPeriodWorkEvaluationDocument
|
||||
{
|
||||
$newDocument = new AccompanyingPeriodWorkEvaluationDocument();
|
||||
$newDocument
|
||||
->setTitle($document->getTitle().' ('.$this->translator->trans('accompanying_course_evaluation_document.duplicated_at', ['at' => $this->clock->now()]).')')
|
||||
->setStoredObject($this->storedObjectDuplicate->duplicate($document->getStoredObject()))
|
||||
;
|
||||
|
||||
$evaluation->addDocument($newDocument);
|
||||
|
||||
return $newDocument;
|
||||
}
|
||||
}
|
||||
|
@@ -1993,33 +1993,3 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
|
||||
/1.0/person/accompanying-course-work-evaluation-document/{document_id}/evaluation/{evaluation_id}/duplicate:
|
||||
post:
|
||||
tags:
|
||||
- accompanying-course-work-evaluation-document
|
||||
summary: Dupliate an an accompanying period work evaluation document to another evaluation
|
||||
parameters:
|
||||
- in: path
|
||||
name: document_id
|
||||
required: true
|
||||
description: The document's id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
- in: path
|
||||
name: evaluation_id
|
||||
required: true
|
||||
description: The evaluation's id
|
||||
schema:
|
||||
type: integer
|
||||
format: integer
|
||||
minimum: 1
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
|
@@ -750,42 +750,6 @@ evaluation:
|
||||
delay: Délai
|
||||
notificationDelay: Délai de notification
|
||||
url: Lien internet
|
||||
title: Ecrire une évaluation
|
||||
status: Statut
|
||||
choose_a_status: Choisir un statut
|
||||
startdate: Date d'ouverture
|
||||
enddate: Date de fin
|
||||
maxdate: Date d'échéance
|
||||
warning_interval: Rappel (jours)
|
||||
public_comment: Note publique
|
||||
comment_placeholder: Commencez à écrire ...
|
||||
generate_a_document: Générer un document
|
||||
choose_a_template: Choisir un modèle
|
||||
add_a_document: Ajouter un document
|
||||
add: Ajouter une évaluation
|
||||
time_spent: Temps de rédaction
|
||||
select_time_spent: Indiquez le temps de rédaction
|
||||
Documents: Documents
|
||||
document_add: Générer ou téléverser un document
|
||||
document_upload: Téléverser un document
|
||||
document_title: Titre du document
|
||||
template_title: Nom du template
|
||||
browse: Ajouter un document
|
||||
replace: Remplacer
|
||||
download: Télécharger le fichier existant
|
||||
notification_notify_referrer: Notifier le référent
|
||||
notification_notify_any: Notifier d'autres utilisateurs
|
||||
notification_send: Envoyer une notification
|
||||
document:
|
||||
edit: Modifier
|
||||
delete: Supprimer
|
||||
move: Déplacer
|
||||
duplicate: Dupliquer
|
||||
duplicate_here: Dupliquer ici
|
||||
duplicate_to_other_evaluation: Dupliquer vers une autre évaluation
|
||||
duplicate_success: Le document d'évaluation a été dupliquer
|
||||
move_success: Le document d'évaluation a été déplacer
|
||||
|
||||
|
||||
goal:
|
||||
desactivationDate: Date de désactivation
|
||||
@@ -810,6 +774,7 @@ relation:
|
||||
reverseTitle: Deuxième membre
|
||||
|
||||
days: jours
|
||||
months: mois
|
||||
years: années
|
||||
|
||||
# specific to closing motive
|
||||
@@ -961,7 +926,7 @@ accompanying_course_work:
|
||||
types_filter: Filtrer par type d'action
|
||||
user_filter: Filtrer par intervenant
|
||||
On-going works over total: Actions en cours / Actions du parcours
|
||||
my_actions_filter: Mes actions (où j'interviens)
|
||||
|
||||
|
||||
#
|
||||
Person addresses: Adresses de résidence
|
||||
@@ -1557,6 +1522,3 @@ my_parcours_filters:
|
||||
parcours_intervening: Intervenant
|
||||
is_open: Parcours ouverts
|
||||
is_closed: Parcours clôturés
|
||||
|
||||
document_duplicate:
|
||||
to_evaluation_success: "Le document a été dupliquer"
|
||||
|
@@ -624,7 +624,8 @@ final class SingleTaskController extends AbstractController
|
||||
->addCheckbox('status', $statuses, $statuses, $statusTrans);
|
||||
|
||||
$states = $this->singleTaskStateRepository->findAllExistingStates();
|
||||
$checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['to_validate', 'in_progress', 'closed', 'canceled', 'validated'], true)));
|
||||
$checked = array_values(array_filter($states, fn (string $state) => !in_array($state, ['in_progress', 'closed', 'canceled', 'validated'], true)));
|
||||
|
||||
if ([] !== $states) {
|
||||
$filterBuilder
|
||||
->addCheckbox('states', $states, $checked);
|
||||
|
@@ -1,125 +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\ThirdPartyBundle\Controller;
|
||||
|
||||
use Chill\PersonBundle\Form\PersonConfimDuplicateType;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Chill\ThirdPartyBundle\Form\ThirdpartyFindDuplicateType;
|
||||
use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Component\Translation\TranslatableMessage;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ThirdpartyDuplicateController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly ThirdpartyMergeService $thirdPartyMergeService, private readonly TranslatorInterface $translator) {}
|
||||
|
||||
/**
|
||||
* @ParamConverter("thirdparty", options={"id": "thirdparty_id"})
|
||||
*/
|
||||
#[Route(path: '/{_locale}/3party/{thirdparty_id}/find-manually', name: 'chill_thirdparty_find_duplicate')]
|
||||
public function findManuallyDuplicateAction(ThirdParty $thirdparty, Request $request)
|
||||
{
|
||||
$suggested = [];
|
||||
|
||||
if ('child' === $thirdparty->getKind()) {
|
||||
$suggested = $thirdparty->getParent()->getChildren();
|
||||
}
|
||||
|
||||
$form = $this->createForm(ThirdpartyFindDuplicateType::class, null, ['suggested' => $suggested]);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$thirdparty2 = $form->get('thirdparty')->getData();
|
||||
|
||||
$direction = $form->get('direction')->getData();
|
||||
|
||||
if ('starting' === $direction) {
|
||||
$params = [
|
||||
'thirdparty1_id' => $thirdparty->getId(),
|
||||
'thirdparty2_id' => $thirdparty2->getId(),
|
||||
];
|
||||
} else {
|
||||
$params = [
|
||||
'thirdparty1_id' => $thirdparty2->getId(),
|
||||
'thirdparty2_id' => $thirdparty->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('chill_thirdparty_duplicate_confirm', $params);
|
||||
}
|
||||
|
||||
return $this->render('@ChillThirdParty/ThirdPartyDuplicate/find_duplicate.html.twig', [
|
||||
'thirdparty' => $thirdparty,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ParamConverter("thirdparty1", options={"id": "thirdparty1_id"})
|
||||
* @ParamConverter("thirdparty2", options={"id": "thirdparty2_id"})
|
||||
*/
|
||||
#[Route(path: '/{_locale}/3party/{thirdparty1_id}/duplicate/{thirdparty2_id}/confirm', name: 'chill_thirdparty_duplicate_confirm')]
|
||||
public function confirmAction(ThirdParty $thirdparty1, ThirdParty $thirdparty2, Request $request)
|
||||
{
|
||||
try {
|
||||
$this->validateThirdpartyMerge($thirdparty1, $thirdparty2);
|
||||
$form = $this->createForm(PersonConfimDuplicateType::class);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
|
||||
$this->thirdPartyMergeService->merge($thirdparty1, $thirdparty2);
|
||||
|
||||
$session = $request->getSession();
|
||||
if ($session instanceof Session) {
|
||||
$session->getFlashBag()->add('success', new TranslatableMessage('thirdparty_duplicate.Merge successful'));
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('chill_crud_3party_3party_view', ['id' => $thirdparty1->getId()]);
|
||||
}
|
||||
|
||||
return $this->render('@ChillThirdParty/ThirdPartyDuplicate/confirm.html.twig', [
|
||||
'thirdparty' => $thirdparty1,
|
||||
'thirdparty2' => $thirdparty2,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->addFlash('error', $this->translator->trans($e->getMessage()));
|
||||
|
||||
return $this->redirectToRoute('chill_thirdparty_find_duplicate', [
|
||||
'thirdparty_id' => $thirdparty1->getId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateThirdpartyMerge(ThirdParty $thirdparty1, ThirdParty $thirdparty2): void
|
||||
{
|
||||
$constraints = [
|
||||
[$thirdparty1 === $thirdparty2, 'thirdparty_duplicate.You cannot merge a thirdparty with itself. Please choose a different thirdparty'],
|
||||
[$thirdparty1->getKind() !== $thirdparty2->getKind(), 'thirdparty_duplicate.A thirdparty can only be merged with a thirdparty of the same kind'],
|
||||
[$thirdparty1->getParent() !== $thirdparty2->getParent(), 'thirdparty_duplicate.Two child thirdparties must have the same parent'],
|
||||
];
|
||||
|
||||
foreach ($constraints as [$condition, $message]) {
|
||||
if ($condition) {
|
||||
throw new \InvalidArgumentException($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,41 +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\ThirdPartyBundle\Form;
|
||||
|
||||
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ThirdpartyFindDuplicateType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('thirdparty', PickThirdpartyDynamicType::class, [
|
||||
'label' => 'Find duplicate',
|
||||
'mapped' => false,
|
||||
'suggested' => $options['suggested'],
|
||||
])
|
||||
->add('direction', HiddenType::class, [
|
||||
'data' => 'starting',
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'suggested' => [],
|
||||
]);
|
||||
}
|
||||
}
|
@@ -171,13 +171,7 @@
|
||||
<a class="btn btn-sm btn-show" target="_blank" title="{{ 'Show thirdparty'|trans }}"
|
||||
href="{{ path('chill_crud_3party_3party_view', { id: thirdparty.isChild ? thirdparty.parent.id : thirdparty.id }) }}"></a>
|
||||
</li>
|
||||
{% elseif is_granted('CHILL_3PARTY_3PARTY_UPDATE', thirdparty) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_thirdparty_find_duplicate',
|
||||
{ 'thirdparty_id': thirdparty.id }) }}"
|
||||
title="{{ 'Merge'|trans }}"
|
||||
class="btn btn-misc"><i class="bi bi-chevron-contract"></i></a>
|
||||
</li>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
|
||||
{% if options['customButtons']['after'] is defined %}
|
||||
|
@@ -128,7 +128,7 @@
|
||||
<div class="flex-table">
|
||||
{% for tp in thirdParty.activeChildren %}
|
||||
<div class="item-bloc">
|
||||
{{ tp|chill_entity_render_box({'render': 'bloc', 'addLink': false, 'isConfidential': tp.contactDataAnonymous ? true : false, 'showFusion': true }) }}
|
||||
{{ tp|chill_entity_render_box({'render': 'bloc', 'addLink': false, 'isConfidential': tp.contactDataAnonymous ? true : false }) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@@ -1,34 +0,0 @@
|
||||
{%- macro details(thirdparty, options) -%}
|
||||
|
||||
<ul>
|
||||
<li><b>{{ 'name'|trans }}</b>:
|
||||
{{ thirdparty.name }}</li>
|
||||
<li><b>{{ 'First name'|trans }}</b>:
|
||||
{% if thirdparty.firstname %}{{ thirdparty.firstname }}{% endif %}</li>
|
||||
<li><b>{{ 'thirdparty.Civility'|trans }}</b>:
|
||||
{% if thirdparty.getCivility %}{{ thirdparty.getCivility.name|localize_translatable_string }}{% endif %}</li>
|
||||
<li><b>{{ 'thirdparty.NameCompany'|trans }}</b>:
|
||||
{% if thirdparty.nameCompany is not empty %}{{ thirdparty.nameCompany }}{% endif %}</li>
|
||||
<li><b>{{ 'thirdparty.Acronym'|trans }}</b>:
|
||||
{% if thirdparty.acronym %}{{ thirdparty.acronym }}{% endif %}</li>
|
||||
<li><b>{{ 'thirdparty.Profession'|trans }}</b>:
|
||||
{% if thirdparty.profession %}{{ thirdparty.profession }}{% endif %}</li>
|
||||
<li><b>{{ 'telephone'|trans }}</b>:
|
||||
{% if thirdparty.telephone %}{{ thirdparty.telephone|chill_format_phonenumber }}{% endif %}</li>
|
||||
<li><b>{{ 'email'|trans }}</b>:
|
||||
{% if thirdparty.email is not null %}{{ thirdparty.email }}{% endif %}</li>
|
||||
<li><b>{{ 'Address'|trans }}</b>:
|
||||
{%- if thirdparty.getAddress is not empty -%}
|
||||
{{ thirdparty.getAddress|chill_entity_render_box }}
|
||||
{% endif %}</li>
|
||||
<li><b>{{ 'thirdparty.Contact data are confidential'|trans }}</b>:
|
||||
{{ thirdparty.contactDataAnonymous }}</li>
|
||||
<li><b>{{ 'Contacts'|trans }}</b>:
|
||||
<ul>
|
||||
{% for c in thirdparty.getChildren %}
|
||||
<li>{{ c.name }} {{ c.firstName }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
{% endmacro %}
|
@@ -1,97 +0,0 @@
|
||||
{% extends "@ChillMain/layout.html.twig" %}
|
||||
|
||||
{% import '@ChillThirdParty/ThirdPartyDuplicate/_details.html.twig' as details %}
|
||||
|
||||
{% block title %}{{ 'thirdparty_duplicate.Thirdparty duplicate title'|trans ~ ' ' ~ thirdparty.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
div.duplicate-content {
|
||||
margin: 0 2rem;
|
||||
}
|
||||
div.col {
|
||||
padding: 1em;
|
||||
border: 3px solid #cccccc;
|
||||
}
|
||||
div.border {
|
||||
border: 4px solid #3c9f8d;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
<div class="container-fluid content"><div class="duplicate-content">
|
||||
|
||||
<h1>{{ 'thirdparty_duplicate.title'|trans }}</h1>
|
||||
|
||||
<div class="col-md-11">
|
||||
<p><b>{{ 'thirdparty_duplicate.Thirdparty to delete'|trans }}</b>:
|
||||
{{ 'thirdparty_duplicate.Thirdparty to delete explanation'|trans }}
|
||||
</p>
|
||||
<div class="col">
|
||||
|
||||
<h1><span><a class="btn btn-show" target="_blank" title="{{ 'Open in another window'|trans }}" href="{{ path('chill_crud_3party_3party_view', { id : thirdparty2.id }) }}"></a></span>
|
||||
{{ thirdparty2 }}
|
||||
</h1>
|
||||
|
||||
<h4>{{ 'Deleted datas'|trans ~ ':' }}</h4>
|
||||
{{ details.details(thirdparty2) }}
|
||||
|
||||
{# <h4>{{ 'Moved links'|trans ~ ':' }}</h4>#}
|
||||
{# {{ details.links(thirdparty2) }}#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-11 mt-3">
|
||||
<p><b>{{ 'thirdparty_duplicate.Thirdparty to keep'|trans }}</b>:
|
||||
{{ 'thirdparty_duplicate.Thirdparty to keep explanation'|trans }}
|
||||
</p>
|
||||
<div class="col border">
|
||||
|
||||
<h1><span><a class="btn btn-show" target="_blank" title="{{ 'Open in another window'|trans }}" href="{{ path('chill_crud_3party_3party_view', { id : thirdparty.id }) }}"></a></span>
|
||||
{{ thirdparty }}
|
||||
</h1>
|
||||
|
||||
<h4>{{ 'thirdparty_duplicate.Data to keep'|trans ~ ':' }}</h4>
|
||||
{{ details.details(thirdparty) }}
|
||||
|
||||
{# <h4>{{ 'thirdparty_duplicate.links to keep'|trans ~ ':' }}</h4>#}
|
||||
{# {{ sidepane.links(thirdparty) }}#}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
<div class="col-md-12 centered">
|
||||
|
||||
<div class="container-fluid" style="padding-top: 1em;">
|
||||
<div class="clear" style="padding-top: 10px;">
|
||||
{{ form_widget(form.confirm) }}
|
||||
</div>
|
||||
<div class="col-11">
|
||||
{{ form_label(form.confirm) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('chill_thirdparty_find_duplicate', {thirdparty_id : thirdparty.id}) }}" class="btn btn-cancel">
|
||||
{{ 'Cancel'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ path('chill_thirdparty_duplicate_confirm', { thirdparty1_id : thirdparty2.id, thirdparty2_id : thirdparty.id }) }}"
|
||||
class="btn btn-action">
|
||||
<i class="fa fa-exchange"></i>
|
||||
{{ 'Invert'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-submit" type="submit"><i class="fa fa-cog fa-fw"></i>{{ 'Merge'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
</div></div>
|
||||
{% endblock %}
|
@@ -1,38 +0,0 @@
|
||||
{% extends "@ChillMain/layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_thirdparty_duplicate' %}
|
||||
|
||||
{% block title %}{{ 'thirdparty_duplicate.find'|trans ~ ' ' ~ thirdparty.name|capitalize }}{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="person-duplicate">
|
||||
|
||||
<h1>{{ 'thirdparty_duplicate.find'|trans }}</h1>
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_rest(form) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('chill_crud_3party_3party_view', {'id' : thirdparty.id}) }}" class="btn btn-cancel">
|
||||
{{ 'Return'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-submit" type="submit">{{ 'Next'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ encore_entry_script_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ encore_entry_link_tags('mod_pickentity_type') }}
|
||||
{% endblock %}
|
@@ -1,115 +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\ThirdPartyBundle\Service;
|
||||
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Doctrine\DBAL\Exception;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
|
||||
readonly class ThirdpartyMergeService
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $em) {}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function merge(ThirdParty $toKeep, ThirdParty $toDelete): void
|
||||
{
|
||||
$conn = $this->em->getConnection();
|
||||
$conn->beginTransaction();
|
||||
|
||||
try {
|
||||
$queries = [
|
||||
...$this->updateReferences($toKeep, $toDelete),
|
||||
...$this->removeThirdparty($toKeep, $toDelete),
|
||||
];
|
||||
|
||||
foreach ($queries as $query) {
|
||||
$conn->executeStatement($query['sql'], $query['params']);
|
||||
}
|
||||
|
||||
$conn->commit();
|
||||
} catch (\Exception $e) {
|
||||
$conn->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MappingException
|
||||
*/
|
||||
private function updateReferences(ThirdParty $toKeep, ThirdParty $toDelete): array
|
||||
{
|
||||
$queries = [];
|
||||
$allMeta = $this->em->getMetadataFactory()->getAllMetadata();
|
||||
|
||||
foreach ($allMeta as $meta) {
|
||||
if ($meta->isMappedSuperclass) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tableName = $meta->getTableName();
|
||||
foreach ($meta->getAssociationMappings() as $assoc) {
|
||||
if (ThirdParty::class !== $assoc['targetEntity']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// phpstan wants boolean for if condition
|
||||
if (($assoc['type'] & ClassMetadata::TO_ONE) !== 0) {
|
||||
$joinColumn = $meta->getSingleAssociationJoinColumnName($assoc['fieldName']);
|
||||
|
||||
$suffix = (ThirdParty::class === $assoc['sourceEntity']) ? 'chill_3party.' : '';
|
||||
|
||||
$queries[] = [
|
||||
'sql' => "UPDATE {$suffix}{$tableName} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete",
|
||||
'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()],
|
||||
];
|
||||
} elseif (ClassMetadata::MANY_TO_MANY === $assoc['type'] && isset($assoc['joinTable'])) {
|
||||
$joinTable = $assoc['joinTable']['name'];
|
||||
$prefix = null !== ($assoc['joinTable']['schema'] ?? null) ? $assoc['joinTable']['schema'].'.' : '';
|
||||
$joinColumn = $assoc['joinTable']['inverseJoinColumns'][0]['name'];
|
||||
$queries[] = [
|
||||
'sql' => "UPDATE {$prefix}{$joinTable} SET {$joinColumn} = :toKeep WHERE {$joinColumn} = :toDelete AND NOT EXISTS (SELECT 1 FROM {$prefix}{$joinTable} WHERE {$joinColumn} = :toKeep)",
|
||||
'params' => ['toDelete' => $toDelete->getId(), 'toKeep' => $toKeep->getId()],
|
||||
];
|
||||
|
||||
$queries[] = [
|
||||
'sql' => "DELETE FROM {$joinTable} WHERE {$joinColumn} = :toDelete",
|
||||
'params' => ['toDelete' => $toDelete->getId()],
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $queries;
|
||||
}
|
||||
|
||||
public function removeThirdparty(ThirdParty $toKeep, ThirdParty $toDelete): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'sql' => 'UPDATE chill_3party.third_party SET parent_id = :toKeep WHERE parent_id = :toDelete',
|
||||
'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()],
|
||||
],
|
||||
[
|
||||
'sql' => 'UPDATE chill_3party.thirdparty_category SET thirdparty_id = :toKeep WHERE thirdparty_id = :toDelete AND NOT EXISTS (SELECT 1 FROM chill_3party.thirdparty_category WHERE thirdparty_id = :toKeep)',
|
||||
'params' => ['toKeep' => $toKeep->getId(), 'toDelete' => $toDelete->getId()],
|
||||
],
|
||||
[
|
||||
'sql' => 'DELETE FROM chill_3party.third_party WHERE id = :toDelete',
|
||||
'params' => ['toDelete' => $toDelete->getId()],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@@ -39,7 +39,6 @@ class ThirdPartyRender implements ChillEntityRenderInterface
|
||||
'showContacts' => $options['showContacts'] ?? false,
|
||||
'showParent' => $options['showParent'] ?? true,
|
||||
'isConfidential' => $options['isConfidential'] ?? false,
|
||||
'showFusion' => $options['showFusion'] ?? false,
|
||||
];
|
||||
|
||||
return
|
||||
|
@@ -1,86 +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\ThirdPartyBundle\Tests\Service;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdPartyCategory;
|
||||
use Chill\ThirdPartyBundle\Service\ThirdpartyMergeService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class ThirdpartyMergeServiceTest extends KernelTestCase
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
private ThirdpartyMergeService $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
self::bootKernel();
|
||||
$this->em = self::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
$this->service = new ThirdpartyMergeService($this->em);
|
||||
}
|
||||
|
||||
public function testMergeUpdatesReferencesAndDeletesThirdparty(): void
|
||||
{
|
||||
// Create ThirdParty entities
|
||||
$toKeep = new ThirdParty();
|
||||
$toKeep->setName('Thirdparty to keep');
|
||||
$this->em->persist($toKeep);
|
||||
|
||||
$toDelete = new ThirdParty();
|
||||
$toDelete->setName('Thirdparty to delete');
|
||||
$this->em->persist($toDelete);
|
||||
|
||||
// Create a related entity with TO_ONE relation (thirdparty parent)
|
||||
$relatedToOneEntity = new ThirdParty();
|
||||
$relatedToOneEntity->setName('RelatedToOne thirdparty');
|
||||
$relatedToOneEntity->setParent($toDelete);
|
||||
$this->em->persist($relatedToOneEntity);
|
||||
|
||||
// Create a related entity with TO_MANY relation (thirdparty category)
|
||||
$thirdpartyCategory = new ThirdPartyCategory();
|
||||
$thirdpartyCategory->setName(['fr' => 'Thirdparty category']);
|
||||
$this->em->persist($thirdpartyCategory);
|
||||
$toDelete->addCategory($thirdpartyCategory);
|
||||
$this->em->persist($toDelete);
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->setDate(new \DateTime());
|
||||
$activity->addThirdParty($toDelete);
|
||||
$this->em->persist($activity);
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
// Run merge
|
||||
$this->service->merge($toKeep, $toDelete);
|
||||
$this->em->refresh($toKeep);
|
||||
$this->em->refresh($relatedToOneEntity);
|
||||
|
||||
// Check that references were updated
|
||||
$this->assertEquals($toKeep->getId(), $relatedToOneEntity->getParent()->getId(), 'The parent thirdparty was succesfully merged');
|
||||
|
||||
$updatedRelatedManyEntity = $this->em->find(ThirdPartyCategory::class, $thirdpartyCategory->getId());
|
||||
$this->assertContains($updatedRelatedManyEntity, $toKeep->getCategories(), 'The thirdparty category was found in the toKeep entity');
|
||||
|
||||
// Check that toDelete was removed
|
||||
$this->em->clear();
|
||||
$deletedThirdParty = $this->em->find(ThirdParty::class, $toDelete->getId());
|
||||
$this->assertNull($deletedThirdParty);
|
||||
}
|
||||
}
|
@@ -1,14 +1,14 @@
|
||||
---
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
Chill\ThirdPartyBundle\Serializer\Normalizer\:
|
||||
autowire: true
|
||||
resource: '../Serializer/Normalizer/'
|
||||
tags:
|
||||
- { name: 'serializer.normalizer', priority: 64 }
|
||||
|
||||
Chill\ThirdPartyBundle\Export\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Export/'
|
||||
|
||||
Chill\ThirdPartyBundle\Validator\:
|
||||
@@ -16,5 +16,3 @@ services:
|
||||
autowire: true
|
||||
resource: '../Validator/'
|
||||
|
||||
Chill\ThirdPartyBundle\Service\ThirdpartyMergeService: ~
|
||||
|
||||
|
@@ -135,6 +135,7 @@ is thirdparty: Le demandeur est un tiers
|
||||
Filter by person's who have a residential address located at a thirdparty of type: Filtrer les usagers qui ont une addresse de résidence chez un tiers
|
||||
"Filtered by person's who have a residential address located at a thirdparty of type %thirdparty_type% and valid on %date_calc%": "Uniquement les usagers qui ont une addresse de résidence chez un tiers de catégorie %thirdparty_type% et valide sur la date %date_calc%"
|
||||
|
||||
|
||||
# admin
|
||||
admin:
|
||||
export_description: Liste des tiers (format CSV)
|
||||
@@ -154,16 +155,3 @@ Telephone2: Autre téléphone
|
||||
Contact email: Courrier électronique du contact
|
||||
Contact address: Adresse du contact
|
||||
Contact profession: Profession du contact
|
||||
|
||||
thirdparty_duplicate:
|
||||
title: Fusionner les tiers doublons
|
||||
find: Désigner un tiers doublon
|
||||
Thirdparty to keep: Tiers à conserver
|
||||
Thirdparty to delete: Tiers à supprimer
|
||||
Thirdparty to delete explanation: Ce tiers sera supprimé. Seuls les contacts de ce tiers, énumérés ci-dessous, seront transférés.
|
||||
Thirdparty to keep explanation: Ce tiers sera conservé
|
||||
Data to keep: Données conservées
|
||||
You cannot merge a thirdparty with itself. Please choose a different thirdparty: Vous ne pouvez pas fusionner un tiers avec lui-même. Veuillez choisir un autre tiers.
|
||||
A thirdparty can only be merged with a thirdparty of the same kind: Un tiers ne peut être fusionné qu'avec un tiers de même type.
|
||||
Two child thirdparties must have the same parent: Deux tiers de type « contact » doivent avoir le même tiers parent.
|
||||
Merge successful: La fusion a été effectuée avec succès
|
||||
|
Reference in New Issue
Block a user