From f2f4f98f6883392ffa5861b9e59c20fdbf5ba8ab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Thu, 22 Sep 2022 16:39:30 +0200
Subject: [PATCH] family: weekly agenda & many weeks display (#69454)

---
 .../static/css/combo.weekly_agenda.scss       | 178 +++++++++---------
 .../family/static/js/combo.weekly_agenda.js   | 116 ++++++++----
 .../templates/combo/family/weekly_agenda.html |  75 ++++----
 tests/test_family.py                          |  11 +-
 4 files changed, 214 insertions(+), 166 deletions(-)

diff --git a/combo/apps/family/static/css/combo.weekly_agenda.scss b/combo/apps/family/static/css/combo.weekly_agenda.scss
index 3120e53e..75f97c58 100644
--- a/combo/apps/family/static/css/combo.weekly_agenda.scss
+++ b/combo/apps/family/static/css/combo.weekly_agenda.scss
@@ -1,104 +1,100 @@
-.weeklyagenda-cell {
+.weekly-agenda-cell {
 	margin-top: 1em;
+	display: grid;
+	grid-template-columns: repeat(3, auto);
+	grid-template-rows: repeat(2, auto);
+	justify-items: left;
+	--min-column-width: 250;
+}
+
+.weekly-agenda-cell--previous-week,
+.weekly-agenda-cell--next-week {
+	height: 3em;
+	width: 3em;
+}
+
+.weekly-agenda-cell--next-week {
+	margin-left: 1rem;
+}
+
+.weekly-agenda-cell--edit-btn {
+	grid-column: 2;
+	grid-row: 2;
+}
+
+.weekly-agenda-cell--week-list {
+	overflow: hidden;
+	margin-bottom: 2rem;
+	width: 100%;
+}
+
+.weekly-agenda-cell--slider {
+	column-gap: 0px;
+	display: grid;
+	grid-auto-columns: 0px;
+	grid-auto-rows: 0px;
+	grid-auto-flow: column;
+	position: relative;
+}
+
+.weekly-agenda-cell--week-item:nth-child(even) {
+	background: #fafafa;
+}
+
+.weekly-agenda-cell--week-title {
+	border-bottom: 1px solid #888;
+	font-weight: bold;
+	margin-bottom: 1rem;
 	display: flex;
-	ul {
-		padding: 0;
-		margin: 0;
-		ul {
-			list-style: none;
-		}
-	}
-	& > ul {
-		width: 100%;
-		& > li {
-			display: none;
-			&.shown {
-				display: block;
-			}
-		}
-	}
-	button.previous-week,
-	button.next-week {
-		height: 3em;
-		z-index: 10;
-	}
-	button.previous-week {
-		margin-right: 1em;
-	}
-	button.next-week {
-		margin-left: 1em;
-		margin-right: 0;
-	}
-	.week-title {
-		display: block;
-		height: 3em;
-		line-height: 3em;
+	justify-content: center;
+	align-items: center;
+	height: 3.2rem;
+}
+
+.weekly-agenda-cell--day-item {
+	margin: 0 1rem;
+}
+
+.weekly-agenda-cell--day-title {
+	border-bottom: 1px solid #888;
+	margin: 1rem 0;
+	font-weight: bold;
+}
+
+.weekly-agenda-cell--day-no-activity {
+	color: #888;
+}
+
+.weekly-agenda-cell--activity-item {
+	display: flex;
+	align-items: baseline;
+	margin: 0.3rem 0;
+	&+ .weekly-agenda-cell--day-no-activity {
+		display: none;
 	}
-	.no-activity {
+	&.disabled {
 		color: #888;
 	}
-	li {
-		&.day-title {
-			margin: 1em 0 0.5em 0;
-		}
-		&.disabled {
-			color: #888;
-		}
-		& > span {
-			position: relative;
-		}
-	}
-	span > span {
-		padding-left: 1.8em;
-		&::before {
-			display: block;
-			content: '';
-			position: absolute;
-			margin: auto;
-			height: calc(0.66rem + 2px);
-			width: calc(0.66rem + 2px);
-			background: transparent;
-			top: 0.33rem;
-			left: 0;
-			border: 1px solid #aaa;
-			border-radius: 2px;
-		}
-		&::after {
-			display: block;
-			content: '';
-			position: absolute;
-			margin: auto;
-			height: calc(0.66rem);
-			width: calc(0.66rem);
-			background: transparent;
-			transition: background 0.1s linear;
-			top: calc(0.33rem + 1px);
-			left: 1px;
-		}
-	}
-	& [data-status=booked] span > span::after {
-		background: #3c3;
+}
+
+.weekly-agenda-cell--activity-status {
+	min-height: calc(0.66rem + 2px);
+	min-width: calc(0.66rem + 2px);
+	background: transparent;
+	border: 1px solid #aaa;
+	border-radius: 2px;
+	margin-right: 1rem;
+	&.booked {
+		background: green;
 	}
-	& [data-status=cancelled] span > span::after {
+	&.cancelled {
 		background: yellow;
 	}
-	& [data-status=absence] span > span::after {
+	&.absence {
 		background: red;
 	}
 }
 
-br.weekbreak {
-	display: none;
-}
-
-@media screen and (max-width: 500px) {
-	br.weekbreak { display: block; }
-	div.weeklyagenda-cell > ul {
-		margin: 0 -4em;
-		& .week-title {
-			margin: 0 4em;
-			line-height: 150%;
-			text-align: center;
-		}
-	}
+.weekly-agenda-cell--activity-label {
+	flex-grow: 1;
 }
diff --git a/combo/apps/family/static/js/combo.weekly_agenda.js b/combo/apps/family/static/js/combo.weekly_agenda.js
index 7c605473..14393bf5 100644
--- a/combo/apps/family/static/js/combo.weekly_agenda.js
+++ b/combo/apps/family/static/js/combo.weekly_agenda.js
@@ -1,46 +1,90 @@
 $(function () {
-  init_agenda = function(cell_id) {
-    $('.weeklyagenda-cell-' + cell_id + ' .previous-week').on('click', function() {
-      var $cell = $(this).parents('.weeklyagenda-cell-' + cell_id);
-      var $prev = $('li.shown', $cell).prev();
-      if ($prev.length) { $('li.shown', $cell).removeClass('shown'); $prev.addClass('shown'); }
-      return false;
-    });
-    $('.weeklyagenda-cell-' + cell_id + ' .next-week').on('click', function() {
-      var $cell = $(this).parents('.weeklyagenda-cell-' + cell_id);
-      var $next = $('li.shown', $cell).next();
-      if ($next.length) { $('li.shown', $cell).removeClass('shown'); $next.addClass('shown'); }
-      return false;
-    });
-    $('.weeklyagenda-cell-' + cell_id + ' li.day-title').each(function(idx, elem) {
-      /* hide empty days */
-      var $next = $(elem).next();
-      if ($next.length == 0 || $next.is('.day-title')) {
-        $(elem).hide();
+  function initAgendaCell($cell, prefix) {
+    const $weekList = $(prefix + '--week-list', $cell);
+    const $slider = $(prefix + '--slider', $cell);
+    const $currentWeek = $(prefix + '--week-title.current', $cell);
+    const $nextWeekButton = $(prefix + '--next-week', $cell);
+    const $previousWeekButton = $(prefix + '--previous-week', $cell);
+    const $editAgendaBtn = $(prefix + '--edit-btn', $cell);
+
+    let $selectedWeek = $currentWeek;
+
+    function getPreviousWeek() { return $selectedWeek.prevAll(prefix + '--week-title:first'); }
+    function getNextWeek() { return $selectedWeek.nextAll(prefix + '--week-title:first'); }
+
+    function setCurrentWeek($week, enableTransition) {
+      if(!$week.length) {
+          return;
       }
+      const currentOffset = $week[0].offsetLeft;
+      $slider.css({
+        'transform': `translate(-${currentOffset}px)`,
+        'transition': (enableTransition ? 'transform 0.5s' : '')
+      });
+
+      $selectedWeek = $week;
+      $nextWeekButton.prop('disabled', getNextWeek().length == 0);
+      $previousWeekButton.prop('disabled', getPreviousWeek().length == 0);
+
+      const selectedWeekIsInPast = $selectedWeek.nextAll(prefix + '--week-title.current').length;
+      $weekToEdit = selectedWeekIsInPast ? $currentWeek : $selectedWeek;
+      $editAgendaBtn.prop('href', $weekToEdit.attr('data-edit-url'));
+    }
+
+    function setPreviousWeek() { setCurrentWeek(getPreviousWeek(), true); }
+    function setNextWeek() { setCurrentWeek(getNextWeek(), true); }
+
+    $previousWeekButton.on('click', setPreviousWeek);
+    $nextWeekButton.on('click', setNextWeek);
+
+    let touchStartX = 0;
+    $cell.on('touchstart', (e) => {
+      touchStartX = e.changedTouches[0].screenX;
     });
-    $('.weeklyagenda-cell-' + cell_id + ' li.week').each(function(idx, elem) {
-      /* hide no-activity message if not empty */
-      if ($('.activity', $(elem)).length > 0) {
-        $('.no-activity', $(elem)).hide();
-      }
-      /* hide booking button if all items are disabled */
-      if ($('.activity', $(elem)).not('.disabled').length == 0) {
-        $('.booking-btn', $(elem)).hide();
+
+    $cell.on('touchend', (e) => {
+      const touchEndX = e.changedTouches[0].screenX;
+      if (touchEndX - touchStartX < -30) {
+        setNextWeek();
+      } else if (touchEndX - touchStartX > 30) {
+        setPreviousWeek();
       }
     });
-    $('.weeklyagenda-cell-' + cell_id).each(function(idx, elem) {
-      /* init first week shown */
-      var $cell = $(this);
-      $('li', $cell).removeClass('shown');
-      if ($('li.day-title.current', $cell).length) {
-        $('li.day-title.current', $cell).parent().parent().addClass('shown');
-      } else {
-        $('li', $cell).first().addClass('shown');
+
+    function updateColumnsWidth() {
+      const minWidth = $slider.css('--min-column-width');
+      const availableWidth = $weekList[0].offsetWidth;
+      let columnsWidth = availableWidth < minWidth ? availableWidth : availableWidth / Math.floor(availableWidth / minWidth);
+      $slider.css('grid-template-columns', `repeat(53, ${columnsWidth}px)`);
+
+      if($selectedWeek) {
+        // Update slider offset to bring it at the new position of the selectedWeek
+        setCurrentWeek($selectedWeek, false);
       }
-    });
+    }
+
+    function hideEmptyDays() {
+      let nbRows = 8;
+      for(let i = 0; i < 7; ++i) {
+        const $days = $(prefix + `--day-item[data-weekday=${i}]`, $cell);
+        const $activities = $(prefix + '--activity-item', $days);
+        if(!$activities.length) {
+          $days.css('display', 'none');
+          nbRows -= 1;
+        }
+
+        $slider.css('grid-template-rows', `repeat(${nbRows}, auto`);
+      }
+    }
+
+    hideEmptyDays();
+    new ResizeObserver(updateColumnsWidth).observe($weekList[0]);
+    addEventListener('resize', updateColumnsWidth);
   }
+
   $(document).on('combo:cell-loaded', function(ev, cell) {
-    init_agenda($('.weeklyagenda-cell', $(cell)).data('cell-id'));
+    if ($('.weekly-agenda-cell', $(cell)).length) {
+      initAgendaCell($(cell), '.weekly-agenda-cell');
+    }
   });
 });
diff --git a/combo/apps/family/templates/combo/family/weekly_agenda.html b/combo/apps/family/templates/combo/family/weekly_agenda.html
index 840fb8cb..6ba8ce68 100644
--- a/combo/apps/family/templates/combo/family/weekly_agenda.html
+++ b/combo/apps/family/templates/combo/family/weekly_agenda.html
@@ -8,43 +8,50 @@
 </h2>
 {% endif %}
 
-<div class="weeklyagenda-cell weeklyagenda-cell-{{ cell.pk }}" data-cell-id="{{ cell.pk }}">
+<div class="weekly-agenda-cell weekly-agenda-cell-{{ cell.pk }}" data-cell-id="{{ cell.pk }}">
 {% with first_monday=json.data.0.date|date|adjust_to_week_monday last_item=json.data|last %}
-{% with last_day=last_item.date|date|adjust_to_week_monday|add_days:6 %}
-{% now 'Y-m-d' as now %}
-
-{% spaceless %}
-<button class="previous-week">←</button>
-<ul>
-{% for day in first_monday|iterate_days_until:last_day %}
-{% if day.weekday == 0 %}
-{% with sunday=day|add_days:6 %}
-<li class="week"><span class="week-title">{% blocktrans with day_date=day|date:"d/m" sunday_date=sunday|date:"d/m" %}Week<br class="weekbreak"> of {{ day_date }} to {{ sunday_date }}{% endblocktrans %}</span><ul>
-{% endwith %}
-{% endif %}
-<li class="day-title {% if now == day|date:"Y-m-d" %}current{% endif %}" data-weekday="{{ day.weekday }}"><strong>{{ day|date:"l d/m" }}</strong></li>
-  {% with day_str=day|date:"Y-m-d" %}
-    {% for item in json.data %}
-      {% if item.date == day_str %}
-
-  <li class="activity {% if item.disabled %}disabled{% endif %}" data-status="{{ item.status }}">
-    <span><span>{{ item.label }}</span></span>
-   </li>
-
+  {% with last_day=last_item.date|date|adjust_to_week_monday|add_days:6 %}
+    {% now 'Y-m-W' as current_week %}
+    {% spaceless %}
+      <button class="weekly-agenda-cell--previous-week">←</button>
+      <div class="weekly-agenda-cell--week-list">
+        <div class="weekly-agenda-cell--slider">
+          {% for day in first_monday|iterate_days_until:last_day %}
+            {% if day.weekday == 0 %}
+              {% with sunday=day|add_days:6 %}
+                <div class="weekly-agenda-cell--week-title {% if current_week == day|date:'Y-m-W' %}current{% endif %}"
+                     {% if booking_form_url %}data-edit-url="{{ booking_form_url }}{% if '?' in booking_form_url %}&{% else %}?{% endif %}current={{ day|adjust_to_week_monday|date:'Y-m-d' }}"{% endif %}>
+                  {% blocktrans with day_date=day|date:'d/m' sunday_date=sunday|date:'d/m' %}Week<br class="weekbreak"> of {{ day_date }} to {{ sunday_date }}{% endblocktrans %}
+                </div>
+              {% endwith %}
+            {% endif %}
+            <div class="weekly-agenda-cell--day-item" data-weekday="{{ day.weekday }}">
+              <div class="weekly-agenda-cell--day-title" >
+                {{ day|date:"l d/m" }}
+              </div>
+              {% with day_str=day|date:"Y-m-d" %}
+                {% for item in json.data %}
+                  {% if item.date == day_str %}
+                    <div class="weekly-agenda-cell--activity-item {% if item.disabled %}disabled{% endif %}">
+                      <div class="weekly-agenda-cell--activity-status {{ item.status }}"/></div>
+                      <div class="weekly-agenda-cell--activity-label"/>{{ item.label }}</div>
+                    </div>
+                  {% endif %}
+                {% endfor %}
+              {% endwith %}
+              <div class="weekly-agenda-cell--day-no-activity">{% trans "No activity this week" %}</div>
+            </div>
+          {% endfor %}
+        </div>
+      </div>
+      <button class="weekly-agenda-cell--next-week">→</button>
+      {% if booking_form_url %}
+      <a class="pk-button weekly-agenda-cell--edit-btn">
+        {% trans "Update bookings" %}
+      </a>
       {% endif %}
-    {% endfor %}
+    {% endspaceless %}
   {% endwith %}
-{% if day.weekday == 6 %}
-</ul>
-{% if booking_form_url %}<p class="booking-btn"><a class="pk-button" href="{{ booking_form_url }}{% if '?' in booking_form_url %}&{% else %}?{% endif %}current={{ day|adjust_to_week_monday|date:"Y-m-d" }}">{% trans "Update bookings" %}</a></p>{% endif %}
-<p class="no-activity">{% trans "No activity this week" %}</p>
-</li>
-{% endif %}
-{% endfor %}
-</ul>
-<button class="next-week">→</button>
-{% endspaceless %}
-{% endwith %}
 {% endwith %}
 </div>
 {% endif %}
diff --git a/tests/test_family.py b/tests/test_family.py
index 1cf50be8..b08759fe 100644
--- a/tests/test_family.py
+++ b/tests/test_family.py
@@ -295,20 +295,21 @@ def test_weeklyagenda_cell_booking_form_url(context):
     with mock.patch('requests.Session.get') as requests_get:
         requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
         result = cell.render(context)
-        assert 'booking-btn' not in result
+        assert 'pk-button weekly-agenda-cell--edit-btn' not in result
+        assert 'data-edit-url' not in result
 
     cell.booking_form_url = 'http://example.com/foobar/'
     cell.save()
     with mock.patch('requests.Session.get') as requests_get:
         requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
         result = cell.render(context)
-        assert 'booking-btn' in result
-        assert 'http://example.com/foobar/?current=2022-02-28' in result
+        assert 'pk-button weekly-agenda-cell--edit-btn' in result
+        assert 'data-edit-url="http://example.com/foobar/?current=2022-02-28"' in result
 
     cell.booking_form_url = 'http://example.com/foobar/?user={{ user_nameid }}'
     cell.save()
     with mock.patch('requests.Session.get') as requests_get:
         requests_get.return_value = MockedRequestResponse(content=json.dumps(data))
         result = cell.render(context)
-        assert 'booking-btn' in result
-        assert 'http://example.com/foobar/?user=xyz&current=2022-02-28' in result
+        assert 'pk-button weekly-agenda-cell--edit-btn' in result
+        assert 'data-edit-url="http://example.com/foobar/?user=xyz&current=2022-02-28"' in result
-- 
2.35.1

