* * @method Disposition|null find($id, $lockMode = null, $lockVersion = null) * @method Disposition|null findOneBy(array $criteria, array $orderBy = null) * @method Disposition[] findAll() * @method Disposition[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ class DispositionRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Disposition::class); } public function getUpcomingDispositionsByTeamerQuery(Teamer $teamer): Query { $qb = $this->createQueryBuilder('disposition'); return $qb ->select('disposition', 'assignment', 'job_profile', 'destination') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.jobProfile', 'job_profile') ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->andX( $qb->expr()->eq('disposition.teamer', ':teamer'), $qb->expr()->neq('assignment.status', ':assignmentStatus'), $qb->expr()->neq('disposition.status', ':dispositionStatus'), $qb->expr()->orX( $qb->expr()->andX( $qb->expr()->isNull('assignment.dateTo'), $qb->expr()->gte('destination.dateTo', ':date') ), $qb->expr()->andX( $qb->expr()->isNotNull('assignment.dateTo'), $qb->expr()->gte('assignment.dateTo', ':date') ) ) )) ->setParameter('teamer', $teamer) ->setParameter('assignmentStatus', Assignment::STATUS_CALLED_OFF) ->setParameter('dispositionStatus', Disposition::STATUS_CALLED_OFF) ->setParameter('date', new \DateTimeImmutable()) ->getQuery() ; } public function getUpcomingDispositionsByTeamer(Teamer $teamer, int $limit = 5): array { return $this ->getUpcomingDispositionsByTeamerQuery($teamer) ->setMaxResults($limit) ->getResult() ; } public function getRecentDispositionsByTeamerQuery(Teamer $teamer): Query { $qb = $this->createQueryBuilder('disposition'); return $qb ->select('disposition', 'assignment', 'job_profile', 'destination', 'feedback') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.jobProfile', 'job_profile') ->innerJoin('assignment.destination', 'destination') ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->andX( $qb->expr()->eq('disposition.teamer', ':teamer'), $qb->expr()->neq('assignment.status', ':assignmentStatus'), $qb->expr()->neq('disposition.status', ':dispositionStatus'), $qb->expr()->orX( $qb->expr()->andX( $qb->expr()->isNull('assignment.dateTo'), $qb->expr()->lte('destination.dateTo', ':date') ), $qb->expr()->andX( $qb->expr()->isNotNull('assignment.dateTo'), $qb->expr()->lte('assignment.dateTo', ':date') ) ) )) ->setParameter('teamer', $teamer) ->setParameter('assignmentStatus', Assignment::STATUS_CALLED_OFF) ->setParameter('dispositionStatus', Disposition::STATUS_CALLED_OFF) ->setParameter('date', new \DateTimeImmutable()) ->getQuery() ; } public function findCurrentDispositionsByAssignment(Assignment $assignment): array { $qb = $this->createQueryBuilder('disposition'); return $qb ->select('disposition', 'teamer', 'user', 'feedback') ->innerJoin('disposition.teamer', 'teamer') ->leftJoin('teamer.user', 'user') ->leftJoin('teamer.feedback', 'feedback') ->where($qb->expr()->eq('disposition.assignment', ':assignment')) ->setParameter('assignment', $assignment) ->getQuery() ->getResult() ; } public function findRecentDispositionsByTeamer(Teamer $teamer, int $limit = 5): array { $qb = $this->createQueryBuilder('disposition'); return $qb ->select('disposition', 'assignment', 'destination', 'feedback') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->andX( $qb->expr()->eq('disposition.teamer', ':teamer'), $qb->expr()->neq('assignment.status', ':assignmentStatus'), $qb->expr()->neq('disposition.status', ':dispositionStatus') )) ->setParameter('teamer', $teamer) ->setParameter('assignmentStatus', Assignment::STATUS_CALLED_OFF) ->setParameter('dispositionStatus', Disposition::STATUS_CALLED_OFF) ->orderBy('destination.dateFrom', 'DESC') ->setMaxResults($limit) ->getQuery() ->getResult() ; } public function findCurrentDispositionsByTeamer(Teamer $teamer): array { $qb = $this->createQueryBuilder('disposition'); return $qb ->select('disposition', 'assignment', 'destination') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->andX( $qb->expr()->eq('disposition.teamer', ':teamer'), $qb->expr()->in('disposition.status', ':status') )) ->setParameter('teamer', $teamer) ->setParameter('status', [ Disposition::STATUS_NEW, Disposition::STATUS_CONFIRMED, ]) ->orderBy('destination.dateFrom', 'ASC') ->getQuery() ->getResult() ; } public function findNewDispositionsByHotelCodes(mixed $hotelCode): array { $hotelCodes = (array) $hotelCode; $qb = $this->createQueryBuilder('disposition'); return $qb ->select('disposition', 'assignment', 'job_profile', 'destination') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.jobProfile', 'job_profile') ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->andX( $qb->expr()->orX( $qb->expr()->in($qb->expr()->substring('destination.hotelCode', 1, 3), ':hotelCodes'), $qb->expr()->in($qb->expr()->substring('destination.hotelCode', -3, 3), ':hotelCodes'), ), $qb->expr()->gte('destination.dateTo', ':dateTo'), $qb->expr()->eq('disposition.status', ':status') )) ->orderBy('destination.dateFrom', 'ASC') ->setParameter('hotelCodes', $hotelCodes) ->setParameter('dateTo', new \DateTimeImmutable()) ->setParameter('status', Disposition::STATUS_CONFIRMED) ->getQuery() ->getResult() ; } public function getDispositionsWithPendingFeedbackQuery(mixed $hotelCode = null, ?int $offsetDays = null): Query { $hotelCodes = (array) $hotelCode; $qb = $this->createQueryBuilder('disposition'); $qb ->select('disposition', 'assignment', 'job_profile', 'destination', 'feedback', 'teamer', 'user') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.jobProfile', 'job_profile') ->innerJoin('disposition.teamer', 'teamer') ->innerJoin('teamer.user', 'user') ->innerJoin('assignment.destination', 'destination') ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->andX( $qb->expr()->notIn('assignment.status', ':status'), $qb->expr()->orX( $qb->expr()->andX( $qb->expr()->isNotNull('assignment.dateTo'), $qb->expr()->lte('assignment.dateTo', ':dateTo') ), $qb->expr()->lte('destination.dateTo', ':dateTo') ), $qb->expr()->isNull('feedback') )) ->setParameter('status', [Assignment::STATUS_CALLED_OFF, Assignment::STATUS_DELETED]) ; $dateTo = new \DateTimeImmutable(); if (null !== $offsetDays) { $dateTo = $dateTo->modify('-'.$offsetDays.' days'); } $qb->setParameter('dateTo', $dateTo); if (null !== $hotelCode) { $qb ->andWhere($qb->expr()->orX( $qb->expr()->in($qb->expr()->substring('destination.hotelCode', 1, 3), ':hotelCodes'), $qb->expr()->in($qb->expr()->substring('destination.hotelCode', -3, 3), ':hotelCodes'), )) ->setParameter('hotelCodes', $hotelCodes) ; } return $qb->getQuery(); } public function findDispositionsWithOverdueFeedback(?int $offsetDays = null): array { return $this ->getDispositionsWithPendingFeedbackQuery(null, $offsetDays) ->getResult() ; } public function findDispositionsWithPendingFeedbackByHotelCodes(mixed $hotelCode): array { return $this ->getDispositionsWithPendingFeedbackQuery($hotelCode) ->getResult() ; } /** * @return Disposition[]|array */ public function findEndedDispositions(): array { $qb = $this->createQueryBuilder('disposition'); return $qb ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') ->where($qb->expr()->andX( $qb->expr()->in('disposition.status', ':status'), $qb->expr()->orX( $qb->expr()->andX( $qb->expr()->isNotNull('assignment.dateTo'), $qb->expr()->lte('assignment.dateTo', ':dateTo') ), $qb->expr()->lte('destination.dateTo', ':dateTo') ) )) ->setParameter('status', [ Disposition::STATUS_NEW, Disposition::STATUS_CHECKING_CONTRACT, Disposition::STATUS_CONFIRMED, ]) ->setParameter('dateTo', new \DateTimeImmutable()) ->getQuery() ->getResult() ; } public function getNew(int $limit = 5): array { $qb = $this->createQueryBuilder('disposition'); return $qb ->select('disposition', 'assignment', 'destination', 'job_profile') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') ->innerJoin('assignment.jobProfile', 'job_profile') ->where($qb->expr()->gt('destination.dateFrom', ':dateFrom')) ->orderBy('disposition.createdAt', 'DESC') ->setParameter('dateFrom', new \DateTimeImmutable()) ->setMaxResults($limit) ->getQuery() ->getResult() ; } /** * Returns feedback statistics per hotel for dispositions with status 'ended' or 'completed'. * * @return array */ public function getFeedbackStatisticsByHotel( ?\DateTimeImmutable $dateFrom = null, ?\DateTimeImmutable $dateTo = null, ): array { $qb = $this->createQueryBuilder('disposition'); $qb ->select( 'destination.hotelCode AS hotelCode', 'destination.hotel AS hotel', 'COUNT(disposition.id) AS totalDispositions', 'SUM(CASE WHEN disposition.feedback IS NOT NULL THEN 1 ELSE 0 END) AS providedFeedbacks' ) ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->in('disposition.status', ':statuses')) ->setParameter('statuses', [ Disposition::STATUS_ENDED, Disposition::STATUS_PAID, Disposition::STATUS_COMPLETED, ]) ->groupBy('destination.hotelCode', 'destination.hotel') ->orderBy('destination.hotel', 'ASC') ; // Filter by effective date range (assignment date or fallback to destination date) if (null !== $dateFrom) { $qb ->andWhere($qb->expr()->orX( $qb->expr()->andX( $qb->expr()->isNotNull('assignment.dateFrom'), $qb->expr()->gte('assignment.dateFrom', ':dateFrom') ), $qb->expr()->andX( $qb->expr()->isNull('assignment.dateFrom'), $qb->expr()->gte('destination.dateFrom', ':dateFrom') ) )) ->setParameter('dateFrom', $dateFrom) ; } if (null !== $dateTo) { $qb ->andWhere($qb->expr()->orX( $qb->expr()->andX( $qb->expr()->isNotNull('assignment.dateTo'), $qb->expr()->lte('assignment.dateTo', ':dateTo') ), $qb->expr()->andX( $qb->expr()->isNull('assignment.dateTo'), $qb->expr()->lte('destination.dateTo', ':dateTo') ) )) ->setParameter('dateTo', $dateTo) ; } $results = $qb->getQuery()->getResult(); // Calculate missing feedbacks and percentage return array_map(function (array $row): array { $total = (int) $row['totalDispositions']; $provided = (int) $row['providedFeedbacks']; $missing = $total - $provided; $percentage = $total > 0 ? round(($provided / $total) * 100, 1) : 0.0; return [ 'hotelCode' => $row['hotelCode'], 'hotel' => $row['hotel'], 'totalDispositions' => $total, 'providedFeedbacks' => $provided, 'missingFeedbacks' => $missing, 'percentageProvided' => $percentage, ]; }, $results); } /** * Returns feedback statistics grouped by normalized hotel code (base 3-char code). * * @return array */ public function getFeedbackStatisticsByNormalizedHotelCode( ?\DateTimeImmutable $dateFrom = null, ?\DateTimeImmutable $dateTo = null, ): array { $qb = $this->createQueryBuilder('disposition'); // Normalize hotel code: strip SER prefix to get base 3-char code $normalizedHotelCode = "CASE WHEN destination.hotelCode LIKE 'SER%' THEN SUBSTRING(destination.hotelCode, 4, 3) ELSE SUBSTRING(destination.hotelCode, 1, 3) END"; $qb ->select( $normalizedHotelCode.' AS hotelCode', 'COUNT(disposition.id) AS totalDispositions', 'SUM(CASE WHEN disposition.feedback IS NOT NULL THEN 1 ELSE 0 END) AS providedFeedbacks' ) ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.destination', 'destination') ->leftJoin('disposition.feedback', 'feedback') ->where($qb->expr()->in('disposition.status', ':statuses')) ->setParameter('statuses', [ Disposition::STATUS_ENDED, Disposition::STATUS_PAID, Disposition::STATUS_COMPLETED, ]) ->groupBy('hotelCode') ->orderBy('hotelCode', 'ASC') ; if (null !== $dateFrom) { $qb ->andWhere($qb->expr()->orX( $qb->expr()->andX( $qb->expr()->isNotNull('assignment.dateFrom'), $qb->expr()->gte('assignment.dateFrom', ':dateFrom') ), $qb->expr()->andX( $qb->expr()->isNull('assignment.dateFrom'), $qb->expr()->gte('destination.dateFrom', ':dateFrom') ) )) ->setParameter('dateFrom', $dateFrom) ; } if (null !== $dateTo) { $qb ->andWhere($qb->expr()->orX( $qb->expr()->andX( $qb->expr()->isNotNull('assignment.dateTo'), $qb->expr()->lte('assignment.dateTo', ':dateTo') ), $qb->expr()->andX( $qb->expr()->isNull('assignment.dateTo'), $qb->expr()->lte('destination.dateTo', ':dateTo') ) )) ->setParameter('dateTo', $dateTo) ; } $results = $qb->getQuery()->getResult(); return array_map(function (array $row): array { $total = (int) $row['totalDispositions']; $provided = (int) $row['providedFeedbacks']; $missing = $total - $provided; $percentage = $total > 0 ? round(($provided / $total) * 100, 1) : 0.0; return [ 'hotelCode' => $row['hotelCode'], 'totalDispositions' => $total, 'providedFeedbacks' => $provided, 'missingFeedbacks' => $missing, 'percentageProvided' => $percentage, ]; }, $results); } public function findOverdueContracts(): array { $dueDate = (new \DateTimeImmutable())->modify('-7 days'); $qb = $this->createQueryBuilder('disposition'); return $qb ->select('disposition', 'assignment', 'job_profile', 'destination', 'document', 'teamer', 'user') ->innerJoin('disposition.assignment', 'assignment') ->innerJoin('assignment.jobProfile', 'job_profile') ->innerJoin('assignment.destination', 'destination') ->leftJoin('disposition.documents', 'document') ->innerJoin('disposition.teamer', 'teamer') ->innerJoin('teamer.user', 'user') ->where($qb->expr()->andX( $qb->expr()->lte('disposition.createdAt', ':dueDate'), $qb->expr()->eq('disposition.status', ':status'), $qb->expr()->isNull('document') )) ->setParameter('dueDate', $dueDate) ->setParameter('status', Disposition::STATUS_NEW) ->orderBy('disposition.createdAt', 'ASC') ->getQuery() ->getResult() ; } }