diff --git a/src/Pages/EmissionManager.vue b/src/Pages/EmissionManager.vue
index ee2921815637fdc706ea371d765c7e1a91f47d1f..adf7d883e2e214c4ea31dee513b3aa8486771841 100644
--- a/src/Pages/EmissionManager.vue
+++ b/src/Pages/EmissionManager.vue
@@ -83,115 +83,12 @@
           </div>
         </KeepAlive>
 
-        <div
+        <CalendarDayView
           v-if="view === 'day'"
-          :class="{
-            'schedule-panel tw-w-full': true,
-          }"
-        >
-          <div class="tw-flex tw-items-center tw-justify-between tw-mb-4">
-            <h3>{{ prettyDate(selectedDay) }}</h3>
-
-            <b-button-group>
-              <b-button type="info" @click="changeDay(-1)">
-                <svg
-                  class="tw-w-4 tw-h-4"
-                  fill="none"
-                  stroke="currentColor"
-                  viewBox="0 0 24 24"
-                  xmlns="http://www.w3.org/2000/svg"
-                >
-                  <path
-                    stroke-linecap="round"
-                    stroke-linejoin="round"
-                    stroke-width="2"
-                    d="M15 19l-7-7 7-7"
-                  />
-                </svg>
-              </b-button>
-
-              <b-button type="info" @click="changeDay(1)">
-                <svg
-                  class="tw-w-4 tw-h-4"
-                  fill="none"
-                  stroke="currentColor"
-                  viewBox="0 0 24 24"
-                  xmlns="http://www.w3.org/2000/svg"
-                >
-                  <path
-                    stroke-linecap="round"
-                    stroke-linejoin="round"
-                    stroke-width="2"
-                    d="M9 5l7 7-7 7"
-                  />
-                </svg>
-              </b-button>
-            </b-button-group>
-          </div>
-
-          <div
-            v-for="timeslot in timeslotsForDay"
-            :key="timeslot.id"
-            class="timeslot tw-w-full tw-py-2 tw-px-3 tw-rounded hover:tw-bg-gray-200 hover:tw-text-gray-900 tw-cursor-pointer tw-border tw-border-solid tw-border-gray-200 tw-mb-2"
-            :class="{
-              'tw-bg-gray-900 tw-text-white':
-                new Date() >= new Date(timeslot.start) && new Date() <= new Date(timeslot.end),
-              'tw-bg-gray-200 tw-text-gray-600 tw-opacity-75': new Date() >= new Date(timeslot.end),
-            }"
-            @click="() => timeslotClicked(timeslot)"
-          >
-            <div class="tw-flex tw-justify-between tw-items-center">
-              <div>
-                <p class="tw-mb-0 tw-leading-tight tw-font-bold">
-                  <SafeHTML :html="timeslot.show.name" sanitize-preset="inline-noninteractive" />
-                </p>
-                <span class="tw-text-sm">
-                  {{ prettyTime(timeslot.start) }} - {{ prettyTime(timeslot.end) }}
-                  <span v-if="timeslot.repetitionOfId" class="tw-text-gray-400">{{
-                    $t('calendar.repetition')
-                  }}</span>
-                </span>
-              </div>
-
-              <div v-if="loaded.playlists">
-                <div v-if="!timeslot.playlistId">
-                  <span v-if="timeslot.show.defaultPlaylistId" class="tw-leading-none">
-                    <span class="tw-block">
-                      <strong>{{ $t('emissionTable.playlist') }}:</strong>
-                      {{
-                        getPlaylistById(timeslot.show.defaultPlaylistId)?.description ||
-                        timeslot.show.defaultPlaylistId
-                      }}
-                    </span>
-                    <span class="tw-text-xs tw-text-red-500">{{ $t('calendar.fallback') }}</span>
-                  </span>
-                  <span v-else>
-                    {{ $t('calendar.empty') }}
-                  </span>
-                </div>
-                <div v-else-if="timeslot.playlist" class="tw-leading-none">
-                  <span class="tw-block">
-                    <strong>{{ $t('emissionTable.playlist') }}:</strong>
-                    {{ timeslot.playlist.description || timeslot.playlistId }}
-                  </span>
-                  <span class="tw-text-sm">
-                    <strong>{{ $t('emissionTable.duration') }}:</strong>
-                    {{ playlistDuration(timeslot.playlist) }}
-                    <span
-                      v-if="isMismatchedLength(timeslot.playlist, timeslot)"
-                      class="is-mismatched"
-                    >
-                      {{ $t('calendar.mismatchedLength') }}
-                    </span>
-                  </span>
-                </div>
-                <div v-else>
-                  <p>{{ $t('emissionTable.missingPlaylistData') }}</p>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
+          :selected-day="selectedDay"
+          @change-day="changeDay($event)"
+          @edit-timeslot="editTimeslot($event)"
+        />
       </div>
 
       <app-modalEmissionManagerCreate
@@ -237,11 +134,11 @@ import {
   getNextAvailableSlot,
   sanitizeHTML,
 } from '@/util'
-import SafeHTML from '@/components/generic/SafeHTML'
+import CalendarDayView from '@/components/CalendarDayView.vue'
 
 export default {
   components: {
-    SafeHTML,
+    CalendarDayView,
     PageHeader,
     ServerErrors,
     FullCalendar,
@@ -294,29 +191,6 @@ export default {
       }
     },
 
-    timeslotsForDay() {
-      return this.timeslots
-        .filter(
-          (timeslot) =>
-            getISODateString(new Date(timeslot.start)) === getISODateString(this.selectedDay),
-        )
-        .map((timeslot) => {
-          const id = timeslot.showId
-          const show = this.getShow({ id })
-          const playlist = timeslot.playlistId
-            ? this.getPlaylistById(timeslot.playlistId) ?? null
-            : null
-
-          return {
-            ...timeslot,
-            playlist,
-            show: {
-              ...show,
-            },
-          }
-        })
-    },
-
     /**
      * this is the whole configuration for our schedule calendar, including
      * simple event handlers that do not need the whole components scope
@@ -493,10 +367,6 @@ export default {
     this.stopRenderWatcher()
   },
   methods: {
-    changeView(view) {
-      this.view = view
-    },
-
     changeDay(delta) {
       this.selectedDay = new Date(this.selectedDay.setDate(this.selectedDay.getDate() + delta))
     },
@@ -529,15 +399,8 @@ export default {
       }
     },
 
-    timeslotClicked(slot) {
-      const timeslot = { ...slot }
-      timeslot.showId = slot.show.id
-
-      if (timeslot.showId !== this.selectedShow.id) {
-        this.switchShow(this.getShowIndexById(timeslot.showId))
-      } else {
-        this.$refs.appModalEmissionManagerEdit.open(timeslot)
-      }
+    editTimeslot(timeslot) {
+      this.$refs.appModalEmissionManagerEdit.open(timeslot)
     },
 
     // this handler will be called whenever the user clicks on one of the
@@ -863,28 +726,6 @@ export default {
         },
       })
     },
-
-    isMismatchedLength(playlist, timeslot) {
-      const timeslotDuration = this.minutesToNanoseconds(
-        this.prettyDuration(timeslot.start, timeslot.end).minutes,
-      )
-
-      const playlistDuration = this.hmsToNanoseconds(this.playlistDuration(playlist))
-
-      let delta = 0
-      const unknowns = playlist.entries.filter((entry) => !entry.duration)
-      if (unknowns.length === 1) {
-        delta = timeslotDuration - playlistDuration
-      }
-
-      console.log(timeslotDuration, playlistDuration + delta)
-
-      return timeslotDuration !== playlistDuration + delta
-    },
-
-    notYetImplemented: function () {
-      alert(this.$t('unimplemented'))
-    },
   },
 }
 </script>
diff --git a/src/components/CalendarDayView.vue b/src/components/CalendarDayView.vue
new file mode 100644
index 0000000000000000000000000000000000000000..89726311ee295d6e08487af652ac0c1203385657
--- /dev/null
+++ b/src/components/CalendarDayView.vue
@@ -0,0 +1,166 @@
+<template>
+  <div class="schedule-panel tw-w-full">
+    <div class="tw-flex tw-items-center tw-justify-between tw-mb-4">
+      <h3>{{ prettyDate(selectedDay) }}</h3>
+
+      <div class="fc fc-direction-ltr">
+        <div class="fc-button-group">
+          <button
+            type="button"
+            class="fc-button fc-prev-button fc-button-primary"
+            @click="emit('changeDay', -1)"
+          >
+            <span class="fc-icon fc-icon-chevron-left"></span>
+          </button>
+
+          <button
+            type="button"
+            class="fc-button fc-next-button fc-button-primary"
+            @click="emit('changeDay', 1)"
+          >
+            <span class="fc-icon fc-icon-chevron-right"></span>
+          </button>
+        </div>
+      </div>
+    </div>
+
+    <div
+      v-for="{
+        timeslot,
+        timeslotDurationInSeconds,
+        playlist,
+        playlistDurationInSeconds,
+        show,
+      } in slotData"
+      :key="timeslot.id"
+      class="timeslot tw-w-full tw-py-2 tw-px-3 tw-rounded hover:tw-bg-gray-200 hover:tw-text-gray-900 tw-cursor-pointer tw-border tw-border-solid tw-border-gray-200 tw-mb-2"
+      :class="{
+        'tw-bg-gray-900 tw-text-white':
+          now >= parseISO(timeslot.start) && now <= parseISO(timeslot.end),
+        'tw-bg-gray-200 tw-text-gray-600 tw-opacity-75': now >= parseISO(timeslot.end),
+      }"
+      @click="onTimeslotClick(timeslot)"
+    >
+      <div class="tw-flex tw-justify-between tw-items-center">
+        <div>
+          <p class="tw-mb-0 tw-leading-tight tw-font-bold">
+            <SafeHTML v-if="show" :html="show.name" sanitize-preset="inline-noninteractive" />
+          </p>
+          <span class="tw-text-sm">
+            {{ prettyTime(timeslot.start) }} - {{ prettyTime(timeslot.end) }}
+            <span v-if="timeslot.repetitionOfId" class="tw-text-gray-400">
+              {{ t('calendar.repetition') }}
+            </span>
+          </span>
+        </div>
+
+        <div v-if="(timeslot.playlistId || show?.defaultPlaylistId) && playlist">
+          <p>
+            <strong>{{ t('emissionTable.playlist') }}:</strong>
+            {{ playlist.description }}
+            <span
+              v-if="!timeslot.playlistId && show?.defaultPlaylistId"
+              class="tw-text-xs tw-text-red-500"
+            >
+              {{ t('calendar.fallback') }}
+            </span>
+          </p>
+
+          <p v-if="timeslot.playlistId" class="tw-text-sm">
+            <strong>{{ t('emissionTable.duration') }}:</strong>
+            {{ secondsToDurationString(playlistDurationInSeconds ?? timeslotDurationInSeconds) }}
+            <span
+              v-if="
+                playlistDurationInSeconds !== null &&
+                timeslotDurationInSeconds !== playlistDurationInSeconds
+              "
+              class="is-mismatched"
+            >
+              {{ t('calendar.mismatchedLength') }}
+            </span>
+          </p>
+        </div>
+
+        <p v-else>
+          {{ t('calendar.empty') }}
+        </p>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import SafeHTML from '@/components/generic/SafeHTML'
+import { usePretty } from '@/mixins/prettyDate'
+import { useI18n } from '@/i18n'
+import { computed } from 'vue'
+import { getISODateString, useSelectedShow } from '@/utilities'
+import { useStore } from 'vuex'
+import { Playlist, Show, TimeSlot } from '@/types'
+import { parseISO } from 'date-fns'
+import { calculatePlaylistDurationInSeconds, usePlaylistStore } from '@/stores/playlists'
+import { calculateDurationSeconds, secondsToDurationString } from '@/util'
+import { useNow } from '@vueuse/core'
+
+type SlotData = {
+  timeslot: TimeSlot
+  timeslotDurationInSeconds: number
+  show: Show | undefined
+  playlist: Playlist | undefined
+  playlistDurationInSeconds: number | null
+}
+
+const props = defineProps<{
+  selectedDay: Date
+}>()
+const emit = defineEmits<{
+  changeDay: [offset: number]
+  editTimeslot: [timeslot: TimeSlot]
+}>()
+const { t } = useI18n()
+const { prettyDate, prettyTime } = usePretty()
+const now = useNow({ interval: 60_000 })
+const store = useStore()
+const selectedShow = useSelectedShow()
+const shows = computed<Show[]>(() => store.state.shows.shows)
+const timeslots = computed<TimeSlot[]>(() => store.state.shows.timeslots)
+const { itemMap: playlistMap, retrieve: retrievePlaylist } = usePlaylistStore()
+const selectedDayISODate = computed(() => getISODateString(props.selectedDay))
+const slotData = computed<SlotData[]>(() => {
+  const result: SlotData[] = []
+  const daySlots = timeslots.value.filter(
+    (timeslot) => timeslot.start.split('T')[0] === selectedDayISODate.value,
+  )
+
+  for (const timeslot of daySlots) {
+    const show = shows.value.find(({ id }) => timeslot.showId === id)
+    const playlistId = timeslot.playlistId ?? show?.defaultPlaylistId
+    // enqueue a request for the playlist in case it’s not yet in the store
+    if (playlistId) retrievePlaylist(playlistId, undefined, { useCached: true })
+    const playlist = playlistId ? playlistMap.get(playlistId) : undefined
+    const timeslotDurationInSeconds = calculateDurationSeconds(
+      parseISO(timeslot.start),
+      parseISO(timeslot.end),
+    )
+    const playlistDurationInSeconds = playlist ? calculatePlaylistDurationInSeconds(playlist) : null
+
+    result.push({
+      timeslot,
+      timeslotDurationInSeconds,
+      playlist,
+      playlistDurationInSeconds,
+      show,
+    })
+  }
+
+  return result
+})
+
+function onTimeslotClick(timeslot: TimeSlot) {
+  if (selectedShow.value.id !== timeslot.showId) {
+    selectedShow.value = { id: timeslot.showId } as Show
+  } else {
+    emit('editTimeslot', timeslot)
+  }
+}
+</script>
diff --git a/src/util.ts b/src/util.ts
index 8d954fabd22454c7c963ed5f3549da2bebe7d740..685723f731b8f44ae23560680333188ddd4d4aa0 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -134,3 +134,10 @@ export function useSelectedShow() {
     },
   })
 }
+
+export function secondsToDurationString(seconds: number): string {
+  const h = Math.floor(seconds / 3600)
+  const m = Math.floor((seconds % 3600) / 60)
+  const s = Math.round(seconds % 60)
+  return `${h ? h + 'h ' : ''}${m ? m + 'min ' : ''}${s ? s + 's' : ''}`
+}