@@ -563,7 +563,7 @@ protected function addEntityCheckbox(array &$data, object $entity, mixed $identi
563563 '<input type="checkbox" name="ids[]" value="%s" class="%s"> ' ,
564564 $ identifierValue ,
565565 $ checkboxClass
566- )
566+ ),
567567 ];
568568 }
569569 }
@@ -665,4 +665,120 @@ protected function processAddFormForExternalIdEntity(
665665
666666 return null ;
667667 }
668+
669+ /**
670+ * Get the previous and next object IDs for navigation.
671+ *
672+ * @param class-string $entityClass Entity class to query
673+ * @param mixed $currentIdValue Current value of the ID field
674+ * @param string $idField Field to return as the ID (e.g., 'externalid', 'submitid')
675+ * @param array<string, 'ASC'|'DESC'> $orderBy Sort criteria as field => direction (e.g., ['e.submittime' => 'ASC', 'e.submitid' => 'ASC'])
676+ * @param bool $filterOnContest Whether to filter results by current contests
677+ *
678+ * @return array{previous: string|int|null, next: string|int|null}
679+ */
680+ protected function getPreviousAndNextObjectIds (
681+ string $ entityClass ,
682+ mixed $ currentIdValue ,
683+ string $ idField = 'externalid ' ,
684+ array $ orderBy = ['e.externalid ' => 'ASC ' ],
685+ bool $ filterOnContest = false ,
686+ ): array {
687+ $ result = ['previous ' => null , 'next ' => null ];
688+
689+ // Fetch the current entity once to get field values
690+ $ currentEntity = $ this ->em ->getRepository ($ entityClass )->findOneBy ([$ idField => $ currentIdValue ]);
691+ if ($ currentEntity === null ) {
692+ return $ result ;
693+ }
694+
695+ $ accessor = PropertyAccess::createPropertyAccessor ();
696+
697+ // Pre-compute field values for comparison
698+ $ fieldValues = [];
699+ foreach (array_keys ($ orderBy ) as $ field ) {
700+ $ fieldName = str_replace ('e. ' , '' , $ field );
701+ $ fieldValues [$ field ] = $ accessor ->getValue ($ currentEntity , $ fieldName );
702+ }
703+
704+ // Build the comparison conditions based on the sort criteria.
705+ // For multi-column ordering, we need: (col1 < val1) OR (col1 = val1 AND col2 < val2) etc.
706+ $ buildComparisonConditions = function (string $ operator ) use ($ orderBy , $ fieldValues ): array {
707+ $ conditions = [];
708+ $ parameters = [];
709+ $ fields = array_keys ($ orderBy );
710+ $ directions = array_values ($ orderBy );
711+
712+ for ($ i = 0 ; $ i < count ($ fields ); $ i ++) {
713+ $ equalityParts = [];
714+ // Add equality conditions for all previous columns
715+ for ($ j = 0 ; $ j < $ i ; $ j ++) {
716+ $ field = $ fields [$ j ];
717+ $ paramName = 'eq_ ' . $ j ;
718+ $ equalityParts [] = "$ field = : $ paramName " ;
719+ $ parameters [$ paramName ] = $ fieldValues [$ field ];
720+ }
721+
722+ // Add the comparison for this column
723+ $ field = $ fields [$ i ];
724+ $ direction = $ directions [$ i ];
725+ // For "previous": if ASC, we want < ; if DESC, we want >
726+ // For "next": if ASC, we want > ; if DESC, we want <
727+ $ compOp = ($ operator === 'previous ' )
728+ ? ($ direction === 'ASC ' ? '< ' : '> ' )
729+ : ($ direction === 'ASC ' ? '> ' : '< ' );
730+ $ paramName = 'cmp_ ' . $ i ;
731+ $ comparisonPart = "$ field $ compOp : $ paramName " ;
732+ $ parameters [$ paramName ] = $ fieldValues [$ field ];
733+
734+ if (!empty ($ equalityParts )) {
735+ $ conditions [] = '( ' . implode (' AND ' , $ equalityParts ) . ' AND ' . $ comparisonPart . ') ' ;
736+ } else {
737+ $ conditions [] = '( ' . $ comparisonPart . ') ' ;
738+ }
739+ }
740+
741+ return ['condition ' => implode (' OR ' , $ conditions ), 'parameters ' => $ parameters ];
742+ };
743+
744+ foreach (['previous ' , 'next ' ] as $ direction ) {
745+ $ qb = $ this ->em ->createQueryBuilder ()
746+ ->select ("e. $ idField " )
747+ ->from ($ entityClass , 'e ' );
748+
749+ // Build and apply the comparison conditions
750+ $ comp = $ buildComparisonConditions ($ direction );
751+ if (!empty ($ comp ['condition ' ])) {
752+ $ qb ->andWhere ($ comp ['condition ' ]);
753+ foreach ($ comp ['parameters ' ] as $ param => $ value ) {
754+ $ qb ->setParameter ($ param , $ value );
755+ }
756+ }
757+
758+ // Apply contest filter
759+ if ($ filterOnContest && $ contest = $ this ->dj ->getCurrentContest ()) {
760+ $ qb ->andWhere ('e.contest = :contest ' )
761+ ->setParameter ('contest ' , $ contest );
762+ }
763+
764+ // Apply ordering (reversed for previous)
765+ foreach ($ orderBy as $ field => $ dir ) {
766+ $ actualDir = $ direction === 'previous '
767+ ? ($ dir === 'ASC ' ? 'DESC ' : 'ASC ' )
768+ : $ dir ;
769+ $ qb ->addOrderBy ($ field , $ actualDir );
770+ }
771+
772+ $ qb ->setMaxResults (1 );
773+
774+ try {
775+ $ value = $ qb ->getQuery ()->getSingleScalarResult ();
776+ $ result [$ direction ] = $ value ;
777+ } catch (NoResultException ) {
778+ // No previous/next found, leave as null
779+ }
780+ }
781+
782+ return $ result ;
783+ }
668784}
0 commit comments