diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index e2114f05d..d3310fe15 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -22,6 +22,7 @@ use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodLocationHistory; +use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodStepHistory; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive; use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; @@ -336,6 +337,12 @@ class AccompanyingPeriod implements */ private string $step = self::STEP_DRAFT; + /** + * @ORM\OneToMany(targetEntity=AccompanyingPeriodStepHistory::class, + * mappedBy="period", cascade={"persist", "remove"}, orphanRemoval=true) + */ + private Collection $stepHistories; + /** * @ORM\Column(type="datetime", nullable=true, options={"default": NULL}) */ @@ -390,7 +397,6 @@ class AccompanyingPeriod implements */ public function __construct(?DateTime $dateOpening = null) { - $this->setOpeningDate($dateOpening ?? new DateTime('now')); $this->participations = new ArrayCollection(); $this->scopes = new ArrayCollection(); $this->socialIssues = new ArrayCollection(); @@ -399,6 +405,8 @@ class AccompanyingPeriod implements $this->resources = new ArrayCollection(); $this->userHistories = new ArrayCollection(); $this->locationHistories = new ArrayCollection(); + $this->stepHistories = new ArrayCollection(); + $this->setOpeningDate($dateOpening ?? new DateTime('now')); } /** @@ -966,6 +974,11 @@ class AccompanyingPeriod implements return $this->step; } + public function getStepHistories(): Collection + { + return $this->stepHistories; + } + public function getUser(): ?User { return $this->user; @@ -1234,7 +1247,11 @@ class AccompanyingPeriod implements */ public function setOpeningDate($openingDate) { - $this->openingDate = $openingDate; + if ($this->openingDate !== $openingDate) { + $this->openingDate = $openingDate; + + $this->ensureStepContinuity(); + } return $this; } @@ -1333,6 +1350,14 @@ class AccompanyingPeriod implements $this->bootPeriod(); } + if (self::STEP_DRAFT !== $this->step && $previous !== $step) { + // we create a new history + $history = new AccompanyingPeriodStepHistory(); + $history->setStep($this->step)->setStartDate(new DateTimeImmutable('now')); + + $this->addStepHistory($history); + } + return $this; } @@ -1373,6 +1398,17 @@ class AccompanyingPeriod implements return $this; } + private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory): self + { + if (!$this->stepHistories->contains($stepHistory)) { + $this->stepHistories[] = $stepHistory; + $stepHistory->setPeriod($this); + $this->ensureStepContinuity(); + } + + return $this; + } + private function bootPeriod(): void { // first location history @@ -1384,6 +1420,43 @@ class AccompanyingPeriod implements $this->addLocationHistory($locationHistory); } + private function ensureStepContinuity(): void + { + // ensure continuity of histories + $criteria = new Criteria(); + $criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]); + + /** @var Iterator $steps */ + $steps = $this->getStepHistories()->matching($criteria)->getIterator(); + $steps->rewind(); + + // we set the start date of the first step as the opening date, only if it is + // not greater than the end date + /** @var AccompanyingPeriodStepHistory $current */ + $current = $steps->current(); + + if (null === $current) { + return; + } + + if ($this->getOpeningDate()->format('Y-m-d') !== $current->getStartDate()->format('Y-m-d') + && ($this->getOpeningDate() <= $current->getEndDate() || null === $current->getEndDate())) { + $current->setStartDate(DateTimeImmutable::createFromMutable($this->getOpeningDate())); + } + + // then we set all the end date to the start date of the next one + do { + /** @var AccompanyingPeriodStepHistory $current */ + $current = $steps->current(); + $steps->next(); + + if ($steps->valid()) { + $next = $steps->current(); + $current->setEndDate($next->getStartDate()); + } + } while ($steps->valid()); + } + private function setRequestorPerson(?Person $requestorPerson = null): self { $this->requestorPerson = $requestorPerson; diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php new file mode 100644 index 000000000..f9baffa35 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod/AccompanyingPeriodStepHistory.php @@ -0,0 +1,115 @@ +endDate; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getPeriod(): AccompanyingPeriod + { + return $this->period; + } + + public function getStartDate(): ?DateTimeImmutable + { + return $this->startDate; + } + + public function getStep(): string + { + return $this->step; + } + + public function setEndDate(?DateTimeImmutable $endDate): self + { + $this->endDate = $endDate; + + return $this; + } + + /** + * @internal use AccompanyingPeriod::addLocationHistory + */ + public function setPeriod(AccompanyingPeriod $period): self + { + $this->period = $period; + + return $this; + } + + public function setStartDate(?DateTimeImmutable $startDate): self + { + $this->startDate = $startDate; + + return $this; + } + + public function setStep(string $step): AccompanyingPeriodStepHistory + { + $this->step = $step; + + return $this; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php index a3d2260e3..aad98a394 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/PersonFilters/MaritalStatusFilter.php @@ -12,12 +12,9 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Filter\PersonFilters; use Chill\MainBundle\Export\FilterInterface; -use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\MaritalStatus; use Chill\PersonBundle\Export\Declarations; -use DateTime; -use Doctrine\ORM\Query\Expr\Andx; use Symfony\Bridge\Doctrine\Form\Type\EntityType; class MaritalStatusFilter implements FilterInterface @@ -37,25 +34,10 @@ class MaritalStatusFilter implements FilterInterface public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data) { - $where = $qb->getDQLPart('where'); - - $clause = $qb->expr()->andX( - $qb->expr()->in('person.maritalStatus', ':maritalStatus'), - $qb->expr()->orX( - $qb->expr()->eq('person.maritalStatusDate', ':calc_date'), - $qb->expr()->isNull('person.maritalStatusDate') - ) + $qb->andWhere( + $qb->expr()->in('person.maritalStatus', ':maritalStatus') ); - - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } - - $qb->add('where', $where); $qb->setParameter('maritalStatus', $data['maritalStatus']); - $qb->setParameter('calc_date', $data['calc_date']); } public function applyOn() @@ -75,11 +57,6 @@ class MaritalStatusFilter implements FilterInterface 'multiple' => true, 'expanded' => true, ]); - - $builder->add('calc_date', ChillDateType::class, [ - 'label' => 'Marital status at this time', - 'data' => new DateTime(), - ]); } public function describeAction($data, $format = 'string') diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php index 4c6033ca9..2f38da244 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/AccompanyingPeriodTest.php @@ -29,6 +29,38 @@ use function count; */ final class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase { + public function testChangeStepKeepHistory() + { + $period = new AccompanyingPeriod(); + + $this->assertCount(0, $period->getStepHistories(), 'at initialization, period should not have any step history'); + + $period->setStep(AccompanyingPeriod::STEP_DRAFT); + + $this->assertCount(0, $period->getStepHistories(), 're applying a draft should not create a history'); + + $period->setStep(AccompanyingPeriod::STEP_CONFIRMED); + + $this->assertCount(1, $period->getStepHistories()); + $this->assertEquals(AccompanyingPeriod::STEP_CONFIRMED, $period->getStepHistories()->first()->getStep()); + + $period->setOpeningDate($aMonthAgo = new DateTime('1 month ago')); + + $this->assertCount(1, $period->getStepHistories()); + $this->assertEquals($aMonthAgo, $period->getStepHistories()->first()->getStartDate(), 'when changing the opening date, the start date of the first history should change'); + + $period->setOpeningDate($tenDaysAgo = new DateTime('10 days ago')); + + $this->assertCount(1, $period->getStepHistories()); + $this->assertEquals($tenDaysAgo, $period->getStepHistories()->first()->getStartDate(), 'when changing the opening date, the start date of the first history should change'); + + $period->setStep(AccompanyingPeriod::STEP_CLOSED); + $this->assertCount(2, $period->getStepHistories()); + + $period->setOpeningDate($tomorrow = new DateTime('tomorrow')); + $this->assertEquals($tenDaysAgo, $period->getStepHistories()->first()->getStartDate(), 'when changing the opening date to a later one and no history after, start date should change'); + } + public function testClosingEqualOpening() { $datetime = new DateTime('now'); diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20221014115500.php b/src/Bundle/ChillPersonBundle/migrations/Version20221014115500.php new file mode 100644 index 000000000..fe7b920d9 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20221014115500.php @@ -0,0 +1,69 @@ +addSql('DROP SEQUENCE chill_person_accompanying_period_step_history_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_person_accompanying_period_step_history'); + } + + public function getDescription(): string + { + return 'Add step history on accompanying periods'; + } + + public function up(Schema $schema): void + { + $this->addSql('CREATE SEQUENCE chill_person_accompanying_period_step_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_person_accompanying_period_step_history (id INT NOT NULL, period_id INT DEFAULT NULL, + endDate DATE DEFAULT NULL, startDate DATE NOT NULL, step TEXT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, + updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, + updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id)) + '); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CHECK (startDate <= endDate)'); + + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT ' . + 'chill_internal_acp_steps_not_overlaps EXCLUDE USING GIST( + -- extension btree_gist required to include comparaison with integer + period_id WITH =, + daterange(startDate, endDate, \'[)\') WITH && + ) + INITIALLY DEFERRED'); + + $this->addSql('CREATE INDEX IDX_84D514ACEC8B7ADE ON chill_person_accompanying_period_step_history (period_id)'); + $this->addSql('CREATE INDEX IDX_84D514AC3174800F ON chill_person_accompanying_period_step_history (createdBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.endDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.startDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_step_history.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT FK_84D514ACEC8B7ADE FOREIGN KEY (period_id) REFERENCES chill_person_accompanying_period (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_accompanying_period_step_history ADD CONSTRAINT FK_84D514AC3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_84D514AC65FF1AEC ON chill_person_accompanying_period_step_history (updatedBy_id)'); + + // fill the tables with current state + $this->addSql( + 'INSERT INTO chill_person_accompanying_period_step_history (id, period_id, startDate, endDate, step, createdAt, updatedAt) + SELECT nextval(\'chill_person_accompanying_period_step_history_id_seq\'), id, openingDate, null, step, NOW(), NOW() FROM chill_person_accompanying_period WHERE step = \'CONFIRMED\' + UNION + SELECT nextval(\'chill_person_accompanying_period_step_history_id_seq\'), id, openingDate, closingDate, \'CONFIRMED\', NOW(), NOW() FROM chill_person_accompanying_period WHERE step = \'CLOSED\' + UNION + SELECT nextval(\'chill_person_accompanying_period_step_history_id_seq\'), id, closingDate, null, \'CLOSED\', NOW(), NOW() FROM chill_person_accompanying_period WHERE step = \'CLOSED\' + ' + ); + } +}