diff --git a/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue b/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue
new file mode 100644
index 0000000000000000000000000000000000000000..12c21e656097e1a848b9d139e228b34e6a9d356f
--- /dev/null
+++ b/aleksis/apps/chronos/frontend/components/NoTimetableCard.vue
@@ -0,0 +1,34 @@
+<script>
+export default {
+  name: "NoTimetableCard",
+  props: {
+    titleKey: {
+      type: String,
+      required: false,
+      default: "chronos.timetable.no_timetable_selected.title",
+    },
+    descriptionKey: {
+      type: String,
+      required: false,
+      default: "chronos.timetable.no_timetable_selected.description",
+    },
+  },
+};
+</script>
+
+<template>
+  <v-card
+    class="full-height d-flex align-center justify-center py-10"
+    v-bind="$attrs"
+  >
+    <div class="text-center">
+      <v-icon color="secondary" size="60" class="mb-4"> mdi-grid-off </v-icon>
+      <div class="text-h5 grey--text text--darken-2 mb-2">
+        {{ $t(titleKey) }}
+      </div>
+      <div class="text-body-2 grey--text text--darken-2">
+        {{ $t(descriptionKey) }}
+      </div>
+    </div>
+  </v-card>
+</template>
diff --git a/aleksis/apps/chronos/frontend/components/Timetable.vue b/aleksis/apps/chronos/frontend/components/Timetable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3a7471f6210136eeb59d4aa81f8fa061c0c3c56f
--- /dev/null
+++ b/aleksis/apps/chronos/frontend/components/Timetable.vue
@@ -0,0 +1,218 @@
+<script>
+import { gqlAvailableTimetables } from "./timetables.graphql";
+import NoTimetableCard from "./NoTimetableCard.vue";
+
+export default {
+  name: "Timetable",
+  components: { NoTimetableCard },
+  apollo: {
+    availableTimetables: {
+      query: gqlAvailableTimetables,
+      result(data) {
+        console.log(
+          data.data.availableTimetables.map((a) => {
+            return a.id + a.name;
+          })
+        );
+      },
+    },
+  },
+  data() {
+    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" },
+      },
+    };
+  },
+  watch: {
+    selected(selected) {
+      if (selected == null) {
+        this.selectedFull = null;
+      }
+    },
+    selectedFull(selectedFull) {
+      // Align navigation with currently selected timetable
+      if (!selectedFull) {
+        this.$router.push({ name: "chronos.timetable" });
+      } else if (
+        selectedFull.objId !== this.$route.params.id ||
+        selectedFull.type !== this.$route.params.type
+      ) {
+        this.$router.push({
+          name: "chronos.timetableWithId",
+          params: {
+            type: selectedFull.type.toLowerCase(),
+            id: selectedFull.objId,
+          },
+        });
+      }
+    },
+  },
+  methods: {
+    findNextTimetable(offset = 1) {
+      const currentIndex = this.availableTimetablesIds.indexOf(
+        this.selectedFull.id
+      );
+      const newIndex = currentIndex + offset;
+      if (newIndex < 0 || newIndex >= this.availableTimetablesIds.length) {
+        return null;
+      }
+      return this.availableTimetables[newIndex];
+    },
+    selectTimetable(timetable) {
+      this.selected = timetable.id;
+      this.selectedFull = timetable;
+    },
+  },
+  computed: {
+    selectedTypesFull() {
+      return this.selectedTypes.map((type) => {
+        return this.types[type];
+      });
+    },
+    availableTimetablesFiltered() {
+      // Filter timetables by selected types
+      return this.availableTimetables.filter((timetable) => {
+        return this.selectedTypes.indexOf(timetable.type) !== -1;
+      });
+    },
+    availableTimetablesIds() {
+      return this.availableTimetables.map((timetable) => timetable.id);
+    },
+    prevTimetable() {
+      return this.findNextTimetable(-1);
+    },
+    nextTimetable() {
+      return this.findNextTimetable(1);
+    },
+  },
+};
+</script>
+
+<template>
+  <div>
+    <v-row>
+      <v-col cols="3">
+        <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>
+
+          <!-- 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-card>
+      </v-col>
+      <v-col cols="9" class="full-height">
+        <!-- No timetable card-->
+        <no-timetable-card v-if="selectedFull == null" />
+
+        <!-- Calendar card-->
+        <v-card v-else>
+          <div class="d-flex">
+            <v-card-title>
+              {{ selectedFull.name }}
+            </v-card-title>
+            <v-spacer />
+            <div class="pa-2 mt-1">
+              <v-btn
+                icon
+                :disabled="!prevTimetable"
+                @click="selectTimetable(prevTimetable)"
+                :title="$t('chronos.timetable.prev')"
+              >
+                <v-icon>mdi-chevron-left</v-icon>
+              </v-btn>
+              <v-chip label color="secondary" outlined class="mx-1">{{
+                selectedFull.shortName
+              }}</v-chip>
+              <v-btn
+                icon
+                :disabled="!nextTimetable"
+                @click="selectTimetable(nextTimetable)"
+                :title="$t('chronos.timetable.next')"
+              >
+                <v-icon>mdi-chevron-right</v-icon>
+              </v-btn>
+            </div>
+          </div>
+        </v-card>
+      </v-col>
+    </v-row>
+  </div>
+</template>
diff --git a/aleksis/apps/chronos/frontend/components/timetables.graphql b/aleksis/apps/chronos/frontend/components/timetables.graphql
new file mode 100644
index 0000000000000000000000000000000000000000..5bbe46f734f1981ad1c2648d50a8a6e8d7bd04fc
--- /dev/null
+++ b/aleksis/apps/chronos/frontend/components/timetables.graphql
@@ -0,0 +1,9 @@
+query gqlAvailableTimetables {
+  availableTimetables {
+    id
+    objId
+    type
+    name
+    shortName
+  }
+}
diff --git a/aleksis/apps/chronos/frontend/index.js b/aleksis/apps/chronos/frontend/index.js
index 501071992b10bfad99480b80d2810628d19b48aa..c6492797fe70c3e789fb1f937c7035ad7b3d1da7 100644
--- a/aleksis/apps/chronos/frontend/index.js
+++ b/aleksis/apps/chronos/frontend/index.js
@@ -1,4 +1,5 @@
 import { hasPersonValidator } from "aleksis.core/routeValidators";
+import Timetable from "./components/Timetable.vue";
 
 export default {
   meta: {
@@ -11,6 +12,22 @@ export default {
     byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true,
   },
   children: [
+    {
+      path: "timetable/",
+      component: Timetable,
+      name: "chronos.timetable",
+      meta: {
+        inMenu: true,
+        titleKey: "chronos.timetable.menu_title",
+        icon: "mdi-grid",
+        permission: "chronos.view_timetables_rule",
+      },
+    },
+    {
+      path: "timetable/:type/:id/",
+      component: Timetable,
+      name: "chronos.timetableWithId",
+    },
     {
       path: "",
       component: () => import("aleksis.core/components/LegacyBaseTemplate.vue"),
diff --git a/aleksis/apps/chronos/frontend/messages/en.json b/aleksis/apps/chronos/frontend/messages/en.json
index fb73dca543dfd16a3f9371e3f813fc3d8e4cbd58..4128f0b933a0d07e698204c678da08ca7a9177d3 100644
--- a/aleksis/apps/chronos/frontend/messages/en.json
+++ b/aleksis/apps/chronos/frontend/messages/en.json
@@ -3,7 +3,14 @@
     "menu_title": "Timetables",
     "timetable": {
       "menu_title_all": "All timetables",
-      "menu_title_my": "My timetable"
+      "menu_title_my": "My timetable",
+      "no_timetable_selected": {
+        "title": "No Timetable Selected",
+        "description": "Select a timetable on the left side to show it in this place"
+      },
+      "search": "Search Timetables",
+      "prev": "Previous Timetable",
+      "next": "Next Timetable"
     },
     "lessons": {
       "menu_title_daily": "Daily lessons"
diff --git a/aleksis/apps/chronos/schema/__init__.py b/aleksis/apps/chronos/schema/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e5c7d4e85bd83b35b0ba4873c36b7f6b0ad2aa8f
--- /dev/null
+++ b/aleksis/apps/chronos/schema/__init__.py
@@ -0,0 +1,95 @@
+import graphene
+from graphene_django import DjangoObjectType
+
+from aleksis.core.models import Group, Person, Room
+
+from ..util.chronos_helpers import get_classes, get_rooms, get_teachers
+
+
+class TimetablePersonType(DjangoObjectType):
+    class Meta:
+        model = Person
+        fields = ("id", "first_name", "last_name", "short_name")
+        skip_registry = True
+
+
+class TimetableGroupType(DjangoObjectType):
+    class Meta:
+        model = Group
+        fields = ("id", "name", "short_name")
+        skip_registry = True
+
+
+class TimetableRoomType(DjangoObjectType):
+    class Meta:
+        model = Room
+        fields = ("id", "name", "short_name")
+        skip_registry = True
+
+
+class TimetableType(graphene.Enum):
+    TEACHER = "teacher"
+    GROUP = "group"
+    ROOM = "room"
+
+
+class TimetableObjectType(graphene.ObjectType):
+    id = graphene.String()  # noqa
+    obj_id = graphene.String()
+    name = graphene.String()
+    short_name = graphene.String()
+    type = graphene.Field(TimetableType)  # noqa
+
+    def resolve_obj_id(root, info, **kwargs):
+        return root.id
+
+    def resolve_id(root, info, **kwargs):
+        return f"{root.type.value}-{root.id}"
+
+
+class Query(graphene.ObjectType):
+    timetable_teachers = graphene.List(TimetablePersonType)
+    timetable_groups = graphene.List(TimetableGroupType)
+    timetable_rooms = graphene.List(TimetableRoomType)
+    available_timetables = graphene.List(TimetableObjectType)
+
+    def resolve_timetable_teachers(self, info, **kwargs):
+        return get_teachers(info.context.user)
+
+    def resolve_timetable_groups(self, info, **kwargs):
+        return get_classes(info.context.user)
+
+    def resolve_timetable_rooms(self, info, **kwargs):
+        return get_rooms(info.context.user)
+
+    def resolve_available_timetables(self, info, **kwargs):
+        all_timetables = []
+        for group in get_classes(info.context.user):
+            all_timetables.append(
+                TimetableObjectType(
+                    id=group.id,
+                    name=group.name,
+                    short_name=group.short_name,
+                    type=TimetableType.GROUP,
+                )
+            )
+
+        for teacher in get_teachers(info.context.user):
+            print(teacher.full_name)
+            all_timetables.append(
+                TimetableObjectType(
+                    id=teacher.id,
+                    name=teacher.full_name,
+                    short_name=teacher.short_name,
+                    type=TimetableType.TEACHER,
+                )
+            )
+
+        for room in get_rooms(info.context.user):
+            all_timetables.append(
+                TimetableObjectType(
+                    id=room.id, name=room.name, short_name=room.short_name, type=TimetableType.ROOM
+                )
+            )
+
+        return all_timetables
diff --git a/aleksis/apps/chronos/util/chronos_helpers.py b/aleksis/apps/chronos/util/chronos_helpers.py
index 17d98c54a88e28c9be5ca81d8561924d4155c83a..5664c0b97e13e68ab18120a2cadfd39396eaa8a4 100644
--- a/aleksis/apps/chronos/util/chronos_helpers.py
+++ b/aleksis/apps/chronos/util/chronos_helpers.py
@@ -9,7 +9,7 @@ from django.utils import timezone
 
 from guardian.core import ObjectPermissionChecker
 
-from aleksis.core.models import Announcement, Group, Person, Room, SchoolTerm
+from aleksis.core.models import Announcement, Group, Person, Room
 from aleksis.core.util.core_helpers import get_site_preferences
 from aleksis.core.util.predicates import check_global_permission
 
@@ -74,10 +74,8 @@ def get_teachers(user: "User"):
     """Get the teachers whose timetables are allowed to be seen by current user."""
     checker = ObjectPermissionChecker(user)
 
-    school_term = SchoolTerm.current
-    school_term_q = Q(lessons_as_teacher__validity__school_term=school_term) if school_term else Q()
     teachers = (
-        Person.objects.annotate(lessons_count=Count("lessons_as_teacher", filter=school_term_q))
+        Person.objects.annotate(lessons_count=Count("lesson_events_as_teacher"))
         .filter(lessons_count__gt=0)
         .order_by("short_name", "last_name")
     )
@@ -93,6 +91,8 @@ def get_teachers(user: "User"):
 
         teachers = teachers.filter(Q(pk=user.person.pk) | Q(pk__in=wanted_teachers))
 
+    teachers = teachers.distinct()
+
     return teachers
 
 
@@ -103,8 +103,8 @@ def get_classes(user: "User"):
     classes = (
         Group.objects.for_current_school_term_or_all()
         .annotate(
-            lessons_count=Count("lessons"),
-            child_lessons_count=Count("child_groups__lessons"),
+            lessons_count=Count("lesson_events"),
+            child_lessons_count=Count("child_groups__lesson_events"),
         )
         .filter(
             Q(lessons_count__gt=0, parent_groups=None)
@@ -128,6 +128,8 @@ def get_classes(user: "User"):
         if user.person.primary_group:
             classes = classes.filter(Q(pk=user.person.primary_group.pk))
 
+    classes = classes.distinct()
+
     return classes
 
 
@@ -135,13 +137,8 @@ def get_rooms(user: "User"):
     """Get the rooms whose timetables are allowed to be seen by current user."""
     checker = ObjectPermissionChecker(user)
 
-    school_term = SchoolTerm.current
-    school_term_q = (
-        Q(lesson_periods__lesson__validity__school_term=school_term) if school_term else Q()
-    )
-
     rooms = (
-        Room.objects.annotate(lessons_count=Count("lesson_periods", filter=school_term_q))
+        Room.objects.annotate(lessons_count=Count("lesson_events"))
         .filter(lessons_count__gt=0)
         .order_by("short_name", "name")
     )
@@ -157,6 +154,8 @@ def get_rooms(user: "User"):
 
         rooms = rooms.filter(Q(pk__in=wanted_rooms))
 
+    rooms = rooms.distinct()
+
     return rooms