Skip to content
Snippets Groups Projects
EmissionManager.vue 28.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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'],
    
            .filter(
              (timeslot) =>
                getISODateString(new Date(timeslot.start)) === getISODateString(this.selectedDay),
            )
    
    Konrad Mohrfeldt's avatar
    Konrad Mohrfeldt committed
            .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))
    
          const 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) {
    
          const 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 (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':
              this.conflictSolutions[toResolve.hash] = mode
              calendarSlot.className = 'timeslot-accepted'
    
              calendarSlot.title = this.$t('conflictResolution.conflictingSlot')
    
              calendarSlot.start = originalSlot.start
              calendarSlot.end = originalSlot.end
    
              for (const 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
    
          const 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'))
        },
      },
    }