Compare commits

..

3 Commits

Author SHA1 Message Date
ma0068 3689c64b7b make also notizlayout configurable 2026-07-09 11:01:00 +02:00
ma0068 a4c5f9946f change doku 2026-07-07 16:56:35 +02:00
ma0068 81f0b7a4bf make showErweitert configurable 2026-07-07 16:43:01 +02:00
155 changed files with 4514 additions and 13310 deletions
+1 -12
View File
@@ -29,7 +29,7 @@ $config['ALLOWED_NOTEN_ABGABETOOL'] = [10, 14]; // tbl_note pk
// benotete projektarbeiten sperren weitere terminanlage & bearbeitung, diese noten sind ausnahmen dieser Regel
// wie zB "Nicht beurteilt" & "Noch nicht eingetragen"
$config['NONFINAL_NOTEN_ABGABETOOL'] = [9];
$config['beurteilung_link_fallback'] = 'cis/private/lehre/projektbeurteilungDocumentExport.php?projektarbeit_id=?&betreuerart_kurzbz=?&person_id=?';
$config['beurteilung_link_fallback'] = 'addons/fhtw/content/projektbeurteilung/projektbeurteilungDocumentExport.php?projektarbeit_id=?&betreuerart_kurzbz=?&person_id=?';
$config['PROJEKTARBEITSBEURTEILUNG_MAIL_BASELINK_ERSTBEGUTACHTER'] = 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/ProjektarbeitsbeurteilungErstbegutachter';
$config['PROJEKTARBEITSBEURTEILUNG_MAIL_BASELINK_ZWEITBEGUTACHTER'] = 'index.ci.php/extensions/FHC-Core-Projektarbeitsbeurteilung/ProjektarbeitsbeurteilungErstbegutachter';
@@ -38,19 +38,8 @@ $config['SIGNATUR_CHECK_PAABGABETYPEN'] = ['end'];
// to be used as "https://moodle.technikum-wien.at/course/view.php?idnumber=dl{$stg_kz}" for stg specific moodle routing
$config['STG_MOODLE_LINK'] = 'https://moodle.technikum-wien.at/course/view.php?idnumber=dl';
// TODO: check if these links change if the file changes and how to better retrieve the link?
$config['SIGNATUR_INFO_LINK_GERMAN'] = 'https://cis.technikum-wien.at/cms/dms.php?id=214779';
$config['SIGNATUR_INFO_LINK_ENGLISH'] = 'https://cis.technikum-wien.at/cms/dms.php?id=264256';
$config['ASSISTENZ_SAMMELMAIL_BUTTON_STUDENT'] = true;
$config['ASSISTENZ_SAMMELMAIL_BUTTON_BETREUER'] = true;
$config['BETREUER_SAMMELMAIL_BUTTON_STUDENT'] = true;
$config['MULTIEDIT_TABLE'] = true;
$config['STUDENT_EDIT_PROJEKTARBEIT_TITLE'] = true;
$config['CONFETTI_ON_ENDUPLOAD'] = true;
-3
View File
@@ -64,9 +64,6 @@ $route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1';
$route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1';
$route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1';
$route['Cis/MyLv/.*'] = 'Cis/MyLv/index/$1';
$route['Cis/OtherLvPlan/.*'] = 'Cis/OtherLvPlan/index/$1';
//Route for LV Plan Stg/Semester/Verband/Gruppe
$route['Cis/StgOrgLvPlan/.*'] = 'Cis/StgOrgLvPlan/index/$1';
$route['Abgabetool/Assistenz'] = 'Cis/Abgabetool/Assistenz';
$route['Abgabetool/Assistenz/(:any)'] = 'Cis/Abgabetool/Assistenz/$1';
+1 -2
View File
@@ -22,9 +22,8 @@ unset($config['student']['searchfields']['email']);
unset($config['student']['searchfields']['tel']);
$config['student']['resultfields'] = [
"s.student_uid AS uid",
"s.matrikelnr AS personenkennzeichen",
"s.matrikelnr",
"p.person_id",
"p.matr_nr AS matrikelnummer",
"(p.vorname || ' ' || p.nachname) AS name",
"ARRAY[s.student_uid || '@' || '" . DOMAIN . "'] AS email",
"CASE
+7 -1
View File
@@ -60,7 +60,13 @@ $config['tabs'] =
],
'notes' => [
//if true, the count of Messages will be shown in the header of the Tab Messages
'showCountNotes' => true
'showCountNotes' => true,
//if true: section with following fields will be displayed: 'verfasser', 'bearbeiter', 'von', 'bis' and 'erledigt'
'showErweitert' => true,
//notizlayout: choose of "classicFas", "twoColumnsFormLeft", twoColumnsFormRight, popupModal"
'notizLayout' => 'popupModal'
],
'combinePeople' => [
//multitab should only be shown with this length of selection
+26 -5
View File
@@ -31,8 +31,12 @@ class Abgabetool extends Auth_Controller
{
// TODO: routing from index based on berechtigung?
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'Abgabetool']);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Abgabetool']);
} else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'Abgabetool']);
}
@@ -40,8 +44,12 @@ class Abgabetool extends Auth_Controller
public function Student($student_uid_prop = '')
{
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'AbgabetoolStudent']);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'AbgabetoolStudent']);
} else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolStudent', 'student_uid_prop' => $student_uid_prop]);
}
@@ -49,8 +57,12 @@ class Abgabetool extends Auth_Controller
public function Mitarbeiter()
{
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'AbgabetoolMitarbeiter']);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'AbgabetoolMitarbeiter']);
} else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolMitarbeiter']);
}
@@ -58,8 +70,13 @@ class Abgabetool extends Auth_Controller
public function Assistenz($stg_kz_prop = '')
{
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'AbgabetoolAssistenz']);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'AbgabetoolAssistenz']);
} else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolAssistenz', 'stg_kz_prop' => $stg_kz_prop]);
}
@@ -67,8 +84,12 @@ class Abgabetool extends Auth_Controller
public function Deadlines()
{
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'DeadlinesOverview']);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'DeadlinesOverview']);
} else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'DeadlinesOverview']);
}
+1 -1
View File
@@ -40,7 +40,7 @@ class Auth extends FHC_Controller
if ($this->form_validation->run())
{
redirect($this->authlib->getLandingPage('/Cis4'));
redirect($this->authlib->getLandingPage('/CisVue/Dashboard'));
}
else
{
+7 -1
View File
@@ -28,6 +28,12 @@ class LvPlan extends Auth_Controller
*/
public function index()
{
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'LvPlan']);
$viewData = array(
'uid'=>getAuthUID(),
'timezone' => $this->config->item('timezone')
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'LvPlan']);
}
}
+6 -1
View File
@@ -26,6 +26,11 @@ class MyLv extends Auth_Controller
*/
public function index()
{
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'MyLv']);
$viewData = array(
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'MyLv']);
}
}
+8 -2
View File
@@ -27,7 +27,13 @@ class MyLvPlan extends Auth_Controller
* @return void
*/
public function index()
{
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'MyLvPlan']);
{
$viewData = array(
'uid'=>getAuthUID(),
'timezone' => $this->config->item('timezone')
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'MyLvPlan']);
}
}
@@ -1,34 +0,0 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
/**
*
*/
class OtherLvPlan extends Auth_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/other_lv_plan:r']
]);
// Load Config
$this->load->config('calendar');
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
/**
* @return void
*/
public function index()
{
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'OtherLvPlan']);
}
}
+39 -19
View File
@@ -55,7 +55,15 @@ class Profil extends Auth_Controller
*/
public function index()
{
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'profilIndex']);
$this->load->library('ProfilLib');
$profil_data = $this->profillib->getView(getAuthUID());
$profil_data = hasData($profil_data) ? getData($profil_data) : null;
$viewData = array(
'editable'=>true,
'profil_data' => $profil_data,
);
$this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'profilIndex']);
}
/**
@@ -65,13 +73,23 @@ class Profil extends Auth_Controller
*/
public function View($uid)
{
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'profilViewUid']);
$this->load->library('ProfilLib');
$profil_data = $this->profillib->getView($uid);
$profil_data = hasData($profil_data) ? getData($profil_data) : null;
$viewData = array (
'uid' => $uid,
'profil_data'=>$profil_data,
);
if($uid == getAuthUID()){
$viewData['editable'] = true;
}
$this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'profilViewUid']);
}
/**
* checks whether a specific userID is a student or not (foreword declaration of the function isStudent in Student_model.php)
* checks whether a specific userID is a mitarbeiter or not (foreword declaration of the function isMitarbeiter in Mitarbeiter_model.php)
* @access public
* @param $uid the userID used to check if it is a student
* @param $uid the userID used to check if it is a mitarbeiter
* @return boolean
*/
public function isStudent($uid)
@@ -101,7 +119,7 @@ class Profil extends Auth_Controller
}
/**
* gets the adressen that are marked as zustell from the currently logged in user
* gets the adressen that are marked as zustell from the currenlty logged in user
* @access public
* @return array a list of adresse_id's
*/
@@ -244,23 +262,23 @@ class Profil extends Auth_Controller
$this->GemeindeModel->addDistinct();
$this->GemeindeModel->addSelect(["name"]);
if ($nation == "A") {
if (isset($zip) && $zip > 999 && $zip < 32000) {
if (isset($zip) && $zip > 999 && $zip < 32000) {
$gemeinde_res = $this->GemeindeModel->loadWhere(['plz' => $zip]);
if (isError($gemeinde_res)) {
show_error("error while trying to query bis.tbl_gemeinde");
$gemeinde_res = $this->GemeindeModel->loadWhere(['plz' => $zip]);
if (isError($gemeinde_res)) {
show_error("error while trying to query bis.tbl_gemeinde");
}
$gemeinde_res = hasData($gemeinde_res) ? getData($gemeinde_res) : null;
$gemeinde_res = array_map(function ($obj) {
return $obj->name;
}, $gemeinde_res);
echo json_encode($gemeinde_res);
} else {
echo json_encode(error("ortschaftskennziffer code was not valid"));
}
$gemeinde_res = hasData($gemeinde_res) ? getData($gemeinde_res) : null;
$gemeinde_res = array_map(function ($obj) {
return $obj->name;
}, $gemeinde_res);
echo json_encode($gemeinde_res);
} else {
echo json_encode(error("ortschaftskennziffer code was not valid"));
}
} else {
echo json_encode(error("Nation was not 'A' (Austria)"));
echo json_encode(error("Nation was not 'A' (Austria)"));
}
}
@@ -732,4 +750,6 @@ class Profil extends Auth_Controller
$zutrittskarte_ausgegebenam = str_replace("-", ".", $zutrittskarte_ausgegebenam);
return $zutrittskarte_ausgegebenam;
}
}
+6 -1
View File
@@ -25,6 +25,11 @@ class Raumsuche extends Auth_Controller
*/
public function index()
{
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'Raumsuche']);
$viewData = array(
'uid'=>getAuthUID(),
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Raumsuche']);
}
}
@@ -1,33 +0,0 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
*/
class StgOrgLvPlan extends Auth_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r']
]);
// Load Config
$this->load->config('calendar');
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
/**
* @return void
*/
public function index()
{
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'StgOrgLvPlan']);
}
}
+4 -1
View File
@@ -29,7 +29,10 @@ class Studium extends Auth_Controller
*/
public function index()
{
$this->load->view('CisRouterView/CisRouterView.php',['route' => 'studium']);
$viewData = array(
);
$this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'studium']);
}
+15 -6
View File
@@ -1,7 +1,6 @@
<?php
if (!defined('BASEPATH'))
exit('No direct script access allowed');
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
@@ -14,9 +13,9 @@ class Cis4 extends Auth_Controller
public function __construct()
{
parent::__construct(
array(
'index' => 'basis/cis:r'
)
array(
'index' => 'basis/cis:r'
)
);
// Load Config
@@ -31,6 +30,16 @@ class Cis4 extends Auth_Controller
*/
public function index()
{
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'FhcDashboard']);
$this->load->model('person/Person_model', 'PersonModel');
$personData = getData($this->PersonModel->getByUid(getAuthUID()))[0];
$viewData = array(
'uid' => getAuthUID(),
'name' => $personData->vorname,
'person_id' => $personData->person_id,
'timezone' => $this->config->item('timezone')
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'FhcDashboard']);
}
}
@@ -0,0 +1,43 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
*/
class Dashboard extends Auth_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct(
array(
'index' => 'dashboard/benutzer:r'
)
);
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
/**
* @return void
*/
public function index()
{
$this->load->model('person/Person_model','PersonModel');
$personData = getData($this->PersonModel->getByUid(getAuthUID()))[0];
$viewData = array(
'uid' => getAuthUID(),
'name' => $personData->vorname,
'person_id' => $personData->person_id
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData]);
}
}
File diff suppressed because it is too large Load Diff
@@ -18,8 +18,6 @@
if (! defined('BASEPATH')) exit('No direct script access allowed');
use \DateTime as DateTime;
class Bookmark extends FHCAPI_Controller
{
@@ -30,162 +28,111 @@ class Bookmark extends FHCAPI_Controller
{
parent::__construct([
'getBookmarks' => self::PERM_LOGGED,
'delete' => self::PERM_LOGGED,
'insert' => self::PERM_LOGGED,
'delete' => self::PERM_LOGGED,
'insert' => self::PERM_LOGGED,
'update' => self::PERM_LOGGED,
'changeOrder' => self::PERM_LOGGED
]);
]);
$this->load->model('dashboard/Bookmark_model', 'BookmarkModel');
$this->uid = getAuthUID();
$this->pid = getAuthPersonID();
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* gets the bookmarks associated to a user
/**
* gets the bookmarks associated to a user
* @access public
* @return void
*/
public function getBookmarks()
{
$this->BookmarkModel->addOrder("sort");
$this->BookmarkModel->addOrder("bookmark_id");
$bookmarks = $this->BookmarkModel->loadWhere(["uid"=>$this->uid]);
$bookmarks = $this->getDataOrTerminateWithError($bookmarks);
$bookmarks = $this->getDataOrTerminateWithError($bookmarks);
$this->terminateWithSuccess($bookmarks);
}
$this->terminateWithSuccess($bookmarks);
}
/**
* deletes bookmark from associated user
/**
* deletes bookmark from associated user
* @access public
* @return void
*/
public function delete($bookmark_id)
public function delete($bookmark_id)
{
$bookmark = $this->BookmarkModel->load($bookmark_id);
$bookmark = $this->BookmarkModel->load($bookmark_id);
$bookmark = current($this->getDataOrTerminateWithError($bookmark));
$bookmark = current($this->getDataOrTerminateWithError($bookmark));
// only delete bookmark if the user is the owner of the bookmark
if ($bookmark->uid == $this->uid || $this->permissionlib->isBerechtigt('admin')) {
$delete_result = $this->BookmarkModel->delete($bookmark_id);
// only delete bookmark if the user is the owner of the bookmark
if($bookmark->uid == $this->uid || $this->permissionlib->isBerechtigt('admin')){
$delete_result = $this->getDataOrTerminateWithError($delete_result);
$delete_result = $this->BookmarkModel->delete($bookmark_id);
$this->terminateWithSuccess($delete_result);
} else {
$this->_outputAuthError(['delete' => ['admin:rw']]);
}
}
$delete_result = $this->getDataOrTerminateWithError($delete_result);
/**
* inserts new bookmark into the bookmark table
$this->terminateWithSuccess($delete_result);
}else{
$this->_outputAuthError(['delete' => ['admin:rw']]);
}
}
/**
* inserts new bookmark into the bookmark table
* @access public
* @return void
*/
public function insert()
public function insert()
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_rules('url', 'URL', 'required|valid_url|max_length[511]');
$this->form_validation->set_rules('title', 'Title', 'required|max_length[255]');
if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array());
// form validation
$this->load->library('form_validation');
$this->form_validation->set_rules('url', 'URL', 'required|valid_url|max_length[511]');
$this->form_validation->set_rules('title', 'Title', 'required|max_length[255]');
if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$url = $this->input->post('url', true);
$title = $this->input->post('title', true);
$tag = $this->input->post('tag', true);
if (is_array($tag)) {
$tag = json_encode($tag); // convert PHP array to JSON string
}
$sort = $this->input->post('sort', true);
$url = $this->input->post('url',true);
$title = $this->input->post('title',true);
$tag = $this->input->post('tag', true);
$insert_into_result = $this->BookmarkModel->insert([
'uid' => $this->uid,
'url' => $url,
'title' => $title,
'tag' => $tag,
'insertvon' => $this->uid,
'updateamum' => null,
'updatevon' => null,
'sort' => $sort
]);
$insert_into_result = $this->BookmarkModel->insert(['uid'=>$this->uid, 'url'=>$url, 'title'=>$title,'tag'=>$tag, 'insertvon'=>$this->uid, 'updateamum'=>NULL, 'updatevon'=>NULL]);
$insert_into_result = $this->getDataOrTerminateWithError($insert_into_result);
$insert_into_result = $this->getDataOrTerminateWithError($insert_into_result);
$this->terminateWithSuccess($insert_into_result);
}
$this->terminateWithSuccess($insert_into_result);
}
/**
* updates bookmark in the bookmark table
* updates bookmark in the bookmark table
* @access public
* @return void
*/
public function update($bookmark_id)
public function update($bookmark_id)
{
// form validation
$this->load->library('form_validation');
$this->form_validation->set_rules('url', 'URL', 'required|valid_url|max_length[511]');
$this->form_validation->set_rules('title', 'Title', 'required|max_length[255]');
if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array());
// form validation
$this->load->library('form_validation');
$this->form_validation->set_rules('url', 'URL', 'required|valid_url|max_length[511]');
$this->form_validation->set_rules('title', 'Title', 'required|max_length[255]');
if($this->form_validation->run() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$url = $this->input->post('url', true);
$title = $this->input->post('title', true);
$tag = $this->input->post('tag', true);
if (is_array($tag)) {
$tag = json_encode($tag);
}
$url = $this->input->post('url',true);
$title = $this->input->post('title',true);
$now = new DateTime();
$now = $now->format('Y-m-d H:i:s');
$update_result = $this->BookmarkModel->update($bookmark_id, [
'url' => $url,
'title' => $title,
'tag' => $tag,
'updateamum' => $now
]);
$update_result = $this->BookmarkModel->update($bookmark_id,['url'=>$url, 'title'=>$title,'updateamum'=>$now]);
$update_result = $this->getDataOrTerminateWithError($update_result);
$update_result = $this->getDataOrTerminateWithError($update_result);
$this->terminateWithSuccess($update_result);
}
$this->terminateWithSuccess($update_result);
/**
* changes sort of two bookmarks in the bookmark table
* @access public
* @return void
*/
public function changeOrder($bookmark_id1, $bookmark_id2)
{
$update_result = [];
$result1 = $this->BookmarkModel->load($bookmark_id1);
$data1 = $this->getDataOrTerminateWithError($result1);
$sort1 = current($data1)->sort;
$result2 = $this->BookmarkModel->load(["bookmark_id"=>$bookmark_id2]);
$data2 = $this->getDataOrTerminateWithError($result2);
$sort2 = current($data2)->sort;
$update_result1 = $this->BookmarkModel->update($bookmark_id1, [
'sort' => $sort2
]);
$update_result[] = $this->getDataOrTerminateWithError($update_result1);
$update_result2 = $this->BookmarkModel->update($bookmark_id2, [
'sort' => $sort1
]);
$update_result[] = $this->getDataOrTerminateWithError($update_result2);
$this->terminateWithSuccess($update_result);
}
}
}
@@ -27,7 +27,7 @@ class Cis4FhcApi extends FHCAPI_Controller
public function __construct()
{
parent::__construct([
'dashboardViewData' => self::PERM_LOGGED,
'getViewData' => self::PERM_LOGGED,
]);
}
@@ -36,22 +36,17 @@ class Cis4FhcApi extends FHCAPI_Controller
// Public methods
/**
* retrieves view data for dashboard view
* @access public
* @param $uid the userID for which profile is being viewed, null or missing value implies one's own profile
*/
public function dashboardViewData()
* fetches ViewData
*/
public function getViewData()
{
$this->load->model('person/Person_model','PersonModel');
$personData = getData($this->PersonModel->getByUid(getAuthUID()))[0];
$this->load->config('calendar');
$viewData = array(
'uid' => getAuthUID(),
'name' => $personData->vorname,
'person_id' => $personData->person_id,
'timezone' => $this->config->item('timezone'),
'person_id' => $personData->person_id
);
$this->terminateWithSuccess($viewData);
@@ -16,13 +16,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (!defined('BASEPATH'))
exit('No direct script access allowed');
if (! defined('BASEPATH')) exit('No direct script access allowed');
class CisMenu extends FHCAPI_Controller
{
/**
* Object initialization
*/
@@ -32,95 +31,28 @@ class CisMenu extends FHCAPI_Controller
'getMenu' => self::PERM_LOGGED,
]);
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
/**
* fetches the menu for CIS from the database based on the userLanguage
*/
public function getMenu()
public function getMenu()
{
$this->load->model('content/Content_model', 'ContentModel');
$this->load->config('cis');
$cis4_content_id = $this->config->item('cis_menu_root_content_id');
$result = $this->ContentModel->getMenu($cis4_content_id, getAuthUID(), getUserLanguage());
$cis4_content_id =$this->config->item('cis_menu_root_content_id');
$result = $this->ContentModel->getMenu($cis4_content_id, getAuthUID(),getUserLanguage());
$result = $this->getDataOrTerminateWithError($result);
$menu = $result->childs ?? [];
$menu = $this->generateUrlsForMenuItems($menu);
$this->terminateWithSuccess($menu);
}
private function generateUrlsForMenuItems($menuItems)
{
return array_map(
function ($menuItem) {
return $this->generateUrlForMenuItem($menuItem);
},
$menuItems
);
}
private function generateUrlForMenuItem($menuItem)
{
$menuItem->url = $this->menuItemUrlHelper($menuItem);
unset($menuItem->content);
if ($menuItem->childs && count($menuItem->childs)) {
$menuItem->childs = $this->generateUrlsForMenuItems($menuItem->childs);
}
return $menuItem;
}
private function menuItemUrlHelper($menuItem)
{
if ($menuItem->template_kurzbz !== 'redirect') {
return site_url("/CisVue/Cms/content/" . $menuItem->content_id);
}
if (!$menuItem->content || !mb_strlen($menuItem->content)) {
return '';
}
$doc = new DOMDocument();
$doc->loadXML($menuItem->content);
$urlElem = $doc->getElementsByTagName('url')->item(0);
if (!$urlElem) {
return '';
}
$url = $urlElem->textContent;
if (strpos($url, '../cms/news.php') !== false) {
$newsRegex = '/^\.\.\/cms\/news\.php/';
$url = preg_replace($newsRegex, site_url("/CisVue/Cms/news"), $url);
}
if (strpos($url, '../cms/content.php?') !== false) {
$contentRegex = '/^\.\.\/cms\/content\.php\?content_id=([0-9]+)/';
$matches = [];
preg_match($contentRegex, $url, $matches);
$url = site_url('/CisVue/Cms/content/' . $matches[1]);
}
if (strpos($url, '../index.ci.php') !== false) {
$indexRegex = '/^\.\.\/index\.ci\.php/';
$url = preg_replace($indexRegex, site_url(), $url);
}
if (strpos($url, '../') !== false) {
$relativeRegex = '/^\.\.\//';
$url = preg_replace($relativeRegex, base_url(), $url);
}
return $url;
}
}
}
@@ -38,8 +38,7 @@ class Lehre extends FHCAPI_Controller
parent::__construct([
'lvStudentenMail' => self::PERM_LOGGED,
'LV' => self::PERM_LOGGED,
'Pruefungen' => self::PERM_LOGGED,
'semesterAverageGrade' => self::PERM_LOGGED,
'Pruefungen' => self::PERM_LOGGED
]);
}
@@ -101,49 +100,5 @@ class Lehre extends FHCAPI_Controller
$this->terminateWithSuccess($result);
}
/**
* calculates and returns the grade average and weighted average for a specific semester
* @param string $studiensemester_kurzbz
* @return void
*/
public function semesterAverageGrade($studiensemester_kurzbz)
{
$this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
$semesterLvs = $this->LehrveranstaltungModel->getLvsByStudentWithGrades(getAuthUID(), $studiensemester_kurzbz, getUserLanguage());
if (isError($semesterLvs))
return $this->outputJsonError(getError($semesterLvs));
$semesterLvsData = getData($semesterLvs);
$doGradesExist = false;
$sum = 0;
$count = 0;
$sumWeighted = 0;
$sumEcts = 0;
foreach ($semesterLvsData as $lv) {
if (!$lv->znote || $lv->znote < 1 || $lv->znote > 5)
continue;
$doGradesExist = true;
$sum += $lv->znote;
$count++;
$sumWeighted += $lv->znote * floatval($lv->ects);
$sumEcts += floatval($lv->ects);
}
$averageGrade = null;
$weightedAverageGrade = null;
if ($doGradesExist) {
$averageGrade = $sum/$count;
$weightedAverageGrade = $sumWeighted/$sumEcts;
}
$this->terminateWithSuccess(['average_grade' => $averageGrade, 'weighted_average_grade' => $weightedAverageGrade]);
}
}
@@ -16,8 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (!defined('BASEPATH'))
exit('No direct script access allowed');
if (! defined('BASEPATH')) exit('No direct script access allowed');
use CI3_Events as Events;
use \DateTime as DateTime;
@@ -34,25 +33,19 @@ class LvPlan extends FHCAPI_Controller
parent::__construct([
'getRoomplan' => self::PERM_LOGGED,
'Stunden' => self::PERM_LOGGED,
'getReservierungen' => self::PERM_LOGGED,
'Stunden' => self::PERM_LOGGED,
'getReservierungen' => self::PERM_LOGGED,
'LvPlanEvents' => self::PERM_LOGGED,
'eventsPersonal' => self::PERM_LOGGED,
'eventsLv' => self::PERM_LOGGED,
'getLehreinheitStudiensemester' => self::PERM_LOGGED,
'studiensemesterDateInterval' => self::PERM_LOGGED,
'getLvPlanForStudiensemester' => self::PERM_LOGGED,
'getLv' => self::PERM_LOGGED,
'eventsStgOrg' => self::PERM_LOGGED,
'fetchFerienEvents' => self::PERM_LOGGED,
'getStudiengaenge' => self::PERM_LOGGED,
'getLehrverband' => self::PERM_LOGGED,
'permissionOtherLvPlan' => self::PERM_LOGGED,
'compactibleEventTypes' => self::PERM_LOGGED,
'getLv' => self::PERM_LOGGED
]);
$this->load->library('LogLib');
$this->loglib->setConfigs(array(
$this->load->library('LogLib');
$this->loglib->setConfigs(array(
'classIndex' => 5,
'functionIndex' => 5,
'lineIndex' => 4,
@@ -60,17 +53,17 @@ class LvPlan extends FHCAPI_Controller
'dbExecuteUser' => 'RESTful API'
));
$this->load->library('form_validation');
$this->load->library('form_validation');
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* fetches LvPlan and Moodle events together
* @access public
*
*/
* fetches LvPlan and Moodle events together
* @access public
*
*/
public function LvPlanEvents()
{
$hasLv = $this->input->post('lv_id');
@@ -90,30 +83,24 @@ class LvPlan extends FHCAPI_Controller
// form validation
$this->form_validation->set_rules('start_date', "start_date", "required");
$this->form_validation->set_rules('end_date', "end_date", "required");
if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array());
// storing the post parameter in local variables
$start_date = $this->input->post('start_date', true);
$end_date = $this->input->post('end_date', true);
$uid = $this->input->post('uid', true);
// disallow accessing other user's events if missing permission
if ($uid && $uid !== getAuthUID() && !$this->permissionlib->isBerechtigt('basis/other_lv_plan')) {
$this->terminateWithError("Missing permission to view other users' timetables!");
}
// fetching lvplan events
$result = $this->stundenplanlib->getEventsUser($start_date, $end_date, $uid);
$result = $this->stundenplanlib->getEventsUser($start_date, $end_date);
$lvplanEvents = $this->getDataOrTerminateWithError($result);
// fetching moodle events
$moodleEvents = $this->fetchMoodleEvents($start_date, $end_date, $uid);
$moodleEvents = $this->fetchMoodleEvents($start_date, $end_date);
// fetching ferien events
$ferienEvents = $this->fetchFerienEvents($start_date, $end_date, $uid);
$ferienEvents = $this->fetchFerienEvents($start_date, $end_date);
$this->terminateWithSuccess(array_merge(
$lvplanEvents,
@@ -122,45 +109,6 @@ class LvPlan extends FHCAPI_Controller
));
}
/**
* fetches LvPlan for studiengang / semester / verband / gruppe
*
* @access public
*/
public function eventsStgOrg()
{
$this->load->library('StundenplanLib');
// form validation
$this->form_validation->set_rules('start_date', "start_date", "required");
$this->form_validation->set_rules('end_date', "end_date", "required");
//$this->form_validation->set_rules('stg_kz', "stg_kz", "required"); //no validation show empty calendar
if (!$this->form_validation->run()) {
$this->terminateWithValidationErrors($this->form_validation->error_array());
$stgOrgEvents = [];
$ferienEvents = [];
} else {
$start_date = $this->input->post('start_date', true);
$end_date = $this->input->post('end_date', true);
$stg_kz = $this->input->post('stg_kz', true);
$sem = $this->input->post('sem', true);
$verband = $this->input->post('verband', true);
$gruppe = $this->input->post('gruppe', true);
$result = $this->stundenplanlib->getEventsStgOrg($start_date, $end_date, $stg_kz, $sem, $verband, $gruppe);
$stgOrgEvents = $this->getDataOrTerminateWithError($result);
$result = $this->stundenplanlib->fetchFerienTageEvents($start_date, $end_date, $stg_kz);
$ferienEvents = $this->getDataOrTerminateWithError($result);
}
$this->terminateWithSuccess(array_merge(
$stgOrgEvents,
$ferienEvents
));
}
/**
* fetches LvPlan and Ferien events together for the lv
*
@@ -174,7 +122,7 @@ class LvPlan extends FHCAPI_Controller
$this->form_validation->set_rules('start_date', "start_date", "required");
$this->form_validation->set_rules('end_date', "end_date", "required");
$this->form_validation->set_rules('lv_id', "lv_id", "required|integer");
if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array());
@@ -189,6 +137,7 @@ class LvPlan extends FHCAPI_Controller
// fetching ferien events
$ferienEvents = $this->fetchFerienEvents($start_date, $end_date);
$this->terminateWithSuccess(array_merge(
$lvplanEvents,
@@ -197,42 +146,40 @@ class LvPlan extends FHCAPI_Controller
}
//TODO: delete this function if we don't use the old calendar export endpoints anymore
public function studiensemesterDateInterval($date)
{
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$studiensemester = $this->StudiensemesterModel->getByDate(date_format(date_create($date), 'Y-m-d'));
$studiensemester = current($this->getDataOrTerminateWithError($studiensemester));
public function studiensemesterDateInterval($date){
$this->load->model('organisation/Studiensemester_model','StudiensemesterModel');
$studiensemester =$this->StudiensemesterModel->getByDate(date_format(date_create($date),'Y-m-d'));
$studiensemester =current($this->getDataOrTerminateWithError($studiensemester));
$this->terminateWithSuccess($studiensemester);
}
public function getLvPlanForStudiensemester($studiensemester, $lvid)
{
public function getLvPlanForStudiensemester($studiensemester,$lvid){
$this->load->library('StundenplanLib');
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$studiensemester_result = $this->StudiensemesterModel->loadWhere(["studiensemester_kurzbz" => $studiensemester]);
$this->load->model('organisation/Studiensemester_model','StudiensemesterModel');
$studiensemester_result = $this->StudiensemesterModel->loadWhere(["studiensemester_kurzbz"=>$studiensemester]);
$studiensemester_result = current($this->getDataOrTerminateWithError($studiensemester_result));
$timespan_start = new DateTime($studiensemester_result->start);
$timespan_ende = new DateTime($studiensemester_result->ende);
$lvplan = $this->stundenplanlib->getStundenplan(date_format($timespan_start, 'Y-m-d'), date_format($timespan_ende, 'Y-m-d'), $lvid);
$lvplan = $this->stundenplanlib->getStundenplan(date_format($timespan_start, 'Y-m-d'),date_format($timespan_ende, 'Y-m-d'), $lvid);
$this->terminateWithSuccess($lvplan);
}
/**
* fetches Stunden layout from database
* @access public
*
*/
public function Stunden()
/**
* fetches Stunden layout from database
* @access public
*
*/
public function Stunden()
{
$this->load->model('ressource/Stunde_model', 'StundeModel');
$this->StundeModel->addOrder('stunde', 'ASC');
$stunden = $this->StundeModel->load();
$stunden = $this->getDataOrTerminateWithError($stunden);
$stunden = $this->getDataOrTerminateWithError($stunden);
$this->terminateWithSuccess($stunden);
}
@@ -263,10 +210,10 @@ class LvPlan extends FHCAPI_Controller
$roomplan_data = $this->stundenplanlib->getRoomplan($ort_kurzbz, $start_date, $end_date);
$roomplan_data = $this->getDataOrTerminateWithError($roomplan_data);
$this->terminateWithSuccess($roomplan_data);
}
/**
* gets the reservierungen of a room if the ort_kurzbz parameter is
* supplied otherwise gets the reservierungen of the lvplan of a student
@@ -279,32 +226,25 @@ class LvPlan extends FHCAPI_Controller
{
$this->form_validation->set_rules('start_date', "StartDate", "required");
$this->form_validation->set_rules('end_date', "EndDate", "required");
if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array());
// storing the post parameter in local variables
$start_date = $this->input->post('start_date', true);
$end_date = $this->input->post('end_date', true);
$uid = $this->input->post('uid', true);
// disallow accessing other user's reservierungen if missing permission
if ($uid && $uid !== getAuthUID() && !$this->permissionlib->isBerechtigt('basis/other_lv_plan')) {
$this->terminateWithError("Missing permission to view other users' timetables!");
}
// get data
$this->load->library('StundenplanLib');
$result = $this->stundenplanlib->getReservierungen($start_date, $end_date, $ort_kurzbz, $uid);
$result = $this->stundenplanlib->getReservierungen($start_date, $end_date, $ort_kurzbz);
$result = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($result);
}
public function getLehreinheitStudiensemester($lehreinheit_id)
{
public function getLehreinheitStudiensemester($lehreinheit_id){
$this->load->model('education/Lehreinheit_model', 'LehreinheitModel');
$this->LehreinheitModel->addSelect(["studiensemester_kurzbz"]);
$result = $this->LehreinheitModel->load($lehreinheit_id);
@@ -347,68 +287,6 @@ class LvPlan extends FHCAPI_Controller
return $this->terminateWithSuccess(current($result));
}
public function getStudiengaenge()
{
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->StudiengangModel->addOrder('typ');
$this->StudiengangModel->addOrder('kurzbz');
$result = $this->StudiengangModel->loadWhere([
'aktiv' => true
]);
$data = $this->getDataOrTerminateWithError($result);
return $this->terminateWithSuccess($data);
}
public function getLehrverband($studiengang_kz, $semester = null, $verband = null)
{
$this->load->model('organisation/Lehrverband_model', 'LehrverbandModel');
$where = [
'aktiv' => true,
'studiengang_kz' => $studiengang_kz,
];
if ($semester !== null && $semester !== 'null' && $semester !== 'undefined') {
$where['semester'] = $semester;
}
if ($verband !== null && $verband !== 'null' && $verband !== 'undefined') {
$where['verband'] = $verband;
}
$this->LehrverbandModel->addOrder('studiengang_kz');
$this->LehrverbandModel->addOrder('semester');
$this->LehrverbandModel->addOrder('verband');
$this->LehrverbandModel->addOrder('gruppe');
$result = $this->LehrverbandModel->loadWhere($where);
$data = $this->getDataOrTerminateWithError($result);
return $this->terminateWithSuccess($data);
}
/**
* Checks if the current user has permission to view other users' timetables
*
* @return void
*/
public function permissionOtherLvPlan()
{
$this->terminateWithSuccess($this->permissionlib->isBerechtigt('basis/other_lv_plan'));
}
/**
* get event types which can be compacted in lv plan display
*
* @return void
*/
public function compactibleEventTypes()
{
$this->terminateWithSuccess(["lehreinheit", "reservierung", "ferien", "moodle"]);
}
/**
* fetch moodle events
*
@@ -416,34 +294,30 @@ class LvPlan extends FHCAPI_Controller
* @param string $end_date
* @return array
*/
private function fetchMoodleEvents($start_date, $end_date, $uid = null)
private function fetchMoodleEvents($start_date, $end_date)
{
if ($uid && $uid !== getAuthUID()) {
return [];
}
$this->load->config('calendar');
$tz = new DateTimeZone($this->config->item('timezone'));
$start = new DateTime($start_date);
$start->setTimezone($tz);
$end = new DateTime($end_date);
$end->setTimezone($tz);
$end->modify('+1 day -1 second');
$moodle_events = [];
Events::trigger(
'moodleCalendarEvents',
function &() use (&$moodle_events) {
function & () use (&$moodle_events) {
return $moodle_events;
},
[
'start_date' => $start->format('c'),
'end_date' => $end->format('c'),
'username' => $uid ?? getAuthUID()
'username' => getAuthUID()
]
);
@@ -457,23 +331,23 @@ class LvPlan extends FHCAPI_Controller
* @param string $end_date
* @return array
*/
private function fetchFerienEvents($start_date, $end_date, $uid = null)
private function fetchFerienEvents($start_date, $end_date)
{
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->load->model('education/Studentlehrverband_model', 'StudentLehrverbandModel');
$currentStudiensemester = $this->StudiensemesterModel->getByDate($start_date);
$currentStudiensemester = $this->getDataOrTerminateWithError($currentStudiensemester);
if ($currentStudiensemester) {
$studentsemester_kurzbz = current($currentStudiensemester)->studiensemester_kurzbz;
$studiengang = $this->StudentLehrverbandModel->loadWhere([
"student_uid" => $uid ?? getAuthUID(),
"student_uid" => getAuthUID(),
"studiensemester_kurzbz" => $studentsemester_kurzbz
]);
$studiengang = $this->getDataOrTerminateWithError($studiengang);
if ($studiengang)
$studiengang_kz = current($studiengang)->studiengang_kz;
else
@@ -483,7 +357,7 @@ class LvPlan extends FHCAPI_Controller
}
$ferienEvents = $this->stundenplanlib->fetchFerienTageEvents($start_date, $end_date, $studiengang_kz);
return $this->getDataOrTerminateWithError($ferienEvents);
}
}
@@ -1,76 +0,0 @@
<?php
/**
* Copyright (C) 2024 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class OtherLvPlan extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'otherLvPlanViewData' => ['basis/other_lv_plan:r'],
]);
$this->load->model('ressource/mitarbeiter_model', 'MitarbeiterModel');
$this->load->model('person/Benutzer_model', 'BenutzerModel');
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* retrieves viewData for other lv plan view
* @access public
* @param $uid the userID for which the other lv plan is being viewed
*/
public function otherLvPlanViewData($uid)
{
$isMitarbeiterResult = $this->MitarbeiterModel->isMitarbeiter($uid);
$isMitarbeiter = getData($isMitarbeiterResult);
$isStudent = !$isMitarbeiter;
$this->BenutzerModel->addSelect(["foto", "vorname", "nachname"]);
$this->BenutzerModel->addJoin("tbl_person", "person_id");
$personResult = $this->BenutzerModel->load([$uid]);
$person = hasData($personResult) ? getData($personResult) : null;
$viewData = [
"user_data" => [
"username" => $uid,
"is_student" => $isStudent,
"is_mitarbeiter" => $isMitarbeiter,
"foto" => $person[0]->foto,
"vorname" => $person[0]->vorname,
"nachname" => $person[0]->nachname,
],
];
$this->terminateWithSuccess($viewData);
}
// -----------------------------------------------------------------------------------------------------------------
// Private methods
}
@@ -32,8 +32,6 @@ class Phrasen extends FHCAPI_Controller
'setLanguage' => self::PERM_ANONYMOUS,
'getLanguage' => self::PERM_ANONYMOUS,
'getAllLanguages' => self::PERM_ANONYMOUS,
'getPhrases' => self::PERM_ANONYMOUS,
'getTabulatorPhrases' => self::PERM_ANONYMOUS,
]);
$this->load->helper('hlp_language');
@@ -94,59 +92,4 @@ class Phrasen extends FHCAPI_Controller
$this->terminateWithSuccess($langs);
}
public function getPhrases()
{
$postParams = $this->getPostJSON();
$languages = $postParams->languages;
if (!$languages || !count($languages)) {
$this->load->model('system/Sprache_model', 'sprachenModel');
$activeLanguages = $this->sprachenModel->loadWhere(array('content' => true));
$activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages);
$languages = array_map(
function ($languageData) {
return $languageData->sprache;
},
$activeLanguagesData
);
}
$this->load->model('system/Phrase_model', 'phraseModel');
$phrasesGroupedByCategory = $postParams->phrasesGroupedByCategory;
$result = [];
foreach ($languages as $language) {
$phrases = $this->phraseModel->getPhrasesByCategoryAndPhrasesAndLanguage($phrasesGroupedByCategory, $language);
$result[$language] = $this->getDataOrTerminateWithError($phrases);
}
$this->terminateWithSuccess($result);
}
public function getTabulatorPhrases()
{
$languages = json_decode($this->input->get('languages'));
if (!$languages || !count($languages)) {
$this->load->model('system/Sprache_model', 'sprachenModel');
$activeLanguages = $this->sprachenModel->loadWhere(array('content' => true));
$activeLanguagesData = $this->getDataOrTerminateWithError($activeLanguages);
$languages = array_map(
function ($languageData) {
return $languageData->sprache;
},
$activeLanguagesData
);
}
$this->load->model('system/Phrase_model', 'phraseModel');
$result = [];
foreach ($languages as $language) {
$tabulatorPhrases = $this->phraseModel->getPhrasesByCategoryAndLanguage(['tabulator'], $language);
$result[$language] = $this->getDataOrTerminateWithError($tabulatorPhrases);
}
header('Pragma: private');
header('Cache-Control: private, max-age=' . (60 * 60 * 24 * 30));
$this->terminateWithSuccess($result);
}
}
@@ -16,8 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (!defined('BASEPATH'))
exit('No direct script access allowed');
if (! defined('BASEPATH')) exit('No direct script access allowed');
class Profil extends FHCAPI_Controller
{
@@ -28,13 +27,13 @@ class Profil extends FHCAPI_Controller
public function __construct()
{
parent::__construct([
'fotoSperre' => self::PERM_LOGGED,
'fotoSperre' => self::PERM_LOGGED,
'getGemeinden' => self::PERM_LOGGED,
'getAllNationen' => self::PERM_LOGGED,
'isMitarbeiter' => self::PERM_LOGGED,
'profilViewData' => self::PERM_LOGGED,
]);
$this->load->library('PermissionLib');
$this->load->model('ressource/mitarbeiter_model', 'MitarbeiterModel');
@@ -49,37 +48,28 @@ class Profil extends FHCAPI_Controller
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* retrieves view data for profile view
* @access public
* @param $uid the userID for which profile is being viewed, null or missing value implies one's own profile
*/
public function profilViewData($uid = null)
{
$authUid = getAuthUID();
$isProfilOfAuthUser = !$uid || $uid === $authUid;
public function profilViewData($uid=null){
$this->load->library('ProfilLib');
$profileData = $this->profillib->getView($uid ?? $authUid);
$profileData = hasData($profileData) ? getData($profileData) : null;
$viewData = [
'editable' => $isProfilOfAuthUser,
'profil_data' => $profileData,
'permissions' => [
'basis/other_lv_plan' => $this->permissionlib->isBerechtigt(('basis/other_lv_plan'))
]
];
if ($isProfilOfAuthUser) {
$viewData['calendar_sync_urls'] = $this->getCalendarSyncUrlData();
$editable = false;
if(isset($uid) && $uid != null){
$profil_data = $this->profillib->getView($uid);
if($uid == getAuthUID()){
$editable = true;
}
}else{
$editable = true;
$profil_data = $this->profillib->getView(getAuthUID());
}
$profil_data = hasData($profil_data) ? getData($profil_data) : null;
$viewData = array(
'editable'=>$editable,
'profil_data' => $profil_data,
);
$this->terminateWithSuccess($viewData);
}
/**
/**
* update column foto_sperre in public.tbl_person
* @access public
* @param boolean $value new value for the column
@@ -87,9 +77,9 @@ class Profil extends FHCAPI_Controller
*/
public function fotoSperre($value)
{
if (!isset($value)) {
$this->terminateWithError("Missing parameter", self::ERROR_TYPE_GENERAL);
}
if(!isset($value)){
$this->terminateWithError("Missing parameter", self::ERROR_TYPE_GENERAL);
}
$res = $this->PersonModel->update($this->pid, ["foto_sperre" => $value]);
if (isError($res)) {
@@ -97,10 +87,10 @@ class Profil extends FHCAPI_Controller
}
$this->PersonModel->addSelect("foto_sperre");
$res = $this->PersonModel->load($this->pid);
$res = $this->getDataOrTerminateWithError($res);
$this->terminateWithSuccess(current($res));
$res = $this->getDataOrTerminateWithError($res);
$this->terminateWithSuccess(current($res));
}
/**
@@ -119,7 +109,7 @@ class Profil extends FHCAPI_Controller
if (isError($nation_res)) {
$this->terminateWithError("error while trying to query table codex.tbl_nation", self::ERROR_TYPE_GENERAL);
}
$nation_res = $this->getDataOrTerminateWithError($nation_res);
$this->terminateWithSuccess($nation_res);
@@ -127,30 +117,30 @@ class Profil extends FHCAPI_Controller
public function getGemeinden($nation, $zip)
{
if (!isset($nation) || !isset($zip)) {
if(!isset($nation) || !isset($zip)){
echo json_encode(error("Missing parameters"));
return;
}
$this->load->model('codex/Gemeinde_model', "GemeindeModel");
$gemeinde_res = $this->GemeindeModel->getGemeindeByPlz($zip);
if (isError($gemeinde_res)) {
$this->terminateWithError(getError($gemeinde_res), self::ERROR_TYPE_GENERAL);
$this->terminateWithError(getError($gemeinde_res),self::ERROR_TYPE_GENERAL);
}
$gemeinde_res = $this->getDataOrTerminateWithError($gemeinde_res);
/* $gemeinde_res = array_map(function ($obj) {
return $obj->ortschaftsname;
}, $gemeinde_res); */
$this->terminateWithSuccess($gemeinde_res);
$this->terminateWithSuccess($gemeinde_res);
}
/**
* checks whether a specific userID is a mitarbeiter or not (foreword declaration of the function isMitarbeiter in Mitarbeiter_model.php)
* @access public
@@ -160,48 +150,23 @@ class Profil extends FHCAPI_Controller
public function isMitarbeiter($uid)
{
if (!$uid)
$this->terminateWithError("No uid provided", self::ERROR_TYPE_GENERAL);
if(!$uid) $this->terminateWithError("No uid provided", self::ERROR_TYPE_GENERAL);
$result = $this->MitarbeiterModel->isMitarbeiter($uid);
if (isError($result)) {
$this->terminateWithError("error when calling Mitarbeiter_model function isMitarbeiter with uid " . $uid, self::ERROR_TYPE_GENERAL);
}
$result = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($result);
}
// -----------------------------------------------------------------------------------------------------------------
// Private methods
/**
* gets the identifier, phrase, and url for each calendar sync option
* @access private
* @return array array of arrays, where each child array is a sync option
*/
private function getCalendarSyncUrlData()
{
return [
[
"identifier" => "cal_dav",
"labelPhrase" => "profil/calendar_sync_cal_dav",
"url" => APP_ROOT . "webdav/lvplan.php/calendars/" . $this->uid . "/LVPlan-" . $this->uid,
],
[
"identifier" => "cal_dav_principal",
"labelPhrase" => "profil/calendar_sync_cal_dav_principal",
"url" => APP_ROOT . "webdav/lvplan.php/principals/" . $this->uid,
],
[
"identifier" => "i_cal",
"labelPhrase" => "profil/calendar_sync_i_cal",
"url" => APP_ROOT . "webdav/google.php?cal=" . encryptData($this->uid, LVPLAN_CYPHER_KEY) . "&" . microtime(true),
],
];
}
}
@@ -1,64 +0,0 @@
<?php
/**
* Copyright (C) 2024 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class StgOrgLvPlan extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'stgOrgLvPlanViewData' => self::PERM_LOGGED,
]);
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* fetches view data for stg org lv plan
* @access public
*/
public function stgOrgLvPlanViewData()
{
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
$this->StudiengangModel->addOrder('typ');
$this->StudiengangModel->addOrder('kurzbz');
$result = $this->StudiengangModel->loadWhere([
'aktiv' => true
]);
$studiengaenge = $this->getDataOrTerminateWithError($result);
$viewData = array(
'studiengaenge' => $studiengaenge,
);
$this->terminateWithSuccess($viewData);
}
// -----------------------------------------------------------------------------------------------------------------
// Private methods
}
@@ -62,36 +62,21 @@ class Studium extends FHCAPI_Controller
if($this->getDataOrTerminateWithError($this->StudentModel->isStudent(getAuthUID()))){
$studentLehrverband =$this->StudentlehrverbandModel->loadWhere(["student_uid" => getAuthUID(), "studiensemester_kurzbz" => $aktuelles_studiensemester->studiensemester_kurzbz]);
//TODO(Manu) check if use Fallback or just comment out all paramschecks?
//add Fallback: if no LehrverbandData of actual semester, get Data of previous one
if(!hasData($studentLehrverband))
{
$result= $this->StudiensemesterModel->getPreviousFrom($aktuelles_studiensemester->studiensemester_kurzbz);
$data = $this->getDataOrTerminateWithError($result);
$vorheriges_studiensemester = current($data)->studiensemester_kurzbz;
$studentLehrverband =$this->StudentlehrverbandModel->loadWhere(["student_uid" => getAuthUID(), "studiensemester_kurzbz" => $vorheriges_studiensemester]);
}
$studentLehrverband = current(getData($studentLehrverband));
$studentLehrverband = current($this->getDataOrTerminateWithError($studentLehrverband));
$student_studiensemester = $studentLehrverband->studiensemester_kurzbz;
$student_studiengang = $studentLehrverband->studiengang_kz;
$student_semester = $studentLehrverband->semester;
$student_studienplan = $this->getStudienPlanFromPrestudentStatus(getAuthPersonId())->studienplan_id;
if(!isset($parameter_studiensemester)) {
$student_studiensemester = $studentLehrverband->studiensemester_kurzbz;
$parameter_studiensemester = $student_studiensemester;
}
if(!isset($parameter_studiengang)) {
$student_studiengang = $studentLehrverband->studiengang_kz;
$parameter_studiengang = $student_studiengang;
}
if(!isset($parameter_semester)) {
$student_semester = $studentLehrverband->semester;
$parameter_semester = $student_semester;
}
if(!isset($parameter_studiensemester))
$parameter_studiensemester = $student_studiensemester;
if(!isset($parameter_studiengang))
$parameter_studiengang = $student_studiengang;
if(!isset($parameter_semester))
$parameter_semester = $student_semester;
if(!isset($parameter_studienplan))
$parameter_studienplan = $student_studienplan;
$parameter_studienplan = $student_studienplan;
}
if(isset($parameter_studiensemester)){
@@ -111,7 +96,8 @@ class Studium extends FHCAPI_Controller
// fetch studiensemester
$allStudienSemester = $this->getDataOrTerminateWithError($this->StudiensemesterModel->load());
if(isset($parameter_studiensemester) && !empty(array_filter($allStudienSemester, function($studiensemester) use($parameter_studiensemester){
return $studiensemester->studiensemester_kurzbz == $parameter_studiensemester->studiensemester_kurzbz;
}))){
@@ -230,8 +216,6 @@ class Studium extends FHCAPI_Controller
$studienplaene = array_map(function($studienplan){
$orgform = current($this->getDataOrTerminateWithError($this->OrgformModel->loadWhere(["orgform_kurzbz" => $studienplan->orgform_kurzbz])));
$studienplan->orgform_bezeichnung = $orgform->bezeichnung;
// bezeichnung_mehrsprachig
$studienplan->orgform_bezeichnung_english = $orgform->bezeichnung_mehrsprachig[1];
return $studienplan;
},$studienplaene);
return $studienplaene;
@@ -40,32 +40,11 @@ class Board extends FHCAPI_Controller
public function list()
{
$this->DashboardModel->addSelect('dashboard_id');
$this->DashboardModel->addSelect('dashboard_kurzbz');
$this->DashboardModel->addSelect('tbl_dashboard.beschreibung');
$this->DashboardModel->addSelect("(
SELECT json_agg(w.*)
FROM dashboard.tbl_widget w
JOIN dashboard.tbl_dashboard_widget dw
USING(widget_id)
WHERE dw.dashboard_id=tbl_dashboard.dashboard_id
) AS \"widgetSetup\"");
$result = $this->DashboardModel->load();
$data = $this->getDataOrTerminateWithError($result);
$data = array_map(function ($dashboard) {
$tmpSetups = json_decode($dashboard->widgetSetup);
$tmpSetups = array_map(function ($widget) {
$widget->setup->file = absoluteJsImportUrl($widget->setup->file);
return $widget;
}, $tmpSetups);
$dashboard->widgetSetup = $tmpSetups;
return $dashboard;
}, $data);
$this->terminateWithSuccess($data);
$this->terminateWithSuccess($result);
}
public function create()
@@ -103,7 +82,7 @@ class Board extends FHCAPI_Controller
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
$this->terminateWithSuccess($result);
}
public function delete()
@@ -137,6 +116,6 @@ class Board extends FHCAPI_Controller
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
$this->terminateWithSuccess($result);
}
}
@@ -120,7 +120,10 @@ class Preset extends FHCAPI_Controller
$conf = $this->dashboardlib->getPreset($db, $funktion);
if ($conf) {
$preset = json_decode($conf->preset, true);
$result[$funktion] = $preset;
if (!isset($preset[$funktion]) || !isset($preset[$funktion]['widgets']))
$result[$funktion] = [];
else
$result[$funktion] = $preset[$funktion]['widgets'];
} else {
$result[$funktion] = [];
}
@@ -151,7 +154,7 @@ class Preset extends FHCAPI_Controller
$preset_decoded = json_decode($preset->preset, true);
$preset_decoded[$widget['widgetid']] = $widget;
$this->dashboardlib->addWidgetsToWidgets($preset_decoded, $dashboard_kurzbz, $funktion_kurzbz, [$widget]);
$preset->preset = json_encode($preset_decoded);
@@ -183,10 +186,8 @@ class Preset extends FHCAPI_Controller
$preset_decoded = json_decode($preset->preset, true);
if (!isset($preset_decoded[$widgetid]))
if (!$this->dashboardlib->removeWidgetFromWidgets($preset_decoded, $funktion_kurzbz, $widgetid))
show_404();
unset($preset_decoded[$widgetid]);
$preset->preset = json_encode($preset_decoded);
@@ -48,9 +48,25 @@ class User extends FHCAPI_Controller
$uid = $this->authlib->getAuthObj()->username;
$mergedconfig = $this->dashboardlib->getMergedUserConfig($dashboard->dashboard_id, $uid);
/*$mergedconfig = $this->dashboardlib->getMergedConfig($dashboard->dashboard_id, $uid);
$this->terminateWithSuccess($mergedconfig);
$this->terminateWithSuccess([
'general' => call_user_func_array(
'array_merge_recursive',
$mergedconfig
)
]);*/
$defaultconfig = $this->dashboardlib->getDefaultConfig($dashboard->dashboard_id);
$userconfig = $this->dashboardlib->getUserConfig($dashboard->dashboard_id, $uid);
$defaultconfig_squashed = $defaultconfig ? call_user_func_array('array_replace_recursive', $defaultconfig) : [];
$userconfig_squashed = $userconfig ? call_user_func_array('array_replace_recursive', $userconfig) : [];
$mergedconfig = array_replace_recursive($defaultconfig_squashed, $userconfig_squashed);
$this->terminateWithSuccess([
DashboardLib::SECTION_IF_FUNKTION_KURZBZ_IS_NULL => $mergedconfig
]);
}
public function addWidget()
@@ -70,15 +86,26 @@ class User extends FHCAPI_Controller
if (!isset($widget['widgetid']))
$widget['widgetid'] = $this->dashboardlib->generateWidgetId($dashboard_kurzbz);
if (isset($widget['source']))
unset($widget['source']);
$override = $this->dashboardlib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
$override_decoded = json_decode($override->override, true);
$override_decoded[$widget['widgetid']] = $widget;
if (!isset($override_decoded['general']) || !is_array($override_decoded['general']))
$override_decoded['general'] = [];
if (!isset($override_decoded['general']['widgets']))
$override_decoded['general']['widgets'] = [];
$override_decoded['general']['widgets'][$widget['widgetid']] = $widget;
// NOTE(chris): remove doubles in other funktionen
foreach ($override_decoded as $funktion => $array) {
if ($funktion == 'general')
continue;
if (isset($array['widgets']) && isset($array['widgets'][$widget['widgetid']]))
unset($override_decoded[$funktion]['widgets'][$widget['widgetid']]);
}
$override->override = json_encode($override_decoded);
$result = $this->dashboardlib->insertOrUpdateOverride($override);
@@ -108,10 +135,18 @@ class User extends FHCAPI_Controller
$override_decoded = json_decode($override->override, true);
if (!isset($override_decoded[$widget_id]))
show_404();
unset($override_decoded[$widget_id]);
foreach (array_keys($override_decoded) as $k) {
if (!isset($override_decoded[$k]["widgets"])) {
unset($override_decoded[$k]);
continue;
}
if (isset($override_decoded[$k]["widgets"][$widget_id])) {
unset($override_decoded[$k]["widgets"][$widget_id]);
}
if (!$override_decoded[$k]["widgets"]) {
unset($override_decoded[$k]);
}
}
$override->override = json_encode($override_decoded);
@@ -90,15 +90,6 @@ class Projektarbeit extends FHCAPI_Controller
if (!isset($projektarbeit_id) || !is_numeric($projektarbeit_id)) return $this->terminateWithError('Projektarbeit Id missing', self::ERROR_TYPE_GENERAL);
$result = $this->fetchProjektarbeitByID($projektarbeit_id);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(current($data));
}
private function fetchProjektarbeitById($projektarbeit_id) {
$this->ProjektarbeitModel->resetQuery();
$this->ProjektarbeitModel->addSelect(
'lehre.tbl_projektarbeit.projektarbeit_id, titel, titel_english, themenbereich, projekttyp_kurzbz, lehrveranstaltung_id, lehreinheit_id,
firma_id, beginn, ende, gesperrtbis, note, final, freigegeben, tbl_projektarbeit.anmerkung, fa.name AS firma_name'
@@ -106,10 +97,13 @@ class Projektarbeit extends FHCAPI_Controller
$this->ProjektarbeitModel->addJoin('lehre.tbl_lehreinheit le', 'lehreinheit_id');
$this->ProjektarbeitModel->addJoin('lehre.tbl_lehrveranstaltung lv', 'lehrveranstaltung_id');
$this->ProjektarbeitModel->addJoin('public.tbl_firma fa', 'firma_id', 'LEFT');
return $this->ProjektarbeitModel->loadWhere(
$result = $this->ProjektarbeitModel->loadWhere(
array('projektarbeit_id' => $projektarbeit_id)
);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess(current($data));
}
/**
@@ -138,8 +132,7 @@ class Projektarbeit extends FHCAPI_Controller
);
$data = $this->getDataOrTerminateWithError($result);
$data = $this->getDataOrTerminateWithError($this->fetchProjektarbeitById($data));
$this->terminateWithSuccess($data);
}
@@ -287,7 +280,10 @@ class Projektarbeit extends FHCAPI_Controller
*/
public function getNoten()
{
$result = $this->NoteModel->getAllActive();
$this->NoteModel->addOrder('notenwert', 'ASC');
$this->NoteModel->addOrder('bezeichnung', 'ASC');
$result = $this->NoteModel->load();
if (isError($result)) return $this->terminateWithError(getError($result), self::ERROR_TYPE_GENERAL);
@@ -358,8 +358,7 @@ class AbgabetoolJob extends JOB_Controller
foreach($assistenzMap as $assistenz_person_id => $tupelArr) {
$abgabenString = '<div style="font-family: Arial, sans-serif; color: #333;">';
$hasContent = false;
foreach($tupelArr as $tupel) {
$projektarbeit_id = $tupel[0];
$assistenzRow = $tupel[1];
@@ -378,7 +377,6 @@ class AbgabetoolJob extends JOB_Controller
if(count($relevantAbgaben) == 0) {
continue;
}
$hasContent = true;
// Format the Student Name
$s = $relevantAbgaben[0];
@@ -449,12 +447,7 @@ class AbgabetoolJob extends JOB_Controller
}
$abgabenString .= '</div>';
// skip send entirely
if (!$hasContent) {
continue;
}
// done with building the change list, now send it
$assistenzRow = $tupelArr[0][1];
$anrede = $assistenzRow->anrede;
-53
View File
@@ -263,59 +263,6 @@ class ProfilLib{
$element->mailto = "mailto:" . $element->gruppe_kurzbz . "@" . DOMAIN;
return $element;
}, $mailverteiler_res);
$this->ci->load->model("crm/Student_model", "StudentModel");
$this->ci->StudentModel->addJoin("tbl_benutzer", "tbl_benutzer.uid = tbl_student.student_uid");
$this->ci->StudentModel->addJoin("tbl_person", "tbl_benutzer.person_id = tbl_person.person_id");
$this->ci->StudentModel->addJoin("tbl_studiengang", "tbl_student.studiengang_kz = tbl_studiengang.studiengang_kz");
$this->ci->StudentModel->addSelect("matr_nr, semester, verband, gruppe, kurzbzlang");
$studentResult = $this->ci->StudentModel->loadWhere(["student_uid" => $uid]);
if (isError($studentResult)) {
return error(getData($studentResult));
}
$studentResultData = getData($studentResult);
$studentData = null;
if (is_array($studentResultData) && count($studentResultData)) {
$studentData = $studentResultData[0];
}
if ($studentData && $studentData->matr_nr) {
$this->ci->load->library("phrasesLib", ["profil"], "phrases");
$standardCourseVerteiler = trim($studentData->kurzbzlang) . "_STD";
$mailverteiler_res[] = [
"beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $standardCourseVerteiler,
"gruppe_kurzbz" => $standardCourseVerteiler,
"mailto" => "mailto:" . strtolower($standardCourseVerteiler) . "@" . DOMAIN,
];
$semesterVerteiler = trim($studentData->kurzbzlang) . trim($studentData->semester);
$mailverteiler_res[] = [
"beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $semesterVerteiler,
"gruppe_kurzbz" => $semesterVerteiler,
"mailto" => "mailto:" . strtolower($semesterVerteiler) . "@" . DOMAIN,
];
if ($studentData->verband && strlen(trim($studentData->verband))) {
$verbandVerteiler = $semesterVerteiler . trim($studentData->verband);
$mailverteiler_res[] = [
"beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $verbandVerteiler,
"gruppe_kurzbz" => $verbandVerteiler,
"mailto" => "mailto:" . strtolower($verbandVerteiler) . "@" . DOMAIN,
];
if ($studentData->gruppe && strlen(trim($studentData->gruppe))) {
$gruppeVerteiler = $verbandVerteiler . trim($studentData->gruppe);
$mailverteiler_res[] = [
"beschreibung" => $this->ci->phrases->t('profil', 'alleStudentenVon') . " " . $gruppeVerteiler,
"gruppe_kurzbz" => $gruppeVerteiler,
"mailto" => "mailto:" . strtolower($gruppeVerteiler) . "@" . DOMAIN,
];
}
}
}
return $mailverteiler_res;
}
+7 -31
View File
@@ -40,16 +40,13 @@ class StundenplanLib
* @return stdClass
* @access public
*/
public function getEventsUser($start, $end, $uid = null)
public function getEventsUser($start, $end)
{
$this->_ci =& get_instance();
$this->_ci->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
if (!$uid) {
$uid = getAuthUID();
}
$uid = getAuthUID();
if (is_null($uid))
return error("No UID");
@@ -220,7 +217,7 @@ class StundenplanLib
* @param string $ort_kurzbz
* @return stdClass
*/
public function getReservierungen($start_date, $end_date, $ort_kurzbz = '', $uid = null)
public function getReservierungen($start_date, $end_date, $ort_kurzbz = '')
{
$this->_ci =& get_instance();
@@ -231,14 +228,14 @@ class StundenplanLib
$this->_ci->load->model('ressource/Reservierung_model', 'ReservierungModel');
$this->_ci->load->model('ressource/Stundenplan_model', 'StundenplanModel');
$is_mitarbeiter = getData($this->_ci->MitarbeiterModel->isMitarbeiter($uid ?? getAuthUID()));
$is_mitarbeiter = getData($this->_ci->MitarbeiterModel->isMitarbeiter(getAuthUID()));
if ($is_mitarbeiter && empty($ort_kurzbz)) {
// request for personal lvplan show only reservations of logged in user
$reservierungen = $this->_ci->ReservierungModel->getReservierungenMitarbeiter($start_date, $end_date, $uid);
$reservierungen = $this->_ci->ReservierungModel->getReservierungenMitarbeiter($start_date, $end_date);
} else {
// querying the reservierungen
$reservierungen = $this->_ci->ReservierungModel->getReservierungen($start_date, $end_date, $ort_kurzbz, $uid);
$reservierungen = $this->_ci->ReservierungModel->getReservierungen($start_date, $end_date, $ort_kurzbz);
}
if (isError($reservierungen))
@@ -360,10 +357,7 @@ class StundenplanLib
if (isError($ort_content_object)) {
return error(getData($ort_content_object));
}
$ort_content_object_data = getData($ort_content_object);
$ort_content_object = (is_array($ort_content_object_data) && count($ort_content_object_data) > 0)
? $ort_content_object_data[0]
: null;
$ort_content_object = getData($ort_content_object)[0];
if($ort_content_object) {
$item->ort_content_id = $ort_content_object->content_id;
}
@@ -451,24 +445,6 @@ class StundenplanLib
return success($ferienEventsFlattened);
}
public function getEventsStgOrg( $start, $end, $stg_kz, $sem, $verband, $gruppe)
{
$this->_ci =& get_instance();
$this->_ci->load->model('ressource/Stundenplan_model', 'StundenplanModel');
$stundenplan_data = $this->_ci->StundenplanModel->getStundenplanStudiengang($start, $end, $stg_kz, $sem, $verband, $gruppe);
if (isError($stundenplan_data))
return $stundenplan_data;
$stundenplan_data = getData($stundenplan_data) ?? [];
$function_error = $this->expandObjectInformation($stundenplan_data);
if ($function_error)
return $function_error;
return success($stundenplan_data);
}
// start of the private functions ########################################################################################################
// function used to sort an array of studiensemester strings
@@ -37,9 +37,7 @@ class DashboardLib
public function getDashboardByKurzbz($dashboard_kurzbz)
{
$result = $this->_ci->DashboardModel->loadWhere([
'dashboard_kurzbz' => $dashboard_kurzbz
]);
$result = $this->_ci->DashboardModel->getDashboardByKurzbz($dashboard_kurzbz);
if (hasData($result))
{
@@ -49,21 +47,17 @@ class DashboardLib
return null;
}
public function getMergedUserConfig($dashboard_id, $uid)
public function getMergedConfig($dashboard_id, $uid)
{
$defaultconfig = $this->getUserBaseConfig($dashboard_id);
$userconfig = $this->getUserOverrideConfig($dashboard_id, $uid);
$defaultconfig = $this->getDefaultConfig($dashboard_id);
$userconfig = $this->getUserConfig($dashboard_id, $uid);
$sourceconfig = array_map(function ($value) {
return ['source' => $value['source']];
}, $defaultconfig);
$mergedconfig = array_replace_recursive($defaultconfig, $userconfig, $sourceconfig);
$mergedconfig = array_replace_recursive($defaultconfig, $userconfig);
return $mergedconfig;
}
protected function getUserBaseConfig($dashboard_id)
public function getDefaultConfig($dashboard_id)
{
$funktion_kurzbzs = [];
$rights = $this->_ci->permissionlib->getAccessRights();
@@ -93,11 +87,7 @@ class DashboardLib
$preset = json_decode($presetobj->preset, true);
if (null !== $preset)
{
$preset = array_map(function ($value) use ($presetobj) {
$value['source'] = $presetobj->funktion_kurzbz ?: self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
return $value;
}, $preset);
$defaultconfig = array_merge_recursive($defaultconfig, $preset);
$defaultconfig = array_replace_recursive($defaultconfig, $preset);
}
}
}
@@ -105,7 +95,7 @@ class DashboardLib
return $defaultconfig;
}
protected function getUserOverrideConfig($dashboard_id, $uid)
public function getUserConfig($dashboard_id, $uid)
{
$res_userconfig = $this->_ci->DashboardOverrideModel->getOverride($dashboard_id, $uid);
@@ -134,7 +124,7 @@ class DashboardLib
$emptyoverride = new stdClass();
$emptyoverride->dashboard_id = $dashboard->dashboard_id;
$emptyoverride->uid = $uid;
$emptyoverride->override = '[]';
$emptyoverride->override = '{"' . self::USEROVERRIDE_SECTION . '": {"widgets":{}}, "custom": { "widgets" : {}}}';
return $emptyoverride;
}
@@ -153,7 +143,8 @@ class DashboardLib
$emptypreset = new stdClass();
$emptypreset->dashboard_id = $dashboard->dashboard_id;
$emptypreset->funktion_kurzbz = $funktion_kurzbz;
$emptypreset->preset = '[]';
$section = ($funktion_kurzbz !== null) ? $funktion_kurzbz : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
$emptypreset->preset = '{"' . $section . '": { "widgets" : {}},"custom": { "widgets" : {}}}';
return $emptypreset;
}
@@ -218,4 +209,44 @@ class DashboardLib
return $result;
}
public function addWidgetsToWidgets(&$widgets, $dashboard_kurzbz, $section, $addwigets)
{
foreach ($addwigets as $widget)
{
if(!isset($widget['widgetid']))
{
$widget['widgetid'] = $this->generateWidgetId($dashboard_kurzbz);
}
$this->addWidgetToWidgets($widgets, $section, $widget, $widget['widgetid']);
}
}
public function addWidgetToWidgets(&$widgets, $section, $widget, $widgetid)
{
$section = ($section !== null) ? $section : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
if (!isset($widgets[$section]) || !isset($widgets[$section]["widgets"]) || !is_array($widgets[$section]))
{
$widgets[$section] = array();
$widgets[$section]["widgets"] = array();
}
$widgets[$section]["widgets"][$widgetid] = $widget;
}
public function removeWidgetFromWidgets(&$widgets, $section, $widgetid)
{
$section = ($section !== null) ? $section : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL;
if (isset($widgets[$section]) && isset($widgets[$section]["widgets"][$widgetid]))
{
unset($widgets[$section]["widgets"][$widgetid]);
if(empty($widgets[$section]["widgets"]) && $section !== self::USEROVERRIDE_SECTION) {
unset($widgets[$section]);
}
return true;
}
else {
return false;
}
}
}
+3 -3
View File
@@ -234,9 +234,9 @@ class Content_model extends DB_Model
FROM
campus.tbl_content c1
LEFT JOIN
campus.tbl_contentsprache s1 ON c1.content_id=s1.content_id AND s1.sprache=? AND sichtbar=true
campus.tbl_contentsprache s1 ON c1.content_id=s1.content_id AND s1.sprache=?
WHERE
c1.aktiv = true
sichtbar=true
) s2
LEFT JOIN
campus.tbl_contentsprache s3 USING(content_id, sprache)
@@ -277,7 +277,7 @@ class Content_model extends DB_Model
JOIN
campus.tbl_contentsprache s USING(contentsprache_id)
LEFT JOIN
campus.tbl_contentchild k ON(m.content_id=k.content_id) and c.aktiv = true
campus.tbl_contentchild k ON(m.content_id=k.content_id)
WHERE EXISTS (
SELECT 1
FROM campus.tbl_contentgruppe
@@ -11,4 +11,8 @@ class Bookmark_model extends DB_Model
$this->dbTable = 'dashboard.tbl_bookmark';
$this->pk = 'bookmark_id';
}
}
@@ -11,4 +11,15 @@ class Dashboard_model extends DB_Model
$this->dbTable = 'dashboard.tbl_dashboard';
$this->pk = 'dashboard_id';
}
/**
* Get Dashboard by kurzbz.
* @param string dashboard_kurzbz
* @return array
*/
public function getDashboardByKurzbz($dashboard_kurzbz)
{
return $this->loadWhere(array('dashboard_kurzbz' => $dashboard_kurzbz));
}
}
@@ -187,10 +187,7 @@ class Projektarbeit_model extends DB_Model
campus.tbl_paabgabetyp.bezeichnung,
campus.tbl_paabgabetyp.benotbar,
campus.tbl_paabgabe.abgabedatum,
campus.tbl_paabgabe.insertvon,
campus.tbl_paabgabe.insertamum,
campus.tbl_paabgabe.updatevon,
campus.tbl_paabgabe.updateamum
campus.tbl_paabgabe.insertvon
FROM campus.tbl_paabgabe JOIN campus.tbl_paabgabetyp USING(paabgabetyp_kurzbz)
WHERE campus.tbl_paabgabe.projektarbeit_id = ?
ORDER BY campus.tbl_paabgabe.datum";
@@ -210,18 +207,7 @@ class Projektarbeit_model extends DB_Model
campus.tbl_paabgabetyp.paabgabetyp_kurzbz,
campus.tbl_paabgabetyp.bezeichnung,
campus.tbl_paabgabe.abgabedatum,
campus.tbl_paabgabe.insertvon,
campus.tbl_paabgabe.updatevon,
campus.tbl_paabgabe.insertamum,
campus.tbl_paabgabe.updateamum,
(SELECT p.vorname || ' ' || p.nachname
FROM public.tbl_benutzer b
JOIN public.tbl_person p USING(person_id)
WHERE b.uid = campus.tbl_paabgabe.insertvon) AS insertvon_fullname,
(SELECT p.vorname || ' ' || p.nachname
FROM public.tbl_benutzer b
JOIN public.tbl_person p USING(person_id)
WHERE b.uid = campus.tbl_paabgabe.updatevon) AS updatevon_fullname
campus.tbl_paabgabe.insertvon
FROM campus.tbl_paabgabe JOIN campus.tbl_paabgabetyp USING(paabgabetyp_kurzbz)
WHERE campus.tbl_paabgabe.projektarbeit_id IN ?
ORDER BY campus.tbl_paabgabe.datum";
@@ -254,8 +240,7 @@ class Projektarbeit_model extends DB_Model
LEFT JOIN public.tbl_benutzer ben USING (person_id)
LEFT JOIN public.tbl_mitarbeiter ma ON ben.uid = ma.mitarbeiter_uid
WHERE (ben.aktiv OR ben.aktiv IS NULL)
AND projektarbeit_id = ?
AND lehre.tbl_projektbetreuer.betreuerart_kurzbz = ANY('{Begutachter, Betreuer, Erstbetreuer,Erstbegutachter,Senatsvorsitz}')";
AND projektarbeit_id = ?";
return $this->execReadOnlyQuery($qry, [$projektarbeit_id]);
}
@@ -309,142 +294,34 @@ class Projektarbeit_model extends DB_Model
* Get a List of Projektarbeiten of a mitarbeiter with zuordnung
* used by the mitarbeiter cis4 abgabetool.
*/
public function getMitarbeiterProjektarbeiten($person_id, $showAll){
$qry = "WITH secondary_betreuer AS (
SELECT DISTINCT ON (pb.projektarbeit_id)
pb.projektarbeit_id,
pb.person_id AS zweitbetreuer_person_id,
b.uid AS zweitbetreuer_benutzer_uid,
pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz,
ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung,
p.titelpre AS zweitbetreuer_titelpre,
p.vorname AS zweitbetreuer_vorname,
p.nachname AS zweitbetreuer_nachname,
p.titelpost AS zweitbetreuer_titelpost,
trim(
COALESCE(p.titelpre, '') || ' ' ||
COALESCE(p.vorname, '') || ' ' ||
COALESCE(p.nachname, '') || ' ' ||
COALESCE(p.titelpost, '')
) AS zweitbetreuer_full_name
FROM lehre.tbl_projektbetreuer pb
JOIN public.tbl_person p ON p.person_id = pb.person_id
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}')
ORDER BY pb.projektarbeit_id
),
primary_betreuer AS (
SELECT DISTINCT ON (pb.projektarbeit_id)
pb.projektarbeit_id,
pb.person_id AS betreuer_person_id,
pb.betreuerart_kurzbz AS betreuer_betreuerart_kurzbz,
ba.beschreibung AS betreuer_betreuerart_beschreibung,
p.titelpre AS betreuer_titelpre,
p.vorname AS betreuer_vorname,
p.nachname AS betreuer_nachname,
p.titelpost AS betreuer_titelpost,
trim(
COALESCE(p.titelpre, '') || ' ' ||
COALESCE(p.vorname, '') || ' ' ||
COALESCE(p.nachname, '') || ' ' ||
COALESCE(p.titelpost, '')
) AS betreuer_full_name
FROM lehre.tbl_projektbetreuer pb
JOIN public.tbl_person p ON p.person_id = pb.person_id
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
WHERE pb.betreuerart_kurzbz = ANY('{Begutachter, Betreuer, Erstbetreuer,Erstbegutachter,Senatsvorsitz}')
ORDER BY pb.projektarbeit_id
)
SELECT
*
FROM
(SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id)
student_person.vorname, student_person.nachname,
tbl_studiengang.typ, tbl_studiengang.kurzbz,
tbl_projektarbeit.projekttyp_kurzbz, tbl_projekttyp.bezeichnung,
tbl_projektarbeit.titel, tbl_projektarbeit.projektarbeit_id, tbl_projektarbeit.note,
student_benutzer.uid, tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, public.tbl_student.student_uid,
lehre.tbl_projektarbeit.sprache,
lehre.tbl_projektarbeit.seitenanzahl,
lehre.tbl_projektarbeit.kontrollschlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter_en,
lehre.tbl_projektarbeit.abstract,
lehre.tbl_projektarbeit.abstract_en,
lehre.tbl_projektarbeit.insertamum,
(
SELECT orgform_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = (
SELECT prestudent_id FROM public.tbl_student
WHERE student_uid = student_benutzer.uid LIMIT 1
)
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS organisationsform,
(
SELECT COALESCE(tbl_studienplan.orgform_kurzbz,
tbl_prestudentstatus.orgform_kurzbz,
tbl_studiengang.orgform_kurzbz)
FROM public.tbl_prestudent
JOIN public.tbl_prestudentstatus USING (prestudent_id)
JOIN public.tbl_studiensemester USING (studiensemester_kurzbz)
JOIN public.tbl_studiengang USING (studiengang_kz)
LEFT JOIN lehre.tbl_studienplan USING (studienplan_id)
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY tbl_prestudentstatus.datum DESC
LIMIT 1
) AS orgform,
(
SELECT status_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS studienstatus,
pb.betreuer_person_id,
pb.betreuer_betreuerart_kurzbz,
pb.betreuer_betreuerart_beschreibung,
pb.betreuer_titelpre,
pb.betreuer_vorname,
pb.betreuer_nachname,
pb.betreuer_titelpost,
pb.betreuer_full_name,
sb.zweitbetreuer_person_id,
sb.zweitbetreuer_benutzer_uid,
sb.zweitbetreuer_betreuerart_kurzbz,
sb.zweitbetreuer_betreuerart_beschreibung,
sb.zweitbetreuer_titelpre,
sb.zweitbetreuer_vorname,
sb.zweitbetreuer_nachname,
sb.zweitbetreuer_titelpost,
sb.zweitbetreuer_full_name
FROM lehre.tbl_projektarbeit
LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid
LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id
LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
LEFT JOIN primary_betreuer pb ON pb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
WHERE (projekttyp_kurzbz='Bachelor' OR projekttyp_kurzbz='Diplom')
AND (pb.betreuer_person_id = ? OR sb.zweitbetreuer_person_id = ?)
".($showAll?'':' AND student_benutzer.aktiv AND lehre.tbl_projektarbeit.note IS NULL ')."
public function getMitarbeiterProjektarbeiten($uid, $showAll){
$qry = "SELECT
*
FROM
(SELECT tbl_person.vorname, tbl_person.nachname, tbl_studiengang.typ, tbl_studiengang.kurzbz,
tbl_projektarbeit.projekttyp_kurzbz, tbl_projekttyp.bezeichnung, tbl_projektarbeit.titel, tbl_projektarbeit.projektarbeit_id, tbl_projektarbeit.note,
tbl_projektbetreuer.person_id as betreuer_person_id, tbl_projektbetreuer.betreuerart_kurzbz, tbl_betreuerart.beschreibung AS betreuerart_beschreibung,
tbl_benutzer.uid, tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz, public.tbl_student.student_uid
FROM lehre.tbl_projektarbeit
LEFT JOIN lehre.tbl_projektbetreuer using(projektarbeit_id)
LEFT JOIN lehre.tbl_betreuerart using(betreuerart_kurzbz)
LEFT JOIN public.tbl_benutzer on(uid=student_uid)
LEFT JOIN public.tbl_student on(public.tbl_benutzer.uid=public.tbl_student.student_uid)
LEFT JOIN public.tbl_person on(tbl_benutzer.person_id=tbl_person.person_id)
LEFT JOIN lehre.tbl_lehreinheit using(lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung using(lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang on(lehre.tbl_lehrveranstaltung.studiengang_kz=public.tbl_studiengang.studiengang_kz)
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
WHERE (projekttyp_kurzbz='Bachelor' OR projekttyp_kurzbz='Diplom')
AND tbl_projektbetreuer.person_id IN (SELECT person_id FROM public.tbl_benutzer
WHERE public.tbl_benutzer.person_id=lehre.tbl_projektbetreuer.person_id
AND public.tbl_benutzer.uid= ? )
".($showAll?'':' AND public.tbl_benutzer.aktiv AND lehre.tbl_projektarbeit.note IS NULL ')."
AND betreuerart_kurzbz IN ('Betreuer', 'Begutachter', 'Erstbegutachter', 'Zweitbegutachter', 'Erstbetreuer', 'Senatsvorsitz', 'Senatsmitglied')
ORDER BY tbl_projektarbeit.projektarbeit_id, betreuerart_kurzbz desc) as xy
ORDER BY tbl_projektarbeit.projektarbeit_id, betreuerart_kurzbz desc) as xy
ORDER BY nachname;";
return $this->execReadOnlyQuery($qry, array($person_id, $person_id));
return $this->execReadOnlyQuery($qry, array($uid));
}
/**
@@ -464,257 +341,176 @@ class Projektarbeit_model extends DB_Model
public function getProjektarbeitenForStudiengang($studiengang_kz, $benotet) {
$new_qry = "WITH secondary_betreuer AS (
SELECT DISTINCT ON (pb.projektarbeit_id)
pb.projektarbeit_id,
pb.person_id AS zweitbetreuer_person_id,
pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz,
ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung,
p.titelpre AS zweitbetreuer_titelpre,
p.vorname AS zweitbetreuer_vorname,
p.nachname AS zweitbetreuer_nachname,
p.titelpost AS zweitbetreuer_titelpost,
trim(
COALESCE(p.titelpre, '') || ' ' ||
COALESCE(p.vorname, '') || ' ' ||
COALESCE(p.nachname, '') || ' ' ||
COALESCE(p.titelpost, '')
) AS zweitbetreuer_full_name
FROM lehre.tbl_projektbetreuer pb
JOIN public.tbl_person p ON p.person_id = pb.person_id
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}')
ORDER BY pb.projektarbeit_id -- DISTINCT ON needs this to be deterministic
)
$new_qry = "SELECT DISTINCT ON(tmp.projektarbeit_id) *, campus.get_betreuer_details(tmp.zweitbetreuer_person_id) as zweitbetreuer_full_name, campus.get_betreuer_details(tmp.betreuer_person_id) as erstbetreuer_full_name
FROM(
SELECT
DISTINCT ON(tbl_projektarbeit.projektarbeit_id)
tbl_projektarbeit.projekttyp_kurzbz,
tbl_projektarbeit.titel,
tbl_projektarbeit.projektarbeit_id,
tbl_studiengang.typ, tbl_studiengang.kurzbz,
student_benutzer.uid as student_uid,
student_person.vorname as student_vorname,
student_person.nachname as student_nachname,
tbl_student.matrikelnr, tbl_lehreinheit.studiensemester_kurzbz,
betreuer_benutzer.uid as betreuer_benutzer_uid,
betreuer_person.titelpre as betreuer_titelpre,
betreuer_person.vorname as betreuer_vorname,
betreuer_person.nachname as betreuer_nachname,
betreuer_person.titelpost as betreuer_titelpost,
lehre.tbl_projektbetreuer.betreuerart_kurzbz as betreuerart,
lehre.tbl_projektbetreuer.person_id as betreuer_person_id,
lehre.tbl_projektarbeit.sprache as sprache,
lehre.tbl_projektarbeit.seitenanzahl as seitenanzahl,
lehre.tbl_projektarbeit.kontrollschlagwoerter as kontrollschlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter as schlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter_en as schlagwoerter_en,
lehre.tbl_projektarbeit.abstract as abstract,
lehre.tbl_projektarbeit.abstract_en as abstract_en,
lehre.tbl_projektarbeit.insertamum as insertamum,
lehre.tbl_projektarbeit.note as note,
(
SELECT orgform_kurzbz
FROM tbl_prestudentstatus
WHERE prestudent_id = (SELECT prestudent_id
FROM tbl_student
WHERE student_uid = student_benutzer.uid
LIMIT 1)
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
)
as organisationsform,
(
SELECT person_id
FROM lehre.tbl_projektbetreuer
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
AS zweitbetreuer_person_id,
(
SELECT betreuerart_kurzbz
FROM lehre.tbl_projektbetreuer
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
AS zweitbetreuer_betreuerart_kurzbz,
(
SELECT tbl_betreuerart.beschreibung
FROM lehre.tbl_projektbetreuer
JOIN lehre.tbl_betreuerart USING (betreuerart_kurzbz)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
AS zweitbetreuer_betreuerart_beschreibung,
(
SELECT trim(COALESCE(titelpre, '') || ' ' || COALESCE(vorname, '') || ' ' || COALESCE(nachname, '') || ' ' ||
COALESCE(titelpost, ''))
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_full_name,
(
SELECT titelpre
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_titelpre,
(
SELECT vorname
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_vorname,
(
SELECT nachname
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_nachname,
(
SELECT titelpost
FROM public.tbl_person
JOIN lehre.tbl_projektbetreuer ON (lehre.tbl_projektbetreuer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_benutzer ON (public.tbl_benutzer.person_id = public.tbl_person.person_id)
LEFT JOIN public.tbl_mitarbeiter ON (public.tbl_benutzer.uid = public.tbl_mitarbeiter.mitarbeiter_uid)
WHERE projektarbeit_id = tbl_projektarbeit.projektarbeit_id
AND betreuerart_kurzbz IN ('Zweitbetreuer', 'Zweitbegutachter', 'Senatsmitglied')
LIMIT 1
)
as zweitbetreuer_titelpost,
(
SELECT
COALESCE(tbl_studienplan.orgform_kurzbz,
tbl_prestudentstatus.orgform_kurzbz, tbl_studiengang.orgform_kurzbz) as
orgform
FROM
public.tbl_prestudent
JOIN public.tbl_prestudentstatus USING(prestudent_id)
JOIN public.tbl_studiensemester USING(studiensemester_kurzbz)
JOIN public.tbl_studiengang USING(studiengang_kz)
LEFT JOIN lehre.tbl_studienplan USING(studienplan_id)
WHERE
prestudent_id=tbl_student.prestudent_id
ORDER BY tbl_prestudentstatus.datum DESC LIMIT 1
) as orgform,
(SELECT status_kurzbz FROM public.tbl_prestudentstatus
WHERE prestudent_id=tbl_student.prestudent_id
ORDER BY datum DESC, insertamum DESC, ext_id DESC LIMIT 1) as studienstatus
FROM lehre.tbl_projektarbeit
LEFT JOIN public.tbl_benutzer student_benutzer ON (student_benutzer.uid = lehre.tbl_projektarbeit.student_uid)
LEFT JOIN public.tbl_person student_person ON (student_benutzer.person_id = student_person.person_id)
LEFT JOIN public.tbl_student on(student_benutzer.uid = public.tbl_student.student_uid)
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang ON (public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz)
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
LEFT JOIN public.tbl_person betreuer_person ON (betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id)
LEFT JOIN public.tbl_benutzer betreuer_benutzer ON (betreuer_person.person_id = betreuer_benutzer.person_id)
WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom')
AND student_benutzer.aktiv AND (
lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbegutachter'
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Begutachter'
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Betreuer'
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Erstbetreuer'
OR lehre.tbl_projektbetreuer.betreuerart_kurzbz = 'Senatsvorsitz'
)
AND public.tbl_studiengang.studiengang_kz = ?";
SELECT DISTINCT ON (tmp.projektarbeit_id)
*,
campus.get_betreuer_details(tmp.zweitbetreuer_person_id) AS zweitbetreuer_full_name,
campus.get_betreuer_details(tmp.betreuer_person_id) AS erstbetreuer_full_name
FROM (
SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id)
tbl_projektarbeit.projekttyp_kurzbz,
tbl_projektarbeit.titel,
tbl_projektarbeit.projektarbeit_id,
tbl_studiengang.typ,
tbl_studiengang.kurzbz,
student_benutzer.uid AS student_uid,
student_person.vorname AS student_vorname,
student_person.nachname AS student_nachname,
public.tbl_student.matrikelnr,
tbl_lehreinheit.studiensemester_kurzbz,
betreuer_benutzer.uid AS betreuer_benutzer_uid,
betreuer_person.titelpre AS betreuer_titelpre,
betreuer_person.vorname AS betreuer_vorname,
betreuer_person.nachname AS betreuer_nachname,
betreuer_person.titelpost AS betreuer_titelpost,
lehre.tbl_projektbetreuer.betreuerart_kurzbz AS betreuerart,
lehre.tbl_projektbetreuer.person_id AS betreuer_person_id,
lehre.tbl_projektarbeit.sprache,
lehre.tbl_projektarbeit.seitenanzahl,
lehre.tbl_projektarbeit.kontrollschlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter_en,
lehre.tbl_projektarbeit.abstract,
lehre.tbl_projektarbeit.abstract_en,
lehre.tbl_projektarbeit.insertamum,
lehre.tbl_projektarbeit.note,
sb.zweitbetreuer_person_id,
sb.zweitbetreuer_betreuerart_kurzbz,
sb.zweitbetreuer_betreuerart_beschreibung,
sb.zweitbetreuer_full_name,
sb.zweitbetreuer_titelpre,
sb.zweitbetreuer_vorname,
sb.zweitbetreuer_nachname,
sb.zweitbetreuer_titelpost,
(
SELECT orgform_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = (
SELECT prestudent_id FROM public.tbl_student
WHERE student_uid = student_benutzer.uid LIMIT 1
)
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS organisationsform,
(
SELECT COALESCE(tbl_studienplan.orgform_kurzbz,
tbl_prestudentstatus.orgform_kurzbz,
tbl_studiengang.orgform_kurzbz)
FROM public.tbl_prestudent
JOIN public.tbl_prestudentstatus USING (prestudent_id)
JOIN public.tbl_studiensemester USING (studiensemester_kurzbz)
JOIN public.tbl_studiengang USING (studiengang_kz)
LEFT JOIN lehre.tbl_studienplan USING (studienplan_id)
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY tbl_prestudentstatus.datum DESC
LIMIT 1
) AS orgform,
(
SELECT status_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS studienstatus
FROM lehre.tbl_projektarbeit
LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid
LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id
LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
LEFT JOIN public.tbl_person betreuer_person ON betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id
LEFT JOIN public.tbl_benutzer betreuer_benutzer ON betreuer_person.person_id = betreuer_benutzer.person_id
LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom')
AND student_benutzer.aktiv
AND lehre.tbl_projektbetreuer.betreuerart_kurzbz IN (
'Erstbegutachter', 'Begutachter', 'Betreuer', 'Erstbetreuer', 'Senatsvorsitz'
)
AND public.tbl_studiengang.studiengang_kz = ?";
if($benotet == 0) {
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL ";
} else if ($benotet == 1) {
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL ";
}
$new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC
if($benotet == 0) {
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NULL ";
} else if ($benotet == 1) {
$new_qry .= " AND lehre.tbl_projektarbeit.note IS NOT NULL ";
}
$new_qry .= " ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC
) as tmp";
return $this->execReadOnlyQuery($new_qry, array($studiengang_kz));
}
public function getProjektarbeitenForStudent($student_uid) {
$new_qry = "WITH secondary_betreuer AS (
SELECT DISTINCT ON (pb.projektarbeit_id)
pb.projektarbeit_id,
pb.person_id AS zweitbetreuer_person_id,
pb.betreuerart_kurzbz AS zweitbetreuer_betreuerart_kurzbz,
ba.beschreibung AS zweitbetreuer_betreuerart_beschreibung,
p.titelpre AS zweitbetreuer_titelpre,
p.vorname AS zweitbetreuer_vorname,
p.nachname AS zweitbetreuer_nachname,
p.titelpost AS zweitbetreuer_titelpost,
trim(
COALESCE(p.titelpre, '') || ' ' ||
COALESCE(p.vorname, '') || ' ' ||
COALESCE(p.nachname, '') || ' ' ||
COALESCE(p.titelpost, '')
) AS zweitbetreuer_full_name
FROM lehre.tbl_projektbetreuer pb
JOIN public.tbl_person p ON p.person_id = pb.person_id
LEFT JOIN public.tbl_benutzer b ON b.person_id = p.person_id
LEFT JOIN lehre.tbl_betreuerart ba ON ba.betreuerart_kurzbz = pb.betreuerart_kurzbz
WHERE pb.betreuerart_kurzbz = ANY('{Zweitbetreuer,Zweitbegutachter,Senatsmitglied}')
ORDER BY pb.projektarbeit_id -- DISTINCT ON needs this to be deterministic
)
SELECT DISTINCT ON (tmp.projektarbeit_id)
*,
campus.get_betreuer_details(tmp.zweitbetreuer_person_id) AS zweitbetreuer_full_name,
campus.get_betreuer_details(tmp.betreuer_person_id) AS erstbetreuer_full_name
FROM (
SELECT DISTINCT ON (tbl_projektarbeit.projektarbeit_id)
tbl_projektarbeit.projekttyp_kurzbz,
tbl_projektarbeit.titel,
tbl_projektarbeit.projektarbeit_id,
tbl_studiengang.typ,
tbl_studiengang.kurzbz,
student_benutzer.uid AS student_uid,
student_person.vorname AS student_vorname,
student_person.nachname AS student_nachname,
public.tbl_student.matrikelnr,
tbl_lehreinheit.studiensemester_kurzbz,
betreuer_benutzer.uid AS betreuer_benutzer_uid,
betreuer_person.titelpre AS betreuer_titelpre,
betreuer_person.vorname AS betreuer_vorname,
betreuer_person.nachname AS betreuer_nachname,
betreuer_person.titelpost AS betreuer_titelpost,
lehre.tbl_projektbetreuer.betreuerart_kurzbz AS betreuerart,
lehre.tbl_projektbetreuer.person_id AS betreuer_person_id,
lehre.tbl_projektarbeit.sprache,
lehre.tbl_projektarbeit.seitenanzahl,
lehre.tbl_projektarbeit.kontrollschlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter,
lehre.tbl_projektarbeit.schlagwoerter_en,
lehre.tbl_projektarbeit.abstract,
lehre.tbl_projektarbeit.abstract_en,
lehre.tbl_projektarbeit.insertamum,
lehre.tbl_projektarbeit.note,
sb.zweitbetreuer_person_id,
sb.zweitbetreuer_betreuerart_kurzbz,
sb.zweitbetreuer_betreuerart_beschreibung,
sb.zweitbetreuer_full_name,
sb.zweitbetreuer_titelpre,
sb.zweitbetreuer_vorname,
sb.zweitbetreuer_nachname,
sb.zweitbetreuer_titelpost,
(
SELECT orgform_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = (
SELECT prestudent_id FROM public.tbl_student
WHERE student_uid = student_benutzer.uid LIMIT 1
)
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS organisationsform,
(
SELECT COALESCE(tbl_studienplan.orgform_kurzbz,
tbl_prestudentstatus.orgform_kurzbz,
tbl_studiengang.orgform_kurzbz)
FROM public.tbl_prestudent
JOIN public.tbl_prestudentstatus USING (prestudent_id)
JOIN public.tbl_studiensemester USING (studiensemester_kurzbz)
JOIN public.tbl_studiengang USING (studiengang_kz)
LEFT JOIN lehre.tbl_studienplan USING (studienplan_id)
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY tbl_prestudentstatus.datum DESC
LIMIT 1
) AS orgform,
(
SELECT status_kurzbz
FROM public.tbl_prestudentstatus
WHERE prestudent_id = public.tbl_student.prestudent_id
ORDER BY datum DESC, insertamum DESC, ext_id DESC
LIMIT 1
) AS studienstatus
FROM lehre.tbl_projektarbeit
LEFT JOIN public.tbl_benutzer student_benutzer ON student_benutzer.uid = lehre.tbl_projektarbeit.student_uid
LEFT JOIN public.tbl_person student_person ON student_benutzer.person_id = student_person.person_id
LEFT JOIN public.tbl_student ON student_benutzer.uid = public.tbl_student.student_uid
LEFT JOIN lehre.tbl_lehreinheit USING (lehreinheit_id)
LEFT JOIN lehre.tbl_lehrveranstaltung USING (lehrveranstaltung_id)
LEFT JOIN public.tbl_studiengang ON public.tbl_student.studiengang_kz = public.tbl_studiengang.studiengang_kz
LEFT JOIN lehre.tbl_projekttyp USING (projekttyp_kurzbz)
LEFT JOIN lehre.tbl_projektbetreuer USING (projektarbeit_id)
LEFT JOIN public.tbl_person betreuer_person ON betreuer_person.person_id = lehre.tbl_projektbetreuer.person_id
LEFT JOIN public.tbl_benutzer betreuer_benutzer ON betreuer_person.person_id = betreuer_benutzer.person_id
LEFT JOIN secondary_betreuer sb ON sb.projektarbeit_id = tbl_projektarbeit.projektarbeit_id
WHERE (projekttyp_kurzbz = 'Bachelor' OR projekttyp_kurzbz = 'Diplom')
AND student_benutzer.aktiv
AND lehre.tbl_projektbetreuer.betreuerart_kurzbz IN (
'Erstbegutachter', 'Begutachter', 'Betreuer', 'Erstbetreuer', 'Senatsvorsitz'
)
AND public.tbl_student.student_uid = ?
ORDER BY tbl_projektarbeit.projektarbeit_id DESC, student_person.nachname ASC
) as tmp";
return $this->execReadOnlyQuery($new_qry, array($student_uid));
}
/**
*
@@ -18,10 +18,10 @@ class Reservierung_model extends DB_Model
*
* @return stdClass
*/
public function getReservierungen($start_date, $end_date, $ort_kurzbz = null, $uid = null)
public function getReservierungen($start_date, $end_date, $ort_kurzbz = null)
{
$lvplan_reservierungen_query = "SELECT r.* , stund.beginn, stund.ende,
$lvplan_reservierungen_query="SELECT r.* , stund.beginn, stund.ende,
CASE
WHEN r.gruppe_kurzbz IS NOT NULL THEN r.gruppe_kurzbz
ELSE CONCAT(UPPER(studg.typ),UPPER(studg.kurzbz),'-',COALESCE(CAST(r.semester AS varchar),'/'),COALESCE(CAST(r.verband AS varchar),'/'))
@@ -35,7 +35,7 @@ class Reservierung_model extends DB_Model
LEFT JOIN public.tbl_studiensemester ss2 ON slv.studiensemester_kurzbz = ss2.studiensemester_kurzbz AND ss2.start <=r.datum AND ss2.ende >= r.datum
WHERE datum >= ? AND datum <= ? AND (ss1.studiensemester_kurzbz IS NOT NULL
OR ss2.studiensemester_kurzbz IS NOT NULL)";
$raum_reservierungen_query = "SELECT res.*, beginn, ende,
CASE
WHEN res.gruppe_kurzbz IS NOT NULL THEN res.gruppe_kurzbz
@@ -46,9 +46,9 @@ class Reservierung_model extends DB_Model
JOIN lehre.tbl_stunde ON lehre.tbl_stunde.stunde = res.stunde
WHERE res.ort_kurzbz = ? AND datum >= ? AND datum <= ?";
$subquery = is_null($ort_kurzbz) ? $lvplan_reservierungen_query : $raum_reservierungen_query;
$query_result = $this->execReadOnlyQuery("
$subquery = is_null($ort_kurzbz)? $lvplan_reservierungen_query:$raum_reservierungen_query;
$query_result= $this->execReadOnlyQuery("
SELECT
'reservierung' as type, beginn, ende, datum,
COALESCE(titel, beschreibung) as topic,
@@ -59,15 +59,15 @@ class Reservierung_model extends DB_Model
FROM
(
" . $subquery . "
". $subquery ."
) AS subquery
GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung
ORDER BY datum, beginn
", is_null($ort_kurzbz) ? [$uid ?? getAuthUID(), $uid ?? getAuthUID(), $start_date, $end_date] : [$ort_kurzbz, $start_date, $end_date]);
", is_null($ort_kurzbz) ?[getAuthUID(), getAuthUID(),$start_date,$end_date]: [$ort_kurzbz, $start_date, $end_date]);
return $query_result;
}
@@ -76,7 +76,7 @@ class Reservierung_model extends DB_Model
*
* @return stdClass
*/
public function getReservierungenMitarbeiter($start_date, $end_date, $uid = null)
public function getReservierungenMitarbeiter($start_date, $end_date)
{
$raum_reservierungen_query = "SELECT res.*, beginn, ende,
@@ -91,8 +91,8 @@ class Reservierung_model extends DB_Model
$subquery = $raum_reservierungen_query;
$query_result = $this->execReadOnlyQuery("
$query_result= $this->execReadOnlyQuery("
SELECT
'reservierung' as type, beginn, ende, datum,
COALESCE(titel, beschreibung) as topic,
@@ -103,13 +103,13 @@ class Reservierung_model extends DB_Model
FROM
(
" . $subquery . "
". $subquery ."
) AS subquery
GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung
ORDER BY datum, beginn
", [$uid ?? getAuthUID(), $start_date, $end_date]);
", [getAuthUID(), $start_date, $end_date]);
return $query_result;
@@ -129,9 +129,9 @@ class Reservierung_model extends DB_Model
$this->addJoin('public.tbl_studiensemester ss2', 'slv.studiensemester_kurzbz=ss2.studiensemester_kurzbz AND ss2.start<=r.datum AND ss2.ende>=r.datum', 'LEFT');
$this->db->or_where('ss1.studiensemester_kurzbz IS NOT NULL', null, false);
$this->db->or_where('ss2.studiensemester_kurzbz IS NOT NULL', null, false);
$query = $this->db->get_compiled_select('campus.vw_reservierung r');
return $this->execQuery($query, [$uid, $uid]);
}
@@ -388,84 +388,6 @@ class Stundenplan_model extends DB_Model
ORDER BY datum, beginn", [$start_date, $end_date, $ma_uid]);
}
/**
* queries Stundenplan and filters by studiengang, semester, verband gruppe
*
* @return void
*/
public function getStundenplanStudiengang($start_date, $end_date, $stg_kz, $sem, $verband, $gruppe) {
$qry_params = [$start_date, $end_date, $stg_kz];
$qry = "
SELECT
'lehreinheit' as type, beginn, ende, datum,
CONCAT(lehrfach,'-',lehrform) as topic,
array_agg(DISTINCT lektor) as lektor,
array_agg(DISTINCT (gruppe,verband,semester,studiengang_kz,gruppen_kuerzel)) as gruppe,
string_agg(DISTINCT ort_kurzbz, '/') as ort_kurzbz,
array_agg(DISTINCT lehreinheit_id) as lehreinheit_id,
titel, lehrfach, lehrform, lehrfach_bez, organisationseinheit, farbe, lehrveranstaltung_id
FROM
(
SELECT unr,datum,beginn, ende,
CASE
WHEN sp.mitarbeiter_kurzbz IS NOT NULL THEN sp.mitarbeiter_kurzbz
ELSE sp.lektor
END as lektor,
CASE
WHEN gruppe_kurzbz IS NOT NULL THEN gruppe_kurzbz
ELSE CONCAT(UPPER(sp.stg_typ),UPPER(sp.stg_kurzbz),'-',COALESCE(CAST(sp.semester AS varchar),'/'),COALESCE(CAST(sp.verband AS varchar),'/'))
END as gruppen_kuerzel,
(SELECT bezeichnung
FROM public.tbl_organisationseinheit
WHERE oe_kurzbz IN(
SELECT oe_kurzbz
FROM lehre.tbl_lehrveranstaltung
WHERE lehrveranstaltung_id = sp.lehrveranstaltung_id
)) as organisationseinheit,
sp.ort_kurzbz, sp.studiengang_kz, sp.titel,sp.lehreinheit_id,sp.lehrfach_id,sp.anmerkung,fix,lehrveranstaltung_id,stg_kurzbzlang,stg_bezeichnung,stg_typ,fachbereich_kurzbz,lehrfach,lehrfach_bez,farbe,lehrform,anmerkung_lehreinheit,gruppe, verband, semester,stg_kurzbz
FROM (
SELECT sp.*
FROM lehre.vw_stundenplan sp
WHERE
sp.datum >= ?
AND sp.datum <= ?
) sp
JOIN lehre.tbl_stunde ON lehre.tbl_stunde.stunde = sp.stunde
WHERE studiengang_kz = ? ";
if($sem != NULL)
{
$qry_params[] = $sem;
$qry .= " AND (semester = ? OR semester IS NULL)";
}
if($verband != NULL)
{
$qry_params[] = $verband;
$qry .= " AND (verband = ? OR verband IS NULL OR verband = '0' OR verband = '')";
}
if($gruppe != NULL)
{
$qry_params[] = $gruppe;
$qry .= " AND (gruppe = ? OR gruppe IS NULL OR gruppe = '0' OR gruppe = '') ";
}
$qry.= " AND (
gruppe_kurzbz is null OR EXISTS(
SELECT 1
FROM
public.tbl_gruppe WHERE gruppe_kurzbz = sp.gruppe_kurzbz AND direktinskription = false
)
)";
$qry.= " ) as subquery
GROUP BY unr, datum, beginn, ende, titel, lehrform, lehrfach, lehrfach_bez, organisationseinheit, farbe, lehrveranstaltung_id
ORDER BY datum, beginn; ";
return $this->execReadOnlyQuery($qry, $qry_params);
}
/**
* NO STANDALONE FUNCTION - Generates a SQL query string to fetch 'stundenplan' events for a specific student within the current semester.
+1 -1
View File
@@ -38,7 +38,7 @@ $includesArray = array(
$this->load->view('templates/FHC-Header', $includesArray);
?>
<div id="abgabetoolroot" class="h-100" style="max-width: 99%" route=<?php echo json_encode($route) ?>
<div id="abgabetoolroot" class="h-100" style="max-width: 95%;" route=<?php echo json_encode($route) ?>
uid=<?php echo $uid ?>
student_uid_prop="<?php echo $student_uid_prop ?? '' ?>"
stg_kz_prop="<?php echo $stg_kz_prop ?? '' ?>"
@@ -23,14 +23,12 @@ $includesArray = array(
'public/css/components/FormUnderline.css',
'public/css/components/abgabetool/abgabe.css',
'public/css/Cis4/Cms.css',
'public/css/Cis4/Studium.css'
'public/css/Cis4/Studium.css',
),
'customJSs' => array(
'vendor/npm-asset/primevue/accordion/accordion.min.js',
'vendor/npm-asset/primevue/accordiontab/accordiontab.min.js',
'vendor/npm-asset/primevue/checkbox/checkbox.min.js',
'vendor/npm-asset/primevue/chips/chips.min.js',
'vendor/npm-asset/primevue/multiselect/multiselect.min.js',
'vendor/npm-asset/primevue/inputnumber/inputnumber.min.js',
'vendor/npm-asset/primevue/speeddial/speeddial.min.js',
'vendor/npm-asset/primevue/textarea/textarea.min.js',
@@ -41,7 +39,7 @@ $includesArray = array(
'vendor/moment/luxonjs/luxon.min.js'
),
'customJSModules' => array(
'public/js/apps/Cis/Cis.js',
'public/js/apps/Dashboard/Fhc.js',
),
);
@@ -49,6 +47,8 @@ $includesArray = array(
$this->load->view('templates/CISVUE-Header', $includesArray);
?>
<div id="fhccontent" class="h-100" route=<?php echo $route ?>>
<router-view></router-view>
<router-view
:view-data='<?php echo json_encode($viewData) ?>'
></router-view>
</div>
<?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?>
@@ -6,7 +6,7 @@ $includesArray = array(
'fontawesome6' => true,
'axios027' => true,
'customJSModules' => array_merge([
'public/js/apps/Cis/Menu.js'
'public/js/apps/Cis.js'
], $customJSModules ?? []),
'customCSSs' => array_merge([
'public/css/Cis4/Cis.css'
@@ -8,7 +8,7 @@ $includesArray = array(
'axios027' => true,
'primevue3' => true,
'customJSModules' => array_merge([
'public/js/apps/Cis/Menu.js'
'public/js/apps/Cis.js'
], $customJSModules ?? []),
'customCSSs' => array_merge([
'public/css/Cis4/Cis.css',
-18
View File
@@ -70,18 +70,6 @@
}
}
},
{
"type": "package",
"package": {
"name": "drag-drop-touch-js/dragdroptouch",
"version": "2.0.3",
"source": {
"url": "https://github.com/drag-drop-touch-js/dragdroptouch.git",
"type": "git",
"reference": "master"
}
}
},
{
"type": "package",
"package": {
@@ -464,8 +452,6 @@
"easyrdf/easyrdf": "0.9.*",
"drag-drop-touch-js/dragdroptouch": "*",
"fgelinas/timepicker": "0.3.3",
"fortawesome/font-awesome4": "4.7.*",
"fortawesome/font-awesome6": "6.1.*",
@@ -533,9 +519,5 @@
"phpmetrics/phpmetrics": "2.*",
"sebastian/phpcpd": "3.*",
"phpunit/phpunit": "^6"
},
"scripts": {
"post-install-cmd": "@symlink_vendor_to_public",
"symlink_vendor_to_public": "ln -sfn ../vendor ./public/"
}
}
Generated
+1 -11
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "869cbc35bd1ba90ab90934fcb41b0f51",
"content-hash": "f4f0af4586f46f97d8b6092c1ac0fb3a",
"packages": [
{
"name": "afarkas/html5shiv",
@@ -804,16 +804,6 @@
"abandoned": true,
"time": "2018-03-09T06:07:41+00:00"
},
{
"name": "drag-drop-touch-js/dragdroptouch",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/drag-drop-touch-js/dragdroptouch.git",
"reference": "master"
},
"type": "library"
},
{
"name": "easyrdf/easyrdf",
"version": "0.9.1",
+1 -1
View File
@@ -350,7 +350,7 @@ class benutzerberechtigung extends basis_db
};
$roles = defined('STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_ROLES')
? implode(', ', array_map($mapfunc, unserialize(STUDENTS_KEEP_PERMISSIONS_AFTER_USER_INACTIVE_ROLES)))
: "'NO_DEFINED_ROLE'";
: 'NO_DEFINED_ROLE';
$sql = <<<EOSQL
SELECT
+35 -28
View File
@@ -147,8 +147,6 @@ html {
--fhc-cis-menu-lvl-5-color-hover: var(--fhc-text);
--fhc-cis-grade-positive: var(--fhc-success);
--fhc-cis-grade-negative: var(--fhc-danger);
--fhc-offcanvas-zindex: 1045;
}
#themeSwitch i{
@@ -377,7 +375,7 @@ html {
/* searchbar */
#nav-search {
background-color: var(--fhc-primary);
z-index: calc(var(--fhc-offcanvas-zindex) + 1) !important;
z-index: 1;
}
#nav-search.me-3 {
margin: 0 !important;
@@ -388,6 +386,12 @@ html {
#nav-search > .input-group > * {
border-radius: 0 !important;
}
#nav-search .searchbar_results {
top: 100% !important;
left: 0;
right: 0 !important;
width: 100% !important;
}
/* frame */
.in-frame {
@@ -409,18 +413,10 @@ html {
color: var(--fhc-link) !important;
}
#nav-main {
z-index: var(--fhc-offcanvas-zindex);
}
#nav-main-sticky {
max-height: calc(100vh - var(--fhc-cis-header-height));
}
#nav-user-menu {
z-index: calc(var(--fhc-offcanvas-zindex) + 1);
}
#nav-user-menu img {
object-fit: cover;
height: calc( 3 * var(--fhc-cis-header-py));
@@ -461,14 +457,7 @@ html {
/* overflow: visible !important; */
}
#cis-header {
z-index: 10;
}
#cis-header-bar {
position: fixed;
top: 0;
height: var(--fhc-cis-header-height);
width: 100%;
background-color: var(--fhc-primary);
z-index: 10;
}
#cis-header nav {
position: initial;
@@ -484,7 +473,12 @@ html {
display: none;
}
#nav-logo {
position: fixed;
top: 0;
left: 0;
height: var(--fhc-cis-header-height);
width: var(--fhc-cis-menu-width);
background-color: var(--fhc-primary);
padding: var(--fhc-cis-header-py) var(--fhc-cis-header-px);
z-index: 2;
}
@@ -499,27 +493,37 @@ html {
top: var(--fhc-cis-header-height);
display: flex;
flex-direction: column;
height: 100%;
}
#nav-main-sticky > :not(#nav-main-toggle) {
overflow: auto;
}
#nav-main-toggle {
width: 0;
z-index: 1;
}
#nav-main-toggle:hover {
background-color: var(--fhc-secondary-highlight);
}
#nav-main-toggle .div,
#nav-main-toggle .fa-chevron-left {
#nav-main-toggle .btn,
#nav-main-toggle .fa-arrow-circle-left {
transition: all 0.5s ease-in-out;
}
#nav-main-toggle .collapsed.btn {
background-color: transparent !important;
}
#nav-main-toggle .collapsed .fa-chevron-left {
#nav-main-toggle .collapsed .fa-arrow-circle-left {
transform: scaleX(-1);
color: var(--fhc-black-40);
}
#nav-search {
position: relative;
position: fixed;
top: 0;
left: var(--fhc-cis-menu-width);
height: var(--fhc-cis-header-height);
right: calc(var(--fhc-cis-header-height) + 2 * var(--fhc-cis-header-px) - 2 * var(--fhc-cis-header-py));
width: auto !important;
}
#nav-user {
position: fixed;
top: 0;
right: 0;
}
#nav-user-btn {
border-width: 0;
@@ -554,7 +558,7 @@ html {
#nav-main-menu {
height: 100%;
background-color: var(--fhc-primary);
background-color: var(--fhc-cis-menu-bg);
}
#nav-main-menu > div {
width: var(--fhc-cis-menu-width);
@@ -596,6 +600,9 @@ html {
}
#nav-user{
position: relative;
}
#nav-user-btn {
}
#nav-user-btn img {
object-fit: cover;
-4
View File
@@ -277,7 +277,3 @@ html.fs_huge {
}
*/
/* slim ende */
.fhc-xxl-modal {
min-width: 80vw;
}
+1 -101
View File
@@ -305,104 +305,4 @@
/* If you use hover rows, you need to ensure the sticky cell matches the hover color */
#abgabetable .tabulator-row:hover .tabulator-cell.sticky-col {
background-color: #ccc; /* Match your existing hover color */
}
.tabulator-cell {
container-type: inline-size;
}
.tabulator-col-title {
container-type: inline-size;
}
@container (max-width: 100px) {
.full-text {
display: none !important;
}
.short-text {
display: inline-block !important;
width: 100%;
}
}
/*conditional tooltips fix*/
.p-tooltip.custom-tooltip {
z-index: 8001 !important;
}
/* Shrinks font and table rows for desktop users who have zoomed in their browser (150%+).
Does not affect mobile/touchscreen devices, which use touch input instead of a mouse. */
@media (pointer: fine) and (min-resolution: 1.5dppx) {
html.abgabetool {
font-size: 0.5rem;
}
.abgabetool .tabulator-cell,
.abgabetool .tabulator-row {
height: 20px;
max-height: 20px;
}
}
/*confetti celebration on endupload - impossible to miss*/
#confetti-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
overflow: hidden;
}
.confetti-piece {
position: absolute;
opacity: 0;
will-change: top, transform, opacity;
}
/* Background Rain */
@keyframes fallAndSpin {
0% {
top: var(--start-y);
transform: translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg);
opacity: 1;
}
100% {
top: 105vh;
transform: translate3d(var(--drift), 0, 0) rotateX(720deg) rotateY(360deg);
opacity: 0.3;
}
}
/* Corner Cannons*/
@keyframes cannonBlast {
0% {
transform: translate3d(0, 0, 0) scale(0.3) rotate(0deg);
opacity: 1;
animation-timing-function: cubic-bezier(0.1, 0.8, 0.2, 1);
}
30% {
transform: translate3d(var(--blast-x), var(--blast-y), 0) scale(1.2) rotate(270deg);
opacity: 1;
animation-timing-function: linear;
}
100% {
transform: translate3d(calc(var(--blast-x) * 1.4), 15vh, 0) scale(0.4) rotate(630deg);
opacity: 0;
}
}
.p-timeline-event-opposite {
flex: 0 0 90px;
max-width: 90px;
}
.p-timeline-event-content {
flex: 1 1 auto;
min-width: 0;
}
}
+52 -60
View File
@@ -2,7 +2,7 @@
@import './dashboard/news.css';
@import './dashboard/LvPlan.css';
:root {
:root{
--fhc-dashboard-danger: var(--fhc-danger, #842029);
--fhc-dashboard-grid-size: 4;
--fhc-dashboard-link: var(--fhc-link, #0a57ca);
@@ -17,16 +17,22 @@
--fhc-dashboard-section-info-color-hover: var(--fhc-primary-highlight, #005585);
}
.core-dashboard a {
@media(max-width: 577px) {
:root {
--fhc-dashboard-grid-size: 1;
}
}
.core-dashboard a{
color: var(--fhc-dashboard-link);
}
@media (max-width: 576px) {
@media (max-width: 576px){
.widget-icon {
max-height: 250px;
object-fit: cover;
}
.widget-icon-container {
.widget-icon-container{
max-width: 250px;
margin-left: auto;
margin-right: auto;
@@ -40,36 +46,27 @@
background-repeat: no-repeat;
background-position: center;
background-size: cover;
cursor: pointer;
cursor:pointer;
}
.dashboard-section.edit-active {
/**
* replaces margin for extra row
* 10% equals 0.1 of 100%
* 1rem equals the padding of pb-3 that is overwritten here
*/
padding-bottom: calc(10% / var(--fhc-dashboard-grid-size) + 1rem) !important;
}
.dashboard-section > .newGridRow {
position: absolute;
width: 20px;
height: 20px;
padding: 0;
bottom: 0;
left: 50%;
transform: translate(-50%, 50%);
.dashboard-section > .newGridRow{
position:absolute;
width:20px;
height:20px;
padding:0;
bottom:0;
left:50%;
transform:translate(-50%, 50%);
background-color: var(--fhc-dashboard-gridrow-background);
}
.newGridRow:hover {
color: white;
color:white;
background-color: var(--fhc-dashboard-gridrow-background-highlight);
}
.empty-tile-hover:hover {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-500 -500 1448 1512"><path fill="rgb(211, 211, 211)" d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM200 344V280H136c-13.3 0-24-10.7-24-24s10.7-24 24-24h64V168c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24H248v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z"/></svg>');
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="-500 -500 1448 1512"><path fill="rgb(211, 211, 211)" d="M64 32C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64H384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64H64zM200 344V280H136c-13.3 0-24-10.7-24-24s10.7-24 24-24h64V168c0-13.3 10.7-24 24-24s24 10.7 24 24v64h64c13.3 0 24 10.7 24 24s-10.7 24-24 24H248v64c0 13.3-10.7 24-24 24s-24-10.7-24-24z"/></svg>');
}
.alert-danger .form-check-input:checked {
@@ -77,6 +74,16 @@
background-color: var(--fhc-dashboard-danger);
}
:root {
--fhc-dashboard-grid-size: 4;
}
@media(max-width: 1400px) {
:root {
--fhc-dashboard-grid-size: 4;
}
}
@media(max-width: 1200px) {
:root {
--fhc-dashboard-grid-size: 3;
@@ -98,7 +105,6 @@
@media(max-width: 577px) {
:root {
--fhc-dashboard-grid-size: 1;
--fhc-dg-item-py: .75rem;
}
}
@@ -126,64 +132,50 @@
cursor: move !important;
}
.drop-grid-item-resize > .dashboard-item,
.drop-grid-item-move > .dashboard-item {
.draggedItem {
height: 100%;
width: 100%;
background-color: var(--fhc-dashboard-draggeditem-background);
position: relative;
}
.drop-grid-item-resize > .dashboard-item > *,
.drop-grid-item-move > .dashboard-item > * {
display: none;
}
.drop-grid-item-sizechanged > .dashboard-item,
.drop-grid-item-move > .dashboard-item {
.dashboard-item-overlay{
background-color: var(--fhc-dashboard-item-overlay-background);
}
.drop-grid-item-sizechanged > .dashboard-item::before,
.drop-grid-item-move > .dashboard-item::before {
position: absolute;
content: "";
top: .25rem;
left: .25rem;
right: .25rem;
bottom: .25rem;
border: 4px dashed var(--fhc-dashboard-item-overly-border-color);
opacity: .5;
.dashboard-item-overlay::before{
position:absolute;
content:"";
top:0.25rem;
left:0.25rem;
right:0.25rem;
bottom:0.25rem;
border:4px dashed var(--fhc-dashboard-item-overly-border-color);
opacity: 0.5;
}
.drop-grid-item-oversized > .dashboard-item {
/* Bootstrap: border-danger */
--bs-border-opacity: 1;
border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important;
}
#deleteBookmark i {
#deleteBookmark i{
color: var(--fhc-dashboard-danger);
}
.pin:hover {
.pin:hover{
cursor: pointer;
}
.pin[pinned]:hover {
.pin[pinned]:hover{
color: var(--fhc-dashboard-pin-pinned-hover-color);
}
.section-info {
.section-info{
color: var(--fhc-dashboard-section-info-color);
cursor: pointer;
cursor:pointer;
}
.section-info:hover {
color: var(--fhc-dashboard-section-info-color-hover);
}
.drop-grid-item-blocker [pinned='true'] {
.denied-dragging-animation {
animation: wiggle 0.5s linear;
color: var(--fhc-dashboard-denied-dragging-animation-color) !important;
}
@@ -212,13 +204,13 @@
}
.hidden-widget {
.hiddenWidget{
background: var(--fhc-disabled-background);
opacity: 40%;
}
.hidden-widget .card,
.hidden-widget .card-body,
.hidden-widget .card-body * {
.hiddenWidget .card,
.hiddenWidget .card-body,
.hiddenWidget .card-body *{
background: inherit !important;
}
-38
View File
@@ -77,13 +77,6 @@ export default {
}
};
},
patchProjektarbeitAbgabeMultiple(payload) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/patchProjektarbeitAbgabeMultiple',
params: payload
};
},
deleteProjektarbeitAbgabe(paabgabe_id) {
return {
method: 'post',
@@ -91,13 +84,6 @@ export default {
params: { paabgabe_id }
};
},
deleteProjektarbeitAbgabeMultiple(paabgabe_ids) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/deleteProjektarbeitAbgabeMultiple',
params: { paabgabe_ids }
};
},
postSerientermin(datum, paabgabetyp_kurzbz, bezeichnung, kurzbz, upload_allowed, projektarbeit_ids, fixtermin) {
return {
method: 'post',
@@ -153,30 +139,6 @@ export default {
url: '/api/frontend/v1/Abgabe/getSignaturStatusForProjektarbeitAbgaben',
params: {paabgabe_ids, student_uid},
};
},
postStudentProjektarbeitTitel(projektarbeit_id, titel) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/postStudentProjektarbeitTitel',
params: {projektarbeit_id, titel},
};
},
sendZweitbetreuerTokenMail(projektarbeit_id, betreuer_person_id, student_uid) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/sendZweitbetreuerTokenMail',
params: {projektarbeit_id, bperson_id: betreuer_person_id, student_uid},
};
},
fetchProjektarbeitenHistory(student_uid) {
return {
method: 'post',
url: '/api/frontend/v1/Abgabe/fetchProjektarbeitenHistory',
params: {student_uid},
};
}
};
+1 -1
View File
@@ -19,7 +19,7 @@ export default {
getViewData() {
return {
method: 'get',
url: '/api/frontend/v1/Cis4FhcApi/dashboardViewData'
url: '/api/frontend/v1/Cis4FhcApi/getViewData'
};
}
};
-26
View File
@@ -1,26 +0,0 @@
/**
* Copyright (C) 2026 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getAllStudienSemester(studiensemester, studiengang, semester, studienplan) {
return {
method: 'get',
url: 'api/frontend/v1/Studium/getStudienAllSemester/',
params: {studiensemester, studiengang, semester, studienplan}
};
},
}
-6
View File
@@ -35,11 +35,5 @@ export default {
method: 'get',
url: `/api/frontend/v1/Lehre/Pruefungen/${lehrveranstaltung_id}`
};
},
getSemesterAverageGrade(semester) {
return {
method: 'get',
url: `/api/frontend/v1/Lehre/semesterAverageGrade/${semester}`
}
}
};
+5 -48
View File
@@ -16,12 +16,6 @@
*/
export default {
getMyLvPlanViewData() {
return {
method: 'get',
url: `/api/frontend/v1/LvPlan/myLvPlanViewData`,
};
},
getRoomInfo(ort_kurzbz, start_date, end_date) {
return {
method: 'post',
@@ -36,11 +30,11 @@ export default {
params: { start_date, end_date, lv_id }
};
},
eventsPersonal(start_date, end_date, uid = null) {
eventsPersonal(start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/lvPlan/eventsPersonal',
params: { start_date, end_date, uid }
params: { start_date, end_date }
};
},
eventsLv(lv_id, start_date, end_date) {
@@ -63,11 +57,11 @@ export default {
params: { start_date, end_date }
};
},
getLvPlanReservierungen(start_date, end_date, uid = null) {
getLvPlanReservierungen(start_date, end_date) {
return {
method: 'post',
url: '/api/frontend/v1/LvPlan/getReservierungen',
params: { start_date, end_date, uid }
params: { start_date, end_date }
};
},
getLehreinheitStudiensemester(lehreinheit_id) {
@@ -98,42 +92,5 @@ export default {
method: 'get',
url: '/api/frontend/v1/LvPlan/getLv/' + lehrveranstaltung_id
};
},
eventsStgOrg(start_date, end_date, stg_kz, sem, verband, gruppe) {
return {
method: 'post',
url: '/api/frontend/v1/lvPlan/eventsStgOrg',
params: { start_date, end_date, stg_kz, sem, verband, gruppe }
};
},
getStudiengaenge(){
return {
method: 'get',
url: '/api/frontend/v1/lvPlan/getStudiengaenge'
}
},
getLehrverband(stg_kz, sem){
return {
method: 'get',
url: `/api/frontend/v1/lvPlan/getLehrverband/${stg_kz}/${sem}`
}
},
getGruppe(stg_kz, sem, verband){
return {
method: 'get',
url: `/api/frontend/v1/lvPlan/getLehrverband/${stg_kz}/${sem}/${verband}`
}
},
checkPermissionOtherLvPlan(){
return {
method: 'get',
url: '/api/frontend/v1/lvPlan/permissionOtherLvPlan',
}
},
getCompactibleEventTypes(){
return {
method: 'get',
url: '/api/frontend/v1/lvPlan/compactibleEventTypes',
}
},
}
};
-25
View File
@@ -1,25 +0,0 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getOtherLvPlanViewData(uid) {
return {
method: 'get',
url: `/api/frontend/v1/OtherLvPlan/otherLvPlanViewData/${uid}`,
};
},
};
+1 -20
View File
@@ -41,24 +41,5 @@ export default {
method: 'get',
url: '/api/frontend/v1/phrasen/getAllLanguages'
};
},
getPhrases(phrasesGroupedByCategory, languages = []) {
return {
method: 'post',
url: '/api/frontend/v1/phrasen/getPhrases',
params: {
phrasesGroupedByCategory,
languages,
},
};
},
getTabulatorPhrases(languages = []) {
return {
method: 'get',
url: '/api/frontend/v1/phrasen/getTabulatorPhrases',
params: {
languages: JSON.stringify(languages),
},
};
},
}
};
+1 -1
View File
@@ -17,7 +17,7 @@
export default {
getProfilViewData(uid = null) {
profilViewData(uid) {
let url = "/api/frontend/v1/Profil/profilViewData";
if(uid){
url += `/${uid}`;
-25
View File
@@ -1,25 +0,0 @@
/**
* Copyright (C) 2025 fhcomplete.org
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export default {
getStgOrgLvPlanViewData(uid) {
return {
method: 'get',
url: `/api/frontend/v1/StgOrgLvPlan/stgOrgLvPlanViewData`,
};
},
};
+4 -11
View File
@@ -17,7 +17,6 @@
export default {
getBookmarks() {
return {
method: 'get',
url: '/api/frontend/v1/Bookmark/getBookmarks'
@@ -29,24 +28,18 @@ export default {
url: `/api/frontend/v1/Bookmark/delete/${bookmark_id}`
};
},
update({ bookmark_id, url, title, tag }) {
update({ bookmark_id, url, title, tag=null }) {
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/update/${bookmark_id}`,
params: { url, title, tag }
params: { url, title }
};
},
insert({ url, title, tag, sort }) {
insert({ url, title, tag }) {
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/insert`,
params: { url, title, tag, sort }
params: { url, title, tag }
};
},
changeOrder(bookmark_id1, bookmark_id2) {
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/changeOrder/${bookmark_id1}/${bookmark_id2}`,
};
}
};
+7 -4
View File
@@ -25,6 +25,9 @@ const app = Vue.createApp({
},
computed: {
viewData() {
return { uid: this.uid}
},
student_uid_computed() {
return this.student_uid ?? this.uid
},
@@ -52,10 +55,10 @@ const app = Vue.createApp({
},
template: `
<template v-if="comp && uid">
<AbgabetoolStudent v-if="comp == 'AbgabetoolStudent'" :student_uid_prop="student_uid_computed"></AbgabetoolStudent>
<AbgabetoolMitarbeiter v-if="comp == 'AbgabetoolMitarbeiter'"></AbgabetoolMitarbeiter>
<AbgabetoolAssistenz v-if="comp == 'AbgabetoolAssistenz'" :stg_kz_prop="stg_kz_computed"></AbgabetoolAssistenz>
<DeadlineOverview v-if="comp == 'DeadlinesOverview'"></DeadlineOverview>
<AbgabetoolStudent v-if="comp == 'AbgabetoolStudent'" :viewData="viewData" :student_uid_prop="student_uid_computed"></AbgabetoolStudent>
<AbgabetoolMitarbeiter v-if="comp == 'AbgabetoolMitarbeiter'" :viewData="viewData"></AbgabetoolMitarbeiter>
<AbgabetoolAssistenz v-if="comp == 'AbgabetoolAssistenz'" :viewData="viewData" :stg_kz_prop="stg_kz_computed"></AbgabetoolAssistenz>
<DeadlineOverview v-if="comp == 'DeadlinesOverview'" :viewData="viewData"></DeadlineOverview>
</template>
`
});
@@ -1,12 +1,11 @@
import FhcSearchbar from "../../components/searchbar/searchbar.js";
import CisMenu from "../../components/Cis/Menu.js";
import PluginsPhrasen from '../../plugins/Phrasen.js';
import Theme from "../../plugins/Theme.js";
import ApiSearchbar from '../../api/factory/searchbar.js';
import ApiLvPlan from "../../api/factory/lvPlan.js";
import FhcSearchbar from "../components/searchbar/searchbar.js";
import CisMenu from "../components/Cis/Menu.js";
import PluginsPhrasen from '../plugins/Phrasen.js';
import ApiSearchbar from '../api/factory/searchbar.js';
import Theme from "../plugins/Theme.js";
const app = Vue.createApp({
name: 'CisMenuApp',
name: 'CisApp',
components: {
FhcSearchbar,
CisMenu
@@ -134,61 +133,14 @@ const app = Vue.createApp({
childactions: []
}
}
},
windowWidth: 0,
}
};
},
provide() {
return {
isNarrow: Vue.computed(() => this.windowWidth < 992),
isMobile: Vue.computed(() => this.windowWidth < 767),
}
},
methods: {
searchfunction: function(searchsettings) {
return this.$api.call(ApiSearchbar.searchCis(searchsettings));
},
handleWindowResize() {
this.windowWidth = window.innerWidth;
},
},
created() {
this.windowWidth = window.innerWidth;
},
async mounted() {
const openOtherLvPlanAction = {
label: Vue.computed(() => this.$p.t("lehre/stundenplan")),
icon: "fas fa-calendar-days",
type: "link",
action: function (data) {
const uid = JSON.parse(data.data).uid;
const link =
FHC_JS_DATA_STORAGE_OBJECT.app_root +
FHC_JS_DATA_STORAGE_OBJECT.ci_router +
"/Cis/OtherLvPlan/" +
uid;
return link;
},
};
let checkPermissionOtherLvPlanResult = await this.$api.call(
ApiLvPlan.checkPermissionOtherLvPlan(),
);
if (
checkPermissionOtherLvPlanResult.meta.status === "success" &&
checkPermissionOtherLvPlanResult.data
) {
this.searchbaroptions.actions.employee.childactions.push(
openOtherLvPlanAction,
);
this.searchbaroptions.actions.student.childactions.push(
openOtherLvPlanAction,
);
}
window.addEventListener("resize", this.handleWindowResize);
},
beforeUnmount() {
window.removeEventListener("resize", this.handleWindowResize);
},
}
}
});
FhcApps.makeExtendable(app);
+45 -7
View File
@@ -3,10 +3,13 @@ import DashboardAdmin from '../../components/Dashboard/Admin.js';
import PluginsPhrasen from '../../plugins/Phrasen.js';
import ApiRenderers from '../../api/factory/renderers.js';
const app = Vue.createApp({
name: 'DashboardAdminApp',
data: () => ({
appSideMenuEntries: {}
appSideMenuEntries: {},
renderers: null
}),
components: {
CoreNavigationCmpt,
@@ -14,16 +17,51 @@ const app = Vue.createApp({
},
provide() {
return {
// TODO(chris): move those two into the components that need it
renderers: Vue.computed(() => this.renderers),
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone
};
},
created() {
this.$api
.call(ApiRenderers.loadRenderers())
.then(res => {
for (let rendertype of Object.keys(res.data)) {
let modalTitle = null;
let modalContent = null;
let calendarEvent = null;
if (res.data[rendertype].modalTitle)
modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].modalTitle)));
if (res.data[rendertype].modalContent)
modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].modalContent)));
if (res.data[rendertype].calendarEvent)
calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(res.data[rendertype].calendarEvent)));
if (res.data[rendertype].calendarEventStyles) {
var head = document.head;
if (!head.querySelector(`link[href="${res.data[rendertype].calendarEventStyles}"]`)) {
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = res.data[rendertype].calendarEventStyles;
head.appendChild(link);
}
}
if (this.renderers === null) {
this.renderers = {};
}
if (!this.renderers[rendertype]) {
this.renderers[rendertype] = {}
}
this.renderers[rendertype].modalTitle = modalTitle;
this.renderers[rendertype].modalContent = modalContent;
this.renderers[rendertype].calendarEvent = calendarEvent;
}
})
.catch(this.$fhcAlert.handleSystemErrors);
}
});
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
tooltip: 8000
}
})
app.use(PluginsPhrasen);
app.directive('tooltip', primevue.tooltip);
app.mount('#main');
@@ -4,27 +4,25 @@ import Theme from '../../plugins/Theme.js';
import contrast from '../../directives/contrast.js';
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js";
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js";
import MyLvPlan from "../../components/Cis/LvPlan/MyLvPlan.js";
import MyLvPlan from "../../components/Cis/LvPlan/Personal.js";
import MylvStudent from "../../components/Cis/Mylv/Student.js";
import Profil from "../../components/Cis/Profil/Profil.js";
import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js";
import CmsNews from "../../components/Cis/Cms/News.js";
import CmsContent from "../../components/Cis/Cms/Content.js";
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO_DESKTOP, DEFAULT_MODE_RAUMINFO_MOBILE} from "../../components/Cis/Mylv/RoomInformation.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../../components/Cis/Mylv/RoomInformation.js";
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import Studium from "../../components/Cis/Studium/Studium.js";
import StgOrgLvPlan from "../../components/Cis/LvPlan/StgOrg.js";
import OtherLvPlan from "../../components/Cis/LvPlan/OtherLvPlan.js";
import ApiRenderers from '../../api/factory/renderers.js';
import ApiRouteInfo from '../../api/factory/routeinfo.js';
import {capitalize} from "../../helpers/StringHelpers.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const isMobile = window.matchMedia("(max-width: 767px)").matches;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}`),
@@ -87,7 +85,7 @@ const router = VueRouter.createRouter({
name: "RoomInformation",
params: { // in this case always populate other params since they are not optional
ort_kurzbz: to.params.ort_kurzbz,
mode: isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP,
mode: DEFAULT_MODE_RAUMINFO,
focus_date: new Date().toISOString().split("T")[0]
},
};
@@ -104,7 +102,7 @@ const router = VueRouter.createRouter({
const mode = route.params.mode &&
validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase())
? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()
: (isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP);
: DEFAULT_MODE_RAUMINFO;
// default to today date if not provided
const d = new Date(route.params.focus_date)
@@ -199,26 +197,6 @@ const router = VueRouter.createRouter({
};
}
},
{
path: `/Cis/StgOrgLvPlan/:mode?/:focus_date?/:stgkz?/:sem?/:verband?/:gruppe?`,
name: 'StgOrgLvPlan',
component: StgOrgLvPlan,
props(route) {
return {
propsViewData: route.params
};
}
},
{
path: `/Cis/OtherLvPlan/:otherUid/:mode?/:focus_date?`,
name: "OtherLvPlan",
component: OtherLvPlan,
props(route) {
return {
propsViewData: route.params
};
}
},
{
path: `/Cis4`,
name: 'Cis4',
@@ -249,22 +227,26 @@ const router = VueRouter.createRouter({
})
const app = Vue.createApp({
name: 'CisApp',
name: 'FhcApp',
data: () => ({
appSideMenuEntries: {},
windowWidth: 0,
renderers: null,
}),
components: {},
computed: {
isMobile() {
const smallScreen = window.matchMedia("(max-width: 767px)").matches;
const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
return smallScreen;// && touchCapable;
}
},
provide() {
return { // provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language),
isMobile: Vue.computed(() => this.isMobile),
renderers: Vue.computed(() => this.renderers),
isMobile: this.isMobile
}
},
computed: {
isMobile: function() {
return (this.windowWidth < 767);
}
},
methods: {
isInternalRoute(href) {
const internalBase = window.location.origin
@@ -298,21 +280,54 @@ const app = Vue.createApp({
this.$router.push(route);
}
},
handleWindowResize() {
this.windowWidth = window.innerWidth;
},
}
},
created() {
this.windowWidth = window.innerWidth;
async created(){
await this.$api
.call(ApiRenderers.loadRenderers())
.then(res => res.data)
.then(data => {
for (let rendertype of Object.keys(data)) {
let modalTitle = null;
let modalContent = null;
let calendarEvent = null;
if (data[rendertype].modalTitle)
modalTitle = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalTitle)));
if (data[rendertype].modalContent)
modalContent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].modalContent)));
if (data[rendertype].calendarEvent)
calendarEvent = Vue.markRaw(Vue.defineAsyncComponent(() => import(data[rendertype].calendarEvent)));
if (data[rendertype].calendarEventStyles){
var head = document.head;
if(!head.querySelector(`link[href="${data[rendertype].calendarEventStyles}"]`)){
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = data[rendertype].calendarEventStyles;
head.appendChild(link);
}
}
if(this.renderers === null) {
this.renderers = {};
}
if (!this.renderers[rendertype]) {
this.renderers[rendertype] = {}
}
this.renderers[rendertype].modalTitle = modalTitle;
this.renderers[rendertype].modalContent = modalContent;
this.renderers[rendertype].calendarEvent = calendarEvent;
}
});
},
async mounted() {
mounted() {
document.addEventListener('click', this.handleClick);
window.addEventListener("resize", this.handleWindowResize);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClick);
window.removeEventListener("resize", this.handleWindowResize);
},
});
@@ -338,4 +353,4 @@ app.mount('#fhccontent');
router.afterEach((to, from, failure) => {
app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath));
});
});
+16
View File
@@ -0,0 +1,16 @@
import {CoreNavigationCmpt} from '../components/navigation/Navigation.js';
import DashboardAdmin from '../components/Dashboard/Admin.js';
import Phrases from "../plugin/Phrasen.js"
Vue.createApp({
name: 'DashboardAdminApp',
data: () => ({
appSideMenuEntries: {}
}),
components: {
CoreNavigationCmpt,
DashboardAdmin
},
mounted() {
}
}).use(Phrases).mount('#main');
+2 -31
View File
@@ -4,9 +4,7 @@ export default {
name: 'BootstrapModal',
data: () => ({
modal: null,
fullscreen: false,
expandBtnHovered: false,
expandBtnFocused: false,
fullscreen: false
}),
props: {
backdrop: {
@@ -72,29 +70,6 @@ export default {
this.$emit('toggleFullscreen')
}
},
computed: {
getExpandButtonStyles() {
const hovered = this.expandBtnHovered;
const focused = this.expandBtnFocused;
return `display: flex;
align-items: center;
justify-content: center;
width: 1em;
height: 1em;
padding: 0;
border: 0;
background: transparent;
font-size: 1em;
opacity: 0.5;
color: inherit;
cursor: pointer;
line-height: 1;
transition: opacity 0.15s ease;
opacity: ${focused ? '1' : hovered ? '0.75' : '0.5'};
outline: ${focused ? '1px solid currentColor' : 'none'};
outline-offset: 2px;`
}
},
mounted() {
if (this.$refs.modal)
this.modal = new bootstrap.Modal(this.$refs.modal, {
@@ -165,13 +140,9 @@ export default {
<div class="d-flex align-items-center ms-auto gap-2">
<button
type="button"
:style="getExpandButtonStyles"
class="btn mb-1"
v-if="allowFullscreenExpand"
@click="toggleFullscreen"
@mouseenter="expandBtnHovered = true"
@mouseleave="expandBtnHovered = false"
@focusin="expandBtnFocused = true"
@focusout="expandBtnFocused = false"
:aria-label="fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'"
>
<i v-if="!fullscreen" class="fa-solid fa-expand"></i>
@@ -20,8 +20,7 @@ export default {
},
inject: {
mode: "mode",
dropableEvents: "dropableEvents",
timezone: "timezone"
dropableEvents: "dropableEvents"
},
props: {
events: Array,
+36 -120
View File
@@ -1,5 +1,5 @@
import LineEvent from "./Line/Event.js";
import LineBackground from "./Line/Background.js";
import LineEvent from './Line/Event.js';
import LineBackground from './Line/Background.js';
/**
* TODO(chris):
@@ -10,117 +10,54 @@ export default {
name: "GridLine",
components: {
LineEvent,
LineBackground,
LineBackground
},
inject: {
axisRow: "axisRow"
},
inject: ["axisRow", "shouldCompactEvents", "compactibleEventTypes"],
props: {
date: {
type: luxon.DateTime,
required: true,
required: true
},
start: {
type: luxon.DateTime,
required: true,
required: true
},
end: {
type: luxon.DateTime,
required: true,
required: true
},
events: {
type: Array,
default: [],
default: []
},
backgrounds: {
type: Array,
default: [],
},
default: []
}
},
computed: {
formattedEvents() {
let formattedEvents = this.events.map((event) => {
event.rows = [1, -1];
eventsWithRowInfo() {
const events = [];
this.events.forEach(event => {
const rows = [1, -1];
if (event.startsHere) {
event.rows[0] =
"t_" + event.start.diff(this.date).toMillis();
rows[0] = 't_' + event.start.diff(this.date).toMillis();
}
if (event.endsHere) {
event.rows[1] = "t_" + event.end.diff(this.date).toMillis();
rows[1] = 't_' + event.end.diff(this.date).toMillis();
}
return event;
});
if (this.shouldCompactEvents && this.compactibleEventTypes?.length) {
formattedEvents =
this.compactEvents(formattedEvents, this.compactibleEventTypes);
}
return formattedEvents;
},
},
methods: {
compactEvents(events, compactibleEventTypes) {
let formattedEvents = events
.filter(
(event) =>
!compactibleEventTypes.includes(event.type),
)
.map((event) => {
event.display = "default";
return event;
events.push({
...event,
rows
});
let eventsToBeCompacted = events.filter((event) =>
compactibleEventTypes.includes(event.type),
);
let compactedEvents = [];
eventsToBeCompacted.forEach((event) => {
let existingCompactedEvent = compactedEvents.find(
(compactedEvent) =>
event.rows[0] === compactedEvent.rows[0] &&
event.rows[1] === compactedEvent.rows[1],
);
if (!existingCompactedEvent) {
compactedEvents.push({
events: [
{
farbe: event.orig.farbe,
},
],
rows: event.rows,
});
} else {
existingCompactedEvent.events.push({
farbe: event.orig.farbe,
});
}
});
compactedEvents.forEach((compactedEvent) => {
if (compactedEvent.events.length < 4) {
formattedEvents.push({
display: "compacted",
...compactedEvent,
});
} else {
formattedEvents.push({
display: "compacted",
events: compactedEvent.events.slice(0, 3),
rows: compactedEvent.rows,
});
formattedEvents.push({
display: "compactedExtra",
events: compactedEvent.events.slice(3),
rows: compactedEvent.rows,
});
}
});
return formattedEvents;
},
return events;
}
},
template: /* html */ `
template: /* html */`
<div
class="fhc-calendar-base-grid-line"
style="position:relative;display:grid;grid-auto-flow:dense"
@@ -132,38 +69,17 @@ export default {
:end="end"
:background="bg"
></line-background>
<template v-for="(event, i) in formattedEvents" :key="i">
<line-event
v-if="!event.display || event.display === 'default'"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
:event="event"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
</template>
</line-event>
<div
v-else-if="event.display === 'compacted'"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
class="d-flex flex-row justify-content-center gap-1 align-items-center"
>
<span
v-for="(subEvent, subEventIndex) in event.events"
:key="subEventIndex"
:style="subEvent.farbe ? {'background-color': '#' + subEvent.farbe} : {}"
style="height:10px; width:10px;"
class="border border-dark rounded-circle"
></span>
</div>
<div
v-else-if="event.display === 'compactedExtra'"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
class="w-100 d-flex flex-row justify-content-center"
>
{{"+" + event.events.length}}
</div>
</template>
<line-event
v-for="(event, i) in eventsWithRowInfo"
:key="i"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')"
:event="event"
>
<template v-slot="slot">
<slot name="event" v-bind="slot" />
</template>
</line-event>
<slot name="dropzone" />
</div>
`,
};
`
}
+28 -71
View File
@@ -3,20 +3,24 @@ import FhcCalendar from "./Base.js";
import ApiLvPlan from '../../api/factory/lvPlan.js';
import { useEventLoader } from '../../composables/EventLoader.js';
import { useRenderers } from '../../composables/Renderers.js';
import ModeDay from './Mode/Day.js';
import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js';
import ModeList from './Mode/List.js';
export default {
name: "CalendarLvPlan",
components: {
FhcCalendar
},
inject: ["isMobile"],
inject: [
"renderers"
],
props: {
timezone: {
type: String,
required: true
},
date: {
type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local()
@@ -30,16 +34,6 @@ export default {
required: true
}
},
provide() {
return {
shouldCompactEvents: Vue.computed(
() => this.$props.mode === "Month" && this.isMobile,
),
compactibleEventTypes: Vue.computed(
() => this.compactibleEventTypes,
),
};
},
emits: [
"update:date",
"update:mode",
@@ -47,7 +41,11 @@ export default {
],
data() {
return {
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
modes: {
day: Vue.markRaw(ModeDay),
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth)
},
modeOptions: {
day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
@@ -55,13 +53,9 @@ export default {
},
week: {
collapseEmptyDays: false
},
list: {
length: 7,
},
}
},
teachingunits: null,
compactibleEventTypes: [],
teachingunits: null
};
},
computed: {
@@ -83,20 +77,7 @@ export default {
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false })
}
];
},
modes() {
let modes = {
day: Vue.markRaw(ModeDay),
month: Vue.markRaw(ModeMonth),
};
if (this.isMobile) {
modes.list = Vue.markRaw(ModeList);
} else {
modes.week = Vue.markRaw(ModeWeek);
}
return modes;
},
}
},
methods: {
eventStyle(event) {
@@ -107,47 +88,33 @@ export default {
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
this.$emit('update:range', rangeInterval);
},
resetEventLoader() {
this.reset();
},
async getStunden() {
let stundenResponse = await this.$api.call(ApiLvPlan.getStunden());
this.teachingunits = stundenResponse.data.map((el) => ({
id: el.stunde,
start: el.beginn,
end: el.ende,
}));
},
async getCompactibleEventTypes() {
let compactibleEventTypesResponse = await this.$api.call(
ApiLvPlan.getCompactibleEventTypes(),
);
this.compactibleEventTypes = compactibleEventTypesResponse.data;
},
}
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
const { events, lv } = useEventLoader(rangeInterval, props.getPromiseFunc);
Vue.watch(lv, newValue => {
context.emit('update:lv', newValue);
});
const { renderers } = useRenderers();
return {
rangeInterval,
events,
lv,
reset,
renderers
lv
};
},
async created() {
await this.getStunden();
await this.getCompactibleEventTypes();
created() {
this.$api
.call(ApiLvPlan.getStunden())
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
},
template: /* html */`
<fhc-calendar
@@ -169,13 +136,6 @@ export default {
>
<template v-slot="{ event, mode }">
<div
v-if="!event"
class="h-100 d-flex justify-content-center align-items-center"
>
{{ $p.t('lehre/noLvFound') }}
</div>
<div
v-else
:class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)"
@@ -194,9 +154,6 @@ export default {
v-else
:is="renderers[event.type]?.calendarEvent"
:event="event"
:timeSlotDisplayBehavior="
$props.mode.toLowerCase() === 'list' ? 'always' : 'default'
"
></component>
</div>
</template>
+1 -1
View File
@@ -93,7 +93,7 @@ export default {
mounted() {
this.$emit('update:range', this.range);
},
template: /*html*/ `
template: `
<div
class="fhc-calendar-mode-list flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults"
+9 -8
View File
@@ -1,7 +1,6 @@
import FhcCalendar from "./Base.js";
import { useEventLoader } from '../../composables/EventLoader.js';
import { useRenderers } from '../../composables/Renderers.js';
import ModeList from '../Calendar/Mode/List.js';
@@ -10,17 +9,22 @@ export default {
components: {
FhcCalendar
},
inject: [
"renderers"
],
props: {
timezone: {
type: String,
required: true
},
getPromiseFunc: {
type: Function,
required: true
}
},
data() {
const timezone = FHC_JS_DATA_STORAGE_OBJECT.timezone;
return {
timezone,
now: luxon.DateTime.now().setZone(timezone),
now: luxon.DateTime.now().setZone(this.timezone),
modes: {
list: Vue.markRaw(ModeList)
},
@@ -55,12 +59,10 @@ export default {
const rangeInterval = Vue.ref(null);
const { events } = useEventLoader(rangeInterval, props.getPromiseFunc);
const { renderers } = useRenderers();
return {
rangeInterval,
events,
renderers
events
};
},
template: /* html */`
@@ -97,7 +99,6 @@ export default {
<component
:is="renderers[event.type]?.calendarEvent"
:event="event"
:timeSlotDisplayBehavior="'always'"
></component>
</div>
</template>
@@ -2,8 +2,6 @@ import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
import { getDateStyleClass } from "./getDateStyleClass.js";
import {compareISODateValues, formatDateTime, formatISODate, getViennaTodayISO} from "./dateUtils.js";
export const AbgabeMitarbeiterDetail = {
name: "AbgabeMitarbeiterDetail",
@@ -48,7 +46,12 @@ export const AbgabeMitarbeiterDetail = {
eidAkzeptiert: false,
enduploadTermin: null,
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
speedDialItems: [
speedDialItems: [{
label: Vue.computed(() => this.$p.t('abgabetool/c4newAbgabetermin')),
icon: "fa fa-plus",
command: this.openCreateNewAbgabeModal,
disabled: Vue.computed(() => !this.getAllowedToCreateNewTermin)
},
{
label: Vue.computed(() => this.$p.t('abgabetool/c4benoten')),
icon: "fa fa-user-check",
@@ -78,22 +81,6 @@ export const AbgabeMitarbeiterDetail = {
}
},
methods: {
getSavedTerminInfoString(termin) {
const isUpdate = termin.updateamum != null;
const fullname = isUpdate
? termin.updatevon_fullname
: termin.insertvon_fullname;
const datetime = isUpdate
? termin.updateamum
: termin.insertamum;
return this.$p.t('ui/savedAtByV3', [formatDateTime(datetime), fullname])
},
terminIsInvalid(termin) {
return termin.note?.positiv == false && !termin.beurteilungsnotiz
},
getNoteBezeichnung(termin){
if(termin.noteBackend?.bezeichnung) {
return termin.noteBackend?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌'
@@ -126,8 +113,6 @@ export const AbgabeMitarbeiterDetail = {
if(newTerminRes.note) {
newTerminRes.note = noteOpt
newTerminRes.noteBackend = noteOpt // certain UI elements should only reflect persisted state
termin.allowedToDelete = false
newTerminRes.allowedToDelete = false
}
newTerminRes.invertedFixtermin = !newTerminRes.fixtermin
const existingTerminRes = res.data[1]
@@ -147,13 +132,13 @@ export const AbgabeMitarbeiterDetail = {
} else {
const noteOptExisting = this.allowedNotenOptions.find(opt => opt.note == existingTerminRes.note)
existingTerminRes.note = noteOptExisting
termin.paabgabetyp_kurzbz = newTerminRes.paabgabetyp_kurzbz
termin.noteBackend = noteOpt // do NOT take noteOptExisting -> should reflect the "yes the qgate grade is confirmed in backend ux behaviour"
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
}
this.projektarbeit.abgabetermine.sort((a, b) => compareISODateValues(a.datum, b.datum))
this.projektarbeit.abgabetermine.sort((a, b) =>new Date(a.datum) - new Date(b.datum))
const index = this.projektarbeit.abgabetermine.findIndex(t => termin.paabgabe_id == t.paabgabe_id)
@@ -174,7 +159,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '', // todo kurzbz textfield value vorschlag für qualgates
'datum': getViennaTodayISO(),
'datum': new Date().toISOString().split('T')[0],
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': false,
@@ -352,7 +337,16 @@ export const AbgabeMitarbeiterDetail = {
}
},
formatDate(dateParam) {
return formatISODate(dateParam)
// unsafe for datepickers, dont use there
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
const month = padZero(date.getMonth() + 1); // Months are zero-based
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}`
},
openCreateNewAbgabeModal() {
if(this.projektarbeit?.betreuerart_kurzbz == 'Zweitbegutachter') {
@@ -370,7 +364,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
'datum': getViennaTodayISO(),
'datum': new Date().toISOString().split('T')[0],
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': typ.upload_allowed_default,
@@ -404,7 +398,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
'datum': getViennaTodayISO(),
'datum': new Date().toISOString().split('T')[0],
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': false,
@@ -452,7 +446,7 @@ export const AbgabeMitarbeiterDetail = {
}
},
getMessagePtStyle() {
// adjust outer spacing and internal padding to appear similar to download button in size
// adjust outer spacing and internal padding to appear similar to doenload button in size
return {
root: {
style: {
@@ -567,12 +561,6 @@ export const AbgabeMitarbeiterDetail = {
class: "custom-tooltip"
}
},
getTooltipBeurteilungsnotiz() {
return {
value: this.$p.t('abgabetool/c4beurteilungsnotizBeiNegNote'),
class: "custom-tooltip"
}
},
getProjektarbeitTitel() {
if(this.projektarbeit?.titel) return this.$capitalize(this.$p.t('abgabetool/c4titel')) + ': ' + this.projektarbeit.titel
@@ -603,7 +591,7 @@ export const AbgabeMitarbeiterDetail = {
'fixtermin': false,
'invertedFixtermin': true,
'kurzbz': '',
'datum': getViennaTodayISO(),
'datum': new Date().toISOString().split('T')[0],
'note': this.allowedNotenOptions.find(opt => opt.note == 9),
'beurteilungsnotiz': '',
'upload_allowed': typ.upload_allowed_default,
@@ -639,7 +627,6 @@ export const AbgabeMitarbeiterDetail = {
dialogClass="bordered-modal modal-lg"
:backdrop="true"
@hideBsModal="showAutomagicModalPhrase=false;"
bodyClass="px-4 py-4"
>
<template v-slot:title>
<div>
@@ -669,7 +656,6 @@ export const AbgabeMitarbeiterDetail = {
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
:text-input="true"
auto-apply>
</VueDatePicker>
@@ -820,7 +806,6 @@ export const AbgabeMitarbeiterDetail = {
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
:text-input="true"
auto-apply>
</VueDatePicker>
@@ -831,7 +816,7 @@ export const AbgabeMitarbeiterDetail = {
<div class="col-12 col-md-9">
<Dropdown
:style="{'width': '100%'}"
:disabled="!termin.allowedToSave || termin.abgabedatum !== null || termin.noteBackend"
:disabled="!termin.allowedToSave"
:placeholder="getPlaceholderTermin(termin)"
v-model="termin.bezeichnung"
@change="handleChangeAbgabetyp(termin)"
@@ -866,10 +851,8 @@ export const AbgabeMitarbeiterDetail = {
</div>
<div class="row mt-2" v-if="termin.bezeichnung?.benotbar">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4notizQualGatev2') )}}</div>
<div class="col-12 col-md-9" v-tooltip.right="terminIsInvalid(termin) && getTooltipBeurteilungsnotiz">
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz"
:class="{ 'p-invalid': terminIsInvalid(termin) }"
rows="1" class="w-100" :disabled="!termin.allowedToSave"></Textarea>
<div class="col-12 col-md-9">
<Textarea style="margin-bottom: 4px;" v-model="termin.beurteilungsnotiz" rows="1" class="w-100" :disabled="!termin.allowedToSave"></Textarea>
</div>
</div>
@@ -891,7 +874,6 @@ export const AbgabeMitarbeiterDetail = {
:disabled="true"
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
>
</VueDatePicker>
</div>
@@ -907,6 +889,9 @@ export const AbgabeMitarbeiterDetail = {
<Message v-else-if="termin?.signatur == false" severity="error" :closable="false" :pt="getMessagePtStyle"> {{ $capitalize($p.t('abgabetool/c4keineSignatur')) }} </Message>
<Message v-else-if="termin?.signatur == 'error'" severity="warn" :closable="false" :pt="getMessagePtStyle"> {{ $capitalize($p.t('abgabetool/c4signaturServerError')) }} </Message>
</div>
<!-- <div v-else class="col-auto">-->
<!-- <Message severity="info" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4noFileFound') }} </Message>-->
<!-- </div>-->
</div>
</template>
@@ -919,10 +904,10 @@ export const AbgabeMitarbeiterDetail = {
<div class="col-12 col-md-3 fw-bold align-content-center">
{{ $capitalize( $p.t('abgabetool/c4actions') )}}
</div>
<div class="col-12 col-md-5">
<div class="col-12 col-md-9">
<div class="row">
<div class="col-auto">
<button v-if="termin.allowedToSave && !terminIsInvalid(termin)" style="max-height: 40px;" class="btn btn-primary border-0" @click="saveTermin(termin)">
<button v-if="termin.allowedToSave" style="max-height: 40px;" class="btn btn-primary border-0" @click="saveTermin(termin)">
{{ $capitalize( $p.t('abgabetool/c4save') )}}
<i class="fa-solid fa-floppy-disk"></i>
</button>
@@ -947,9 +932,6 @@ export const AbgabeMitarbeiterDetail = {
</div>
</div>
</div>
<div class="col-12 col-md-4 align-content-center text-end text-muted small">
{{getSavedTerminInfoString(termin)}}
</div>
</div>
</AccordionTab>
</template>
@@ -962,8 +944,7 @@ export const AbgabeMitarbeiterDetail = {
<bs-modal
ref="modalContainerZusatzdaten"
class="bootstrap-prompt"
dialogClass="bordered-modal modal-lg"
bodyClass="px-4 py-4">
dialogClass="bordered-modal modal-lg">
<template v-slot:title>
<div>
{{$capitalize( $p.t('abgabetool/c4enduploadZusatzdaten') )}}
@@ -3,9 +3,6 @@ import BsModal from '../../Bootstrap/Modal.js';
import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { formatISODate, getViennaTodayISO } from "./dateUtils.js";
import { validateThesisTitle } from './titleValidation.js'
export const AbgabeStudentDetail = {
name: "AbgabeStudentDetail",
@@ -23,16 +20,7 @@ export const AbgabeStudentDetail = {
VueDatePicker,
FhcOverlay
},
inject: [
'notenOptions',
'isMobile',
'isViewMode',
'moodle_link',
'confetti_on_endupload',
'title_edit_allowed',
'siginfolink_german',
'siginfolink_english'
],
inject: ['notenOptions', 'isMobile', 'isViewMode', 'moodle_link'],
props: {
projektarbeit: {
type: Object,
@@ -43,14 +31,12 @@ export const AbgabeStudentDetail = {
default: false
}
},
emits: ['titel-updated'],
data() {
return {
loading: false,
eidAkzeptiert: false,
enduploadTermin: null,
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
editingTitel: '',
form: Vue.reactive({
sprache: '',
abstract: '',
@@ -63,120 +49,9 @@ export const AbgabeStudentDetail = {
}
},
methods: {
confettiCannons() {
const container = document.getElementById('confetti-container');
if (!container) return;
const colors = ['#FFC107', '#FF5722', '#E91E63', '#00BCD4', '#4CAF50', '#9C27B0'];
const shapes = ['50%', '0%'];
const fragment = document.createDocumentFragment();
// Corner Cannons - Slowed Down)
const cannonCount = 150;
for (let i = 0; i < cannonCount; i++) {
const leftConfetti = document.createElement('div');
leftConfetti.classList.add('confetti-piece');
leftConfetti.style.left = '0px';
leftConfetti.style.top = '100%';
const rightConfetti = document.createElement('div');
rightConfetti.classList.add('confetti-piece');
rightConfetti.style.left = '100vw';
rightConfetti.style.top = '100%';
const colorL = colors[Math.floor(Math.random() * colors.length)];
const colorR = colors[Math.floor(Math.random() * colors.length)];
const shapeL = shapes[Math.floor(Math.random() * shapes.length)];
const shapeR = shapes[Math.floor(Math.random() * shapes.length)];
// Left Styles
leftConfetti.style.background = colorL;
leftConfetti.style.borderRadius = shapeL;
leftConfetti.style.width = `${Math.random() * 10 + 6}px`;
leftConfetti.style.height = `${Math.random() * 14 + 6}px`;
leftConfetti.style.setProperty('--blast-x', `${Math.random() * 50 + 10}vw`);
leftConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`);
// Right Styles
rightConfetti.style.background = colorR;
rightConfetti.style.borderRadius = shapeR;
rightConfetti.style.width = `${Math.random() * 10 + 6}px`;
rightConfetti.style.height = `${Math.random() * 14 + 6}px`;
rightConfetti.style.setProperty('--blast-x', `-${Math.random() * 50 + 10}vw`);
rightConfetti.style.setProperty('--blast-y', `-${Math.random() * 65 + 30}vh`);
// Increased durations to 3s - 5s for a floating gravity effect
const durationL = Math.random() * 2 + 3;
const durationR = Math.random() * 2 + 3;
const delayL = Math.random() * 0.2;
const delayR = Math.random() * 0.2;
leftConfetti.style.animation = `cannonBlast ${durationL}s linear ${delayL}s both`;
rightConfetti.style.animation = `cannonBlast ${durationR}s linear ${delayR}s both`;
fragment.appendChild(leftConfetti);
fragment.appendChild(rightConfetti);
setTimeout(() => leftConfetti.remove(), (delayL + durationL) * 1000);
setTimeout(() => rightConfetti.remove(), (delayR + durationR) * 1000);
}
container.appendChild(fragment);
},
openTitelEdit() {
this.editingTitel = this.projektarbeit.titel ?? '';
this.$refs.modalTitelEdit.show();
},
async saveTitel() {
const validation = validateThesisTitle(this.editingTitel);
if (!validation.isValid) {
if (validation.error === 'empty') {
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4emptyThesisTitle'))
} else if (validation.error === 'invalid_characters') {
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4invalidCharactersThesisTitle'))
}
return false;
}
const confirmed = await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/c4confirmTitelSpeichern'),
acceptLabel: this.$capitalize(this.$p.t('ui/speichern')),
acceptClass: 'p-button-primary',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'p-button-secondary'
});
if (confirmed === false) return;
this.loading = true;
this.$api.call(
ApiAbgabe.postStudentProjektarbeitTitel(
this.projektarbeit.projektarbeit_id,
validation.cleanedTitle
)
).then(res => {
if (res.meta.status === 'success') {
this.projektarbeit.titel = res.data;
this.$emit('titel-updated', {
projektarbeit_id: this.projektarbeit.projektarbeit_id,
titel: res.data
});
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4titelSavedSuccess')));
this.$refs.modalTitelEdit.hide();
} else {
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4titelSaveError')));
}
}).finally(() => {
this.loading = false;
});
},
getNoteBezeichnung(termin){
const noteOpt = this.notenOptions.find(opt => opt.note == termin.note)
if(noteOpt?.bezeichnung) {
return noteOpt?.positiv ? this.$capitalize(this.$p.t('abgabetool/c4positivBenotet')) + ' ✅' : this.$capitalize(this.$p.t('abgabetool/c4negativBenotet')) + ' ❌'
} else if(noteOpt?.benotbar === true && !termin.note) {
@@ -190,7 +65,7 @@ export const AbgabeStudentDetail = {
this.$fhcAlert.alertWarning(this.$capitalize(this.$p.t('global/warningChooseFile')));
return false
}
if(endupload) {
if(await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/confirmEnduploadSpeichern'),
@@ -202,16 +77,16 @@ export const AbgabeStudentDetail = {
return false
}
}
return true;
},
async triggerEndupload() {
if (!await this.validate(this.enduploadTermin, true))
{
return false;
}
// post endabgabe
const formData = new FormData();
formData.append('paabgabetyp_kurzbz', this.enduploadTermin.paabgabetyp_kurzbz)
@@ -219,14 +94,14 @@ export const AbgabeStudentDetail = {
formData.append('paabgabe_id', this.enduploadTermin.paabgabe_id)
formData.append('student_uid', this.projektarbeit.student_uid)
formData.append('bperson_id', this.projektarbeit.bperson_id)
formData.append('sprache', this.form['sprache'].sprache)
formData.append('abstract', this.form['abstract'])
formData.append('abstract_en', this.form['abstract_en'])
formData.append('schlagwoerter', this.form['schlagwoerter'])
formData.append('schlagwoerter_en', this.form['schlagwoerter_en'])
formData.append('seitenanzahl', this.form['seitenanzahl'])
for (let i = 0; i < this.enduploadTermin.file.length; i++) {
formData.append('file', this.enduploadTermin.file[i]);
}
@@ -234,31 +109,39 @@ export const AbgabeStudentDetail = {
this.$api.call(ApiAbgabe.postStudentProjektarbeitEndupload(formData))
.then(res => {
this.handleUploadRes(res, this.enduploadTermin)
if(this.confetti_on_endupload && res.meta.status == "success") {
this.confettiCannons()
}
}).finally(()=> {
this.loading = false
this.loading = false
})
this.$refs.modalContainerEnduploadZusatzdaten.hide()
},
downloadAbgabe(termin) {
const url = `/api/frontend/v1/Abgabe/getStudentProjektarbeitAbgabeFile?paabgabe_id=${termin.paabgabe_id}&student_uid=${this.projektarbeit.student_uid}&projektarbeit_id=${this.projektarbeit.projektarbeit_id}`;
window.open(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + url)
// this.$api.call(ApiAbgabe.getStudentProjektarbeitAbgabeFile(termin.paabgabe_id, this.projektarbeit.student_uid))
},
formatDate(dateParam) {
return formatISODate(dateParam)
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
const month = padZero(date.getMonth() + 1); // Months are zero-based
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}`
},
async upload(termin) {
// only do this on endupload
if (! await this.validate(termin))
{
return false;
}
if(termin.bezeichnung?.paabgabetyp_kurzbz === 'end') {
// open endupload form modal for further inputs
this.enduploadTermin = termin
this.$refs.modalContainerEnduploadZusatzdaten.show()
} else {
@@ -268,7 +151,7 @@ export const AbgabeStudentDetail = {
formData.append('paabgabe_id', termin.paabgabe_id)
formData.append('student_uid', this.projektarbeit.student_uid)
formData.append('bperson_id', this.projektarbeit.bperson_id)
for (let i = 0; i < termin.file.length; i++) {
formData.append('file', termin.file[i]);
}
@@ -278,7 +161,7 @@ export const AbgabeStudentDetail = {
.then(res => {
this.handleUploadRes(res, termin)
}).finally(()=> {
this.loading = false
this.loading = false
})
}
},
@@ -286,18 +169,21 @@ export const AbgabeStudentDetail = {
if(res.meta.status == "success") {
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4fileUploadSuccessv3')))
termin.abgabedatum = getViennaTodayISO();
// update 'abgabedatum' for successful upload -> shows the pdf icon and date once set
termin.abgabedatum = new Date().toISOString().split('T')[0];
if(res?.data?.signatur !== undefined) {
termin.signatur = res.data.signatur
}
} else {
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4fileUploadErrorv3')))
}
if(res.meta.signaturInfo) {
this.$fhcAlert.alertInfo(res.meta.signaturInfo)
}
},
getOptionLabel(option) {
return option.sprache
@@ -309,6 +195,7 @@ export const AbgabeStudentDetail = {
},
watch: {
projektarbeit(newVal) {
// default select german if projektarbeit sprache was null
this.form.sprache = newVal.sprache ? this.allActiveLanguages.find(lang => lang.sprache == newVal.sprache) : this.allActiveLanguages.find(lang => lang.sprache == 'German')
this.form.abstract = newVal.abstract ?? ''
this.form.abstract_en = newVal.abstract_en ?? ''
@@ -316,22 +203,15 @@ export const AbgabeStudentDetail = {
this.form.schlagwoerter_en = newVal.schlagwoerter_en ?? ''
this.form.kontrollschlagwoerter = newVal.kontrollschlagwoerter ?? ''
this.form.seitenanzahl = newVal.seitenanzahl ?? 1
}
},
computed: {
getSignaturInfoLink() {
if(this.$p.user_language.value == 'German' && this.siginfolink_german) return this.siginfolink_german
else if (this.$p.user_language.value == 'English' && this.siginfolink_english) return this.siginfolink_english
},
getSignaturInfoAvailable() {
if(this.$p.user_language.value == 'German' && this.siginfolink_german) return true
else if (this.$p.user_language.value == 'English' && this.siginfolink_english) return true
else return false
},
getMoodleLink() {
return this.moodle_link + this.projektarbeit.studiengang_kz
return this.moodle_link + this.projektarbeit.studiengang_kz
},
getMessagePtStyle() {
// adjust outer spacing and internal padding to appear similar to doenload button in size
return {
root: {
style: {
@@ -364,84 +244,102 @@ export const AbgabeStudentDetail = {
})
return qgatefound
},
isTitelEditAllowed() {
return this.title_edit_allowed && !this.isViewMode && !this.projektarbeit?.note;
},
getTooltipVerspaetet() {
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerspaetet')),
class: "custom-tooltip"
}
},
getTooltipVerpasst() {
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipVerpasst')),
class: "custom-tooltip"
}
},
getTooltipAbzugeben() {
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbzugeben')),
class: "custom-tooltip"
}
},
getTooltipStandard() {
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipStandardv2')),
class: "custom-tooltip"
}
},
getTooltipAbgegeben() {
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgegeben')),
class: "custom-tooltip"
}
},
getTooltipFixtermin() {
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipFixtermin')),
class: "custom-tooltip"
}
},
getTooltipAbgabeDetected() {
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipAbgabeDetected')),
class: "custom-tooltip"
}
},
getTooltipNotAllowedToUpload() {
if(this.isViewMode) {
return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedInViewMode')),
class: "custom-tooltip"
}
} else {
return { value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegularv2')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4studentAbgabeNotAllowedRegular')),
class: "custom-tooltip"
}
}
},
getTooltipBeurteilungerforderlich() {
return { value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')), class: "custom-tooltip" }
return {
value: this.$capitalize(this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')),
class: "custom-tooltip"
}
},
getTooltipBestanden() {
return { value: this.$p.t('abgabetool/c4tooltipBestanden'), class: "custom-tooltip" }
return {
value: this.$p.t('abgabetool/c4tooltipBestanden'),
class: "custom-tooltip"
}
},
getTooltipNichtBestanden() {
return { value: this.$p.t('abgabetool/c4tooltipNichtBestanden'), class: "custom-tooltip" }
return {
value: this.$p.t('abgabetool/c4tooltipNichtBestanden'),
class: "custom-tooltip"
}
},
},
created() {
},
mounted() {
},
template: `
<FhcOverlay :active="loading"></FhcOverlay>
<div v-if="projektarbeit">
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereichv2') )}}</h5>
<h5>{{$capitalize( $p.t('abgabetool/c4abgabeStudentenbereich') )}}</h5>
<div class="row">
<div class="col-8">
<p>{{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p>
<p class="d-flex align-items-center gap-2 mb-2" style="min-width: 0;">
<span
:title="projektarbeit.titel"
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 480px;"
>{{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</span>
<button
v-if="isTitelEditAllowed"
class="btn btn-sm btn-outline-secondary border-0 p-1"
v-tooltip.right="{ value: $capitalize($p.t('abgabetool/c4titelBearbeiten')), class: 'custom-tooltip' }"
@click="openTitelEdit"
>
<i class="fa-solid fa-pen"></i>
</button>
</p>
<p>{{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
<p> {{$capitalize( $p.t('person/student') ) }}: {{projektarbeit?.student}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4titel') ) }}: {{projektarbeit?.titel}}</p>
<p> {{$capitalize( $p.t('abgabetool/c4betreuerv2') ) }}: {{projektarbeit ? $p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz) + ' ' + projektarbeit.betreuer : ''}}</p>
</div>
<div class="col-4">
<div class="row">
<p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }}
<a :href="getMoodleLink" target="_blank">Moodle</a>
</p>
</div>
<div class="row" v-if="getSignaturInfoAvailable">
<a :href="getSignaturInfoLink" target="_blank">{{$p.t('abgabetool/c4signaturinfo')}} <i class="fa-solid fa-circle-info"></i></a>
</div>
<p>{{ $p.t('abgabetool/c4checkoutStgMoodleInfos') }}
<a :href="getMoodleLink" target="_blank">Moodle</a>
</p>
</div>
</div>
@@ -459,6 +357,7 @@ export const AbgabeStudentDetail = {
<i v-else-if="termin.dateStyle == 'beurteilungerforderlich'" v-tooltip.right="getTooltipBeurteilungerforderlich" class="fa-solid fa-list-check"></i>
<i v-else-if="termin.dateStyle == 'bestanden'" v-tooltip.right="getTooltipBestanden" class="fa-solid fa-check"></i>
<i v-else-if="termin.dateStyle == 'nichtbestanden'" v-tooltip.right="getTooltipNichtBestanden" class="fa-solid fa-circle-exclamation"></i>
</div>
<div class="text-start px-2" style="min-width: 150px; max-width: 300px; margin-left: 40px">
<span>{{ termin ? $p.t('abgabetool/c4paatyp' + termin.paabgabetyp_kurzbz) : '' }}</span>
@@ -509,6 +408,8 @@ export const AbgabeStudentDetail = {
</div>
</template>
</Inplace>
</div>
<div class="row mt-2">
@@ -529,7 +430,7 @@ export const AbgabeStudentDetail = {
</VueDatePicker>
</div>
</div>
<div class="row mt-2">
<div class="col-12 col-md-3 fw-bold align-content-center">{{$capitalize( $p.t('abgabetool/c4abgabetyp') )}}</div>
<div class="col-12 col-md-9">
@@ -565,7 +466,7 @@ export const AbgabeStudentDetail = {
<div class="col-12 col-md-9">
<template v-if="termin?.abgabedatum">
<div class="row">
<div style="width:100px; align-content: end;">
<div style="width:100px; align-content: center;">
<h6>{{ termin.abgabedatum?.split("-").reverse().join(".") }}</h6>
</div>
@@ -580,6 +481,9 @@ export const AbgabeStudentDetail = {
<Message v-else-if="termin?.signatur == false" severity="error" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4keineSignatur') }} </Message>
<Message v-else-if="termin?.signatur == 'error'" severity="warn" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4signaturServerError') }} </Message>
</div>
<!-- <div v-else class="col-auto">-->
<!-- <Message severity="info" :closable="false" :pt="getMessagePtStyle"> {{ $p.t('abgabetool/c4noFileFound') }} </Message>-->
<!-- </div>-->
</template>
</div>
</template>
@@ -638,66 +542,25 @@ export const AbgabeStudentDetail = {
<h5>{{ $capitalize( $p.t('abgabetool/c4keineAbgabetermineGefunden') )}}</h5>
</div>
</div>
<div v-if="confetti_on_endupload" id="confetti-container"></div>
<bs-modal
ref="modalTitelEdit"
class="bootstrap-prompt"
dialogClass="bordered-modal"
bodyClass="px-4 py-4"
>
<template v-slot:title>
{{$capitalize( $p.t('abgabetool/c4titelBearbeiten') )}}
</template>
<template v-slot:default>
<div class="mb-2">
<label class="form-label fw-bold">
{{$capitalize( $p.t('abgabetool/c4titel') )}}
</label>
<Textarea
v-model="editingTitel"
rows="2"
maxlength="1024"
class="form-control w-100"
@keydown.enter.prevent="saveTitel"
/>
<div class="form-text text-end">{{ editingTitel.length }} / 1024</div>
</div>
</template>
<template v-slot:footer>
<button
class="btn btn-secondary"
@click="$refs.modalTitelEdit.hide()"
>
{{$capitalize( $p.t('abgabetool/c4Cancel') )}}
</button>
<button
class="btn btn-primary"
:disabled="!editingTitel.trim()"
@click="saveTitel"
>
<i class="fa-solid fa-floppy-disk me-1"></i>
{{$capitalize( $p.t('ui/speichern') )}}
</button>
</template>
</bs-modal>
</div>
<bs-modal
ref="modalContainerEnduploadZusatzdaten"
class="bootstrap-prompt"
dialogClass="bordered-modal modal-lg"
bodyClass="px-4 py-4">
dialogClass="bordered-modal modal-lg">
<template v-slot:title>
<div>
{{$capitalize( $p.t('abgabetool/c4enduploadZusatzdaten') )}}
</div>
<div class="row mb-3 align-items-start">
<p class="ml-4 mr-4">Student UID: {{ projektarbeit?.student_uid}}</p>
</div>
<div class="row mb-3 align-items-start">
<p class="ml-4 mr-4">{{$capitalize( $p.t('abgabetool/c4titel') )}}: {{ projektarbeit?.titel }}</p>
</div>
</template>
<template v-slot:default>
@@ -713,6 +576,15 @@ export const AbgabeStudentDetail = {
</div>
</div>
<!-- lektor fills these out-->
<!-- <div class="row mb-3 align-items-start">-->
<!-- <div class="row">Kontrollierte Schlagwörter</div>-->
<!-- <div class="row">-->
<!-- <Textarea v-model="form.kontrollschlagwoerter"></Textarea>-->
<!-- </div>-->
<!-- -->
<!-- -->
<!-- </div>-->
<div class="row mb-3 align-items-start">
<div class="row">{{$capitalize( $p.t('abgabetool/c4schlagwoerterGer') )}}</div>
<div class="row">
@@ -759,6 +631,7 @@ export const AbgabeStudentDetail = {
<div class="col-9"></div>
<div class="col-2"><p>{{$capitalize( $p.t('abgabetool/c4gelesenUndAkzeptiert') )}}</p></div>
<div class="col-1">
<Checkbox
v-model="eidAkzeptiert"
:binary="true"
@@ -774,8 +647,8 @@ export const AbgabeStudentDetail = {
<div v-show="!allowedToSaveZusatzdaten">{{ $p.t('abgabetool/c4zusatzdatenausfuellen') }}</div>
<button class="btn btn-primary" :disabled="!getAllowedToSendEndupload" @click="triggerEndupload">{{$capitalize( $p.t('ui/hochladen') )}}</button>
</template>
</bs-modal>
`,
};
@@ -1,251 +0,0 @@
import AbgabeterminStatusLegende from "./StatusLegende.js";
import {formatDateTime} from "./dateUtils.js";
export const AbgabeStudentTimeline = {
name: "AbgabeStudentTimeline",
components: {
AbgabeterminStatusLegende,
Timeline: primevue.timeline,
},
props: {
projekte: { type: Array, default: () => [] },
notenOptions: { type: Array, default: () => [] },
formatDateFn: { type: Function, required: true }
},
data() {
return {
legendExpanded: false,
expandedProjects: {}
}
},
computed: {
student() {
return this.projekte?.[0] ?? null
}
},
watch: {
projekte: {
immediate: true,
handler(val) {
// open all projects by default whenever the student changes
const state = {}
val?.forEach(p => { state[p.projektarbeit_id] = true })
this.expandedProjects = state
this.legendExpanded = false
}
}
},
methods: {
getNoteBezeichnung(projektarbeit) {
if(projektarbeit.note && this.notenOptions) {
const noteOpt = this.notenOptions.find(opt => opt.note == projektarbeit.note)
return noteOpt?.bezeichnung
} else {
return ''
}
},
getSavedTerminInfoString(termin) {
const isUpdate = termin.updateamum != null;
const fullname = isUpdate
? termin.updatevon_fullname
: termin.insertvon_fullname;
const datetime = isUpdate
? termin.updateamum
: termin.insertamum;
return this.$p.t('ui/savedAtByV3', [formatDateTime(datetime), fullname])
},
getItemBezeichnung(item) {
if (!item?.bezeichnung) return ''
return item.bezeichnung?.bezeichnung ?? item.bezeichnung
},
getItemNote(item) {
if (!item?.note) return ''
if (item.note?.bezeichnung) return item.note.bezeichnung
return this.notenOptions?.find(n => n.note == item.note)?.bezeichnung ?? String(item.note)
},
getIconClass(dateStyle) {
return ({
verspaetet: 'fa-solid fa-triangle-exclamation',
verpasst: 'fa-solid fa-calendar-xmark',
abzugeben: 'fa-solid fa-hourglass-half',
standard: 'fa-solid fa-clock',
abgegeben: 'fa-solid fa-paperclip',
beurteilungerforderlich: 'fa-solid fa-list-check',
bestanden: 'fa-solid fa-check',
nichtbestanden: 'fa-solid fa-circle-exclamation',
})[dateStyle] ?? ''
},
getBetreuerLabel(projekt) {
return projekt.erstbetreuer_full_name
|| (projekt.betreuer_vorname ? `${projekt.betreuer_vorname} ${projekt.betreuer_nachname}`.trim() : null)
},
toggleProject(id) {
this.expandedProjects[id] = !this.expandedProjects[id]
}
},
template: `
<div v-if="student">
<div class="d-flex align-items-baseline gap-2 mb-3 pb-2 border-bottom">
<span class="fw-bold fs-6">{{ student.student_vorname }} {{ student.student_nachname }}</span>
<span class="text-muted small">{{ student.student_uid }}</span>
<span v-if="student.matrikelnr" class="text-muted small ms-auto">{{ student.matrikelnr }}</span>
</div>
<div v-for="projekt in projekte" :key="projekt.projektarbeit_id" class="mb-2">
<button
class="btn btn-sm w-100 text-start d-flex align-items-center gap-2 rounded bg-light border-0 py-2 px-3"
@click="toggleProject(projekt.projektarbeit_id)"
>
<i
:class="expandedProjects[projekt.projektarbeit_id] ? 'fa-solid fa-chevron-down' : 'fa-solid fa-chevron-right'"
style="width: 12px; flex-shrink: 0;"
></i>
<span class="fw-semibold text-truncate flex-grow-1" :title="projekt.titel">
{{ projekt.titel || projekt.projekttyp_kurzbz || projekt.projektarbeit_id }}
</span>
<span
v-if="projekt.note"
class="small fw-semibold mx-2 flex-shrink-0"
>
{{ getNoteBezeichnung(projekt) }}
</span>
<span class="text-muted small me-1 flex-shrink-0">{{ projekt.studiensemester_kurzbz }}</span>
<span class="badge bg-secondary flex-shrink-0">{{ projekt.abgabetermine?.length ?? 0 }}</span>
</button>
<div v-show="expandedProjects[projekt.projektarbeit_id]" class="px-2 pt-1">
<div v-if="getBetreuerLabel(projekt)" class="text-muted small px-1 mb-2">
{{ projekt.betreuerart || $capitalize($p.t('abgabetool/c4erstbetreuerv2')) }}:
{{ getBetreuerLabel(projekt) }}
</div>
<Timeline :value="projekt.abgabetermine" align="right">
<template #marker="slotProps">
<!-- padding:0 overrides the 34px accordion left-padding from the -header CSS class -->
<div
:class="slotProps.item.dateStyle + '-header'"
style="height: 26px; width: 26px; padding: 0; border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0;"
>
<i :class="getIconClass(slotProps.item.dateStyle)" style="font-size: 0.75rem;"></i>
</div>
</template>
<template #opposite="slotProps">
<div class="text-end small text-nowrap text-muted">
{{ formatDateFn(slotProps.item.datum) }}
</div>
</template>
<template #content="slotProps">
<div class="small pb-1">
<div class="d-flex align-items-center gap-1 fw-semibold">
<div
:class="slotProps.item.dateStyle + '-header'"
style="height:20px;width:20px;padding:0;border-radius:3px;display:flex;align-items:center;justify-content:center;flex-shrink:0;"
>
<i :class="getIconClass(slotProps.item.dateStyle)" style="font-size:.65rem;"></i>
</div>
<span>
{{ getItemBezeichnung(slotProps.item) }}
</span>
<i
v-if="slotProps.item.fixtermin"
class="fa-solid fa-lock text-muted"
title="Fixtermin"
></i>
<i
v-if="slotProps.item.abgabedatum"
class="fa-solid fa-file text-muted"
:title="$capitalize($p.t('abgabetool/c4abgabedatum'))"
></i>
<div class="flex-grow-1 text-end">
<span
v-if="slotProps.item.noteBackend?.bezeichnung"
class="fw-bold"
>
{{ slotProps.item.noteBackend?.positiv
? $capitalize($p.t('abgabetool/c4positivBenotet'))
: $capitalize($p.t('abgabetool/c4negativBenotet'))
}}
</span>
<span
v-else-if="slotProps.item.bezeichnung?.benotbar"
class="fw-bold text-muted"
>
{{ $capitalize($p.t('abgabetool/c4notYetGraded')) }}
</span>
</div>
</div>
<div
v-if="slotProps.item.kurzbz"
class="text-muted fst-italic ms-4"
>
{{ slotProps.item.kurzbz }}
</div>
<div
v-if="slotProps.item.abgabedatum"
class="small text-muted ms-4"
>
{{ $capitalize($p.t('abgabetool/c4abgabedatum')) }}:
{{ formatDateFn(slotProps.item.abgabedatum) }}
</div>
<div
v-if="slotProps.item.beurteilungsnotiz"
class="small ms-4 mt-1 text-muted"
>
{{ slotProps.item.beurteilungsnotiz }}
</div>
<div
v-if="slotProps.item.insertamum"
class="small text-muted ms-4 mt-1"
style="font-size: .72rem;"
>
<i class="fa-solid fa-clock-rotate-left me-1"></i>
{{ getSavedTerminInfoString(slotProps.item) }}
</div>
</div>
</template>
</Timeline>
</div>
</div>
<div class="mt-3 border-top pt-2">
<button
class="btn btn-link btn-sm p-0 text-muted text-decoration-none d-flex align-items-center gap-1"
@click="legendExpanded = !legendExpanded"
>
<i :class="legendExpanded ? 'fa-solid fa-chevron-down' : 'fa-solid fa-chevron-right'" style="font-size: 0.7rem;"></i>
Legende
</button>
<div v-show="legendExpanded" class="mt-2">
<AbgabeterminStatusLegende />
</div>
</div>
</div>
`
}
export default AbgabeStudentTimeline;
File diff suppressed because it is too large Load Diff
@@ -5,9 +5,8 @@ import VueDatePicker from '../../vueDatepicker.js.php';
import ApiAbgabe from '../../../api/factory/abgabe.js'
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass } from "./getDateStyleClass.js";
import { dateFilter } from '../../../tabulator/filters/DatesManual.js';
import { dateFilter } from '../../../tabulator/filters/Dates.js';
import {splitMailsHelper} from "../../../helpers/EmailHelpers.js";
import { formatISODate, getViennaTodayISO, toViennaDate } from "./dateUtils.js";
export const AbgabetoolMitarbeiter = {
name: "AbgabetoolMitarbeiter",
@@ -32,14 +31,19 @@ export const AbgabetoolMitarbeiter = {
old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link)
}
},
props: {
viewData: {
type: Object,
required: true,
default: () => ({name: '', uid: ''}),
validator(value) {
return value && value.uid // && value.name -> extensive viewData use only for cis4 onwards
}
}
},
data() {
return {
filteredRows: null,
count: 0,
filteredcount: 0,
selectedcount: 0,
qgate1FilterSelected: [],
qgate2FilterSelected: [],
tableData: null,
abgabetypenBetreuer: null,
detailIsFullscreen: false,
phrasenPromise: null,
@@ -54,7 +58,7 @@ export const AbgabetoolMitarbeiter = {
allowedNotenOptions: null,
notenOptionsNonFinal: null,
serienTermin: Vue.reactive({
datum: getViennaTodayISO(),
datum: new Date(),
bezeichnung: {
paabgabetyp_kurzbz: 'zwischen',
bezeichnung: 'Zwischenabgabe'
@@ -76,7 +80,7 @@ export const AbgabetoolMitarbeiter = {
abgabeTableOptions: {
minHeight: 250,
index: 'projektarbeit_id',
layout: 'fitData',
layout: 'fitDataStretch',
placeholder: Vue.computed(() => this.$p.t('global/noDataAvailable')),
selectable: true,
selectableCheck: this.selectionCheck,
@@ -134,82 +138,38 @@ export const AbgabetoolMitarbeiter = {
handleClick: this.selectAllHandler
},
width: 50,
cssClass: 'sticky-col',
visible: true
cssClass: 'sticky-col'
},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.formAction, headerFilter: false, headerSort: false, width: 140, minWidth: 50, visible: true, tooltip: false, cssClass: 'sticky-col'},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, minWidth: 140, visible: false,tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100,visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100,visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4studstatus'))), field: 'studienstatus', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 150, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4orgformv2'))), field: 'orgform', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, minWidth: 100,visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 50, visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4note'))), field: 'note_bez', headerFilter: true, visible: false, sorter: this.notenSorter, minWidth: 200, formatter: this.centeredTextFormatter},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, visible: true, minWidth: 100},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, width: 500, visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerv2'))), field: 'betreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPre'))), field: 'betreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerVorname'))), field: 'betreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerNachname'))), field: 'betreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: true},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4erstbetreuerTitelPost'))), field: 'betreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerv2'))), field: 'zweitbetreuer_full_name', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPre'))), field: 'zweitbetreuer_titelpre', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerVorname'))), field: 'zweitbetreuer_vorname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerNachname'))), field: 'zweitbetreuer_nachname', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4zweitbetreuerTitelPost'))), field: 'zweitbetreuer_titelpost', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerartv2'))), field: 'betreuerart_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, minWidth: 100, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))),
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4details'))), field: 'details', formatter: this.detailFormatter, headerFilter: false, headerSort: false, widthGrow: 1, tooltip: false, cssClass: 'sticky-col'},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4personenkennzeichen'))), headerFilter: true, field: 'pkz', formatter: this.pkzTextFormatter, widthGrow: 1, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4vorname'))), field: 'vorname', headerFilter: true, formatter: this.centeredTextFormatter,widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nachname'))), field: 'nachname', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4projekttyp'))), field: 'projekttyp_kurzbz', formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4stg'))), field: 'stg', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4sem'))), field: 'studiensemester_kurzbz', headerFilter: true, formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4titel'))), field: 'titel', headerFilter: true, formatter: this.centeredTextFormatter, maxWidth: 500, widthGrow: 8},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4betreuerartv2'))), field: 'betreuerart_beschreibung',formatter: this.centeredTextFormatter, widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4prevAbgabetermin'))), field: 'prevTermin',
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
tooltip: this.toolTipFuncPrevTermin,
field: 'prevTermin', formatter: this.abgabeterminFormatter, width: 250, visible: false},
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4nextAbgabetermin'))), field: 'nextTermin',
headerFilter: dateFilter,
headerFilterFunc: this.headerFilterTerminCol,
sorter: this.sortFuncTerminCol,
tooltip: this.toolTipFuncNextTermin,
formatter: this.abgabeterminFormatter, width: 250, visible: true},
formatter: this.abgabterminFormatter, widthGrow: 1, width: 250, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate1Status'))),
headerFilter: this.qgateHeaderFilterEditor,
headerFilterFunc: this.qgateHeaderFilterFunc,
headerFilterParams: {},
field: 'qgate1Status',
formatter: this.centeredTextFormatter,
titleFormatter: this.shortLongTitleFormatter,
titleFormatterParams: {
shortForm: 'QG1'
},
width: 50,
tooltip: (e, cell) => {
const data = cell.getData();
return data.qgate1Status
}
},
headerFilter: 'list',
headerFilterParams: { valuesLookup: this.getQGateStatusList },
field: 'qgate1Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('abgabetool/c4qgate2Status'))),
headerFilter: this.qgateHeaderFilterEditor,
headerFilterFunc: this.qgateHeaderFilterFunc,
headerFilterParams: {},
field: 'qgate2Status',
formatter: this.centeredTextFormatter,
titleFormatter: this.shortLongTitleFormatter,
titleFormatterParams: {
shortForm: 'QG2'
},
width: 50,
tooltip: (e, cell) => {
const data = cell.getData();
return data.qgate2Status
}
}
headerFilter: 'list',
headerFilterParams: { valuesLookup: this.getQGateStatusList },
field: 'qgate2Status', formatter: this.centeredTextFormatter, widthGrow: 1, width: 220, tooltip: false}
],
persistence: false,
persistenceID: 'abgabeTableBetreuer2026-05-26'
persistenceID: 'abgabeTableBetreuer2026-02-26'
},
abgabeTableEventHandlers: [{
event: "tableBuilt",
@@ -240,388 +200,11 @@ export const AbgabetoolMitarbeiter = {
})
this.selectedData = data
this.selectedcount = data.length;
}
},
{
event: 'dataFiltered',
handler: (filters, rows) => {
this.filteredRows = rows;
this.filteredcount = rows.length;
if (!this.selectedData.length) return;
const visibleData = new Set(rows.map(r => r.getData()));
const filteredOut = this.selectedData.filter(sd => !visibleData.has(sd));
if (!filteredOut.length) return;
const filteredOutSet = new Set(filteredOut);
this.$refs.abgabeTable.tabulator.getSelectedRows()
.filter(r => filteredOutSet.has(r.getData()))
.forEach(r => r.deselect());
}
}
]};
},
methods: {
notenSorter(a, b, aRow, bRow, column, dir, sorterParams) {
const aData = aRow.getData()
const bData = bRow.getData()
return aData.note - bData.note
},
async openBenotung(type, link) {
if(type === 'new') {
window.open(link, '_blank')
} else if(type === 'old') {
if(await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/c4aeltereParbeitBenotenv2'),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'btn btn-outline-secondary'
}) === false) {
return false
}
window.open(link, '_blank')
} else {
// show info text that no endupload with abgabe has been found
if(await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/c4keinEnduploadErfolgt'),
acceptLabel: this.$capitalize(this.$p.t('abgabetool/c4AcceptAndProceed')),
acceptClass: 'btn btn-danger',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'btn btn-outline-secondary'
}) === false) {
return false
}
}
},
formAction(cell) {
const actionButtons = document.createElement('div');
actionButtons.className = "d-flex gap-3";
actionButtons.style.display = "flex";
actionButtons.style.alignItems = "stretch";
actionButtons.style.justifyContent = "start";
actionButtons.style.height = "100%";
const val = cell.getValue();
const data = cell.getRow().getData()
const createButton = (iconClass, titleKey, clickHandler) => {
const btn = document.createElement('button');
btn.className = 'btn btn-outline-secondary';
btn.style.display = "flex";
btn.style.alignItems = "center"; // center icon vertically
btn.style.justifyContent = "center"; // center icon horizontally
btn.style.height = "100%"; // fill parent container height
btn.style.aspectRatio = "1 / 1"; // keep square shape (optional)
btn.style.padding = "0"; // remove extra padding for compactness
if(iconClass == 'fa fa-timeline') btn.style.transform = "rotate(90deg)";
btn.innerHTML = `<i class="${iconClass}" style="color:#00649C; font-size:1.1rem;"></i>`;
btn.title = this.$capitalize(this.$p.t(titleKey));
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.stopImmediatePropagation();
clickHandler();
});
return btn;
};
actionButtons.append(
createButton('fa fa-folder-open', 'abgabetool/c4details', () => this.setDetailComponent(val)),
);
if(data.isCurrent && data.abgabetermine?.find(termin => termin.paabgabetyp_kurzbz == 'end' && termin.abgabedatum !== null) && data.beurteilungLinkNew) {
actionButtons.append(createButton('fa fa-user-check', 'abgabetool/c4benoten', () => this.openBenotung('new', data.beurteilungLinkNew)))
} else if(data.abgabetermine?.find(termin => termin.paabgabetyp_kurzbz == 'end' && termin.abgabedatum !== null) && data.beurteilungLinkOld) {
actionButtons.append(createButton('fa fa-user-check', 'abgabetool/c4benoten', () => this.openBenotung('old', data.beurteilungLinkOld)))
}
if(this.checkForZweitbetreuerTokenMailAvailability(data)) {
actionButtons.append(createButton('fa fa-envelope-open-text', 'abgabetool/c4zweitBegutachterTokenMailSenden', () => this.sendZweitbetreuerToken(data)))
}
return actionButtons;
},
checkForZweitbetreuerTokenMailAvailability(data) {
const hasEndabgabeWithUpload = !!data.abgabetermine.find(termin => termin.abgabedatum !== null && termin.paabgabetyp_kurzbz == 'end')
const hasZweitbetreuerWithoutBenutzerUid = data.zweitbetreuer_person_id !== null && data.zweitbetreuer_benutzer_uid === null
return hasEndabgabeWithUpload && hasZweitbetreuerWithoutBenutzerUid
},
sendZweitbetreuerToken(data) {
this.$api.call(ApiAbgabe.sendZweitbetreuerTokenMail(data.projektarbeit_id, data.betreuer_person_id, data.student_uid))
.then(res => {
if(res.meta.status == 'success') this.$fhcAlert.alertSuccess(this.$p.t('abgabetool/c4zweitBegutachterTokenMailSuccess'))
})
},
getDateStyleHtml(dateStyle) {
const iconMap = {
'verspaetet': '<i class="fa-solid fa-triangle-exclamation"></i>',
'verpasst': '<i class="fa-solid fa-calendar-xmark"></i>',
'abzugeben': '<i class="fa-solid fa-hourglass-half"></i>',
'standard': '<i class="fa-solid fa-clock"></i>',
'abgegeben': '<i class="fa-solid fa-paperclip"></i>',
'beurteilungerforderlich': '<i class="fa-solid fa-list-check"></i>',
'bestanden': '<i class="fa-solid fa-check"></i>',
'nichtbestanden': '<i class="fa-solid fa-circle-exclamation"></i>',
};
return iconMap[dateStyle] ?? '';
},
statusHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) {
const options = [
{ label: this.$p.t('abgabetool/c4positivBenotet'), value: 'bestanden', dateStyle: 'bestanden' },
{ label: this.$p.t('abgabetool/c4negativBenotet'), value: 'nichtbestanden', dateStyle: 'nichtbestanden' },
{ label: this.$p.t('abgabetool/c4tooltipVerspaetet'), value: 'verspaetet', dateStyle: 'verspaetet' },
{ label: this.$p.t('abgabetool/c4tooltipVerpasst'), value: 'verpasst', dateStyle: 'verpasst' },
{ label: this.$p.t('abgabetool/c4tooltipAbzugeben'), value: 'abzugeben', dateStyle: 'abzugeben' },
{ label: this.$p.t('abgabetool/c4tooltipAbgegeben'), value: 'abgegeben', dateStyle: 'abgegeben' },
{ label: this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich'), value: 'beurteilungerforderlich', dateStyle: 'beurteilungerforderlich' },
{ label: this.$p.t('abgabetool/c4tooltipStandardv2'), value: 'standard', dateStyle: 'standard' },
];
const field = cell.getField();
const stateKey = field + 'FilterSelected'; // e.g. dateStyleFilterSelected
let selected = [...(this[stateKey] || [])];
const wrapper = document.createElement('div');
wrapper.style.cssText = 'position: relative; width: 100%;';
const display = document.createElement('input');
display.readOnly = true;
display.placeholder = '';
display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;';
const dropdown = document.createElement('div');
dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 220px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);';
const updateDisplay = () => {
display.value = options
.filter(o => selected.includes(o.value))
.map(o => o.label)
.join(', ');
};
options.forEach(opt => {
const row = document.createElement('label');
row.style.cssText = 'display: flex; align-items: center; gap: 0; cursor: pointer; white-space: nowrap; padding-right: 8px;';
row.addEventListener('mousedown', e => e.preventDefault());
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = opt.value;
cb.checked = selected.includes(opt.value);
cb.style.cssText = 'margin: 0 6px;';
cb.addEventListener('change', () => {
selected = cb.checked
? [...selected, opt.value]
: selected.filter(v => v !== opt.value);
this[stateKey] = [...selected];
updateDisplay();
success([...selected]);
});
// icon badge — same look as cell
const badge = document.createElement('div');
badge.className = opt.dateStyle + '-header';
badge.style.cssText = `min-width: 36px; height: 36px; display: flex; align-items: center;
justify-content: center; flex-shrink: 0; padding: 0px 17px 0px 17px;`;
badge.innerHTML = this.getDateStyleHtml(opt.dateStyle);
const labelText = document.createElement('span');
labelText.textContent = opt.label;
labelText.style.cssText = 'margin-left: 6px;';
row.appendChild(cb);
row.appendChild(badge);
row.appendChild(labelText);
dropdown.appendChild(row);
});
updateDisplay();
display.addEventListener('click', () => {
if (dropdown.style.display === 'none') {
const rect = display.getBoundingClientRect();
dropdown.style.top = rect.bottom + 'px';
dropdown.style.left = rect.left + 'px';
dropdown.style.display = 'block';
} else {
dropdown.style.display = 'none';
}
});
display.addEventListener('blur', () => {
setTimeout(() => { dropdown.style.display = 'none'; }, 150);
});
document.body.appendChild(dropdown);
wrapper.appendChild(display);
cell.getElement().addEventListener('remove', () => dropdown.remove());
onRendered(() => display.focus());
return wrapper;
},
statusHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) {
if (!filterVal || !filterVal.length) return true;
// rowVal is the raw dateStyle string on the flat table
return filterVal.some(val => val === rowVal);
},
qgateHeaderFilterEditor(cell, onRendered, success, cancel, editorParams) {
const options = [
{ label: '[+] ' + this.$p.t('abgabetool/c4positivBenotet'), value: 'positive' },
{ label: '[-] ' + this.$p.t('abgabetool/c4negativBenotet'), value: 'negative' },
{ label: '[~] ' + this.$p.t('abgabetool/c4notYetGraded'), value: 'not_graded' },
{ label: '[?] ' + this.$p.t('abgabetool/c4notSubmitted'), value: 'not_submitted' },
{ label: '[o] ' + this.$p.t('abgabetool/c4notHappenedYet'), value: 'not_happened' },
{ label: '[--] ' + this.$p.t('abgabetool/c4keinTerminVorhanden'), value: 'no_termin' },
];
const field = cell.getField();
const stateKey = field === 'qgate1Status' ? 'qgate1FilterSelected' : 'qgate2FilterSelected';
let selected = [...(this[stateKey] || [])]; // restore persistence state
const wrapper = document.createElement('div');
wrapper.style.cssText = 'position: relative; width: 100%;';
const display = document.createElement('input');
display.readOnly = true;
display.placeholder = '';
display.style.cssText = 'padding: 4px; width: 100%; box-sizing: border-box; cursor: default; border: 1px solid; outline: none; background: #fff; appearance: none; caret-color: transparent;';
const dropdown = document.createElement('div');
dropdown.style.cssText = 'display: none; position: fixed; background: #fff; border: 1px solid; z-index: 9999; min-width: 180px; box-shadow: 0 2px 6px rgba(0,0,0,0.15);';
options.forEach(opt => {
const row = document.createElement('label');
row.style.cssText = 'display: flex; align-items: center; gap: 6px; padding: 4px 8px; cursor: pointer; white-space: nowrap;';
row.addEventListener('mousedown', e => e.preventDefault());
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = opt.value;
cb.checked = selected.includes(opt.value); // sync with persistence
cb.addEventListener('change', () => {
if (cb.checked) {
selected.push(opt.value);
} else {
selected = selected.filter(v => v !== opt.value);
}
this[stateKey] = [...selected]; // sync with persistence
display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', ');
success([...selected]);
});
row.appendChild(cb);
row.appendChild(document.createTextNode(opt.label));
dropdown.appendChild(row);
});
display.value = options.filter(o => selected.includes(o.value)).map(o => o.label).join(', ');
display.addEventListener('click', () => {
if (dropdown.style.display === 'none') {
const rect = display.getBoundingClientRect();
dropdown.style.top = rect.bottom + 'px';
dropdown.style.left = rect.left + 'px';
dropdown.style.display = 'block';
} else {
dropdown.style.display = 'none';
}
});
display.addEventListener('blur', () => {
setTimeout(() => { dropdown.style.display = 'none'; }, 150);
});
document.body.appendChild(dropdown);
wrapper.appendChild(display);
cell.getElement().addEventListener('remove', () => dropdown.remove());
onRendered(() => display.focus());
return wrapper;
},
qgateHeaderFilterFunc(filterVal, rowVal, rowData, filterParams) {
if (!filterVal || !filterVal.length) return true;
const matches = (val) => {
switch (val) {
case 'positive': return rowVal === this.$p.t('abgabetool/c4positivBenotet');
case 'negative': return rowVal === this.$p.t('abgabetool/c4negativBenotet');
case 'not_graded': return rowVal === this.$p.t('abgabetool/c4notYetGraded');
case 'not_submitted':return rowVal === this.$p.t('abgabetool/c4notSubmitted');
case 'not_happened': return rowVal === this.$p.t('abgabetool/c4notHappenedYet');
case 'no_termin': return rowVal === this.$p.t('abgabetool/c4keinTerminVorhanden');
default: return true;
}
};
// OR logic — row passes if it matches any selected filter
return filterVal.some(val => matches(val));
},
shortLongTitleFormatter(cell, formatterParams, onRendered) {
const longForm = cell.getValue()
const shortForm = formatterParams?.shortForm
if(longForm && shortForm) {
return `<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
${longForm}
</span>
<span class="short-text" style="font-weight: bold; display: none;">
${shortForm}
</span>`
} else {
return `<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
${longForm}
</span>`
}
},
toolTipFuncPrevTermin(e, cell, onRendered) {
const data = cell.getData();
return this.mapDateStyleToTabulatorTooltip(data.prevTermin.dateStyle);
},
toolTipFuncNextTermin(e, cell, onRendered) {
const data = cell.getData();
return this.mapDateStyleToTabulatorTooltip(data.nextTermin.dateStyle);
},
mapDateStyleToTabulatorTooltip(dateStyleString) {
switch(dateStyleString) {
case 'bestanden':
return this.$p.t('abgabetool/c4tooltipBestanden')
break;
case 'nichtbestanden':
return this.$p.t('abgabetool/c4tooltipNichtBestanden')
break;
case 'beurteilungerforderlich':
return this.$p.t('abgabetool/c4tooltipBeurteilungerforderlich')
break;
case 'verspaetet':
return this.$p.t('abgabetool/c4tooltipVerspaetet')
break;
case 'abgegeben':
return this.$p.t('abgabetool/c4tooltipAbgegeben')
break;
case 'verpasst':
return this.$p.t('abgabetool/c4tooltipVerpasst')
break;
case 'abzugeben':
return this.$p.t('abgabetool/c4tooltipAbzugeben')
break;
case 'standard':
return this.$p.t('abgabetool/c4tooltipStandardv2')
break;
default: return ''
}
},
handlePaUpdated(projektarbeit) {
this.checkAbgabetermineProjektarbeit(projektarbeit)
this.$refs.abgabeTable.tabulator.redraw(true)
@@ -634,7 +217,7 @@ export const AbgabetoolMitarbeiter = {
})
const uniqueRecipients = [...new Set(recipientList)];
const subject = ""; // empty subject line
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, null, this.$fhcAlert, this.$p)
splitMailsHelper(uniqueRecipients, param.originalEvent, subject, this.$fhcAlert, this.$p)
},
getQGateStatusList() {
return [
@@ -674,7 +257,7 @@ export const AbgabetoolMitarbeiter = {
if (val instanceof Date) {
dt = luxon.DateTime.fromJSDate(val);
} else if (typeof val === "string") {
dt = toViennaDate(val);
dt = luxon.DateTime.fromISO(val);
} else { // fallback
dt = luxon.DateTime.fromMillis(Number(val));
}
@@ -803,9 +386,6 @@ export const AbgabetoolMitarbeiter = {
}
this.stateRestored = true
// ensure that the filterCollapseables thingy has the correct values
this.$refs.abgabeTable.setSelectedFields();
}
});
@@ -871,32 +451,6 @@ export const AbgabetoolMitarbeiter = {
projekt.qgate2StatusRank = 1
}
})
// set shorthand statuscode once real status has been determined
projekt.qgate1StatusShort = this.mapRankToShortStatus(projekt.qgate1StatusRank)
projekt.qgate2StatusShort = this.mapRankToShortStatus(projekt.qgate2StatusRank)
},
mapRankToShortStatus(rank) {
switch(rank){
case 0: // kein termin vorhanden
return '--'
break;
case 1: // noch nicht stattgefunden
return 'o'
break;
case 2: // noch nicht abgegeben
return '?'
break;
case 3: // noch nicht benotet
return '~'
break;
case 4: // negativ benotet
return '-'
break;
case 5: // positiv benotet
return '+'
break;
}
},
checkAbgabetermineProjektarbeit(projekt) {
const now = luxon.DateTime.now()
@@ -906,7 +460,7 @@ export const AbgabetoolMitarbeiter = {
// while already looping through each termin, calculate datestyle beforehand
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
const date = toViennaDate(termin.datum).endOf('day')
const date = luxon.DateTime.fromISO(termin.datum).endOf('day')
termin.luxonDate = date
termin.diffMs = date.toMillis() - now.toMillis(); // positive = future, negative = past
@@ -928,7 +482,7 @@ export const AbgabetoolMitarbeiter = {
// seperate check for quality gates
this.checkQualityGateStatus(projekt)
},
abgabeterminFormatter(cell) {
abgabterminFormatter(cell) {
const val = cell.getValue()
if(val) {
@@ -960,15 +514,14 @@ export const AbgabetoolMitarbeiter = {
break
}
const typKurzbz = val.paabgabetyp_kurzbz ?? val.bezeichnung?.paabgabetyp_kurzbz
const bezeichnung = this.$p.t('abgabetool/c4paatyp' + typKurzbz)
const bezeichnung = val.bezeichnung?.bezeichnung ?? val.bezeichnung
return '<div style="display: flex; height: 100%">' +
'<div class=' + val.dateStyle + "-header" + ' style="min-width:48px; height: 100%; padding: 0px; display: flex; align-items: center; justify-content: center;">' +
icon +
'<div class=' + val.dateStyle + "-header" + ' style="width:48px; height: 100%; padding: 0px; display: flex; align-items: center; justify-content: center;">' +
icon +
'</div>' +
'<div style="margin-left: 4px;">' +
'<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">'+bezeichnung+' - '+ this.formatDate(val.datum)+'</p>' +
'<p style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+bezeichnung+' - '+ this.formatDate(val.datum)+'</p>' +
'</div>'+
'</div>'
@@ -992,19 +545,16 @@ export const AbgabetoolMitarbeiter = {
},
selectAllHandler(e, cell) {
const table = cell.getTable();
const rows = this.filteredRows ?? table.getRows();
const rows = table.getRows();
// custom select all logic
const allowed = rows.filter(r => r.getData().selectable);
// since betreuerpage acctually has logic behind selectable flag, it is important to go over allowed only here
const selected = allowed.every(r => r.isSelected());
if(selected){
if(selected) {
allowed.forEach(r => r.deselect());
e.target.checked = false;
} else {
allowed.forEach(r => r.select());
e.target.checked = true;
}
// stop built-in handler
@@ -1015,10 +565,18 @@ export const AbgabetoolMitarbeiter = {
this.detailIsFullscreen = !this.detailIsFullscreen
},
getOptionLabelAbgabetyp(option){
return this.$p.t('abgabetool/c4paatyp' + option.paabgabetyp_kurzbz)
return option.bezeichnung
},
formatDate(dateParam) {
return formatISODate(dateParam);
const date = new Date(dateParam)
// handle missing leading 0
const padZero = (num) => String(num).padStart(2, '0');
const month = padZero(date.getMonth() + 1); // Months are zero-based
const day = padZero(date.getDate());
const year = date.getFullYear();
return `${day}.${month}.${year}`;
},
undoSelection(cell) {
// checks if cells row is selected and unselects -> imitates columns which dont trigger row selection
@@ -1031,8 +589,6 @@ export const AbgabetoolMitarbeiter = {
},
selectionCheck(row) {
const data = row.getData()
// zweitbetreuer cant select projektarbeiten for serientermine
if(data?.betreuerart_kurzbz == 'Zweitbegutachter') return false
return true
},
@@ -1056,7 +612,7 @@ export const AbgabetoolMitarbeiter = {
addSeries() {
this.saving = true
this.$api.call(ApiAbgabe.postSerientermin(
this.serienTermin.datum,
this.serienTermin.datum.toISOString(),
this.serienTermin.bezeichnung.paabgabetyp_kurzbz,
this.serienTermin.bezeichnung.bezeichnung,
this.serienTermin.kurzbz,
@@ -1154,7 +710,7 @@ export const AbgabetoolMitarbeiter = {
termin.allowedToSave = paIsBenotet ? false : true
// lektoren are not allowed to delete deadlines with existing submissions
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum && !termin.note
termin.allowedToDelete = termin.allowedToSave && !termin.abgabedatum
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
@@ -1171,37 +727,28 @@ export const AbgabetoolMitarbeiter = {
},
centeredTextFormatter(cell) {
const longForm = cell.getValue()
if(!longForm) return
const data = cell.getData()
const entry = Object.entries(data).find(entry => entry[1] == longForm)
// shortFormKey must have same keyname as longForm but with 'Short' appended
const shortForm = data[entry[0]+'Short']
if(shortForm && longForm) {
return `<div style="display: flex; justify-content: start; align-items: center; height: 100%; width: 100%;">
<span class="full-text" style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">
${longForm}
</span>
<span class="short-text" style="font-weight: bold; display: none;">
${shortForm}
</span>
</div>`;
} else {
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
'<p style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; margin: 0px;">'+longForm+'</p></div>'
}
const val = cell.getValue()
if(!val) return
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<p style="max-width: 100%; width: 100%; overflow-wrap: break-word; word-break: break-word; white-space: normal; margin: 0px; text-align: center">'+val+'</p></div>'
},
detailFormatter(cell) {
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a><i class="fa fa-folder-open" style="color:#00649C"></i></a></div>'
},
beurteilungFormatter(cell) {
const val = cell.getValue()
if(val) {
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a><i class="fa fa-file-pdf" style="color:#00649C"></i></a></div>'
} else return '-'
},
pkzTextFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: start; align-items: center; height: 100%">' +
'<a style="max-width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap;">'+val+'</a></div>'
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%">' +
'<a style="max-width: 100%; word-wrap: break-word; white-space: normal;">'+val+'</a></div>'
},
tableResolve(resolve) {
this.tableBuiltResolve = resolve
@@ -1216,19 +763,15 @@ export const AbgabetoolMitarbeiter = {
return (projekt.typ + projekt.kurzbz)?.toUpperCase()
},
setupData(data){
this.projektarbeiten = data[0]
this.domain = data[1]
this.projektarbeiten = data[0]?.retval?.map(projekt => {
this.tableData = data[0]?.retval?.map(projekt => {
this.checkAbgabetermineProjektarbeit(projekt)
projekt.selectable = projekt.betreuerart_kurzbz !== 'Zweitbegutachter'
if(this.notenOptions && projekt.note) {
const opt = this.notenOptions.find(n => n.note == projekt.note)
// TODO: mehrsprachig englisch -> nevermind the english field in
// notenoption->bezeichnung_mehrsprachig is ALWAYS german
projekt.note_bez = opt?.bezeichnung
}
return {
...projekt,
details: {
@@ -1244,10 +787,9 @@ export const AbgabetoolMitarbeiter = {
titel: projekt.titel
}
})
this.count = this.projektarbeiten.length
this.$refs.abgabeTable.tabulator.setColumns(this.abgabeTableOptions.columns)
this.$refs.abgabeTable.tabulator.setData(this.projektarbeiten);
this.$refs.abgabeTable.tabulator.setData(this.tableData);
},
loadProjektarbeiten(all = false, callback) {
this.$api.call(ApiAbgabe.getMitarbeiterProjektarbeiten(all))
@@ -1296,20 +838,9 @@ export const AbgabetoolMitarbeiter = {
}
this.serienTermin.upload_allowed = newVal.upload_allowed_default
}
},
},
computed: {
countsToHTML() {
return this.$p.t('global/ausgewaehlt')
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
+ ' | '
+ this.$p.t('global/gefiltert')
+ ': '
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
+ ' | '
+ this.$p.t('global/gesamt')
+ ': <strong>' + (this.count || 0) + '</strong>';
},
emailItems() {
const menu = []
@@ -1338,8 +869,6 @@ export const AbgabetoolMitarbeiter = {
}
},
created() {
document.documentElement.classList.add('abgabetool');
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
// fetch config to avoid hard coded links
@@ -1381,16 +910,12 @@ export const AbgabetoolMitarbeiter = {
mounted() {
this.setupMounted()
},
beforeUnmount() {
document.documentElement.classList.remove('abgabetool');
},
template: `
<template v-if="phrasenResolved">
<FhcOverlay :active="loading || saving"></FhcOverlay>
<bs-modal ref="modalContainerAddSeries" class="bootstrap-prompt"
dialogClass="modal-lg"
bodyClass="px-4 py-4">
dialogClass="modal-lg">
<template v-slot:title>
<div>
{{ $p.t('abgabetool/neueTerminserie') }}
@@ -1410,7 +935,6 @@ export const AbgabetoolMitarbeiter = {
:enable-time-picker="false"
locale="de"
format="dd.MM.yyyy"
model-type="yyyy-MM-dd"
:text-input="true"
auto-apply>
</VueDatePicker>
@@ -1458,8 +982,7 @@ export const AbgabetoolMitarbeiter = {
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true"
@toggle-fullscreen="handleToggleFullscreenDetail"
bodyClass="px-4 py-4">
@toggle-fullscreen="handleToggleFullscreenDetail">
<template v-slot:title>
<div>
{{$p.t('abgabetool/c4abgabeMitarbeiterDetailTitle')}}
@@ -1478,13 +1001,12 @@ export const AbgabetoolMitarbeiter = {
<!-- low max height on this vsplit wrapper to avoid padding scrolls, elements have their inherent height anyways -->
<div id="abgabetable" style="max-height:40vw;">
<h2>{{$p.t('abgabetool/abgabetoolTitleBetreuer')}}</h2>
<h2>{{$p.t('abgabetool/abgabetoolTitle')}}</h2>
<hr>
<core-filter-cmpt
:title="''"
@uuidDefined="handleUuidDefined"
ref="abgabeTable"
:description="countsToHTML"
:newBtnShow="true"
:newBtnLabel="$p.t('abgabetool/neueTerminserie')"
:newBtnDisabled="!selectedData.length"
@@ -1,17 +1,14 @@
import AbgabeDetail from "./AbgabeStudentDetail.js";
import ApiAbgabe from '../../../api/factory/abgabe.js'
import ApiAuthinfo from '../../../api/factory/authinfo.js';
import BsModal from "../../Bootstrap/Modal.js";
import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass} from "./getDateStyleClass.js";
import { validateThesisTitle } from './titleValidation.js'
export const AbgabetoolStudent = {
name: "AbgabetoolStudent",
components: {
Accordion: primevue.accordion,
AccordionTab: primevue.accordiontab,
Textarea: primevue.textarea,
BsModal,
AbgabeDetail,
FhcOverlay
@@ -20,17 +17,21 @@ export const AbgabetoolStudent = {
return {
notenOptions: Vue.computed(() => this.notenOptions),
isViewMode: Vue.computed(() => this.isViewMode),
moodle_link: Vue.computed(() => this.moodle_link),
title_edit_allowed: Vue.computed(() => this.title_edit_allowed),
confetti_on_endupload: Vue.computed(() => this.confetti_on_endupload),
siginfolink_german: Vue.computed(() => this.siginfolink_german),
siginfolink_english: Vue.computed(() => this.siginfolink_english)
moodle_link: Vue.computed(() => this.moodle_link)
}
},
props: {
student_uid_prop: {
default: null
},
viewData: {
type: Object,
required: true,
default: () => ({uid: ''}),
validator(value) {
return value && value.uid
}
}
},
data() {
return {
@@ -43,86 +44,14 @@ export const AbgabetoolStudent = {
detail: null,
projektarbeiten: null,
selectedProjektarbeit: null,
moodle_link: null,
uid: null,
title_edit_allowed: null,
confetti_on_endupload: null,
siginfolink_german: null,
siginfolink_english: null,
editingTitel: '',
editingProjektarbeit: null
moodle_link: null
};
},
computed: {
isViewMode() {
return this.student_uid !== this.uid
},
student_uid() {
return this.student_uid_prop || this.uid || null
}
},
methods: {
openTitelEdit(projektarbeit, event) {
// stop the click from toggling the accordion tab
event.stopPropagation();
this.editingProjektarbeit = projektarbeit;
this.editingTitel = projektarbeit.titel ?? '';
this.$refs.modalTitelEdit.show();
},
async saveTitel() {
const validation = validateThesisTitle(this.editingTitel);
if (!validation.isValid) {
if (validation.error === 'empty') {
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4emptyThesisTitle'))
} else if (validation.error === 'invalid_characters') {
this.$fhcAlert.alertWarning(this.$p.t('abgabetool/c4invalidCharactersThesisTitle'))
}
return false;
}
const confirmed = await this.$fhcAlert.confirm({
message: this.$p.t('abgabetool/c4confirmTitelSpeichern'),
acceptLabel: this.$capitalize(this.$p.t('ui/speichern')),
acceptClass: 'p-button-primary',
rejectLabel: this.$capitalize(this.$p.t('abgabetool/c4Cancel')),
rejectClass: 'p-button-secondary'
});
if (confirmed === false) return;
this.loading = true;
this.$api.call(
ApiAbgabe.postStudentProjektarbeitTitel(
this.editingProjektarbeit.projektarbeit_id,
validation.cleanedTitle
)
).then(res => {
if (res.meta.status === 'success') {
// update the local list entry in-place so the accordion header reflects it immediately
this.editingProjektarbeit.titel = res.data;
// keep the open detail modal in sync if it happens to be showing this projektarbeit
if (this.selectedProjektarbeit?.projektarbeit_id === this.editingProjektarbeit.projektarbeit_id) {
this.selectedProjektarbeit.titel = res.data;
}
this.$fhcAlert.alertSuccess(this.$capitalize(this.$p.t('abgabetool/c4titelSavedSuccess')));
this.$refs.modalTitelEdit.hide();
} else {
this.$fhcAlert.alertError(this.$capitalize(this.$p.t('abgabetool/c4titelSaveError')));
}
}).finally(() => {
this.loading = false;
});
},
handleTitelUpdated(projektarbeit_id, titel) {
const pa = this.projektarbeiten?.find(p => p.projektarbeit_id === projektarbeit_id);
if (pa) pa.titel = titel;
},
checkQualityGatesStrict(termine) {
let qgate1Passed = false
let qgate2Passed = false
termine.forEach(t => {
const noteOption = this.notenOptions?.find(opt => opt.note == t.note)
if(noteOption && noteOption.positiv) {
@@ -139,7 +68,7 @@ export const AbgabetoolStudent = {
checkQualityGatesOptional(termine) {
const qgate1found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate1')
const qgate2found = termine.find(t => t.paabgabetyp_kurzbz == 'qualgate2')
let qgate1positiv = true
if(qgate1found) {
qgate1positiv = false
@@ -180,35 +109,47 @@ export const AbgabetoolStudent = {
this.loadAbgaben(details).then((res)=> {
const pa = this.projektarbeiten?.find(projekarbeit => projekarbeit.projektarbeit_id == details.projektarbeit_id)
pa.abgabetermine = res.data[0].retval
const paIsBenotet = pa.note !== null
pa.abgabetermine.forEach(termin => {
termin.file = []
termin.allowedToUpload = false
if(termin.paabgabetyp_kurzbz == 'end') {
// old assumed production logic when qgates are required
// termin.allowedToUpload = !this.isPastDate(termin.datum) && this.checkQualityGatesStrict(pa.abgabetermine)
const inTime = termin.fixtermin ? !this.isPastDate(termin.datum) : true
termin.allowedToUpload = inTime && this.checkQualityGatesOptional(pa.abgabetermine)
// development purposes
// termin.allowedToUpload = this.checkQualityGatesStrict(pa.abgabetermine)
// termin.allowedToUpload = true
} else if(termin.fixtermin) {
termin.allowedToUpload = !this.isPastDate(termin.datum)
} else {
termin.allowedToUpload = termin.upload_allowed
// this could confuse people since we should dont show people this flag
termin.allowedToUpload = termin.upload_allowed
}
// blocks client upload button if projektarbeitet is already beurteilt und thus further abgaben on any termin should be blocked
if(paIsBenotet) termin.allowedToUpload = false
termin.bezeichnung = this.abgabeTypeOptions.find(opt => opt.paabgabetyp_kurzbz === termin.paabgabetyp_kurzbz)
termin.dateStyle = getDateStyleClass(termin, this.notenOptions)
})
pa.betreuer = this.buildBetreuer(pa)
pa.student_uid = this.student_uid
this.selectedProjektarbeit = pa
this.$refs.modalContainerAbgabeDetail.show()
}).finally(()=>{this.loading=false})
},
centeredTextFormatter(cell) {
@@ -230,8 +171,8 @@ export const AbgabetoolStudent = {
},
mailFormatter(cell) {
const val = cell.getValue()
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
return '<div style="display: flex; justify-content: center; align-items: center; height: 100%;">' +
'<a href='+val+'><i class="fa fa-envelope" style="color:#00649C"></i></a></div>'
},
beurteilungFormatter(cell) {
const val = cell.getValue()
@@ -241,17 +182,19 @@ export const AbgabetoolStudent = {
} else return '-'
},
buildMailToLink(projekt) {
// should always be "projekt.mitarbeiter_uid +'@'+ this.domain", built in backend
return 'mailto:' + projekt.email
},
buildBetreuer(abgabe) {
return (abgabe.btitelpre ? abgabe.btitelpre + ' ' : '') + abgabe.bvorname + ' ' + abgabe.bnachname + (abgabe.btitelpost ? ' ' + abgabe.btitelpost : '')
},
async setupData(data){
// this.projektarbeiten = data[0]
const projektarbeiten = data[0] ?? null
if(!projektarbeiten) return
this.projektarbeiten = projektarbeiten.map(projekt => {
let mode = 'detailTermine'
return {
...projekt,
details: {
@@ -285,14 +228,16 @@ export const AbgabetoolStudent = {
.then(res => {
resolve(res)
})
})
})
},
async setupMounted() {
this.loadProjektarbeiten()
},
getAccTabHeaderForProjektarbeit(projektarbeit) {
let title = ''
title += projektarbeit.titel ?? this.$p.t('abgabetool/keinTitel')
return title
},
getMailLink(projektarbeit) {
@@ -300,31 +245,6 @@ export const AbgabetoolStudent = {
return 'mailto:'+projektarbeit.email
} else return ''
},
getZweitbetreuerMailLink(projektarbeit) {
if(projektarbeit.zweitbetreuer_mail) {
return 'mailto:'+projektarbeit.zweitbetreuer_mail
} else return ''
},
hasZweitbetreuer(projektarbeit) {
return !!(projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer)
},
getErstbetreuerEmailLabel(projektarbeit) {
// with only one betreuer keep the generic label; with multiple betreuer
// distinguish the email rows by their betreuungsart bezeichnung
if(!this.hasZweitbetreuer(projektarbeit)) {
return this.$capitalize(this.$p.t('abgabetool/c4betreuerEmailKontaktv2'))
}
const art = projektarbeit.betreuerart_kurzbz
? this.$p.t('abgabetool/c4betrart' + projektarbeit.betreuerart_kurzbz)
: this.$p.t('abgabetool/c4betreuerv2')
return this.$capitalize(this.$p.t('abgabetool/c4emailBetreuungsart', [art]))
},
getZweitbetreuerEmailLabel(projektarbeit) {
const art = projektarbeit.zweitbetreuer_betreuerart_kurzbz
? this.$p.t('abgabetool/c4betrart' + projektarbeit.zweitbetreuer_betreuerart_kurzbz)
: ''
return this.$capitalize(this.$p.t('abgabetool/c4emailBetreuungsart', [art]))
},
getNoteBezeichnung(projektarbeit) {
if(projektarbeit.note && this.notenOptions) {
const noteOpt = this.notenOptions.find(opt => opt.note == projektarbeit.note)
@@ -338,21 +258,25 @@ export const AbgabetoolStudent = {
},
handleDownloadBeurteilung2(projektarbeit) {
window.open(projektarbeit.beurteilung2)
},
async fetchAuthUID() {
const authIdResponse = await this.$api.call(ApiAuthinfo.getAuthUID());
this.uid = authIdResponse.data.uid;
},
}
},
watch: {
},
computed: {
isViewMode() {
return this.student_uid !== this.viewData.uid
},
student_uid() {
return this.student_uid_prop || this.viewData?.uid || null
}
},
watch: {},
async created() {
// make sure zoom media query doesnt spill ever to other CIS4 sites
document.documentElement.classList.add('abgabetool');
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
this.loading = true
//TODO: SWITCH TO NOTEN API ONCE NOTENTOOL IS IN MASTER TO AVOID DUPLICATE API
await this.$api.call(ApiAbgabe.getNoten()).then(res => {
if(res.meta.status == 'success') {
this.notenOptions = res.data[0]
@@ -365,94 +289,43 @@ export const AbgabetoolStudent = {
this.loading = false
})
// fetch abgabetypen options
this.$api.call(ApiAbgabe.getPaAbgabetypen()).then(res => {
this.abgabeTypeOptions = res.data
}).catch(e => {
this.loading = false
})
// fetch config to avoid hard coded links
this.$api.call(ApiAbgabe.getConfigStudent()).then(res => {
this.moodle_link = res.data?.moodle_link
this.title_edit_allowed = res.data?.title_edit_allowed
this.confetti_on_endupload = res.data?.confetti_on_endupload
this.siginfolink_german = res.data?.siginfolink_german
this.siginfolink_english = res.data?.siginfolink_english
}).catch(e => {
this.loading = false
})
await this.fetchAuthUID();
},
mounted() {
this.setupMounted()
},
beforeUnmount() {
document.documentElement.classList.remove('abgabetool');
},
template: `
<template v-if="phrasenResolved">
<FhcOverlay :active="loading"></FhcOverlay>
<bs-modal ref="modalContainerAbgabeDetail" class="bootstrap-prompt"
dialogClass="modal-xl" :allowFullscreenExpand="true" bodyClass="px-4 py-4">
dialogClass="modal-xl" :allowFullscreenExpand="true">
<template v-slot:title>
<div>
{{$capitalize( $p.t('abgabetool/c4abgabeStudentDetailTitle') )}}
</div>
</template>
<template v-slot:default>
<AbgabeDetail
:projektarbeit="selectedProjektarbeit"
@titel-updated="handleTitelUpdated"
></AbgabeDetail>
</template>
</bs-modal>
<bs-modal
ref="modalTitelEdit"
class="bootstrap-prompt"
dialogClass="bordered-modal"
bodyClass="px-4 py-4"
>
<template v-slot:title>
{{$capitalize( $p.t('abgabetool/c4titelBearbeiten') )}}
</template>
<template v-slot:default>
<div class="mb-2">
<label class="form-label fw-bold">
{{$capitalize( $p.t('abgabetool/c4titel') )}}
</label>
<Textarea
v-model="editingTitel"
rows="2"
maxlength="1024"
class="form-control w-100"
@keydown.enter.prevent="saveTitel"
/>
<div class="form-text text-end">{{ editingTitel.length }} / 1024</div>
</div>
</template>
<template v-slot:footer>
<button
class="btn btn-secondary"
@click="$refs.modalTitelEdit.hide()"
>
{{$capitalize( $p.t('abgabetool/c4Cancel') )}}
</button>
<button
class="btn btn-primary"
:disabled="!editingTitel.trim()"
@click="saveTitel"
>
<i class="fa-solid fa-floppy-disk me-1"></i>
{{$capitalize( $p.t('ui/speichern') )}}
</button>
<AbgabeDetail :projektarbeit="selectedProjektarbeit"></AbgabeDetail>
</template>
</bs-modal>
<h2>{{$capitalize( $p.t('abgabetool/abgabetoolTitle') )}}</h2>
<hr>
<div v-if="projektarbeiten === null || projektarbeiten?.length == 0">
<div v-if="projektarbeiten === null">
{{$capitalize( $p.t('abgabetool/c4abgabeStudentNoProjectsFound') )}}
</div>
@@ -462,12 +335,8 @@ export const AbgabetoolStudent = {
<template #header>
<div class="d-flex row w-100">
<div class="text-start" :class="projektarbeit.note != null ? 'col-6' : 'col-12'"
style="min-width: 0;">
<span
:title="getAccTabHeaderForProjektarbeit(projektarbeit)"
style="display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 600px;"
>{{getAccTabHeaderForProjektarbeit(projektarbeit)}}</span>
<div class="text-start" :class="projektarbeit.note != null ? 'col-6' : 'col-12'">
<span>{{getAccTabHeaderForProjektarbeit(projektarbeit)}}</span>
</div>
<div class="col-6 text-end">
<span>{{getNoteBezeichnung(projektarbeit)}}</span>
@@ -516,7 +385,7 @@ export const AbgabetoolStudent = {
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ getErstbetreuerEmailLabel(projektarbeit) }}</div>
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4betreuerEmailKontaktv2') )}}</div>
<div class="col-8 col-md-9">
<a :href="getMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
</div>
@@ -527,45 +396,17 @@ export const AbgabetoolStudent = {
{{ projektarbeit.zweitbetreuer?.first }}
</div>
</div>
<div v-if="projektarbeit.zweitbetreuer_person_id || projektarbeit.zweitbetreuer" class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{ getZweitbetreuerEmailLabel(projektarbeit) }}</div>
<div class="col-8 col-md-9">
<a v-if="projektarbeit.zweitbetreuer_mail" :href="getZweitbetreuerMailLink(projektarbeit)"><i class="fa fa-envelope" style="color:#00649C"></i></a>
<span v-else>-</span>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4projekttyp') )}}</div>
<div class="col-8 col-md-9">
{{ projektarbeit.projekttypbezeichnung }}
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4titel') )}}</div>
<div class="col-8 col-md-9 d-flex align-items-center gap-2" style="min-width: 0;">
<span
:title="projektarbeit.titel"
style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
>{{ projektarbeit.titel }}</span>
<button
v-if="title_edit_allowed && !isViewMode && projektarbeit.note == null"
class="btn btn-sm btn-outline-secondary border-0 p-1"
v-tooltip.right="{ value: $capitalize($p.t('abgabetool/c4titelBearbeiten')), class: 'custom-tooltip' }"
@click="openTitelEdit(projektarbeit, $event)"
>
<i class="fa-solid fa-pen"></i>
</button>
</div>
</div>
<div class="row mt-2">
<div class="col-4 col-md-3 fw-bold">{{$capitalize( $p.t('abgabetool/c4note') )}}</div>
<div class="col-8 col-md-9">
<span>{{getNoteBezeichnung(projektarbeit)}}</span>
{{ projektarbeit.titel }}
</div>
</div>
</AccordionTab>
</template>
@@ -10,6 +10,14 @@ export const DeadlineOverview = {
person_uid_prop: {
default: null
},
viewData: {
type: Object,
required: true,
default: () => ({name: '', uid: ''}),
validator(value) {
return value && value.name && value.uid
}
}
},
data() {
return {
@@ -1,33 +0,0 @@
const zone = 'Europe/Vienna';
export function getViennaTodayISO() {
return luxon.DateTime.now().setZone(zone).toISODate();
}
export function formatISODate(dateParam) {
if (!dateParam) return '';
const date = luxon.DateTime.fromISO(String(dateParam), { zone });
return date.isValid ? date.toFormat('dd.MM.yyyy') : '';
}
export function formatDateTime(dateParam) {
if (!dateParam) return '';
const date = luxon.DateTime.fromSQL(dateParam);
return date.isValid ? date.toFormat("dd.MM.yyyy HH:mm") : '';
}
export function toViennaDate(dateParam) {
if (!dateParam) return null;
return luxon.DateTime.fromISO(String(dateParam), { zone });
}
export function compareISODateValues(a, b) {
if (!a && !b) return 0;
if (!a) return 1;
if (!b) return -1;
return String(a).localeCompare(String(b));
}
@@ -1,8 +1,8 @@
const zone = 'Europe/Vienna';
const today = luxon.DateTime.now().setZone(zone);
export function getDateStyleClass(termin, notenOptions) {
const today = luxon.DateTime.now().setZone(zone);
const datum = luxon.DateTime.fromISO(termin.datum, { zone }).endOf('day');
const abgabedatum = termin.abgabedatum ? luxon.DateTime.fromISO(termin.abgabedatum, { zone }) : null;
termin.diffindays = datum.diff(today, 'days').days;
@@ -28,11 +28,10 @@ export function getDateStyleClass(termin, notenOptions) {
// no submission yet
if (datum < today) return 'verpasst';
if (termin.diffindays <= 12) return 'abzugeben';
return 'standard';
}
// GENERIC STATUS — applies to all termine
if (datum < today) return 'verpasst';
if (termin.diffindays <= 12) return 'abzugeben';
return 'standard';
}
// GENERIC STATUS
return datum < today ? 'verpasst' : 'standard';
}
@@ -1,25 +0,0 @@
/**
* Validates the thesis title on the frontend
* @param {string} title - The raw input from the title field
* @returns {object} Validation result containing status and cleaned title
*/
export function validateThesisTitle(title) {
if (!title) {
return { isValid: false, error: 'empty' };
}
// Replicate strip_tags / trim
const cleanedTitle = title.replace(/<\/?[^>]+(>|$)/g, "").trim();
if (cleanedTitle === '') {
return { isValid: false, error: 'empty' };
}
// Replicate the emoji/pictograph rejection
const emojiRegex = /\p{Extended_Pictographic}/u;
if (emojiRegex.test(cleanedTitle)) {
return { isValid: false, error: 'invalid_characters' };
}
return { isValid: true, cleanedTitle: cleanedTitle };
}
+97 -119
View File
@@ -2,137 +2,115 @@ import Pagination from "../../Pagination/Pagination.js";
import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js";
import BsConfirm from "../../Bootstrap/Confirm.js";
import ApiCms from "../../../api/factory/cms.js";
import ApiCms from '../../../api/factory/cms.js';
export default {
name: "NewsComponent",
components: {
Pagination,
StudiengangInformation,
},
inject: ["isMobile"],
data() {
return {
content: null,
maxPageCount: 0,
page_size: 10,
page: 1,
};
},
watch: {
"$p.user_language.value": function (sprache) {
this.fetchNews();
},
},
computed: {
sprache: function () {
return this.$p.user_language.value;
},
},
methods: {
async fetchNews() {
let newsResponse = await this.$api.call(
ApiCms.getNews(this.page, this.page_size, this.sprache),
);
this.content = newsResponse.data;
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
el.addEventListener("click", (evt) => {
evt.preventDefault();
BsConfirm.popup(el.dataset.confirm)
.then(() => {
Axios.get(el.href)
.then((res) => {
// TODO(chris): check for success then show message and/or reload
location = location;
})
.catch((err) => console.error("ERROR:", err));
})
.catch(() => {});
});
});
document.querySelectorAll("#cms [data-href]").forEach((el) => {
el.href = el.dataset.href.replace(
/^ROOT\//,
FHC_JS_DATA_STORAGE_OBJECT.app_root,
);
});
await this.$nextTick();
this.formatExternalHtml();
},
async loadNewPageContent(data) {
let newsResponse = await this.$api.call(
ApiCms.getNews(data.page, data.rows),
);
this.content = newsResponse.data;
await this.$nextTick();
this.formatExternalHtml();
},
formatExternalHtml() {
document
.querySelectorAll(".news-list-item .card-header")
.forEach((el) => {
el.classList.add("fhc-primary");
});
document.querySelectorAll(".news-list-item .row").forEach((el) => {
el.classList.add("w-100");
el.classList.add("align-items-center");
});
document
.querySelectorAll(".news-list-item .row h2")
.forEach((el) => {
el.classList.add("mb-0");
});
},
afterPageUpdated(event) {
this.page = event.page;
this.page_size = event.rows;
this.$refs.newsPageHeading.scrollIntoView({block: 'end'});
this.loadNewPageContent(event);
},
},
created() {
components: {
Pagination,
StudiengangInformation,
},
data() {
return {
content: null,
maxPageCount: 0,
page_size: 10,
page:1,
};
},
watch:{
'$p.user_language.value':function(sprache){
this.fetchNews();
}
},
computed:{
sprache: function(){
return this.$p.user_language.value;
},
},
methods: {
fetchNews() {
return this.$api
.call(ApiCms.getNews(this.page, this.page_size, this.sprache))
.then(res => res.data)
.then(result => {
this.content = result;
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
el.addEventListener("click", (evt) => {
evt.preventDefault();
BsConfirm.popup(el.dataset.confirm)
.then(() => {
Axios.get(el.href)
.then((res) => {
// TODO(chris): check for success then show message and/or reload
location = location;
})
.catch((err) => console.error("ERROR:", err));
})
.catch(() => {
});
});
});
document.querySelectorAll("#cms [data-href]").forEach((el) => {
el.href = el.dataset.href.replace(
/^ROOT\//,
FHC_JS_DATA_STORAGE_OBJECT.app_root
);
});
Vue.nextTick(()=>{
document.querySelectorAll(".card-header").forEach((el) => {
el.classList.add("fhc-primary");
});
document.querySelectorAll(".row").forEach((el) => {
el.classList.add("w-100");
el.classList.add("align-items-center");
});
document.querySelectorAll(".row h2").forEach((el) => {
el.classList.add("mb-0");
});
})
});
},
loadNewPageContent(data) {
this.$api
.call(ApiCms.getNews(data.page, data.rows))
.then(res => res.data)
.then(result => {
this.content = result;
});
}
},
created() {
this.fetchNews();
this.$api
.call(ApiCms.getNewsRowCount())
.then((res) => res.data)
.then((result) => {
.then(res => res.data)
.then(result => {
this.maxPageCount = result;
});
},
template: /*html*/ `
<div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
<h2 ref="newsPageHeading" class="fhc-primary-color">News</h2>
<hr/>
<pagination
v-if="content?true:false"
:page="page"
:page_size="page_size"
@pageUpdated="afterPageUpdated($event)"
:maxPageCount="maxPageCount"
></pagination>
<div class="container-fluid mt-4">
<div class="row">
<div class="col" v-html="content">
</div>
<div class="col-auto">
<div style="width:15rem">
<studiengang-information></studiengang-information>
</div>
},
template: /*html*/ `
<h2 class="fhc-primary-color">News</h2>
<hr/>
<pagination v-show="content?true:false" :page_size="page_size" @page="page=$event.page; loadNewPageContent($event)" :maxPageCount="maxPageCount">
</pagination>
<div class="container-fluid mt-4">
<div class="row">
<div class="col" v-html="content">
</div>
<div class="col-auto">
<div style="width:15rem">
<studiengang-information></studiengang-information>
</div>
</div>
</div>
<pagination
v-if="content?true:false"
:page="page"
:page_size="page_size"
@pageUpdated="afterPageUpdated($event)"
:maxPageCount="maxPageCount"
></pagination>
</div>
<pagination v-show="content?true:false" :page_size="page_size" @page="loadNewPageContent" :maxPageCount="maxPageCount">
</pagination>
`,
};
@@ -29,51 +29,51 @@ components:{
},
template:/*html*/`
<div id="fhc-studiengang-informationen">
<template v-if="studiengang?.bezeichnung && semester">
<div class="card card-body mb-3 border-0">
<div class="mb-1">
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiengang')}}:</h2>
<span class="mb-1">{{studiengang?.bezeichnung}}</span>
</div>
<div class="mb-1">
<h2 class="h4 mb-1 pb-0">Moodle:</h2>
<a class="fhc-link-color mb-1" target="_blank" :href="moodleLink">{{studiengang?.kurzbzlang}}</a>
</div>
<div :class="{'mb-1':studiengang?.zusatzinfo_html}">
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiensemester')}}: </h2>
<span class="mb-1">{{semester}}</span>
</div>
<div class="zusatzinfo" v-if="studiengang?.zusatzinfo_html" v-html="studiengang?.zusatzinfo_html"></div>
<template v-if="studiengang?.bezeichnung && semester">
<div class="card card-body mb-3 border-0">
<div class="mb-1">
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiengang')}}:</h2>
<span class="mb-1">{{studiengang?.bezeichnung}}</span>
</div>
</template>
<template v-for="{title, collection} in collection_array">
<template v-if="Array.isArray(collection) && collection.length !==0">
<h2 class="h5 text-truncate">{{title}}</h2>
<template v-if="displayWidget">
<div class="d-flex flex-wrap flex-row mb-3 gap-2">
<template v-for="person in collection">
<studiengang-person displayWidget v-bind="person"></studiengang-person>
</template>
</div>
</template>
<template v-else>
<div class="mb-1">
<h2 class="h4 mb-1 pb-0">Moodle:</h2>
<a class="fhc-link-color mb-1" target="_blank" :href="moodleLink">{{studiengang?.kurzbzlang}}</a>
</div>
<div :class="{'mb-1':studiengang?.zusatzinfo_html}">
<h2 class="h4 mb-1 pb-0">{{$p.t('lehre','studiensemester')}}: </h2>
<span class="mb-1">{{semester}}</span>
</div>
<div class="zusatzinfo" v-if="studiengang?.zusatzinfo_html" v-html="studiengang?.zusatzinfo_html"></div>
</div>
</template>
<template v-for="{title, collection} in collection_array">
<template v-if="Array.isArray(collection) && collection.length !==0">
<h2 class="h5 text-truncate">{{title}}</h2>
<template v-if="displayWidget">
<div class="d-flex flex-wrap flex-row mb-3 gap-2">
<template v-for="person in collection">
<div class="mb-3">
<studiengang-person v-bind="person"></studiengang-person>
</div>
<studiengang-person displayWidget v-bind="person"></studiengang-person>
</template>
</div>
</template>
<template v-else>
<template v-for="person in collection">
<div class="mb-3">
<studiengang-person v-bind="person"></studiengang-person>
</div>
</template>
</template>
</template>
<template v-if="hochschulvertr && Array.isArray(hochschulvertr) && hochschulvertr.length >0">
<studiengang-vertretung showBezeichnung :title="$p.t('studiengangInformation', 'Hochschulvertretung')" :vertretungsList="hochschulvertr"></studiengang-vertretung>
</template>
<template v-if="stdv && Array.isArray(stdv) && stdv.length >0">
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Studienvertretung').concat(studiengang?.kurzbzlang??'')" :vertretungsList="stdv"></studiengang-vertretung>
</template>
<template v-if="jahrgangsvertr && Array.isArray(jahrgangsvertr) && jahrgangsvertr.length >0">
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Jahrgangsvertretung')" :vertretungsList="jahrgangsvertr"></studiengang-vertretung>
</template>
</template>
<template v-if="hochschulvertr && Array.isArray(hochschulvertr) && hochschulvertr.length >0">
<studiengang-vertretung showBezeichnung :title="$p.t('studiengangInformation', 'Hochschulvertretung')" :vertretungsList="hochschulvertr"></studiengang-vertretung>
</template>
<template v-if="stdv && Array.isArray(stdv) && stdv.length >0">
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Studienvertretung').concat(studiengang.kurzbzlang??'')" :vertretungsList="stdv"></studiengang-vertretung>
</template>
<template v-if="jahrgangsvertr && Array.isArray(jahrgangsvertr) && jahrgangsvertr.length >0">
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Jahrgangsvertretung')" :vertretungsList="jahrgangsvertr"></studiengang-vertretung>
</template>
</div>
`,
@@ -27,7 +27,7 @@ export default {
<dl class="stgkontaktinfo">
<dt><i class="fa fa-phone me-2"></i></dt>
<dd class="mb-3"><a class="fhc-link-color" :href="phone?.link">{{phone?.number}}</a></dd>
<dd class="mb-3"><a class="fhc-link-color" :href="phone.link">{{phone.number}}</a></dd>
<dt><i class="fa fa-home me-2"></i></dt>
<dd class="mb-3">{{ort}}</dd>
@@ -3,8 +3,7 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN_MOBILE = 'List';
export const DEFAULT_MODE_LVPLAN_DESKTOP = 'Week';
export const DEFAULT_MODE_LVPLAN = 'Week'
export default {
name: 'LvPlanLehrveranstaltung',
@@ -20,21 +19,15 @@ export default {
lv: null
};
},
inject: ["isMobile"],
computed:{
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
return this.propsViewData?.focus_date;
},
currentMode() {
let validModes = ['day', 'month'];
validModes.push(this.isMobile ? 'list' : 'week');
const defaultMode = this.isMobile ? DEFAULT_MODE_LVPLAN_MOBILE : DEFAULT_MODE_LVPLAN_DESKTOP;
if (!this.propsViewData?.mode || !validModes.includes(this.propsViewData?.mode.toLowerCase()))
return defaultMode;
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
return DEFAULT_MODE_LVPLAN;
return this.propsViewData?.mode;
},
currentLv() {
@@ -54,17 +47,6 @@ export default {
return this.lv.bezeichnung;
}
},
watch: {
async isMobile() {
await this.$nextTick();
this.handleChangeMode(
this.currentMode,
luxon.DateTime.fromISO(this.currentDay, {
zone: this.timezone,
}),
);
},
},
methods: {
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
@@ -113,6 +95,7 @@ export default {
<fhc-calendar
v-else-if="lv"
ref="calendar"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
+8 -10
View File
@@ -3,8 +3,7 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN_MOBILE = 'List';
export const DEFAULT_MODE_LVPLAN_DESKTOP = 'Week';
export const DEFAULT_MODE_LVPLAN = 'Week'
export default {
name: 'LvPlan',
@@ -12,32 +11,31 @@ export default {
FhcCalendar
},
props: {
viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object
},
data() {
const now = luxon.DateTime.now().setZone(this.viewData.timezone);
return {
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
uid: null,
lv: null,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
lv: null
};
},
inject: ["isMobile"],
computed:{
currentDay() {
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.timezone).toISODate();
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
},
currentMode() {
const defaultMode = this.isMobile ? DEFAULT_MODE_LVPLAN_MOBILE : DEFAULT_MODE_LVPLAN_DESKTOP;
return this.propsViewData?.mode || defaultMode;
return this.propsViewData?.mode || DEFAULT_MODE_LVPLAN;
},
downloadLinks() {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
return false;
const opts = { zone: this.timezone };
const opts = { zone: this.viewData.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.toUnixInteger();
@@ -117,7 +115,7 @@ export default {
<fhc-calendar
ref="calendar"
v-model:lv="lv"
:timezone="timezone"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
@@ -1,281 +0,0 @@
import FormForm from "../../Form/Form.js";
import FormInput from "../../Form/Input.js";
import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from "../.././../api/factory/lvPlan.js";
import ApiOtherLvPlan from "../.././../api/factory/otherLvPlan.js";
import ApiAuthinfo from "../../../api/factory/authinfo.js";
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
export default {
name: "OtherLvPlan",
components: {
FormForm,
FormInput,
FhcCalendar,
},
props: {
propsViewData: Object,
},
data() {
return {
localProps: {},
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
isOtherPersonMitarbeiter: false,
isOtherPersonStudent: false,
currentStgBezeichnung: null,
listVerband: [],
listGroup: [],
rangeIntervalFirst: null,
otherPersonData: {
fullName: "",
photo: "",
},
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
};
},
inject: ["isMobile"],
computed: {
currentDay() {
if (
!this.propsViewData?.focus_date ||
isNaN(new Date(this.propsViewData?.focus_date))
)
return luxon.DateTime.now().setZone(this.timezone).toISODate();
return this.propsViewData?.focus_date;
},
currentMode() {
let validModes = ["day", "month"];
validModes.push(this.isMobile ? "list" : "week");
const defaultMode = this.isMobile
? DEFAULT_MODE_LVPLAN_MOBILE
: DEFAULT_MODE_LVPLAN_DESKTOP;
if (
!this.propsViewData?.mode ||
!validModes.includes(this.propsViewData?.mode.toLowerCase())
)
return defaultMode;
return this.propsViewData?.mode;
},
downloadLinks() {
// do not show download links in otherLvPlan Mode
return false;
if (
!this.studiensemester_start ||
!this.studiensemester_ende ||
!this.propsViewData.otherUid
)
return false;
const type = this.isOtherPersonStudent
? "student"
: this.isOtherPersonMitarbeiter
? "lektor"
: null;
if (!type) return;
const opts = { zone: this.timezone };
const start = luxon.DateTime.fromISO(
this.studiensemester_start,
opts,
).toUnixInteger();
const ende = luxon.DateTime.fromISO(
this.studiensemester_ende,
opts,
).toUnixInteger();
const download_link =
FHC_JS_DATA_STORAGE_OBJECT.app_root +
"cis/private/lvplan/stpl_kalender.php" +
"?type=" +
type +
"&pers_uid=" +
this.propsViewData.otherUid +
"&begin=" +
start +
"&ende=" +
ende;
return [
{
title: "excel",
icon: "fa-solid fa-file-excel",
link: download_link + "&format=excel",
},
{
title: "csv",
icon: "fa-solid fa-file-csv",
link: download_link + "&format=csv",
},
{
title: "ical1",
icon: "fa-regular fa-calendar",
link: download_link + "&format=ical&version=1&target=ical",
},
{
title: "ical2",
icon: "fa-regular fa-calendar",
link: download_link + "&format=ical&version=2&target=ical",
},
];
},
get_image_base64_src: function () {
if (!this.otherPersonData.photo?.length) {
return "";
}
return "data:image/jpeg;base64," + this.otherPersonData.photo;
},
},
watch: {
"propsViewData.otherUid": {
handler() {
this.$router.go();
},
},
async isMobile() {
await this.$nextTick();
this.handleChangeMode(
this.currentMode,
luxon.DateTime.fromISO(this.currentDay, {
zone: this.timezone,
}),
);
},
},
methods: {
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
},
handleChangeMode(newMode, day) {
const mode = newMode[0].toUpperCase() + newMode.slice(1);
const focus_date = day.toISODate();
this.$router.push({
name: "OtherLvPlan",
params: {
mode,
focus_date,
},
});
},
updateRange(rangeInterval) {
this.$api
.call(
ApiLvPlan.studiensemesterDateInterval(
rangeInterval.end.startOf("week").toISODate(),
),
)
.then((res) => {
this.studiensemester_kurzbz =
res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende;
});
},
getPromiseFunc(start, end) {
return [
this.$api.call(
ApiLvPlan.eventsPersonal(
start.toISODate(),
end.toISODate(),
this.propsViewData.otherUid,
),
),
this.$api.call(
ApiLvPlan.getLvPlanReservierungen(
start.toISODate(),
end.toISODate(),
this.propsViewData.otherUid,
),
),
];
},
async fetchViewData() {
const viewDataResponse = await this.$api.call(
ApiOtherLvPlan.getOtherLvPlanViewData(
this.propsViewData.otherUid,
),
);
const viewData = viewDataResponse.data;
this.isOtherPersonMitarbeiter =
!!viewData?.user_data?.is_mitarbeiter;
this.isOtherPersonStudent = !!viewData?.user_data?.is_student;
this.otherPersonData.fullName =
viewData?.user_data?.vorname +
" " +
viewData?.user_data?.nachname;
this.otherPersonData.photo = viewData?.user_data?.foto;
},
async redirectToMyLvPlanIfAuthUid() {
const authInfoResponse = await this.$api.call(
ApiAuthinfo.getAuthInfo(),
);
const authId = authInfoResponse.data.uid;
if (authId === this.propsViewData.otherUid) {
this.$router.push({ name: "MyLvPlan" });
}
},
},
async created() {
await this.redirectToMyLvPlanIfAuthUid();
await this.fetchViewData();
},
template: `
<div class="d-flex flex-column h-100">
<h2>
<div class="d-flex flex-row justify-content-between align-items-center">
<span>
{{ $p.t('lehre/stundenplan') + (studiensemester_kurzbz ? " " + studiensemester_kurzbz : "") }}
</span>
<div @click="this.$router.push({name: 'ProfilView', params: {uid: propsViewData.otherUid}})" type="button" class="d-flex flex-row align-items-center gap-3">
<span v-if="otherPersonData.fullName?.length">
{{ otherPersonData.fullName }}
</span>
<img v-if="otherPersonData.photo?.length" alt="profile picture" class=" img-thumbnail " style=" max-height:60px; " :src="get_image_base64_src"/>
</div>
</div>
</h2>
<hr>
<fhc-calendar
v-if="timezone"
ref="calendar"
:timezone="timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@update:range="updateRange"
class="responsive-calendar"
>
<div
v-if="downloadLinks"
class="d-flex gap-1 justify-items-start"
>
<div v-for="{ title, icon, link } in downloadLinks">
<a
:href="link"
:aria-label="title"
class="py-1 btn btn-outline-secondary"
>
<div class="d-flex flex-column">
<i aria-hidden="true" :class="icon"></i>
<span style="font-size:.5rem">{{ title }}</span>
</div>
</a>
</div>
</div>
</fhc-calendar>
</div>
`,
};
@@ -3,8 +3,7 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
export const DEFAULT_MODE_LVPLAN = 'Week'
export default {
name: 'LvPlanPersonal',
@@ -12,6 +11,7 @@ export default {
FhcCalendar
},
props: {
viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object
},
data() {
@@ -21,30 +21,18 @@ export default {
studiensemester_ende: null,
uid: null,
isMitarbeiter: false,
isStudent: false,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
isStudent: false
};
},
inject: ["isMobile"],
computed:{
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(this.timezone).toISODate();
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
return this.propsViewData?.focus_date;
},
currentMode() {
let validModes = ["day", "month"];
validModes.push(this.isMobile ? "list" : "week");
const defaultMode = this.isMobile
? DEFAULT_MODE_LVPLAN_MOBILE
: DEFAULT_MODE_LVPLAN_DESKTOP;
if (
!this.propsViewData?.mode ||
!validModes.includes(this.propsViewData?.mode.toLowerCase())
)
return defaultMode;
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
return DEFAULT_MODE_LVPLAN;
return this.propsViewData?.mode;
},
downloadLinks() {
@@ -59,7 +47,7 @@ export default {
return;
}
const opts = { zone: this.timezone };
const opts = { zone: this.viewData.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.toUnixInteger();
@@ -82,17 +70,6 @@ export default {
];
}
},
watch: {
async isMobile() {
await this.$nextTick();
this.handleChangeMode(
this.currentMode,
luxon.DateTime.fromISO(this.currentDay, {
zone: this.timezone,
}),
);
},
},
methods: {
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
@@ -125,18 +102,16 @@ export default {
this.$api.call(ApiLvPlan.eventsPersonal(start.toISODate(), end.toISODate())),
this.$api.call(ApiLvPlan.getLvPlanReservierungen(start.toISODate(), end.toISODate()))
];
},
async fetchAuthInfo() {
const authInfoResponse = await this.$api.call(ApiAuthinfo.getAuthInfo());
const authInfo = authInfoResponse.data;
this.uid = authInfo.uid;
this.isMitarbeiter = authInfo.isMitarbeiter;
this.isStudent = authInfo.isStudent;
},
}
},
async created() {
await this.fetchAuthInfo();
created() {
this.$api
.call(ApiAuthinfo.getAuthInfo())
.then(res => {
this.uid = res.data.uid;
this.isMitarbeiter = res.data.isMitarbeiter;
this.isStudent = res.data.isStudent;
});
},
template: /*html*/`
<div class="cis-lvplan-personal d-flex flex-column h-100">
@@ -148,9 +123,8 @@ export default {
</h2>
<hr>
<fhc-calendar
v-if="timezone"
ref="calendar"
:timezone="timezone"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
-431
View File
@@ -1,431 +0,0 @@
import FormForm from "../../Form/Form.js";
import FormInput from "../../Form/Input.js";
import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../.././../api/factory/lvPlan.js';
import ApiStgOrgLvPlan from '../.././../api/factory/stgOrgLvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
export default {
name: "LvPlanStgOrg",
components: {
FormForm,
FormInput,
FhcCalendar,
},
props: {
propsViewData: Object
},
data() {
return {
localProps: {},
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
uid: null,
isMitarbeiter: false,
isStudent: false,
currentStgBezeichnung: null,
formData: {
stgkz: null,
sem: null,
verband: null,
gruppe: null,
},
listStg: [],
listSem: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
listVerband: [],
listGroup: [],
rangeIntervalFirst: null,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
};
},
inject: ["isMobile"],
computed: {
maxSemester() {
const currentStg = this.listStg.find(
(item) => item.studiengang_kz === this.formData.stgkz,
);
return currentStg?.max_semester;
},
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(this.timezone).toISODate();
return this.propsViewData?.focus_date;
},
currentMode() {
let validModes = ["day", "month"];
validModes.push(this.isMobile ? "list" : "week");
const defaultMode = this.isMobile
? DEFAULT_MODE_LVPLAN_MOBILE
: DEFAULT_MODE_LVPLAN_DESKTOP;
if (
!this.propsViewData?.mode ||
!validModes.includes(this.propsViewData?.mode.toLowerCase())
)
return defaultMode;
return this.propsViewData?.mode;
},
downloadLinks() {
// do not show download links in StgOrg Mode
return false;
if (
!this.studiensemester_start ||
!this.studiensemester_ende ||
!this.uid
)
return false;
let type = false;
type = this.isStudent ? "student" : type;
type = this.isMitarbeiter ? "lektor" : type;
if (false === type) {
return;
}
const opts = { zone: this.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.toUnixInteger();
const ende = luxon.DateTime
.fromISO(this.studiensemester_ende, opts)
.toUnixInteger();
const download_link =
FHC_JS_DATA_STORAGE_OBJECT.app_root +
"cis/private/lvplan/stpl_kalender.php" +
"?type=" +
type +
"&pers_uid=" +
this.uid +
"&begin=" +
start +
"&ende=" +
ende;
return [
{
title: "excel",
icon: "fa-solid fa-file-excel",
link: download_link + "&format=excel",
},
{
title: "csv",
icon: "fa-solid fa-file-csv",
link: download_link + "&format=csv",
},
{
title: "ical1",
icon: "fa-regular fa-calendar",
link: download_link + "&format=ical&version=1&target=ical",
},
{
title: "ical2",
icon: "fa-regular fa-calendar",
link: download_link + "&format=ical&version=2&target=ical",
},
];
},
},
watch: {
async isMobile() {
await this.$nextTick();
this.handleChangeMode(
this.currentMode,
luxon.DateTime.fromISO(this.currentDay, {
zone: this.timezone,
}),
);
},
},
methods: {
loadLvPlan() {
if (!this.formData.stgkz) {
this.$fhcAlert.alertError(this.$p.t("LvPlan", "chooseStg"));
return;
}
if (
!this.formData.sem &&
(this.formData.verband || this.formData.gruppe)
) {
this.$fhcAlert.alertError(
this.$p.t("LvPlan", "error_SemMissing"),
);
return;
}
if (!this.formData.verband && this.formData.gruppe) {
this.$fhcAlert.alertError(
this.$p.t("LvPlan", "error_VerbandMissing"),
);
return;
}
const params = {
mode: this.currentMode,
focus_date: this.currentDay,
stgkz: this.formData.stgkz,
sem: this.formData.sem,
verband: this.formData.verband,
gruppe: this.formData.gruppe,
};
//ensure logic: no value after a null value in route
if (params.sem == null) {
params.verband = null;
params.gruppe = null;
}
if (params.verband == null) {
params.gruppe = null;
}
//delete all null values to avoid null in router
Object.keys(params).forEach(
(key) => params[key] == null && delete params[key],
);
this.$router.push({
name: "StgOrgLvPlan",
params,
});
this.$refs['calendar']?.resetEventLoader();
},
loadListSem(){
if(!this.listSem)
this.listSem = [...Array(this.maxSemester).keys()].map(i => i + 1);
},
loadListVerband() {
this.$api
.call(ApiLvPlan.getLehrverband(this.formData.stgkz, this.formData.sem, this.formData.verband))
.then(result => {
const data = result.data;
const mappedData = data.map((item) => item.verband);
this.listVerband = [
...new Set(
mappedData.filter(
(v) =>
v !== null &&
v !== undefined &&
String(v).trim() !== "",
),
),
].sort();
})
.catch(this.$fhcAlert.handleSystemError);
},
loadListGroup() {
this.$api
.call(ApiLvPlan.getGruppe(this.formData.stgkz, this.formData.sem, this.formData.verband))
.then(result => {
const data = result.data;
const mappedData = data.map((item) => item.gruppe);
this.listGroup = [
...new Set(
mappedData.filter(
(v) =>
v !== null &&
v !== undefined &&
String(v).trim() !== "",
),
),
].sort();
})
.catch(this.$fhcAlert.handleSystemError);
},
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
},
handleChangeMode(newMode, day) {
const mode = newMode[0].toUpperCase() + newMode.slice(1);
const focus_date = day.toISODate();
this.$router.push({
name: "StgOrgLvPlan",
params: {
mode,
focus_date,
stgkz: this.formData.stgkz,
sem: this.formData.sem,
verband: this.formData.verband,
gruppe: this.formData.gruppe,
},
});
},
updateRange(rangeInterval) {
this.$api
.call(
ApiLvPlan.studiensemesterDateInterval(
rangeInterval.end.startOf("week").toISODate(),
),
)
.then((res) => {
this.studiensemester_kurzbz =
res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende;
});
},
getPromiseFunc(start, end) {
return [
this.$api.call(
ApiLvPlan.eventsStgOrg(
start,
end,
this.formData.stgkz,
this.formData.sem,
this.formData.verband,
this.formData.gruppe,
),
),
];
},
async fetchAuthInfo() {
const authInfoResponse = await this.$api.call(ApiAuthinfo.getAuthInfo());
const authInfo = authInfoResponse.data;
this.uid = authInfo.uid;
this.isMitarbeiter = authInfo.isMitarbeiter;
this.isStudent = authInfo.isStudent;
},
async fetchViewData() {
const viewDataResponse = await this.$api.call(ApiStgOrgLvPlan.getStgOrgLvPlanViewData());
const viewData = viewDataResponse.data;
this.listStg = viewData.studiengaenge;
},
},
async created(){
await this.fetchAuthInfo();
await this.fetchViewData();
if (this.propsViewData) {
this.formData.stgkz = this.propsViewData.stgkz ? this.propsViewData.stgkz: null;
this.formData.sem = this.propsViewData.sem ? this.propsViewData.sem: null;
this.formData.verband = this.propsViewData.verband ? this.propsViewData.verband: null;
this.formData.gruppe = this.propsViewData.gruppe ? this.propsViewData.gruppe: null;
//ensure loading dropdown arrays for version propsView
if(!this.listVerband.length && this.formData.sem)
{
this.loadListVerband();
}
if(!this.listGroup.length && this.formData.verband)
{
this.loadListGroup();
}
}
if (this.formData.stgkz) {
this.loadLvPlan();
}
},
template: `
<div class="cis-lvplan-stg-org d-flex flex-column h-100">
<div class="mt-3">
<form-form class="row row-cols-1 row-cols-md-2 row-cols-lg-4 row-cols-xl-5 g-3 mb-3">
<form-input
type="select"
v-model="formData.stgkz"
@change="loadListSem(formData.stgkz)"
>
<option :value="null" selected>{{ $p.t('LvPlan/chooseStg') }}</option>
<option
v-for="stg in listStg"
:key="stg.studiengang_kz"
:value="stg.studiengang_kz"
>
{{ stg.kurzbzlang }} ({{ stg.bezeichnung }})
</option>
</form-input>
<form-input
type="select"
v-model="formData.sem"
@change="loadListVerband()"
@click="loadListSem(formData.stgkz)"
>
<option :value="null" selected>Semester</option>
<option
v-for="sem in listSem"
:key="sem"
:value="sem"
>
{{ sem }}
</option>
</form-input>
<form-input
type="select"
v-model="formData.verband"
@change="loadListGroup()"
>
<option :value="null" selected>{{ $p.t('lehre/verband') }} </option>
<option
v-for="verband in listVerband"
:key="verband"
:value="verband"
>
{{ verband }}
</option>
</form-input>
<form-input
type="select"
v-model="formData.gruppe"
>
<option :value="null" selected>{{ $p.t('gruppenmanagement/gruppe') }}</option>
<option
v-for="group in listGroup"
:key="group"
:value="group"
>
{{ group }}
</option>
</form-input>
<button type="button" class="btn btn-secondary" @click="loadLvPlan">{{ $p.t('LvPlan/loadLvPlan') }}</button>
</form-form>
</div>
<template v-if="timezone">
<fhc-calendar
v-if="propsViewData && propsViewData.stgkz"
ref="calendar"
v-model:lv="formData"
:timezone="timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@update:range="updateRange"
class="responsive-calendar"
>
<div
v-if="downloadLinks"
class="d-flex gap-1 justify-items-start"
>
<div v-for="{ title, icon, link } in downloadLinks">
<a
:href="link"
:aria-label="title"
class="py-1 btn btn-outline-secondary"
>
<div class="d-flex flex-column">
<i aria-hidden="true" :class="icon"></i>
<span style="font-size:.5rem">{{ title }}</span>
</div>
</a>
</div>
</div>
</fhc-calendar>
</template>
</div>
`,
};
+36 -93
View File
@@ -30,7 +30,6 @@ export default {
menuOpen:true,
};
},
inject: ["isNarrow", "isMobile"],
provide(){
return{
setActiveEntry: this.setActiveEntry,
@@ -59,7 +58,7 @@ export default {
},
site_url(){
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
},
}
},
methods: {
fetchMenu() {
@@ -113,101 +112,45 @@ export default {
});
},
template: /*html*/`
<div id="cis-header-bar" class="d-flex flex-row flex-grow-1">
<div id="nav-logo" class="d-none d-lg-block">
<div class="d-flex h-100 justify-content-between">
<a :href="rootUrl">
<img :src="logoUrl" alt="Corporate Identity Logo">
</a>
</div>
</div>
<div
v-if="isNarrow"
:class="{'collapse multi-collapse collapse-horizontal show': isMobile}"
id="navbar-toggler-collapsible"
>
<div class="d-flex flex-row align-items-center h-100" style="width: 35px">
<button id="nav-main-btn" class="navbar-toggler rounded-0 px-2 border-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<fhc-searchbar
:searchoptions="searchbaroptions"
:searchfunction="searchfunction"
ref="searchbar"
id="nav-search"
class="fhc-searchbar flex-grow-1 py-1 py-lg-2"
>
<template #collapseToggler="{ isSearchShownInMobileView }">
<span
v-if="isMobile"
type="button"
data-bs-toggle="collapse"
data-bs-target=".multi-collapse"
aria-controls="searchbar-collapsible navbar-toggler-collapsible options-collapsible"
aria-expanded="false"
class="d-flex flex-row align-items-center pe-1"
style="color: white"
>
<i v-if="isSearchShownInMobileView" class="fa-solid fa-chevron-left ps-3"></i>
<i v-else class="fa-solid fa-magnifying-glass ps-2"></i>
</span>
</template>
</fhc-searchbar>
<div
id="options-collapsible"
:class="{'collapse multi-collapse collapse-horizontal show': isMobile}"
>
<div :style="!isMobile ? '' : 'width: 105px'" class="d-flex flex-row ps-3 justify-content-end">
<span class="d-flex flex-row align-items-center">
<theme-switch></theme-switch>
</span>
<div id="nav-user">
<button id="nav-user-btn" class="btn btn-link rounded-0" type="button" data-bs-toggle="collapse" data-bs-target="#nav-user-menu" aria-expanded="false" aria-controls="nav-user-menu">
<img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/>
</button>
<ul ref="navUserDropdown"
@[\`shown.bs.collapse\`]="handleShowNavUser"
@[\`hide.bs.collapse\`]="handleHideNavUser"
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn">
<li><a class="fhc-dark-bg btn rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li>
<li >
<cis-sprachen @languageChanged="fetchMenu"></cis-sprachen>
</li>
<li><hr class="dropdown-divider m-0 "></li>
<li ><a class="fhc-dark-bg btn rounded-0 d-block" :href="logoutUrl">Logout</a></li>
</ul>
</div>
</div>
<button id="nav-main-btn" class="navbar-toggler rounded-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<fhc-searchbar ref="searchbar" id="nav-search" class="fhc-searchbar w-100 py-1 py-lg-2" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar>
<div id="nav-logo" class="d-none d-lg-block">
<div class="d-flex h-100 justify-content-between">
<a :href="rootUrl">
<img :src="logoUrl" alt="Corporate Identity Logo">
</a>
<theme-switch></theme-switch>
</div>
</div>
<div id="nav-user">
<button id="nav-user-btn" class="btn btn-link rounded-0" type="button" data-bs-toggle="collapse" data-bs-target="#nav-user-menu" aria-expanded="false" aria-controls="nav-user-menu">
<img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/>
</button>
<ul ref="navUserDropdown"
@[\`shown.bs.collapse\`]="handleShowNavUser"
@[\`hide.bs.collapse\`]="handleHideNavUser"
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn">
<li><a class="fhc-dark-bg btn rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li>
<li >
<cis-sprachen @languageChanged="fetchMenu"></cis-sprachen>
</li>
<li><hr class="dropdown-divider m-0 "></li>
<li ><a class="fhc-dark-bg btn rounded-0 d-block" :href="logoutUrl">Logout</a></li>
</ul>
</div>
<nav id="nav-main" class="offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false">
<div id="nav-main-sticky">
<div class="d-flex flex-row h-100">
<div class="offcanvas-body p-0">
<div id="nav-main-menu" class="nav-menu-collapse collapse collapse-horizontal show">
<div class="flex-grow-1">
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeEntry" v-for="entry in entries" :key="entry.content_id" :entry="entry" />
</div>
</div>
</div>
<div id="nav-main-toggle" class="d-none d-lg-block">
<div
@click="menuOpen = !menuOpen"
:aria-label="menuCollapseAriaLabel"
type="button"
class="h-100 d-flex align-items-center px-2"
data-bs-toggle="collapse"
data-bs-target=".nav-menu-collapse"
aria-expanded="true"
aria-controls="nav-sprachen nav-main-menu"
>
<i aria-hidden="true" class="fa-solid fa-chevron-left fhc-text"></i>
<div id="nav-main-toggle" class="position-static d-none d-lg-block ">
<button :aria-label="menuCollapseAriaLabel" type="button" @click="menuOpen = !menuOpen" class="btn text-light rounded-0 p-1 d-flex align-items-center" data-bs-toggle="collapse" data-bs-target=".nav-menu-collapse" aria-expanded="true" aria-controls="nav-sprachen nav-main-menu">
<i aria-hidden="true" class="fa fa-arrow-circle-left fhc-text"></i>
</button>
</div>
<div class="offcanvas-body p-0">
<div id="nav-main-menu" class="nav-menu-collapse collapse collapse-horizontal show">
<div>
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeEntry" v-for="entry in entries" :key="entry.content_id" :entry="entry" />
</div>
</div>
</div>
+38 -5
View File
@@ -68,8 +68,41 @@ export default {
return false;
}
},
link() {
if (this.entry.template_kurzbz == 'redirect') {
if (!this.entry.content)
return '';
let xmlDoc = (new DOMParser()).parseFromString(this.entry.content,"text/xml");
let url = xmlDoc.getElementsByTagName('url')[0];
if (!url)
return '';
// TODO(chris): replace get params
url = url.childNodes[0].nodeValue + "";
if (url.includes("../cms/news.php")) {
let news_regex = new RegExp("^\.\./cms/news\.php");
url = url.replace(news_regex, FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/news');
}
else if (url.includes("../cms/content.php?")) {
let content_regex = new RegExp("^\.\./cms/content.php\\?content_id=([0-9]+)");
let content_regex_result = content_regex.exec(url);
// content_regex_result[1] will be the first matched group
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/content/' + content_regex_result[1];
}
else if(url.includes("../index.ci.php")){
let index_regex = new RegExp("^\.\./index\.ci\.php");
url = url.replace(index_regex, FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router);
}
else if (url.includes("../")) {
let relative_regex = new RegExp("^\.\./");
url = url.replace(relative_regex, FHC_JS_DATA_STORAGE_OBJECT.app_root);
}
return url;
}
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/content/' + this.entry.content_id;
},
hasFullLink() {
return this.entry.url.startsWith(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router)
return this.link.startsWith(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router)
},
target() {
if (this.entry.template_kurzbz == 'redirect') {
@@ -112,7 +145,7 @@ export default {
this.addUrlCount(count);
},
checkActiveUrl(url){
this.getUrlMatchPoints(url,this.entry.url);
this.getUrlMatchPoints(url,this.link);
let url_hash_spaceSymbol_regex = new RegExp("%20","gi");
let url_hash_sharpSymbol_regex = new RegExp("^#");
@@ -122,7 +155,7 @@ export default {
// if the url hash contains the titel of the menu
// or if the url equals the link of a menu
// then set the menu active
if (url_hash == this.entry.titel || url.href == this.entry.url) {
if (url_hash == this.entry.titel || url.href == this.link) {
this.setActiveEntry(this.entry.content_id);
}
},
@@ -171,7 +204,7 @@ export default {
<template v-if="hasChilds">
<div class="btn-group w-100">
<a :target="target"
:href="(entry.menu_open && hasFullLink) ? entry.url : null"
:href="(entry.menu_open && hasFullLink)?link:null"
@click="toggleCollapse"
:class="{
'btn btn-default rounded-0 text-start': true,
@@ -194,7 +227,7 @@ export default {
</ul>
</template>
<a v-else
:href="entry.url"
:href="link"
:target="target"
:class="{
'btn btn-default rounded-0 w-100 text-start': true,
@@ -100,7 +100,7 @@ export default {
</template>
<template v-else>
<span v-if="event?.lehrfach_bez ">{{event?.lehrfach_bez + (event?.stg_kurzbzlang?' / ' + event?.stg_kurzbzlang:'')}}</span>
<span v-else>{{$p.t('lehre','lvUebersicht')}}</span>
<span v-else>Lehrveranstaltungs Übersicht</span>
</template>
</template>
@@ -2,8 +2,7 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js';
export const DEFAULT_MODE_RAUMINFO_MOBILE = 'List';
export const DEFAULT_MODE_RAUMINFO_DESKTOP = 'Week';
export const DEFAULT_MODE_RAUMINFO = 'Week'
export default {
name: "RoomInformation",
@@ -14,14 +13,12 @@ export default {
viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object
},
inject: ["isMobile"],
computed: {
currentDay() {
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
},
currentMode() {
const defaultMode = this.isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP;
return this.propsViewData?.mode || defaultMode;
return this.propsViewData?.mode || DEFAULT_MODE_RAUMINFO;
}
},
methods:{
@@ -54,6 +51,7 @@ export default {
<hr>
<fhc-calendar
ref="calendar"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
+2 -3
View File
@@ -36,9 +36,8 @@ export default {
return this.lvs.filter(lv => lv.studiengang_kz == studiengang.studiengang_kz && lv.semester == studiengang.semester);
}
},
template: `
<div class="mylv-semester" v-if="ready">
<mylv-semester-studiengang v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" v-bind="studiengang" :lvs="lvsForStudiengang(studiengang)" :semesterInfo="$props.semester" />
template: `<div class="mylv-semester" v-if="ready">
<mylv-semester-studiengang v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" v-bind="studiengang" :lvs="lvsForStudiengang(studiengang)"/>
</div>
<div class="mylv-semester text-center" v-else>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
@@ -1,11 +1,9 @@
import MylvSemesterStudiengangLv from "./Studiengang/Lv.js";
import MylvSemesterStudiengangAverageGrade from "./Studiengang/AverageGrade.js";
import Phrasen from "../../../../mixins/Phrasen.js";
export default {
components: {
MylvSemesterStudiengangLv,
MylvSemesterStudiengangAverageGrade
MylvSemesterStudiengangLv
},
mixins: [
Phrasen
@@ -14,7 +12,6 @@ export default {
bezeichnung: String,
kuerzel: String,
semester: [String,Number],
semesterInfo: [String,Number],
lvs: Array,
sg_bezeichnung_eng: String
},
@@ -32,10 +29,9 @@ export default {
methods: {
note(lv) {
return lv.benotung ? lv.znote || lv.lvnote || null : null;
},
}
},
template: `<div class="card mb-3">
<div class="card-body">
<h4 class="card-title mb-3">{{$p.user_language.value === 'English' ? sg_bezeichnung_eng : bezeichnung}} - {{kuerzel}}
<small>{{semester}}.{{$p.t('lehre/semester')}}</small>
@@ -45,7 +41,6 @@ export default {
<mylv-semester-studiengang-lv v-bind="lv" class="text-center h-100"></mylv-semester-studiengang-lv>
</div>
</div>
<mylv-semester-studiengang-average-grade :semesterInfo="$props.semesterInfo" />
</div>
</div>`
};
@@ -1,86 +0,0 @@
import Phrasen from "../../../../../mixins/Phrasen.js";
import ApiLehre from "../../../../../api/factory/lehre.js";
export default {
mixins: [
Phrasen
],
props: {
semesterInfo: String,
},
data() {
return {
gradeAverage: null,
gradeWeightedAverage: null,
}
},
methods: {
async fetchAverageGrade() {
this.gradeAverage = null;
this.gradeWeightedAverage = null;
if (!this.$props.semesterInfo) return;
let gradeAverageResponse = await this.$api.call(
ApiLehre.getSemesterAverageGrade(this.$props.semesterInfo),
);
const gradeAverageResponseData = gradeAverageResponse.data;
this.gradeAverage =
gradeAverageResponseData.average_grade?.toFixed(2);
this.gradeWeightedAverage =
gradeAverageResponseData.weighted_average_grade?.toFixed(2);
},
},
watch: {
semesterInfo() {
this.fetchAverageGrade();
},
},
async created() {
await this.fetchAverageGrade();
},
template: /*html*/`
<div class="card mylv-semester-studiengang-grades">
<div class="card-header text-center">
<h6>{{$p.t('lehre/notenstatistik')}}</h6>
</div>
<div v-if="gradeAverage && gradeWeightedAverage">
<table class="card-body table w-auto mx-auto">
<tbody>
<tr>
<td class="text-end">
{{$p.t('lehre/headerAverage')}}
</td>
<td class="text-start">
{{ gradeAverage }}
</td>
</tr>
<tr>
<td class="text-end">
{{$p.t('lehre/headerWeightedAverage')}}
</td>
<td class="text-start">
{{ gradeWeightedAverage }}
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="card-body text-center">
<p>{{$p.t('lehre/info_noGradesYet')}}</p>
</div>
<div v-if="gradeAverage && gradeWeightedAverage" class="card-footer d-flex align-items-start text-muted small">
<i class="fa fa-circle-info me-2 mt-1"></i>
<div>
<strong>{{$p.t('ui', 'hinweis')}}</strong><br>
* {{$p.t('lehre/noticeAverage')}}
<br>
** {{$p.t('lehre/noticeWeightedAverage')}}
</div>
</div>
</div>
`
}
@@ -24,9 +24,9 @@ export default {
farbe: String,
lvinfo: Boolean,
benotung: Boolean,
lvnote: [String, Number],
lvnote: String,
lvnotebez: Array,
znote: [String, Number],
znote: String,
znotebez: Array,
studiengang_kuerzel: String,
semester: [String, Number],
@@ -37,11 +37,6 @@ export default {
positiv: Boolean,
note_index: String
},
provide() {
return {
studium_studiensemester: Vue.computed(() => this.studien_semester),
}
},
data: () => {
return {
pruefungenData: null,
@@ -78,11 +73,11 @@ export default {
},
grade() {
const languageIndex = this.$p.user_language.value === 'English' ? 1 : 0
// no more showing of grade LV, if grade Zeugnis is not existing yet
if(this.benotung && this.znotebez?.length) {
return this.znotebez[languageIndex]
}
else return null
} else if(this.benotung && this.lvnotebez?.length) {
return this.lvnotebez[languageIndex]
} else return null
},
LvHasPruefungenInformation(){
return this.pruefungenData && this.pruefungenData.length > 0;
@@ -124,6 +119,7 @@ export default {
}
},
openPruefungen() {
// early return if the pruefungenData is empty or not set
if (!this.LvHasPruefungenInformation) return;
LvPruefungen.popup({
@@ -209,14 +205,21 @@ export default {
</template>
</div>
<div v-if="!emptyMenu" class="card-footer">
<div
@click.prevent="openPruefungen()"
:type="LvHasPruefungenInformation ? 'button' : ''"
class="d-flex flex-row align-items-center gap-1"
>
<i class="fa fa-check text-success" v-if="positiv"></i>
<span :style="'color:'+gradeColor">{{ grade || $p.t('lehre/noGrades') }}</span>
<i v-if="LvHasPruefungenInformation" class="fa fa-circle-info ms-1"></i>
<div class="row">
<!-- template for the LV if there are multiple pruefungen -->
<template v-if="LvHasPruefungenInformation">
<a href="#" class="col-auto text-start text-decoration-none" @click.prevent="openPruefungen">
<i class="fa fa-check text-success" v-if="positiv"></i>
<span class="ps-1" :style="'color:'+gradeColor">{{ grade || $p.t('lehre/noGrades') }}</span>
</a>
</template>
<!-- template for the LV with no pruefungen -->
<template v-else>
<span class="col-auto text-start text-decoration-none" >
<i class="fa fa-check text-success" v-if="positiv"></i>
<span class="ps-1" :style="'color:'+gradeColor">{{ grade || $p.t('lehre/noGrades') }}</span>
</span>
</template>
</div>
</div>
</div>`
+25 -26
View File
@@ -96,35 +96,34 @@ export default {
},
template: `
<div>
<h2>{{$p.t('lehre/myLV')}}</h2>
<hr>
<div class="mylv-student" v-if="ready">
<div v-if="currentSemester" class="row justify-content-center mb-3">
<div class="col-auto d-none">
<label class="col-form-label">{{$p.t('lehre/studiensemester')}}</label>
</div>
<div class="col-auto">
<div class="input-group">
<button :aria-label="$p.t('lehre','previousStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','previousStudSemester')}" class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
<i class="fa fa-caret-left" aria-hidden="true"></i>
</button>
<select ref="studiensemester" v-model="currentSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="updateRouter($event.target.value)">
<option v-for="semester in studiensemester" :key="semester.studiensemester_kurzbz">{{semester.studiensemester_kurzbz}}</option>
</select>
<button class="btn btn-outline-secondary" :aria-label="$p.t('lehre','nextStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','nextStudSemester')}" type="button" :disabled="currentIsLast" @click="nextSem">
<i class="fa fa-caret-right" aria-hidden="true"></i>
</button>
</div>
<h2>{{$p.t('lehre/myLV')}}</h2>
<hr>
<div class="mylv-student" v-if="ready">
<div v-if="currentSemester" class="row justify-content-center mb-3">
<div class="col-auto d-none">
<label class="col-form-label">{{$p.t('lehre/studiensemester')}}</label>
</div>
<div class="col-auto">
<div class="input-group">
<button :aria-label="$p.t('lehre','previousStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','previousStudSemester')}" class="btn btn-outline-secondary" type="button" :disabled="currentIsFirst" @click="prevSem">
<i class="fa fa-caret-left" aria-hidden="true"></i>
</button>
<select ref="studiensemester" v-model="currentSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="updateRouter($event.target.value)">
<option v-for="semester in studiensemester" :key="semester.studiensemester_kurzbz">{{semester.studiensemester_kurzbz}}</option>
</select>
<button class="btn btn-outline-secondary" :aria-label="$p.t('lehre','nextStudSemester')" v-tooltip.top="{showDelay:1000, value:$p.t('lehre','nextStudSemester')}" type="button" :disabled="currentIsLast" @click="nextSem">
<i class="fa fa-caret-right" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="alert alert-danger" role="alert" v-else>
{{$p.t('lehre/noLvFound')}}
</div>
<mylv-semester v-bind="current"/>
</div>
<div class="mylv-student text-center" v-else>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
<div class="alert alert-danger" role="alert" v-else>
{{$p.t('lehre/noLvFound')}}
</div>
<mylv-semester v-bind="current"/>
</div>
<div class="mylv-student text-center" v-else>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>`
};

Some files were not shown because too many files have changed in this diff Show More