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' : ''}` +}