Skip to content
Snippets Groups Projects
EmissionManager.vue 25.3 KiB
Newer Older
  • Learn to ignore specific revisions
  •         <template v-if="selectedShow">
                <auth-wall>
                    <show-selector
                            ref="showSelector"
                            :title="$t('navigation.calendar')"
                            :callback="showHasSwitched"
                    />
                    <hr>
    
                    <b-alert
                            variant="danger"
                            :show="conflictMode"
    
                        <div
                                v-if="conflictMode"
                                align="center"
                        >
                            <h4>Conflict Resolution</h4>
                            <p>
                                for new schedule
                                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>
    
    
                            <div v-if="submitting">
                                <b-row>
                                    <b-col align="center">
                                        <img
                                                src="/assets/radio.gif"
                                                alt="submitting resolve data"
                                        >
                                    </b-col>
                                </b-row>
                            </div>
                            <div v-else>
                                <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>
                                </p>
                            </div>
    
                        </div>
    
                    </b-alert>
    
                    <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"
                    />
                </auth-wall>
    
                <app-modalEmissionManagerCreate
                        ref="appModalEmissionManagerCreate"
                />
                <app-modalEmissionManagerResolve
                        ref="appModalEmissionManagerResolve"
                />
                <app-modalEmissionManagerEdit
                        ref="appModalEmissionManagerEdit"
    
            <div
                    v-else
                    class="tw-text-center"
                    v-html="this.$t('no_assigned_shows', { admin_url: adminUrl })"
    
        import {mapGetters} from 'vuex'
        import {FullCalendar} from 'vue-full-calendar'
        import 'fullcalendar/dist/fullcalendar.css'
    
        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'
    
        export default {
            components: {
                FullCalendar,
                AuthWall,
                'show-selector': showSelector,
                'app-modalEmissionManagerCreate': modalEmissionManagerCreate,
                'app-modalEmissionManagerResolve': modalEmissionManagerResolve,
                'app-modalEmissionManagerEdit': modalEmissionManagerEdit,
    
    
            mixins: [rrules],
    
            data() {
                return {
                    calendarSlots: [],
    
                    // 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: [],
    
                    // 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,
                                editable: 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);
                        },
                    },
    
            },
    
            computed: {
                loaded() {
                    return {
                        shows: this.$store.state.shows.loaded['shows'],
                        timeslots: this.$store.state.shows.loaded['timeslots'],
                    }
                },
    
                ...mapGetters({
                    shows: 'shows/shows',
                    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()
                        })
    
    
            methods: {
                switchShow() {
                    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.$log.debug('show has switched to', this.selectedShow.name)
                    let start = this.$refs.calendar.fireMethod('getView').start.format()
                    let end = this.$refs.calendar.fireMethod('getView').end.format()
                    this.loadTimeslots(start, end)
                },
    
                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.selectedShow.id) {
                            this.switchShow(this.getShowIndexById(timeslot.show))
                        } else {
                            this.$refs.appModalEmissionManagerEdit.open(timeslot)
                        }
                    }
                },
    
                // 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) {
                    if (!this.conflictMode) {
                        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.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)
                    }
                },
    
                resolveEvent(toResolve, mode) {
                    let calendarSlot = this.calendarSlots.find(s => s.id === toResolve.id)
                    let 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
                    let 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
                    let 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
    
                    // for a detailed description of the resolution modes, see conflict-resolution.md
                    // and conflict-resolution.pdf at https://gitlab.servus.at/autoradio/meta
                    switch (mode) {
                        case 'theirs':
                            this.conflictSolutions[toResolve.hash] = mode
                            calendarSlot.className = 'timeslot-discarded'
                            calendarSlot.title = 'new'
                            calendarSlot.start = originalSlot.start
                            calendarSlot.end = originalSlot.end
                            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
                            calendarSlot.className = 'timeslot-accepted'
                            calendarSlot.title = 'new'
                            calendarSlot.start = originalSlot.start
                            calendarSlot.end = originalSlot.end
                            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':
                            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
                                this.calendarSlots.push({
                                    id: calendarSlot.id * 10,
                                    start: toResolve.collisions[0].end,
                                    end: originalSlot.end,
                                    title: 'new [theirs-both]',
                                    className: 'timeslot-partly',
                                    editable: false,
                                })
                            }
                            for (let theirs of toResolve.collisions) {
                                this.calendarSlots.find(s => s.id === theirs.id).className = 'timeslot-accepted'
                            }
                            this.renderView(null)
                            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
                                this.calendarSlots.push({
                                    id: calendarSlot.id * 10,
                                    start: originalSlot.end,
                                    end: toResolve.collisions[0].end,
                                    title: conflictingSlot.title,
                                    className: 'timeslot-partly',
                                    editable: false,
                                })
                            }
                            for (let theirs of toResolve.collisions) {
                                this.calendarSlots.find(s => s.id === theirs.id).className = 'timeslot-partly'
                            }
                            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)
                    this.submitting = true
                    this.$store.dispatch('shows/submitSchedule', {
                        showId: this.selectedShow.id,
                        schedule: resolvedSchedule,
                        callback: (response) => {
                            this.submitting = false
                            if (response.data.projected === undefined) {
                                this.conflictMode = false
                                this.renderView(null)
                            } else {
                                this.resolve(response.data)
                            }
                        },
                        callbackCancel: () => {
                            this.submitting = false
                        }
                    })
                },
    
                loadCalendarSlots() {
                    this.calendarSlots = []
                    for (let i in this.timeslots) {
                        let highlighting = 'otherShow'
                        if (this.timeslots[i].show === this.selectedShow.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
                        })
                    }
                },
    
                loadTimeslots(start, end) {
                    this.$store.dispatch('shows/fetchTimeslots', {
                        start: start,
                        end: end,
                        callback: () => {
                            this.loadCalendarSlots()
                        }
                    })
                },
    
                notYetImplemented: function () {
                    alert('By the mighty witchcraftry of the mother of time!\n\nThis feature is not implemented yet.')
                },
            },
        }
    
        .otherShow {
            background-color: #eee;
        }
    
        a.currentShow {
            background-color: #17a2b8;
        }
    
        .conflict {
            background-color: #b00;
        }
    
        .noconflict {
            background-color: #17a2b8;
        }
    
        .timeslot-discarded {
            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;
        }