diff --git a/Justfile b/Justfile index ad99996..a584e10 100644 --- a/Justfile +++ b/Justfile @@ -23,7 +23,7 @@ fixtures: docker compose exec php bin/console doctrine:fixtures:load --purge-with-truncate --no-interaction --group=dev translations: - docker compose exec php bin/console translation:extract --force --format=xliff --sort=asc --clean nl + docker compose exec php bin/console translation:extract --force --format=xliff --sort=asc nl fix-cs: docker compose exec php vendor/bin/php-cs-fixer fix diff --git a/src/Controller/Backoffice/QuizController.php b/src/Controller/Backoffice/QuizController.php index 6261893..c1501fe 100644 --- a/src/Controller/Backoffice/QuizController.php +++ b/src/Controller/Backoffice/QuizController.php @@ -53,11 +53,12 @@ class QuizController extends AbstractController )] public function overview(Season $season, Quiz $quiz): Response { - $fetchedQuiz = $this->quizRepository->fetchWithQuestions($quiz->id); + $fetchedQuiz = $this->quizRepository->fetchWithQuestionsAndCandidates($quiz->id); return $this->render('backoffice/quiz.html.twig', [ 'season' => $season, 'quiz' => $fetchedQuiz, + 'questionErrors' => $fetchedQuiz->getQuestionErrors(), 'activeTab' => 'overview', 'template' => 'backoffice/quiz/tab_overview.html.twig', ]); diff --git a/src/Entity/Question.php b/src/Entity/Question.php index eefd3f9..cb63479 100644 --- a/src/Entity/Question.php +++ b/src/Entity/Question.php @@ -54,85 +54,6 @@ class Question return $this; } - public function getErrors(): ?string - { - if (0 === \count($this->answers)) { - return 'This question has no answers'; - } - - $correctAnswers = $this->answers->filter(static fn (Answer $answer): bool => $answer->isRightAnswer)->count(); - - if (0 === $correctAnswers) { - return 'This question has no correct answers'; - } - - if ($correctAnswers > 1) { - return 'This question has multiple correct answers'; - } - - // Check if any answer in the entire quiz has candidate relations - $hasCandidateRelations = false; - foreach ($this->quiz->questions as $quizQuestion) { - foreach ($quizQuestion->answers as $answer) { - if ($answer->candidates->count() > 0) { - $hasCandidateRelations = true; - break 2; - } - } - } - - // Only validate candidate-answer relations if at least one exists in the quiz - if ($hasCandidateRelations) { - $seasonCandidates = $this->quiz->season->candidates; - $candidateCounts = []; - - // Count how many times each candidate appears in answers - foreach ($this->answers as $answer) { - foreach ($answer->candidates as $candidate) { - $candidateId = $candidate->id->toString(); - if (!isset($candidateCounts[$candidateId])) { - $candidateCounts[$candidateId] = ['name' => $candidate->name, 'count' => 0]; - } - ++$candidateCounts[$candidateId]['count']; - } - } - - // Check for missing and duplicate candidates - $missing = []; - $duplicates = []; - - foreach ($seasonCandidates as $candidate) { - $candidateId = $candidate->id->toString(); - $count = $candidateCounts[$candidateId]['count'] ?? 0; - - if (0 === $count) { - $missing[] = $candidate->name; - } elseif ($count > 1) { - $duplicates[] = $candidate->name; - } - } - - if (!empty($missing) || !empty($duplicates)) { - $errors = []; - if (!empty($missing)) { - // If all candidates are missing, show a special message - if (\count($missing) === $seasonCandidates->count()) { - $errors[] = 'No candidates assigned to this question'; - } else { - $errors[] = 'Missing candidates: '.implode(', ', $missing); - } - } - if (!empty($duplicates)) { - $errors[] = 'Duplicate candidates: '.implode(', ', $duplicates); - } - - return implode('. ', $errors); - } - } - - return null; - } - public function __toString(): string { return $this->question; diff --git a/src/Entity/Quiz.php b/src/Entity/Quiz.php index 0d369d0..303cec4 100644 --- a/src/Entity/Quiz.php +++ b/src/Entity/Quiz.php @@ -68,4 +68,101 @@ class Quiz return $this; } + + /** + * Get errors for all questions in the quiz. + * Returns an array where keys are question IDs and values are error messages. + * @return array + */ + public function getQuestionErrors(): array + { + $errors = []; + + // Check if any answer in the entire quiz has candidate relations + $hasCandidateRelations = false; + foreach ($this->questions as $question) { + foreach ($question->answers as $answer) { + if ($answer->candidates->count() > 0) { + $hasCandidateRelations = true; + break 2; + } + } + } + + foreach ($this->questions as $question) { + $error = $this->getQuestionError($question, $hasCandidateRelations); + if ($error !== null) { + $errors[$question->id->toString()] = $error; + } + } + + return $errors; + } + + private function getQuestionError(Question $question, bool $hasCandidateRelations): ?string + { + if (0 === \count($question->answers)) { + return 'This question has no answers'; + } + + $correctAnswers = $question->answers->filter(static fn (Answer $answer): bool => $answer->isRightAnswer)->count(); + + if (0 === $correctAnswers) { + return 'This question has no correct answers'; + } + + if ($correctAnswers > 1) { + return 'This question has multiple correct answers'; + } + + // Only validate candidate-answer relations if at least one exists in the quiz + if ($hasCandidateRelations) { + $seasonCandidates = $this->season->candidates; + $candidateCounts = []; + + // Count how many times each candidate appears in answers + foreach ($question->answers as $answer) { + foreach ($answer->candidates as $candidate) { + $candidateId = $candidate->id->toString(); + if (!isset($candidateCounts[$candidateId])) { + $candidateCounts[$candidateId] = ['name' => $candidate->name, 'count' => 0]; + } + $candidateCounts[$candidateId]['count']++; + } + } + + // Check for missing and duplicate candidates + $missing = []; + $duplicates = []; + + foreach ($seasonCandidates as $candidate) { + $candidateId = $candidate->id->toString(); + $count = $candidateCounts[$candidateId]['count'] ?? 0; + + if ($count === 0) { + $missing[] = $candidate->name; + } elseif ($count > 1) { + $duplicates[] = $candidate->name; + } + } + + if (!empty($missing) || !empty($duplicates)) { + $errors = []; + if (!empty($missing)) { + // If all candidates are missing, show a special message + if (count($missing) === $seasonCandidates->count()) { + $errors[] = 'No candidates assigned to this question'; + } else { + $errors[] = 'Missing candidates: ' . implode(', ', $missing); + } + } + if (!empty($duplicates)) { + $errors[] = 'Duplicate candidates: ' . implode(', ', $duplicates); + } + return implode('. ', $errors); + } + } + + return null; + } } diff --git a/src/Entity/Season.php b/src/Entity/Season.php index a116000..6426f61 100644 --- a/src/Entity/Season.php +++ b/src/Entity/Season.php @@ -30,6 +30,7 @@ class Season /** @var Collection */ #[ORM\OneToMany(targetEntity: Quiz::class, mappedBy: 'season', cascade: ['persist'], orphanRemoval: true)] + #[ORM\OrderBy(['id' => 'ASC'])] public private(set) Collection $quizzes; /** @var Collection */ @@ -39,6 +40,7 @@ class Season /** @var Collection */ #[ORM\ManyToMany(targetEntity: User::class, inversedBy: 'seasons')] + #[ORM\OrderBy(['email' => 'ASC'])] public private(set) Collection $owners; #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')] diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index c274979..b8ffe67 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -17,12 +17,16 @@ class SettingsForm extends AbstractType { $builder ->add('showNumbers', options: [ + 'label' => 'Show Numbers', 'label_attr' => ['class' => 'checkbox-switch'], 'attr' => ['role' => 'switch', 'switch' => null]]) ->add('confirmAnswers', options: [ + 'label' => 'Confirm Answers', 'label_attr' => ['class' => 'checkbox-switch'], 'attr' => ['role' => 'switch', 'switch' => null]]) - ->add('save', SubmitType::class) + ->add('save', SubmitType::class, [ + 'label' => 'Save', + ]) ; } diff --git a/src/Repository/QuizRepository.php b/src/Repository/QuizRepository.php index 4fa3ae0..5190687 100644 --- a/src/Repository/QuizRepository.php +++ b/src/Repository/QuizRepository.php @@ -116,4 +116,21 @@ class QuizRepository extends ServiceEntityRepository where q.id = :id dql)->setParameter('id', $id)->getSingleResult(); } + + /** + * Fetch quiz with all relations needed for error checking. + * This includes: questions, answers, answer candidates, and season candidates. + */ + public function fetchWithQuestionsAndCandidates(Uuid $id): Quiz + { + return $this->getEntityManager()->createQuery(<<setParameter('id', $id)->getSingleResult(); + } } diff --git a/templates/backoffice/base.html.twig b/templates/backoffice/base.html.twig index 111d1d6..eecd557 100644 --- a/templates/backoffice/base.html.twig +++ b/templates/backoffice/base.html.twig @@ -2,3 +2,14 @@ {% block importmap %}{{ importmap('backoffice') }}{% endblock %} {% block title %}Tijd voor de test | {% endblock %} {% block nav %}{{ include('backoffice/nav.html.twig') }}{% endblock %} + +{% block main %} +
+
+ {% block breadcrumbs %}{% endblock %} +
+ {{ include('flashes.html.twig') }} + {% block body %} + {% endblock body %} +
+{% endblock %} diff --git a/templates/backoffice/index.html.twig b/templates/backoffice/index.html.twig index 43afa5b..4fd6c99 100644 --- a/templates/backoffice/index.html.twig +++ b/templates/backoffice/index.html.twig @@ -2,9 +2,17 @@ {% block title %}{{ parent() }}Backoffice{% endblock %} +{% block breadcrumbs %} + +{% endblock %} + {% block body %} -
-

+
+

{{ is_granted('ROLE_ADMIN') ? 'All Seasons'|trans : 'Your Seasons'|trans }}

@@ -12,7 +20,7 @@
{% if seasons %} - +
{% if is_granted('ROLE_ADMIN') %} diff --git a/templates/backoffice/prepare_elimination/index.html.twig b/templates/backoffice/prepare_elimination/index.html.twig index b873316..1c50933 100644 --- a/templates/backoffice/prepare_elimination/index.html.twig +++ b/templates/backoffice/prepare_elimination/index.html.twig @@ -1,20 +1,17 @@ {% extends 'backoffice/base.html.twig' %} -{% block body %} -
- -
+{% block breadcrumbs %} + +{% endblock %} + +{% block body %}
@@ -32,7 +29,7 @@
{% endfor %} -
+
@@ -42,7 +39,7 @@
-

Hier kan dus weer wat uitleg komen

+

{{ 'Help text for preparing elimination'|trans }}

{% endblock %} diff --git a/templates/backoffice/quiz.html.twig b/templates/backoffice/quiz.html.twig index 8922410..e1d99fb 100644 --- a/templates/backoffice/quiz.html.twig +++ b/templates/backoffice/quiz.html.twig @@ -2,6 +2,16 @@ {% block title %}{{ parent() }}{{ season.name }}{% endblock %} +{% block breadcrumbs %} + +{% endblock %} + {% block body %} {% set tabs = [ {id: 'overview', label: 'Overview'|trans, route: 'tvdt_backoffice_quiz_overview'}, @@ -9,8 +19,8 @@ {id: 'candidates', label: 'Candidates'|trans, route: 'tvdt_backoffice_quiz_candidates'}, ] %} -

{{ 'Quiz'|trans }}: {{ season.name }} - {{ quiz.name }}

-
+
{% for answer in question.answers %} - + {% endfor %} diff --git a/templates/backoffice/quiz/tab_overview.html.twig b/templates/backoffice/quiz/tab_overview.html.twig index be3e34c..bb2f1e5 100644 --- a/templates/backoffice/quiz/tab_overview.html.twig +++ b/templates/backoffice/quiz/tab_overview.html.twig @@ -1,5 +1,5 @@ -

Quick actions

-
{{ 'Candidate'|trans }}{{ answer }}{{ answer }}
+

{{ 'Number of dropouts:'|trans }} {{ quiz.dropouts }}

+
@@ -75,4 +74,3 @@ {% endfor %}
{{ 'Candidate'|trans }}
-

diff --git a/templates/backoffice/quiz_add.html.twig b/templates/backoffice/quiz_add.html.twig index 9931f6f..a43c0e1 100644 --- a/templates/backoffice/quiz_add.html.twig +++ b/templates/backoffice/quiz_add.html.twig @@ -1,9 +1,19 @@ {% extends 'backoffice/base.html.twig' %} +{% block breadcrumbs %} + +{% endblock %} + {% block body %}
-

{{ t('Add a quiz to %name%', {'%name%': season.name})|trans }}

+

{{ t('Add a quiz to %name%', {'%name%': season.name})|trans }}

{{ form_start(form) }} {{ form_row(form.name) }} {{ form_row(form.sheet) }} @@ -11,13 +21,11 @@ {{ form_end(form) }}
-

- Hier kan nog tekst komen met wat uitleg +

+ {{ 'Help text for adding a quiz'|trans }}

-
- {% endblock %} {% block title %} diff --git a/templates/backoffice/season.html.twig b/templates/backoffice/season.html.twig index 2a55e86..9b5d88f 100644 --- a/templates/backoffice/season.html.twig +++ b/templates/backoffice/season.html.twig @@ -1,37 +1,47 @@ {% extends 'backoffice/base.html.twig' %} {% block title %}{{ parent() }}{{ season.name }}{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + {% block body %} -

{{ 'Season'|trans }}: {{ season.name }}

+

{{ 'Season'|trans }}: {{ season.name }}

-
-

{{ 'Quizzes'|trans }}

+
+

{{ 'Quizzes'|trans }}

{{ 'Add'|trans }}
-
+
{% for quiz in season.quizzes %} {{ quiz.name }} {% else %} - No quizzes + {{ 'No quizzes'|trans }} {% endfor %}
-
-

{{ 'Candidates'|trans }}

+
+

{{ 'Candidates'|trans }}

{{ 'Add Candidate'|trans }}
-
    +
      {% for candidate in season.candidates %}
    • {{ candidate.name }}
    • {% endfor %}
    -
    -

    {{ 'Settings'|trans }}

    +
    +

    {{ 'Settings'|trans }}

    {{ form(form) }}
    diff --git a/templates/backoffice/season_add.html.twig b/templates/backoffice/season_add.html.twig index fa2953b..a907712 100644 --- a/templates/backoffice/season_add.html.twig +++ b/templates/backoffice/season_add.html.twig @@ -1,17 +1,26 @@ {% extends 'backoffice/base.html.twig' %} +{% block breadcrumbs %} + +{% endblock %} + {% block body %}
    -

    {{ 'Create a season'|trans }}

    +

    {{ 'Create a season'|trans }}

    {{ form_start(form) }} {{ form_row(form.name) }} {{ form_end(form) }}
    -

    - Hier kan nog tekst komen met wat uitleg +

    + {{ 'Help text for creating a season'|trans }}

    diff --git a/templates/backoffice/season_add_candidates.html.twig b/templates/backoffice/season_add_candidates.html.twig index 783af97..319263c 100644 --- a/templates/backoffice/season_add_candidates.html.twig +++ b/templates/backoffice/season_add_candidates.html.twig @@ -1,17 +1,27 @@ {% extends 'backoffice/base.html.twig' %} +{% block breadcrumbs %} + +{% endblock %} + {% block body %}
    -

    {{ 'Add Candidates'|trans }}

    +

    {{ 'Add Candidates'|trans }}

    {{ form_start(form) }} {{ form_row(form.candidates) }} {{ form_end(form) }}
    -

    - Hier kan nog tekst komen met wat uitleg +

    + {{ 'Help text for adding candidates'|trans }}

    diff --git a/translations/messages+intl-icu.nl.xliff b/translations/messages+intl-icu.nl.xliff index a92915c..c4d522e 100644 --- a/translations/messages+intl-icu.nl.xliff +++ b/translations/messages+intl-icu.nl.xliff @@ -21,6 +21,10 @@ Add Candidates Voeg kandidaten toe + + Add Quiz + Test toevoegen + Add a quiz to %name% Voeg een test toe aan %name% @@ -35,11 +39,11 @@ Are you sure you want to clear all the results? This will also delete al the eliminations. - Weet je zeker datatype je de resultaten will leegmaken? Dit gooit ook alle eliminaties weg. + Weet je zeker dat je de resultaten wilt leegmaken? Dit gooit ook alle eliminaties weg. Are you sure you want to delete this quiz? - Weet je zeker datatype je deze test will verwijderen? + Weet je zeker dat je deze test wilt verwijderen? Back @@ -49,6 +53,10 @@ Candidate Kandidaat + + Candidate answers saved + Kandidaatantwoorden opgeslagen + Candidate not found Kandidaat niet gevonden @@ -115,12 +123,32 @@ Error clearing quiz - Fout bij leegmaken test + Fout bij het leegmaken van de test Green Groen + + Help text for adding a quiz + Upload een XLSX-bestand met vragen en antwoorden voor je test. + + + Help text for adding candidates + Voeg kandidaten toe aan dit seizoen. Eén naam per regel. + + + Help text for creating a season + Maak een nieuw seizoen aan om tests en kandidaten te beheren. + + + Help text for preparing elimination + Kies welke kandidaten groen of rood krijgen voor de eliminatie. + + + Home + Home + Invalid season code Ongeldige seizoencode @@ -161,6 +189,10 @@ No active quiz Geen actieve test + + No quizzes + Geen tests + No results Geen resultaten @@ -175,7 +207,7 @@ Owner(s) - Eigena(a)r(en) + Eigenaar(s) Password @@ -183,11 +215,19 @@ Penalty - Straf + Straftijd Please Confirm - Bevestig Alsjeblieft + Bevestig alsjeblieft + + + Confirm Answers + Bevestig antwoorden + + + Show Numbers + Toon nummers Please Confirm your Email @@ -205,6 +245,14 @@ Prepare Custom Elimination Bereid aangepaste eliminatie voor + + Prepare Elimination + Bereid eliminatie voor + + + Previous + Vorige + Questions Vragen @@ -267,7 +315,7 @@ Save and start elimination - Opslaan en start eliminatie + Opslaan en eliminatie starten Score