diff options
| author | Moritz Strohm <strohm@data-quest.de> | 2026-01-14 10:29:35 +0000 |
|---|---|---|
| committer | Moritz Strohm <strohm@data-quest.de> | 2026-01-14 10:29:35 +0000 |
| commit | 78e46de33b3f205aae375d1ea6d4fe088e0e5124 (patch) | |
| tree | 4b305bf3f7b5d066ac28f011fe752e98901e714c /resources/vue | |
| parent | f637e7ae2d086941a11297ccc29ac273ad6759b0 (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.vue | 266 | ||||
| -rw-r--r-- | resources/vue/apps/CourseDateFormContent.vue | 227 | ||||
| -rw-r--r-- | resources/vue/base-components.js | 1 | ||||
| -rw-r--r-- | resources/vue/components/CourseDateRoomFieldset.vue | 310 | ||||
| -rw-r--r-- | resources/vue/components/Multiselect.vue | 13 | ||||
| -rw-r--r-- | resources/vue/components/StudipSelect.vue | 5 |
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> |
