From 680de640261a0b9005a6b3c084506d6abf51b433 Mon Sep 17 00:00:00 2001 From: Rasmus Fuhse Date: Fri, 19 Dec 2025 12:20:47 +0000 Subject: Resolve "Widget Aktiver Prozesse" Closes #5675 Merge request studip/studip!4299 --- app/controllers/running_processes.php | 46 ++++++++ .../6.2.4_step5675_running_processes_widget.php | 16 +++ lib/classes/Processes/ProcessProvider.php | 13 ++ lib/classes/Processes/Questionnaires.php | 86 ++++++++++++++ lib/classes/Processes/TimedFolders.php | 42 +++++++ lib/classes/RunningProcess.php | 37 ++++++ lib/modules/RunningProcessesWidget.php | 26 ++++ lib/plugins/core/RunningProcessPlugin.php | 11 ++ .../assets/stylesheets/scss/runningprocesses.scss | 64 ++++++++++ resources/assets/stylesheets/studip.scss | 1 + resources/vue/apps/RunningProcesses.vue | 131 +++++++++++++++++++++ resources/vue/utils/getRemainingTime.js | 65 ++++++++++ 12 files changed, 538 insertions(+) create mode 100644 app/controllers/running_processes.php create mode 100644 db/migrations/6.2.4_step5675_running_processes_widget.php create mode 100644 lib/classes/Processes/ProcessProvider.php create mode 100644 lib/classes/Processes/Questionnaires.php create mode 100644 lib/classes/Processes/TimedFolders.php create mode 100644 lib/classes/RunningProcess.php create mode 100644 lib/modules/RunningProcessesWidget.php create mode 100644 lib/plugins/core/RunningProcessPlugin.php create mode 100644 resources/assets/stylesheets/scss/runningprocesses.scss create mode 100644 resources/vue/apps/RunningProcesses.vue create mode 100644 resources/vue/utils/getRemainingTime.js diff --git a/app/controllers/running_processes.php b/app/controllers/running_processes.php new file mode 100644 index 0000000..988062e --- /dev/null +++ b/app/controllers/running_processes.php @@ -0,0 +1,46 @@ +processes = array_merge( + \Studip\Processes\Questionnaires::getProcesses(User::findCurrent()), + \Studip\Processes\TimedFolders::getProcesses(User::findCurrent()) + ); + $plugins = PluginManager::getInstance()->getPlugins('RunningProcessPlugin'); + foreach ($plugins as $plugin) { + $this->processes = array_merge($this->processes, $plugin->getRunningProcesses()); + } + + $this->contexts = []; + foreach ($this->processes as $process) { + if (!isset($this->contexts[$process->context_id])) { + $context = get_object_by_range_id($process->context_id); + + if ($context) { + if ($context instanceof Course) { + $avatar = CourseAvatar::getAvatar($process->context_id); + } else { + $avatar = InstituteAvatar::getAvatar($process->context_id); + } + $this->contexts[$process->context_id] = [ + 'id' => $process->context_id, + 'name' => (string) $context->name, + 'url' => URLHelper::getURL('dispatch.php/course/go', ['to' => $process->context_id]), + 'avatar' => $avatar->getURL(Avatar::SMALL), + ]; + } + } + } + + $this->render_vue_app( + Studip\VueApp::create('RunningProcesses') + ->withProps([ + 'contexts' => $this->contexts, + 'processes' => array_map(function ($process) { return $process->toArray(); }, $this->processes) + ]) + ); + } +} diff --git a/db/migrations/6.2.4_step5675_running_processes_widget.php b/db/migrations/6.2.4_step5675_running_processes_widget.php new file mode 100644 index 0000000..a68dcb8 --- /dev/null +++ b/db/migrations/6.2.4_step5675_running_processes_widget.php @@ -0,0 +1,16 @@ +fetchColumn("SELECT MAX(navigationpos) + 1 FROM plugins WHERE plugintype = 'PortalPlugin'"); + $sql = "INSERT INTO plugins (pluginclassname, pluginname, plugintype, enabled, navigationpos) VALUES (?)"; + DBManager::get()->execute($sql, [['RunningProcessesWidget', 'RunningProcessesWidget', 'PortalPlugin', 'yes', $pos]]); + + $sql = "INSERT INTO roles_plugins (roleid, pluginid) + SELECT roleid, ? FROM roles WHERE `system` = 'y' AND rolename != 'Nobody'"; + DBManager::get()->execute($sql, [DBManager::get()->lastInsertId()]); + } + +} diff --git a/lib/classes/Processes/ProcessProvider.php b/lib/classes/Processes/ProcessProvider.php new file mode 100644 index 0000000..fcce4bd --- /dev/null +++ b/lib/classes/Processes/ProcessProvider.php @@ -0,0 +1,13 @@ +prepare(" + SELECT `questionnaires`.* + FROM `questionnaires` + INNER JOIN `questionnaire_assignments` USING (`questionnaire_id`) + LEFT JOIN `seminar_user` ON (`seminar_user`.`Seminar_id` = `questionnaire_assignments`.`range_id` AND `questionnaire_assignments`.`range_type` = 'course') + LEFT JOIN `user_inst` ON (`user_inst`.`Institut_id` = `questionnaire_assignments`.`range_id` AND `questionnaire_assignments`.`range_type` = 'institute') + LEFT JOIN `statusgruppe_user` ON (`statusgruppe_user`.`statusgruppe_id` = `questionnaire_assignments`.`range_id` AND `questionnaire_assignments`.`range_type` = 'statusgruppe') + LEFT JOIN `statusgruppen` ON (`statusgruppen`.`statusgruppe_id` = `questionnaire_assignments`.`range_id` AND `questionnaire_assignments`.`range_type` = 'statusgruppe') + LEFT JOIN `seminar_user` AS `teacher` ON (`teacher`.`Seminar_id` = `statusgruppen`.`range_id` AND `questionnaire_assignments`.`range_type` = 'statusgruppe' AND `teacher`.`status` IN ('tutor', 'dozent')) + WHERE `questionnaires`.`startdate` <= UNIX_TIMESTAMP() + AND `questionnaires`.`stopdate` >= UNIX_TIMESTAMP() + AND `questionnaires`.`visible` = 1 + AND (`seminar_user`.`user_id` = :user_id OR `teacher`.`user_id` = :user_id OR `user_inst`.`user_id` = :user_id OR `statusgruppe_user`.`user_id` = :user_id) + GROUP BY `questionnaires`.`questionnaire_id` + "); + $statement->execute(['user_id' => $user->id]); + $questionnaires = $statement->fetchAll(\PDO::FETCH_ASSOC); + $result = []; + + foreach ($questionnaires as $questionnaire_data) { + $questionnaire = \Questionnaire::buildExisting($questionnaire_data); + if ($questionnaire->isViewable() && (!$questionnaire->isAnswered() || $questionnaire->isEditable())) { + foreach ($questionnaire->assignments as $assignment) { + if ($questionnaire->isAnswerable() || $questionnaire->isEditable()) { + + $answers = $questionnaire->countAnswers(); + if ($assignment->range_type === 'course') { + $allPersons = \CourseMember::countBySQL("`Seminar_id` = ?", [$assignment->range_id]); + $range_id = $assignment->range_id; + } elseif($assignment->range_type === 'institute') { + $allPersons = \InstituteMember::countBySQL("`Institut_id` = ?", [$assignment->range_id]); + $range_id = $assignment->range_id; + } elseif($assignment->range_type === 'statusgruppe') { + $allPersons = \StatusgruppeUser::countBySQL("`statusgruppe_id` = ?", [$assignment->range_id]); + $statusgroup = \Statusgruppen::find($assignment->range_id); + if ($statusgroup) { + $range_id = $statusgroup->range_id; + } + } + + $result[] = new \RunningProcess( + $range_id, + \Icon::create("vote"), + _('Fragebogen'), + $questionnaire->isEditable() + ? \URLHelper::getURL('dispatch.php/questionnaire/evaluate/'.$questionnaire->id) + : \URLHelper::getURL('dispatch.php/questionnaire/answer/'.$questionnaire->id), + $questionnaire->startdate, + $questionnaire->stopdate, + true, + $questionnaire->title, + $questionnaire->isEditable() ? $answers.'/'.$allPersons : '', + $questionnaire->isEditable() ? sprintf(_('Rücklaufquote: %s'), $answers.'/'.$allPersons) : '' + ); + } + } + } + } + return $result; + } +} diff --git a/lib/classes/Processes/TimedFolders.php b/lib/classes/Processes/TimedFolders.php new file mode 100644 index 0000000..1460183 --- /dev/null +++ b/lib/classes/Processes/TimedFolders.php @@ -0,0 +1,42 @@ + $user->id]); + $result = []; + foreach ($folders as $folder) { + $folderType = $folder->getTypedFolder(); + if ($folderType->isVisible($user->id) && ($folderType->end_time > 0)) { + $files = $folderType->getFiles(); + $result[] = new \RunningProcess( + $folder->range_id, + $folderType->getIcon(), + _('Zeitgesteuerter Ordner'), + \URLHelper::getURL('dispatch.php/course/files/index/'.$folder->id, ['cid' => $folder->range_id]), + $folderType->start_time, + $folderType->end_time, + false, + $folder->name, + $folderType->isReadable($user->id) && (count($files) > 0) ? sprintf(ngettext('%d Datei', '%d Dateien', count($files)), count($files)) : '' + ); + } + } + return $result; + } +} diff --git a/lib/classes/RunningProcess.php b/lib/classes/RunningProcess.php new file mode 100644 index 0000000..ca71068 --- /dev/null +++ b/lib/classes/RunningProcess.php @@ -0,0 +1,37 @@ +id = uniqid(); + } + + public function toArray() + { + return [ + 'id' => $this->id, + 'context_id' => $this->context_id, + 'icon' => $this->icon->asImagePath(), + 'type' => $this->type, + 'url' => $this->url, + 'begin' => $this->begin, + 'end' => $this->end, + 'dialog' => $this->dialog, + 'title' => $this->title, + 'additionalShortInfo' => $this->additionalShortInfo, + 'additionalInfoTitleTag' => $this->additionalInfoTitleTag + ]; + } +} diff --git a/lib/modules/RunningProcessesWidget.php b/lib/modules/RunningProcessesWidget.php new file mode 100644 index 0000000..54a4bd4 --- /dev/null +++ b/lib/modules/RunningProcessesWidget.php @@ -0,0 +1,26 @@ + _('Dieses Widget zeigt offene Prozesse, wie unbearbeitete Fragebögen oder zeitgesteuerte Dateiordner, aus Ihren Veranstaltungen oder Einrichtungen an.') + ]; + } + + function getPortalTemplate() + { + $controller = app(\Trails\Dispatcher::class)->load_controller('running_processes'); + $response = $controller->relayWithRedirect('running_processes/widget'); + $template = $GLOBALS['template_factory']->open('shared/string'); + $template->content = $response->body; + + return $template; + } +} diff --git a/lib/plugins/core/RunningProcessPlugin.php b/lib/plugins/core/RunningProcessPlugin.php new file mode 100644 index 0000000..e64e75c --- /dev/null +++ b/lib/plugins/core/RunningProcessPlugin.php @@ -0,0 +1,11 @@ + +
+ +
+ + + diff --git a/resources/vue/utils/getRemainingTime.js b/resources/vue/utils/getRemainingTime.js new file mode 100644 index 0000000..2d66606 --- /dev/null +++ b/resources/vue/utils/getRemainingTime.js @@ -0,0 +1,65 @@ +/** + * Berechnet die verbleibende Zeit eines Prozesses als formatierten String + * @param {Object} process - Der Prozess mit begin und end Timestamp (in Sekunden) + * @param {number} currentTime - Die aktuelle Zeit in Millisekunden (Date.now()) + * @param {Function} gettext - Die Gettext-Funktion für Übersetzungen + * @returns {string} - Formatierte verbleibende Zeit + */ +export function getRemainingTime(remainingSeconds, ngettext) { + if (remainingSeconds < 61) { + return ngettext( + '%{seconds} Sekunde', + '%{seconds} Sekunden', + remainingSeconds, + { minutes: remainingSeconds } + ); + } + if (remainingSeconds < 3601) { + //return gettext('%{minutes} Minuten', {minutes: Math.round(remainingSeconds / 60)}); + return ngettext( + '%{minutes} Sekunde', + '%{minutes} Sekunden', + Math.round(remainingSeconds / 60), + { minutes: Math.round(remainingSeconds / 60) } + ); + } + if (remainingSeconds < 86401) { + return ngettext( + '%{hours} Stunde', + '%{hours} Stunden', + Math.round(remainingSeconds / 3600), + { hours: Math.round(remainingSeconds / 3600) } + ); + } + if (remainingSeconds < 604801) { + return ngettext( + '%{days} Tag', + '%{days} Tage', + Math.round(remainingSeconds / 86400), + { days: Math.round(remainingSeconds / 86400) } + ); + } + if (remainingSeconds < 31536001) { + return ngettext( + '%{weeks} Woche', + '%{weeks} Wochen', + Math.round(remainingSeconds / 604800), + { weeks: Math.round(remainingSeconds / 604800) } + ); + } + if (remainingSeconds < 315360001) { + return ngettext( + '%{months} Monat', + '%{months} Monate', + Math.round(remainingSeconds / 2628000), + { months: Math.round(remainingSeconds / 2628000) } + ); + } + + return ngettext( + '%{years} Jahr', + '%{years} Jahre', + Math.round(remainingSeconds / 31536000), + { years: Math.round(remainingSeconds / 31536000) } + ); +} -- cgit v1.0