mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-09-21 14:14:58 +00:00
Compare commits
6 Commits
1.5-branch
...
1.5-branch
Author | SHA1 | Date | |
---|---|---|---|
a79a17b39e | |||
07aad4c7aa | |||
6d2fa87083 | |||
e914abcb6f | |||
c2cfa19e43 | |||
03fb75edc8 |
@@ -29,6 +29,7 @@ use Chill\MainBundle\Entity\HasCenterInterface;
|
|||||||
use Chill\MainBundle\Entity\HasScopeInterface;
|
use Chill\MainBundle\Entity\HasScopeInterface;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistency;
|
use Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistency;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity
|
* Activity
|
||||||
@@ -72,6 +73,8 @@ class Activity implements HasCenterInterface, HasScopeInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @var \Doctrine\Common\Collections\Collection
|
* @var \Doctrine\Common\Collections\Collection
|
||||||
|
*
|
||||||
|
* @Assert\Count(min=1, minMessage="Add at least one reason")
|
||||||
*/
|
*/
|
||||||
private $reasons;
|
private $reasons;
|
||||||
|
|
||||||
|
@@ -99,11 +99,11 @@ class ActivityType extends AbstractType
|
|||||||
))
|
))
|
||||||
->add('attendee', ChoiceType::class, array(
|
->add('attendee', ChoiceType::class, array(
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'required' => false,
|
'required' => true,
|
||||||
'choices_as_values' => true,
|
'choices_as_values' => true,
|
||||||
'choices' => array(
|
'choices' => array(
|
||||||
'present' => true,
|
'yes' => true,
|
||||||
'not present' => false
|
'no' => false
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
->add('user', UserPickerType::class, [
|
->add('user', UserPickerType::class, [
|
||||||
|
@@ -1,2 +1,3 @@
|
|||||||
The reasons's level should not be empty: Le niveau du sujet ne peut pas être vide
|
The reasons's level should not be empty: Le niveau du sujet ne peut pas être vide
|
||||||
At least one reason must be choosen: Au moins un sujet doit être choisi
|
At least one reason must be choosen: Au moins un sujet doit être choisi
|
||||||
|
Add at least one reason: L'activité doit comporter au moins un sujet
|
||||||
|
@@ -139,5 +139,9 @@ class ActionEvent extends Event
|
|||||||
{
|
{
|
||||||
return $this->metadata;
|
return $this->metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setSQLStatement(string $sqlStatement)
|
||||||
|
{
|
||||||
|
$this->sqlStatement = $sqlStatement;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Chill\PersonBundle\Export\Export;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Center;
|
||||||
|
use Chill\MainBundle\Export\DirectExportInterface;
|
||||||
|
use Chill\MainBundle\Export\ExportElementValidatedInterface;
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||||
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
|
use Chill\PersonBundle\Security\Authorization\PersonVoter;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
use Symfony\Component\Security\Core\Role\Role;
|
||||||
|
use Symfony\Component\Translation\TranslatorInterface;
|
||||||
|
use Symfony\Component\Validator\Constraints\Range;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a list of duplicate peoples
|
||||||
|
*/
|
||||||
|
class ListPersonDuplicate implements DirectExportInterface, ExportElementValidatedInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @var EntityManagerInterface
|
||||||
|
*/
|
||||||
|
protected $entityManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Symfony\Component\Translation\TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Symfony\Component\Routing\Generator\UrlGeneratorInterface
|
||||||
|
*/
|
||||||
|
private $router;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
private const PRECISION_DEFAULT_VALUE = 0.7;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
EntityManagerInterface $em,
|
||||||
|
TranslatorInterface $translator,
|
||||||
|
UrlGeneratorInterface $router
|
||||||
|
) {
|
||||||
|
$this->entityManager = $em;
|
||||||
|
$this->translator = $translator;
|
||||||
|
$this->router = $router;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTitle()
|
||||||
|
{
|
||||||
|
return "List duplicates";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getDescription()
|
||||||
|
{
|
||||||
|
return "Create a list of duplicate people.";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*
|
||||||
|
* @param FormBuilderInterface $builder
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
$builder->add('precision', NumberType::class, [
|
||||||
|
'label' => 'Duplicate similarity',
|
||||||
|
'data' => self::PRECISION_DEFAULT_VALUE,
|
||||||
|
//'help' => 'Le degré de similarité mesure le nombre de caractères similaires entre la graphie des noms et prénoms. Un nombre faible indique que des graphies très différentes seront considérées comme similaires.',
|
||||||
|
'constraints' => [
|
||||||
|
new Range([
|
||||||
|
'min' => 0.1,
|
||||||
|
'max' => 1.0,
|
||||||
|
])
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateForm($data, ExecutionContextInterface $context)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function generate(array $acl, array $data = []): Response
|
||||||
|
{
|
||||||
|
$values = [];
|
||||||
|
$values[] = $this->getHeaders();
|
||||||
|
|
||||||
|
$result = $this->getResult($acl, $data);
|
||||||
|
foreach ($result as $row) {
|
||||||
|
$values[] = [
|
||||||
|
$row['id1'],
|
||||||
|
$row['firstname1'],
|
||||||
|
$row['lastname1'],
|
||||||
|
$this->router->generate('chill_person_view', ['person_id' => $row['id1']], UrlGeneratorInterface::ABSOLUTE_URL),
|
||||||
|
$row['id2'],
|
||||||
|
$row['firstname2'],
|
||||||
|
$row['lastname2'],
|
||||||
|
$this->router->generate('chill_person_view', ['person_id' => $row['id2']], UrlGeneratorInterface::ABSOLUTE_URL),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
$spreadsheet->getActiveSheet()->fromArray($values);
|
||||||
|
|
||||||
|
// Make links clickable
|
||||||
|
for ($i = 1; $i <= $spreadsheet->getActiveSheet()->getHighestDataRow(); $i++) {
|
||||||
|
$spreadsheet->getActiveSheet()->getCell('D'.$i)->getHyperlink()
|
||||||
|
->setUrl($spreadsheet->getActiveSheet()->getCell('D'.$i)->getValue());
|
||||||
|
$spreadsheet->getActiveSheet()->getCell('H'.$i)->getHyperlink()
|
||||||
|
->setUrl($spreadsheet->getActiveSheet()->getCell('H'.$i)->getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
$writer = new Xlsx($spreadsheet);
|
||||||
|
$temp_file = sys_get_temp_dir().'/'.uniqid('export_').'.xlsx';
|
||||||
|
$writer->save($temp_file);
|
||||||
|
|
||||||
|
$response = new BinaryFileResponse($temp_file);
|
||||||
|
$response->headers->set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||||
|
$response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'export_duplicate.xlsx');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getResult($acl, $data = [])
|
||||||
|
{
|
||||||
|
$precision = $data['precision'] ?? self::PRECISION_DEFAULT_VALUE;
|
||||||
|
|
||||||
|
$baseSql = 'SELECT
|
||||||
|
p.id as id1, p.firstname as firstname1, p.lastname as lastname1, p.fullnamecanonical as fullnamecanonical1,
|
||||||
|
p2.id as id2, p2.firstname as firstname2, p2.lastname as lastname2, p2.fullnamecanonical as fullnamecanonical2,
|
||||||
|
SIMILARITY(p.fullnamecanonical, p2.fullnamecanonical) AS "similarity nom + prenom",
|
||||||
|
SIMILARITY(p.lastname, p2.lastname) AS "similarity nom",
|
||||||
|
SIMILARITY(p.firstname, p2.firstname) AS "similarity prenom"
|
||||||
|
FROM chill_person_person AS p
|
||||||
|
JOIN chill_person_person AS p2
|
||||||
|
ON p.id < p2.id
|
||||||
|
AND (
|
||||||
|
SIMILARITY(p.fullnamecanonical, p2.fullnamecanonical) > ?
|
||||||
|
OR (UNACCENT(LOWER(p.firstname)) = UNACCENT(LOWER(p2.lastname))
|
||||||
|
AND UNACCENT(LOWER(p.lastname)) = UNACCENT(LOWER(p2.firstname)))
|
||||||
|
)
|
||||||
|
AND p.birthdate = p2.birthdate
|
||||||
|
AND p.center_id = p2.center_id
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT id
|
||||||
|
FROM chill_person_not_duplicate as pnd
|
||||||
|
WHERE (pnd.person1_id = p.id
|
||||||
|
AND pnd.person2_id = p2.id)
|
||||||
|
OR (pnd.person2_id = p.id
|
||||||
|
AND pnd.person1_id = p2.id)
|
||||||
|
)
|
||||||
|
AND p.center_id IN (%centers%)
|
||||||
|
ORDER BY p.fullnamecanonical, p.id, p2.id';
|
||||||
|
|
||||||
|
$centersId = array_map(function ($r) { return $r['center']->getId(); }, $acl);
|
||||||
|
$sql = strtr($baseSql, ['%centers%' => implode(', ', array_fill(0, count($centersId), '?'))]);
|
||||||
|
|
||||||
|
$statement = $this->entityManager->getConnection()->prepare($sql);
|
||||||
|
return $statement->executeQuery([$precision, ...$centersId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaders(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
$this->translator->trans('Person folder number'),
|
||||||
|
$this->translator->trans('Last name'),
|
||||||
|
$this->translator->trans('First name'),
|
||||||
|
$this->translator->trans('Open person folder'),
|
||||||
|
$this->translator->trans('Person folder number'),
|
||||||
|
$this->translator->trans('Last name'),
|
||||||
|
$this->translator->trans('First name'),
|
||||||
|
$this->translator->trans('Open person folder'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function requiredRole(): Role
|
||||||
|
{
|
||||||
|
return new Role(PersonVoter::DUPLICATE);
|
||||||
|
}
|
||||||
|
}
|
@@ -15,7 +15,16 @@ services:
|
|||||||
- "@chill.custom_field.provider"
|
- "@chill.custom_field.provider"
|
||||||
tags:
|
tags:
|
||||||
- { name: chill.export, alias: list_person }
|
- { name: chill.export, alias: list_person }
|
||||||
|
|
||||||
|
chill.person.export.list_person.duplicate:
|
||||||
|
class: Chill\PersonBundle\Export\Export\ListPersonDuplicate
|
||||||
|
arguments:
|
||||||
|
- "@doctrine.orm.entity_manager"
|
||||||
|
- "@translator"
|
||||||
|
- "@router"
|
||||||
|
tags:
|
||||||
|
- { name: chill.export, alias: list_person_duplicate }
|
||||||
|
|
||||||
chill.person.export.filter_gender:
|
chill.person.export.filter_gender:
|
||||||
class: Chill\PersonBundle\Export\Filter\GenderFilter
|
class: Chill\PersonBundle\Export\Filter\GenderFilter
|
||||||
arguments:
|
arguments:
|
||||||
|
@@ -208,6 +208,12 @@ List peoples: Liste des personnes
|
|||||||
Create a list of people according to various filters.: Crée une liste des personnes selon différents filtres.
|
Create a list of people according to various filters.: Crée une liste des personnes selon différents filtres.
|
||||||
Fields to include in export: Champs à inclure dans l'export
|
Fields to include in export: Champs à inclure dans l'export
|
||||||
Address valid at this date: Addresse valide à cette date
|
Address valid at this date: Addresse valide à cette date
|
||||||
|
### export duplicate
|
||||||
|
List duplicates: Liste des doublons potentiels
|
||||||
|
Create a list of duplicate people.: Liste des doublons potentiels, sur base du degré de similarité du nom et prénom des personnes
|
||||||
|
Person folder number: Identifiant du dossier
|
||||||
|
Open person folder: Ouvrir le dossier
|
||||||
|
Duplicate similarity: Degré de similarité des noms et prénoms
|
||||||
|
|
||||||
## filters
|
## filters
|
||||||
Filter by person gender: Filtrer par genre de la personne
|
Filter by person gender: Filtrer par genre de la personne
|
||||||
|
@@ -31,6 +31,10 @@
|
|||||||
{{ form_row(form.streetAddress2) }}
|
{{ form_row(form.streetAddress2) }}
|
||||||
{{ form_row(form.postCode) }}
|
{{ form_row(form.postCode) }}
|
||||||
{{ form_row(form.validFrom) }}
|
{{ form_row(form.validFrom) }}
|
||||||
|
|
||||||
|
{% if form.customs is defined %}
|
||||||
|
{{ form_widget(form.customs) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
|
@@ -35,7 +35,11 @@
|
|||||||
{{ form_errors(form.postCode) }}
|
{{ form_errors(form.postCode) }}
|
||||||
{{ form_row(form.validFrom) }}
|
{{ form_row(form.validFrom) }}
|
||||||
{{ form_errors(form.validFrom) }}
|
{{ form_errors(form.validFrom) }}
|
||||||
|
|
||||||
|
{% if form.customs is defined %}
|
||||||
|
{{ form_widget(form.customs) }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<ul class="record_actions sticky-form-buttons">
|
<ul class="record_actions sticky-form-buttons">
|
||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
|
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
|
||||||
|
@@ -161,7 +161,8 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
|
|||||||
'p.firstName',
|
'p.firstName',
|
||||||
$qb->expr()->literal(' '),
|
$qb->expr()->literal(' '),
|
||||||
'p.lastName'
|
'p.lastName'
|
||||||
).'AS text'
|
).'AS text',
|
||||||
|
'p.birthdate'
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
$qb->select('p');
|
$qb->select('p');
|
||||||
@@ -178,7 +179,15 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
|
|||||||
->addOrderBy('p.lastName');
|
->addOrderBy('p.lastName');
|
||||||
|
|
||||||
if ($options['simplify'] ?? false) {
|
if ($options['simplify'] ?? false) {
|
||||||
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
|
return \array_map(
|
||||||
|
function($row) {
|
||||||
|
$row['text'] .= ' ('.($row['birthdate'] instanceof \DateTimeInterface ? $row['birthdate']->format('d/m/Y').' - ' : '').'dossier n°'.$row['id'].')';
|
||||||
|
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
,
|
||||||
|
$qb->getQuery()->getResult(Query::HYDRATE_ARRAY)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user