From d48546a75747d52354d9bdbf5d39beaf0d4908a2 Mon Sep 17 00:00:00 2001
From: Jonathan Weth <git@jonathanweth.de>
Date: Fri, 25 Aug 2023 12:26:25 +0200
Subject: [PATCH] Add mobile view for timetables and improve select for
 timetables

---
 .../frontend/components/NoTimetableCard.vue   |  10 +-
 .../frontend/components/SelectTimetable.vue   | 117 ++++++++++++
 .../chronos/frontend/components/Timetable.vue | 177 ++++++++----------
 .../frontend/components/timetableTypes.js     |  13 ++
 .../apps/chronos/frontend/messages/en.json    |   3 +-
 5 files changed, 216 insertions(+), 104 deletions(-)
 create mode 100644 aleksis/apps/chronos/frontend/components/SelectTimetable.vue
 create mode 100644 aleksis/apps/chronos/frontend/components/timetableTypes.js

diff --git a/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue b/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue
index 12c21e65..84cf21c8 100644
--- a/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue
+++ b/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue
@@ -26,9 +26,17 @@ export default {
       <div class="text-h5 grey--text text--darken-2 mb-2">
         {{ $t(titleKey) }}
       </div>
-      <div class="text-body-2 grey--text text--darken-2">
+      <div
+        class="text-body-2 grey--text text--darken-2"
+        v-if="$vuetify.breakpoint.lgAndUp"
+      >
         {{ $t(descriptionKey) }}
       </div>
+      <div v-if="$vuetify.breakpoint.mdAndDown">
+        <v-btn color="primary" @click="$emit('selectTimetable')" class="mt-4">
+          {{ $t("chronos.timetable.select") }}
+        </v-btn>
+      </div>
     </div>
   </v-card>
 </template>
diff --git a/aleksis/apps/chronos/frontend/components/SelectTimetable.vue b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue
new file mode 100644
index 00000000..069d74c5
--- /dev/null
+++ b/aleksis/apps/chronos/frontend/components/SelectTimetable.vue
@@ -0,0 +1,117 @@
+<script>
+import timetableTypes from "./timetableTypes";
+
+export default {
+  name: "SelectTimetable",
+  props: {
+    value: {
+      type: String | null,
+      required: true,
+    },
+    availableTimetables: {
+      type: Array,
+      required: true,
+    },
+  },
+  data() {
+    return {
+      selected: null,
+      selectedFull: null,
+      search: "",
+      selectedTypes: ["GROUP", "TEACHER", "ROOM"],
+      types: timetableTypes,
+    };
+  },
+  watch: {
+    value(val) {
+      this.selectedFull = val;
+      this.selected = val.id;
+    },
+    selectedFull(val) {
+      this.$emit("input", val);
+    },
+  },
+  computed: {
+    availableTimetablesFiltered() {
+      // Filter timetables by selected types
+      return this.availableTimetables.filter((timetable) => {
+        return this.selectedTypes.indexOf(timetable.type) !== -1;
+      });
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <v-card-text class="mb-0">
+      <!-- Search field for timetables -->
+      <v-text-field
+        search
+        filled
+        rounded
+        clearable
+        autofocus
+        v-model="search"
+        :placeholder="$t('chronos.timetable.search')"
+        prepend-inner-icon="mdi-magnify"
+        hide-details="auto"
+        class="mb-2"
+      />
+
+      <!-- Filter by timetable types -->
+      <v-btn-toggle v-model="selectedTypes" dense block multiple class="d-flex">
+        <v-btn
+          v-for="type in types"
+          :key="type.id"
+          class="flex-grow-1"
+          :value="type.id"
+        >
+          {{ type.name }}
+        </v-btn>
+      </v-btn-toggle>
+    </v-card-text>
+
+    <!-- Select of available timetables -->
+    <v-data-iterator
+      :items="availableTimetablesFiltered"
+      item-key="id"
+      :search="search"
+      single-expand
+      disable-pagination
+    >
+      <template #default="{ items, isExpanded, expand }">
+        <v-list class="scrollable-list">
+          <v-list-item-group v-model="selected">
+            <v-list-item
+              v-for="item in items"
+              @click="selectedFull = item"
+              :value="item.id"
+              :key="item.id"
+            >
+              <v-list-item-icon color="primary">
+                <v-icon v-if="item.type in types" color="secondary">
+                  {{ types[item.type].icon }}
+                </v-icon>
+                <v-icon v-else color="secondary">mdi-grid</v-icon>
+              </v-list-item-icon>
+              <v-list-item-content>
+                <v-list-item-title>{{ item.name }}</v-list-item-title>
+              </v-list-item-content>
+              <v-list-item-action>
+                <v-icon>mdi-chevron-right</v-icon>
+              </v-list-item-action>
+            </v-list-item>
+          </v-list-item-group>
+        </v-list>
+      </template>
+    </v-data-iterator>
+  </div>
+</template>
+
+<style scoped>
+.scrollable-list {
+  height: 100%;
+  overflow-y: scroll;
+}
+</style>
diff --git a/aleksis/apps/chronos/frontend/components/Timetable.vue b/aleksis/apps/chronos/frontend/components/Timetable.vue
index 73d94954..210bcd88 100644
--- a/aleksis/apps/chronos/frontend/components/Timetable.vue
+++ b/aleksis/apps/chronos/frontend/components/Timetable.vue
@@ -1,10 +1,12 @@
 <script>
 import { gqlAvailableTimetables } from "./timetables.graphql";
 import NoTimetableCard from "./NoTimetableCard.vue";
+import SelectTimetable from "./SelectTimetable.vue";
+import timetableTypes from "./timetableTypes";
 
 export default {
   name: "Timetable",
-  components: { NoTimetableCard },
+  components: { NoTimetableCard, SelectTimetable },
   apollo: {
     availableTimetables: {
       query: gqlAvailableTimetables,
@@ -14,43 +16,26 @@ export default {
     return {
       availableTimetables: [],
       selected: null,
-      selectedFull: null,
       search: "",
       selectedTypes: ["GROUP", "TEACHER", "ROOM"],
-      types: {
-        GROUP: {
-          name: "Groups",
-          id: "GROUP",
-          icon: "mdi-account-group-outline",
-        },
-        TEACHER: {
-          name: "Teachers",
-          id: "TEACHER",
-          icon: "mdi-account-outline",
-        },
-        ROOM: { name: "Rooms", id: "ROOM", icon: "mdi-door" },
-      },
+      types: timetableTypes,
+      selectDialog: false,
     };
   },
   watch: {
     selected(selected) {
-      if (selected == null) {
-        this.selectedFull = null;
-      }
-    },
-    selectedFull(selectedFull) {
       // Align navigation with currently selected timetable
-      if (!selectedFull) {
+      if (!selected) {
         this.$router.push({ name: "chronos.timetable" });
       } else if (
-        selectedFull.objId !== this.$route.params.id ||
-        selectedFull.type !== this.$route.params.type
+        selected.objId !== this.$route.params.id ||
+        selected.type !== this.$route.params.type
       ) {
         this.$router.push({
           name: "chronos.timetableWithId",
           params: {
-            type: selectedFull.type.toLowerCase(),
-            id: selectedFull.objId,
+            type: selected.type.toLowerCase(),
+            id: selected.objId,
           },
         });
       }
@@ -59,7 +44,7 @@ export default {
   methods: {
     findNextTimetable(offset = 1) {
       const currentIndex = this.availableTimetablesIds.indexOf(
-        this.selectedFull.id
+        this.selected.id
       );
       const newIndex = currentIndex + offset;
       if (newIndex < 0 || newIndex >= this.availableTimetablesIds.length) {
@@ -68,8 +53,7 @@ export default {
       return this.availableTimetables[newIndex];
     },
     selectTimetable(timetable) {
-      this.selected = timetable.id;
-      this.selectedFull = timetable;
+      this.selected = timetable;
     },
   },
   computed: {
@@ -100,88 +84,77 @@ export default {
 <template>
   <div>
     <v-row>
-      <v-col cols="2">
+      <v-dialog
+        v-model="selectDialog"
+        fullscreen
+        hide-overlay
+        transition="dialog-bottom-transition"
+      >
         <v-card>
-          <v-card-text class="mb-0">
-            <!-- Search field for timetables -->
-            <v-text-field
-              search
-              filled
-              rounded
-              clearable
-              autofocus
-              v-model="search"
-              :placeholder="$t('chronos.timetable.search')"
-              prepend-inner-icon="mdi-magnify"
-              hide-details="auto"
-              class="mb-2"
-            />
-
-            <!-- Filter by timetable types -->
-            <v-btn-toggle
-              v-model="selectedTypes"
-              dense
-              block
-              multiple
-              class="d-flex"
-            >
-              <v-btn
-                v-for="type in types"
-                :key="type.id"
-                class="flex-grow-1"
-                :value="type.id"
-              >
-                {{ type.name }}
-              </v-btn>
-            </v-btn-toggle>
-          </v-card-text>
+          <v-toolbar dark color="primary">
+            <v-toolbar-title>{{
+              $t("chronos.timetable.select")
+            }}</v-toolbar-title>
+            <v-spacer></v-spacer>
+          </v-toolbar>
+          <select-timetable
+            v-model="selected"
+            @input="selectDialog = false"
+            :availableTimetables="availableTimetables"
+          />
+        </v-card>
+      </v-dialog>
 
-          <!-- Select of available timetables -->
-          <v-data-iterator
-            :items="availableTimetablesFiltered"
-            item-key="id"
-            :search="search"
-            single-expand
-          >
-            <template #default="{ items, isExpanded, expand }">
-              <v-list>
-                <v-list-item-group v-model="selected">
-                  <v-list-item
-                    v-for="item in items"
-                    @click="selectedFull = item"
-                    :value="item.id"
-                    :key="item.id"
-                  >
-                    <v-list-item-icon color="primary">
-                      <v-icon v-if="item.type in types" color="secondary">
-                        {{ types[item.type].icon }}
-                      </v-icon>
-                      <v-icon v-else color="secondary">mdi-grid</v-icon>
-                    </v-list-item-icon>
-                    <v-list-item-content>
-                      <v-list-item-title>{{ item.name }}</v-list-item-title>
-                    </v-list-item-content>
-                    <v-list-item-action>
-                      <v-icon>mdi-chevron-right</v-icon>
-                    </v-list-item-action>
-                  </v-list-item>
-                </v-list-item-group>
-              </v-list>
-            </template>
-          </v-data-iterator>
+      <v-col md="3" lg="3" xl="2" v-if="$vuetify.breakpoint.lgAndUp">
+        <v-card>
+          <select-timetable
+            v-model="selected"
+            :availableTimetables="availableTimetables"
+          />
         </v-card>
       </v-col>
-      <v-col cols="10" class="full-height">
+      <v-col sm="12" md="12" lg="9" xl="10" class="full-height">
         <!-- No timetable card-->
-        <no-timetable-card v-if="selectedFull == null" />
+        <no-timetable-card
+          v-if="selected == null"
+          @selectTimetable="selectDialog = true"
+        />
 
         <!-- Calendar card-->
         <v-card v-else>
-          <div class="d-flex">
+          <div class="d-flex flex-column" v-if="$vuetify.breakpoint.smAndDown">
+            <v-card-title class="pt-2">
+              <v-btn
+                icon
+                :disabled="!prevTimetable"
+                @click="selectTimetable(prevTimetable)"
+                :title="$t('chronos.timetable.prev')"
+                class="mr-1"
+              >
+                <v-icon>mdi-chevron-left</v-icon>
+              </v-btn>
+              <v-spacer />
+              <v-chip outlined color="secondary" @click="selectDialog = true">
+                {{ selected.name }}
+                <v-icon right>mdi-chevron-down</v-icon>
+              </v-chip>
+              <v-spacer />
+              <v-btn
+                icon
+                :disabled="!nextTimetable"
+                @click="selectTimetable(nextTimetable)"
+                :title="$t('chronos.timetable.next')"
+                class="ml-1 float-right"
+              >
+                <v-icon>mdi-chevron-right</v-icon>
+              </v-btn>
+            </v-card-title>
+          </div>
+
+          <div class="d-flex flex-wrap justify-space-between mb-2" v-else>
             <v-card-title>
-              {{ selectedFull.name }}
+              {{ selected.name }}
             </v-card-title>
-            <v-spacer />
             <div class="pa-2 mt-1">
               <v-btn
                 icon
@@ -192,7 +165,7 @@ export default {
                 <v-icon>mdi-chevron-left</v-icon>
               </v-btn>
               <v-chip label color="secondary" outlined class="mx-1">{{
-                selectedFull.shortName
+                selected.shortName
               }}</v-chip>
               <v-btn
                 icon
@@ -206,7 +179,7 @@ export default {
           </div>
           <calendar-with-controls
             :calendar-feeds="[{ name: 'lesson' }]"
-            :params="{ type: selectedFull.type, id: selectedFull.objId }"
+            :params="{ type: selected.type, id: selected.objId }"
           />
         </v-card>
       </v-col>
diff --git a/aleksis/apps/chronos/frontend/components/timetableTypes.js b/aleksis/apps/chronos/frontend/components/timetableTypes.js
new file mode 100644
index 00000000..692854f1
--- /dev/null
+++ b/aleksis/apps/chronos/frontend/components/timetableTypes.js
@@ -0,0 +1,13 @@
+export default {
+  GROUP: {
+    name: "Groups",
+    id: "GROUP",
+    icon: "mdi-account-group-outline",
+  },
+  TEACHER: {
+    name: "Teachers",
+    id: "TEACHER",
+    icon: "mdi-account-outline",
+  },
+  ROOM: { name: "Rooms", id: "ROOM", icon: "mdi-door" },
+};
diff --git a/aleksis/apps/chronos/frontend/messages/en.json b/aleksis/apps/chronos/frontend/messages/en.json
index 4128f0b9..22fca0f2 100644
--- a/aleksis/apps/chronos/frontend/messages/en.json
+++ b/aleksis/apps/chronos/frontend/messages/en.json
@@ -10,7 +10,8 @@
       },
       "search": "Search Timetables",
       "prev": "Previous Timetable",
-      "next": "Next Timetable"
+      "next": "Next Timetable",
+      "select": "Select Timetable"
     },
     "lessons": {
       "menu_title_daily": "Daily lessons"
-- 
GitLab