EmissionManager.vue 12.6 KB
Newer Older
1
<template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  <b-container>
    <b-row v-if="loaded.shows">
      <b-col>
        <h3>{{ shows[currentShow].name }}</h3>
      </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>
30

31
32
    <hr>

33
34
35
36
    <b-alert
      variant="danger"
      :show="conflictMode"
    >
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
      <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>
63
64
    </b-alert>

65
66
67
68
69
70
71
    <full-calendar
      ref="calendar"
      editable="false"
      default-view="agendaWeek"
      :events="calendarSlots"
      :config="calendarConfig"
      @view-render="renderView"
72
73
74
75
76
77
78
79
      @event-selected="eventSelected"
      @event-drop="eventDrop"
      @event-resize="eventResize"
      @event-created="eventCreated"
    />

    <app-modalEmissionManagerCreate
      ref="appModalEmissionManagerCreate"
80
    />
81
82
83
    <app-modalEmissionManagerResolve
      ref="appModalEmissionManagerResolve"
    />
84
  </b-container>
85
86
87
88
</template>

<script>
import axios from 'axios'
89
import { FullCalendar } from 'vue-full-calendar'
90
import 'fullcalendar/dist/fullcalendar.css'
91
import modalEmissionManagerCreate from './EmissionManagerModalCreate.vue'
92
93
import modalEmissionManagerResolve from './EmissionManagerModalResolve.vue'
import rrules from '../mixins/rrules'
94
95
96
97

export default {
  components: {
    FullCalendar,
98
    'app-modalEmissionManagerCreate': modalEmissionManagerCreate,
99
    'app-modalEmissionManagerResolve': modalEmissionManagerResolve,
100
  },
101

102
103
  mixins: [ rrules ],

104
105
  data () {
    return {
106
      currentShow: 0,
107
108
109
      shows: [],
      timeslots: [],
      calendarSlots: [],
110

111
112
113
114
115
116
117
118
119
      // flags for loading data
      loaded: {
        shows: false,
        timeslots: false,
        calendarSlots: false,
      },

      // this flag signifies if we are in conflict resolution mode
      conflictMode: false,
120
121
122
123
124
      // 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: [],
125

126
      // this is the whole configuration for our schedule calendar, including
127
      // simple event handlers that do not need the whole components scope
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
      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,
          },
        },
144
145
146
147
148
        // 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);
        },
149
150
151
152
      },
    }
  },

153
154
155
156
157
158
159
  created () {
    if (this.$route.query.show) {
      this.currentShow = this.$route.query.show
    } else {
      this.currentShow = 0
    }
    this.loadShows()
160
161
162
  },

  methods: {
163
164
165
    switchShow (index) {
      this.currentShow = index
      this.loadCalendarSlots()
166
167
168
169
170
171
172
173
174
175
176
    },

    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'
      }
    },

177
178
179
180
181
182
183
184
185
186
    eventSelected (event) {
      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)
        }
      }
187
188
189
190
191
192
193
194
195
196
197
198
199
200
    },

    eventDrop (event) {
      this.$log.debug('eventDrop', event)
    },

    eventResize (event) {
      this.$log.debug('eventResize', event)
    },

    eventCreated (event) {
      this.$refs.appModalEmissionManagerCreate.open(event.start, event.end)
    },

201
202
203
204
    // 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) {
205
206
207
208
209
210
211
212
213
214
215
        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()
        }
216
217
        // we only load new timeslots, if we are not in conflict mode
        if (!this.conflictMode) {
218
219
          this.loadTimeslots(start, end)
        }
220
221
222
      }
    },

223
224
    resolve (data) {
      this.$log.debug('resolve', data)
225
      this.resolveData = data
226
      this.conflictMode = true
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
      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)
      }
270
271
    },

272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
    resolveEvent (toResolve, mode) {
      this.$log.debug('resolveEvent', toResolve)
      this.conflictCount -= toResolve.collisions.length
      switch (mode) {
        case 'theirs':
          this.conflictSolutions[toResolve.hash] = 'theirs'
          toResolve.className = 'discarded'
          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
      })
    },

328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
    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({
          start: this.timeslots[i].start,
          end: this.timeslots[i].end,
          title: this.getShowTitleById(this.timeslots[i].show),
          className: highlighting
        })
      }
      this.loaded.calendarSlots = true
    },

346
    loadTimeslots (start, end) {
347
      this.$log.debug('loadTimeslots: currentShow = '+this.currentShow)
348
349
350
351
      this.loaded.timeslots = false
      let uri = process.env.VUE_APP_API_STEERING + 'timeslots?start=' + start + '&end=' + end
      axios.get(uri, {
        withCredentials: true,
352
        headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
353
      }).then(response => {
354
        this.timeslots = response.data
355
        this.loaded.timeslots = true
356
        this.loadCalendarSlots()
357
358
359
360
361
362
363
364
365
366
367
368
      }).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,
369
        headers: { 'Authorization': 'Bearer ' + this.$parent.user.access_token }
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
      }).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.')
      })
    },
  },
}
</script>

<style>
.otherShow {
  background-color: #eee;
}
a.currentShow {
  background-color: #17a2b8;
}
393
394
395
396
397
398
.conflict {
  background-color: #b00;
}
.noconflict {
  background-color: #17a2b8;
}
399
400
401
402
.discarded {
  background-color: #eee;
  text-decoration: line-through;
}
403
</style>