Skip to content
Snippets Groups Projects
EmissionManager.vue 17.1 KiB
Newer Older
  • Learn to ignore specific revisions
  •   <b-container>
        <b-row v-if="loaded.shows">
          <b-col>
    
            <b-alert
              :show="showChangedAlert.countdown"
              @dismiss-count-down="showChangedAlertCountdown"
            >
              Selected new show: <b>{{ shows[currentShow].name }}</b>
              <b-progress
                variant="info"
                animated
                :max="showChangedAlert.seconds"
                :value="showChangedAlert.countdown"
                height="3px"
              />
            </b-alert>
            <b-alert
              :show="!showChangedAlert.countdown"
              variant="light"
            >
              Currently selected show: <b>{{ shows[currentShow].name }}</b>
            </b-alert>
    
          </b-col>
          <b-col align="right">
            <b-dropdown
              id="ddshows"
              text="Sendereihe auswählen"
              variant="outline-info"
            >
              <b-dropdown-item
                v-for="(show, index) in shows"
                :key="show.id"
                @click="switchShow(index)"
              >
                {{ show.name }}
              </b-dropdown-item>
            </b-dropdown>
          </b-col>
        </b-row>
        <b-row v-else>
          <b-col cols="12">
            <div align="center">
              ... loading show data ...
            </div>
          </b-col>
        </b-row>
    
        <b-alert
          variant="danger"
          :show="conflictMode"
        >
    
          <div
            v-if="conflictMode"
            align="center"
          >
            <h4>Conflict Resolution</h4>
            <p>
    
              from <b>{{ resolveData.schedule.dstart }}, {{ resolveData.schedule.tstart }}</b>
              to <b>{{ resolveData.schedule.tend }}</b>.
            </p>
            <p v-if="resolveData.schedule.rrule !== 1">
              This is a recurring event: <b>{{ rruleRender(resolveData.schedule.rrule) }}</b>,
              until: <b>{{ resolveData.schedule.until }}</b>.
            </p>
            <p v-if="conflictCount > 0">
    
              Conflicts left to resolve:
              <b-badge variant="danger">
                {{ conflictCount }}
              </b-badge>
              &nbsp;
              <b-button
                variant="danger"
                size="sm"
                @click="resolveCancel"
              >
                Cancel
              </b-button>
    
            </p>
            <p v-else>
              <b-button
                variant="success"
                @click="resolveSubmit"
              >
                0 conflicts left! Submit this solution.
              </b-button>
    
              &nbsp;
              <b-button
                variant="danger"
                @click="resolveCancel"
              >
                Cancel
              </b-button>
    
        <full-calendar
          ref="calendar"
          default-view="agendaWeek"
          :events="calendarSlots"
          :config="calendarConfig"
          @view-render="renderView"
    
          @event-selected="eventSelected"
          @event-drop="eventDrop"
          @event-resize="eventResize"
          @event-created="eventCreated"
        />
    
        <app-modalEmissionManagerCreate
          ref="appModalEmissionManagerCreate"
    
        <app-modalEmissionManagerResolve
          ref="appModalEmissionManagerResolve"
        />
    
        <app-modalEmissionManagerEdit
          ref="appModalEmissionManagerEdit"
        />
    
    </template>
    
    <script>
    import axios from 'axios'
    
    import { FullCalendar } from 'vue-full-calendar'
    
    import 'fullcalendar/dist/fullcalendar.css'
    
    import modalEmissionManagerCreate from './EmissionManagerModalCreate.vue'
    
    import modalEmissionManagerResolve from './EmissionManagerModalResolve.vue'
    
    import modalEmissionManagerEdit from './EmissionManagerModalEdit.vue'
    
    import rrules from '../mixins/rrules'
    
    
    export default {
      components: {
        FullCalendar,
    
        'app-modalEmissionManagerCreate': modalEmissionManagerCreate,
    
        'app-modalEmissionManagerResolve': modalEmissionManagerResolve,
    
        'app-modalEmissionManagerEdit': modalEmissionManagerEdit,
    
          shows: [],
          timeslots: [],
          calendarSlots: [],
    
          showChangedAlert: {
            countdown: 0,
            seconds: 3
          },
    
    
          // flags for loading data
          loaded: {
            shows: false,
            timeslots: false,
            calendarSlots: 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: [],
    
          // this is the whole configuration for our schedule calendar, including
    
          // simple event handlers that do not need the whole components scope
    
          calendarConfig: {
            height: 600,
            firstDay: 1,
            header: {
              left: 'title',
              center: '',
              right: 'today prev,next'
            },
            views: {
              agendaWeek: {
                columnHeaderFormat: 'ddd D.M.',
                timeFormat: 'k:mm',
                slotLabelFormat: 'k:mm',
                allDaySlot: false,
    
            // here we add a simple tooltip to every event, so that the full title
            // of a show can be viewed
            eventRender: function(event, element) {
              element.attr('title', event.title);
            },
    
      created () {
        if (this.$route.query.show) {
          this.currentShow = this.$route.query.show
        } else {
          this.currentShow = 0
        }
        this.loadShows()
    
        switchShow (index) {
          this.currentShow = index
          this.loadCalendarSlots()
    
          this.showChangedAlert.countdown = this.showChangedAlert.seconds
        },
    
        showChangedAlertCountdown(countdown) {
          this.showChangedAlert.countdown = countdown
    
        },
    
        getShowTitleById (id) {
          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) {
          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
          }
        },
    
    
        // 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 opend, otherwise the resolution modal
    
          if (this.conflictMode) {
            if (event.hash === undefined) {
              return
            } else if (this.conflictSolutions[event.hash] === undefined) {
              this.$refs.appModalEmissionManagerResolve.openNotNeeded()
            } else {
              this.$refs.appModalEmissionManagerResolve.open(event)
            }
          }
    
          // standard mode only those events are clickable that belong to the
          // currently selected show.
          else {
            let timeslot = this.timeslots.find(slot => slot.id === event.id)
    
            if (timeslot.show !== this.shows[this.currentShow].id) {
              this.switchShow(this.getShowIndexById(timeslot.show))
            } else {
              this.$refs.appModalEmissionManagerEdit.open(timeslot, this.shows[this.currentShow])
            }
    
        // currently this will not be called, as our events are not editable
        // if editable is set to true in the calendar config, this handler will
        // be called if a timeslot was dragged somewhere else
    
        eventDrop (event) {
          this.$log.debug('eventDrop', event)
    
          this.notYetImplemented()
    
        // currently this will not be called, as our events are not editable
        // if editable is set to true in the calendar config, this handler will
        // be called if a timeslot was resized
    
        eventResize (event) {
          this.$log.debug('eventResize', event)
    
          this.notYetImplemented()
    
        // this handler is called when the user creates a new timeslot
    
        eventCreated (event) {
          this.$refs.appModalEmissionManagerCreate.open(event.start, event.end)
        },
    
    
        // this is called when the user changes the calendar view, so we just
        // refetch the timeslots with the updated visible date range
        renderView (view) {
          if (this.loaded.shows) {
    
            let start = null
            let end = null
            // in case it gets called from a modal, we use the current view
            // otherwise we use the new dates from the view received by the renderView event
            if (view === null) {
              start = this.$refs.calendar.fireMethod('getView').start.format()
              end = this.$refs.calendar.fireMethod('getView').end.format()
            } else {
              start = view.start.format()
              end = view.end.format()
            }
    
            // we only load new timeslots, if we are not in conflict mode
            if (!this.conflictMode) {
    
              this.loadTimeslots(start, end)
            }
    
        resolve (data) {
          this.$log.debug('resolve', data)
    
          this.resolveData = data
    
          this.conflictMode = true
    
          this.conflictCount = 0
          this.conflictSolutions = data.solutions
          this.calendarSlots = []
          try {
            for (let i in data.projected) {
              let newSlot = {
                // 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
                id: Number(data.projected[i].hash),
                // the hash is needed to compare against solutions and conflicts
                hash: data.projected[i].hash,
                start: data.projected[i].start,
                end: data.projected[i].end,
                title: 'new',
                collisions: [],
                solutionChoices: [],
                className: 'noconflict',
                editable: false,
              }
              if (data.projected[i].collisions.length > 0) {
                newSlot.className = 'conflict'
                newSlot.solutionChoices = data.projected[i].solution_choices
                for (let col of data.projected[i].collisions) {
                  let conflictingSlot = {
                    id: col.id,
                    start: col.start,
                    end: col.end,
                    title: col.show_name,
                    className: 'otherShow',
                    editable: false,
                  }
                  this.calendarSlots.push(conflictingSlot)
                  this.conflictCount++
                  newSlot.collisions.push(col)
                }
              }
              this.calendarSlots.push(newSlot)
            }
          } catch (err) {
            this.$log.error(err)
          }
    
          this.$log.debug('resolveEvent:', toResolve)
    
          this.conflictCount -= toResolve.collisions.length
    
          let slotIndex = this.calendarSlots.findIndex(s => s.id === toResolve.id)
    
              this.conflictSolutions[toResolve.hash] = mode
              this.calendarSlots[slotIndex].className = 'timeslot-discarded'
              for (let theirs of toResolve.collisions) {
                this.calendarSlots.find(s => s.id === theirs.id).className = 'timeslot-accepted'
              }
              this.renderView(null)
              break
    
            case 'ours':
              this.conflictSolutions[toResolve.hash] = mode
              this.calendarSlots[slotIndex].className = 'timeslot-accepted'
              for (let theirs of toResolve.collisions) {
                this.calendarSlots.find(s => s.id === theirs.id).className = 'timeslot-discarded'
              }
              this.renderView(null)
              break
    
            case 'theirs-start':
            case 'theirs-end':
            case 'theirs-both':
            case 'ours-start':
            case 'ours-end':
            case 'ours-both':
              this.conflictSolutions[toResolve.hash] = mode
              this.calendarSlots[slotIndex].className = 'timeslot-partly'
              this.calendarSlots[slotIndex].title = 'new [' + mode + ']'
              for (let theirs of toResolve.collisions) {
                this.calendarSlots.find(s => s.id === theirs.id).className = 'timeslot-partly-theirs'
              }
    
              this.renderView(null)
    
              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
          this.renderView(null)
        },
    
    
        // submit a conflict-resolved schedule to steering
        resolveSubmit () {
          // TODO: check why steering retourns 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.fallback_id === null) { this.resolveData.schedule.fallback_id = 0 }
          if (this.resolveData.schedule.automation_id === null) { this.resolveData.schedule.automation_id = 0 }
          if (this.resolveData.schedule.byweekday === undefined) { this.resolveData.schedule.byweekday = 0 }
          // create the resolved schedule object including solutions
          let resolvedSchedule = {
            schedule: this.resolveData.schedule,
            solutions: this.resolveData.solutions,
          }
          this.$log.debug('resolveSubmit: schedule:', resolvedSchedule)
          // now generate the URL and POST it to steering
          let uri = process.env.VUE_APP_API_STEERING_SHOWS + this.shows[this.currentShow].id + '/schedules/'
          axios.post(uri, resolvedSchedule, {
            withCredentials: true,
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
          }).then(response => {
            this.$log.debug('resolveSubmit: response:', response)
            // if for some reason a new conflict arose, e.g. because in the meantime
            // someone else inserted a conflicting schedule, we have to resolve.
            if (response.data.projected === undefined) {
              this.conflictMode = false
              this.renderView(null)
            } else {
              this.resolve(response.data)
            }
          }).catch(error => {
            this.$log.error(error.response.status + ' ' + error.response.statusText)
            this.$log.error(error.response)
            alert('Error: could not submit final schedule. See console for details.')
            // and we leave the modal open, so no call to its .hide function here
          })
        },
    
    
        loadCalendarSlots () {
          this.loaded.calendarSlots = false
          this.calendarSlots = []
          for (let i in this.timeslots) {
            let highlighting = 'otherShow'
            if (this.timeslots[i].show === this.shows[this.currentShow].id) {
              highlighting = 'currentShow'
            }
            this.calendarSlots.push({
    
              id: this.timeslots[i].id,
    
              start: this.timeslots[i].start,
              end: this.timeslots[i].end,
              title: this.getShowTitleById(this.timeslots[i].show),
              className: highlighting
            })
          }
          this.loaded.calendarSlots = true
        },
    
    
        loadTimeslots (start, end) {
          this.loaded.timeslots = false
          let uri = process.env.VUE_APP_API_STEERING + 'timeslots?start=' + start + '&end=' + end
          axios.get(uri, {
            withCredentials: true,
    
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
    
            this.loaded.timeslots = true
    
          }).catch(error => {
            this.$log.error(error.response.status + ' ' + error.response.statusText)
            this.$log.error(error.response)
            alert('Error: could not load timeslots. See console for details.')
          })
        },
    
        loadShows () {
          this.loaded.shows = false
          let uri = process.env.VUE_APP_API_STEERING + 'shows'
          axios.get(uri, {
            withCredentials: true,
    
            headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
    
          }).then(response => {
            this.shows = response.data
            this.loaded.shows = true
            let start = this.$refs.calendar.fireMethod('getView').start.format()
            let end = this.$refs.calendar.fireMethod('getView').end.format()
            this.loadTimeslots(start, end)
          }).catch(error => {
            this.$log.error(error.response.status + ' ' + error.response.statusText)
            this.$log.error(error.response)
            alert('Error: could not load shows. See console for details.')
          })
        },
    
    
        notYetImplemented: function () {
          alert('By the mighty witchcraftry of the mother of time!\n\nThis feature is not implemented yet.')
        },
    
      },
    }
    </script>
    
    <style>
    .otherShow {
      background-color: #eee;
    }
    a.currentShow {
      background-color: #17a2b8;
    }
    
    .conflict {
      background-color: #b00;
    }
    .noconflict {
      background-color: #17a2b8;
    }
    
      background-color: #b00;
      opacity: 0.5;
      text-decoration: line-through !important;
    
    .timeslot-accepted {
      background-color: #17a2b8;
    }
    .timeslot-partly {
      background-color: #17a2b8;
      opacity: 0.5;
      font-weight: bold;
    }
    .timeslot-partly-theirs {
      background-color: #17a2b8;
      opacity: 0.25;
    }