From a3ef933cdd5c04a4cb3dbae4a410e2a81831c84c Mon Sep 17 00:00:00 2001
From: Jan-Hendrik Willms <tleilax+studip@gmail.com>
Date: Wed, 11 May 2022 10:35:45 +0000
Subject: [PATCH] allow pauses for consultation hours, fixes #925

Closes #925

Merge request studip/studip!521
---
 app/controllers/consultation/admin.php        |  10 +-
 app/views/consultation/admin/create.php       |  19 ++++
 lib/models/ConsultationBlock.php              | 103 ++++++++++++++----
 .../bootstrap/studip_helper_attributes.js     |  26 +++++
 4 files changed, 135 insertions(+), 23 deletions(-)

diff --git a/app/controllers/consultation/admin.php b/app/controllers/consultation/admin.php
index 37cb36431e7..2c48d189402 100644
--- a/app/controllers/consultation/admin.php
+++ b/app/controllers/consultation/admin.php
@@ -158,7 +158,9 @@ class Consultation_AdminController extends ConsultationController
                 $this->getDateAndTime('end'),
                 Request::int('day-of-week'),
                 Request::int('interval'),
-                Request::int('duration')
+                Request::int('duration'),
+                Request::bool('pause') ? Request::int('pause_time') : null,
+                Request::bool('pause') ? Request::int('pause_duration') : null
             );
             if ($slot_count >= self::SLOT_COUNT_THRESHOLD && !Request::int('confirmed')) {
                 $this->flash['confirm-many'] = $slot_count;
@@ -183,7 +185,11 @@ class Consultation_AdminController extends ConsultationController
                 $block->note              = Request::get('note');
                 $block->size              = Request::int('size', 1);
 
-                $block->createSlots(Request::int('duration'));
+                $block->createSlots(
+                    Request::int('duration'),
+                    Request::bool('pause') ? Request::int('pause_time') : null,
+                    Request::bool('pause') ? Request::int('pause_duration') : null
+                );
                 $stored += $block->store();
 
                 // Store block responsibilites
diff --git a/app/views/consultation/admin/create.php b/app/views/consultation/admin/create.php
index 6511d791052..4f1a5991b3d 100644
--- a/app/views/consultation/admin/create.php
+++ b/app/views/consultation/admin/create.php
@@ -119,6 +119,25 @@ $intervals = [
             <input required type="text" name="size" id="size"
                    min="1" max="50" value="<?= Request::int('size', 1) ?>">
         </label>
+
+        <label>
+            <input type="checkbox" name="pause" value="1" data-shows=".pause-inputs" data-activates=".pause-inputs input">
+            <?= _('Pausen zwischen den Terminen einfügen?') ?>
+        </label>
+
+        <label class="col-3 pause-inputs">
+            <?= _('Eine Pause nach wie vielen Minuten einfügen?') ?>
+            <input type="number" name="pause_time"
+                   value="<?= htmlReady(Request::int('pause_time', 45)) ?>"
+                   min="1">
+        </label>
+
+        <label class="col-3 pause-inputs">
+            <?= _('Dauer der Pause in Minuten') ?>
+            <input type="number" name="pause_duration"
+                   value="<?= Request::int('pause_duration', 15) ?>"
+                   min="1">
+        </label>
     </fieldset>
 
 <? if ($responsible): ?>
diff --git a/lib/models/ConsultationBlock.php b/lib/models/ConsultationBlock.php
index 01b688a3da2..4104ad8ca4f 100644
--- a/lib/models/ConsultationBlock.php
+++ b/lib/models/ConsultationBlock.php
@@ -118,15 +118,17 @@ class ConsultationBlock extends SimpleORMap implements PrivacyObject
     /**
      * Count generated blocks according to the given data.
      *
-     * @param  int $start    Start of the time range as unix timestamp
-     * @param  int $end      End of the time range as unix timestamp
-     * @param  int $week_day Day of the week the blocks should be created
-     *                          (0 = sunday, 1 = monday ...)
-     * @param  int $interval Week interval (skip $interval weeks between
-     *                          blocks)
-     * @param  int $duration Duration of a slot in minutes
+     * @param  int $start              Start of the time range as unix timestamp
+     * @param  int $end                End of the time range as unix timestamp
+     * @param int $week_day            Day of the week the blocks should be
+     *                                 created (0 = sunday, 1 = monday ...)
+     * @param int $interval            Week interval (skip $interval weeks
+     *                                 between blocks)
+     * @param int $duration            Duration of a slot in minutes
+     * @param int|null $pause_time     Create a pause after $pause_time minutes
+     * @param int|null $pause_duration Duration of the pause
      */
-    public static function countBlocks($start, $end, $week_day, $interval, $duration)
+    public static function countBlocks($start, $end, $week_day, $interval, $duration, $pause_time = null, $pause_duration = null)
     {
         $count = 0;
 
@@ -147,9 +149,23 @@ class ConsultationBlock extends SimpleORMap implements PrivacyObject
                 $block_start = strtotime("today {$start_time}", $current);
                 $block_end   = strtotime("today {$end_time}", $current);
 
-                while ($block_start < $block_end) {
-                    $count += 1;
-                    $block_start = strtotime("+{$duration} minutes", $block_start);
+                $now = $block_start;
+                while ($now < $block_end) {
+                    $is_in_pause = false;
+                    if ($pause_time !== null) {
+                        $is_in_pause = self::checkIfSlotIsInPause(
+                            $now,
+                            strtotime("+{$duration} minutes", $now),
+                            $block_start,
+                            $block_end,
+                            $pause_time,
+                            $pause_duration
+                        );
+                    }
+                    if (!$is_in_pause) {
+                        $count += 1;
+                    }
+                    $now = strtotime("+{$duration} minutes", $now);
                 }
             }
 
@@ -254,20 +270,35 @@ class ConsultationBlock extends SimpleORMap implements PrivacyObject
      * Creates individual slots according to the defined data and given
      * duration.
      *
-     * @param  int $duration Duration of a slot in minutes
+     * @param int      $duration       Duration of a slot in minutes
+     * @param int|null $pause_time     Create a pause after $pause_time minutes
+     * @param int|null $pause_duration Duration of the pause
      */
-    public function createSlots($duration)
+    public function createSlots($duration, int $pause_time = null, int $pause_duration = null)
     {
-        $start = $this->start;
-        while ($start < $this->end) {
-            $slot = new ConsultationSlot();
-            $slot->block_id   = $this->id;
-            $slot->start_time = $start;
-            $slot->end_time   = strtotime("+{$duration} minutes", $start);
+        $now = $this->start;
+        while ($now < $this->end) {
+            $is_in_pause = false;
+            if ($pause_time !== null) {
+                $is_in_pause = self::checkIfSlotIsInPause(
+                    $now,
+                    strtotime("+{$duration} minutes", $now),
+                    $this->start,
+                    $this->end,
+                    $pause_time,
+                    $pause_duration
+                );
+            }
+            if (!$is_in_pause) {
+                $slot = new ConsultationSlot();
+                $slot->block_id   = $this->id;
+                $slot->start_time = $now;
+                $slot->end_time   = strtotime("+{$duration} minutes", $now);
 
-            $this->slots[] = $slot;
+                $this->slots[] = $slot;
+            }
 
-            $start = $slot->end_time;
+            $now = strtotime("+{$duration} minutes", $now);
         }
     }
 
@@ -396,6 +427,36 @@ class ConsultationBlock extends SimpleORMap implements PrivacyObject
     }
 
 
+    /**
+     * Checks if a given time span (defined by $begin and $end) is inside a
+     * defined pause of a block.
+     *
+     * @param int $begin
+     * @param int $end
+     * @param int $block_begin
+     * @param int $block_end
+     * @param int $pause_time
+     * @param int $pause_duration
+     *
+     * @return bool
+     */
+    private static function checkIfSlotIsInPause($begin, $end, $block_begin, $block_end, $pause_time, $pause_duration): bool
+    {
+        $now = $block_begin;
+        while ($now < $block_end) {
+            $pause_begin = strtotime("+{$pause_time} minutes", $now);
+            $pause_end   = strtotime("+{$pause_duration} minutes", $pause_begin);
+
+            if ($begin < $pause_end && $end > $pause_begin) {
+                return true;
+            }
+
+            $now = $pause_end;
+        }
+
+        return false;
+    }
+
     /**
      * @return string A string representation of the consultation block instance.
      */
diff --git a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
index 491d2ac5d98..7af9f9a8e18 100644
--- a/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
+++ b/resources/assets/javascripts/bootstrap/studip_helper_attributes.js
@@ -124,6 +124,32 @@ STUDIP.ready((event) => {
     $('select[data-activates]', event.target).trigger('change');
 });
 
+//
+$(document).on('change', '[data-hides],[data-shows]', function () {
+    if (!$(this).is(':checkbox,:radio')) {
+        return;
+    }
+
+    ['hides', 'shows'].forEach((type) => {
+        var selector = $(this).data(type);
+        if (selector === undefined || $(this).prop('disabled')) {
+            return;
+        }
+
+        var state = $(this).prop('checked') || $(this).prop('indeterminate') || false;
+        $(selector).each(function() {
+            var condition = $(this).data(`${type}Condition`),
+                toggle = state && (!condition || $(condition).length > 0);
+            $(this)
+                .toggle(type === 'shows' ? toggle : !toggle)
+                .trigger('update.proxy');
+        });
+    });
+});
+STUDIP.ready(event => {
+    $('[data-hides],[data-shows]', event.target).trigger('change');
+});
+
 // Enable the user to set the checked state on a subset of related
 // checkboxes by clicking the first checkbox of the subset and then
 // clicking the last checkbox of the subset while holding down the shift
-- 
GitLab