Files
survey-bundle/src/Controller/FrontendModule/MemberSurveyListController.php
T
admin.juergen c7439e4822 Fix customer-reported survey results and list issues
Behebt die Fehler aus der Kunden-Testrunde (Praktikantin, Juli 2026):

- Ergebnisreihenfolge: Fragen werden vor dem Finalisieren nach _sorting
  sortiert. finalizeQuestionResult entfernt _sorting, daher fiel die
  bisherige Nachsortierung auf den alphabetischen Fallback zurück. Online,
  PDF und Excel folgen jetzt derselben Frage-Sortierung.
- Excel-Export: Eigene Fragetext-Zeile über den Antwortspalten (je Frage
  über die zugehörigen Spalten zusammengeführt). Zuvor fehlte der Fragetext.
- Range-Ergebnis: Verteilung deckt die komplette Skala rangeMin..rangeMax in
  rangeStep-Schritten ab, damit die X-Achse linear und vollständig ist
  (vorher nur tatsächlich vorkommende Werte in gleichen Abständen).
- Teilnahmen/Rücklauf pro Frage als absolute Zahlen ("X von Y beantwortet",
  "Z übersprungen") statt reiner Prozent-Rücklaufquote.
- Freitextantworten gruppiert: jede Antwort einmal mit exakter Häufigkeit,
  ohne Prozentangaben und ohne Doppel-Auflistung.
- Frontend-Liste: "Link kopieren"-Button (absolute Reader-URL) für
  veröffentlichte Umfragen; Aktiv-Umschalter mit Textlabel statt nur Punkt;
  Aktionslinks öffnen im selben Tab statt in neuen Tabs.
- Duplizieren: Titel-Kollisionsprüfung ohne Ausschluss der Quell-ID, damit
  aus "... - Kopie" korrekt "... - Kopie 2" wird statt eines Dubletten-Titels.

Der internalNote-Fehler beim Anlegen/Duplizieren war ein fehlendes
DB-Schema-Update (Spalte tl_survey.internalNote) und wurde per
contao:migrate auf der Testumgebung nachgezogen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 19:30:34 +02:00

160 lines
7.2 KiB
PHP

<?php
declare(strict_types=1);
namespace Mummert\SurveyBundle\Controller\FrontendModule;
use Contao\Config;
use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
use Contao\CoreBundle\Twig\FragmentTemplate;
use Contao\FrontendUser;
use Contao\ModuleModel;
use Contao\PageModel;
use Mummert\SurveyBundle\Model\SurveyModel;
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
use Mummert\SurveyBundle\Repository\SurveyRepository;
use Mummert\SurveyBundle\Service\SurveyEditorService;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AsFrontendModule(type: 'member_survey_list', category: 'survey', template: 'member_survey_list')]
#[AsFrontendModule(type: 'member_survey_template_list', category: 'survey', template: 'member_survey_list')]
final class MemberSurveyListController extends AbstractFrontendModuleController
{
public function __construct(
private readonly SurveyRepository $surveyRepository,
private readonly SurveyEditorRepository $surveyEditorRepository,
private readonly SurveyEditorService $surveyEditorService,
) {
}
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
{
$templatesOnly = 'member_survey_template_list' === (string) ($model->type ?? '');
$user = $this->getUser();
if (!$user instanceof FrontendUser) {
$template->set('loginRequired', true);
$template->set('surveys', []);
$template->set('createUrl', null);
$template->set('templatesOnly', $templatesOnly);
return $template->getResponse();
}
if ($request->isMethod('POST') && $request->request->has('_survey_action')) {
return $this->handleAction($request, (int) $user->id);
}
$editPage = $templatesOnly ? null : $this->resolvePage((int) ($model->surveyEditPage ?? 0));
$readerPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyReaderPage'));
$resultsPage = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyResultsPage'));
$items = [];
foreach ($this->surveyRepository->findEditableByMember((int) $user->id, $templatesOnly) as $survey) {
$surveyId = (int) $survey['id'];
$alias = (string) $survey['alias'];
$items[] = [
'id' => $surveyId,
'title' => (string) $survey['title'],
'description' => (string) ($survey['description'] ?? ''),
'isTemplate' => !empty($survey['isTemplate']),
'isActive' => !empty($survey['isActive']),
'isLocked' => !empty($survey['isLocked']),
'published' => !empty($survey['published']),
'questionCount' => (int) ($survey['questionCount'] ?? 0),
'participationCount' => (int) ($survey['participationCount'] ?? 0),
'editUrl' => $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['survey' => $surveyId]) : null,
'publicUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['parameters' => '/'.$alias]) : null,
// Absolute URL für den "Link kopieren"-Button, damit der geteilte
// Link auch außerhalb der Seite funktioniert.
'copyUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['parameters' => '/'.$alias], UrlGeneratorInterface::ABSOLUTE_URL) : null,
'draftUrl' => $readerPage instanceof PageModel ? $this->generateContentUrl($readerPage, ['parameters' => '/'.$alias, 'preview' => '1']) : null,
'resultsUrl' => $resultsPage instanceof PageModel ? $this->generateContentUrl($resultsPage, ['parameters' => '/'.$alias]) : null,
];
}
if ($templatesOnly && [] === $items) {
return new Response('');
}
$template->set('loginRequired', false);
$template->set('surveys', $items);
$template->set('createUrl', !$templatesOnly && $editPage instanceof PageModel ? $this->generateContentUrl($editPage, ['create' => '1']) : null);
$template->set('templatesOnly', $templatesOnly);
return $template->getResponse();
}
private function handleAction(Request $request, int $memberId): Response
{
$surveyId = (int) $request->request->get('item_id', 0);
$action = (string) $request->request->get('_survey_action');
$token = (string) $request->request->get('_token', '');
if ($surveyId <= 0 || '' === $action) {
throw new \RuntimeException('Ungültige Umfrage-Aktion.');
}
// Session-basierter Symfony-Token (wie im Editor): zuverlässig für
// eingeloggte Mitglieder. Der Contao-REQUEST_TOKEN ist cookie-gebunden
// und im Frontend-Fragment-Flow nicht stabil verfügbar.
if (!$this->isCsrfTokenValid($action.'-'.$surveyId, $token)) {
throw new \RuntimeException('Ungültiger CSRF-Token.');
}
$survey = $this->resolveEditableSurvey($surveyId, $memberId);
try {
if ('duplicate-survey' === $action) {
$duplicate = $this->surveyEditorService->duplicateSurvey($survey, $memberId);
$this->addFlash('success', sprintf('Die Umfrage "%s" wurde als Kopie angelegt.', (string) $duplicate->title));
}
if ('publish-survey' === $action) {
$this->surveyEditorService->publishSurvey($survey, $memberId);
$this->addFlash('success', 'Die Umfrage ist jetzt veröffentlicht.');
}
if ('toggle-active' === $action) {
$isActive = $this->surveyEditorService->toggleSurveyActive($survey, $memberId);
$this->addFlash('success', $isActive ? 'Die Umfrage ist jetzt wieder aktiv.' : 'Die Umfrage wurde vorübergehend deaktiviert.');
}
if ('delete-survey' === $action) {
$this->surveyEditorService->deleteSurvey($survey);
$this->addFlash('success', 'Die Umfrage mit allen Fragen, Antworten und Ergebnissen wurde gelöscht.');
}
} catch (\Throwable $exception) {
$this->addFlash('error', $exception->getMessage());
}
return new RedirectResponse($request->getBaseUrl().$request->getPathInfo());
}
private function resolveEditableSurvey(int $surveyId, int $memberId): SurveyModel
{
$survey = $this->surveyRepository->findById($surveyId);
if (!$survey instanceof SurveyModel || !$this->surveyEditorRepository->isEditor($surveyId, $memberId)) {
throw new \RuntimeException('Sie dürfen diese Umfrage nicht bearbeiten.');
}
return $survey;
}
private function resolvePage(int $pageId): ?PageModel
{
if ($pageId <= 0) {
return null;
}
$page = $this->getContaoAdapter(PageModel::class)->findById($pageId);
return $page instanceof PageModel ? $page : null;
}
}