feat: quiz page question rework (#181)

- Replace Bootstrap accordion with flat card list for questions
- Add HTML5 drag-and-drop reordering with placeholder-between-cards UX
  and amber/green/red save status indicator next to the heading
- Add edit button per question opening a Bootstrap modal (bo--modal-form
  Stimulus controller with X-Modal-Request header pattern)
- Show read-only view button instead of edit for locked/finalized quizzes
- Add BankQuestion edit modal in question bank tab using same infrastructure
- Move modal action buttons into modal-footer via <template data-modal-footer>
- Fix IS_AUTHENTICATED_FULLY 403: replace ROLE_USER with IS_AUTHENTICATED
  on all backoffice controllers and in security.yaml access_control
This commit is contained in:
2026-07-06 22:19:51 +02:00
parent 88cff7f480
commit 64a09453e6
18 changed files with 458 additions and 110 deletions
@@ -38,7 +38,7 @@ use Tvdt\Security\Voter\SeasonVoter;
use Tvdt\Service\QuestionBankService;
#[AsController]
#[IsGranted('ROLE_USER')]
#[IsGranted('IS_AUTHENTICATED')]
class QuestionBankController extends AbstractController
{
public function __construct(
@@ -120,6 +120,8 @@ class QuestionBankController extends AbstractController
{
$this->assertSameSeason($season, $bankQuestion->season);
$isModalRequest = $request->headers->has('X-Modal-Request');
$form = $this->createForm(BankQuestionFormType::class, $bankQuestion, ['season' => $season]);
$form->handleRequest($request);
@@ -130,14 +132,29 @@ class QuestionBankController extends AbstractController
$this->addFlash(FlashType::Success, $this->translator->trans('Question updated'));
if ($isModalRequest) {
return new Response('', Response::HTTP_NO_CONTENT);
}
return $this->redirectToRoute('tvdt_backoffice_question_bank', ['seasonCode' => $season->seasonCode]);
}
return $this->render('backoffice/question_bank/form.html.twig', [
$template = $isModalRequest
? 'backoffice/question_bank/_form_body.html.twig'
: 'backoffice/question_bank/form.html.twig';
$response = $this->render($template, [
'season' => $season,
'form' => $form,
'bankQuestion' => $bankQuestion,
'isModal' => $isModalRequest,
]);
if ($form->isSubmitted()) {
$response->setStatusCode(Response::HTTP_UNPROCESSABLE_ENTITY);
}
return $response;
}
#[IsCsrfTokenValid('delete_bank_question')]