Commit 0e4d90bb authored by jackie / Andrea Ida Malkah Klaura's avatar jackie / Andrea Ida Malkah Klaura
Browse files

Merge branch 'feature-emissionmanager' into develop

parents e2d23fb2 4ce06afa
......@@ -34,21 +34,36 @@
variant="danger"
:show="conflictMode"
>
<b-row>
<b-col cols="12">
<div align="center">
<h4>Conflict Resolution</h4>
</div>
</b-col>
<b-col>
... coming soon ...
</b-col>
</b-row>
<div
v-if="conflictMode"
align="center"
>
<h4>Conflict Resolution</h4>
<p>for new schedule</p>
<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 to resolve: {{ conflictCount }}
</p>
<p v-else>
<b-button
variant="success"
@click="resolveSubmit"
>
0 conflicts left! Submit this solution.
</b-button>
</p>
</div>
</b-alert>
<full-calendar
ref="calendar"
editable="false"
default-view="agendaWeek"
:events="calendarSlots"
:config="calendarConfig"
......@@ -62,6 +77,12 @@
<app-modalEmissionManagerCreate
ref="appModalEmissionManagerCreate"
/>
<app-modalEmissionManagerResolve
ref="appModalEmissionManagerResolve"
/>
<app-modalEmissionManagerEdit
ref="appModalEmissionManagerEdit"
/>
</b-container>
</template>
......@@ -70,13 +91,20 @@ 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,
......@@ -93,6 +121,11 @@ export default {
// 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
......@@ -110,6 +143,7 @@ export default {
timeFormat: 'k:mm',
slotLabelFormat: 'k:mm',
allDaySlot: false,
editable: false,
},
},
// here we add a simple tooltip to every event, so that the full title
......@@ -145,18 +179,47 @@ export default {
}
},
eventSelected (event, jsEvent, view) {
this.$log.debug('eventSelected', event, jsEvent, view)
// 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) { return }
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)
},
......@@ -176,11 +239,8 @@ export default {
start = view.start.format()
end = view.end.format()
}
// if we are in conflict resolution mode we do not load all timeslots
// but only the conflicting ones
if (this.conflictMode) {
this.loadConflictSlots(start, end)
} else {
// we only load new timeslots, if we are not in conflict mode
if (!this.conflictMode) {
this.loadTimeslots(start, end)
}
}
......@@ -188,7 +248,108 @@ export default {
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)
}
},
resolveEvent (toResolve, mode) {
this.conflictCount -= toResolve.collisions.length
let slotIndex = this.calendarSlots.findIndex(s => s.id === toResolve.id)
switch (mode) {
case 'theirs':
this.conflictSolutions[toResolve.hash] = 'theirs'
this.calendarSlots[slotIndex].className = 'ours-discarded'
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
}
},
// 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 () {
......@@ -200,6 +361,7 @@ export default {
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),
......@@ -246,9 +408,9 @@ export default {
})
},
updateSchedules () {
this.$log.debug(this.$refs.calendar.fireMethod('getView').start.format())
}
notYetImplemented: function () {
alert('By the mighty witchcraftry of the mother of time!\n\nThis feature is not implemented yet.')
},
},
}
</script>
......@@ -260,4 +422,15 @@ export default {
a.currentShow {
background-color: #17a2b8;
}
.conflict {
background-color: #b00;
}
.noconflict {
background-color: #17a2b8;
}
.ours-discarded {
background-color: #b00;
opacity: 0.5;
text-decoration: line-through !important;
}
</style>
......@@ -81,9 +81,10 @@
<script>
import axios from 'axios'
import prettyDate from '../mixins/prettyDate'
import rrules from '../mixins/rrules'
export default {
mixins: [ prettyDate ],
mixins: [ prettyDate, rrules ],
data () {
return {
......@@ -97,21 +98,6 @@ export default {
until: null,
rrule: 1
},
rruleOptions: [
{ value: 1, text: 'einmalig' },
{ value: 2, text: 'täglich' },
{ value: 3, text: 'werktäglich' },
{ value: 4, text: 'wöchentlich' },
{ value: 5, text: 'zweiwöchentlich' },
{ value: 6, text: 'vierwöchentlich' },
{ value: 7, text: 'gerade Kalenderwoche' },
{ value: 8, text: 'ungerade Kalenderwoche' },
{ value: 9, text: 'Jede 1. Woche im Monat' },
{ value: 10, text: 'Jede 2. Woche im Monat' },
{ value: 11, text: 'Jede 3. Woche im Monat' },
{ value: 12, text: 'Jede 4. Woche im Monat' },
{ value: 13, text: 'Jede 5. Woche im Monat' },
],
}
},
......
<template>
<div>
<b-modal
ref="modalEmissionManagerEdit"
title="Edit a schedule"
size="lg"
>
<p>
Editing a timeslot/schedule for show:
<b v-if="loaded.modal">
<b>{{ show.name }}</b>
</b>
</p>
<p v-if="loaded.modal">
This timeslot starts at
<b-badge variant="info">
{{ prettyDateTime(timeslot.start) }}
</b-badge>
and ends at
<b-badge variant="info">
{{ prettyDateTime(timeslot.end) }}
</b-badge>
</p>
<div v-if="loaded.schedule">
<div v-if="schedule.rrule === 1">
<p>This is a single emission. No other timeslots in this schedule.</p>
</div>
<div v-else>
<p>This is a recurring event: <b>{{ rruleRender(schedule.rrule) }}</b>, until: {{ prettyDate(schedule.until) }}</p>
<p>All <i>upcoming</i> timeslots of this schedule:</p>
<ul v-if="loaded.scheduleTimeslots">
<li
v-for="slot in scheduleTimeslots"
:key="slot.id"
>
from
<b-badge :variant="timeslot.id === slot.id ? 'info' : 'light'">
{{ prettyDateTime(slot.start) }}
</b-badge>
to
<b-badge :variant="timeslot.id === slot.id ? 'info' : 'light'">
{{ prettyDateTime(slot.end) }}
</b-badge>
</li>
</ul>
</div>
<p>What do you want to do with it?</p>
<div align="center">
<b-button-group>
<b-button
variant="danger"
size="sm"
@click="deleteFullSchedule(schedule.id)"
>
<span v-if="schedule.rrule === 1">Delete</span>
<span v-else>Delete schedule + all timeslots</span>
</b-button>
<b-button
v-if="schedule.rrule > 1"
variant="danger"
size="sm"
@click="notYetImplemented()"
>
Delete only this timeslot
</b-button>
<b-button
v-if="schedule.rrule > 1"
variant="danger"
size="sm"
@click="notYetImplemented()"
>
Delete this + all future timeslots
</b-button>
</b-button-group>
</div>
</div>
<div v-else>
<img
src="../assets/radio.gif"
alt="loading schedule data"
>
</div>
</b-modal>
</div>
</template>
<script>
import axios from 'axios'
import prettyDate from '../mixins/prettyDate'
import rrules from '../mixins/rrules'
export default {
mixins: [ prettyDate, rrules ],
data () {
return {
timeslot: null,
schedule: null,
scheduleTimeslots: null,
show: null,
loaded: {
modal: false,
schedule: false,
scheduleTimeslots: false,
}
}
},
methods: {
deleteFullSchedule (id) {
let uri = process.env.VUE_APP_API_STEERING + 'shows/' + this.show.id + '/schedules/' + id + '/'
axios.delete(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.$parent.user.access_token }
}).then(() => {
this.$refs.modalEmissionManagerEdit.hide()
this.$parent.renderView(null)
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not delete full schedule. See console for details.')
})
},
loadSchedule (id) {
this.loaded.schedule = false
let uri = process.env.VUE_APP_API_STEERING + 'shows/' + this.show.id + '/schedules/' + id + '/'
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.$parent.user.access_token }
}).then(response => {
this.schedule = response.data
this.loaded.schedule = true
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not load schedule. See console for details.')
})
},
loadScheduleTimeslots (id) {
this.loaded.scheduleTimeslots = false
let uri = process.env.VUE_APP_API_STEERING + 'shows/' + this.show.id + '/schedules/' + id + '/timeslots/'
axios.get(uri, {
withCredentials: true,
headers: { 'Authorization': 'Bearer ' + this.$parent.$parent.user.access_token }
}).then(response => {
this.scheduleTimeslots = response.data
this.loaded.scheduleTimeslots = true
}).catch(error => {
this.$log.error(error.response.status + ' ' + error.response.statusText)
this.$log.error(error.response)
alert('Error: could not load timeslots of this schedule. See console for details.')
})
},
// initialise a new schedule and open the modal
open (timeslot, show) {
this.timeslot = timeslot
this.show = show
this.loaded.modal = true
this.$refs.modalEmissionManagerEdit.show()
this.loadSchedule(timeslot.schedule)
this.loadScheduleTimeslots(timeslot.schedule)
},
notYetImplemented: function () {
alert('By the mighty witchcraftry of the mother of time!\n\nThis feature is not implemented yet.')
},
}
}
</script>
<style scoped>
</style>
<template>
<div>
<b-modal
ref="modalEmissionManagerResolve"
title="Resolve a timeslot conflict"
size="lg"
@ok="resolve"
>
<p>
Resolving a conflict for a new schedule of the show:
<b v-if="$parent.loaded.shows">
<b>{{ $parent.shows[$parent.currentShow].name }}</b>
</b>
</p>
<p v-if="loaded">
The new projected slot starts at
<b-badge variant="danger">
{{ toResolve.start.format('YYYY-DD-MM HH:mm') }}
</b-badge>
and ends at
<b-badge variant="danger">
{{ toResolve.end.format('YYYY-DD-MM HH:mm') }}
</b-badge>
.
</p>
<p>
It conflicts with the following timeslots:
</p>
<ul v-if="loaded">
<li
v-for="col in toResolve.collisions"
:key="col.id"
>
<i>{{ col.show_name }}</i> from
<b-badge variant="success">
{{ col.start.slice(0,16) }}
</b-badge>
to
<b-badge variant="success">
{{ col.end.slice(0,16) }}
</b-badge>
</li>
</ul>
<p>
What should we do?
</p>
<div align="center">
<b-button-group v-if="loaded">
<b-button
v-if="toResolve.solutionChoices.indexOf('ours') >= 0"
variant="danger"
size="sm"
@click="resolve('ours')"
>
Create new,<br>
delete existing.
</b-button>
<b-button
v-if="toResolve.solutionChoices.indexOf('theirs') >= 0"
variant="success"
size="sm"
@click="resolve('theirs')"
>
Discard new,<br>
keep existing.
</b-button>
<b-button
v-if="toResolve.solutionChoices.indexOf('theirs-start') >= 0"
variant="info"
size="sm"
@click="notYetImplemented"
>
theirs-start<br>
TODO: describe
</b-button>
<b-button