Skip to content
Snippets Groups Projects
EmissionManager.vue 29 KiB
Newer Older
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
  <b-container>
    <template v-if="selectedShow">
      <auth-wall>
        <show-selector
          ref="showSelector"
          :title="$t('navigation.calendar')"
          :calendar="true"
          :callback="showHasSwitched"
          @set-view="changeView"
        />
        <hr />

        <b-alert :variant="conflictCount > 0 ? 'danger' : 'success'" :show="conflictMode">
          <div v-if="conflictMode">
            <h4>{{ $t('conflictResolution.title') }}</h4>
            <p
              :class="{
                'tw-mb-4': resolveData.schedule.rrule === 1,
                'tw-mb-0': resolveData.schedule.rrule !== 1,
              }"
              v-html="
                $t('conflictResolution.newSchedule', {
                  firstDate: prettyDate(resolveData.schedule.first_date),
                  startTime: resolveData.schedule.start_time,
                  endTime: resolveData.schedule.end_time,
                })
              "
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed

            <p
              v-if="resolveData.schedule.rrule !== 1"
              v-html="
                $t('conflictResolution.recurringSchedule', {
                  rrule: rruleRender(resolveData.schedule.rrule),
                  lastDate: prettyDate(resolveData.schedule.last_date),
                })
              "
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed

            <div v-if="submitting">
              <b-row>
                <b-col align="center">
                  <img src="/assets/radio.gif" :alt="$t('loading')" />
                </b-col>
              </b-row>
            </div>
            <div v-else>
              <div v-if="conflictCount > 0">
                <p>{{ $t('conflictResolution.leftToResolve', { smart_count: conflictCount }) }}</p>

                <b-button variant="danger" size="sm" @click="resolveCancel">
                  {{ $t('cancel') }}
                </b-button>
              </div>
              <div v-else>
                <p>{{ $t('conflictResolution.noneLeftToResolve') }}</p>
                <b-button variant="success" @click="resolveSubmit">
                  {{ $t('conflictResolution.applySolution') }}
                </b-button>
                &nbsp;
                <b-button variant="danger" @click="resolveCancel">
                  {{ $t('cancel') }}
                </b-button>
              </div>
            </div>
          </div>
        </b-alert>

        <server-errors :errors="serverErrors" />

Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
          <div :class="{ 'tw-hidden': view !== 'week' }">
            <FullCalendar ref="calendar" :options="calendarConfig" />
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
          </div>

          <div
            :class="{
              'schedule-panel tw-w-full': true,
              'tw-hidden': view !== 'day',
            }"
          >
            <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': true,
                '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">
                    {{ timeslot.show.name }}
                  </p>
                  <span class="tw-text-sm">
                    {{ prettyTime(timeslot.start) }} - {{ prettyTime(timeslot.end) }}
                    <span v-if="timeslot.is_repetition" class="tw-text-gray-400">{{
                      $t('calendar.repetition')
                    }}</span>
                  </span>
                </div>
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
                <div v-if="loaded.playlists">
                  <div v-if="!timeslot.playlist_id">
                    <span v-if="timeslot.show.default_playlist_id" class="tw-leading-none">
                      <span class="tw-block"
                        ><strong>{{ $t('emissionTable.playlist') }}:</strong>
                        {{
                          getPlaylistById(timeslot.show.default_playlist_id).description ||
                          timeslot.show.default_playlist_id
                        }}</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 class="tw-leading-none">
                    <span class="tw-block"
                      ><strong>{{ $t('emissionTable.playlist') }}:</strong>
                      {{ timeslot.playlist.description || timeslot.playlist_id }}</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>
              </div>
            </div>
          </div>
        </div>
      </auth-wall>

      <app-modalEmissionManagerCreate
        ref="appModalEmissionManagerCreate"
        @conflict="enterConflictMode"
      />
      <app-modalEmissionManagerResolve
        ref="appModalEmissionManagerResolve"
        @resolve-conflict="resolveConflict"
      />
      <app-modalEmissionManagerEdit
        ref="appModalEmissionManagerEdit"
        @conflict="enterConflictMode"
      />
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
    </template>

    <div
      v-else-if="loaded.shows && !selectedShow"
      class="tw-text-center"
      v-html="$t('noAssignedShows', { adminUrl })"
    />
  </b-container>
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
import { mapGetters } from 'vuex'
// We need the @fullcalendar/core/vdom import because it has side-effects without which
// Vite would throw an error when loading the FullCalendar vue component.
import '@fullcalendar/core/vdom'
import FullCalendar from '@fullcalendar/vue3'
import fullCalendarTimeGridPlugin from '@fullcalendar/timegrid'
import fullCalendarInteractionPlugin from '@fullcalendar/interaction'

import showSelector from '@/components/ShowSelector.vue'
import modalEmissionManagerCreate from '@/components/emissions/ModalCreate.vue'
import modalEmissionManagerResolve from '@/components/emissions/ModalResolve.vue'
import modalEmissionManagerEdit from '@/components/emissions/ModalEdit.vue'
import AuthWall from '@/components/AuthWall.vue'
import rrules from '@/mixins/rrules'
import prettyDate from '@/mixins/prettyDate'
import playlist from '@/mixins/playlist'
import ServerErrors from '@/components/ServerErrors.vue'
import { getISODateString } from '@/utilities'
    FullCalendar,
    AuthWall,
    'show-selector': showSelector,
    'app-modalEmissionManagerCreate': modalEmissionManagerCreate,
    'app-modalEmissionManagerResolve': modalEmissionManagerResolve,
    'app-modalEmissionManagerEdit': modalEmissionManagerEdit,
  },

  mixins: [rrules, prettyDate, playlist],
      view: 'week',
      adminUrl: `${import.meta.env.VUE_APP_BASEURI_STEERING}/admin`,
      calendarSlots: [],
      selectedDay: new Date(),

      // flag for when submitting resolve data
      submitting: false,

      // this flag signifies if we are in conflict resolution mode
      conflictMode: false,

      // when conflict mode is activated, this should hold the steering response
      // from schedule creation, with all the conflicts and solutions
      resolveData: null,
      conflictCount: 0,
      conflictSolutions: [],
      currentStart: undefined,
      currentEnd: undefined,
      stopRenderWatcher: () => {},
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
      getShow: 'shows/getShowByDataParam',
    }),

    loaded() {
      return {
        shows: this.$store.state.shows.loaded['shows'],
        timeslots: this.$store.state.shows.loaded['timeslots'],
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
        playlists: this.$store.state.playlists.loaded['playlists'],
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
        .filter((timeslot) => {
          const dateFormat = new Intl.DateTimeFormat('en', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
          return dateFormat.format(new Date(timeslot.start)) === dateFormat.format(this.selectedDay)
        })
        .map((timeslot) => {
          const id = timeslot.show
          const show = this.getShow({ id })

          return {
            ...timeslot,
            playlist: timeslot.playlist_id ? this.getPlaylistById(timeslot.playlist_id) : null,
            show: {
              ...show,
            },
          }
        })
    },

    // this is the whole configuration for our schedule calendar, including
    // simple event handlers that do not need the whole components scope
    calendarConfig() {
      const selectDay = this.selectDay.bind(this)

      return {
        plugins: [fullCalendarTimeGridPlugin, fullCalendarInteractionPlugin],
        initialView: 'timeGridWeek',
        locale: this.$activeLocale(),
        height: 600,
        firstDay: 1,
        navLinks: true,
        events: this.calendarSlots,
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
          today: this.$t('calendar.today'),
        headerToolbar: {
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
          right: 'today prev,next',
        dayHeaderFormat: { day: 'numeric', month: 'numeric', weekday: 'short' },
        eventTimeFormat: { hour: 'numeric', minute: '2-digit' },
        slotLabelFormat: { hour: 'numeric', minute: '2-digit' },
        allDaySlot: false,
        editable: false,
        datesSet: (view) => {
          if (
            this.currentStart?.toISOString?.() !== view.start.toISOString() ||
            this.currentEnd?.toISOString?.() !== view.end.toISOString()
          ) {
            this.currentStart = view.start
            this.currentEnd = view.end
          }
        eventDidMount({ event, el }) {
          // here we add a simple tooltip to every event, so that the full title
          // of a show can be viewed
          el.setAttribute('title', event.title)
        eventClick: this.eventSelected,
        select: this.createEvent,
        selectable: true,
        selectMirror: true,
          selectDay(selectedDate)
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
          document
            .querySelectorAll('thead .fc-day[data-date]')
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
            .forEach((el) => el.classList.remove('fc-day-selected'))
          document
            .querySelector(`thead .fc-day[data-date="${getISODateString(selectedDate)}"]`)
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
            .classList.add('fc-day-selected')
        },
      playlists: 'playlists/playlists',
      selectedShow: 'shows/selectedShow',
      timeslots: 'shows/timeslots',
      getPlaylistById: 'playlists/getPlaylistById',
      files: 'files/files',
      getFileById: 'files/getFileById',
  },

  created() {
    this.$store.dispatch('shows/fetchShows', {
      callback: () => {
        if (!this.selectedShow) {
          return
        }

        this.$nextTick(() => {
          this.showHasSwitched()
          this.$refs.showSelector.updateInputSelector()
        })
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
        document
          .querySelectorAll('.fc-day-header[data-date]')
          .forEach((el) => el.classList.remove('fc-day-selected'))
        document
          .querySelector(`.fc-day-header[data-date="${getISODateString()}"]`)
          .classList.add('fc-day-selected')
      } catch (e) {
        // Full Calendar might not be initialized yet.
      }
    // Watches all relevant properties for timeslot updates
    // and executes the timeslot updater when a change is detected.
    // Array joining in the source is done because arrays are compared by identity in JavaScript.
    // Also… this would look so much nicer as a watchEffect(() => { ... })
    // but would obviously require a Composition-API rewrite.
    this.stopRenderWatcher = this.$watch(
      (em) =>
        [
          em.loaded.shows,
          em.loaded.playlists,
          em.conflictMode,
          em.currentStart?.toISOString?.(),
          em.currentEnd?.toISOString?.(),
        ].join('-'),
      () => {
        // this is called when the user changes the calendar view, so we just
        // re-fetch the timeslots with the updated visible date range
        if (this.loaded.shows && this.loaded.playlists) {
          // we only load new timeslots, if we are not in conflict mode
          if (!this.conflictMode) {
            this.loadTimeslots(
              getISODateString(this.currentStart),
              getISODateString(this.currentEnd),
            )
          }
        }
      },
    )
  },
  unmounted() {
    this.stopRenderWatcher()
  },
    changeView(view) {
      this.view = view
    },

    changeDay(delta) {
      this.selectedDay = new Date(this.selectedDay.setDate(this.selectedDay.getDate() + delta))
    },

    selectDay(date) {
      this.selectedDay = date
    },

    switchShow(index) {
      this.$store.commit('shows/switchShow', index)
      this.loadCalendarSlots()
    },

    // This is the callback function that is activated by the show-selector
    // component, whenever the user switches to a different show
    showHasSwitched() {
      this.$store.dispatch('playlists/fetch', {
        slug: this.selectedShow.slug,
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
        callback: () => console.log(this.playlists),
      this.$log.debug('show has switched to', this.selectedShow.name)
      this.loadTimeslots(getISODateString(this.currentStart), getISODateString(this.currentEnd))
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
      let i = this.shows.findIndex((show) => show.id === id)
      if (i >= 0) {
        return this.shows[i].name
      } else {
        return 'Error: no show found for this timeslot'
      }
    },

    getShowIndexById(id) {
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
      let i = this.shows.findIndex((show) => show.id === id)
      if (i >= 0) {
        return i
      } else {
        this.$log.error('No show found for id ' + id)
        return 0
      }
    },

    timeslotClicked(slot) {
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
      const timeslot = { ...slot }
      timeslot.show = slot.show.id

      if (timeslot.show !== this.selectedShow.id) {
        this.switchShow(this.getShowIndexById(timeslot.show))
      } else {
        this.$refs.appModalEmissionManagerEdit.open(timeslot)
      }
    },

    // this handler will be called whenever the user clicks on one of the
    // displayed timeslots
    eventSelected({ event }) {
      // in conflict resolution mode only the newly generated events are
      // clickable. if there is no conflict for an event, only a modal
      // with a short notice should be opened, otherwise the resolution modal
        const projectedTimeslot = event.extendedProps
        if (this.conflictSolutions[projectedTimeslot.hash] === undefined) {
          this.$refs.appModalEmissionManagerResolve.openNotNeeded()
        } else {
          this.$refs.appModalEmissionManagerResolve.open(projectedTimeslot)
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
      // standard mode only those events are clickable that belong to the
        const selectedTimeslotId = event.extendedProps.id
        const timeslot = this.timeslots.find((slot) => slot.id === selectedTimeslotId)
        if (timeslot.show !== this.selectedShow.id) {
          this.switchShow(this.getShowIndexById(timeslot.show))
        } else {
          this.$refs.appModalEmissionManagerEdit.open(timeslot)
        }
      }
    },

    // this handler is called when the user creates a new timeslot
    createEvent({ start, end }) {
        this.$refs.appModalEmissionManagerCreate.open(start, end)
    enterConflictMode(data) {
      this.resolveData = data
      this.conflictMode = true
      this.conflictCount = 0
      this.conflictSolutions = data.solutions
      this.calendarSlots = []
      try {
        for (const projectedTimeslot of data.projected) {
          // we need a numeric ID for the event for later selection by the user.
          // with converting the hash to a number (in this case a float), we
          // do not risk using a number that is already used by a timeslot id
          // of a conflicting timeslot
          const id = Number(projectedTimeslot.hash)
          const newSlot = {
            id,
            start: projectedTimeslot.start,
            end: projectedTimeslot.end,
            title: this.$t('conflictResolution.conflictingSlot'),
            extendedProps: {
              id,
              // the hash is needed to compare against solutions and conflicts
              hash: projectedTimeslot.hash,
              start: projectedTimeslot.start,
              end: projectedTimeslot.end,
              collisions: [],
              solutionChoices: [],
              title: this.$t('conflictResolution.conflictingSlot'),
            },

          if (projectedTimeslot.collisions.length > 0) {
            newSlot.extendedProps.solutionChoices = projectedTimeslot.solution_choices
            for (const collision of projectedTimeslot.collisions) {
              const conflictingSlot = {
                id: collision.id,
                start: collision.start,
                end: collision.end,
                title: collision.show_name,
                extendedProps: {
                  id: collision.id,
                  start: collision.start,
                  end: collision.end,
                  title: collision.show_name,
                },
              }
              this.calendarSlots.push(conflictingSlot)
              this.conflictCount++
              newSlot.extendedProps.collisions.push(collision)
            }
          }
          this.calendarSlots.push(newSlot)
        }
      } catch (err) {
        this.$log.error(err)
      }
    },

    resolveConflict(toResolve, mode) {
      const calendarSlot = this.calendarSlots.find((s) => s.id === toResolve.id)
      const originalSlot = this.resolveData.projected.find((s) => s.hash === toResolve.hash)
      // we only need the conflicting slot specifically for theirs-both mode, where there should be only one collision
      const conflictingSlot = this.calendarSlots.find((s) => s.id === toResolve.collisions[0].id)

      // we only reduce the conflict count, if there was no other solution set already
      if (this.conflictSolutions[toResolve.hash] === '') {
        this.conflictCount -= toResolve.collisions.length
      }

      // if there already was a resolution chosen before, that added a second timeslot
      // because either the "ours" or "theirs" was split, we have to clean it up before
      // in the calendar before setting a new resolution
      const oldResolutionSlot = this.calendarSlots.findIndex((s) => s.id === calendarSlot.id * 10)
      if (oldResolutionSlot > -1) {
        this.calendarSlots.splice(oldResolutionSlot, 1)
      }

      // and in case their was a ours-* choice before, we have to reset the (one)
      // conflicting slot to its original start and end time
      conflictingSlot.start = toResolve.collisions[0].start
      conflictingSlot.end = toResolve.collisions[0].end

David Trattnig's avatar
David Trattnig committed
      // for a detailed description of the resolution modes, see https://docs.aura.radio/en/latest/user/timeslot-collision-detection.html
      // and https://docs.aura.radio/en/latest/developer/misc/conflict-resolution.html
      switch (mode) {
        case 'theirs':
          this.conflictSolutions[toResolve.hash] = mode
          calendarSlot.className = 'timeslot-discarded'
          calendarSlot.title = this.$t('conflictResolution.conflictingSlot')
          calendarSlot.start = originalSlot.start
          calendarSlot.end = originalSlot.end
          for (let theirs of toResolve.collisions) {
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
            this.calendarSlots.find((s) => s.id === theirs.id).className = 'timeslot-accepted'
          }
          break

        case 'ours':
          this.conflictSolutions[toResolve.hash] = mode
          calendarSlot.className = 'timeslot-accepted'
          calendarSlot.title = this.$t('conflictResolution.conflictingSlot')
          calendarSlot.start = originalSlot.start
          calendarSlot.end = originalSlot.end
          for (let theirs of toResolve.collisions) {
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
            this.calendarSlots.find((s) => s.id === theirs.id).className = 'timeslot-discarded'
          }
          break

        case 'theirs-start':
        case 'theirs-end':
        case 'theirs-both':
          this.conflictSolutions[toResolve.hash] = mode
          calendarSlot.className = 'timeslot-partly'
          calendarSlot.title = 'new [' + mode + ']'
          if (mode === 'theirs-start') {
            calendarSlot.start = toResolve.collisions[0].end
            calendarSlot.end = originalSlot.end
          } else if (mode === 'theirs-end') {
            calendarSlot.start = originalSlot.start
            calendarSlot.end = toResolve.collisions[0].start
          } else {
            calendarSlot.start = originalSlot.start
            calendarSlot.end = toResolve.collisions[0].start
            // TODO: that id generation seems __very__ weird
            const id = calendarSlot.id * 10
              start: toResolve.collisions[0].end,
              end: originalSlot.end,
              title: 'new [theirs-both]',
              className: 'timeslot-partly',
              editable: false,
              extendedProps: {
                id,
                start: toResolve.collisions[0].end,
                end: originalSlot.end,
                title: 'new [theirs-both]',
              },
          for (const theirs of toResolve.collisions) {
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
            this.calendarSlots.find((s) => s.id === theirs.id).className = 'timeslot-accepted'
          }
          break

        case 'ours-start':
        case 'ours-end':
        case 'ours-both':
          this.conflictSolutions[toResolve.hash] = mode
          calendarSlot.className = 'timeslot-accepted'
          calendarSlot.title = 'new [' + mode + ']'
          if (mode === 'ours-start') {
            conflictingSlot.start = toResolve.collisions[0].start
            conflictingSlot.end = originalSlot.start
          } else if (mode === 'ours-end') {
            conflictingSlot.start = originalSlot.end
            conflictingSlot.end = toResolve.collisions[0].end
          } else {
            conflictingSlot.start = toResolve.collisions[0].start
            conflictingSlot.end = originalSlot.start
            // TODO: that id generation seems __very__ weird
            const id = calendarSlot.id * 10
              start: originalSlot.end,
              end: toResolve.collisions[0].end,
              title: conflictingSlot.title,
              className: 'timeslot-partly',
              editable: false,
              extendedProps: {
                id,
                start: originalSlot.end,
                end: toResolve.collisions[0].end,
                title: conflictingSlot.title,
              },
          for (const theirs of toResolve.collisions) {
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
            this.calendarSlots.find((s) => s.id === theirs.id).className = 'timeslot-partly'
          }
          break

        default:
          this.$log.error('EmissionManager.resolveEvent')
          this.$log.error('toResolve:', toResolve)
          this.$log.error('mode:', mode)
          alert('Error: an undefined conflict resolution mode was chosen. See console for details')
          break
      }
    },

    resolveCancel() {
      this.conflictMode = false
    },

    // submit a conflict-resolved schedule to steering
      // TODO: check why steering returns undefined and null values here
      if (this.resolveData.schedule.add_business_days_only === undefined) {
        this.resolveData.schedule.add_business_days_only = false
      }
      if (this.resolveData.schedule.add_days_no === null) {
        this.resolveData.schedule.add_days_no = 0
      }
      if (this.resolveData.schedule.is_repetition === undefined) {
        this.resolveData.schedule.is_repetition = false
      }
      if (this.resolveData.schedule.default_playlist_id === null) {
        this.resolveData.schedule.default_playlist_id = 0
      }
      if (this.resolveData.schedule.automation_id === null) {
        this.resolveData.schedule.automation_id = 0
      }
      if (this.resolveData.schedule.by_weekday === undefined) {
        this.resolveData.schedule.by_weekday = 0
      }

      // create the resolved schedule object including solutions
      let resolvedSchedule = {
        schedule: this.resolveData.schedule,
        solutions: this.resolveData.solutions,
      }
      this.$log.debug('resolveSubmit: schedule:', resolvedSchedule)
      this.submitting = true
      this.serverErrors = []
      try {
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
        await this.$store.dispatch('shows/submitSchedule', {
          showId: this.selectedShow.id,
          schedule: resolvedSchedule,
        })
        this.conflictMode = false
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
        if (e.response?.status === 409) {
          this.enterConflictMode(e.response.data)
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
        } else {
          this.serverErrors = e.errors
        }
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
        this.submitting = false
      for (const timeslot of this.timeslots) {
        const isEmpty = !timeslot.playlist_id
        let emptyText = ''
        if (timeslot.show === this.selectedShow.id) {
          highlighting = 'currentShow '
          highlighting += isEmpty ? 'emptySlot' : ''
          emptyText = isEmpty ? this.$t('calendar.empty') : ''
        }

        this.calendarSlots.push({
          id: timeslot.id,
          start: timeslot.start,
          end: timeslot.end,
          title: this.getShowTitleById(timeslot.show) + `\n${emptyText}`,
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
          className: highlighting,
          extendedProps: {
            id: timeslot.id,
            start: timeslot.start,
            end: timeslot.end,
            title: this.getShowTitleById(timeslot.show) + `\n${emptyText}`,
          },
        })
      }
    },

    loadTimeslots(start, end) {
      this.$store.dispatch('shows/fetchTimeslots', {
        start: start,
        end: end,
        callback: () => {
          this.loadCalendarSlots()
    isMismatchedLength(playlist, timeslot) {
      const timeslotDuration = this.minutesToNanoseconds(
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
        this.prettyDuration(timeslot.start, timeslot.end).minutes,
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
      const playlistDuration = this.hmsToNanoseconds(this.playlistDuration(playlist))
      let delta = 0
Konrad Mohrfeldt's avatar
Konrad Mohrfeldt committed
      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'))
    },
  },
}