aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichaela Brückner <brueckner@data-quest.de>2025-06-26 09:51:32 +0200
committerDavid Siegfried <david.siegfried@uni-vechta.de>2025-06-26 07:51:32 +0000
commitd87fff87cd24cfcd7ad72a02af251ad890cf646e (patch)
tree20dc6e62a02b0ca1396998bcb90871ffa3994b79
parentf385c70d09166f6a41cc49922510daa1ec3402b4 (diff)
Resolve "Veranstaltungs-Stundenplan: Anzeigefilter für Terminkachel einbauen"
Closes #5585 Merge request studip/studip!4209
-rw-r--r--app/controllers/admin/courseplanning.php58
-rw-r--r--db/migrations/6.1.2_add_timetable_filters.php64
-rw-r--r--lib/classes/InstituteCalendarHelper.php30
-rw-r--r--lib/models/CourseDate.php13
-rw-r--r--resources/assets/javascripts/lib/fullcalendar.js21
-rw-r--r--resources/vue/apps/CoursePlanningTileFilter.vue66
6 files changed, 243 insertions, 9 deletions
diff --git a/app/controllers/admin/courseplanning.php b/app/controllers/admin/courseplanning.php
index 81e90a5..100088e 100644
--- a/app/controllers/admin/courseplanning.php
+++ b/app/controllers/admin/courseplanning.php
@@ -72,6 +72,12 @@ class Admin_CourseplanningController extends AuthenticatedController
Icon::create('file-pdf')
);
+ Sidebar::get()->getWidget('actions')->addLink(
+ _('Ansichtsoptionen'),
+ $this->tilefilterURL('overview'),
+ Icon::create('admin')
+ )->asDialog('size=400x350');
+
$this->courses = $this->getFilteredCourses();
$this->events = InstituteCalendarHelper::getEvents(
$this->courses,
@@ -128,6 +134,12 @@ class Admin_CourseplanningController extends AuthenticatedController
Icon::create('admin')
)->asDialog('size=auto');
+ Sidebar::get()->getWidget('actions')->addLink(
+ _('Ansichtsoptionen'),
+ $this->tilefilterURL('weekday', $day_of_week),
+ Icon::create('admin')
+ )->asDialog('size=400x350');
+
$this->cal_date = $cal_date;
$this->courses = $this->getFilteredCourses();
$this->events = InstituteCalendarHelper::getEvents($this->courses, $GLOBALS['user']->cfg->MY_INSTITUTES_DEFAULT, $this->semester, $day_of_week);
@@ -887,4 +899,50 @@ class Admin_CourseplanningController extends AuthenticatedController
}
$this->redirect('admin/courseplanning/index');
}
+
+ /**
+ * Sets filters for the title on the date tile
+ */
+ public function tilefilter_action($view = null, $weekday = null)
+ {
+ PageLayout::setTitle(_('Angezeigte Veranstaltungsdaten'));
+
+ $config = User::findCurrent()->getConfiguration();
+
+ $this->render_vue_app(
+ Studip\VueApp::create('CoursePlanningTileFilter')
+ ->withProps([
+ 'view' => $view,
+ 'weekday' => $weekday,
+ 'config' => [
+ 'course_number' => (bool) $config->getValue('TIMETABLE_COURSE_NUMBER_VISIBLE'),
+ 'course_name' => (bool) $config->getValue('TIMETABLE_COURSE_NAME_VISIBLE'),
+ 'lecturers' => (bool) $config->getValue('TIMETABLE_LECTURERS_VISIBLE'),
+ 'rooms' => (bool) $config->getValue('TIMETABLE_ROOMS_VISIBLE'),
+ ]
+ ])
+ );
+
+ $this->view = $view;
+ $this->weekday = $weekday;
+ $this->config = UserConfig::get($GLOBALS['user']->id);
+ }
+
+ public function store_tilefilter_action($view = null, $weekday = null)
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $GLOBALS['user']->cfg->store('TIMETABLE_COURSE_NUMBER_VISIBLE', Request::bool('course_number'));
+ $GLOBALS['user']->cfg->store('TIMETABLE_COURSE_NAME_VISIBLE', Request::bool('course_name'));
+ $GLOBALS['user']->cfg->store('TIMETABLE_LECTURERS_VISIBLE', Request::bool('lecturers'));
+ $GLOBALS['user']->cfg->store('TIMETABLE_ROOMS_VISIBLE', Request::bool('rooms'));
+
+ if ($view === 'overview') {
+ $this->redirect('admin/courseplanning/index');
+ } elseif ($view === 'weekday') {
+ $this->redirect('admin/courseplanning/weekday/' . $weekday);
+ }
+
+ PageLayout::postSuccess(_('Ihre Einstellungen wurden gespeichert.'));
+ }
}
diff --git a/db/migrations/6.1.2_add_timetable_filters.php b/db/migrations/6.1.2_add_timetable_filters.php
new file mode 100644
index 0000000..14869b5
--- /dev/null
+++ b/db/migrations/6.1.2_add_timetable_filters.php
@@ -0,0 +1,64 @@
+<?php
+
+final class AddTimetableFilters extends Migration
+{
+ public function description()
+ {
+ return 'Adds config fields for filtering content on timetable tiles';
+ }
+
+
+ public function up()
+ {
+ $query = "INSERT IGNORE INTO `config` (
+ `field`, `value`, `type`, `range`, `section`,
+ `mkdate`, `chdate`, `description`
+ ) VALUES (
+ :field, :value, :type, 'user', '',
+ UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), :description
+ )";
+ $statement = DBManager::get()->prepare($query);
+
+ $statement->execute([
+ ':field' => 'TIMETABLE_COURSE_NUMBER_VISIBLE',
+ ':value' => 1,
+ ':type' => 'int',
+ ':description' => 'Soll die Veranstaltungs-Nummer auf dem Stundenplaneintrag sichtbar sein?',
+ ]);
+ $statement->execute([
+ ':field' => 'TIMETABLE_COURSE_NAME_VISIBLE',
+ ':value' => 1,
+ ':type' => 'int',
+ ':description' => 'Soll der Veranstaltungs-Titel auf dem Stundenplaneintrag sichtbar sein?',
+ ]);
+ $statement->execute([
+ ':field' => 'TIMETABLE_LECTURERS_VISIBLE',
+ ':value' => 0,
+ ':type' => 'boolean',
+ ':description' => 'Sollen die Dozenten einer Veranstaltung auf dem Stundenplaneintrag sichtbar sein?',
+ ]);
+ $statement->execute([
+ ':field' => 'TIMETABLE_ROOMS_VISIBLE',
+ ':value' => 0,
+ ':type' => 'boolean',
+ ':description' => 'Soll der Raum einer Veranstaltung auf dem Stundenplaneintrag sichtbar sein?',
+ ]);
+
+ }
+
+ public function down()
+ {
+ DBManager::get()->exec("
+ DELETE `config`, `config_values`
+ FROM `config`
+ LEFT JOIN `config_values` USING (`field`)
+ WHERE `config`.`field` IN (
+ 'TIMETABLE_COURSE_NUMBER_VISIBLE',
+ 'TIMETABLE_COURSE_NAME_VISIBLE',
+ 'TIMETABLE_LECTURERS_VISIBLE',
+ 'TIMETABLE_ROOMS_VISIBLE'
+ )
+ ");
+
+ }
+}
diff --git a/lib/classes/InstituteCalendarHelper.php b/lib/classes/InstituteCalendarHelper.php
index 343bc76..13677ea 100644
--- a/lib/classes/InstituteCalendarHelper.php
+++ b/lib/classes/InstituteCalendarHelper.php
@@ -436,10 +436,22 @@ class InstituteCalendarHelper
}
}
+ $next_single_date = CourseDate::getNextDateByMetadate($cycle_date->metadate_id);
+ if ($next_single_date) {
+ $room_name = $next_single_date->getRoomName() ?: _('ohne Raumangabe');
+ }
+
+ $fields = [
+ 'course_number' => UserConfig::get($GLOBALS['user']->id)->TIMETABLE_COURSE_NUMBER_VISIBLE ? ($course->veranstaltungsnummer ?: _('(keine VA-Nummer)')) : null,
+ 'course_name' => UserConfig::get($GLOBALS['user']->id)->TIMETABLE_COURSE_NAME_VISIBLE ? $course->getFullName('name') : null,
+ 'lecturers' => UserConfig::get($GLOBALS['user']->id)->TIMETABLE_LECTURERS_VISIBLE ? self::getLecturers($course) : null,
+ 'room' => UserConfig::get($GLOBALS['user']->id)->TIMETABLE_ROOMS_VISIBLE ? $room_name : null
+ ];
+
$events[] = [
'resourceId' => $resource_column,
'id' => $cycle_date->id,
- 'title' => $name,
+ 'title' => empty($fields) ? $name : '',
'start' => $start,
'end' => $end,
'textColor' => $textcolor,
@@ -456,6 +468,8 @@ class InstituteCalendarHelper
'tooltip' => self::getCycleInfos($course, $cycle_date),
'icon' => $is_start_editable ? '' : 'lock-locked',
'conform' => $conform,
+ // custom props (event.extendedProps)
+ 'content_fields' => $fields,
];
}
}, array_keys($courses));
@@ -702,6 +716,20 @@ class InstituteCalendarHelper
return $info_string;
}
+ private static function getLecturers(Course $course): array
+ {
+ $dozenten = [];
+ $lecturers = '';
+ foreach (CourseMember::findByCourseAndStatus($course->id, 'dozent') as $cmember) {
+ $dozenten[$cmember->user->user_id] = $cmember->user->getFullName();
+ }
+ if ($dozenten) {
+ $lecturers .= implode(', ', $dozenten) . "\n";
+ }
+
+ return $lecturers;
+ }
+
public static function getBackgroundEvents($start = null)
{
$datetime = new DateTime();
diff --git a/lib/models/CourseDate.php b/lib/models/CourseDate.php
index 0fb9e08..038a120 100644
--- a/lib/models/CourseDate.php
+++ b/lib/models/CourseDate.php
@@ -740,5 +740,18 @@ class CourseDate extends SimpleORMap implements PrivacyObject, Event
);
}
+ /**
+ * returns the next single date within a given cycle
+ */
+ public static function getNextDateByMetadate($metadate_id): ?self
+ {
+ return self::findOneBySQL(
+ "`metadate_id` = :metadate_id
+ AND `date` >= UNIX_TIMESTAMP()
+ ORDER BY `date`",
+ ['metadate_id' => $metadate_id]
+ );
+ }
+
//End of Event interface implementation.
}
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js
index f4e2826..350c72f 100644
--- a/resources/assets/javascripts/lib/fullcalendar.js
+++ b/resources/assets/javascripts/lib/fullcalendar.js
@@ -579,14 +579,19 @@ class Fullcalendar
if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
$(eventElement).attr('title', event.extendedProps.tooltip);
- $(eventElement).find('.fc-title').html(
- $('<div>').css({
- width: 'calc(100% - 21px)',
- height: '100%',
- wordBreak: 'break-word'
- }).text(eventElement.text)
- );
- $(eventElement).find('.fc-title').append(
+ if (event.extendedProps.content_fields) {
+ for (const [css_class, field] of Object.entries(event.extendedProps.content_fields)) {
+ $(eventElement).find('.fc-content').append(
+ $('<div>').css({
+ width: 'calc(100% - 21px)',
+ height: '100%',
+ wordBreak: 'break-word'
+ }).text(field)
+ .addClass(css_class + ' fc-title')
+ );
+ }
+ }
+ $(eventElement).find('.fc-content').append(
$('<button class="event-colorpicker">').addClass(iconColor)
);
} else {
diff --git a/resources/vue/apps/CoursePlanningTileFilter.vue b/resources/vue/apps/CoursePlanningTileFilter.vue
new file mode 100644
index 0000000..0ac69b1
--- /dev/null
+++ b/resources/vue/apps/CoursePlanningTileFilter.vue
@@ -0,0 +1,66 @@
+<template>
+ <form method="post" :action="storeURL" class="default">
+ <input type="hidden" :name="csrf.name" :value="csrf.value">
+ <input v-for="(_, key) in items"
+ :key="`input-${key}`"
+ type="hidden"
+ :name="key"
+ :value="checkboxes[key] ? 1 : 0"
+ >
+
+ <fieldset>
+ <legend>{{ $gettext('Angezeigte Veranstaltungsdaten') }}</legend>
+
+ <label v-for="(label, key) in items" :key="key">
+ <input :name="key"
+ type="checkbox"
+ v-model="checkboxes[key]"
+ :disabled="isDisabled(key)"
+ >
+ {{ label }}
+ </label>
+ </fieldset>
+
+ <footer data-dialog-button>
+ <button type="submit" class="accept button">
+ {{ $gettext('Speichern') }}
+ </button>
+ </footer>
+ </form>
+</template>
+<script setup lang="ts">
+import { computed, reactive, unref } from "vue";
+import { $gettext } from "../../assets/javascripts/lib/gettext";
+
+type ValidField = 'course_number' | 'course_name' | 'lecturers' | 'rooms';
+
+const props = defineProps({
+ view: [String, null],
+ weekday: [String, null],
+ config: Object,
+});
+
+const checkboxes = reactive({...unref(props.config)});
+if (!checkboxes.course_number && !checkboxes.course_name) {
+ checkboxes.course_name = true;
+}
+
+const csrf = computed(() => window.STUDIP.CSRF_TOKEN);
+
+const items: Record<ValidField, string> = {
+ course_number: $gettext('Veranstaltungsnummer'),
+ course_name: $gettext('Veranstaltungstitel'),
+ lecturers: $gettext('Lehrende'),
+ rooms: $gettext('Raum'),
+};
+
+const storeURL = window.STUDIP.URLHelper.getURL(`dispatch.php/admin/courseplanning/store_tilefilter/${props.view}/${props.weekday}`, {}, true);
+
+function isDisabled(f: string): boolean {
+ const field = f as ValidField;
+
+ return (field === 'course_number' && !checkboxes.course_name)
+ || (field === 'course_name' && !checkboxes.course_number);
+
+}
+</script>