Newer
Older
<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>
<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>
<p v-if="conflictCount > 0">
Conflicts left to resolve:
<b-badge variant="danger">
{{ conflictCount }}
</b-badge>
<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>
<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"
/>
</b-container>
</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,
mixins: [ rrules ],
data () {
return {
currentShow: 0,
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()
},
methods: {
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
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
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) {
this.$log.debug('resolveEvent:', toResolve)
this.conflictCount -= toResolve.collisions.length
let slotIndex = this.calendarSlots.findIndex(s => s.id === toResolve.id)
switch (mode) {
case 'theirs':
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
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'
}
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)
},
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
// 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 }
}).then(response => {
this.timeslots = response.data
this.loaded.timeslots = true
this.loadCalendarSlots()
}).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;
}
.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;
}
.timeslot-partly-theirs {
background-color: #17a2b8;
opacity: 0.25;
}