aboutsummaryrefslogtreecommitdiff
path: root/resources/vue
diff options
context:
space:
mode:
authorMoritz Strohm <strohm@data-quest.de>2026-01-14 10:29:35 +0000
committerMoritz Strohm <strohm@data-quest.de>2026-01-14 10:29:35 +0000
commit78e46de33b3f205aae375d1ea6d4fe088e0e5124 (patch)
tree4b305bf3f7b5d066ac28f011fe752e98901e714c /resources/vue
parentf637e7ae2d086941a11297ccc29ac273ad6759b0 (diff)
allow booking separable rooms in courses, closes #639
Closes #639 Merge request studip/studip!4039
Diffstat (limited to 'resources/vue')
-rw-r--r--resources/vue/apps/CourseBlockAppointments.vue266
-rw-r--r--resources/vue/apps/CourseDateFormContent.vue227
-rw-r--r--resources/vue/base-components.js1
-rw-r--r--resources/vue/components/CourseDateRoomFieldset.vue310
-rw-r--r--resources/vue/components/Multiselect.vue13
-rw-r--r--resources/vue/components/StudipSelect.vue5
6 files changed, 821 insertions, 1 deletions
diff --git a/resources/vue/apps/CourseBlockAppointments.vue b/resources/vue/apps/CourseBlockAppointments.vue
new file mode 100644
index 0000000..35ae1c4
--- /dev/null
+++ b/resources/vue/apps/CourseBlockAppointments.vue
@@ -0,0 +1,266 @@
+<template>
+ <fieldset>
+ <legend>{{ $gettext('Grunddaten') }}</legend>
+ <section>
+ <label class="col-2">
+ {{ $gettext('Startdatum') }}
+ <datepicker name="start_date"
+ v-model="start_date"></datepicker>
+ </label>
+ <label class="col-2">
+ {{ $gettext('Enddatum') }}
+ <datepicker name="end_date"
+ v-model="end_date"></datepicker>
+ </label>
+ </section>
+ <section>
+ <label class="col-2">
+ {{ $gettext('Beginn') }}
+ <timepicker name="start_time"
+ v-model="start_time_str"></timepicker>
+ </label>
+ <label class="col-2">
+ {{ $gettext('Ende') }}
+ <timepicker name="end_time"
+ v-model="end_time_str"></timepicker>
+ </label>
+ </section>
+ <section id="block_appointment_days">
+ <label>{{ $gettext('Die Termine finden an folgenden Tagen statt:') }}</label>
+ <label class="col-2">
+ <input type="checkbox" name="dow[]" v-model="all_days_selected" value="all"
+ :checked="all_days_selected">
+ {{ $gettext('Jeden Tag') }}
+ </label>
+ <label class="col-2">
+ <input type="checkbox" name="dow[]" v-model="mon_fri_selected" value="mon_fri"
+ :checked="mon_fri_selected">
+ {{ $gettext('Montag - Freitag') }}
+ </label>
+
+ <label class="col-2">
+ <input type="checkbox" name="dow[]" v-model="dow" :value="1"
+ :checked="dow.includes(1)">
+ {{ $gettext('Montag') }}
+ </label>
+ <label class="col-2">
+ <input type="checkbox" name="dow[]" v-model="dow" :value="2"
+ :checked="dow.includes(2)">
+ {{ $gettext('Dienstag') }}
+ </label>
+ <label class="col-2">
+ <input type="checkbox" name="dow[]" v-model="dow" :value="3"
+ :checked="dow.includes(3)">
+ {{ $gettext('Mittwoch') }}
+ </label>
+ <label class="col-2">
+ <input type="checkbox" name="dow[]" v-model="dow" :value="4"
+ :checked="dow.includes(4)">
+ {{ $gettext('Donnerstag') }}
+ </label>
+ <label class="col-2">
+ <input type="checkbox" name="dow[]" v-model="dow" :value="5"
+ :checked="dow.includes(5)">
+ {{ $gettext('Freitag') }}
+ </label>
+ <label class="col-2">
+ <input type="checkbox" name="dow[]" v-model="dow" :value="6"
+ :checked="dow.includes(6)">
+ {{ $gettext('Samstag') }}
+ </label>
+ <label class="col-2">
+ <input type="checkbox" name="dow[]" v-model="dow" :value="0"
+ :checked="dow.includes(0)">
+ {{ $gettext('Sonntag') }}
+ </label>
+ </section>
+ <section>
+ <label>
+ {{ $gettext('Anzahl der Termine') }}
+ <input type="number" name="date_count"
+ min="1" :max="this.time_ranges.length"
+ v-model="this.date_count">
+ </label>
+ <studip-message-box v-if="this.date_count > 50"
+ type="info" :hideClose="true"
+ :hideDetails="false">
+ {{ $gettextInterpolate(
+ 'Sie legen %{count} Termine an. Bitte kontrollieren Sie Ihre Eingaben.',
+ {count: this.date_count}
+ ) }}
+ </studip-message-box>
+ </section>
+ </fieldset>
+ <CourseDateRoomFieldset
+ :time_ranges="time_ranges"
+ :room_management_enabled="room_management_enabled"
+ :initial_selected_room_option="'noroom'"
+ :allow_multiple_room_bookings="allow_multiple_room_bookings"
+ :initial_preparation_time="initial_preparation_time"
+ :initial_subsequent_time="initial_subsequent_time"
+ :max_preparation_time="max_preparation_time"
+ ></CourseDateRoomFieldset>
+ <fieldset>
+ <legend>{{ $gettext('Weitere Angaben') }}</legend>
+ <label>
+ {{ $gettext('Termintyp') }}
+ <select name="date_type"
+ v-model="selected_date_type">
+ <option v-for="date_type in date_types" :value="date_type.id" :key="date_type.id">
+ {{ date_type.name}}
+ </option>
+ </select>
+ </label>
+ <label>
+ {{ $gettext('Zugewiesene Lehrende') }}
+ <multiselect name="assigned_lecturers[]"
+ :options="available_lecturer_options"
+ v-model="selected_lecturer_list"
+ :no_options_text="$gettext('Keine Lehrenden auswählbar')"
+ :value="selected_lecturer_list"
+ ></multiselect>
+ <input type="hidden" name="assigned_lecturers[]"
+ v-for="item in selected_lecturer_list"
+ v-bind:key="item" :value="item">
+ </label>
+ </fieldset>
+</template>
+<script>
+import {$gettext} from "../../assets/javascripts/lib/gettext";
+import CourseDateRoomFieldset from "../components/CourseDateRoomFieldset.vue";
+import Timepicker from "../components/Timepicker.vue";
+import Datepicker from "../components/Datepicker.vue";
+import StudipMessageBox from "../components/StudipMessageBox.vue";
+export default {
+ name: 'CourseBlockAppointments',
+ components: {StudipMessageBox, CourseDateRoomFieldset, Timepicker, Datepicker},
+ props: {
+ room_management_enabled: {
+ type: Boolean,
+ required: true,
+ default: false
+ },
+ max_preparation_time: {
+ type: Number,
+ required: false,
+ default: 999
+ },
+ initial_preparation_time: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ initial_subsequent_time: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ allow_multiple_room_bookings: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ date_types: {
+ type: Array,
+ required: true
+ },
+ available_lecturers: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
+ selected_lecturers: {
+ type: Array,
+ required: false,
+ default: () => []
+ }
+ },
+ methods: {
+ $gettext,
+ },
+ data() {
+ let now = new Date();
+ //Use the next half hour as default:
+ let start_date = new Date(Math.ceil(now.getTime() / 1800000) * 1800000);
+ let end_date = new Date((Math.ceil(now.getTime() / 1800000) * 1800000) + 1800000);
+ let start_time_str = STUDIP.DateTime.pad(start_date.getHours()) + ':' + STUDIP.DateTime.pad(start_date.getMinutes());
+ let end_time_str = STUDIP.DateTime.pad(end_date.getHours()) + ':' + STUDIP.DateTime.pad(end_date.getMinutes());
+ let selected_date_type = '';
+ if (this.date_types.length > 0) {
+ selected_date_type = this.date_types[0].id;
+ }
+
+ return {
+ start_date: now.getTime() / 1000,
+ end_date: now.getTime() / 1000,
+ start_time_str,
+ end_time_str,
+ all_days_selected: true,
+ mon_fri_selected: false,
+ dow: [],
+ date_count: 0,
+ last_changed_start_date: new Date(),
+ last_changed_end_date: new Date(),
+ last_changed_start_time: new Date(),
+ last_changed_end_time: new Date(),
+ last_changed_dow: new Date(),
+ available_lecturer_options: this.available_lecturers !== undefined ? this.available_lecturers : [],
+ selected_lecturer_list: this.selected_lecturers !== undefined ? this.selected_lecturers : [],
+ selected_date_type
+ }
+ },
+ computed: {
+ time_ranges() {
+ if (this.start_date > this.end_date) {
+ //Invalid time range selection.
+ return [];
+ }
+ let start_time_parts = this.start_time_str.split(':');
+ let end_time_parts = this.end_time_str.split(':');
+ if (start_time_parts.length !== 2 || end_time_parts.length !== 2) {
+ //Invalid time format.
+ return [];
+ }
+ let day_numbers = [];
+ if (this.all_days_selected) {
+ day_numbers = [0, 1, 2, 3, 4, 5, 6];
+ } else if (this.mon_fri_selected) {
+ day_numbers = [1, 2, 3, 4, 5];
+ } else {
+ day_numbers = this.dow;
+ }
+ if (day_numbers.length === 0) {
+ //No days selected. Nothing to do.
+ return [];
+ }
+ let current_start = new Date(this.start_date * 1000);
+ current_start.setHours(parseInt(start_time_parts[0]), parseInt(start_time_parts[1]), 0, 0);
+ let current_end = new Date(this.end_date * 1000);
+ current_end.setHours(parseInt(end_time_parts[0]), parseInt(end_time_parts[1]), 0, 0);
+
+ let new_time_ranges = [];
+ while (current_start < current_end) {
+ let relevant_day = current_start.getDay();
+ if (day_numbers.includes(relevant_day)) {
+ //Put the day into the time ranges.
+ let range_start = new Date(current_start.getTime());
+ let range_end = new Date(current_start.getTime());
+ range_end.setHours(parseInt(end_time_parts[0]), parseInt(end_time_parts[1]), 0, 0);
+ new_time_ranges.push({start: range_start, end: range_end});
+ }
+ current_start.setDate(current_start.getDate() + 1);
+ }
+ return new_time_ranges;
+ }
+ },
+ watch: {
+ time_ranges(newValue) {
+ if (newValue === undefined) {
+ this.date_count = 0;
+ } else {
+ this.date_count = newValue.length;
+ }
+ }
+ }
+}
+</script>
diff --git a/resources/vue/apps/CourseDateFormContent.vue b/resources/vue/apps/CourseDateFormContent.vue
new file mode 100644
index 0000000..0cf6b40
--- /dev/null
+++ b/resources/vue/apps/CourseDateFormContent.vue
@@ -0,0 +1,227 @@
+<template>
+ <fieldset>
+ <legend>{{ $gettext('Grunddaten') }}</legend>
+ <label class="col-2">
+ {{ $gettext('Datum') }}
+ <datepicker name="date"
+ v-model="start_date"></datepicker>
+ </label>
+ <label class="col-2">
+ {{ $gettext('Beginn') }}
+ <timepicker name="start_time"
+ v-model="start_time_str"></timepicker>
+ </label>
+ <label class="col-2">
+ {{ $gettext('Ende') }}
+ <timepicker name="end_time"
+ v-model="end_time_str"></timepicker>
+ </label>
+ </fieldset>
+ <CourseDateRoomFieldset
+ :time_ranges="time_ranges"
+ :course_date_ids="course_date_ids"
+ :show_nochange_option="course_date_ids.length > 0"
+ :room_management_enabled="room_management_enabled"
+ :initial_selected_rooms="selected_rooms"
+ :initial_room_name="initial_room_name"
+ :allow_multiple_room_bookings="allow_multiple_room_bookings"
+ :initial_preparation_time="initial_preparation_time"
+ :initial_subsequent_time="initial_subsequent_time"
+ ></CourseDateRoomFieldset>
+
+ <fieldset>
+ <legend>{{ $gettext('Weitere Angaben') }}</legend>
+ <label>
+ {{ $gettext('Termintyp') }}
+ <select name="date_type"
+ v-model="selected_date_type">
+ <option v-for="date_type in date_types" :value="date_type.id" :key="date_type.id">
+ {{date_type.name}}
+ </option>
+ </select>
+ </label>
+
+ <label>
+ {{ $gettext('Zugewiesene Lehrende') }}
+ <multiselect name="assigned_lecturers[]"
+ :options="available_lecturer_options"
+ v-model="selected_lecturer_list"
+ :no_options_text="$gettext('Keine Lehrenden auswählbar')"
+ :value="selected_lecturer_list"
+ ></multiselect>
+ <input type="hidden" name="assigned_lecturers[]"
+ v-for="item in selected_lecturer_list"
+ v-bind:key="item" :value="item">
+ </label>
+ <label>
+ {{ $gettext('Beteiligte Gruppen') }}
+ <multiselect name="assigned_groups[]"
+ :options="available_group_options"
+ v-model="selected_group_list"
+ :no_options_text="$gettext('Keine Gruppen auswählbar')"
+ :value="selected_group_list"
+ ></multiselect>
+ <input type="hidden" name="assigned_groups[]"
+ v-for="item in selected_group_list"
+ v-bind:key="item" :value="item">
+ </label>
+ <label v-if="enable_number_of_participants">
+ {{ $gettext('Anzahl der Teilnehmenden') }}
+ <input type="number" min="0"
+ name="number_of_participants">
+ </label>
+ </fieldset>
+</template>
+<script>
+import {$gettext} from "../../assets/javascripts/lib/gettext";
+import Datepicker from "../components/Datepicker.vue";
+import Timepicker from "../components/Timepicker.vue";
+import Multiselect from "../components/Multiselect.vue";
+import CourseDateRoomFieldset from "../components/CourseDateRoomFieldset.vue";
+
+export default {
+ name: 'CourseDateFormContent',
+ components: {CourseDateRoomFieldset, Multiselect, Timepicker, Datepicker},
+ props: {
+ course_date: {
+ type: Object,
+ required: false,
+ default: null
+ },
+ date_types: {
+ type: Array,
+ required: true
+ },
+ room_management_enabled: {
+ type: Boolean,
+ required: true,
+ default: false
+ },
+ initial_preparation_time: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ initial_subsequent_time: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ max_preparation_time: {
+ type: Number,
+ required: false,
+ default: 999
+ },
+ allow_multiple_room_bookings: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ enable_number_of_participants: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ selected_rooms: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
+ available_lecturers: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
+ selected_lecturers: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
+ available_groups: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
+ selected_groups: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
+ },
+ data() {
+ let selected_date_type = '';
+ let course_date_ids = [];
+ let start_date = null;
+ let end_date = null;
+ let initial_room_name = '';
+ if (this.course_date) {
+ start_date = new Date(this.course_date.date * 1000);
+ end_date = new Date(this.course_date.end_time * 1000);
+ selected_date_type = this.course_date.date_typ;
+ course_date_ids.push(this.course_date.termin_id);
+ initial_room_name = this.course_date.raum;
+ } else {
+ start_date = new Date();
+ if (this.date_types[0] !== undefined) {
+ selected_date_type = this.date_types[0].id;
+ }
+ //Round the time values to the next half hour:
+ start_date = new Date(Math.ceil(start_date.getTime() / 1800000) * 1800000);
+ end_date = new Date((Math.ceil(start_date.getTime() / 1800000) * 1800000) + 1800000);
+ }
+ let start_time_str = null;
+ let end_time_str = null;
+ if (start_date && end_date) {
+ start_time_str = STUDIP.DateTime.pad(start_date.getHours()) + ':' + STUDIP.DateTime.pad(start_date.getMinutes());
+ end_time_str = STUDIP.DateTime.pad(end_date.getHours()) + ':' + STUDIP.DateTime.pad(end_date.getMinutes());
+ }
+
+ return {
+ start_date,
+ start_time_str,
+ end_time_str,
+ course_date_ids,
+ selected_date_type,
+ booking_selected: this.room_management_enabled && this.selected_rooms,
+ separable_room_name: '',
+ initial_room_name,
+ last_changed_date: new Date(),
+ last_changed_start_time: new Date(),
+ last_changed_end_time: new Date(),
+ available_lecturer_options: this.available_lecturers !== undefined ? this.available_lecturers : [],
+ selected_lecturer_list: this.selected_lecturers !== undefined ? this.selected_lecturers : [],
+ available_group_options: this.available_groups !== undefined ? this.available_groups : [],
+ selected_group_list: this.selected_groups !== undefined ? this.selected_groups : []
+ };
+ },
+ methods: {
+ $gettext
+ },
+ computed: {
+ time_ranges() {
+ let start = new Date(this.start_date);
+ let end = new Date(this.start_date);
+ if (typeof(this.start_date) === 'number') {
+ //The start date is not a date object but a timestamp.
+ start = new Date(this.start_date * 1000);
+ end = new Date(this.start_date * 1000);
+ }
+ let start_time_parts = this.start_time_str.split(':');
+ if (start_time_parts.length !== 2) {
+ //Invalid time string.
+ return [];
+ }
+ start.setHours(parseInt(start_time_parts[0]), parseInt(start_time_parts[1]), 0);
+
+ let end_time_parts = this.end_time_str.split(':');
+ if (end_time_parts.length !== 2) {
+ //Invalid time string.
+ return [];
+ }
+ end.setHours(parseInt(end_time_parts[0]), parseInt(end_time_parts[1]), 0);
+
+ return [{start, end}];
+ }
+ }
+}
+</script>
diff --git a/resources/vue/base-components.js b/resources/vue/base-components.js
index 904afa1..dd6561e 100644
--- a/resources/vue/base-components.js
+++ b/resources/vue/base-components.js
@@ -3,6 +3,7 @@ import { defineAsyncComponent } from 'vue';
const BaseComponents = {
CaptchaInput: defineAsyncComponent(() => import('./components/form_inputs/CaptchaInput.vue')),
CalendarPermissionsTable: defineAsyncComponent(() => import('./components/form_inputs/CalendarPermissionsTable.vue')),
+ CourseDateRoomFieldset: defineAsyncComponent(() => import('./components/CourseDateRoomFieldset.vue')),
DateListInput: defineAsyncComponent(() => import('./components/form_inputs/DateListInput.vue')),
Datepicker: defineAsyncComponent(() => import('./components/Datepicker.vue')),
Datetimepicker: defineAsyncComponent(() => import('./components/Datetimepicker.vue')),
diff --git a/resources/vue/components/CourseDateRoomFieldset.vue b/resources/vue/components/CourseDateRoomFieldset.vue
new file mode 100644
index 0000000..1bdec9a
--- /dev/null
+++ b/resources/vue/components/CourseDateRoomFieldset.vue
@@ -0,0 +1,310 @@
+<template>
+ <fieldset>
+ <legend>{{ $gettext('Raumangaben') }}</legend>
+
+ <section v-if="room_management_enabled">
+ <studip-message-box v-if="selected_room_option === 'room' && available_rooms.length === 0 && searched_for_rooms"
+ hide-close="true">
+ {{ $gettext('Im gewählten Zeitbereich sind keine buchbaren Räume verfügbar.') }}
+ </studip-message-box>
+ <label>
+ <input type="radio" name="room"
+ v-model="selected_room_option"
+ value="room">
+ {{ $gettext('Gebuchte Räume') }}
+ </label>
+ <label v-if="selected_room_option === 'room' && available_rooms.length > 0" for="room_ids[]">
+ {{ $gettext('Raum auswählen') }}
+ </label>
+ <span class="flex-row">
+ <StudipSelect v-if="allow_multiple_room_bookings"
+ v-model="selected_room_list"
+ :no_options_text="$gettext('Kein Raum verfügbar')"
+ :options="available_rooms"
+ multiple
+ style="flex-grow: 2">
+ <template #selected-option="{id, label}">
+ <span>{{ label }}</span>
+ <input type="hidden" name="room_ids[]" :value="id">
+ </template>
+ </StudipSelect>
+ <select v-if="!allow_multiple_room_bookings"
+ name="room_ids[]"
+ style="flex-grow: 2"
+ v-model="selected_room_list">
+ <option v-for="room of available_rooms" :key="room.id"
+ :value="room.id" :selected="selected_room_list.includes(room.id)">
+ {{ room.label }}
+ </option>
+ </select>
+ <studip-icon v-if="show_ajax_indicator" shape="reload" role="info"></studip-icon>
+ </span>
+ <section v-if="selected_room_option === 'room' && available_rooms.length > 0 && Object.keys(visible_info_texts).length > 0">
+ <h3>{{ $gettext('Hinweise zu teilbaren Räumen') }}</h3>
+ <ul class="default">
+ <li v-for="item in visible_info_texts" v-bind:key="item">
+ {{ item }}
+ </li>
+ </ul>
+ </section>
+ <label v-if="selected_room_option === 'room' && available_rooms.length > 0">
+ {{ $gettext('Rüstzeit vor dem Termin (in Minuten)') }}
+ <input type="number" name="preparation_time"
+ class="preparation-time"
+ v-model="preparation_time"
+ min="0"
+ :max="max_preparation_time">
+ </label>
+ <label v-if="selected_room_option === 'room' && available_rooms.length > 0">
+ {{ $gettext('Rüstzeit nach dem Termin (in Minuten)') }}
+ <input type="number" name="subsequent_time"
+ class="preparation-time"
+ v-model="subsequent_time"
+ min="0"
+ :max="max_preparation_time">
+ </label>
+ </section>
+
+ <label>
+ <input type="radio" name="room"
+ v-model="selected_room_option"
+ value="freetext">
+ {{ $gettext('Freie Ortsangabe (keine Raumbuchung)') }}
+ </label>
+ <label v-if="selected_room_option === 'freetext'">
+ <input type="text" name="room_name"
+ v-model="room_name"
+ :placeholder="$gettext('Freie Ortsangabe (keine Raumbuchung)')">
+ </label>
+
+ <label>
+ <input type="radio" name="room"
+ v-model="selected_room_option"
+ value="noroom">
+ {{ $gettext('Kein Raum') }}
+ </label>
+ <label v-if="show_nochange_option">
+ <input type="radio" name="room"
+ v-model="selected_room_option"
+ value="nochange">
+ {{ $gettext('Keine Änderung') }}
+ </label>
+ </fieldset>
+</template>
+<script>
+import {$gettext} from "../../assets/javascripts/lib/gettext";
+import StudipMessageBox from "../components/StudipMessageBox.vue";
+import StudipSelect from "../components/StudipSelect.vue";
+import StudipIcon from "../components/StudipIcon.vue";
+import {jsonapi} from "../../assets/javascripts/lib/jsonapi";
+
+export default {
+ name: 'CourseDateRoomFieldset',
+ components: {StudipMessageBox, StudipSelect, StudipIcon},
+ props: {
+ time_ranges: {
+ type: Array,
+ required: true,
+ default: () => []
+ },
+ course_date_ids: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
+ room_management_enabled: {
+ type: Boolean,
+ required: true,
+ default: false
+ },
+ allow_multiple_room_bookings: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ max_preparation_time: {
+ type: Number,
+ required: false,
+ default: 999
+ },
+ initial_selected_rooms: {
+ type: Array,
+ required: false,
+ default: () => []
+ },
+ initial_selected_room_option: {
+ type: String,
+ required: false,
+ default: 'nochange'
+ },
+ show_nochange_option: {
+ type: Boolean,
+ required: false,
+ default: false
+ },
+ initial_preparation_time: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ initial_subsequent_time: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ initial_room_name: {
+ type: String,
+ required: false,
+ default: ''
+ }
+ },
+ data() {
+ let room_option = this.initial_selected_room_option;
+ if (!this.show_nochange_option) {
+ room_option = 'noroom';
+ }
+ return {
+ searched_for_rooms: false,
+ available_rooms: [],
+ preparation_time: this.initial_preparation_time,
+ subsequent_time: this.initial_subsequent_time,
+ room_name: this.initial_room_name,
+ info_texts: [],
+ selected_room_list: this.initial_selected_rooms !== undefined ? this.initial_selected_rooms : [],
+ selected_room_option: room_option,
+ show_ajax_indicator: false
+ }
+ },
+ methods: {
+ $gettext,
+ getAvailableRooms() {
+ if (this.selected_room_option !== 'room') {
+ //We don't need to look for available rooms.
+ return;
+ }
+
+ //Reload the list of available rooms:
+ this.show_ajax_indicator = true;
+ try {
+ const options = {
+ method: 'GET',
+ data: {
+ time_ranges: JSON.stringify(this.time_ranges)
+ },
+ async: true
+ };
+ if (this.course_date_ids) {
+ options.data.course_date_ids = this.course_date_ids;
+ }
+ jsonapi.request('available-rooms', options).then((response) => {
+ const json = JSON.parse(response);
+ if (!json) {
+ //Error fetching the available rooms.
+ this.available_rooms = [];
+ this.searched_for_rooms = true;
+ }
+ //Change the format for the multiselect and strip the info text.
+ this.available_rooms = [];
+ let available_room_ids = [];
+ this.info_texts = {};
+ let current_separable_room_id = '';
+ for (let item of json) {
+ //$item is an object with the attributes id, name and info_text.
+ if (item.id.startsWith('separable_room-')) {
+ //It is a separable room.
+ this.available_rooms.push(
+ {
+ id: item.id,
+ label: item.name,
+ indented: false,
+ separable_room_id: item.separable_room_id
+ }
+ );
+ available_room_ids.push(item.id);
+ current_separable_room_id = item.id.substring(15);
+ } else if (item.separable_room_id) {
+ //Indent the name of the room part of the separable room:
+ this.available_rooms.push(
+ {
+ id: item.id,
+ label: item.name,
+ indented: current_separable_room_id.length > 0,
+ separable_room_id: item.separable_room_id
+ }
+ );
+ available_room_ids.push(item.id);
+ } else {
+ //A room that is not part of a separable room.
+ current_separable_room_id = '';
+ this.available_rooms.push(
+ {
+ id: item.id,
+ label: item.name,
+ indented: false,
+ separable_room_id: null
+ }
+ );
+ available_room_ids.push(item.id);
+ }
+ if (item.info_text && item.info_text.length > 0 && item.separable_room_id) {
+ this.info_texts[item.id] = {
+ separable_room_id: item.separable_room_id,
+ info_text: item.info_text
+ };
+ }
+ }
+ this.searched_for_rooms = true;
+ //Update the selected rooms: If a room is not present in the list of available rooms,
+ //it shall be removed from the list.
+ let new_selected_rooms = [];
+ for (let selected_room of this.selected_room_list) {
+ if (available_room_ids.includes(selected_room.id)) {
+ new_selected_rooms.push(selected_room);
+ }
+ }
+ this.selected_room_list = new_selected_rooms;
+ });
+ } catch (error) {
+ console.error(error);
+ //Clear the list of available rooms, since we cannot determine
+ //if the current list is accurate.
+ this.available_rooms = [];
+ this.searched_for_rooms = true;
+ }
+ this.show_ajax_indicator = false;
+ },
+ },
+ computed: {
+ visible_info_texts() {
+ let new_visible_info_texts = {};
+ for (let item of this.selected_room_list) {
+ if (item.separable_room_id && this.info_texts[item.id]) {
+ let item_info_text = this.info_texts[item.id];
+ new_visible_info_texts[item_info_text.separable_room_id] = item_info_text.info_text;
+ }
+ }
+ return new_visible_info_texts;
+ }
+ },
+ watch: {
+ time_ranges(new_ranges, old_ranges) {
+ if (old_ranges === undefined || old_ranges === new_ranges) {
+ //Do nothing.
+ return;
+ }
+ if (this.selected_room_option === 'room') {
+ this.getAvailableRooms();
+ }
+ },
+ selected_room_option(new_options, old_options) {
+ if (old_options === undefined || old_options === new_options) {
+ //Do nothing.
+ return;
+ }
+ if (this.selected_room_option === 'room') {
+ this.getAvailableRooms();
+ }
+ }
+ }
+}
+</script>
diff --git a/resources/vue/components/Multiselect.vue b/resources/vue/components/Multiselect.vue
index 7b70f22..ed1f23c 100644
--- a/resources/vue/components/Multiselect.vue
+++ b/resources/vue/components/Multiselect.vue
@@ -7,7 +7,10 @@
v-bind="$attrs"
>
<template v-slot:no-options>
- {{ $gettext('Keine Auswahlmöglichkeiten') }}
+ {{ this.no_options_text }}
+ </template>
+ <template #open-indicator="{ selectAttributes }">
+ <span v-bind="selectAttributes"><studip-icon shape="arr_1down" :size="10"/></span>
</template>
</v-select>
</template>
@@ -15,9 +18,12 @@
<script>
import vSelect from 'vue-select';
import 'vue-select/dist/vue-select.css'
+import {$gettext} from "../../assets/javascripts/lib/gettext";
+import StudipIcon from "./StudipIcon.vue";
export default {
name: 'multiselect',
components: {
+ StudipIcon,
vSelect,
},
emits: ['update:model-value'],
@@ -36,6 +42,11 @@ export default {
options: {
type: Object,
required: true
+ },
+ no_options_text: {
+ type: String,
+ required: false,
+ default: $gettext('Keine Auswahlmöglichkeiten')
}
},
data () {
diff --git a/resources/vue/components/StudipSelect.vue b/resources/vue/components/StudipSelect.vue
index 1246aac..dca450a 100644
--- a/resources/vue/components/StudipSelect.vue
+++ b/resources/vue/components/StudipSelect.vue
@@ -110,3 +110,8 @@ export default {
}
};
</script>
+<style>
+.studip-v-select .vs__dropdown-toggle {
+ max-height: fit-content;
+}
+</style>