feat: remind teamers of open questionnaires on the dashboard
This commit is contained in:
1 parent
5e4e574076
commit
890e04f8a6
7 files changed
+313
-4
No files matched your search
@@ -51,6 +51,20 @@ class QuestionnaireCatalog
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The services.yaml parameter questionnaire_go_live as a date: the teamer questionnaire is
|
||||
* offered for assignments ending on or after it, and to nobody while it is null.
|
||||
*/
|
||||
public static function parseGoLive(?string $goLive): ?\DateTimeImmutable
|
||||
{
|
||||
if (null === $goLive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return \DateTimeImmutable::createFromFormat('!Y-m-d', $goLive)
|
||||
?: throw new \InvalidArgumentException(sprintf('questionnaire_go_live "%s" is not a Y-m-d date', $goLive));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int|null $version null for the current version
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Controller\Teamer;
|
||||
|
||||
use App\Config\QuestionnaireCatalog;
|
||||
use App\Entity\Application;
|
||||
use App\Entity\Assignment;
|
||||
use App\Entity\Disposition;
|
||||
@@ -71,6 +72,14 @@ class IndexController extends AbstractController
|
||||
|
||||
$dispositionDocuments = $this->processDocuments($upcomingDispositions);
|
||||
|
||||
// offered to nobody until the go-live date is configured, as in DispositionVoter
|
||||
$questionnaireGoLive = QuestionnaireCatalog::parseGoLive($this->getParameter('questionnaire_go_live'));
|
||||
$pendingQuestionnaires = null === $questionnaireGoLive ? [] : $this
|
||||
->entityManager
|
||||
->getRepository(Disposition::class)
|
||||
->findPendingQuestionnaireByTeamer($teamer, $questionnaireGoLive)
|
||||
;
|
||||
|
||||
// the list merges in season assignments; the document notices above stay on dispositions
|
||||
$upcomingPlacements = array_slice($this->upcomingPlacementCollector->collect($teamer), 0, 5);
|
||||
|
||||
@@ -85,6 +94,7 @@ class IndexController extends AbstractController
|
||||
// the season page shows only this one, so only this one is linked
|
||||
'linkedSeasonAssignment' => $this->seasonAssignmentResolver->resolveFor($teamer),
|
||||
'dispositionDocuments' => $dispositionDocuments,
|
||||
'pendingQuestionnaires' => $pendingQuestionnaires,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Config\QuestionnaireCatalog;
|
||||
use App\Entity\Assignment;
|
||||
use App\Entity\Disposition;
|
||||
use App\Entity\QuestionnaireResponse;
|
||||
use App\Entity\Teamer;
|
||||
use App\Entity\Upload;
|
||||
use App\Enum\QuestionnaireResponseStatus;
|
||||
use App\Repository\Filter\SeasonPeriodFilter;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Comparison;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
@@ -72,6 +76,73 @@ class DispositionRepository extends ServiceEntityRepository
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* The teamer's placements whose questionnaire (config/questionnaires/teamer_escort.yaml) is
|
||||
* still to be submitted: not started, or saved as a draft. The rules are those of
|
||||
* DispositionVoter::QUESTIONNAIRE - bus escort, over (today included), on or after go-live,
|
||||
* neither called off nor skip-formalities - so every row shown on the dashboard can be opened.
|
||||
*
|
||||
* The response is joined as an arbitrary entity because the association is one-way; the
|
||||
* questionnaire key has to be part of the JOIN condition, or a response to another
|
||||
* questionnaire would hide this one.
|
||||
*
|
||||
* Rows are [0 => Disposition, 'responseStatus' => ?QuestionnaireResponseStatus].
|
||||
*/
|
||||
public function getPendingQuestionnaireQuery(Teamer $teamer, \DateTimeImmutable $goLive): Query
|
||||
{
|
||||
$qb = $this->createQueryBuilder('disposition');
|
||||
|
||||
return $qb
|
||||
->select('disposition', 'assignment', 'destination', 'response.status AS responseStatus')
|
||||
->innerJoin('disposition.assignment', 'assignment')
|
||||
->innerJoin('assignment.destination', 'destination')
|
||||
->leftJoin(
|
||||
QuestionnaireResponse::class,
|
||||
'response',
|
||||
Join::ON,
|
||||
'response.disposition = disposition AND response.questionnaire = :questionnaire'
|
||||
)
|
||||
->where($qb->expr()->andX(
|
||||
$qb->expr()->eq('disposition.teamer', ':teamer'),
|
||||
$qb->expr()->neq('disposition.status', ':dispositionCalledOff'),
|
||||
$qb->expr()->notIn('assignment.status', ':assignmentStatuses'),
|
||||
$qb->expr()->eq('assignment.skipFormalities', ':skipFormalities'),
|
||||
$qb->expr()->isNotNull('assignment.pickup'),
|
||||
$qb->expr()->neq('assignment.pickup', 0),
|
||||
$qb->expr()->gte('destination.dateTo', ':goLive'),
|
||||
$qb->expr()->lte('destination.dateTo', ':today'),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('response.id'),
|
||||
$qb->expr()->eq('response.status', ':draft')
|
||||
)
|
||||
))
|
||||
->orderBy('destination.dateTo', \SortDirection::Descending)
|
||||
->setParameter('teamer', $teamer)
|
||||
->setParameter('questionnaire', QuestionnaireCatalog::TEAMER_ESCORT)
|
||||
->setParameter('dispositionCalledOff', Disposition::STATUS_CALLED_OFF)
|
||||
->setParameter('assignmentStatuses', [Assignment::STATUS_CALLED_OFF, Assignment::STATUS_DELETED])
|
||||
->setParameter('skipFormalities', false)
|
||||
->setParameter('goLive', $goLive, Types::DATE_IMMUTABLE)
|
||||
->setParameter('today', new \DateTimeImmutable('today'), Types::DATE_IMMUTABLE)
|
||||
->setParameter('draft', QuestionnaireResponseStatus::DRAFT)
|
||||
->getQuery()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{disposition: Disposition, draft: bool}>
|
||||
*/
|
||||
public function findPendingQuestionnaireByTeamer(Teamer $teamer, \DateTimeImmutable $goLive): array
|
||||
{
|
||||
return array_map(
|
||||
static fn (array $row): array => [
|
||||
'disposition' => $row[0],
|
||||
'draft' => QuestionnaireResponseStatus::DRAFT === $row['responseStatus'],
|
||||
],
|
||||
$this->getPendingQuestionnaireQuery($teamer, $goLive)->getResult(),
|
||||
);
|
||||
}
|
||||
|
||||
public function getRecentDispositionsByTeamerQuery(Teamer $teamer): Query
|
||||
{
|
||||
$qb = $this->createQueryBuilder('disposition');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Security\Voter;
|
||||
|
||||
use App\Config\QuestionnaireCatalog;
|
||||
use App\Entity\Disposition;
|
||||
use App\Entity\Upload;
|
||||
use App\Entity\User;
|
||||
@@ -33,10 +34,7 @@ class DispositionVoter extends Voter
|
||||
private readonly Security $security,
|
||||
?string $questionnaireGoLive,
|
||||
) {
|
||||
$this->questionnaireGoLive = null === $questionnaireGoLive
|
||||
? null
|
||||
: (\DateTimeImmutable::createFromFormat('!Y-m-d', $questionnaireGoLive)
|
||||
?: throw new \InvalidArgumentException(sprintf('questionnaire_go_live "%s" is not a Y-m-d date', $questionnaireGoLive)));
|
||||
$this->questionnaireGoLive = QuestionnaireCatalog::parseGoLive($questionnaireGoLive);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
@@ -128,6 +126,9 @@ class DispositionVoter extends Voter
|
||||
* assignment ending today counts as over, as in DispositionRepository's feedback queries.
|
||||
*
|
||||
* Skip-formalities placements are left out, as they are from contract, invoice and feedback.
|
||||
*
|
||||
* DispositionRepository::getPendingQuestionnaireQuery() applies the same rules in DQL for the
|
||||
* dashboard; change both together.
|
||||
*/
|
||||
private function assertQuestionnaireAllowed(TokenInterface $token, Disposition $disposition): bool
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user