db7094fd1e
CSRF / actions: - List action forms now send both REQUEST_TOKEN (Contao gate) and _token (Symfony, validated via isCsrfTokenValid) — mirrors the editor; fixes the 400/500 "Ungültiger CSRF-Token" on publish/toggle/duplicate/delete Public reader / URLs: - Reader & results read auto_item via Input::get (marks the route param used) so clean path URLs /<page>/<alias> no longer 404 - List controller generates path URLs via ['parameters' => '/'.$alias] - Allow re-participation in the same session after a 60s cooldown Review fixes: - Results: merge answer snapshots by questionId+type (no more split cards on label edits); seed "Vielleicht" when allowMaybe so screen matches Excel - Reader: replace generic answer hint with per-option "weiter zu Frage X" only when a jump is configured; strip HTML tags from survey description - PDF: render free-text frequency summary; drop duplicate "Minimum" label - Drag-sort: bundle SortableJS locally, drop the duplicated inline sort CSS/JS from _survey_assets, throttle the backend MutationObserver - Backend: move hardcoded tl_survey operation labels to TL_LANG (de+en) - Remove dead code: SurveyEditorVoter, SurveyPermissionService, categoryFilter/categorySummary + helpers, orphaned publishSurvey lang key Reader layout: - max-width 1000px (other views keep 1400px); back/next buttons fill the width as equal, card-sized halves; sort handle icon offset tweak Docs: update AI_HANDOVER (dual-token CSRF, clean URLs) and README Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
179 lines
5.3 KiB
PHP
179 lines
5.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mummert\SurveyBundle\Service;
|
|
|
|
use Mummert\SurveyBundle\Model\SurveyContentModel;
|
|
use Mummert\SurveyBundle\Model\SurveyModel;
|
|
use Mummert\SurveyBundle\Model\SurveySubmissionModel;
|
|
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
|
use Mummert\SurveyBundle\Repository\SurveyQuestionRepository;
|
|
|
|
final class SurveyFlowService
|
|
{
|
|
public function __construct(
|
|
private readonly SurveyEngine $surveyEngine,
|
|
private readonly SurveyRepository $surveyRepository,
|
|
private readonly SurveyQuestionRepository $surveyQuestionRepository,
|
|
) {
|
|
}
|
|
|
|
public function resolveCurrentQuestion(SurveyModel $survey, SurveySubmissionModel $submission): ?SurveyContentModel
|
|
{
|
|
if ('1' === (string) $submission->isFinished) {
|
|
return null;
|
|
}
|
|
|
|
$questions = $this->getOrderedQuestions($survey);
|
|
$currentQuestionId = (int) $submission->currentQuestion;
|
|
|
|
if ($currentQuestionId > 0) {
|
|
foreach ($questions as $question) {
|
|
if ((int) $question->id === $currentQuestionId) {
|
|
return $question;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $questions[0] ?? null;
|
|
}
|
|
|
|
public function determineNextQuestion(SurveyModel $survey, SurveyContentModel $currentQuestion, string $normalizedAnswer): ?SurveyContentModel
|
|
{
|
|
$questions = $this->getOrderedQuestions($survey);
|
|
$configuredJumpTarget = $this->resolveYesNoJumpTarget($currentQuestion, $questions, $normalizedAnswer);
|
|
|
|
if ($configuredJumpTarget instanceof SurveyContentModel) {
|
|
return $configuredJumpTarget;
|
|
}
|
|
|
|
$conditionalTarget = $this->surveyEngine->resolveConditionalTarget($survey, $currentQuestion, $normalizedAnswer);
|
|
|
|
if ($conditionalTarget instanceof SurveyContentModel) {
|
|
return $conditionalTarget;
|
|
}
|
|
|
|
foreach ($questions as $index => $question) {
|
|
if ((int) $question->id === (int) $currentQuestion->id) {
|
|
return $questions[$index + 1] ?? null;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function getFirstQuestion(SurveyModel $survey): ?SurveyContentModel
|
|
{
|
|
$questions = $this->getOrderedQuestions($survey);
|
|
|
|
return $questions[0] ?? null;
|
|
}
|
|
|
|
public function resolveQuestionById(SurveyModel $survey, int $questionId): ?SurveyContentModel
|
|
{
|
|
if ($questionId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($this->getOrderedQuestions($survey) as $question) {
|
|
if ((int) $question->id === $questionId) {
|
|
return $question;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array{current:int,total:int,percentage:int}
|
|
*/
|
|
public function getProgress(SurveyModel $survey, ?SurveyContentModel $currentQuestion): array
|
|
{
|
|
$questions = $this->getOrderedQuestions($survey);
|
|
$total = max(1, count($questions));
|
|
$current = 1;
|
|
|
|
if ($currentQuestion instanceof SurveyContentModel) {
|
|
foreach ($questions as $index => $question) {
|
|
if ((int) $question->id === (int) $currentQuestion->id) {
|
|
$current = $index + 1;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
$current = $total;
|
|
}
|
|
|
|
return [
|
|
'current' => $current,
|
|
'total' => $total,
|
|
'percentage' => (int) round(($current / $total) * 100),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Beschreibt das Sprungziel einer Ja/Nein-Antwort über Positionsnummer und Label.
|
|
*
|
|
* @return array{position:int,label:string}|null
|
|
*/
|
|
public function describeJumpTarget(SurveyModel $survey, int $targetQuestionId): ?array
|
|
{
|
|
if ($targetQuestionId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($this->getOrderedQuestions($survey) as $index => $question) {
|
|
if ((int) $question->id === $targetQuestionId) {
|
|
return [
|
|
'position' => $index + 1,
|
|
'label' => trim((string) $question->question),
|
|
];
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return list<SurveyContentModel>
|
|
*/
|
|
private function getOrderedQuestions(SurveyModel $survey): array
|
|
{
|
|
$surveyId = (int) $survey->id;
|
|
|
|
if ($this->surveyQuestionRepository->countPublishedBySurvey($surveyId) > 0) {
|
|
return $this->surveyQuestionRepository->findAllBySurvey($surveyId, true);
|
|
}
|
|
|
|
return $this->surveyQuestionRepository->findAllBySurvey($surveyId);
|
|
}
|
|
|
|
/**
|
|
* @param list<SurveyContentModel> $questions
|
|
*/
|
|
private function resolveYesNoJumpTarget(SurveyContentModel $currentQuestion, array $questions, string $normalizedAnswer): ?SurveyContentModel
|
|
{
|
|
if ('yes_no_maybe' !== (string) $currentQuestion->type) {
|
|
return null;
|
|
}
|
|
|
|
$targetId = match ($normalizedAnswer) {
|
|
'yes' => (int) ($currentQuestion->jumpOnYes ?? 0),
|
|
'no' => (int) ($currentQuestion->jumpOnNo ?? 0),
|
|
default => 0,
|
|
};
|
|
|
|
if ($targetId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($questions as $question) {
|
|
if ((int) $question->id === $targetId) {
|
|
return $question;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
} |