From ea3004c96bc0ed907d4f16e8e216dca71d6ccf0b Mon Sep 17 00:00:00 2001 From: Andy Scherzinger Date: Sat, 7 Nov 2026 15:50:14 +0100 Subject: [PATCH] feat(conditional): allow branches on date and time questions A date or time question opens a branch when the answer lies within the range of a condition, its bounds included. Bounds are stored in the answer format (YYYY-MM-DD, HH:mm) and are optional. Deprecated datetime questions get the same conditions. The code comes from the original pull request #3123 by Micke Nordin and TimedIn, with the review fixes applied. Assisted-by: Claude Code:claude-opus-5-5 Signed-off-by: Andy Scherzinger --- docs/DataStructure.md | 17 +- lib/Constants.php | 4 + lib/Service/SubmissionService.php | 30 ++++ .../Conditional/BranchConditionEditor.vue | 162 ++++++++++++++++++ src/components/Questions/QuestionDate.vue | 1 + src/utils/Branches.ts | 23 +++ tests/Unit/Service/FormsServiceTest.php | 10 ++ tests/Unit/Service/SubmissionServiceTest.php | 44 +++++ 8 files changed, 290 insertions(+), 1 deletion(-) diff --git a/docs/DataStructure.md b/docs/DataStructure.md index 4f02f7b48..28946a8a9 100644 --- a/docs/DataStructure.md +++ b/docs/DataStructure.md @@ -270,7 +270,7 @@ Optional extra settings for some [Question Types](#question-types) | `columns` | `grid` | Array | - | Array of column identifiers / labels for grid questions | | `rows` | `grid` | Array | - | Array of row identifiers / labels for grid questions | | `questionType` | `grid` | String | `checkbox`, `number`, `radio` | Type of cell for grid questions (checkbox, numeric input, or radio) | -| `branches` | `dropdown`, `multiple`, `multiple_unique` | Array | Array of [Branch objects](#branch-object) | Branches with follow-up questions, see [Follow-up Questions](#follow-up-questions) | +| `branches` | see [Follow-up Questions](#follow-up-questions) | Array | Array of [Branch objects](#branch-object) | Branches with follow-up questions, see [Follow-up Questions](#follow-up-questions) | ### Option Types @@ -304,6 +304,9 @@ Follow-up questions (subquestions) are questions of their own with additional pr | `multiple_unique` | `option_selected` | Radio buttons - the selected option | | `dropdown` | `option_selected` | Dropdown - the selected option | | `multiple` | `options_combination` | Checkboxes - all options of a combination | +| `date` | `date_range` | Date - range including its bounds (`YYYY-MM-DD`) | +| `datetime` | `date_range` | Deprecated date and time (`YYYY-MM-DD HH:mm`) | +| `time` | `date_range` | Time - range including its bounds (`HH:mm`) | A branch is shown when any of its conditions matches. Several branches can be shown at once. @@ -353,6 +356,18 @@ Conditions determine when a branch is activated. The structure depends on the ty All options in `optionIds` must be selected for the condition to match. +#### date_range (for date, datetime, time) + +```json +{ "type": "date_range", "min": "2024-01-01", "max": "2024-12-31" } +``` + +```json +{ "type": "date_range", "min": "09:00", "max": "17:00" } +``` + +`min` and `max` use the answer format of the question type and are optional. + ### Example A dropdown question with a branch of follow-up questions: diff --git a/lib/Constants.php b/lib/Constants.php index 629ddcc6e..e46b83143 100644 --- a/lib/Constants.php +++ b/lib/Constants.php @@ -244,6 +244,7 @@ class Constants { */ public const CONDITION_TYPE_OPTION_SELECTED = 'option_selected'; public const CONDITION_TYPE_OPTIONS_COMBINATION = 'options_combination'; + public const CONDITION_TYPE_DATE_RANGE = 'date_range'; /** * Question types that can have branches with follow-up questions in `extraSettings.branches`, @@ -255,6 +256,9 @@ class Constants { self::ANSWER_TYPE_MULTIPLEUNIQUE => [self::CONDITION_TYPE_OPTION_SELECTED], self::ANSWER_TYPE_DROPDOWN => [self::CONDITION_TYPE_OPTION_SELECTED], self::ANSWER_TYPE_MULTIPLE => [self::CONDITION_TYPE_OPTIONS_COMBINATION], + self::ANSWER_TYPE_DATE => [self::CONDITION_TYPE_DATE_RANGE], + self::ANSWER_TYPE_DATETIME => [self::CONDITION_TYPE_DATE_RANGE], + self::ANSWER_TYPE_TIME => [self::CONDITION_TYPE_DATE_RANGE], ]; /** diff --git a/lib/Service/SubmissionService.php b/lib/Service/SubmissionService.php index 288c4234c..86568ec4d 100644 --- a/lib/Service/SubmissionService.php +++ b/lib/Service/SubmissionService.php @@ -1077,6 +1077,36 @@ private function evaluateBranchConditions(string $triggerType, array $triggerAns } } return false; + case Constants::ANSWER_TYPE_DATE: + case Constants::ANSWER_TYPE_DATETIME: + case Constants::ANSWER_TYPE_TIME: + // Date range conditions + $dateValue = $triggerAnswer[0] ?? ''; + if (empty($dateValue)) { + return false; + } + $format = Constants::ANSWER_PHPDATETIME_FORMAT[$triggerType] ?? 'Y-m-d'; + $date = \DateTime::createFromFormat('!' . $format, $dateValue); + if (!$date) { + return false; + } + + foreach ($conditions as $condition) { + $min = isset($condition['min']) ? \DateTime::createFromFormat('!' . $format, $condition['min']) : null; + $max = isset($condition['max']) ? \DateTime::createFromFormat('!' . $format, $condition['max']) : null; + + $inRange = true; + if ($min && $date < $min) { + $inRange = false; + } + if ($max && $date > $max) { + $inRange = false; + } + if ($inRange) { + return true; + } + } + return false; default: return false; } diff --git a/src/components/Conditional/BranchConditionEditor.vue b/src/components/Conditional/BranchConditionEditor.vue index eb74f74b0..9b3b938a7 100644 --- a/src/components/Conditional/BranchConditionEditor.vue +++ b/src/components/Conditional/BranchConditionEditor.vue @@ -31,17 +31,39 @@ @update:modelValue="onMultipleOptionsSelect" /> + + +