Commit de06f230 authored by Ernesto Rico Schmidt's avatar Ernesto Rico Schmidt
Browse files

Merge branch 'kmohrf/93-steering-scheduling' into 'master'

Fix error code handling for scheduling API

Closes #93

See merge request !11
parents c6a960b4 e01a3692
Pipeline #1873 passed with stages
in 6 minutes and 31 seconds
......@@ -26,9 +26,9 @@
}"
v-html="$t('conflictResolution.newSchedule', {
dstart: prettyDate(resolveData.schedule.dstart),
tstart: resolveData.schedule.tstart,
tend: resolveData.schedule.tend,
firstDate: prettyDate(resolveData.schedule.first_date),
startTime: resolveData.schedule.start_time,
endTime: resolveData.schedule.end_time,
})"
/>
......@@ -36,7 +36,7 @@
v-if="resolveData.schedule.rrule !== 1"
v-html="$t('conflictResolution.recurringSchedule', {
rrule: rruleRender(resolveData.schedule.rrule),
until: prettyDate(resolveData.schedule.until)
lastDate: prettyDate(resolveData.schedule.last_date)
})"
/>
......@@ -82,6 +82,8 @@
</div>
</b-alert>
<server-errors :errors="serverErrors" />
<div class="tw-flex">
<div :class="{ 'tw-hidden': view !== 'week'}">
<full-calendar
......@@ -239,9 +241,11 @@ 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'
export default {
components: {
ServerErrors,
FullCalendar,
AuthWall,
'show-selector': showSelector,
......@@ -270,6 +274,7 @@ export default {
resolveData: null,
conflictCount: 0,
conflictSolutions: [],
serverErrors: [],
}
},
......@@ -696,7 +701,7 @@ export default {
},
// submit a conflict-resolved schedule to steering
resolveSubmit() {
async resolveSubmit() {
// 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
......@@ -713,8 +718,8 @@ export default {
if (this.resolveData.schedule.automation_id === null) {
this.resolveData.schedule.automation_id = 0
}
if (this.resolveData.schedule.byweekday === undefined) {
this.resolveData.schedule.byweekday = 0
if (this.resolveData.schedule.by_weekday === undefined) {
this.resolveData.schedule.by_weekday = 0
}
// create the resolved schedule object including solutions
......@@ -725,22 +730,23 @@ export default {
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)
this.serverErrors = []
try {
await this.$store.dispatch('shows/submitSchedule', {
showId: this.selectedShow.id,
schedule: resolvedSchedule,
})
this.conflictMode = false
this.renderView(null)
} catch (e) {
if (e.response?.status === 409) {
this.resolve(e.response.data)
} else {
this.resolve(response.data)
this.serverErrors = e.errors
}
},
callbackCancel: () => {
} finally {
this.submitting = false
}
})
}
},
loadCalendarSlots() {
......
<template>
<div v-if="errors.length > 0">
<b-alert
v-for="error in translatedServerErrors"
:key="error.code"
variant="danger"
:show="true"
>
{{ error.message }}
</b-alert>
</div>
</template>
<script>
export default {
props: {
errors: {
type: Array,
required: true,
},
translationBase: {
type: String,
default: 'steeringErrorCodes'
}
},
computed: {
translatedServerErrors () {
return this.errors.map(({ code, message }) => {
const translationKey = `${this.translationBase}.${code}`
return {
code,
message: this.$te(translationKey) ? this.$t(translationKey) : message
}
})
}
}
}
</script>
......@@ -14,6 +14,8 @@
<span v-html="$t('scheduleEditor.pastEventWarning')" />
</b-alert>
<server-errors :errors="serverErrors" />
<div v-if="loaded.modal && !submitting">
<b-row>
<b-col cols="12">
......@@ -32,7 +34,7 @@
<label class="tw-w-full tw-font-bold">
<span class="tw-inline-block tw-pb-2">{{ $t('scheduleEditor.fromDate' ) }}</span>
<b-form-input
v-model="valuePick.dstart"
v-model="valuePick.firstDate"
type="date"
/>
</label>
......@@ -47,7 +49,7 @@
v-html="$t('scheduleEditor.toDate')"
/>
<b-form-input
v-model="valuePick.until"
v-model="valuePick.lastDate"
type="date"
/>
</label>
......@@ -56,7 +58,7 @@
<label class="tw-w-full tw-font-bold">
<span class="tw-inline-block tw-pb-2">{{ $t('scheduleEditor.from') }}</span>
<b-form-input
v-model="valuePick.tstart"
v-model="valuePick.startTime"
type="time"
/>
</label>
......@@ -65,7 +67,7 @@
<label class="tw-w-full tw-font-bold">
<span class="tw-inline-block tw-pb-2">{{ $t('scheduleEditor.to') }}</span>
<b-form-input
v-model="valuePick.tend"
v-model="valuePick.endTime"
type="time"
/>
</label>
......@@ -108,8 +110,10 @@
import {mapGetters} from 'vuex'
import prettyDate from '../../mixins/prettyDate'
import rrules from '../../mixins/rrules'
import ServerErrors from '@/components/ServerErrors'
export default {
components: {ServerErrors},
mixins: [prettyDate, rrules],
data() {
......@@ -118,12 +122,13 @@
loadedModal: false,
submitting: false,
pastEventWarning: false,
serverErrors: [],
initialLastDate: null,
valuePick: {
dstart: null,
tstart: null,
dend: null,
tend: null,
until: null,
firstDate: null,
startTime: null,
endTime: null,
lastDate: null,
rrule: 1
},
}
......@@ -138,10 +143,11 @@
},
projectedTimeslotDuration() {
const {dstart, tstart, dend, tend} = this.valuePick
const {firstDate, startTime, endTime} = this.valuePick
const d1 = new Date(`${firstDate} ${startTime}`)
const d2 = new Date(`${this.initialLastDate} ${endTime}`)
const d1 = new Date(`${dstart} ${tstart}`)
const d2 = new Date(`${dend} ${tend}`)
const diffInNs = (d2 - d1) * 1000 * 1000
const diffInMinutes = this.nanosecondsToMinutes(diffInNs)
......@@ -155,15 +161,15 @@
},
methods: {
create(event) {
async create(event) {
// prevent the modal from closing automatically on click
event.preventDefault()
// check for past dates; as past dates will be ignored by steering we
// want to make the user aware - otherwise it might get confusing in
// conflict resolution
let now = this.apiDate(new Date())
if (this.valuePick.dstart < now) {
this.valuePick.dstart = now
if (this.valuePick.firstDate < now) {
this.valuePick.firstDate = now
this.pastEventWarning = true
return
} else {
......@@ -171,97 +177,63 @@
}
// take all values that have been picked and put them into our new
// schedule. so far we do not need any transformations.
this.newSchedule.schedule.dstart = this.valuePick.dstart
this.newSchedule.schedule.tstart = this.valuePick.tstart
this.newSchedule.schedule.tend = this.valuePick.tend
this.newSchedule.schedule.until = this.valuePick.until
this.newSchedule.schedule.first_date = this.valuePick.firstDate
this.newSchedule.schedule.start_time = this.valuePick.startTime
this.newSchedule.schedule.end_time = this.valuePick.endTime
this.newSchedule.schedule.last_date = this.valuePick.lastDate
this.newSchedule.schedule.rrule = this.valuePick.rrule
this.newSchedule.schedule.byweekday = this.getWeekdayFromApiDate(this.valuePick.dstart)
this.newSchedule.schedule.by_weekday = this.getWeekdayFromApiDate(this.valuePick.firstDate)
// ok then, let's submit and see if any conflicts arise
this.submitting = true
this.$store.dispatch('shows/submitSchedule', {
showId: this.selectedShow.id,
schedule: this.newSchedule,
callback: (response) => {
// we have to check if there are any conflicts with existing timeslots
let conflicts = false
for (let i in response.data.projected) {
if (response.data.projected[i].collisions.length > 0) {
conflicts = true
break
}
}
// if there are no conflicts we can set an empty solutions object
// in our newSchedule and submit it.
if (!conflicts) {
this.newSchedule.solutions = {}
this.submit()
// otherwise we have to resolve the conflict first.
} else {
this.submitting = false
this.$parent.resolve(response.data)
this.$refs.modalEmissionManagerCreate.hide()
}
},
callbackCancel: () => {
this.submitting = false
}
})
},
submit() {
this.$store.dispatch('shows/submitSchedule', {
showId: this.selectedShow.id,
schedule: this.newSchedule,
callback: (response) => {
this.submitting = false
// if for some reason a new conflict arose, e.g. because in the meantime
// someone else inserted a conflicting schedule, we have to resolve.
// TODO: based on single event schedule
// TODO: check for complex schedules with resolved conflicts
if (response.data.projected === undefined) {
this.$parent.renderView(null)
} else {
this.$log.debug('Timeslot conflict. Switching to resolve mode.')
this.$parent.resolve(response.data)
}
this.serverErrors = []
try {
await this.$store.dispatch('shows/submitSchedule', {
showId: this.selectedShow.id,
schedule: this.newSchedule,
})
this.$parent.renderView(null)
this.$refs.modalEmissionManagerCreate.hide()
} catch (e) {
const responseData = e.response?.data
if (e.response?.status === 409) {
this.$parent.resolve(responseData)
this.$refs.modalEmissionManagerCreate.hide()
},
callbackCancel: () => {
this.submitting = false
} else {
this.serverErrors = e.errors ?? []
}
})
} finally {
this.submitting = false
}
},
// initialise a new schedule and open the modal
open(start, end) {
let dstart = start.format('YYYY-MM-DD')
let tstart = start.format('HH:mm')
let dend = end.format('YYYY-MM-DD')
let tend = end.format('HH:mm')
let until = end.format('YYYY-MM-DD')
let now = this.apiDate(new Date())
if (dstart < now) {
dstart = now
let firstDate = start.format('YYYY-MM-DD')
const startTime = start.format('HH:mm')
const endTime = end.format('HH:mm')
const lastDate = end.format('YYYY-MM-DD')
const now = this.apiDate(new Date())
if (firstDate < now) {
firstDate = now
this.pastEventWarning = true
} else {
this.pastEventWarning = false
}
this.valuePick.dstart = dstart
this.valuePick.tstart = tstart
this.valuePick.dend = dend
this.valuePick.tend = tend
this.valuePick.until = until
this.valuePick.firstDate = firstDate
this.valuePick.startTime = startTime
this.valuePick.endTime = endTime
this.valuePick.lastDate = lastDate
this.initialLastDate = lastDate
this.newSchedule = {
schedule: {
rrule: 1,
show: 0,
byweekday: 0,
dstart: dstart,
tstart: tstart,
tend: tend,
until: until,
by_weekday: 0,
first_date: firstDate,
start_time: startTime,
end_time: endTime,
last_date: lastDate,
is_repetition: false,
add_days_no: 0,
add_business_days_only: false,
......
......@@ -5,13 +5,15 @@
:title="$t('scheduleEditor.titleEdit', { show: selectedShow.name })"
size="lg"
>
<server-errors :errors="serverErrors" />
<div v-if="timeslot && loaded.schedule">
<p
class="tw-mb-0"
v-html="$t('scheduleEditor.timeslotRuns', {
dstart: prettyDate(timeslot.start),
tstart: prettyTime(timeslot.start),
tend: prettyTime(timeslot.end),
firstDate: prettyDate(timeslot.start),
startTime: prettyTime(timeslot.start),
endTime: prettyTime(timeslot.end),
})"
/>
......@@ -29,9 +31,9 @@
<span
v-if="scheduleTimeslots.length > 1"
v-html="'<br>' + $t('scheduleEditor.coexistingTimeslot', {
dstart: timeslot.start === scheduleTimeslots[0].start ? prettyDate(scheduleTimeslots[1].start) : prettyDate(scheduleTimeslots[0].start),
tstart: timeslot.start === scheduleTimeslots[0].start ? prettyTime(scheduleTimeslots[1].start) : prettyTime(scheduleTimeslots[0].start),
tend: timeslot.start === scheduleTimeslots[0].start ? prettyTime(scheduleTimeslots[1].end) : prettyTime(scheduleTimeslots[0].end),
firstDate: timeslot.start === scheduleTimeslots[0].start ? prettyDate(scheduleTimeslots[1].start) : prettyDate(scheduleTimeslots[0].start),
startTime: timeslot.start === scheduleTimeslots[0].start ? prettyTime(scheduleTimeslots[1].start) : prettyTime(scheduleTimeslots[0].start),
endTime: timeslot.start === scheduleTimeslots[0].start ? prettyTime(scheduleTimeslots[1].end) : prettyTime(scheduleTimeslots[0].end),
})"
/>
</span>
......@@ -41,7 +43,7 @@
<p
v-html="$t('scheduleEditor.recurringSchedule', {
rrule: rruleRender(schedule.rrule),
until: prettyDate(schedule.until),
lastDate: prettyDate(schedule.last_date),
})"
/>
......@@ -240,8 +242,10 @@
import {mapGetters} from 'vuex'
import prettyDate from '../../mixins/prettyDate'
import rrules from '../../mixins/rrules'
import ServerErrors from '@/components/ServerErrors'
export default {
components: {ServerErrors},
mixins: [prettyDate, rrules],
data() {
......@@ -258,6 +262,7 @@
addNoOfDays: 1,
repetitionRule: 1,
repetitionTime: '',
serverErrors: [],
}
},
......@@ -304,53 +309,53 @@
return start > now
},
createRepetitionSchedule() {
async createRepetitionSchedule() {
const {onlyBusinessDays, addNoOfDays} = this.getRepetitionParameters()
let {dstart, tstart, tend, rrule, until, default_playlist_id, automation_id, byweekday} = this.schedule
let {first_date, time_start, time_end, rrule, last_date, default_playlist_id, automation_id, by_weekday} = this.schedule
if (this.repetitionTime.length > 0) {
const newTstart = `${this.repetitionTime}:00`
const oldTstartNs = this.hmsToNanoseconds(tstart)
const oldTendNs = this.hmsToNanoseconds(tend)
const newTstartNs = this.hmsToNanoseconds(newTstart)
const newStartTime = `${this.repetitionTime}:00`
const newStartTimeNs = this.hmsToNanoseconds(newStartTime)
const oldStartTimeNs = this.hmsToNanoseconds(time_start)
const oldEndTimeNs = this.hmsToNanoseconds(time_end)
tstart = newTstart
tend = this.prettyNanoseconds(newTstartNs + (oldTendNs - oldTstartNs))
console.log(tstart, tend)
time_start = newStartTime
time_end = this.prettyNanoseconds(newStartTimeNs + (oldEndTimeNs - oldStartTimeNs))
}
const newSchedule = {
schedule: {
dstart,
tstart,
tend,
first_date,
time_start,
time_end,
rrule,
until,
last_date,
default_playlist_id,
automation_id,
byweekday,
by_weekday,
is_repetition: true,
add_business_days_only: onlyBusinessDays,
add_days_no: parseInt(addNoOfDays, 10),
}
}
this.$store.dispatch('shows/submitSchedule', {
showId: this.selectedShow.id,
schedule: newSchedule,
callback: (response) => {
console.log(this.$parent)
if (response.data.projected === undefined) {
this.$parent.renderView(null)
} else {
this.$log.debug('Timeslot conflict. Switching to resolve mode.')
this.$parent.resolve(response.data)
}
this.$refs.modalEmissionManagerEdit.hide()
},
})
this.serverErrors = []
try {
await this.$store.dispatch('shows/submitSchedule', {
showId: this.selectedShow.id,
schedule: newSchedule,
})
this.$parent.renderView(null)
} catch (e) {
if (e.response?.status === 409) {
this.$log.debug('Timeslot conflict. Switching to resolve mode.')
this.$parent.resolve(e.response.data)
} else {
this.serverErrors = e.errors ?? []
}
} finally {
this.$refs.modalEmissionManagerEdit.hide()
}
},
deleteFullSchedule(id) {
......@@ -427,8 +432,8 @@
this.$store.dispatch('shows/fetchTimeslots', {
id: this.selectedShow.id,
schedule: scheduleId,
start: this.schedule.dstart,
end: this.schedule.until,
start: this.schedule.first_date,
end: this.schedule.last_date,
})
},
......
......@@ -10,9 +10,9 @@
<div v-if="loaded.modal">
<p
v-html="$t('conflictResolution.projectedSlot', {
dstart: prettyDate(toResolve.start),
tstart: prettyTime(toResolve.start),
tend: prettyTime(toResolve.end),
firstDate: prettyDate(toResolve.start),
startTime: prettyTime(toResolve.start),
endTime: prettyTime(toResolve.end),
})"
/>
......
......@@ -29,7 +29,7 @@
{{ renderRruleForSchedule(schedule) }}
</td>
<td class="text-right">
{{ prettyHours(schedule.tstart) }} - {{ prettyHours(schedule.tend) }}
{{ prettyHours(schedule.start_time) }} - {{ prettyHours(schedule.end_time) }}
</td>
</tr>
</tbody>
......@@ -100,7 +100,7 @@
}
const rrule = uppercaseFirst(this.rruleRender(schedule.rrule))
const weekday = lowercaseFirst(this.prettyWeekday(schedule.byweekday))
const weekday = lowercaseFirst(this.prettyWeekday(schedule.by_weekday))
return `${rrule} ${weekday}s`
}
......
......@@ -336,11 +336,11 @@ export default {
'titleNoConflict': 'Kein Konflikt vorhanden',
'noConflict': 'Dieser Sendeplatz hat keinen Konflikt. Wunderbar!',
'projectedSlot': 'Der neue Sendeplatz läuft am <b>%{dstart}</b> von <b>%{tstart}</b> bis <b>%{tend}</b>',
'projectedSlot': 'Der neue Sendeplatz läuft am <b>%{firstDate}</b> von <b>%{startTime}</b> bis <b>%{endTime}</b>',
'conflictsWith': 'Er steht im Konflikt mit den folgenden Sendeplätzen',
'newSchedule': 'von <b>%{dstart}, %{tstart}</b> bis <b>%{tend}</b>',
'recurringSchedule': 'Dieses Ausstrahlungs-Schema wiederholt sich <b>%{rrule}</b> bis <b>%{until}</b>',
'newSchedule': 'von <b>%{firstDate}, %{startTime}</b> bis <b>%{endTime}</b>',
'recurringSchedule': 'Dieses Ausstrahlungs-Schema wiederholt sich <b>%{rrule}</b> bis <b>%{lastDate}</b>',
'leftToResolve': 'Noch %{smart_count} Konflikt zu beheben |||| Noch %{smart_count} Konflikte zu beheben',
'noneLeftToResolve': 'Keine Konflikte mehr zu beheben!',
'applySolution': 'Lösung anwenden',
......@@ -366,11 +366,11 @@ export default {
'titleCreate': 'Neues Austrahlungs-Schema für "%{show}" erstellen',
'titleEdit': 'Austrahlungs-Schema der Sendunge "%{show}" bearbeiten',
'timeslotRuns': 'Dieser Sendeplatz läuft am <b>%{dstart}</b> von <b>%{tstart}</b> bis <b>%{tend}</b>',
'recurringSchedule': 'Dieses Programm wiederholt sich <b>%{rrule}</b> bis <b>%{until}</b>',
'timeslotRuns': 'Dieser Sendeplatz läuft am <b>%{firstDate}</b> von <b>%{startTime}</b> bis <b>%{endTime}</b>',
'recurringSchedule': 'Dieses Programm wiederholt sich <b>%{rrule}</b> bis <b>%{lastDate}</b>',
'singleEmission': 'Der Sendeplatz ist eine einmalige Ausstrahlung',
'coexistingTimeslot': 'Aber aufgrund einer Konfliktbehebung gibt es einen weiteren Sendeplatz am <b>%{dstart}</b> von <b>%{tstart}</b> bis <b>%{tend}</b>',
'coexistingTimeslot': 'Aber aufgrund einer Konfliktbehebung gibt es einen weiteren Sendeplatz am <b>%{firstDate}</b> von <b>%{startTime}</b> bis <b>%{endTime}</b>',