mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'features/household-validation' into features/household-edit-members-forms-improve-household
This commit is contained in:
commit
9f3cd943cb
@ -0,0 +1,10 @@
|
||||
/*
|
||||
* when an alert is the first child of the page, with a banner, we do not want the alert to be merged with the banner
|
||||
*/
|
||||
div.container.content {
|
||||
& > div {
|
||||
div.alert:nth-child(2) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
|
||||
div.alert.alert-with-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
ul.record_actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
li:nth-child(1n+2) {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1050px) {
|
||||
flex-direction: column;
|
||||
|
||||
ul.record_actions {
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,10 @@
|
||||
*/
|
||||
|
||||
|
||||
@import 'alert-first-child';
|
||||
@import 'alert-with-actions';
|
||||
|
||||
|
||||
/* [hack] /!\ Contourne le positionnement problématique du div#content_conainter suivant,
|
||||
* car sa position: relative le place au-dessus du bandeau et les liens sont incliquables */
|
||||
div.subheader {
|
||||
|
@ -164,9 +164,11 @@
|
||||
{{ encore_entry_script_tags('ckeditor5') }}
|
||||
{% endif %}
|
||||
<script type="text/javascript">
|
||||
window.addEventListener('DOMContentLoaded', function(e) {
|
||||
chill.checkOtherValueOnChange();
|
||||
$('.select2').select2({allowClear: true});
|
||||
chill.categoryLinkParentChildSelect();
|
||||
});
|
||||
</script>
|
||||
{% block js%}<!-- nothing added to js -->{% endblock %}
|
||||
</body>
|
||||
|
216
src/Bundle/ChillMainBundle/Tests/Util/DateRangeCoveringTest.php
Normal file
216
src/Bundle/ChillMainBundle/Tests/Util/DateRangeCoveringTest.php
Normal file
@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Tests\Util;
|
||||
|
||||
use Chill\MainBundle\Util\DateRangeCovering;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class DateRangeCoveringTest extends TestCase
|
||||
{
|
||||
|
||||
public function testCoveringWithMinCover1()
|
||||
{
|
||||
$cover = new DateRangeCovering(1, new \DateTimeZone('Europe/Brussels'));
|
||||
$cover
|
||||
->add(new \DateTime('2010-01-01'), new \DateTime('2010-12-01'), 1)
|
||||
->add(new \DateTime('2010-06-01'), new \DateTime('2011-06-01'), 2)
|
||||
->add(new \DateTime('2019-06-01'), new \DateTime('2019-06-01'), 3)
|
||||
->compute()
|
||||
;
|
||||
|
||||
$this->assertTrue($cover->hasIntersections());
|
||||
$this->assertIsArray($cover->getIntersections());
|
||||
$this->assertCount(1, $cover->getIntersections());
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-06-01'),
|
||||
$cover->getIntersections()[0][0],
|
||||
"assert date start are the intersection"
|
||||
);
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-12-01'),
|
||||
$cover->getIntersections()[0][1],
|
||||
"assert date end are the intersection"
|
||||
);
|
||||
$this->assertIsArray($cover->getIntersections()[0][2]);
|
||||
$this->assertContains(1, $cover->getIntersections()[0][2]);
|
||||
$this->assertContains(2, $cover->getIntersections()[0][2]);
|
||||
$this->assertNotContains(3, $cover->getIntersections()[0][2]);
|
||||
}
|
||||
|
||||
public function testCoveringWithMinCover1WithTwoIntersections()
|
||||
{
|
||||
$cover = new DateRangeCovering(1, new \DateTimeZone('Europe/Brussels'));
|
||||
$cover
|
||||
->add(new \DateTime('2010-01-01'), new \DateTime('2010-12-01'), 1)
|
||||
->add(new \DateTime('2010-06-01'), new \DateTime('2011-06-01'), 2)
|
||||
->add(new \DateTime('2019-01-01'), new \DateTime('2019-12-01'), 3)
|
||||
->add(new \DateTime('2019-06-01'), new \DateTime('2020-06-01'), 4)
|
||||
->compute()
|
||||
;
|
||||
|
||||
$this->assertTrue($cover->hasIntersections());
|
||||
$this->assertIsArray($cover->getIntersections());
|
||||
$this->assertCount(2, $cover->getIntersections());
|
||||
|
||||
$intersections = $cover->getIntersections();
|
||||
|
||||
// sort the intersections to compare them in expected order
|
||||
\usort($intersections, function($a, $b) {
|
||||
if ($a[0] === $b[0]) {
|
||||
return $a[1] <=> $b[1];
|
||||
}
|
||||
|
||||
return $a[0] <=> $b[0];
|
||||
});
|
||||
|
||||
// first intersection
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-06-01'),
|
||||
$intersections[0][0],
|
||||
"assert date start are the intersection"
|
||||
);
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-12-01'),
|
||||
$intersections[0][1],
|
||||
"assert date end are the intersection"
|
||||
);
|
||||
$this->assertIsArray($intersections[0][2]);
|
||||
$this->assertContains(1, $intersections[0][2]);
|
||||
$this->assertContains(2, $intersections[0][2]);
|
||||
$this->assertNotContains(3, $intersections[0][2]);
|
||||
$this->assertNotContains(4, $intersections[0][2]);
|
||||
|
||||
// second intersection
|
||||
$this->assertEquals(
|
||||
new \DateTime('2019-06-01'),
|
||||
$intersections[1][0],
|
||||
"assert date start are the intersection"
|
||||
);
|
||||
$this->assertEquals(
|
||||
new \DateTime('2019-12-01'),
|
||||
$intersections[1][1],
|
||||
"assert date end are the intersection"
|
||||
);
|
||||
$this->assertIsArray($intersections[1][2]);
|
||||
$this->assertContains(3, $intersections[1][2]);
|
||||
$this->assertContains(4, $intersections[1][2]);
|
||||
$this->assertNotContains(1, $intersections[1][2]);
|
||||
$this->assertNotContains(2, $intersections[1][2]);
|
||||
}
|
||||
|
||||
public function testCoveringWithMinCover2()
|
||||
{
|
||||
$cover = new DateRangeCovering(2, new \DateTimeZone('Europe/Brussels'));
|
||||
$cover
|
||||
->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), 1)
|
||||
->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), 2)
|
||||
->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), 3)
|
||||
->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), 4)
|
||||
->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), 5)
|
||||
->compute()
|
||||
;
|
||||
$this->assertTrue($cover->hasIntersections());
|
||||
$this->assertIsArray($cover->getIntersections());
|
||||
$this->assertCount(1, $cover->getIntersections());
|
||||
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-06-01'),
|
||||
$cover->getIntersections()[0][0],
|
||||
"assert date start are the intersection"
|
||||
);
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-09-01'),
|
||||
$cover->getIntersections()[0][1],
|
||||
"assert date end are the intersection"
|
||||
);
|
||||
$this->assertIsArray($cover->getIntersections()[0][2]);
|
||||
$this->assertContains(1, $cover->getIntersections()[0][2]);
|
||||
$this->assertContains(2, $cover->getIntersections()[0][2]);
|
||||
$this->assertContains(3, $cover->getIntersections()[0][2]);
|
||||
$this->assertNotContains(4, $cover->getIntersections()[0][2]);
|
||||
$this->assertNotContains(5, $cover->getIntersections()[0][2]);
|
||||
}
|
||||
|
||||
public function testCoveringWithMinCover2AndThreePeriodsCovering()
|
||||
{
|
||||
$cover = new DateRangeCovering(2, new \DateTimeZone('Europe/Brussels'));
|
||||
$cover
|
||||
->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), 1)
|
||||
->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), 2)
|
||||
->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), 3)
|
||||
->add(new \DateTime('2009-01-01'), new \DateTime('2010-09-15'), 4)
|
||||
->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), 5)
|
||||
->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), 6)
|
||||
->compute()
|
||||
;
|
||||
|
||||
$this->assertTrue($cover->hasIntersections());
|
||||
$this->assertIsArray($cover->getIntersections());
|
||||
$this->assertCount(1, $cover->getIntersections());
|
||||
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-04-01'),
|
||||
$cover->getIntersections()[0][0],
|
||||
"assert date start are the intersection"
|
||||
);
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-09-15'),
|
||||
$cover->getIntersections()[0][1],
|
||||
"assert date end are the intersection"
|
||||
);
|
||||
$this->assertIsArray($cover->getIntersections()[0][2]);
|
||||
$this->assertContains(1, $cover->getIntersections()[0][2]);
|
||||
$this->assertContains(2, $cover->getIntersections()[0][2]);
|
||||
$this->assertContains(3, $cover->getIntersections()[0][2]);
|
||||
$this->assertContains(4, $cover->getIntersections()[0][2]);
|
||||
$this->assertNotContains(5, $cover->getIntersections()[0][2]);
|
||||
$this->assertNotContains(6, $cover->getIntersections()[0][2]);
|
||||
}
|
||||
|
||||
public function testCoveringWithMinCover2AndThreePeriodsCoveringWithNullMetadata()
|
||||
{
|
||||
$cover = new DateRangeCovering(2, new \DateTimeZone('Europe/Brussels'));
|
||||
$cover
|
||||
->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), null)
|
||||
->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), null)
|
||||
->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), null)
|
||||
->add(new \DateTime('2009-01-01'), new \DateTime('2010-09-15'), null)
|
||||
->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), null)
|
||||
->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), null)
|
||||
->compute()
|
||||
;
|
||||
|
||||
$this->assertTrue($cover->hasIntersections());
|
||||
$this->assertIsArray($cover->getIntersections());
|
||||
$this->assertCount(1, $cover->getIntersections());
|
||||
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-04-01'),
|
||||
$cover->getIntersections()[0][0],
|
||||
"assert date start are the intersection"
|
||||
);
|
||||
$this->assertEquals(
|
||||
new \DateTime('2010-09-15'),
|
||||
$cover->getIntersections()[0][1],
|
||||
"assert date end are the intersection"
|
||||
);
|
||||
$this->assertIsArray($cover->getIntersections()[0][2]);
|
||||
}
|
||||
|
||||
|
||||
public function testCoveringWithMinCover3Absent()
|
||||
{
|
||||
$cover = new DateRangeCovering(3, new \DateTimeZone('Europe/Brussels'));
|
||||
$cover
|
||||
->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), 1)
|
||||
->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), 2)
|
||||
->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), 3)
|
||||
->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), 4)
|
||||
->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), 5)
|
||||
->compute()
|
||||
;
|
||||
$this->assertFalse($cover->hasIntersections());
|
||||
$this->assertIsArray($cover->getIntersections());
|
||||
$this->assertCount(0, $cover->getIntersections());
|
||||
}
|
||||
}
|
224
src/Bundle/ChillMainBundle/Util/DateRangeCovering.php
Normal file
224
src/Bundle/ChillMainBundle/Util/DateRangeCovering.php
Normal file
@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Util;
|
||||
|
||||
/**
|
||||
* Utilities to compare date periods
|
||||
*
|
||||
* This class allow to compare periods when there are period covering. The
|
||||
* argument `minCovers` allow to find also when there are more than 2 period
|
||||
* which intersects.
|
||||
*
|
||||
* Example: a team may have maximum 2 leaders on a same period: you will
|
||||
* find here all periods where there are more than 2 leaders.
|
||||
*
|
||||
* Usage:
|
||||
*
|
||||
* ```php
|
||||
* $cover = new DateRangeCovering(2); // 2 means we will have periods
|
||||
* // when there are 2+ periods intersecting
|
||||
* $cover
|
||||
* ->add(new \DateTime('2010-01-01'), new \DateTime('2010-10-01'), null)
|
||||
* ->add(new \DateTime('2010-06-01'), new \DateTime('2010-09-01'), null)
|
||||
* ->add(new \DateTime('2010-04-01'), new \DateTime('2010-12-01'), null)
|
||||
* ->add(new \DateTime('2009-01-01'), new \DateTime('2010-09-15'), null)
|
||||
* ->add(new \DateTime('2019-01-01'), new \DateTime('2019-10-01'), null)
|
||||
* ->add(new \DateTime('2019-06-01'), new \DateTime('2019-09-01'), null)
|
||||
* ->compute()
|
||||
* ;
|
||||
* $cover->getIntersections();
|
||||
* ```
|
||||
*/
|
||||
class DateRangeCovering
|
||||
{
|
||||
private bool $computed = false;
|
||||
|
||||
private array $intersections = [];
|
||||
|
||||
private array $intervals = [];
|
||||
|
||||
private int $minCover;
|
||||
|
||||
private int $uniqueKeyCounter = 0;
|
||||
|
||||
private array $metadatas = [];
|
||||
|
||||
private array $sequence = [];
|
||||
|
||||
private \DateTimeZone $tz;
|
||||
|
||||
/**
|
||||
* @param int $minCover the minimum of covering required
|
||||
*/
|
||||
public function __construct(int $minCover, \DateTimeZone $tz)
|
||||
{
|
||||
if ($minCover < 0) {
|
||||
throw new \LogicException("argument minCover cannot be lower than 0");
|
||||
}
|
||||
|
||||
$this->minCover = $minCover;
|
||||
$this->tz = $tz;
|
||||
}
|
||||
|
||||
public function add(\DateTimeInterface $start, \DateTimeInterface $end = null, $metadata = null): self
|
||||
{
|
||||
if ($this->computed) {
|
||||
throw new \LogicException("You cannot add intervals to a computed instance");
|
||||
}
|
||||
|
||||
$k = $this->uniqueKeyCounter++;
|
||||
$this->intervals[$k] = [$start, $end];
|
||||
$this->metadatas[$k] = $metadata;
|
||||
|
||||
$this->addToSequence($start->getTimestamp(), $k, null);
|
||||
$this->addToSequence(
|
||||
NULL === $end ? PHP_INT_MAX : $end->getTimestamp(), null, $k
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function addToSequence($timestamp, int $start = null, int $end = null)
|
||||
{
|
||||
if (!\array_key_exists($timestamp, $this->sequence)) {
|
||||
$this->sequence[$timestamp] = [ 's' => [], 'e' => [] ];
|
||||
}
|
||||
|
||||
if (NULL !== $start) {
|
||||
$this->sequence[$timestamp]['s'][] = $start;
|
||||
}
|
||||
if (NULL !== $end) {
|
||||
$this->sequence[$timestamp]['e'][] = $end;
|
||||
}
|
||||
}
|
||||
|
||||
public function compute(): self
|
||||
{
|
||||
\ksort($this->sequence);
|
||||
|
||||
$currentPeriod = [];
|
||||
$currents = [];
|
||||
$isOpen = false;
|
||||
$overs = [];
|
||||
|
||||
foreach ($this->sequence as $ts => $moves) {
|
||||
$currents = \array_merge($currents, $moves['s']);
|
||||
$currents = \array_diff($currents, $moves['e']);
|
||||
|
||||
if (count($currents) > $this->minCover && !$isOpen) {
|
||||
$currentPeriod[0] = $ts;
|
||||
$currentPeriod[2] = $currents;
|
||||
$isOpen = true;
|
||||
} elseif ($isOpen && count($currents) <= $this->minCover) {
|
||||
$currentPeriod[1] = $ts;
|
||||
$overs[] = $currentPeriod;
|
||||
$currentPeriod = [];
|
||||
$isOpen = false;
|
||||
} elseif ($isOpen) {
|
||||
$currentPeriod[2] = \array_merge($currentPeriod[2], $currents);
|
||||
}
|
||||
}
|
||||
|
||||
// process metadata
|
||||
foreach ($overs as list($start, $end, $metadata)) {
|
||||
$this->intersections[] = [
|
||||
(new \DateTimeImmutable('@'.$start))
|
||||
->setTimezone($this->tz),
|
||||
$end === PHP_INT_MAX ? null : (new \DateTimeImmutable('@'.$end))
|
||||
->setTimezone($this->tz),
|
||||
\array_values(
|
||||
\array_intersect_key(
|
||||
$this->metadatas,
|
||||
\array_flip(\array_unique($metadata))
|
||||
)
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
$this->computed = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function process(array $intersections): array
|
||||
{
|
||||
$result = [];
|
||||
$starts = [];
|
||||
$ends = [];
|
||||
$metadatas = [];
|
||||
|
||||
while (null !== ($current = \array_pop($intersections))) {
|
||||
list($cStart, $cEnd, $cMetadata) = $current;
|
||||
$n = count($cMetadata);
|
||||
|
||||
foreach ($intersections as list($iStart, $iEnd, $iMetadata)) {
|
||||
$start = max($cStart, $iStart);
|
||||
$end = min($cEnd, $iEnd);
|
||||
|
||||
if ($start <= $end) {
|
||||
if (FALSE !== ($key = \array_search($start, $starts))) {
|
||||
if ($ends[$key] === $end) {
|
||||
$metadatas[$key] = \array_unique(\array_merge($metadatas[$key], $iMetadata));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$starts[] = $start;
|
||||
$ends[] = $end;
|
||||
$metadatas[] = \array_unique(\array_merge($iMetadata, $cMetadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recompose results
|
||||
foreach ($starts as $k => $start) {
|
||||
$result[] = [$start, $ends[$k], \array_unique($metadatas[$k])];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function addToIntersections(array $intersections, array $intersection)
|
||||
{
|
||||
$foundExisting = false;
|
||||
list($nStart, $nEnd, $nMetadata) = $intersection;
|
||||
|
||||
\array_walk($intersections,
|
||||
function(&$i, $key) use ($nStart, $nEnd, $nMetadata, $foundExisting) {
|
||||
if ($foundExisting) {
|
||||
return;
|
||||
};
|
||||
if ($i[0] === $nStart && $i[1] === $nEnd) {
|
||||
$foundExisting = true;
|
||||
$i[2] = \array_merge($i[2], $nMetadata);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!$foundExisting) {
|
||||
$intersections[] = $intersection;
|
||||
}
|
||||
|
||||
return $intersections;
|
||||
}
|
||||
|
||||
public function hasIntersections(): bool
|
||||
{
|
||||
if (!$this->computed) {
|
||||
throw new \LogicException(sprintf("You cannot call the method %s before ".
|
||||
"'process'", __METHOD));
|
||||
}
|
||||
|
||||
return count($this->intersections) > 0;
|
||||
}
|
||||
|
||||
public function getIntersections(): array
|
||||
{
|
||||
if (!$this->computed) {
|
||||
throw new \LogicException(sprintf("You cannot call the method %s before ".
|
||||
"'process'", __METHOD));
|
||||
}
|
||||
|
||||
return $this->intersections;
|
||||
}
|
||||
|
||||
}
|
@ -86,8 +86,19 @@ class AccompanyingCourseController extends Controller
|
||||
*/
|
||||
public function indexAction(AccompanyingPeriod $accompanyingCourse): Response
|
||||
{
|
||||
// compute some warnings
|
||||
// get persons without household
|
||||
$withoutHousehold = [];
|
||||
foreach ($accompanyingCourse->getParticipations() as
|
||||
$p) {
|
||||
if (FALSE === $p->getPerson()->isSharingHousehold()) {
|
||||
$withoutHousehold[] = $p->getPerson();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [
|
||||
'accompanyingCourse' => $accompanyingCourse
|
||||
'accompanyingCourse' => $accompanyingCourse,
|
||||
'withoutHousehold' => $withoutHousehold
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,13 @@
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\PersonBundle\Form\HouseholdType;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Household\Position;
|
||||
@ -16,6 +19,16 @@ use Chill\PersonBundle\Entity\Household\Position;
|
||||
*/
|
||||
class HouseholdController extends AbstractController
|
||||
{
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
/**
|
||||
* @param TranslatorInterface $translator
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(
|
||||
* "/{household_id}/summary",
|
||||
@ -66,10 +79,17 @@ class HouseholdController extends AbstractController
|
||||
// some queries
|
||||
$household->getMembers()->initialize();
|
||||
|
||||
if ($request->query->has('edit')) {
|
||||
$form = $this->createMetadataForm($household);
|
||||
} else {
|
||||
$form = null;
|
||||
}
|
||||
|
||||
return $this->render('@ChillPerson/Household/members.html.twig',
|
||||
[
|
||||
'household' => $household,
|
||||
'positions' => $positions
|
||||
'positions' => $positions,
|
||||
'form' => NULL !== $form ? $form->createView(): $form
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -143,4 +163,53 @@ class HouseholdController extends AbstractController
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(
|
||||
* "/{household_id}/members/metadata/edit",
|
||||
* name="chill_person_household_members_metadata_edit",
|
||||
* methods={"GET", "POST"}
|
||||
* )
|
||||
* @ParamConverter("household", options={"id" = "household_id"})
|
||||
*/
|
||||
public function editHouseholdMetadata(Request $request, Household $household)
|
||||
{
|
||||
// TODO ACL
|
||||
$form = $this->createMetadataForm($household);
|
||||
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() and $form->isValid()) {
|
||||
$this->getDoctrine()->getManager()->flush();
|
||||
|
||||
$this->addFlash('success', $this->translator->trans('household.data_saved'));
|
||||
|
||||
return $this->redirectToRoute('chill_person_household_members', [
|
||||
'household_id' => $household->getId()
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->render('@ChillPerson/Household/edit_member_metadata.html.twig', [
|
||||
'household' => $household,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
private function createMetadataForm(Household $household): FormInterface
|
||||
{
|
||||
$form = $this->createForm(
|
||||
HouseholdType::class,
|
||||
$household,
|
||||
[
|
||||
'action' => $this->generateUrl(
|
||||
'chill_person_household_members_metadata_edit',
|
||||
[
|
||||
'household_id' => $household->getId()
|
||||
]
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
|
@ -50,8 +50,12 @@ class HouseholdMemberController extends ApiController
|
||||
|
||||
// TODO ACL
|
||||
//
|
||||
// TODO validation
|
||||
//
|
||||
$errors = $editor->validate();
|
||||
|
||||
if (count($errors) > 0) {
|
||||
return $this->json($errors, 422);
|
||||
}
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
|
||||
// if new household, persist it
|
||||
@ -155,7 +159,9 @@ class HouseholdMemberController extends ApiController
|
||||
{
|
||||
// TODO ACL
|
||||
|
||||
$form = $this->createForm(HouseholdMemberType::class, $member);
|
||||
$form = $this->createForm(HouseholdMemberType::class, $member, [
|
||||
'validation_groups' => [ 'household_memberships' ]
|
||||
]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
|
@ -41,6 +41,8 @@ use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
|
||||
use Chill\PersonBundle\Repository\PersonNotDuplicateRepository;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
|
||||
final class PersonController extends AbstractController
|
||||
{
|
||||
@ -416,4 +418,29 @@ final class PersonController extends AbstractController
|
||||
|
||||
return $person;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @Route(
|
||||
* "/{_locale}/person/household/{person_id}/history",
|
||||
* name="chill_person_household_person_history",
|
||||
* methods={"GET", "POST"}
|
||||
* )
|
||||
* @ParamConverter("person", options={"id" = "person_id"})
|
||||
*/
|
||||
public function householdHistoryByPerson(Request $request, Person $person): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person,
|
||||
"You are not allowed to see this person.");
|
||||
|
||||
$event = new PrivacyEvent($person);
|
||||
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
|
||||
|
||||
return $this->render(
|
||||
'@ChillPerson/Person/household_history.html.twig',
|
||||
[
|
||||
'person' => $person
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Chill\MainBundle\Entity\Address;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Validator\Constraints\Household\MaxHolder;
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
@ -20,6 +22,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
|
||||
* "household"=Household::class
|
||||
* })
|
||||
* @MaxHolder(groups={"household_memberships"})
|
||||
*/
|
||||
class Household
|
||||
{
|
||||
@ -27,7 +30,7 @@ class Household
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
* @Serializer\Groups({"write"})
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
@ -52,10 +55,26 @@ class Household
|
||||
*/
|
||||
private Collection $members;
|
||||
|
||||
/**
|
||||
* @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_members_")
|
||||
*/
|
||||
private CommentEmbeddable $commentMembers;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="boolean", name="waiting_for_birth", options={"default": false})
|
||||
*/
|
||||
private bool $waitingForBirth = false;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="date_immutable", name="waiting_for_birth_date", nullable=true, options={"default": null})
|
||||
*/
|
||||
private ?\DateTimeImmutable $waitingForBirthDate = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->addresses = new ArrayCollection();
|
||||
$this->members = new ArrayCollection();
|
||||
$this->commentMembers = new CommentEmbeddable();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@ -129,6 +148,53 @@ class Household
|
||||
return $this->members;
|
||||
}
|
||||
|
||||
public function getMembersOnRange(\DateTimeImmutable $from, ?\DateTimeImmutable $to): Collection
|
||||
{
|
||||
$criteria = new Criteria();
|
||||
$expr = Criteria::expr();
|
||||
|
||||
$criteria->where(
|
||||
$expr->gte('startDate', $from)
|
||||
);
|
||||
|
||||
if (NULL !== $to) {
|
||||
$criteria->andWhere(
|
||||
$expr->orX(
|
||||
$expr->lte('endDate', $to),
|
||||
$expr->eq('endDate', NULL)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->getMembers()
|
||||
->matching($criteria)
|
||||
;
|
||||
}
|
||||
|
||||
public function getMembersDuringMembership(HouseholdMember $membership)
|
||||
{
|
||||
return $this->getMembersOnRange(
|
||||
$membership->getStartDate(),
|
||||
$membership->getEndDate()
|
||||
)->filter(
|
||||
function(HouseholdMember $m) use ($membership) {
|
||||
return $m !== $membership;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function getMembersHolder(): Collection
|
||||
{
|
||||
$criteria = new Criteria();
|
||||
$expr = Criteria::expr();
|
||||
|
||||
$criteria->where(
|
||||
$expr->eq('holder', true)
|
||||
);
|
||||
|
||||
return $this->getMembers()->matching($criteria);
|
||||
}
|
||||
|
||||
public function getCurrentMembers(?\DateTimeImmutable $now = null): Collection
|
||||
{
|
||||
$criteria = new Criteria();
|
||||
@ -148,6 +214,22 @@ class Household
|
||||
return $this->getMembers()->matching($criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* get current members ids
|
||||
*
|
||||
* Used in serialization
|
||||
*
|
||||
* @Serializer\Groups({"read"})
|
||||
* @Serializer\SerializedName("current_members_id")
|
||||
*
|
||||
*/
|
||||
public function getCurrentMembersIds(?\DateTimeImmutable $now = null): Collection
|
||||
{
|
||||
return $this->getCurrentMembers($now)->map(
|
||||
fn (HouseholdMember $m) => $m->getId()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the persons currently associated to the household.
|
||||
*
|
||||
@ -236,7 +318,42 @@ class Household
|
||||
}
|
||||
}
|
||||
dump($cond);
|
||||
}
|
||||
|
||||
public function getCommentMembers(): CommentEmbeddable
|
||||
{
|
||||
return $this->commentMembers;
|
||||
}
|
||||
|
||||
public function setCommentMembers(CommentEmbeddable $commentMembers): self
|
||||
{
|
||||
$this->commentMembers = $commentMembers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWaitingForBirth(): bool
|
||||
{
|
||||
return $this->waitingForBirth;
|
||||
}
|
||||
|
||||
public function setWaitingForBirth(bool $waitingForBirth): self
|
||||
{
|
||||
$this->waitingForBirth = $waitingForBirth;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWaitingForBirthDate(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->waitingForBirthDate;
|
||||
}
|
||||
|
||||
public function setWaitingForBirthDate(?\DateTimeImmutable $waitingForBirthDate): self
|
||||
{
|
||||
$this->waitingForBirthDate = $waitingForBirthDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Household\Position;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
|
||||
/**
|
||||
@ -28,18 +29,25 @@ class HouseholdMember
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=Position::class)
|
||||
* @Serializer\Groups({"read"})
|
||||
* @Assert\NotNull(groups={"household_memberships"})
|
||||
*/
|
||||
private ?Position $position = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
|
||||
* @Serializer\Groups({"read"})
|
||||
* @Assert\NotNull(groups={"household_memberships"})
|
||||
*/
|
||||
private ?\DateTimeImmutable $startDate = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="date_immutable", nullable= true, options={"default": null})
|
||||
* @Serializer\Groups({"read"})
|
||||
* @Assert\GreaterThan(
|
||||
* propertyPath="startDate",
|
||||
* message="household_membership.The end date must be after start date",
|
||||
* groups={"household_memberships"}
|
||||
* )
|
||||
*/
|
||||
private ?\DateTimeImmutable $endDate = null;
|
||||
|
||||
@ -67,6 +75,8 @@ class HouseholdMember
|
||||
* targetEntity="\Chill\PersonBundle\Entity\Person"
|
||||
* )
|
||||
* @Serializer\Groups({"read"})
|
||||
* @Assert\Valid(groups={"household_memberships"})
|
||||
* @Assert\NotNull(groups={"household_memberships"})
|
||||
*/
|
||||
private ?Person $person = null;
|
||||
|
||||
@ -76,6 +86,8 @@ class HouseholdMember
|
||||
* @ORM\ManyToOne(
|
||||
* targetEntity="\Chill\PersonBundle\Entity\Household\Household"
|
||||
* )
|
||||
* @Assert\Valid(groups={"household_memberships"})
|
||||
* @Assert\NotNull(groups={"household_memberships"})
|
||||
*/
|
||||
private ?Household $household = null;
|
||||
|
||||
@ -194,4 +206,13 @@ class HouseholdMember
|
||||
{
|
||||
return $this->holder;
|
||||
}
|
||||
|
||||
public function isCurrent(\DateTimeImmutable $at = null): bool
|
||||
{
|
||||
$at = NULL === $at ? new \DateTimeImmutable('now'): $at;
|
||||
|
||||
return $this->getStartDate() < $at && (
|
||||
NULL === $this->getEndDate() || $at < $this->getEndDate()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1219,6 +1219,50 @@ class Person implements HasCenterInterface
|
||||
return $this->householdParticipations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get participation where the person does share the household.
|
||||
*
|
||||
* Order by startDate, desc
|
||||
*/
|
||||
public function getHouseholdParticipationsShareHousehold(): Collection
|
||||
{
|
||||
$criteria = new Criteria();
|
||||
$expr = Criteria::expr();
|
||||
|
||||
$criteria
|
||||
->where(
|
||||
$expr->eq('shareHousehold', true)
|
||||
)
|
||||
->orderBy(['startDate' => Criteria::DESC])
|
||||
;
|
||||
|
||||
return $this->getHouseholdParticipations()
|
||||
->matching($criteria)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get participation where the person does not share the household.
|
||||
*
|
||||
* Order by startDate, desc
|
||||
*/
|
||||
public function getHouseholdParticipationsNotShareHousehold(): Collection
|
||||
{
|
||||
$criteria = new Criteria();
|
||||
$expr = Criteria::expr();
|
||||
|
||||
$criteria
|
||||
->where(
|
||||
$expr->eq('shareHousehold', false)
|
||||
)
|
||||
->orderBy(['startDate' => Criteria::DESC])
|
||||
;
|
||||
|
||||
return $this->getHouseholdParticipations()
|
||||
->matching($criteria)
|
||||
;
|
||||
}
|
||||
|
||||
public function getCurrentHousehold(?\DateTimeImmutable $at = null): ?Household
|
||||
{
|
||||
$criteria = new Criteria();
|
||||
|
@ -35,7 +35,8 @@ class HouseholdMemberType extends AbstractType
|
||||
}
|
||||
$builder
|
||||
->add('comment', ChillTextareaType::class, [
|
||||
'label' => 'household.Comment'
|
||||
'label' => 'household.Comment',
|
||||
'required' => false
|
||||
])
|
||||
;
|
||||
}
|
||||
|
40
src/Bundle/ChillPersonBundle/Form/HouseholdType.php
Normal file
40
src/Bundle/ChillPersonBundle/Form/HouseholdType.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Form\Type\ChillDateType;
|
||||
use Chill\MainBundle\Form\Type\CommentType;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class HouseholdType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('commentMembers', CommentType::class, [
|
||||
'label' => 'household.comment_membership',
|
||||
'required' => false
|
||||
])
|
||||
->add('waitingForBirth', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'household.expecting_birth'
|
||||
])
|
||||
->add('waitingForBirthDate', ChillDateType::class, [
|
||||
'required' => false,
|
||||
'label' => 'household.date_expecting_birth',
|
||||
'input' => 'datetime_immutable'
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Household::class,
|
||||
]);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
namespace Chill\PersonBundle\Household;
|
||||
|
||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Entity\Household\Position;
|
||||
@ -19,9 +20,11 @@ class MembersEditor
|
||||
private array $persistables = [];
|
||||
private array $membershipsAffected = [];
|
||||
|
||||
public const VALIDATION_GROUP = 'household_memberships';
|
||||
|
||||
public function __construct(ValidatorInterface $validator, ?Household $household)
|
||||
{
|
||||
$this->validation = $validator;
|
||||
$this->validator = $validator;
|
||||
$this->household = $household;
|
||||
}
|
||||
|
||||
@ -41,12 +44,12 @@ class MembersEditor
|
||||
$this->household->addMember($membership);
|
||||
|
||||
if ($position->getShareHousehold()) {
|
||||
foreach ($person->getHouseholdParticipations() as $participation) {
|
||||
if (FALSE === $participation->getShareHousehold()) {
|
||||
foreach ($person->getHouseholdParticipationsShareHousehold() as $participation) {
|
||||
if ($participation === $membership) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($participation === $membership) {
|
||||
if ($participation->getStartDate() > $membership->getStartDate()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -92,7 +95,18 @@ class MembersEditor
|
||||
|
||||
public function validate(): ConstraintViolationListInterface
|
||||
{
|
||||
if ($this->hasHousehold()) {
|
||||
$list = $this->validator
|
||||
->validate($this->getHousehold(), null, [ self::VALIDATION_GROUP ]);
|
||||
} else {
|
||||
$list = new ConstraintViolationList();
|
||||
}
|
||||
|
||||
foreach ($this->membershipsAffected as $m) {
|
||||
$list->addAll($this->validator->validate($m, null, [ self::VALIDATION_GROUP ]));
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function getPersistable(): array
|
||||
@ -100,6 +114,7 @@ class MembersEditor
|
||||
return $this->persistables;
|
||||
}
|
||||
|
||||
|
||||
public function getHousehold(): ?Household
|
||||
{
|
||||
return $this->household;
|
||||
|
@ -64,6 +64,16 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
|
||||
'order' => 50
|
||||
]);
|
||||
|
||||
$menu->addChild($this->translator->trans('household.person history'), [
|
||||
'route' => 'chill_person_household_person_history',
|
||||
'routeParameters' => [
|
||||
'person_id' => $parameters['person']->getId()
|
||||
]
|
||||
])
|
||||
->setExtras([
|
||||
'order' => 99999
|
||||
]);
|
||||
|
||||
$menu->addChild($this->translator->trans('Person duplicate'), [
|
||||
'route' => 'chill_person_duplicate_view',
|
||||
'routeParameters' => [
|
||||
@ -71,7 +81,7 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
|
||||
]
|
||||
])
|
||||
->setExtras([
|
||||
'order' => 51
|
||||
'order' => 99999
|
||||
]);
|
||||
|
||||
if ($this->showAccompanyingPeriod === 'visible') {
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { ShowHide } from 'ShowHide/show_hide.js';
|
||||
|
||||
let
|
||||
k = document.getElementById('waitingForBirthContainer'),
|
||||
waitingForBirthDate = document.getElementById('waitingForBirthDateContainer')
|
||||
;
|
||||
|
||||
console.log(k );
|
||||
|
||||
new ShowHide({
|
||||
'container': [waitingForBirthDate],
|
||||
'froms': [k ],
|
||||
'event_name': 'input',
|
||||
'debug': true,
|
||||
'test': function(froms, event) {
|
||||
for (let f of froms.values()) {
|
||||
console.log(f);
|
||||
for (let input of f.querySelectorAll('input').values()) {
|
||||
return input.checked;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
@ -16,32 +16,13 @@ const householdMove = (payload) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
throw Error('Error with testing move');
|
||||
});
|
||||
};
|
||||
|
||||
const householdMoveTest = (payload) => {
|
||||
const url = `/api/1.0/person/household/members/move/test.json`;
|
||||
return fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(response => {
|
||||
if (response.status === 422) {
|
||||
return response.json();
|
||||
}
|
||||
if (response.ok) {
|
||||
// return an empty array if ok
|
||||
return new Promise((resolve, reject) => resolve({ violations: [] }) );
|
||||
}
|
||||
throw Error('Error with testing move');
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
householdMove,
|
||||
householdMoveTest
|
||||
};
|
||||
|
@ -30,7 +30,7 @@
|
||||
<img src="~ChillMainAssets/img/draggable.svg" class="drag-icon" />
|
||||
<person :person="conc.person"></person>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="conc.person.birthdate !== null">
|
||||
{{ $t('person.born', {'gender': conc.person.gender} ) }}
|
||||
{{ $d(conc.person.birthdate.datetime, 'short') }}
|
||||
</div>
|
||||
|
@ -12,6 +12,9 @@
|
||||
<li v-for="(msg, index) in warnings">
|
||||
{{ $t(msg.m, msg.a) }}
|
||||
</li>
|
||||
<li v-for="msg in errors">
|
||||
{{ msg }}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
@ -34,8 +37,9 @@ export default {
|
||||
computed: {
|
||||
...mapState({
|
||||
warnings: (state) => state.warnings,
|
||||
hasNoWarnings: (state) => state.warnings.length === 0,
|
||||
hasWarnings: (state) => state.warnings.length > 0,
|
||||
errors: (state) => state.errors,
|
||||
hasNoWarnings: (state) => state.warnings.length === 0 && state.errors.length === 0,
|
||||
hasWarnings: (state) => state.warnings.length > 0 || state.errors.length > 0,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
|
@ -8,7 +8,7 @@
|
||||
{{ $t('household_members_editor.holder') }}
|
||||
</span>
|
||||
</div>
|
||||
<div>{{ $t('person.born', {'gender': conc.person.gender} ) }}</div>
|
||||
<div v-if="conc.person.birthdate !== null">{{ $t('person.born', {'gender': conc.person.gender} ) }}</div>
|
||||
</div>
|
||||
<div class="item-col box-where">
|
||||
<ul class="list-content fa-ul">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<span class="chill-entity chill-entity__person">
|
||||
<span class="chill-entity__person__text">
|
||||
<span class="chill-entity__person__text chill_denomination">
|
||||
{{ person.text }}
|
||||
</span>
|
||||
</span>
|
||||
|
@ -18,6 +18,45 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if withoutHousehold|length > 0 %}
|
||||
<div class="alert alert-danger alert-with-actions">
|
||||
<div class="message">
|
||||
{{ 'Some peoples does not belong to any household currently. Add them to an household soon'|trans }}
|
||||
</div>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button class="btn btn-primary" data-toggle="collapse" href="#withoutHouseholdList">
|
||||
{{ 'Add to household now'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="withoutHouseholdList" class="collapse">
|
||||
<form method="GET" action="{{ chill_path_add_return_path('chill_person_household_members_editor') }}">
|
||||
|
||||
<h3>{{ 'household.Select people to move'|trans }}</h3>
|
||||
<ul>
|
||||
{% for p in withoutHousehold %}
|
||||
<li>
|
||||
<input type="checkbox" name="persons[]" value="{{ p.id }}" />
|
||||
{{ p|chill_entity_render_box }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="submit" class="sc-button bt-edit">
|
||||
{{ 'household.Household editor'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<h2>{{ 'Associated peoples'|trans }}</h2>
|
||||
<div class="flex-table">
|
||||
{% for p in accompanyingCourse.participations %}
|
||||
|
@ -0,0 +1,42 @@
|
||||
{% extends '@ChillPerson/Household/layout.html.twig' %}
|
||||
|
||||
{% block title 'household.Edit member metadata'|trans %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
{{ form_start(form) }}
|
||||
|
||||
{{ form_row(form.commentMembers) }}
|
||||
|
||||
<div id="waitingForBirthContainer">
|
||||
{{ form_row(form.waitingForBirth) }}
|
||||
</div>
|
||||
|
||||
<div id="waitingForBirthDateContainer">
|
||||
{{ form_row(form.waitingForBirthDate) }}
|
||||
</div>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a
|
||||
href="{{ chill_path_add_return_path('chill_person_household_members', { 'household_id': household.id }) }}"
|
||||
class="sc-button bt-cancel"
|
||||
/>
|
||||
{{ 'Cancel'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="submit" class="sc-button bt-save">
|
||||
{{ 'Save'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ encore_entry_script_tags('household_edit_metadata') }}
|
||||
{% endblock %}
|
@ -5,6 +5,60 @@
|
||||
{% block content %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
{% if form is not null %}
|
||||
{{ form_start(form) }}
|
||||
|
||||
{{ form_row(form.commentMembers) }}
|
||||
|
||||
<div id="waitingForBirthContainer">
|
||||
{{ form_row(form.waitingForBirth) }}
|
||||
</div>
|
||||
|
||||
<div id="waitingForBirthDateContainer">
|
||||
{{ form_row(form.waitingForBirthDate) }}
|
||||
</div>
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="submit" class="sc-button bt-save">
|
||||
{{ 'Save'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
{% else %}
|
||||
|
||||
{% if not household.commentMembers.isEmpty() %}
|
||||
{{ household.commentMembers|chill_entity_render_box }}
|
||||
{% endif %}
|
||||
|
||||
{% if household.waitingForBirth %}
|
||||
{% if household.waitingForBirthDate is not null %}
|
||||
{{ 'household.Expecting for birth on date'|trans({ 'date': household.waitingForBirthDate|format_date('long') }) }}
|
||||
{% else %}
|
||||
{{ 'household.Expecting for birth'|trans }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="chill-no-data-statement">
|
||||
{{ 'household.Any expecting birth'|trans }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a
|
||||
href="{{ chill_path_add_return_path('chill_person_household_members', { 'household_id': household.id, 'edit': 1 }) }}"
|
||||
class="sc-button bt-edit"
|
||||
>
|
||||
{{ 'household.Comment and expecting birth'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% for p in positions %}
|
||||
<h3>{{ p.label|localize_translatable_string }}</h3>
|
||||
|
||||
@ -149,3 +203,7 @@
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
{{ encore_entry_script_tags('household_edit_metadata') }}
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,201 @@
|
||||
{% extends "@ChillPerson/layout.html.twig" %}
|
||||
|
||||
{% set activeRouteKey = 'chill_person_view' %}
|
||||
|
||||
{% block title 'household.Household history for %name%'|trans({'name': person|chill_entity_render_string}) %}
|
||||
|
||||
|
||||
{% block personcontent %}
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
<h2>{{ 'household.Household shared'|trans }}</h2>
|
||||
|
||||
{% set memberships = person.getHouseholdParticipationsShareHousehold() %}
|
||||
|
||||
{% if memberships|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'household.Never in any household'|trans }}</p>
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="sc-button"
|
||||
href="{{
|
||||
chill_path_add_return_path(
|
||||
'chill_person_household_members_editor',
|
||||
{ 'persons': [ person.id ]}) }}"
|
||||
>
|
||||
<i class="fa fa-sign-out"></i>
|
||||
{{ 'household.Join'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% else %}
|
||||
|
||||
<div class="household">
|
||||
<div class="household__address">
|
||||
|
||||
{% if not person.isSharingHousehold() %}
|
||||
<div class="row">
|
||||
<div class="household__address--date"></div>
|
||||
<div class="household__address--content">
|
||||
<div class="cell">
|
||||
<a class="sc-button"
|
||||
href="{{
|
||||
chill_path_add_return_path(
|
||||
'chill_person_household_members_editor',
|
||||
{ 'persons': [ person.id ]}) }}"
|
||||
>
|
||||
<i class="fa fa-sign-out"></i>
|
||||
{{ 'household.Join'|trans }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for p in memberships %}
|
||||
<div class="row">
|
||||
<div class="household__address--date">
|
||||
<div class="cell">
|
||||
<div class="pill">
|
||||
{{ p.startDate|format_date('long') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="household__address--content">
|
||||
<div class="cell">
|
||||
<i class="dot"></i>
|
||||
<div>
|
||||
<div>
|
||||
<p>
|
||||
<i class="fa fa-home"></i>
|
||||
<a
|
||||
href="{{ chill_path_add_return_path(
|
||||
'chill_person_household_summary',
|
||||
{ 'household_id': p.household.id }
|
||||
) }}"
|
||||
>
|
||||
{{ 'household.Household number'|trans({'household_num': p.household.id }) }}
|
||||
</a>
|
||||
</p>
|
||||
<p>{{ p.position.label|localize_translatable_string }} {% if p.holder %}<span class="badge badge-primary">{{ 'household.holder'|trans }}</span>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{% set simultaneous = p.household.getMembersDuringMembership(p) %}
|
||||
{% if simultaneous|length == 0 %}
|
||||
<p class="chill-no-data-statement">
|
||||
{{ 'household.Any simultaneous members'|trans }}
|
||||
</p>
|
||||
{% else %}
|
||||
{{ 'household.Members at same time'|trans }}:
|
||||
{% for p in simultaneous -%}
|
||||
{{- p.person|chill_entity_render_box({'addLink': true }) -}}
|
||||
{%- if p.holder %} <span class="badge badge-primary">{{'household.holder'|trans }}</span> {% endif %}
|
||||
{%- if not loop.last %}, {% endif -%}
|
||||
{%- endfor -%}
|
||||
{% endif %}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a
|
||||
href="{{ chill_path_add_return_path('chill_person_household_member_edit', { id: p.id }) }}"
|
||||
class="sc-button bt-edit"
|
||||
></a>
|
||||
</li>
|
||||
{% if p.isCurrent() %}
|
||||
<li>
|
||||
<a class="sc-button"
|
||||
href="{{ chill_path_add_return_path(
|
||||
'chill_person_household_members_editor',
|
||||
{ 'persons': [ person.id ], 'allow_leave_without_household': true }) }}"
|
||||
>
|
||||
<i class="fa fa-sign-out"></i>
|
||||
{{ 'household.Leave'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2>{{ 'household.Household not shared'|trans }}</h2>
|
||||
|
||||
{% set memberships = person.getHouseholdParticipationsNotShareHousehold() %}
|
||||
|
||||
{% if memberships|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'household.Never in any household'|trans }}</p>
|
||||
{% else %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'household.from'|trans }}</th>
|
||||
<th>{{ 'household.to'|trans }}</th>
|
||||
<th>{{ 'household.Household'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for p in memberships %}
|
||||
<tr>
|
||||
<td>{{ p.startDate|format_date('long') }}</td>
|
||||
<td>
|
||||
{% if p.endDate is not empty %}
|
||||
{{ p.endDate|format_date('long') }}
|
||||
{% else %}
|
||||
{{ 'household.Membership currently running'|trans }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<p>
|
||||
<i class="fa fa-home"></i>
|
||||
<a
|
||||
href="{{ chill_path_add_return_path(
|
||||
'chill_person_household_summary',
|
||||
{ 'household_id': p.household.id }
|
||||
) }}"
|
||||
>
|
||||
{{ 'household.Household number'|trans({'household_num': p.household.id }) }}
|
||||
</a>
|
||||
</p>
|
||||
<p>{{ p.position.label|localize_translatable_string }} {% if p.holder %}<span class="badge badge-primary">{{ 'household.holder'|trans }}</span>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
{% set simultaneous = p.household.getMembersDuringMembership(p) %}
|
||||
{% if simultaneous|length == 0 %}
|
||||
<p class="chill-no-data-statement">
|
||||
{{ 'household.Any simultaneous members'|trans }}
|
||||
</p>
|
||||
{% else %}
|
||||
{{ 'household.Members at same time'|trans }}:
|
||||
{% for p in simultaneous -%}
|
||||
{{- p.person|chill_entity_render_box({'addLink': true }) -}}
|
||||
{%- if p.holder %} <span class="badge badge-primary">{{'household.holder'|trans }}</span> {% endif %}
|
||||
{%- if not loop.last %}, {% endif -%}
|
||||
{%- endfor -%}
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a
|
||||
href="{{ chill_path_add_return_path('chill_person_household_member_edit', { id: p.id }) }}"
|
||||
class="sc-button bt-edit"
|
||||
></a>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Tests\Validator\Household;
|
||||
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Entity\Household\Position;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Templating\Entity\PersonRender;
|
||||
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequentialValidator;
|
||||
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
|
||||
class HouseholdMembershipSequentialValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
public function testEmptyPerson()
|
||||
{
|
||||
$constraint = $this->getConstraint();
|
||||
|
||||
$person = new Person();
|
||||
|
||||
$this->validator->validate($person, $constraint);
|
||||
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testMembershipCovering()
|
||||
{
|
||||
$constraint = $this->getConstraint();
|
||||
|
||||
$person = new Person();
|
||||
$household = new Household();
|
||||
$position = (new Position())
|
||||
->setShareHousehold(true)
|
||||
;
|
||||
$membership = (new HouseholdMember())
|
||||
->setPosition($position)
|
||||
->setStartDate(new \DateTimeImmutable('2010-01-01'))
|
||||
->setPerson($person)
|
||||
;
|
||||
$membership = (new HouseholdMember())
|
||||
->setPosition($position)
|
||||
->setStartDate(new \DateTimeImmutable('2011-01-01'))
|
||||
->setPerson($person)
|
||||
;
|
||||
|
||||
$this->validator->validate($person, $constraint);
|
||||
|
||||
$this->buildViolation('msg')
|
||||
->setParameters([
|
||||
'%person_name%' => 'name',
|
||||
'%from%' => '01-01-2011',
|
||||
'%nbHousehold%' => 2
|
||||
])
|
||||
->assertRaised()
|
||||
;
|
||||
}
|
||||
|
||||
public function testMembershipCoveringNoShareHousehold()
|
||||
{
|
||||
$constraint = $this->getConstraint();
|
||||
|
||||
$person = new Person();
|
||||
$household = new Household();
|
||||
$position = (new Position())
|
||||
->setShareHousehold(false)
|
||||
;
|
||||
$membership = (new HouseholdMember())
|
||||
->setPosition($position)
|
||||
->setStartDate(new \DateTimeImmutable('2010-01-01'))
|
||||
->setPerson($person)
|
||||
;
|
||||
$membership = (new HouseholdMember())
|
||||
->setPosition($position)
|
||||
->setStartDate(new \DateTimeImmutable('2011-01-01'))
|
||||
->setPerson($person)
|
||||
;
|
||||
|
||||
$this->validator->validate($person, $constraint);
|
||||
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
protected function getConstraint()
|
||||
{
|
||||
return new HouseholdMembershipSequential([
|
||||
'message' => 'msg'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function createValidator()
|
||||
{
|
||||
$render = $this->createMock(PersonRender::class);
|
||||
$render->method('renderString')
|
||||
->willReturn('name')
|
||||
;
|
||||
|
||||
return new HouseholdMembershipSequentialValidator($render);
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Tests\Validator\Household;
|
||||
|
||||
use Chill\PersonBundle\Entity\Household\HouseholdMember;
|
||||
use Chill\PersonBundle\Entity\Household\Position;
|
||||
use Chill\PersonBundle\Entity\Household\Household;
|
||||
use Chill\PersonBundle\Validator\Constraints\Household\MaxHolderValidator;
|
||||
use Chill\PersonBundle\Validator\Constraints\Household\MaxHolder;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
|
||||
class MaxHolderValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider provideInvalidHousehold
|
||||
*/
|
||||
public function testHouseholdInvalid(Household $household, $parameters)
|
||||
{
|
||||
$constraint = $this->getConstraint();
|
||||
|
||||
$this->validator->validate($household, $constraint);
|
||||
|
||||
$this->buildViolation('msg')
|
||||
->setParameters($parameters)
|
||||
->assertRaised()
|
||||
;
|
||||
}
|
||||
|
||||
protected function getConstraint()
|
||||
{
|
||||
return new MaxHolder([
|
||||
'message' => 'msg',
|
||||
'messageInfinity' => 'msgInfinity'
|
||||
]);
|
||||
}
|
||||
|
||||
public function provideInvalidHousehold()
|
||||
{
|
||||
$household = new Household();
|
||||
$position = (new Position())
|
||||
->setAllowHolder(true);
|
||||
$household
|
||||
->addMember(
|
||||
(new HouseholdMember())
|
||||
->setHolder(true)
|
||||
->setStartDate(new \DateTimeImmutable('2010-01-01'))
|
||||
->setEndDate(new \DateTimeImmutable('2010-12-01'))
|
||||
)
|
||||
->addMember(
|
||||
(new HouseholdMember())
|
||||
->setHolder(true)
|
||||
->setStartDate(new \DateTimeImmutable('2010-06-01'))
|
||||
->setEndDate(new \DateTimeImmutable('2010-07-01'))
|
||||
)
|
||||
->addMember(
|
||||
(new HouseholdMember())
|
||||
->setHolder(true)
|
||||
->setStartDate(new \DateTimeImmutable('2010-01-01'))
|
||||
->setEndDate(new \DateTimeImmutable('2010-12-01'))
|
||||
)
|
||||
;
|
||||
|
||||
yield [
|
||||
$household,
|
||||
[
|
||||
'{{ start }}' => '01-06-2010',
|
||||
'{{ end }}' => '01-07-2010'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
protected function createValidator()
|
||||
{
|
||||
return new MaxHolderValidator();
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Validator\Constraints\Household;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
*/
|
||||
class HouseholdMembershipSequential extends Constraint
|
||||
{
|
||||
public $message = 'household_membership.Person with membership covering';
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return [ self::CLASS_CONSTRAINT ];
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Validator\Constraints\Household;
|
||||
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\MainBundle\Util\DateRangeCovering;
|
||||
use Chill\PersonBundle\Templating\Entity\PersonRender;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
|
||||
/**
|
||||
* Validate that a person does not belong to two household at
|
||||
* the same time
|
||||
*/
|
||||
class HouseholdMembershipSequentialValidator extends ConstraintValidator
|
||||
{
|
||||
private PersonRender $render;
|
||||
|
||||
public function __construct(PersonRender $render)
|
||||
{
|
||||
$this->render = $render;
|
||||
}
|
||||
|
||||
public function validate($person, Constraint $constraint)
|
||||
{
|
||||
if (!$person instanceof Person) {
|
||||
throw new UnexpectedTypeException($constraint, Person::class);
|
||||
}
|
||||
|
||||
$participations = $person->getHouseholdParticipationsShareHousehold();
|
||||
|
||||
if ($participations->count() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$covers = new DateRangeCovering(1, $participations->first()
|
||||
->getStartDate()->getTimezone());
|
||||
|
||||
foreach ($participations as $k => $p) {
|
||||
$covers->add($p->getStartDate(), $p->getEndDate(), $k);
|
||||
}
|
||||
|
||||
$covers->compute();
|
||||
|
||||
if ($covers->hasIntersections()) {
|
||||
foreach ($covers->getIntersections() as list($start, $end, $metadata)) {
|
||||
$participation = $participations[$metadata[0]];
|
||||
$nbHousehold = count($metadata);
|
||||
|
||||
$this->context
|
||||
->buildViolation($constraint->message)
|
||||
->setParameters([
|
||||
'%person_name%' => $this->render->renderString(
|
||||
$participation->getPerson(), []
|
||||
),
|
||||
// TODO when date is correctly i18n, fix this
|
||||
'%from%' => $start->format('d-m-Y'),
|
||||
'%nbHousehold%' => $nbHousehold,
|
||||
|
||||
])
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\PersonBundle\Validator\Constraints\Household;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
*/
|
||||
class MaxHolder extends Constraint
|
||||
{
|
||||
public $message = 'household.max_holder_overflowed';
|
||||
public $messageInfinity = 'household.max_holder_overflowed_infinity';
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
@ -2,7 +2,46 @@
|
||||
|
||||
namespace Chill\PersonBundle\Validator\Constraints\Household;
|
||||
|
||||
class MaxHolderValidator
|
||||
use Chill\MainBundle\Util\DateRangeCovering;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedValueException;
|
||||
|
||||
class MaxHolderValidator extends ConstraintValidator
|
||||
{
|
||||
private const MAX_HOLDERS = 2;
|
||||
|
||||
public function validate($household, Constraint $constraint)
|
||||
{
|
||||
$holders = $household->getMembersHolder();
|
||||
|
||||
if ($holders->count() <= self::MAX_HOLDERS) {
|
||||
return;
|
||||
}
|
||||
|
||||
$covers = new DateRangeCovering(self::MAX_HOLDERS,
|
||||
$holders[0]->getStartDate()->getTimezone());
|
||||
|
||||
foreach ($holders as $key => $member) {
|
||||
$covers->add($member->getStartDate(), $member->getEndDate(), $key);
|
||||
}
|
||||
|
||||
$covers->compute();
|
||||
|
||||
if ($covers->hasIntersections()) {
|
||||
foreach ($covers->getIntersections() as list($start, $end, $ids)) {
|
||||
$msg = $end === null ? $constraint->messageInfinity :
|
||||
$constraint->message;
|
||||
|
||||
$this->context->buildViolation($msg)
|
||||
->setParameters([
|
||||
'{{ start }}' => $start->format('d-m-Y'), // TODO fix when MessageParameter works with timezone
|
||||
'{{ end }}' => $end === null ? null : $end->format('d-m-Y')
|
||||
])
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -12,4 +12,5 @@ module.exports = function(encore, entries)
|
||||
encore.addEntry('household_address', __dirname + '/Resources/public/vuejs/HouseholdAddress/index.js');
|
||||
encore.addEntry('household_members_editor', __dirname + '/Resources/public/vuejs/HouseholdMembersEditor/index.js');
|
||||
encore.addEntry('vue_accourse', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js');
|
||||
encore.addEntry('household_edit_metadata', __dirname + '/Resources/public/modules/household_edit_metadata/index.js');
|
||||
};
|
||||
|
@ -57,6 +57,11 @@ services:
|
||||
tags:
|
||||
- { name: validator.constraint_validator, alias: birthdate_not_before }
|
||||
|
||||
Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequentialValidator:
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: validator.constraint_validator }
|
||||
|
||||
Chill\PersonBundle\Repository\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
@ -61,6 +61,9 @@ Chill\PersonBundle\Entity\Person:
|
||||
- Callback:
|
||||
callback: isAddressesValid
|
||||
groups: [addresses_consistent]
|
||||
- Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential:
|
||||
groups: [ 'household_memberships' ]
|
||||
|
||||
|
||||
Chill\PersonBundle\Entity\AccompanyingPeriod:
|
||||
properties:
|
||||
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Add comments and expecting birth to household
|
||||
*/
|
||||
final class Version20210614191600 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add comments and expecting birth to household';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_person_household ADD comment_members TEXT DEFAULT \'\' NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_household ADD waiting_for_birth BOOLEAN DEFAULT \'false\' NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_household ADD waiting_for_birth_date DATE DEFAULT NULL');
|
||||
$this->addSql('COMMENT ON COLUMN chill_person_household.waiting_for_birth_date IS \'(DC2Type:date_immutable)\'');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_person_household DROP comment_members');
|
||||
$this->addSql('ALTER TABLE chill_person_household DROP waiting_for_birth');
|
||||
$this->addSql('ALTER TABLE chill_person_household DROP waiting_for_birth_date');
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\Migrations\Person;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Set comment in household as Embedded Comment
|
||||
*/
|
||||
final class Version20210615074857 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'replace comment in household as embedded comment';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_person_household RENAME comment_members TO comment_members_comment');
|
||||
$this->addSql('ALTER TABLE chill_person_household ALTER COLUMN comment_members_comment DROP NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_household ALTER COLUMN comment_members_comment SET DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_household ADD comment_members_userId INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_household ADD comment_members_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_household ADD CONSTRAINT fk_household_comment_embeddable_user FOREIGN KEY (comment_members_userId) REFERENCES users (id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_person_household RENAME comment_members_comment TO comment_members');
|
||||
$this->addSql('ALTER TABLE chill_person_household ALTER comment_members SET DEFAULT \'\'');
|
||||
$this->addSql('ALTER TABLE chill_person_household ALTER comment_members SET NOT NULL');
|
||||
$this->addSql('ALTER TABLE chill_person_household DROP comment_members_comment');
|
||||
$this->addSql('ALTER TABLE chill_person_household DROP comment_members_userId');
|
||||
$this->addSql('ALTER TABLE chill_person_household DROP comment_members_date');
|
||||
}
|
||||
}
|
@ -7,7 +7,12 @@ Born the date: >-
|
||||
|
||||
household:
|
||||
Household: Ménage
|
||||
Household number: Ménage {household_num}
|
||||
Household members: Membres du ménage
|
||||
Household editor: Modifier l'appartenance
|
||||
Members at same time: Membres simultanés
|
||||
Any simultaneous members: Aucun membre simultanément
|
||||
Select people to move: Choisir les usagers
|
||||
Show future or past memberships: >-
|
||||
{length, plural,
|
||||
one {Montrer une ancienne appartenance}
|
||||
@ -17,6 +22,7 @@ household:
|
||||
Those members does not share address: Ces usagers ne partagent pas l'adresse du ménage.
|
||||
Any persons into this position: Aucune personne n'appartient au ménage à cette position.
|
||||
Leave: Quitter le ménage
|
||||
Join: Rejoindre un ménage
|
||||
Household file: Dossier ménage
|
||||
Add a member: Ajouter un membre
|
||||
Update membership: Modifier
|
||||
@ -38,3 +44,21 @@ household:
|
||||
many {et # autres personnes}
|
||||
other {et # autres personnes}
|
||||
}
|
||||
Expecting for birth on date: Naissance attendue pour le {date}
|
||||
Expecting for birth: Naissance attendue (date inconnue)
|
||||
Any expecting birth: Aucune naissance proche n'a été renseignée.
|
||||
Comment and expecting birth: Commentaire et naissance attendue
|
||||
Edit member metadata: Données supplémentaires
|
||||
comment_membership: Commentaire général sur les membres
|
||||
expecting_birth: Naissance attendue ?
|
||||
date_expecting_birth: Date de la naissance attendue
|
||||
data_saved: Données enregistrées
|
||||
Household history for %name%: Historique des ménages pour {name}
|
||||
Household shared: Ménages domiciliés
|
||||
Household not shared: Ménage non domiciliés
|
||||
Never in any household: Membre d'aucun ménage
|
||||
Membership currently running: En cours
|
||||
from: Depuis
|
||||
to: Jusqu'au
|
||||
person history: Ménages
|
||||
|
||||
|
@ -178,6 +178,8 @@ Edit & activate accompanying course: Modifier et valider
|
||||
See accompanying periods: Voir les périodes d'accompagnement
|
||||
See accompanying period: Voir cette période d'accompagnement
|
||||
Referrer: Référent
|
||||
Some peoples does not belong to any household currently. Add them to an household soon: Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur appartenance à un ménage dès que possible.
|
||||
Add to household now: Ajouter à un ménage
|
||||
|
||||
# pickAPersonType
|
||||
Pick a person: Choisir une personne
|
||||
|
@ -33,3 +33,11 @@ You should select an option: Une option doit être choisie.
|
||||
|
||||
# aggregator by age
|
||||
The date should not be empty: La date ne doit pas être vide
|
||||
|
||||
# household
|
||||
household:
|
||||
max_holder_overflowed_infinity: Il ne peut pas y avoir plus de deux titulaires simultanément. Or, avec cette modification, ce nombre sera dépassé à partir du {{ start }}.
|
||||
max_holder_overflowed: Il ne peut y avoir plus de deux titulaires simultanément. Or, avec cette modification, ce nombre sera dépassé entre le {{ start }} et le {{ end }}.
|
||||
household_membership:
|
||||
The end date must be after start date: La date de la fin de l'appartenance doit être postérieure à la date de début.
|
||||
Person with membership covering: Une personne ne peut pas appartenir à deux ménages simultanément. Or, avec cette modification, %person_name% appartiendrait à %nbHousehold% ménages à partir du %from%.
|
||||
|
Loading…
x
Reference in New Issue
Block a user