Merge branch 'cis40_2026-02_rc' into feature-76669/optimizing-menu-load

This commit is contained in:
adisposkofh
2026-04-28 13:00:27 +02:00
112 changed files with 7185 additions and 2776 deletions
+3
View File
@@ -64,6 +64,9 @@ $route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1';
$route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1'; $route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1';
$route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1'; $route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1';
$route['Cis/MyLv/.*'] = 'Cis/MyLv/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'] = 'Cis/Abgabetool/Assistenz';
$route['Abgabetool/Assistenz/(:any)'] = 'Cis/Abgabetool/Assistenz/$1'; $route['Abgabetool/Assistenz/(:any)'] = 'Cis/Abgabetool/Assistenz/$1';
+5 -26
View File
@@ -31,12 +31,8 @@ class Abgabetool extends Auth_Controller
{ {
// TODO: routing from index based on berechtigung? // TODO: routing from index based on berechtigung?
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) { if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Abgabetool']); $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'Abgabetool']);
} else { } else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'Abgabetool']); $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'Abgabetool']);
} }
@@ -44,12 +40,8 @@ class Abgabetool extends Auth_Controller
public function Student($student_uid_prop = '') public function Student($student_uid_prop = '')
{ {
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) { if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'AbgabetoolStudent']); $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'AbgabetoolStudent']);
} else { } else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolStudent', 'student_uid_prop' => $student_uid_prop]); $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolStudent', 'student_uid_prop' => $student_uid_prop]);
} }
@@ -57,12 +49,8 @@ class Abgabetool extends Auth_Controller
public function Mitarbeiter() public function Mitarbeiter()
{ {
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) { if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'AbgabetoolMitarbeiter']); $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'AbgabetoolMitarbeiter']);
} else { } else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolMitarbeiter']); $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolMitarbeiter']);
} }
@@ -70,13 +58,8 @@ class Abgabetool extends Auth_Controller
public function Assistenz($stg_kz_prop = '') public function Assistenz($stg_kz_prop = '')
{ {
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) { if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'AbgabetoolAssistenz']); $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'AbgabetoolAssistenz']);
} else { } else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolAssistenz', 'stg_kz_prop' => $stg_kz_prop]); $this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'AbgabetoolAssistenz', 'stg_kz_prop' => $stg_kz_prop]);
} }
@@ -84,12 +67,8 @@ class Abgabetool extends Auth_Controller
public function Deadlines() public function Deadlines()
{ {
$viewData = array(
'uid'=>getAuthUID(),
);
if(defined('CIS4') && CIS4) { if(defined('CIS4') && CIS4) {
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'DeadlinesOverview']); $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'DeadlinesOverview']);
} else { } else {
$this->load->view('Cis/Abgabetool.php', ['uid' => getAuthUID(), 'route' => 'DeadlinesOverview']); $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()) if ($this->form_validation->run())
{ {
redirect($this->authlib->getLandingPage('/CisVue/Dashboard')); redirect($this->authlib->getLandingPage('/Cis4'));
} }
else else
{ {
+1 -7
View File
@@ -28,12 +28,6 @@ class LvPlan extends Auth_Controller
*/ */
public function index() 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']);
} }
} }
+1 -6
View File
@@ -26,11 +26,6 @@ class MyLv extends Auth_Controller
*/ */
public function index() public function index()
{ {
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'MyLv']);
$viewData = array(
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'MyLv']);
} }
} }
+2 -8
View File
@@ -27,13 +27,7 @@ class MyLvPlan extends Auth_Controller
* @return void * @return void
*/ */
public function index() 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']);
} }
} }
@@ -0,0 +1,34 @@
<?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']);
}
}
+19 -39
View File
@@ -55,15 +55,7 @@ class Profil extends Auth_Controller
*/ */
public function index() 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']);
} }
/** /**
@@ -73,23 +65,13 @@ class Profil extends Auth_Controller
*/ */
public function View($uid) public function View($uid)
{ {
$this->load->library('ProfilLib'); $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'profilViewUid']);
$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 mitarbeiter or not (foreword declaration of the function isMitarbeiter in Mitarbeiter_model.php) * checks whether a specific userID is a student or not (foreword declaration of the function isStudent in Student_model.php)
* @access public * @access public
* @param $uid the userID used to check if it is a mitarbeiter * @param $uid the userID used to check if it is a student
* @return boolean * @return boolean
*/ */
public function isStudent($uid) public function isStudent($uid)
@@ -119,7 +101,7 @@ class Profil extends Auth_Controller
} }
/** /**
* gets the adressen that are marked as zustell from the currenlty logged in user * gets the adressen that are marked as zustell from the currently logged in user
* @access public * @access public
* @return array a list of adresse_id's * @return array a list of adresse_id's
*/ */
@@ -262,23 +244,23 @@ class Profil extends Auth_Controller
$this->GemeindeModel->addDistinct(); $this->GemeindeModel->addDistinct();
$this->GemeindeModel->addSelect(["name"]); $this->GemeindeModel->addSelect(["name"]);
if ($nation == "A") { if ($nation == "A") {
if (isset($zip) && $zip > 999 && $zip < 32000) { if (isset($zip) && $zip > 999 && $zip < 32000) {
$gemeinde_res = $this->GemeindeModel->loadWhere(['plz' => $zip]); $gemeinde_res = $this->GemeindeModel->loadWhere(['plz' => $zip]);
if (isError($gemeinde_res)) { if (isError($gemeinde_res)) {
show_error("error while trying to query bis.tbl_gemeinde"); 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 { } else {
echo json_encode(error("Nation was not 'A' (Austria)")); echo json_encode(error("Nation was not 'A' (Austria)"));
} }
} }
@@ -750,6 +732,4 @@ class Profil extends Auth_Controller
$zutrittskarte_ausgegebenam = str_replace("-", ".", $zutrittskarte_ausgegebenam); $zutrittskarte_ausgegebenam = str_replace("-", ".", $zutrittskarte_ausgegebenam);
return $zutrittskarte_ausgegebenam; return $zutrittskarte_ausgegebenam;
} }
} }
+1 -6
View File
@@ -25,11 +25,6 @@ class Raumsuche extends Auth_Controller
*/ */
public function index() 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']);
} }
} }
@@ -0,0 +1,33 @@
<?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']);
}
}
+1 -4
View File
@@ -29,10 +29,7 @@ class Studium extends Auth_Controller
*/ */
public function index() public function index()
{ {
$viewData = array( $this->load->view('CisRouterView/CisRouterView.php',['route' => 'studium']);
);
$this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'studium']);
} }
+6 -15
View File
@@ -1,6 +1,7 @@
<?php <?php
if (! defined('BASEPATH')) exit('No direct script access allowed'); if (!defined('BASEPATH'))
exit('No direct script access allowed');
/** /**
* *
@@ -13,9 +14,9 @@ class Cis4 extends Auth_Controller
public function __construct() public function __construct()
{ {
parent::__construct( parent::__construct(
array( array(
'index' => 'basis/cis:r' 'index' => 'basis/cis:r'
) )
); );
// Load Config // Load Config
@@ -30,16 +31,6 @@ class Cis4 extends Auth_Controller
*/ */
public function index() public function index()
{ {
$this->load->model('person/Person_model', 'PersonModel'); $this->load->view('CisRouterView/CisRouterView.php', ['route' => 'FhcDashboard']);
$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']);
} }
} }
@@ -1,43 +0,0 @@
<?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]);
}
}
@@ -18,6 +18,8 @@
if (! defined('BASEPATH')) exit('No direct script access allowed'); if (! defined('BASEPATH')) exit('No direct script access allowed');
use \DateTime as DateTime;
class Bookmark extends FHCAPI_Controller class Bookmark extends FHCAPI_Controller
{ {
@@ -28,111 +30,162 @@ class Bookmark extends FHCAPI_Controller
{ {
parent::__construct([ parent::__construct([
'getBookmarks' => self::PERM_LOGGED, 'getBookmarks' => self::PERM_LOGGED,
'delete' => self::PERM_LOGGED, 'delete' => self::PERM_LOGGED,
'insert' => self::PERM_LOGGED, 'insert' => self::PERM_LOGGED,
'update' => self::PERM_LOGGED, 'update' => self::PERM_LOGGED,
]); 'changeOrder' => self::PERM_LOGGED
]);
$this->load->model('dashboard/Bookmark_model', 'BookmarkModel'); $this->load->model('dashboard/Bookmark_model', 'BookmarkModel');
$this->uid = getAuthUID(); $this->uid = getAuthUID();
$this->pid = getAuthPersonID(); $this->pid = getAuthPersonID();
} }
//------------------------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------------------------
// Public methods // Public methods
/** /**
* gets the bookmarks associated to a user * gets the bookmarks associated to a user
* @access public * @access public
* @return void * @return void
*/ */
public function getBookmarks() public function getBookmarks()
{ {
$this->BookmarkModel->addOrder("bookmark_id"); $this->BookmarkModel->addOrder("sort");
$bookmarks = $this->BookmarkModel->loadWhere(["uid"=>$this->uid]); $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
* @access public
* @return void
*/
public function delete($bookmark_id)
{
$bookmark = $this->BookmarkModel->load($bookmark_id);
$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);
$delete_result = $this->getDataOrTerminateWithError($delete_result);
$this->terminateWithSuccess($delete_result);
}else{
$this->_outputAuthError(['delete' => ['admin:rw']]);
}
}
/**
* inserts new bookmark into the bookmark table
* @access public
* @return void
*/
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() == 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);
$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);
$this->terminateWithSuccess($insert_into_result);
}
/** /**
* updates bookmark in the bookmark table * deletes bookmark from associated user
* @access public * @access public
* @return void * @return void
*/ */
public function update($bookmark_id) public function delete($bookmark_id)
{ {
// form validation $bookmark = $this->BookmarkModel->load($bookmark_id);
$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); $bookmark = current($this->getDataOrTerminateWithError($bookmark));
$title = $this->input->post('title',true);
// 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);
$delete_result = $this->getDataOrTerminateWithError($delete_result);
$this->terminateWithSuccess($delete_result);
} else {
$this->_outputAuthError(['delete' => ['admin:rw']]);
}
}
/**
* inserts new bookmark into the bookmark table
* @access public
* @return void
*/
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());
$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);
$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->getDataOrTerminateWithError($insert_into_result);
$this->terminateWithSuccess($insert_into_result);
}
/**
* updates bookmark in the bookmark table
* @access public
* @return void
*/
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());
$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);
}
$now = new DateTime(); $now = new DateTime();
$now = $now->format('Y-m-d H:i:s'); $now = $now->format('Y-m-d H:i:s');
$update_result = $this->BookmarkModel->update($bookmark_id,['url'=>$url, 'title'=>$title,'updateamum'=>$now]); $update_result = $this->BookmarkModel->update($bookmark_id, [
'url' => $url,
'title' => $title,
'tag' => $tag,
'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() public function __construct()
{ {
parent::__construct([ parent::__construct([
'getViewData' => self::PERM_LOGGED, 'dashboardViewData' => self::PERM_LOGGED,
]); ]);
} }
@@ -36,17 +36,22 @@ class Cis4FhcApi extends FHCAPI_Controller
// Public methods // Public methods
/** /**
* fetches ViewData * retrieves view data for dashboard view
*/ * @access public
public function getViewData() * @param $uid the userID for which profile is being viewed, null or missing value implies one's own profile
*/
public function dashboardViewData()
{ {
$this->load->model('person/Person_model','PersonModel'); $this->load->model('person/Person_model','PersonModel');
$personData = getData($this->PersonModel->getByUid(getAuthUID()))[0]; $personData = getData($this->PersonModel->getByUid(getAuthUID()))[0];
$this->load->config('calendar');
$viewData = array( $viewData = array(
'uid' => getAuthUID(), 'uid' => getAuthUID(),
'name' => $personData->vorname, 'name' => $personData->vorname,
'person_id' => $personData->person_id 'person_id' => $personData->person_id,
'timezone' => $this->config->item('timezone'),
); );
$this->terminateWithSuccess($viewData); $this->terminateWithSuccess($viewData);
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 CI3_Events as Events;
use \DateTime as DateTime; use \DateTime as DateTime;
@@ -33,19 +34,25 @@ class LvPlan extends FHCAPI_Controller
parent::__construct([ parent::__construct([
'getRoomplan' => self::PERM_LOGGED, 'getRoomplan' => self::PERM_LOGGED,
'Stunden' => self::PERM_LOGGED, 'Stunden' => self::PERM_LOGGED,
'getReservierungen' => self::PERM_LOGGED, 'getReservierungen' => self::PERM_LOGGED,
'LvPlanEvents' => self::PERM_LOGGED, 'LvPlanEvents' => self::PERM_LOGGED,
'eventsPersonal' => self::PERM_LOGGED, 'eventsPersonal' => self::PERM_LOGGED,
'eventsLv' => self::PERM_LOGGED, 'eventsLv' => self::PERM_LOGGED,
'getLehreinheitStudiensemester' => self::PERM_LOGGED, 'getLehreinheitStudiensemester' => self::PERM_LOGGED,
'studiensemesterDateInterval' => self::PERM_LOGGED, 'studiensemesterDateInterval' => self::PERM_LOGGED,
'getLvPlanForStudiensemester' => self::PERM_LOGGED, 'getLvPlanForStudiensemester' => self::PERM_LOGGED,
'getLv' => 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,
]); ]);
$this->load->library('LogLib'); $this->load->library('LogLib');
$this->loglib->setConfigs(array( $this->loglib->setConfigs(array(
'classIndex' => 5, 'classIndex' => 5,
'functionIndex' => 5, 'functionIndex' => 5,
'lineIndex' => 4, 'lineIndex' => 4,
@@ -53,17 +60,17 @@ class LvPlan extends FHCAPI_Controller
'dbExecuteUser' => 'RESTful API' 'dbExecuteUser' => 'RESTful API'
)); ));
$this->load->library('form_validation'); $this->load->library('form_validation');
} }
//------------------------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------------------------
// Public methods // Public methods
/** /**
* fetches LvPlan and Moodle events together * fetches LvPlan and Moodle events together
* @access public * @access public
* *
*/ */
public function LvPlanEvents() public function LvPlanEvents()
{ {
$hasLv = $this->input->post('lv_id'); $hasLv = $this->input->post('lv_id');
@@ -83,24 +90,30 @@ class LvPlan extends FHCAPI_Controller
// form validation // form validation
$this->form_validation->set_rules('start_date', "start_date", "required"); $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('end_date', "end_date", "required");
if (!$this->form_validation->run()) if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array()); $this->terminateWithValidationErrors($this->form_validation->error_array());
// storing the post parameter in local variables // storing the post parameter in local variables
$start_date = $this->input->post('start_date', true); $start_date = $this->input->post('start_date', true);
$end_date = $this->input->post('end_date', true); $end_date = $this->input->post('end_date', true);
$uid = $this->input->post('uid', true);
// disallow accessing other user's lv plan 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 // fetching lvplan events
$result = $this->stundenplanlib->getEventsUser($start_date, $end_date); $result = $this->stundenplanlib->getEventsUser($start_date, $end_date, $uid);
$lvplanEvents = $this->getDataOrTerminateWithError($result); $lvplanEvents = $this->getDataOrTerminateWithError($result);
// fetching moodle events // fetching moodle events
$moodleEvents = $this->fetchMoodleEvents($start_date, $end_date); $moodleEvents = $uid ? [] : $this->fetchMoodleEvents($start_date, $end_date);
// fetching ferien events // fetching ferien events
$ferienEvents = $this->fetchFerienEvents($start_date, $end_date); $ferienEvents = $this->fetchFerienEvents($start_date, $end_date, $uid);
$this->terminateWithSuccess(array_merge( $this->terminateWithSuccess(array_merge(
$lvplanEvents, $lvplanEvents,
@@ -109,6 +122,45 @@ 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 * fetches LvPlan and Ferien events together for the lv
* *
@@ -122,7 +174,7 @@ class LvPlan extends FHCAPI_Controller
$this->form_validation->set_rules('start_date', "start_date", "required"); $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('end_date', "end_date", "required");
$this->form_validation->set_rules('lv_id', "lv_id", "required|integer"); $this->form_validation->set_rules('lv_id', "lv_id", "required|integer");
if (!$this->form_validation->run()) if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array()); $this->terminateWithValidationErrors($this->form_validation->error_array());
@@ -137,7 +189,6 @@ class LvPlan extends FHCAPI_Controller
// fetching ferien events // fetching ferien events
$ferienEvents = $this->fetchFerienEvents($start_date, $end_date); $ferienEvents = $this->fetchFerienEvents($start_date, $end_date);
$this->terminateWithSuccess(array_merge( $this->terminateWithSuccess(array_merge(
$lvplanEvents, $lvplanEvents,
@@ -146,40 +197,42 @@ class LvPlan extends FHCAPI_Controller
} }
//TODO: delete this function if we don't use the old calendar export endpoints anymore //TODO: delete this function if we don't use the old calendar export endpoints anymore
public function studiensemesterDateInterval($date){ public function studiensemesterDateInterval($date)
$this->load->model('organisation/Studiensemester_model','StudiensemesterModel'); {
$studiensemester =$this->StudiensemesterModel->getByDate(date_format(date_create($date),'Y-m-d')); $this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$studiensemester =current($this->getDataOrTerminateWithError($studiensemester)); $studiensemester = $this->StudiensemesterModel->getByDate(date_format(date_create($date), 'Y-m-d'));
$studiensemester = current($this->getDataOrTerminateWithError($studiensemester));
$this->terminateWithSuccess($studiensemester); $this->terminateWithSuccess($studiensemester);
} }
public function getLvPlanForStudiensemester($studiensemester,$lvid){ public function getLvPlanForStudiensemester($studiensemester, $lvid)
{
$this->load->library('StundenplanLib'); $this->load->library('StundenplanLib');
$this->load->model('organisation/Studiensemester_model','StudiensemesterModel'); $this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$studiensemester_result = $this->StudiensemesterModel->loadWhere(["studiensemester_kurzbz"=>$studiensemester]); $studiensemester_result = $this->StudiensemesterModel->loadWhere(["studiensemester_kurzbz" => $studiensemester]);
$studiensemester_result = current($this->getDataOrTerminateWithError($studiensemester_result)); $studiensemester_result = current($this->getDataOrTerminateWithError($studiensemester_result));
$timespan_start = new DateTime($studiensemester_result->start); $timespan_start = new DateTime($studiensemester_result->start);
$timespan_ende = new DateTime($studiensemester_result->ende); $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); $this->terminateWithSuccess($lvplan);
}
/** }
* fetches Stunden layout from database
* @access public
* /**
*/ * fetches Stunden layout from database
public function Stunden() * @access public
*
*/
public function Stunden()
{ {
$this->load->model('ressource/Stunde_model', 'StundeModel'); $this->load->model('ressource/Stunde_model', 'StundeModel');
$this->StundeModel->addOrder('stunde', 'ASC'); $this->StundeModel->addOrder('stunde', 'ASC');
$stunden = $this->StundeModel->load(); $stunden = $this->StundeModel->load();
$stunden = $this->getDataOrTerminateWithError($stunden); $stunden = $this->getDataOrTerminateWithError($stunden);
$this->terminateWithSuccess($stunden); $this->terminateWithSuccess($stunden);
} }
@@ -210,10 +263,10 @@ class LvPlan extends FHCAPI_Controller
$roomplan_data = $this->stundenplanlib->getRoomplan($ort_kurzbz, $start_date, $end_date); $roomplan_data = $this->stundenplanlib->getRoomplan($ort_kurzbz, $start_date, $end_date);
$roomplan_data = $this->getDataOrTerminateWithError($roomplan_data); $roomplan_data = $this->getDataOrTerminateWithError($roomplan_data);
$this->terminateWithSuccess($roomplan_data); $this->terminateWithSuccess($roomplan_data);
} }
/** /**
* gets the reservierungen of a room if the ort_kurzbz parameter is * gets the reservierungen of a room if the ort_kurzbz parameter is
* supplied otherwise gets the reservierungen of the lvplan of a student * supplied otherwise gets the reservierungen of the lvplan of a student
@@ -226,25 +279,27 @@ class LvPlan extends FHCAPI_Controller
{ {
$this->form_validation->set_rules('start_date', "StartDate", "required"); $this->form_validation->set_rules('start_date', "StartDate", "required");
$this->form_validation->set_rules('end_date', "EndDate", "required"); $this->form_validation->set_rules('end_date', "EndDate", "required");
if (!$this->form_validation->run()) if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array()); $this->terminateWithValidationErrors($this->form_validation->error_array());
// storing the post parameter in local variables // storing the post parameter in local variables
$start_date = $this->input->post('start_date', true); $start_date = $this->input->post('start_date', true);
$end_date = $this->input->post('end_date', true); $end_date = $this->input->post('end_date', true);
$uid = $this->input->post('uid', true);
// get data // get data
$this->load->library('StundenplanLib'); $this->load->library('StundenplanLib');
$result = $this->stundenplanlib->getReservierungen($start_date, $end_date, $ort_kurzbz); $result = $this->stundenplanlib->getReservierungen($start_date, $end_date, $ort_kurzbz, $uid);
$result = $this->getDataOrTerminateWithError($result); $result = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($result); $this->terminateWithSuccess($result);
} }
public function getLehreinheitStudiensemester($lehreinheit_id){ public function getLehreinheitStudiensemester($lehreinheit_id)
{
$this->load->model('education/Lehreinheit_model', 'LehreinheitModel'); $this->load->model('education/Lehreinheit_model', 'LehreinheitModel');
$this->LehreinheitModel->addSelect(["studiensemester_kurzbz"]); $this->LehreinheitModel->addSelect(["studiensemester_kurzbz"]);
$result = $this->LehreinheitModel->load($lehreinheit_id); $result = $this->LehreinheitModel->load($lehreinheit_id);
@@ -287,6 +342,68 @@ class LvPlan extends FHCAPI_Controller
return $this->terminateWithSuccess(current($result)); 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"]);
}
/** /**
* fetch moodle events * fetch moodle events
* *
@@ -299,19 +416,19 @@ class LvPlan extends FHCAPI_Controller
$this->load->config('calendar'); $this->load->config('calendar');
$tz = new DateTimeZone($this->config->item('timezone')); $tz = new DateTimeZone($this->config->item('timezone'));
$start = new DateTime($start_date); $start = new DateTime($start_date);
$start->setTimezone($tz); $start->setTimezone($tz);
$end = new DateTime($end_date); $end = new DateTime($end_date);
$end->setTimezone($tz); $end->setTimezone($tz);
$end->modify('+1 day -1 second'); $end->modify('+1 day -1 second');
$moodle_events = []; $moodle_events = [];
Events::trigger( Events::trigger(
'moodleCalendarEvents', 'moodleCalendarEvents',
function & () use (&$moodle_events) { function &() use (&$moodle_events) {
return $moodle_events; return $moodle_events;
}, },
[ [
@@ -331,23 +448,23 @@ class LvPlan extends FHCAPI_Controller
* @param string $end_date * @param string $end_date
* @return array * @return array
*/ */
private function fetchFerienEvents($start_date, $end_date) private function fetchFerienEvents($start_date, $end_date, $uid = null)
{ {
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel'); $this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->load->model('education/Studentlehrverband_model', 'StudentLehrverbandModel'); $this->load->model('education/Studentlehrverband_model', 'StudentLehrverbandModel');
$currentStudiensemester = $this->StudiensemesterModel->getByDate($start_date); $currentStudiensemester = $this->StudiensemesterModel->getByDate($start_date);
$currentStudiensemester = $this->getDataOrTerminateWithError($currentStudiensemester); $currentStudiensemester = $this->getDataOrTerminateWithError($currentStudiensemester);
if ($currentStudiensemester) { if ($currentStudiensemester) {
$studentsemester_kurzbz = current($currentStudiensemester)->studiensemester_kurzbz; $studentsemester_kurzbz = current($currentStudiensemester)->studiensemester_kurzbz;
$studiengang = $this->StudentLehrverbandModel->loadWhere([ $studiengang = $this->StudentLehrverbandModel->loadWhere([
"student_uid" => getAuthUID(), "student_uid" => $uid ?? getAuthUID(),
"studiensemester_kurzbz" => $studentsemester_kurzbz "studiensemester_kurzbz" => $studentsemester_kurzbz
]); ]);
$studiengang = $this->getDataOrTerminateWithError($studiengang); $studiengang = $this->getDataOrTerminateWithError($studiengang);
if ($studiengang) if ($studiengang)
$studiengang_kz = current($studiengang)->studiengang_kz; $studiengang_kz = current($studiengang)->studiengang_kz;
else else
@@ -357,7 +474,7 @@ class LvPlan extends FHCAPI_Controller
} }
$ferienEvents = $this->stundenplanlib->fetchFerienTageEvents($start_date, $end_date, $studiengang_kz); $ferienEvents = $this->stundenplanlib->fetchFerienTageEvents($start_date, $end_date, $studiengang_kz);
return $this->getDataOrTerminateWithError($ferienEvents); return $this->getDataOrTerminateWithError($ferienEvents);
} }
} }
@@ -0,0 +1,76 @@
<?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
}
@@ -16,7 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 class Profil extends FHCAPI_Controller
{ {
@@ -27,13 +28,13 @@ class Profil extends FHCAPI_Controller
public function __construct() public function __construct()
{ {
parent::__construct([ parent::__construct([
'fotoSperre' => self::PERM_LOGGED, 'fotoSperre' => self::PERM_LOGGED,
'getGemeinden' => self::PERM_LOGGED, 'getGemeinden' => self::PERM_LOGGED,
'getAllNationen' => self::PERM_LOGGED, 'getAllNationen' => self::PERM_LOGGED,
'isMitarbeiter' => self::PERM_LOGGED, 'isMitarbeiter' => self::PERM_LOGGED,
'profilViewData' => self::PERM_LOGGED, 'profilViewData' => self::PERM_LOGGED,
]); ]);
$this->load->library('PermissionLib'); $this->load->library('PermissionLib');
$this->load->model('ressource/mitarbeiter_model', 'MitarbeiterModel'); $this->load->model('ressource/mitarbeiter_model', 'MitarbeiterModel');
@@ -48,28 +49,37 @@ class Profil extends FHCAPI_Controller
//------------------------------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------------------------------
// Public methods // Public methods
public function profilViewData($uid=null){
/**
* 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;
$this->load->library('ProfilLib'); $this->load->library('ProfilLib');
$editable = false; $profileData = $this->profillib->getView($uid ?? $authUid);
if(isset($uid) && $uid != null){ $profileData = hasData($profileData) ? getData($profileData) : null;
$profil_data = $this->profillib->getView($uid);
if($uid == getAuthUID()){ $viewData = [
$editable = true; 'editable' => $isProfilOfAuthUser,
} 'profil_data' => $profileData,
}else{ 'permissions' => [
$editable = true; 'basis/other_lv_plan' => $this->permissionlib->isBerechtigt(('basis/other_lv_plan'))
$profil_data = $this->profillib->getView(getAuthUID()); ]
];
if ($isProfilOfAuthUser) {
$viewData['calendar_sync_urls'] = $this->getCalendarSyncUrlData();
} }
$profil_data = hasData($profil_data) ? getData($profil_data) : null;
$viewData = array(
'editable'=>$editable,
'profil_data' => $profil_data,
);
$this->terminateWithSuccess($viewData); $this->terminateWithSuccess($viewData);
} }
/** /**
* update column foto_sperre in public.tbl_person * update column foto_sperre in public.tbl_person
* @access public * @access public
* @param boolean $value new value for the column * @param boolean $value new value for the column
@@ -77,9 +87,9 @@ class Profil extends FHCAPI_Controller
*/ */
public function fotoSperre($value) public function fotoSperre($value)
{ {
if(!isset($value)){ if (!isset($value)) {
$this->terminateWithError("Missing parameter", self::ERROR_TYPE_GENERAL); $this->terminateWithError("Missing parameter", self::ERROR_TYPE_GENERAL);
} }
$res = $this->PersonModel->update($this->pid, ["foto_sperre" => $value]); $res = $this->PersonModel->update($this->pid, ["foto_sperre" => $value]);
if (isError($res)) { if (isError($res)) {
@@ -87,10 +97,10 @@ class Profil extends FHCAPI_Controller
} }
$this->PersonModel->addSelect("foto_sperre"); $this->PersonModel->addSelect("foto_sperre");
$res = $this->PersonModel->load($this->pid); $res = $this->PersonModel->load($this->pid);
$res = $this->getDataOrTerminateWithError($res); $res = $this->getDataOrTerminateWithError($res);
$this->terminateWithSuccess(current($res)); $this->terminateWithSuccess(current($res));
} }
/** /**
@@ -109,7 +119,7 @@ class Profil extends FHCAPI_Controller
if (isError($nation_res)) { if (isError($nation_res)) {
$this->terminateWithError("error while trying to query table codex.tbl_nation", self::ERROR_TYPE_GENERAL); $this->terminateWithError("error while trying to query table codex.tbl_nation", self::ERROR_TYPE_GENERAL);
} }
$nation_res = $this->getDataOrTerminateWithError($nation_res); $nation_res = $this->getDataOrTerminateWithError($nation_res);
$this->terminateWithSuccess($nation_res); $this->terminateWithSuccess($nation_res);
@@ -117,30 +127,30 @@ class Profil extends FHCAPI_Controller
public function getGemeinden($nation, $zip) public function getGemeinden($nation, $zip)
{ {
if(!isset($nation) || !isset($zip)){ if (!isset($nation) || !isset($zip)) {
echo json_encode(error("Missing parameters")); echo json_encode(error("Missing parameters"));
return; return;
} }
$this->load->model('codex/Gemeinde_model', "GemeindeModel"); $this->load->model('codex/Gemeinde_model', "GemeindeModel");
$gemeinde_res = $this->GemeindeModel->getGemeindeByPlz($zip); $gemeinde_res = $this->GemeindeModel->getGemeindeByPlz($zip);
if (isError($gemeinde_res)) { 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 = $this->getDataOrTerminateWithError($gemeinde_res);
/* $gemeinde_res = array_map(function ($obj) { /* $gemeinde_res = array_map(function ($obj) {
return $obj->ortschaftsname; return $obj->ortschaftsname;
}, $gemeinde_res); */ }, $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) * checks whether a specific userID is a mitarbeiter or not (foreword declaration of the function isMitarbeiter in Mitarbeiter_model.php)
* @access public * @access public
@@ -150,23 +160,48 @@ class Profil extends FHCAPI_Controller
public function isMitarbeiter($uid) 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); $result = $this->MitarbeiterModel->isMitarbeiter($uid);
if (isError($result)) { if (isError($result)) {
$this->terminateWithError("error when calling Mitarbeiter_model function isMitarbeiter with uid " . $uid, self::ERROR_TYPE_GENERAL); $this->terminateWithError("error when calling Mitarbeiter_model function isMitarbeiter with uid " . $uid, self::ERROR_TYPE_GENERAL);
} }
$result = $this->getDataOrTerminateWithError($result); $result = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($result); $this->terminateWithSuccess($result);
} }
// ----------------------------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------------------------
// Private methods // 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),
],
];
}
} }
@@ -0,0 +1,64 @@
<?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,21 +62,36 @@ class Studium extends FHCAPI_Controller
if($this->getDataOrTerminateWithError($this->StudentModel->isStudent(getAuthUID()))){ if($this->getDataOrTerminateWithError($this->StudentModel->isStudent(getAuthUID()))){
$studentLehrverband =$this->StudentlehrverbandModel->loadWhere(["student_uid" => getAuthUID(), "studiensemester_kurzbz" => $aktuelles_studiensemester->studiensemester_kurzbz]); $studentLehrverband =$this->StudentlehrverbandModel->loadWhere(["student_uid" => getAuthUID(), "studiensemester_kurzbz" => $aktuelles_studiensemester->studiensemester_kurzbz]);
$studentLehrverband = current($this->getDataOrTerminateWithError($studentLehrverband));
//TODO(Manu) check if use Fallback or just comment out all paramschecks?
$student_studiensemester = $studentLehrverband->studiensemester_kurzbz; //add Fallback: if no LehrverbandData of actual semester, get Data of previous one
$student_studiengang = $studentLehrverband->studiengang_kz; if(!hasData($studentLehrverband))
$student_semester = $studentLehrverband->semester; {
$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));
$student_studienplan = $this->getStudienPlanFromPrestudentStatus(getAuthPersonId())->studienplan_id; $student_studienplan = $this->getStudienPlanFromPrestudentStatus(getAuthPersonId())->studienplan_id;
if(!isset($parameter_studiensemester)) if(!isset($parameter_studiensemester)) {
$parameter_studiensemester = $student_studiensemester; $student_studiensemester = $studentLehrverband->studiensemester_kurzbz;
if(!isset($parameter_studiengang)) $parameter_studiensemester = $student_studiensemester;
$parameter_studiengang = $student_studiengang; }
if(!isset($parameter_semester)) if(!isset($parameter_studiengang)) {
$parameter_semester = $student_semester; $student_studiengang = $studentLehrverband->studiengang_kz;
$parameter_studiengang = $student_studiengang;
}
if(!isset($parameter_semester)) {
$student_semester = $studentLehrverband->semester;
$parameter_semester = $student_semester;
}
if(!isset($parameter_studienplan)) if(!isset($parameter_studienplan))
$parameter_studienplan = $student_studienplan; $parameter_studienplan = $student_studienplan;
} }
if(isset($parameter_studiensemester)){ if(isset($parameter_studiensemester)){
@@ -96,8 +111,7 @@ class Studium extends FHCAPI_Controller
// fetch studiensemester // fetch studiensemester
$allStudienSemester = $this->getDataOrTerminateWithError($this->StudiensemesterModel->load()); $allStudienSemester = $this->getDataOrTerminateWithError($this->StudiensemesterModel->load());
if(isset($parameter_studiensemester) && !empty(array_filter($allStudienSemester, function($studiensemester) use($parameter_studiensemester){ if(isset($parameter_studiensemester) && !empty(array_filter($allStudienSemester, function($studiensemester) use($parameter_studiensemester){
return $studiensemester->studiensemester_kurzbz == $parameter_studiensemester->studiensemester_kurzbz; return $studiensemester->studiensemester_kurzbz == $parameter_studiensemester->studiensemester_kurzbz;
}))){ }))){
@@ -216,6 +230,8 @@ class Studium extends FHCAPI_Controller
$studienplaene = array_map(function($studienplan){ $studienplaene = array_map(function($studienplan){
$orgform = current($this->getDataOrTerminateWithError($this->OrgformModel->loadWhere(["orgform_kurzbz" => $studienplan->orgform_kurzbz]))); $orgform = current($this->getDataOrTerminateWithError($this->OrgformModel->loadWhere(["orgform_kurzbz" => $studienplan->orgform_kurzbz])));
$studienplan->orgform_bezeichnung = $orgform->bezeichnung; $studienplan->orgform_bezeichnung = $orgform->bezeichnung;
// bezeichnung_mehrsprachig
$studienplan->orgform_bezeichnung_english = $orgform->bezeichnung_mehrsprachig[1];
return $studienplan; return $studienplan;
},$studienplaene); },$studienplaene);
return $studienplaene; return $studienplaene;
@@ -40,11 +40,32 @@ class Board extends FHCAPI_Controller
public function list() 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(); $result = $this->DashboardModel->load();
$data = $this->getDataOrTerminateWithError($result); $data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($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);
} }
public function create() public function create()
@@ -82,7 +103,7 @@ class Board extends FHCAPI_Controller
$data = $this->getDataOrTerminateWithError($result); $data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($result); $this->terminateWithSuccess($data);
} }
public function delete() public function delete()
@@ -116,6 +137,6 @@ class Board extends FHCAPI_Controller
$data = $this->getDataOrTerminateWithError($result); $data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($result); $this->terminateWithSuccess($data);
} }
} }
@@ -120,10 +120,7 @@ class Preset extends FHCAPI_Controller
$conf = $this->dashboardlib->getPreset($db, $funktion); $conf = $this->dashboardlib->getPreset($db, $funktion);
if ($conf) { if ($conf) {
$preset = json_decode($conf->preset, true); $preset = json_decode($conf->preset, true);
if (!isset($preset[$funktion]) || !isset($preset[$funktion]['widgets'])) $result[$funktion] = $preset;
$result[$funktion] = [];
else
$result[$funktion] = $preset[$funktion]['widgets'];
} else { } else {
$result[$funktion] = []; $result[$funktion] = [];
} }
@@ -154,7 +151,7 @@ class Preset extends FHCAPI_Controller
$preset_decoded = json_decode($preset->preset, true); $preset_decoded = json_decode($preset->preset, true);
$this->dashboardlib->addWidgetsToWidgets($preset_decoded, $dashboard_kurzbz, $funktion_kurzbz, [$widget]); $preset_decoded[$widget['widgetid']] = $widget;
$preset->preset = json_encode($preset_decoded); $preset->preset = json_encode($preset_decoded);
@@ -186,8 +183,10 @@ class Preset extends FHCAPI_Controller
$preset_decoded = json_decode($preset->preset, true); $preset_decoded = json_decode($preset->preset, true);
if (!$this->dashboardlib->removeWidgetFromWidgets($preset_decoded, $funktion_kurzbz, $widgetid)) if (!isset($preset_decoded[$widgetid]))
show_404(); show_404();
unset($preset_decoded[$widgetid]);
$preset->preset = json_encode($preset_decoded); $preset->preset = json_encode($preset_decoded);
@@ -48,25 +48,9 @@ class User extends FHCAPI_Controller
$uid = $this->authlib->getAuthObj()->username; $uid = $this->authlib->getAuthObj()->username;
/*$mergedconfig = $this->dashboardlib->getMergedConfig($dashboard->dashboard_id, $uid); $mergedconfig = $this->dashboardlib->getMergedUserConfig($dashboard->dashboard_id, $uid);
$this->terminateWithSuccess([ $this->terminateWithSuccess($mergedconfig);
'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() public function addWidget()
@@ -86,26 +70,15 @@ class User extends FHCAPI_Controller
if (!isset($widget['widgetid'])) if (!isset($widget['widgetid']))
$widget['widgetid'] = $this->dashboardlib->generateWidgetId($dashboard_kurzbz); $widget['widgetid'] = $this->dashboardlib->generateWidgetId($dashboard_kurzbz);
if (isset($widget['source']))
unset($widget['source']);
$override = $this->dashboardlib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid); $override = $this->dashboardlib->getOverrideOrCreateEmptyOverride($dashboard_kurzbz, $uid);
$override_decoded = json_decode($override->override, true); $override_decoded = json_decode($override->override, true);
if (!isset($override_decoded['general']) || !is_array($override_decoded['general'])) $override_decoded[$widget['widgetid']] = $widget;
$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); $override->override = json_encode($override_decoded);
$result = $this->dashboardlib->insertOrUpdateOverride($override); $result = $this->dashboardlib->insertOrUpdateOverride($override);
@@ -135,18 +108,10 @@ class User extends FHCAPI_Controller
$override_decoded = json_decode($override->override, true); $override_decoded = json_decode($override->override, true);
foreach (array_keys($override_decoded) as $k) { if (!isset($override_decoded[$widget_id]))
if (!isset($override_decoded[$k]["widgets"])) { show_404();
unset($override_decoded[$k]);
continue; unset($override_decoded[$widget_id]);
}
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); $override->override = json_encode($override_decoded);
+517
View File
@@ -0,0 +1,517 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/*
|--------------------------------------------------------------------------
| Base Site URL
|--------------------------------------------------------------------------
|
| URL to your CodeIgniter root. Typically this will be your base URL,
| WITH a trailing slash:
|
| http://example.com/
|
| If this is not set then CodeIgniter will try guess the protocol, domain
| and path to your installation. However, you should always configure this
| explicitly and never rely on auto-guessing, especially in production
| environments.
|
*/
$config['base_url'] = 'https://c3p0.dev.technikum-wien.at/ma1434/core/FHC-Core/';
/*
|--------------------------------------------------------------------------
| Index File
|--------------------------------------------------------------------------
|
| Typically this will be your index.php file, unless you've renamed it to
| something else. If you are using mod_rewrite to remove the page set this
| variable so that it is blank.
|
*/
$config['index_page'] = 'index.ci.php';
/*
|--------------------------------------------------------------------------
| URI PROTOCOL
|--------------------------------------------------------------------------
|
| This item determines which server global should be used to retrieve the
| URI string. The default setting of 'REQUEST_URI' works for most servers.
| If your links do not seem to work, try one of the other delicious flavors:
|
| 'REQUEST_URI' Uses $_SERVER['REQUEST_URI']
| 'QUERY_STRING' Uses $_SERVER['QUERY_STRING']
| 'PATH_INFO' Uses $_SERVER['PATH_INFO']
|
| WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded!
*/
$config['uri_protocol'] = 'REQUEST_URI';
/*
|--------------------------------------------------------------------------
| URL suffix
|--------------------------------------------------------------------------
|
| This option allows you to add a suffix to all URLs generated by CodeIgniter.
| For more information please see the user guide:
|
| http://codeigniter.com/user_guide/general/urls.html
*/
$config['url_suffix'] = '';
/*
|--------------------------------------------------------------------------
| Default Language
|--------------------------------------------------------------------------
|
| This determines which set of language files should be used. Make sure
| there is an available translation if you intend to use something other
| than english.
|
*/
$config['language'] = '';
/*
|--------------------------------------------------------------------------
| Default Character Set
|--------------------------------------------------------------------------
|
| This determines which character set is used by default in various methods
| that require a character set to be provided.
|
| See http://php.net/htmlspecialchars for a list of supported charsets.
|
*/
$config['charset'] = 'UTF-8';
/*
|--------------------------------------------------------------------------
| Enable/Disable System Hooks
|--------------------------------------------------------------------------
|
| If you would like to use the 'hooks' feature you must enable it by
| setting this variable to TRUE (boolean). See the user guide for details.
|
*/
$config['enable_hooks'] = FALSE;
/*
|--------------------------------------------------------------------------
| Class Extension Prefix
|--------------------------------------------------------------------------
|
| This item allows you to set the filename/classname prefix when extending
| native libraries. For more information please see the user guide:
|
| http://codeigniter.com/user_guide/general/core_classes.html
| http://codeigniter.com/user_guide/general/creating_libraries.html
|
*/
$config['subclass_prefix'] = 'FHC_';
/*
|--------------------------------------------------------------------------
| Composer auto-loading
|--------------------------------------------------------------------------
|
| Enabling this setting will tell CodeIgniter to look for a Composer
| package auto-loader script in application/vendor/autoload.php.
|
| $config['composer_autoload'] = TRUE;
|
| Or if you have your vendor/ directory located somewhere else, you
| can opt to set a specific path as well:
|
| $config['composer_autoload'] = '/path/to/vendor/autoload.php';
|
| For more information about Composer, please visit http://getcomposer.org/
|
| Note: This will NOT disable or override the CodeIgniter-specific
| autoloading (application/config/autoload.php)
*/
$config['composer_autoload'] = FALSE;
/*
|--------------------------------------------------------------------------
| Allowed URL Characters
|--------------------------------------------------------------------------
|
| This lets you specify which characters are permitted within your URLs.
| When someone tries to submit a URL with disallowed characters they will
| get a warning message.
|
| As a security measure you are STRONGLY encouraged to restrict URLs to
| as few characters as possible. By default only these are allowed: a-z 0-9~%.:_-
|
| Leave blank to allow all characters -- but only if you are insane.
|
| The configured value is actually a regular expression character group
| and it will be executed as: ! preg_match('/^[<permitted_uri_chars>]+$/i
|
| DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
|
*/
$config['permitted_uri_chars'] = 'a-z 0-9~%.:_\-';
/*
|--------------------------------------------------------------------------
| Enable Query Strings
|--------------------------------------------------------------------------
|
| By default CodeIgniter uses search-engine friendly segment based URLs:
| example.com/who/what/where/
|
| By default CodeIgniter enables access to the $_GET array. If for some
| reason you would like to disable it, set 'allow_get_array' to FALSE.
|
| You can optionally enable standard query string based URLs:
| example.com?who=me&what=something&where=here
|
| Options are: TRUE or FALSE (boolean)
|
| The other items let you set the query string 'words' that will
| invoke your controllers and its functions:
| example.com/index.php?c=controller&m=function
|
| Please note that some of the helpers won't work as expected when
| this feature is enabled, since CodeIgniter is designed primarily to
| use segment based URLs.
|
*/
$config['allow_get_array'] = TRUE;
$config['enable_query_strings'] = FALSE;
$config['controller_trigger'] = 'c';
$config['function_trigger'] = 'm';
$config['directory_trigger'] = 'd';
/*
|--------------------------------------------------------------------------
| Error Logging Threshold
|--------------------------------------------------------------------------
|
| You can enable error logging by setting a threshold over zero. The
| threshold determines what gets logged. Threshold options are:
|
| 0 = Disables logging, Error logging TURNED OFF
| 1 = Error Messages (including PHP errors)
| 2 = Debug Messages
| 3 = Informational Messages
| 4 = All Messages
|
| You can also pass an array with threshold levels to show individual error types
|
| array(2) = Debug Messages, without Error Messages
|
| For a live site you'll usually only enable Errors (1) to be logged otherwise
| your log files will fill up very fast.
|
*/
$config['log_threshold'] = 1;
/*
|--------------------------------------------------------------------------
| Error Logging Directory Path
|--------------------------------------------------------------------------
|
| Leave this BLANK unless you would like to set something other than the default
| application/logs/ directory. Use a full server path with trailing slash.
|
*/
$config['log_path'] = '';
/*
|--------------------------------------------------------------------------
| Log File Extension
|--------------------------------------------------------------------------
|
| The default filename extension for log files. The default 'php' allows for
| protecting the log files via basic scripting, when they are to be stored
| under a publicly accessible directory.
|
| Note: Leaving it blank will default to 'php'.
|
*/
$config['log_file_extension'] = 'log';
/*
|--------------------------------------------------------------------------
| Log File Permissions
|--------------------------------------------------------------------------
|
| The file system permissions to be applied on newly created log files.
|
| IMPORTANT: This MUST be an integer (no quotes) and you MUST use octal
| integer notation (i.e. 0700, 0644, etc.)
*/
$config['log_file_permissions'] = 0644;
/*
|--------------------------------------------------------------------------
| Date Format for Logs
|--------------------------------------------------------------------------
|
| Each item that is logged has an associated date. You can use PHP date
| codes to set your own date formatting
|
*/
$config['log_date_format'] = 'Y-m-d H:i:s';
/*
|--------------------------------------------------------------------------
| Error Views Directory Path
|--------------------------------------------------------------------------
|
| Leave this BLANK unless you would like to set something other than the default
| application/views/errors/ directory. Use a full server path with trailing slash.
|
*/
$config['error_views_path'] = '';
/*
|--------------------------------------------------------------------------
| Cache Directory Path
|--------------------------------------------------------------------------
|
| Leave this BLANK unless you would like to set something other than the default
| application/cache/ directory. Use a full server path with trailing slash.
|
*/
$config['cache_path'] = '';
/*
|--------------------------------------------------------------------------
| Cache Include Query String
|--------------------------------------------------------------------------
|
| Whether to take the URL query string into consideration when generating
| output cache files. Valid options are:
|
| FALSE = Disabled
| TRUE = Enabled, take all query parameters into account.
| Please be aware that this may result in numerous cache
| files generated for the same page over and over again.
| array('q') = Enabled, but only take into account the specified list
| of query parameters.
|
*/
$config['cache_query_string'] = FALSE;
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| If you use the Encryption class, you must set an encryption key.
| See the user guide for more info.
|
| http://codeigniter.com/user_guide/libraries/encryption.html
|
*/
$config['encryption_key'] = '';
/*
|--------------------------------------------------------------------------
| Session Variables
|--------------------------------------------------------------------------
|
| 'sess_driver'
|
| The storage driver to use: files, database, redis, memcached
|
| 'sess_cookie_name'
|
| The session cookie name, must contain only [0-9a-z_-] characters
|
| 'sess_expiration'
|
| The number of SECONDS you want the session to last.
| Setting to 0 (zero) means expire when the browser is closed.
|
| 'sess_save_path'
|
| The location to save sessions to, driver dependent.
|
| For the 'files' driver, it's a path to a writable directory.
| WARNING: Only absolute paths are supported!
|
| For the 'database' driver, it's a table name.
| Please read up the manual for the format with other session drivers.
|
| IMPORTANT: You are REQUIRED to set a valid save path!
|
| 'sess_match_ip'
|
| Whether to match the user's IP address when reading the session data.
|
| 'sess_time_to_update'
|
| How many seconds between CI regenerating the session ID.
| NOTE: Keep it as it is to prevent security issues (https://en.wikipedia.org/wiki/Session_fixation)
|
| 'sess_regenerate_destroy'
|
| Whether to destroy session data associated with the old session ID
| when auto-regenerating the session ID. When set to FALSE, the data
| will be later deleted by the garbage collector.
|
| Other session cookie settings are shared with the rest of the application,
| except for 'cookie_prefix' and 'cookie_httponly', which are ignored here.
|
*/
$config['sess_driver'] = 'files';
$config['sess_cookie_name'] = 'sess_ci_session';
$config['sess_expiration'] = 1800; // Session expires every 30 minutes
$config['sess_save_path'] = NULL;
$config['sess_match_ip'] = FALSE;
$config['sess_time_to_update'] = 300;
$config['sess_regenerate_destroy'] = FALSE;
/*
|--------------------------------------------------------------------------
| Cookie Related Variables
|--------------------------------------------------------------------------
|
| 'cookie_prefix' = Set a cookie name prefix if you need to avoid collisions
| 'cookie_domain' = Set to .your-domain.com for site-wide cookies
| 'cookie_path' = Typically will be a forward slash
| 'cookie_secure' = Cookie will only be set if a secure HTTPS connection exists.
| 'cookie_httponly' = Cookie will only be accessible via HTTP(S) (no javascript)
|
| Note: These settings (with the exception of 'cookie_prefix' and
| 'cookie_httponly') will also affect sessions.
|
*/
$config['cookie_prefix'] = '';
$config['cookie_domain'] = '';
$config['cookie_path'] = '/';
$config['cookie_secure'] = FALSE;
$config['cookie_httponly'] = FALSE;
/*
|--------------------------------------------------------------------------
| Standardize newlines
|--------------------------------------------------------------------------
|
| Determines whether to standardize newline characters in input data,
| meaning to replace \r\n, \r, \n occurrences with the PHP_EOL value.
|
| This is particularly useful for portability between UNIX-based OSes,
| (usually \n) and Windows (\r\n).
|
*/
$config['standardize_newlines'] = FALSE;
/*
|--------------------------------------------------------------------------
| Global XSS Filtering
|--------------------------------------------------------------------------
|
| Determines whether the XSS filter is always active when GET, POST or
| COOKIE data is encountered
|
| WARNING: This feature is DEPRECATED and currently available only
| for backwards compatibility purposes!
|
*/
$config['global_xss_filtering'] = FALSE;
/*
|--------------------------------------------------------------------------
| Cross Site Request Forgery
|--------------------------------------------------------------------------
| Enables a CSRF cookie token to be set. When set to TRUE, token will be
| checked on a submitted form. If you are accepting user data, it is strongly
| recommended CSRF protection be enabled.
|
| 'csrf_token_name' = The token name
| 'csrf_cookie_name' = The cookie name
| 'csrf_expire' = The number in seconds the token should expire.
| 'csrf_regenerate' = Regenerate token on every submission
| 'csrf_exclude_uris' = Array of URIs which ignore CSRF checks
*/
$config['csrf_protection'] = FALSE;
$config['csrf_token_name'] = 'csrf_test_name';
$config['csrf_cookie_name'] = 'csrf_cookie_name';
$config['csrf_expire'] = 7200;
$config['csrf_regenerate'] = TRUE;
$config['csrf_exclude_uris'] = array();
/*
|--------------------------------------------------------------------------
| Output Compression
|--------------------------------------------------------------------------
|
| Enables Gzip output compression for faster page loads. When enabled,
| the output class will test whether your server supports Gzip.
| Even if it does, however, not all browsers support compression
| so enable only if you are reasonably sure your visitors can handle it.
|
| Only used if zlib.output_compression is turned off in your php.ini.
| Please do not use it together with httpd-level output compression.
|
| VERY IMPORTANT: If you are getting a blank page when compression is enabled it
| means you are prematurely outputting something to your browser. It could
| even be a line of whitespace at the end of one of your scripts. For
| compression to work, nothing can be sent before the output buffer is called
| by the output class. Do not 'echo' any values with compression enabled.
|
*/
$config['compress_output'] = FALSE;
/*
|--------------------------------------------------------------------------
| Master Time Reference
|--------------------------------------------------------------------------
|
| Options are 'local' or any PHP supported timezone. This preference tells
| the system whether to use your server's local time as the master 'now'
| reference, or convert it to the configured one timezone. See the 'date
| helper' page of the user guide for information regarding date handling.
|
*/
$config['time_reference'] = 'local';
/*
|--------------------------------------------------------------------------
| Rewrite PHP Short Tags
|--------------------------------------------------------------------------
|
| If your PHP installation does not have short tag support enabled CI
| can rewrite the tags on-the-fly, enabling you to utilize that syntax
| in your view files. Options are TRUE or FALSE (boolean)
|
| Note: You need to have eval() enabled for this to work.
|
*/
$config['rewrite_short_tags'] = FALSE;
/*
|--------------------------------------------------------------------------
| Reverse Proxy IPs
|--------------------------------------------------------------------------
|
| If your server is behind a reverse proxy, you must whitelist the proxy
| IP addresses from which CodeIgniter should trust headers such as
| HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify
| the visitor's IP address.
|
| You can use both an array or a comma-separated list of proxy addresses,
| as well as specifying whole subnets. Here are a few examples:
|
| Comma-separated: '10.0.1.200,192.168.5.0/24'
| Array: array('10.0.1.200', '192.168.5.0/24')
*/
$config['proxy_ips'] = '';
/*
|--------------------------------------------------------------------------
| FHComplete Build Version
|--------------------------------------------------------------------------
|
| Version Number of the Current Build
| This is used to invalidate Cache for JS and CSS Files
|
| Example: 2019102901
*/
$config['fhcomplete_build_version'] = '2019102903';
+122
View File
@@ -0,0 +1,122 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
defined('DB_HOST') OR require_once './config/system.config.inc.php'; // For CLI-Migrations
/*
| -------------------------------------------------------------------
| DATABASE CONNECTIVITY SETTINGS
| -------------------------------------------------------------------
| This file will contain the settings needed to access your database.
|
| For complete instructions please consult the 'Database Connection'
| page of the User Guide.
|
| -------------------------------------------------------------------
| EXPLANATION OF VARIABLES
| -------------------------------------------------------------------
|
| ['dsn'] The full DSN string describe a connection to the database.
| ['hostname'] The hostname of your database server.
| ['username'] The username used to connect to the database
| ['password'] The password used to connect to the database
| ['database'] The name of the database you want to connect to
| ['dbdriver'] The database driver. e.g.: mysqli.
| Currently supported:
| cubrid, ibase, mssql, mysql, mysqli, oci8,
| odbc, pdo, postgre, sqlite, sqlite3, sqlsrv
| ['dbprefix'] You can add an optional prefix, which will be added
| to the table name when using the Query Builder class
| ['pconnect'] TRUE/FALSE - Whether to use a persistent connection
| ['db_debug'] TRUE/FALSE - Whether database errors should be displayed.
| ['cache_on'] TRUE/FALSE - Enables/disables query caching
| ['cachedir'] The path to the folder where cache files should be stored
| ['char_set'] The character set used in communicating with the database
| ['dbcollat'] The character collation used in communicating with the database
| NOTE: For MySQL and MySQLi databases, this setting is only used
| as a backup if your server is running PHP < 5.2.3 or MySQL < 5.0.7
| (and in table creation queries made with DB Forge).
| There is an incompatibility in PHP with mysql_real_escape_string() which
| can make your site vulnerable to SQL injection if you are using a
| multi-byte character set and are running versions lower than these.
| Sites using Latin-1 or UTF-8 database character set and collation are unaffected.
| ['swap_pre'] A default table prefix that should be swapped with the dbprefix
| ['encrypt'] Whether or not to use an encrypted connection.
|
| 'mysql' (deprecated), 'sqlsrv' and 'pdo/sqlsrv' drivers accept TRUE/FALSE
| 'mysqli' and 'pdo/mysql' drivers accept an array with the following options:
|
| 'ssl_key' - Path to the private key file
| 'ssl_cert' - Path to the public key certificate file
| 'ssl_ca' - Path to the certificate authority file
| 'ssl_capath' - Path to a directory containing trusted CA certificats in PEM format
| 'ssl_cipher' - List of *allowed* ciphers to be used for the encryption, separated by colons (':')
| 'ssl_verify' - TRUE/FALSE; Whether verify the server certificate or not ('mysqli' only)
|
| ['compress'] Whether or not to use client compression (MySQL only)
| ['stricton'] TRUE/FALSE - forces 'Strict Mode' connections
| - good for ensuring strict SQL while developing
| ['ssl_options'] Used to set various SSL options that can be used when making SSL connections.
| ['failover'] array - A array with 0 or more data for connections if the main should fail.
| ['save_queries'] TRUE/FALSE - Whether to "save" all executed queries.
| NOTE: Disabling this will also effectively disable both
| $this->db->last_query() and profiling of DB queries.
| When you run a query, with this setting set to TRUE (default),
| CodeIgniter will store the SQL statement for debugging purposes.
| However, this may cause high memory usage, especially if you run
| a lot of SQL queries ... disable this to avoid that problem.
|
| The $active_group variable lets you choose which connection group to
| make active. By default there is only one group (the 'default' group).
|
| The $query_builder variables lets you determine whether or not to load
| the query builder class.
*/
$active_group = 'default';
$query_builder = TRUE;
$db['default'] = array(
'dsn' => '',
'hostname' => DB_HOST,
'username' => DB_USER,
'password' => DB_PASSWORD,
'port' => DB_PORT,
'database' => DB_NAME,
'dbdriver' => 'postgre',
'dbprefix' => '',
'pconnect' => DB_CONNECT_PERSISTENT,
'db_debug' => (ENVIRONMENT !== 'production'),
'cache_on' => FALSE,
'cachedir' => '',
'char_set' => 'utf8',
'dbcollat' => 'utf8_general_ci',
'swap_pre' => '',
'encrypt' => FALSE,
'compress' => FALSE,
'stricton' => FALSE,
'failover' => array(),
'save_queries' => TRUE
);
$db['system'] = array(
'dsn' => '',
'hostname' => DB_HOST,
'username' => 'fhcomplete',
'password' => 'Fhcomplet3Onc4p1',
'database' => DB_NAME,
'port' => DB_PORT,
'dbschema' => 'public',
'dbdriver' => 'postgre',
'dbprefix' => '',
'pconnect' => DB_CONNECT_PERSISTENT,
'db_debug' => (ENVIRONMENT !== 'production'),
'cache_on' => FALSE,
'cachedir' => '',
'char_set' => 'utf8',
'dbcollat' => 'utf8_general_ci',
'swap_pre' => '',
'encrypt' => FALSE,
'compress' => FALSE,
'stricton' => FALSE,
'failover' => array(),
'save_queries' => TRUE
);
+27 -6
View File
@@ -40,13 +40,16 @@ class StundenplanLib
* @return stdClass * @return stdClass
* @access public * @access public
*/ */
public function getEventsUser($start, $end) public function getEventsUser($start, $end, $uid = null)
{ {
$this->_ci =& get_instance(); $this->_ci =& get_instance();
$this->_ci->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel'); $this->_ci->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
$uid = getAuthUID(); if (!$uid) {
$uid = getAuthUID();
}
if (is_null($uid)) if (is_null($uid))
return error("No UID"); return error("No UID");
@@ -217,7 +220,7 @@ class StundenplanLib
* @param string $ort_kurzbz * @param string $ort_kurzbz
* @return stdClass * @return stdClass
*/ */
public function getReservierungen($start_date, $end_date, $ort_kurzbz = '') public function getReservierungen($start_date, $end_date, $ort_kurzbz = '', $uid = null)
{ {
$this->_ci =& get_instance(); $this->_ci =& get_instance();
@@ -228,14 +231,14 @@ class StundenplanLib
$this->_ci->load->model('ressource/Reservierung_model', 'ReservierungModel'); $this->_ci->load->model('ressource/Reservierung_model', 'ReservierungModel');
$this->_ci->load->model('ressource/Stundenplan_model', 'StundenplanModel'); $this->_ci->load->model('ressource/Stundenplan_model', 'StundenplanModel');
$is_mitarbeiter = getData($this->_ci->MitarbeiterModel->isMitarbeiter(getAuthUID())); $is_mitarbeiter = getData($this->_ci->MitarbeiterModel->isMitarbeiter($uid ?? getAuthUID()));
if ($is_mitarbeiter && empty($ort_kurzbz)) { if ($is_mitarbeiter && empty($ort_kurzbz)) {
// request for personal lvplan show only reservations of logged in user // request for personal lvplan show only reservations of logged in user
$reservierungen = $this->_ci->ReservierungModel->getReservierungenMitarbeiter($start_date, $end_date); $reservierungen = $this->_ci->ReservierungModel->getReservierungenMitarbeiter($start_date, $end_date, $uid);
} else { } else {
// querying the reservierungen // querying the reservierungen
$reservierungen = $this->_ci->ReservierungModel->getReservierungen($start_date, $end_date, $ort_kurzbz); $reservierungen = $this->_ci->ReservierungModel->getReservierungen($start_date, $end_date, $ort_kurzbz, $uid);
} }
if (isError($reservierungen)) if (isError($reservierungen))
@@ -445,6 +448,24 @@ class StundenplanLib
return success($ferienEventsFlattened); 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 ######################################################################################################## // start of the private functions ########################################################################################################
// function used to sort an array of studiensemester strings // function used to sort an array of studiensemester strings
@@ -37,7 +37,9 @@ class DashboardLib
public function getDashboardByKurzbz($dashboard_kurzbz) public function getDashboardByKurzbz($dashboard_kurzbz)
{ {
$result = $this->_ci->DashboardModel->getDashboardByKurzbz($dashboard_kurzbz); $result = $this->_ci->DashboardModel->loadWhere([
'dashboard_kurzbz' => $dashboard_kurzbz
]);
if (hasData($result)) if (hasData($result))
{ {
@@ -47,17 +49,21 @@ class DashboardLib
return null; return null;
} }
public function getMergedConfig($dashboard_id, $uid) public function getMergedUserConfig($dashboard_id, $uid)
{ {
$defaultconfig = $this->getDefaultConfig($dashboard_id); $defaultconfig = $this->getUserBaseConfig($dashboard_id);
$userconfig = $this->getUserConfig($dashboard_id, $uid); $userconfig = $this->getUserOverrideConfig($dashboard_id, $uid);
$mergedconfig = array_replace_recursive($defaultconfig, $userconfig); $sourceconfig = array_map(function ($value) {
return ['source' => $value['source']];
}, $defaultconfig);
$mergedconfig = array_replace_recursive($defaultconfig, $userconfig, $sourceconfig);
return $mergedconfig; return $mergedconfig;
} }
public function getDefaultConfig($dashboard_id) protected function getUserBaseConfig($dashboard_id)
{ {
$funktion_kurzbzs = []; $funktion_kurzbzs = [];
$rights = $this->_ci->permissionlib->getAccessRights(); $rights = $this->_ci->permissionlib->getAccessRights();
@@ -87,7 +93,11 @@ class DashboardLib
$preset = json_decode($presetobj->preset, true); $preset = json_decode($presetobj->preset, true);
if (null !== $preset) if (null !== $preset)
{ {
$defaultconfig = array_replace_recursive($defaultconfig, $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);
} }
} }
} }
@@ -95,7 +105,7 @@ class DashboardLib
return $defaultconfig; return $defaultconfig;
} }
public function getUserConfig($dashboard_id, $uid) protected function getUserOverrideConfig($dashboard_id, $uid)
{ {
$res_userconfig = $this->_ci->DashboardOverrideModel->getOverride($dashboard_id, $uid); $res_userconfig = $this->_ci->DashboardOverrideModel->getOverride($dashboard_id, $uid);
@@ -124,7 +134,7 @@ class DashboardLib
$emptyoverride = new stdClass(); $emptyoverride = new stdClass();
$emptyoverride->dashboard_id = $dashboard->dashboard_id; $emptyoverride->dashboard_id = $dashboard->dashboard_id;
$emptyoverride->uid = $uid; $emptyoverride->uid = $uid;
$emptyoverride->override = '{"' . self::USEROVERRIDE_SECTION . '": {"widgets":{}}, "custom": { "widgets" : {}}}'; $emptyoverride->override = '[]';
return $emptyoverride; return $emptyoverride;
} }
@@ -143,8 +153,7 @@ class DashboardLib
$emptypreset = new stdClass(); $emptypreset = new stdClass();
$emptypreset->dashboard_id = $dashboard->dashboard_id; $emptypreset->dashboard_id = $dashboard->dashboard_id;
$emptypreset->funktion_kurzbz = $funktion_kurzbz; $emptypreset->funktion_kurzbz = $funktion_kurzbz;
$section = ($funktion_kurzbz !== null) ? $funktion_kurzbz : self::SECTION_IF_FUNKTION_KURZBZ_IS_NULL; $emptypreset->preset = '[]';
$emptypreset->preset = '{"' . $section . '": { "widgets" : {}},"custom": { "widgets" : {}}}';
return $emptypreset; return $emptypreset;
} }
@@ -209,44 +218,4 @@ class DashboardLib
return $result; 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 FROM
campus.tbl_content c1 campus.tbl_content c1
LEFT JOIN LEFT JOIN
campus.tbl_contentsprache s1 ON c1.content_id=s1.content_id AND s1.sprache=? campus.tbl_contentsprache s1 ON c1.content_id=s1.content_id AND s1.sprache=? AND sichtbar=true
WHERE WHERE
sichtbar=true c1.aktiv = true
) s2 ) s2
LEFT JOIN LEFT JOIN
campus.tbl_contentsprache s3 USING(content_id, sprache) campus.tbl_contentsprache s3 USING(content_id, sprache)
@@ -277,7 +277,7 @@ class Content_model extends DB_Model
JOIN JOIN
campus.tbl_contentsprache s USING(contentsprache_id) campus.tbl_contentsprache s USING(contentsprache_id)
LEFT JOIN LEFT JOIN
campus.tbl_contentchild k ON(m.content_id=k.content_id) campus.tbl_contentchild k ON(m.content_id=k.content_id) and c.aktiv = true
WHERE EXISTS ( WHERE EXISTS (
SELECT 1 SELECT 1
FROM campus.tbl_contentgruppe FROM campus.tbl_contentgruppe
@@ -11,8 +11,4 @@ class Bookmark_model extends DB_Model
$this->dbTable = 'dashboard.tbl_bookmark'; $this->dbTable = 'dashboard.tbl_bookmark';
$this->pk = 'bookmark_id'; $this->pk = 'bookmark_id';
} }
} }
@@ -11,15 +11,4 @@ class Dashboard_model extends DB_Model
$this->dbTable = 'dashboard.tbl_dashboard'; $this->dbTable = 'dashboard.tbl_dashboard';
$this->pk = 'dashboard_id'; $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));
}
} }
@@ -18,10 +18,10 @@ class Reservierung_model extends DB_Model
* *
* @return stdClass * @return stdClass
*/ */
public function getReservierungen($start_date, $end_date, $ort_kurzbz = null) public function getReservierungen($start_date, $end_date, $ort_kurzbz = null, $uid = null)
{ {
$lvplan_reservierungen_query="SELECT r.* , stund.beginn, stund.ende, $lvplan_reservierungen_query = "SELECT r.* , stund.beginn, stund.ende,
CASE CASE
WHEN r.gruppe_kurzbz IS NOT NULL THEN r.gruppe_kurzbz 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),'/')) 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 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 WHERE datum >= ? AND datum <= ? AND (ss1.studiensemester_kurzbz IS NOT NULL
OR ss2.studiensemester_kurzbz IS NOT NULL)"; OR ss2.studiensemester_kurzbz IS NOT NULL)";
$raum_reservierungen_query = "SELECT res.*, beginn, ende, $raum_reservierungen_query = "SELECT res.*, beginn, ende,
CASE CASE
WHEN res.gruppe_kurzbz IS NOT NULL THEN res.gruppe_kurzbz 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 JOIN lehre.tbl_stunde ON lehre.tbl_stunde.stunde = res.stunde
WHERE res.ort_kurzbz = ? AND datum >= ? AND datum <= ?"; WHERE res.ort_kurzbz = ? AND datum >= ? AND datum <= ?";
$subquery = is_null($ort_kurzbz)? $lvplan_reservierungen_query:$raum_reservierungen_query; $subquery = is_null($ort_kurzbz) ? $lvplan_reservierungen_query : $raum_reservierungen_query;
$query_result= $this->execReadOnlyQuery(" $query_result = $this->execReadOnlyQuery("
SELECT SELECT
'reservierung' as type, beginn, ende, datum, 'reservierung' as type, beginn, ende, datum,
COALESCE(titel, beschreibung) as topic, COALESCE(titel, beschreibung) as topic,
@@ -59,15 +59,15 @@ class Reservierung_model extends DB_Model
FROM FROM
( (
". $subquery ." " . $subquery . "
) AS subquery ) AS subquery
GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung
ORDER BY datum, beginn ORDER BY datum, beginn
", is_null($ort_kurzbz) ?[getAuthUID(), getAuthUID(),$start_date,$end_date]: [$ort_kurzbz, $start_date, $end_date]); ", is_null($ort_kurzbz) ? [$uid ?? getAuthUID(), $uid ?? getAuthUID(), $start_date, $end_date] : [$ort_kurzbz, $start_date, $end_date]);
return $query_result; return $query_result;
} }
@@ -76,7 +76,7 @@ class Reservierung_model extends DB_Model
* *
* @return stdClass * @return stdClass
*/ */
public function getReservierungenMitarbeiter($start_date, $end_date) public function getReservierungenMitarbeiter($start_date, $end_date, $uid = null)
{ {
$raum_reservierungen_query = "SELECT res.*, beginn, ende, $raum_reservierungen_query = "SELECT res.*, beginn, ende,
@@ -91,8 +91,8 @@ class Reservierung_model extends DB_Model
$subquery = $raum_reservierungen_query; $subquery = $raum_reservierungen_query;
$query_result= $this->execReadOnlyQuery(" $query_result = $this->execReadOnlyQuery("
SELECT SELECT
'reservierung' as type, beginn, ende, datum, 'reservierung' as type, beginn, ende, datum,
COALESCE(titel, beschreibung) as topic, COALESCE(titel, beschreibung) as topic,
@@ -103,13 +103,13 @@ class Reservierung_model extends DB_Model
FROM FROM
( (
". $subquery ." " . $subquery . "
) AS subquery ) AS subquery
GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung GROUP BY datum, beginn, ende, ort_kurzbz, titel, beschreibung
ORDER BY datum, beginn ORDER BY datum, beginn
", [getAuthUID(), $start_date, $end_date]); ", [$uid ?? getAuthUID(), $start_date, $end_date]);
return $query_result; 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->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('ss1.studiensemester_kurzbz IS NOT NULL', null, false);
$this->db->or_where('ss2.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'); $query = $this->db->get_compiled_select('campus.vw_reservierung r');
return $this->execQuery($query, [$uid, $uid]); return $this->execQuery($query, [$uid, $uid]);
} }
@@ -388,6 +388,84 @@ class Stundenplan_model extends DB_Model
ORDER BY datum, beginn", [$start_date, $end_date, $ma_uid]); 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. * NO STANDALONE FUNCTION - Generates a SQL query string to fetch 'stundenplan' events for a specific student within the current semester.
@@ -23,12 +23,14 @@ $includesArray = array(
'public/css/components/FormUnderline.css', 'public/css/components/FormUnderline.css',
'public/css/components/abgabetool/abgabe.css', 'public/css/components/abgabetool/abgabe.css',
'public/css/Cis4/Cms.css', 'public/css/Cis4/Cms.css',
'public/css/Cis4/Studium.css', 'public/css/Cis4/Studium.css'
), ),
'customJSs' => array( 'customJSs' => array(
'vendor/npm-asset/primevue/accordion/accordion.min.js', 'vendor/npm-asset/primevue/accordion/accordion.min.js',
'vendor/npm-asset/primevue/accordiontab/accordiontab.min.js', 'vendor/npm-asset/primevue/accordiontab/accordiontab.min.js',
'vendor/npm-asset/primevue/checkbox/checkbox.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/inputnumber/inputnumber.min.js',
'vendor/npm-asset/primevue/speeddial/speeddial.min.js', 'vendor/npm-asset/primevue/speeddial/speeddial.min.js',
'vendor/npm-asset/primevue/textarea/textarea.min.js', 'vendor/npm-asset/primevue/textarea/textarea.min.js',
@@ -39,7 +41,7 @@ $includesArray = array(
'vendor/moment/luxonjs/luxon.min.js' 'vendor/moment/luxonjs/luxon.min.js'
), ),
'customJSModules' => array( 'customJSModules' => array(
'public/js/apps/Dashboard/Fhc.js', 'public/js/apps/Cis/Cis.js',
), ),
); );
@@ -47,8 +49,6 @@ $includesArray = array(
$this->load->view('templates/CISVUE-Header', $includesArray); $this->load->view('templates/CISVUE-Header', $includesArray);
?> ?>
<div id="fhccontent" class="h-100" route=<?php echo $route ?>> <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> </div>
<?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?> <?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?>
@@ -6,7 +6,7 @@ $includesArray = array(
'fontawesome6' => true, 'fontawesome6' => true,
'axios027' => true, 'axios027' => true,
'customJSModules' => array_merge([ 'customJSModules' => array_merge([
'public/js/apps/Cis.js' 'public/js/apps/Cis/Menu.js'
], $customJSModules ?? []), ], $customJSModules ?? []),
'customCSSs' => array_merge([ 'customCSSs' => array_merge([
'public/css/Cis4/Cis.css' 'public/css/Cis4/Cis.css'
@@ -8,7 +8,7 @@ $includesArray = array(
'axios027' => true, 'axios027' => true,
'primevue3' => true, 'primevue3' => true,
'customJSModules' => array_merge([ 'customJSModules' => array_merge([
'public/js/apps/Cis.js' 'public/js/apps/Cis/Menu.js'
], $customJSModules ?? []), ], $customJSModules ?? []),
'customCSSs' => array_merge([ 'customCSSs' => array_merge([
'public/css/Cis4/Cis.css', 'public/css/Cis4/Cis.css',
+14
View File
@@ -70,6 +70,18 @@
} }
} }
}, },
{
"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", "type": "package",
"package": { "package": {
@@ -452,6 +464,8 @@
"easyrdf/easyrdf": "0.9.*", "easyrdf/easyrdf": "0.9.*",
"drag-drop-touch-js/dragdroptouch": "*",
"fgelinas/timepicker": "0.3.3", "fgelinas/timepicker": "0.3.3",
"fortawesome/font-awesome4": "4.7.*", "fortawesome/font-awesome4": "4.7.*",
"fortawesome/font-awesome6": "6.1.*", "fortawesome/font-awesome6": "6.1.*",
Generated
+11 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f4f0af4586f46f97d8b6092c1ac0fb3a", "content-hash": "869cbc35bd1ba90ab90934fcb41b0f51",
"packages": [ "packages": [
{ {
"name": "afarkas/html5shiv", "name": "afarkas/html5shiv",
@@ -804,6 +804,16 @@
"abandoned": true, "abandoned": true,
"time": "2018-03-09T06:07:41+00:00" "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", "name": "easyrdf/easyrdf",
"version": "0.9.1", "version": "0.9.1",
+6
View File
@@ -0,0 +1,6 @@
{
"name": "FHC-Core",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+11 -1
View File
@@ -147,6 +147,8 @@ html {
--fhc-cis-menu-lvl-5-color-hover: var(--fhc-text); --fhc-cis-menu-lvl-5-color-hover: var(--fhc-text);
--fhc-cis-grade-positive: var(--fhc-success); --fhc-cis-grade-positive: var(--fhc-success);
--fhc-cis-grade-negative: var(--fhc-danger); --fhc-cis-grade-negative: var(--fhc-danger);
--fhc-offcanvas-zindex: 1045;
} }
#themeSwitch i{ #themeSwitch i{
@@ -375,7 +377,7 @@ html {
/* searchbar */ /* searchbar */
#nav-search { #nav-search {
background-color: var(--fhc-primary); background-color: var(--fhc-primary);
z-index: 1; z-index: calc(var(--fhc-offcanvas-zindex) + 1) !important;
} }
#nav-search.me-3 { #nav-search.me-3 {
margin: 0 !important; margin: 0 !important;
@@ -413,10 +415,18 @@ html {
color: var(--fhc-link) !important; color: var(--fhc-link) !important;
} }
#nav-main {
z-index: var(--fhc-offcanvas-zindex);
}
#nav-main-sticky { #nav-main-sticky {
max-height: calc(100vh - var(--fhc-cis-header-height)); max-height: calc(100vh - var(--fhc-cis-header-height));
} }
#nav-user-menu {
z-index: calc(var(--fhc-offcanvas-zindex) + 1);
}
#nav-user-menu img { #nav-user-menu img {
object-fit: cover; object-fit: cover;
height: calc( 3 * var(--fhc-cis-header-py)); height: calc( 3 * var(--fhc-cis-header-py));
+60 -52
View File
@@ -2,7 +2,7 @@
@import './dashboard/news.css'; @import './dashboard/news.css';
@import './dashboard/LvPlan.css'; @import './dashboard/LvPlan.css';
:root{ :root {
--fhc-dashboard-danger: var(--fhc-danger, #842029); --fhc-dashboard-danger: var(--fhc-danger, #842029);
--fhc-dashboard-grid-size: 4; --fhc-dashboard-grid-size: 4;
--fhc-dashboard-link: var(--fhc-link, #0a57ca); --fhc-dashboard-link: var(--fhc-link, #0a57ca);
@@ -17,22 +17,16 @@
--fhc-dashboard-section-info-color-hover: var(--fhc-primary-highlight, #005585); --fhc-dashboard-section-info-color-hover: var(--fhc-primary-highlight, #005585);
} }
@media(max-width: 577px) { .core-dashboard a {
:root {
--fhc-dashboard-grid-size: 1;
}
}
.core-dashboard a{
color: var(--fhc-dashboard-link); color: var(--fhc-dashboard-link);
} }
@media (max-width: 576px){ @media (max-width: 576px) {
.widget-icon { .widget-icon {
max-height: 250px; max-height: 250px;
object-fit: cover; object-fit: cover;
} }
.widget-icon-container{ .widget-icon-container {
max-width: 250px; max-width: 250px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@@ -46,27 +40,36 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
cursor:pointer; cursor: pointer;
} }
.dashboard-section > .newGridRow{ .dashboard-section.edit-active {
position:absolute; /**
width:20px; * replaces margin for extra row
height:20px; * 10% equals 0.1 of 100%
padding:0; * 1rem equals the padding of pb-3 that is overwritten here
bottom:0; */
left:50%; padding-bottom: calc(10% / var(--fhc-dashboard-grid-size) + 1rem) !important;
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); background-color: var(--fhc-dashboard-gridrow-background);
} }
.newGridRow:hover { .newGridRow:hover {
color:white; color: white;
background-color: var(--fhc-dashboard-gridrow-background-highlight); background-color: var(--fhc-dashboard-gridrow-background-highlight);
} }
.empty-tile-hover:hover { .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 { .alert-danger .form-check-input:checked {
@@ -74,16 +77,6 @@
background-color: var(--fhc-dashboard-danger); 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) { @media(max-width: 1200px) {
:root { :root {
--fhc-dashboard-grid-size: 3; --fhc-dashboard-grid-size: 3;
@@ -105,6 +98,7 @@
@media(max-width: 577px) { @media(max-width: 577px) {
:root { :root {
--fhc-dashboard-grid-size: 1; --fhc-dashboard-grid-size: 1;
--fhc-dg-item-py: .75rem;
} }
} }
@@ -132,50 +126,64 @@
cursor: move !important; cursor: move !important;
} }
.draggedItem { .drop-grid-item-resize > .dashboard-item,
.drop-grid-item-move > .dashboard-item {
height: 100%; height: 100%;
width: 100%; width: 100%;
background-color: var(--fhc-dashboard-draggeditem-background); background-color: var(--fhc-dashboard-draggeditem-background);
position: relative; position: relative;
} }
.dashboard-item-overlay{ .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 {
background-color: var(--fhc-dashboard-item-overlay-background); background-color: var(--fhc-dashboard-item-overlay-background);
} }
.dashboard-item-overlay::before{ .drop-grid-item-sizechanged > .dashboard-item::before,
position:absolute; .drop-grid-item-move > .dashboard-item::before {
content:""; position: absolute;
top:0.25rem; content: "";
left:0.25rem; top: .25rem;
right:0.25rem; left: .25rem;
bottom:0.25rem; right: .25rem;
border:4px dashed var(--fhc-dashboard-item-overly-border-color); bottom: .25rem;
opacity: 0.5; border: 4px dashed var(--fhc-dashboard-item-overly-border-color);
opacity: .5;
} }
#deleteBookmark i{ .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 {
color: var(--fhc-dashboard-danger); color: var(--fhc-dashboard-danger);
} }
.pin:hover{ .pin:hover {
cursor: pointer; cursor: pointer;
} }
.pin[pinned]:hover{ .pin[pinned]:hover {
color: var(--fhc-dashboard-pin-pinned-hover-color); color: var(--fhc-dashboard-pin-pinned-hover-color);
} }
.section-info{ .section-info {
color: var(--fhc-dashboard-section-info-color); color: var(--fhc-dashboard-section-info-color);
cursor:pointer; cursor: pointer;
} }
.section-info:hover { .section-info:hover {
color: var(--fhc-dashboard-section-info-color-hover); color: var(--fhc-dashboard-section-info-color-hover);
} }
.denied-dragging-animation { .drop-grid-item-blocker [pinned='true'] {
animation: wiggle 0.5s linear; animation: wiggle 0.5s linear;
color: var(--fhc-dashboard-denied-dragging-animation-color) !important; color: var(--fhc-dashboard-denied-dragging-animation-color) !important;
} }
@@ -204,13 +212,13 @@
} }
.hiddenWidget{ .hidden-widget {
background: var(--fhc-disabled-background); background: var(--fhc-disabled-background);
opacity: 40%; opacity: 40%;
} }
.hiddenWidget .card, .hidden-widget .card,
.hiddenWidget .card-body, .hidden-widget .card-body,
.hiddenWidget .card-body *{ .hidden-widget .card-body * {
background: inherit !important; background: inherit !important;
} }
+1 -1
View File
@@ -19,7 +19,7 @@ export default {
getViewData() { getViewData() {
return { return {
method: 'get', method: 'get',
url: '/api/frontend/v1/Cis4FhcApi/getViewData' url: '/api/frontend/v1/Cis4FhcApi/dashboardViewData'
}; };
} }
}; };
+26
View File
@@ -0,0 +1,26 @@
/**
* 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}
};
},
}
+48 -5
View File
@@ -16,6 +16,12 @@
*/ */
export default { export default {
getMyLvPlanViewData() {
return {
method: 'get',
url: `/api/frontend/v1/LvPlan/myLvPlanViewData`,
};
},
getRoomInfo(ort_kurzbz, start_date, end_date) { getRoomInfo(ort_kurzbz, start_date, end_date) {
return { return {
method: 'post', method: 'post',
@@ -30,11 +36,11 @@ export default {
params: { start_date, end_date, lv_id } params: { start_date, end_date, lv_id }
}; };
}, },
eventsPersonal(start_date, end_date) { eventsPersonal(start_date, end_date, uid = null) {
return { return {
method: 'post', method: 'post',
url: '/api/frontend/v1/lvPlan/eventsPersonal', url: '/api/frontend/v1/lvPlan/eventsPersonal',
params: { start_date, end_date } params: { start_date, end_date, uid }
}; };
}, },
eventsLv(lv_id, start_date, end_date) { eventsLv(lv_id, start_date, end_date) {
@@ -57,11 +63,11 @@ export default {
params: { start_date, end_date } params: { start_date, end_date }
}; };
}, },
getLvPlanReservierungen(start_date, end_date) { getLvPlanReservierungen(start_date, end_date, uid = null) {
return { return {
method: 'post', method: 'post',
url: '/api/frontend/v1/LvPlan/getReservierungen', url: '/api/frontend/v1/LvPlan/getReservierungen',
params: { start_date, end_date } params: { start_date, end_date, uid }
}; };
}, },
getLehreinheitStudiensemester(lehreinheit_id) { getLehreinheitStudiensemester(lehreinheit_id) {
@@ -92,5 +98,42 @@ export default {
method: 'get', method: 'get',
url: '/api/frontend/v1/LvPlan/getLv/' + lehrveranstaltung_id 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
@@ -0,0 +1,25 @@
/**
* 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 -1
View File
@@ -17,7 +17,7 @@
export default { export default {
profilViewData(uid) { getProfilViewData(uid = null) {
let url = "/api/frontend/v1/Profil/profilViewData"; let url = "/api/frontend/v1/Profil/profilViewData";
if(uid){ if(uid){
url += `/${uid}`; url += `/${uid}`;
+25
View File
@@ -0,0 +1,25 @@
/**
* 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`,
};
},
};
+11 -4
View File
@@ -17,6 +17,7 @@
export default { export default {
getBookmarks() { getBookmarks() {
return { return {
method: 'get', method: 'get',
url: '/api/frontend/v1/Bookmark/getBookmarks' url: '/api/frontend/v1/Bookmark/getBookmarks'
@@ -28,18 +29,24 @@ export default {
url: `/api/frontend/v1/Bookmark/delete/${bookmark_id}` url: `/api/frontend/v1/Bookmark/delete/${bookmark_id}`
}; };
}, },
update({ bookmark_id, url, title, tag=null }) { update({ bookmark_id, url, title, tag }) {
return { return {
method: 'post', method: 'post',
url: `/api/frontend/v1/Bookmark/update/${bookmark_id}`, url: `/api/frontend/v1/Bookmark/update/${bookmark_id}`,
params: { url, title } params: { url, title, tag }
}; };
}, },
insert({ url, title, tag }) { insert({ url, title, tag, sort }) {
return { return {
method: 'post', method: 'post',
url: `/api/frontend/v1/Bookmark/insert`, url: `/api/frontend/v1/Bookmark/insert`,
params: { url, title, tag } params: { url, title, tag, sort }
}; };
},
changeOrder(bookmark_id1, bookmark_id2) {
return {
method: 'post',
url: `/api/frontend/v1/Bookmark/changeOrder/${bookmark_id1}/${bookmark_id2}`,
};
} }
}; };
@@ -4,25 +4,27 @@ import Theme from '../../plugins/Theme.js';
import contrast from '../../directives/contrast.js'; import contrast from '../../directives/contrast.js';
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js"; import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js";
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js"; import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js";
import MyLvPlan from "../../components/Cis/LvPlan/Personal.js"; import MyLvPlan from "../../components/Cis/LvPlan/MyLvPlan.js";
import MylvStudent from "../../components/Cis/Mylv/Student.js"; import MylvStudent from "../../components/Cis/Mylv/Student.js";
import Profil from "../../components/Cis/Profil/Profil.js"; import Profil from "../../components/Cis/Profil/Profil.js";
import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js"; import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js";
import CmsNews from "../../components/Cis/Cms/News.js"; import CmsNews from "../../components/Cis/Cms/News.js";
import CmsContent from "../../components/Cis/Cms/Content.js"; import CmsContent from "../../components/Cis/Cms/Content.js";
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js"; import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import RoomInformation, {DEFAULT_MODE_RAUMINFO} from "../../components/Cis/Mylv/RoomInformation.js"; import RoomInformation, {DEFAULT_MODE_RAUMINFO_DESKTOP, DEFAULT_MODE_RAUMINFO_MOBILE} from "../../components/Cis/Mylv/RoomInformation.js";
import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js"; import AbgabetoolStudent from "../../components/Cis/Abgabetool/AbgabetoolStudent.js";
import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js"; import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMitarbeiter.js";
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js"; import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js"; import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import Studium from "../../components/Cis/Studium/Studium.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 ApiRouteInfo from '../../api/factory/routeinfo.js';
import {capitalize} from "../../helpers/StringHelpers.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 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({ const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}`), history: VueRouter.createWebHistory(`/${ciPath}`),
@@ -85,7 +87,7 @@ const router = VueRouter.createRouter({
name: "RoomInformation", name: "RoomInformation",
params: { // in this case always populate other params since they are not optional params: { // in this case always populate other params since they are not optional
ort_kurzbz: to.params.ort_kurzbz, ort_kurzbz: to.params.ort_kurzbz,
mode: DEFAULT_MODE_RAUMINFO, mode: isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP,
focus_date: new Date().toISOString().split("T")[0] focus_date: new Date().toISOString().split("T")[0]
}, },
}; };
@@ -102,7 +104,7 @@ const router = VueRouter.createRouter({
const mode = route.params.mode && const mode = route.params.mode &&
validModes.includes(route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()) 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() ? route.params.mode.charAt(0).toUpperCase() + route.params.mode.slice(1).toLowerCase()
: DEFAULT_MODE_RAUMINFO; : (isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP);
// default to today date if not provided // default to today date if not provided
const d = new Date(route.params.focus_date) const d = new Date(route.params.focus_date)
@@ -197,6 +199,26 @@ 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`, path: `/Cis4`,
name: 'Cis4', name: 'Cis4',
@@ -227,7 +249,7 @@ const router = VueRouter.createRouter({
}) })
const app = Vue.createApp({ const app = Vue.createApp({
name: 'FhcApp', name: 'CisApp',
data: () => ({ data: () => ({
appSideMenuEntries: {}, appSideMenuEntries: {},
renderers: null, renderers: null,
@@ -238,13 +260,12 @@ const app = Vue.createApp({
const smallScreen = window.matchMedia("(max-width: 767px)").matches; const smallScreen = window.matchMedia("(max-width: 767px)").matches;
const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0; const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
return smallScreen;// && touchCapable; return smallScreen;// && touchCapable;
} },
}, },
provide() { provide() {
return { // provide injectable & watchable language property return { // provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language), language: Vue.computed(() => this.$p.user_language),
renderers: Vue.computed(() => this.renderers), isMobile: this.isMobile,
isMobile: this.isMobile
} }
}, },
methods: { methods: {
@@ -282,46 +303,6 @@ const app = Vue.createApp({
} }
} }
}, },
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;
}
});
},
mounted() { mounted() {
document.addEventListener('click', this.handleClick); document.addEventListener('click', this.handleClick);
@@ -353,4 +334,4 @@ app.mount('#fhccontent');
router.afterEach((to, from, failure) => { router.afterEach((to, from, failure) => {
app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath)); app.config.globalProperties.$api.call(ApiRouteInfo.info('cis4', to.fullPath));
}); });
@@ -1,11 +1,12 @@
import FhcSearchbar from "../components/searchbar/searchbar.js"; import FhcSearchbar from "../../components/searchbar/searchbar.js";
import CisMenu from "../components/Cis/Menu.js"; import CisMenu from "../../components/Cis/Menu.js";
import PluginsPhrasen from '../plugins/Phrasen.js'; import PluginsPhrasen from '../../plugins/Phrasen.js';
import ApiSearchbar from '../api/factory/searchbar.js'; import Theme from "../../plugins/Theme.js";
import Theme from "../plugins/Theme.js"; import ApiSearchbar from '../../api/factory/searchbar.js';
import ApiLvPlan from "../../api/factory/lvPlan.js";
const app = Vue.createApp({ const app = Vue.createApp({
name: 'CisApp', name: 'CisMenuApp',
components: { components: {
FhcSearchbar, FhcSearchbar,
CisMenu CisMenu
@@ -136,11 +137,53 @@ const app = Vue.createApp({
} }
}; };
}, },
computed: {
isMobile() {
const smallScreen = window.matchMedia("(max-width: 767px)").matches;
const touchCapable = ("ontouchstart" in window) || navigator.maxTouchPoints > 0;
return smallScreen;// && touchCapable;
},
},
provide() {
return {
isMobile: this.isMobile
}
},
methods: { methods: {
searchfunction: function(searchsettings) { searchfunction: function(searchsettings) {
return this.$api.call(ApiSearchbar.searchCis(searchsettings)); return this.$api.call(ApiSearchbar.searchCis(searchsettings));
} }
} },
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,
);
}
},
}); });
FhcApps.makeExtendable(app); FhcApps.makeExtendable(app);
+7 -45
View File
@@ -3,13 +3,10 @@ import DashboardAdmin from '../../components/Dashboard/Admin.js';
import PluginsPhrasen from '../../plugins/Phrasen.js'; import PluginsPhrasen from '../../plugins/Phrasen.js';
import ApiRenderers from '../../api/factory/renderers.js';
const app = Vue.createApp({ const app = Vue.createApp({
name: 'DashboardAdminApp', name: 'DashboardAdminApp',
data: () => ({ data: () => ({
appSideMenuEntries: {}, appSideMenuEntries: {}
renderers: null
}), }),
components: { components: {
CoreNavigationCmpt, CoreNavigationCmpt,
@@ -17,51 +14,16 @@ const app = Vue.createApp({
}, },
provide() { provide() {
return { return {
// TODO(chris): move those two into the components that need it
renderers: Vue.computed(() => this.renderers),
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone 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.use(PluginsPhrasen);
app.directive('tooltip', primevue.tooltip); app.directive('tooltip', primevue.tooltip);
app.mount('#main'); app.mount('#main');
-16
View File
@@ -1,16 +0,0 @@
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');
@@ -20,7 +20,8 @@ export default {
}, },
inject: { inject: {
mode: "mode", mode: "mode",
dropableEvents: "dropableEvents" dropableEvents: "dropableEvents",
timezone: "timezone"
}, },
props: { props: {
events: Array, events: Array,
+120 -37
View File
@@ -1,5 +1,5 @@
import LineEvent from './Line/Event.js'; import LineEvent from "./Line/Event.js";
import LineBackground from './Line/Background.js'; import LineBackground from "./Line/Background.js";
/** /**
* TODO(chris): * TODO(chris):
@@ -10,54 +10,117 @@ export default {
name: "GridLine", name: "GridLine",
components: { components: {
LineEvent, LineEvent,
LineBackground LineBackground,
},
inject: {
axisRow: "axisRow"
}, },
inject: ["axisRow", "shouldCompactEvents", "compactibleEventTypes"],
props: { props: {
date: { date: {
type: luxon.DateTime, type: luxon.DateTime,
required: true required: true,
}, },
start: { start: {
type: luxon.DateTime, type: luxon.DateTime,
required: true required: true,
}, },
end: { end: {
type: luxon.DateTime, type: luxon.DateTime,
required: true required: true,
}, },
events: { events: {
type: Array, type: Array,
default: [] default: [],
}, },
backgrounds: { backgrounds: {
type: Array, type: Array,
default: [] default: [],
} },
}, },
computed: { computed: {
eventsWithRowInfo() { formattedEvents() {
const events = []; let formattedEvents = this.events.map((event) => {
this.events.forEach(event => { event.rows = [1, -1];
const rows = [1, -1];
if (event.startsHere) { if (event.startsHere) {
rows[0] = 't_' + event.start.diff(this.date).toMillis(); event.rows[0] =
"t_" + event.start.diff(this.date).toMillis();
} }
if (event.endsHere) { if (event.endsHere) {
rows[1] = 't_' + event.end.diff(this.date).toMillis(); event.rows[1] = "t_" + event.end.diff(this.date).toMillis();
} }
events.push({ return event;
...event,
rows
});
}); });
return events;
} if (this.shouldCompactEvents && this.compactibleEventTypes?.length) {
formattedEvents =
this.compactEvents(formattedEvents, this.compactibleEventTypes);
}
return formattedEvents;
},
}, },
template: /* html */` methods: {
compactEvents(events, compactibleEventTypes) {
let formattedEvents = events
.filter(
(event) =>
!compactibleEventTypes.includes(event.type),
)
.map((event) => {
event.display = "default";
return event;
});
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;
},
},
template: /* html */ `
<div <div
class="fhc-calendar-base-grid-line" class="fhc-calendar-base-grid-line"
style="position:relative;display:grid;grid-auto-flow:dense" style="position:relative;display:grid;grid-auto-flow:dense"
@@ -69,17 +132,37 @@ export default {
:end="end" :end="end"
:background="bg" :background="bg"
></line-background> ></line-background>
<line-event <template v-for="(event, i) in formattedEvents" :key="i">
v-for="(event, i) in eventsWithRowInfo" <line-event
:key="i" v-if="!event.display || event.display === 'default'"
:style="'grid-' + axisRow + ': ' + event.rows.join('/')" :style="'grid-' + axisRow + ': ' + event.rows.join('/')"
:event="event" :event="event"
> >
<template v-slot="slot"> <template v-slot="slot">
<slot name="event" v-bind="slot" /> <slot name="event" v-bind="slot" />
</template> </template>
</line-event> </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"
>
<i
v-for="(subEvent, subEventIndex) in event.events"
:key="subEventIndex"
class="fa-solid fa-circle fa-2xs"
:style="subEvent.farbe ? {color: '#' + subEvent.farbe} : {}"
></i>
</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>
<slot name="dropzone" /> <slot name="dropzone" />
</div> </div>
` `,
} };
+68 -28
View File
@@ -3,24 +3,20 @@ import FhcCalendar from "./Base.js";
import ApiLvPlan from '../../api/factory/lvPlan.js'; import ApiLvPlan from '../../api/factory/lvPlan.js';
import { useEventLoader } from '../../composables/EventLoader.js'; import { useEventLoader } from '../../composables/EventLoader.js';
import { useRenderers } from '../../composables/Renderers.js';
import ModeDay from './Mode/Day.js'; import ModeDay from './Mode/Day.js';
import ModeWeek from './Mode/Week.js'; import ModeWeek from './Mode/Week.js';
import ModeMonth from './Mode/Month.js'; import ModeMonth from './Mode/Month.js';
import ModeList from './Mode/List.js';
export default { export default {
name: "CalendarLvPlan", name: "CalendarLvPlan",
components: { components: {
FhcCalendar FhcCalendar
}, },
inject: [ inject: ["isMobile"],
"renderers"
],
props: { props: {
timezone: {
type: String,
required: true
},
date: { date: {
type: [Date, String, Number, luxon.DateTime], type: [Date, String, Number, luxon.DateTime],
default: luxon.DateTime.local() default: luxon.DateTime.local()
@@ -34,6 +30,16 @@ export default {
required: true required: true
} }
}, },
provide() {
return {
shouldCompactEvents: Vue.computed(
() => this.$props.mode === "Month" && this.isMobile,
),
compactibleEventTypes: Vue.computed(
() => this.compactibleEventTypes,
),
};
},
emits: [ emits: [
"update:date", "update:date",
"update:mode", "update:mode",
@@ -41,11 +47,7 @@ export default {
], ],
data() { data() {
return { return {
modes: { timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
day: Vue.markRaw(ModeDay),
week: Vue.markRaw(ModeWeek),
month: Vue.markRaw(ModeMonth)
},
modeOptions: { modeOptions: {
day: { day: {
emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')), emptyMessage: Vue.computed(() => this.$p.t('lehre/noLvFound')),
@@ -53,9 +55,13 @@ export default {
}, },
week: { week: {
collapseEmptyDays: false collapseEmptyDays: false
} },
list: {
length: 7,
},
}, },
teachingunits: null teachingunits: null,
compactibleEventTypes: [],
}; };
}, },
computed: { computed: {
@@ -77,7 +83,20 @@ export default {
label: now.startOf('minute').toISOTime({ suppressSeconds: true, includeOffset: false }) 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: { methods: {
eventStyle(event) { eventStyle(event) {
@@ -88,33 +107,47 @@ export default {
updateRange(rangeInterval) { updateRange(rangeInterval) {
this.rangeInterval = rangeInterval; this.rangeInterval = rangeInterval;
this.$emit('update:range', 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) { setup(props, context) {
const rangeInterval = Vue.ref(null); const rangeInterval = Vue.ref(null);
const { events, lv } = useEventLoader(rangeInterval, props.getPromiseFunc); const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
Vue.watch(lv, newValue => { Vue.watch(lv, newValue => {
context.emit('update:lv', newValue); context.emit('update:lv', newValue);
}); });
const { renderers } = useRenderers();
return { return {
rangeInterval, rangeInterval,
events, events,
lv lv,
reset,
renderers
}; };
}, },
created() { async created() {
this.$api await this.getStunden();
.call(ApiLvPlan.getStunden()) await this.getCompactibleEventTypes();
.then(res => {
return this.teachingunits = res.data.map(el => ({
id: el.stunde,
start: el.beginn,
end: el.ende
}));
});
}, },
template: /* html */` template: /* html */`
<fhc-calendar <fhc-calendar
@@ -136,6 +169,13 @@ export default {
> >
<template v-slot="{ event, mode }"> <template v-slot="{ event, mode }">
<div <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'" :class="'event-type-' + event.type + ' ' + mode + 'PageContainer'"
:type="mode == 'day' ? 'button' : undefined" :type="mode == 'day' ? 'button' : undefined"
:style="eventStyle(event)" :style="eventStyle(event)"
+1 -1
View File
@@ -93,7 +93,7 @@ export default {
mounted() { mounted() {
this.$emit('update:range', this.range); this.$emit('update:range', this.range);
}, },
template: ` template: /*html*/ `
<div <div
class="fhc-calendar-mode-list flex-grow-1 position-relative" class="fhc-calendar-mode-list flex-grow-1 position-relative"
@cal-click-default.capture="handleClickDefaults" @cal-click-default.capture="handleClickDefaults"
+7 -9
View File
@@ -1,6 +1,7 @@
import FhcCalendar from "./Base.js"; import FhcCalendar from "./Base.js";
import { useEventLoader } from '../../composables/EventLoader.js'; import { useEventLoader } from '../../composables/EventLoader.js';
import { useRenderers } from '../../composables/Renderers.js';
import ModeList from '../Calendar/Mode/List.js'; import ModeList from '../Calendar/Mode/List.js';
@@ -9,22 +10,17 @@ export default {
components: { components: {
FhcCalendar FhcCalendar
}, },
inject: [
"renderers"
],
props: { props: {
timezone: {
type: String,
required: true
},
getPromiseFunc: { getPromiseFunc: {
type: Function, type: Function,
required: true required: true
} }
}, },
data() { data() {
const timezone = FHC_JS_DATA_STORAGE_OBJECT.timezone;
return { return {
now: luxon.DateTime.now().setZone(this.timezone), timezone,
now: luxon.DateTime.now().setZone(timezone),
modes: { modes: {
list: Vue.markRaw(ModeList) list: Vue.markRaw(ModeList)
}, },
@@ -59,10 +55,12 @@ export default {
const rangeInterval = Vue.ref(null); const rangeInterval = Vue.ref(null);
const { events } = useEventLoader(rangeInterval, props.getPromiseFunc); const { events } = useEventLoader(rangeInterval, props.getPromiseFunc);
const { renderers } = useRenderers();
return { return {
rangeInterval, rangeInterval,
events events,
renderers
}; };
}, },
template: /* html */` template: /* html */`
@@ -42,14 +42,6 @@ export const AbgabetoolAssistenz = {
stg_kz_prop: { stg_kz_prop: {
default: null default: null
}, },
viewData: {
type: Object,
required: true,
default: () => ({name: '', uid: ''}),
validator(value) {
return value && value.uid // && value.name -> extensive viewData use only for cis4 onwards
}
}
}, },
data() { data() {
return { return {
@@ -31,16 +31,6 @@ export const AbgabetoolMitarbeiter = {
old_abgabe_beurteilung_link: Vue.computed(() => this.old_abgabe_beurteilung_link) 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() { data() {
return { return {
tableData: null, tableData: null,
@@ -3,6 +3,7 @@ import ApiAbgabe from '../../../api/factory/abgabe.js'
import BsModal from "../../Bootstrap/Modal.js"; import BsModal from "../../Bootstrap/Modal.js";
import FhcOverlay from "../../Overlay/FhcOverlay.js"; import FhcOverlay from "../../Overlay/FhcOverlay.js";
import { getDateStyleClass} from "./getDateStyleClass.js"; import { getDateStyleClass} from "./getDateStyleClass.js";
import ApiAuthinfo from "../../../api/factory/authinfo.js";
export const AbgabetoolStudent = { export const AbgabetoolStudent = {
name: "AbgabetoolStudent", name: "AbgabetoolStudent",
@@ -24,14 +25,6 @@ export const AbgabetoolStudent = {
student_uid_prop: { student_uid_prop: {
default: null default: null
}, },
viewData: {
type: Object,
required: true,
default: () => ({uid: ''}),
validator(value) {
return value && value.uid
}
}
}, },
data() { data() {
return { return {
@@ -44,9 +37,18 @@ export const AbgabetoolStudent = {
detail: null, detail: null,
projektarbeiten: null, projektarbeiten: null,
selectedProjektarbeit: null, selectedProjektarbeit: null,
moodle_link: null moodle_link: null,
uid: null,
}; };
}, },
computed: {
isViewMode() {
return this.student_uid !== this.uid
},
student_uid() {
return this.student_uid_prop || this.uid || null
}
},
methods: { methods: {
checkQualityGatesStrict(termine) { checkQualityGatesStrict(termine) {
let qgate1Passed = false let qgate1Passed = false
@@ -258,18 +260,11 @@ export const AbgabetoolStudent = {
}, },
handleDownloadBeurteilung2(projektarbeit) { handleDownloadBeurteilung2(projektarbeit) {
window.open(projektarbeit.beurteilung2) window.open(projektarbeit.beurteilung2)
}
},
watch: {
},
computed: {
isViewMode() {
return this.student_uid !== this.viewData.uid
}, },
student_uid() { async fetchAuthUID() {
return this.student_uid_prop || this.viewData?.uid || null const authIdResponse = await this.$api.call(ApiAuthinfo.getAuthUID());
} this.uid = authIdResponse.data.uid;
},
}, },
async created() { async created() {
this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global']) this.phrasenPromise = this.$p.loadCategory(['abgabetool', 'global'])
@@ -302,6 +297,8 @@ export const AbgabetoolStudent = {
}).catch(e => { }).catch(e => {
this.loading = false this.loading = false
}) })
await this.fetchAuthUID();
}, },
mounted() { mounted() {
this.setupMounted() this.setupMounted()
@@ -10,14 +10,6 @@ export const DeadlineOverview = {
person_uid_prop: { person_uid_prop: {
default: null default: null
}, },
viewData: {
type: Object,
required: true,
default: () => ({name: '', uid: ''}),
validator(value) {
return value && value.name && value.uid
}
}
}, },
data() { data() {
return { return {
+114 -92
View File
@@ -2,115 +2,137 @@ import Pagination from "../../Pagination/Pagination.js";
import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js"; import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js";
import BsConfirm from "../../Bootstrap/Confirm.js"; import BsConfirm from "../../Bootstrap/Confirm.js";
import ApiCms from '../../../api/factory/cms.js'; import ApiCms from "../../../api/factory/cms.js";
export default { export default {
name: "NewsComponent", name: "NewsComponent",
components: { components: {
Pagination, Pagination,
StudiengangInformation, 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;
}, },
}, inject: ["isMobile"],
methods: { data() {
fetchNews() { return {
return this.$api content: null,
.call(ApiCms.getNews(this.page, this.page_size, this.sprache)) maxPageCount: 0,
.then(res => res.data) page_size: 10,
.then(result => { page: 1,
this.content = result; };
},
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) => { document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
el.addEventListener("click", (evt) => { el.addEventListener("click", (evt) => {
evt.preventDefault(); evt.preventDefault();
BsConfirm.popup(el.dataset.confirm) BsConfirm.popup(el.dataset.confirm)
.then(() => { .then(() => {
Axios.get(el.href) Axios.get(el.href)
.then((res) => { .then((res) => {
// TODO(chris): check for success then show message and/or reload // TODO(chris): check for success then show message and/or reload
location = location; location = location;
})
.catch((err) => console.error("ERROR:", err));
}) })
.catch(() => { .catch((err) => console.error("ERROR:", err));
}); })
}); .catch(() => {});
}); });
document.querySelectorAll("#cms [data-href]").forEach((el) => { });
el.href = el.dataset.href.replace( document.querySelectorAll("#cms [data-href]").forEach((el) => {
/^ROOT\//, el.href = el.dataset.href.replace(
FHC_JS_DATA_STORAGE_OBJECT.app_root /^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");
});
}) 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");
}); });
}, },
loadNewPageContent(data) { afterPageUpdated(event) {
this.$api this.page = event.page;
.call(ApiCms.getNews(data.page, data.rows)) this.page_size = event.rows;
.then(res => res.data) this.$refs.newsPageHeading.scrollIntoView({block: 'end'});
.then(result => { this.loadNewPageContent(event);
this.content = result; },
},
}); created() {
} this.fetchNews();
},
created() {
this.fetchNews();
this.$api this.$api
.call(ApiCms.getNewsRowCount()) .call(ApiCms.getNewsRowCount())
.then(res => res.data) .then((res) => res.data)
.then(result => { .then((result) => {
this.maxPageCount = result; this.maxPageCount = result;
}); });
}, },
template: /*html*/ ` template: /*html*/ `
<h2 class="fhc-primary-color">News</h2> <div :class="{'pb-3': isMobile}" class="overflow-x-hidden">
<hr/> <h2 ref="newsPageHeading" class="fhc-primary-color">News</h2>
<pagination v-show="content?true:false" :page_size="page_size" @page="page=$event.page; loadNewPageContent($event)" :maxPageCount="maxPageCount"> <hr/>
</pagination> <pagination
<div class="container-fluid mt-4"> v-show="content?true:false"
<div class="row"> :page="page"
<div class="col" v-html="content"> :page_size="page_size"
</div> @pageUpdated="afterPageUpdated($event)"
<div class="col-auto"> :maxPageCount="maxPageCount"
<div style="width:15rem"> ></pagination>
<studiengang-information></studiengang-information> <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>
</div> </div>
</div> </div>
<pagination
v-show="content?true:false"
:page="page"
:page_size="page_size"
@pageUpdated="afterPageUpdated($event)"
:maxPageCount="maxPageCount"
></pagination>
</div> </div>
<pagination v-show="content?true:false" :page_size="page_size" @page="loadNewPageContent" :maxPageCount="maxPageCount">
</pagination>
`, `,
}; };
@@ -3,7 +3,8 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js'; import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js'; import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN = 'Week' export const DEFAULT_MODE_LVPLAN_MOBILE = 'List';
export const DEFAULT_MODE_LVPLAN_DESKTOP = 'Week';
export default { export default {
name: 'LvPlanLehrveranstaltung', name: 'LvPlanLehrveranstaltung',
@@ -19,15 +20,21 @@ export default {
lv: null lv: null
}; };
}, },
inject: ["isMobile"],
computed:{ computed:{
currentDay() { currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date))) if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate(); return luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
return this.propsViewData?.focus_date; return this.propsViewData?.focus_date;
}, },
currentMode() { currentMode() {
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase())) let validModes = ['day', 'month'];
return DEFAULT_MODE_LVPLAN; 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; return this.propsViewData?.mode;
}, },
currentLv() { currentLv() {
@@ -95,7 +102,6 @@ export default {
<fhc-calendar <fhc-calendar
v-else-if="lv" v-else-if="lv"
ref="calendar" ref="calendar"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc" :get-promise-func="getPromiseFunc"
:date="currentDay" :date="currentDay"
:mode="currentMode" :mode="currentMode"
+10 -8
View File
@@ -3,7 +3,8 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js'; import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js'; import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN = 'Week' export const DEFAULT_MODE_LVPLAN_MOBILE = 'List';
export const DEFAULT_MODE_LVPLAN_DESKTOP = 'Week';
export default { export default {
name: 'LvPlan', name: 'LvPlan',
@@ -11,31 +12,32 @@ export default {
FhcCalendar FhcCalendar
}, },
props: { props: {
viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object propsViewData: Object
}, },
data() { data() {
const now = luxon.DateTime.now().setZone(this.viewData.timezone);
return { return {
studiensemester_kurzbz: null, studiensemester_kurzbz: null,
studiensemester_start: null, studiensemester_start: null,
studiensemester_ende: null, studiensemester_ende: null,
uid: null, uid: null,
lv: null lv: null,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
}; };
}, },
inject: ["isMobile"],
computed:{ computed:{
currentDay() { currentDay() {
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate(); return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.timezone).toISODate();
}, },
currentMode() { currentMode() {
return this.propsViewData?.mode || DEFAULT_MODE_LVPLAN; const defaultMode = this.isMobile ? DEFAULT_MODE_LVPLAN_MOBILE : DEFAULT_MODE_LVPLAN_DESKTOP;
return this.propsViewData?.mode || defaultMode;
}, },
downloadLinks() { downloadLinks() {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid) if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
return false; return false;
const opts = { zone: this.viewData.timezone }; const opts = { zone: this.timezone };
const start = luxon.DateTime const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts) .fromISO(this.studiensemester_start, opts)
.toUnixInteger(); .toUnixInteger();
@@ -115,7 +117,7 @@ export default {
<fhc-calendar <fhc-calendar
ref="calendar" ref="calendar"
v-model:lv="lv" v-model:lv="lv"
:timezone="viewData.timezone" :timezone="timezone"
:get-promise-func="getPromiseFunc" :get-promise-func="getPromiseFunc"
:date="currentDay" :date="currentDay"
:mode="currentMode" :mode="currentMode"
@@ -3,7 +3,8 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js'; import ApiLvPlan from '../../../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js'; import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN = 'Week' export const DEFAULT_MODE_LVPLAN_DESKTOP = "Week";
export const DEFAULT_MODE_LVPLAN_MOBILE = "List";
export default { export default {
name: 'LvPlanPersonal', name: 'LvPlanPersonal',
@@ -11,7 +12,6 @@ export default {
FhcCalendar FhcCalendar
}, },
props: { props: {
viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object propsViewData: Object
}, },
data() { data() {
@@ -21,18 +21,30 @@ export default {
studiensemester_ende: null, studiensemester_ende: null,
uid: null, uid: null,
isMitarbeiter: false, isMitarbeiter: false,
isStudent: false isStudent: false,
timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
}; };
}, },
inject: ["isMobile"],
computed:{ computed:{
currentDay() { currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date))) if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate(); return luxon.DateTime.now().setZone(this.timezone).toISODate();
return this.propsViewData?.focus_date; return this.propsViewData?.focus_date;
}, },
currentMode() { currentMode() {
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase())) let validModes = ["day", "month"];
return DEFAULT_MODE_LVPLAN; 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; return this.propsViewData?.mode;
}, },
downloadLinks() { downloadLinks() {
@@ -47,7 +59,7 @@ export default {
return; return;
} }
const opts = { zone: this.viewData.timezone }; const opts = { zone: this.timezone };
const start = luxon.DateTime const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts) .fromISO(this.studiensemester_start, opts)
.toUnixInteger(); .toUnixInteger();
@@ -102,16 +114,18 @@ export default {
this.$api.call(ApiLvPlan.eventsPersonal(start.toISODate(), end.toISODate())), this.$api.call(ApiLvPlan.eventsPersonal(start.toISODate(), end.toISODate())),
this.$api.call(ApiLvPlan.getLvPlanReservierungen(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;
},
}, },
created() { async created() {
this.$api await this.fetchAuthInfo();
.call(ApiAuthinfo.getAuthInfo())
.then(res => {
this.uid = res.data.uid;
this.isMitarbeiter = res.data.isMitarbeiter;
this.isStudent = res.data.isStudent;
});
}, },
template: /*html*/` template: /*html*/`
<div class="cis-lvplan-personal d-flex flex-column h-100"> <div class="cis-lvplan-personal d-flex flex-column h-100">
@@ -123,8 +137,9 @@ export default {
</h2> </h2>
<hr> <hr>
<fhc-calendar <fhc-calendar
v-if="timezone"
ref="calendar" ref="calendar"
:timezone="viewData.timezone" :timezone="timezone"
:get-promise-func="getPromiseFunc" :get-promise-func="getPromiseFunc"
:date="currentDay" :date="currentDay"
:mode="currentMode" :mode="currentMode"
@@ -0,0 +1,269 @@
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() {
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();
},
},
},
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>
`,
};
+413
View File
@@ -0,0 +1,413 @@
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() {
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",
},
];
},
},
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();
}
}
},
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>
`,
};
@@ -0,0 +1,187 @@
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 = 'Week';
export default {
name: 'LvPlanStgOrg',
inject: {
cisRoot: {
from: 'cisRoot'
},
},
components: {
FhcCalendar,
},
props: {
viewData: Object,
propsViewData: Object
},
data() {
return {
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
uid: null,
isMitarbeiter: false,
isStudent: false,
listStg: [],
currentStgBezeichnung: null
};
},
computed:{
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
return this.propsViewData?.focus_date;
},
currentMode() {
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
return DEFAULT_MODE_LVPLAN;
return this.propsViewData?.mode;
},
downloadLinks() {
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.viewData.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' }
];
}
},
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: "StgOrgLvPlan",
params: {
mode,
focus_date,
stgkz: this.propsViewData.stgkz,
sem: this.propsViewData.sem,
verband: this.propsViewData.verband,
gruppe: this.propsViewData.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.toISODate(), end.toISODate(), this.propsViewData.stgkz, this.propsViewData.sem, this.propsViewData.verband, this.propsViewData.gruppe)),
//local for test
/* this.$api.call(ApiLvPlan.eventsStgOrg(start.toISODate(), end.toISODate(), this.stgkz, this.sem, this.verband, this.gruppe))*/
];
},
backToDropdown(){
this.$router.push({
name: "OverviewLvPlan",
});
}
},
created(){
this.$api
.call(ApiAuthinfo.getAuthInfo())
.then(res => {
this.uid = res.data.uid;
this.isMitarbeiter = res.data.isMitarbeiter;
this.isStudent = res.data.isStudent;
});
if(this.propsViewData.stgkz) {
this.$api
.call(ApiLvPlan.getStudiengaenge())
.then(result => {
const currentStg = result.data.find(
item => item.studiengang_kz == this.propsViewData.stgkz
);
this.currentStgBezeichnung = currentStg.kurzbzlang + " - " + currentStg.bezeichnung;
})
.catch(this.$fhcAlert.handleSystemError);
}
},
template: `
<div class="cis-lvplan-stg-org d-flex flex-column h-100">
<h2>{{ $p.t('LvPlan/headerLvPlanLvVerband') }}</h2>
<p v-if="propsViewData.stgkz"><span><strong>{{currentStgBezeichnung}}</strong></span>
<span v-if="propsViewData.sem" style="margin-left:10px;"> Semester: {{propsViewData.sem}} </span>
<span v-if="propsViewData.verband" style="margin-left:10px;"> Verband: {{propsViewData.verband}} </span>
<span v-if="propsViewData.gruppe" style="margin-left:10px;"> Gruppe: {{propsViewData.gruppe}} </span>
<button class="py-1 btn btn-outline-secondary" :title="this.$p.t('LvPlan', 'backToDropdown')" style="margin-left:10px;"> <i class="fa fa-arrow-left fa-xs" @click="backToDropdown"></i></button>
</p>
<p v-else>{{ $p.t('LvPlan/noStgProvided') }}</p>
<fhc-calendar
ref="calendar"
:timezone="viewData.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>
`,
};
+47 -20
View File
@@ -30,6 +30,7 @@ export default {
menuOpen:true, menuOpen:true,
}; };
}, },
inject: ["isMobile"],
provide(){ provide(){
return{ return{
setActiveEntry: this.setActiveEntry, setActiveEntry: this.setActiveEntry,
@@ -58,7 +59,7 @@ export default {
}, },
site_url(){ site_url(){
return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router; return FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
} },
}, },
methods: { methods: {
fetchMenu() { fetchMenu() {
@@ -112,10 +113,26 @@ export default {
}); });
}, },
template: /*html*/` template: /*html*/`
<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"> <div
<span class="navbar-toggler-icon"></span> id="header-options-collapsible"
</button> class="collapse multi-collapse collapse-horizontal show"
<fhc-searchbar ref="searchbar" id="nav-search" class="fhc-searchbar w-100 py-1 py-lg-2" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar> >
<div class="d-flex flex-row align-items-center gap-2 h-100" style="width: 79px">
<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>
<span v-if="isMobile" class="d-flex flex-row align-items-center">
<theme-switch></theme-switch>
</span>
</div>
</div>
<fhc-searchbar
:searchoptions="searchbaroptions"
:searchfunction="searchfunction"
ref="searchbar"
id="nav-search"
class="fhc-searchbar w-100 py-1 py-lg-2"
></fhc-searchbar>
<div id="nav-logo" class="d-none d-lg-block"> <div id="nav-logo" class="d-none d-lg-block">
<div class="d-flex h-100 justify-content-between"> <div class="d-flex h-100 justify-content-between">
<a :href="rootUrl"> <a :href="rootUrl">
@@ -124,22 +141,32 @@ export default {
<theme-switch></theme-switch> <theme-switch></theme-switch>
</div> </div>
</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"> <div
<img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/> id="header-usermenu-collapsible"
</button> class="collapse multi-collapse collapse-horizontal show"
<ul ref="navUserDropdown" >
@[\`shown.bs.collapse\`]="handleShowNavUser" <div
@[\`hide.bs.collapse\`]="handleHideNavUser" :style="!isMobile ? '' : 'width: 51px'"
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn"> id="nav-user"
<li><a class="fhc-dark-bg btn rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li> >
<li > <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">
<cis-sprachen @languageChanged="fetchMenu"></cis-sprachen> <img :src="avatarUrl" :alt="$p.t('profilUpdate/profilBild')" class="bg-dark avatar rounded-circle border border-dark"/>
</li> </button>
<li><hr class="dropdown-divider m-0 "></li> <ul ref="navUserDropdown"
<li ><a class="fhc-dark-bg btn rounded-0 d-block" :href="logoutUrl">Logout</a></li> @[\`shown.bs.collapse\`]="handleShowNavUser"
</ul> @[\`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> </div>
<nav id="nav-main" class="offcanvas offcanvas-start" tabindex="-1" aria-labelledby="nav-main-btn" data-bs-backdrop="false"> <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 id="nav-main-sticky">
<div id="nav-main-toggle" class="position-static d-none d-lg-block "> <div id="nav-main-toggle" class="position-static d-none d-lg-block ">
@@ -100,7 +100,7 @@ export default {
</template> </template>
<template v-else> <template v-else>
<span v-if="event?.lehrfach_bez ">{{event?.lehrfach_bez + (event?.stg_kurzbzlang?' / ' + event?.stg_kurzbzlang:'')}}</span> <span v-if="event?.lehrfach_bez ">{{event?.lehrfach_bez + (event?.stg_kurzbzlang?' / ' + event?.stg_kurzbzlang:'')}}</span>
<span v-else>Lehrveranstaltungs Übersicht</span> <span v-else>{{$p.t('lehre','lvUebersicht')}}</span>
</template> </template>
</template> </template>
@@ -2,7 +2,8 @@ import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../../../api/factory/lvPlan.js'; import ApiLvPlan from '../../../api/factory/lvPlan.js';
export const DEFAULT_MODE_RAUMINFO = 'Week' export const DEFAULT_MODE_RAUMINFO_MOBILE = 'List';
export const DEFAULT_MODE_RAUMINFO_DESKTOP = 'Week';
export default { export default {
name: "RoomInformation", name: "RoomInformation",
@@ -13,12 +14,14 @@ export default {
viewData: Object, // NOTE(chris): this is inherited from router-view viewData: Object, // NOTE(chris): this is inherited from router-view
propsViewData: Object propsViewData: Object
}, },
inject: ["isMobile"],
computed: { computed: {
currentDay() { currentDay() {
return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(this.viewData.timezone).toISODate(); return this.propsViewData?.focus_date || luxon.DateTime.now().setZone(FHC_JS_DATA_STORAGE_OBJECT.timezone).toISODate();
}, },
currentMode() { currentMode() {
return this.propsViewData?.mode || DEFAULT_MODE_RAUMINFO; const defaultMode = this.isMobile ? DEFAULT_MODE_RAUMINFO_MOBILE : DEFAULT_MODE_RAUMINFO_DESKTOP;
return this.propsViewData?.mode || defaultMode;
} }
}, },
methods:{ methods:{
@@ -51,7 +54,6 @@ export default {
<hr> <hr>
<fhc-calendar <fhc-calendar
ref="calendar" ref="calendar"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc" :get-promise-func="getPromiseFunc"
:date="currentDay" :date="currentDay"
:mode="currentMode" :mode="currentMode"
@@ -1,9 +1,11 @@
import MylvSemesterStudiengangLv from "./Studiengang/Lv.js"; import MylvSemesterStudiengangLv from "./Studiengang/Lv.js";
import MylvSemesterStudiengangAverageGrade from "./Studiengang/AverageGrade.js";
import Phrasen from "../../../../mixins/Phrasen.js"; import Phrasen from "../../../../mixins/Phrasen.js";
export default { export default {
components: { components: {
MylvSemesterStudiengangLv MylvSemesterStudiengangLv,
MylvSemesterStudiengangAverageGrade
}, },
mixins: [ mixins: [
Phrasen Phrasen
@@ -29,9 +31,10 @@ export default {
methods: { methods: {
note(lv) { note(lv) {
return lv.benotung ? lv.znote || lv.lvnote || null : null; return lv.benotung ? lv.znote || lv.lvnote || null : null;
} },
}, },
template: `<div class="card mb-3"> template: `<div class="card mb-3">
<div class="card-body"> <div class="card-body">
<h4 class="card-title mb-3">{{$p.user_language.value === 'English' ? sg_bezeichnung_eng : bezeichnung}} - {{kuerzel}} <h4 class="card-title mb-3">{{$p.user_language.value === 'English' ? sg_bezeichnung_eng : bezeichnung}} - {{kuerzel}}
<small>{{semester}}.{{$p.t('lehre/semester')}}</small> <small>{{semester}}.{{$p.t('lehre/semester')}}</small>
@@ -41,6 +44,7 @@ export default {
<mylv-semester-studiengang-lv v-bind="lv" class="text-center h-100"></mylv-semester-studiengang-lv> <mylv-semester-studiengang-lv v-bind="lv" class="text-center h-100"></mylv-semester-studiengang-lv>
</div> </div>
</div> </div>
<mylv-semester-studiengang-average-grade :lvs="lvs" />
</div> </div>
</div>` </div>`
}; };
@@ -0,0 +1,94 @@
import Phrasen from "../../../../../mixins/Phrasen.js";
export default {
mixins: [
Phrasen
],
props: {
lvs: Array,
},
data: ( ) =>{
return {
gradeAverage: null,
gradeWeightedAverage: null,
existingGrades: false
}
},
methods: {
calculateAverages(){
let sum = 0;
let count = 0;
let sumWeighted = 0;
let sumEcts = 0;
this.lvs.forEach((lv) => {
if ((lv.znote >= 1 && lv.znote <= 5) && lv.znote!= null) {
this.existingGrades = true;
sum+= lv.znote;
count++;
sumWeighted += lv.znote * Number(lv.ects);
sumEcts += Number(lv.ects);
}
});
this.gradeAverage = (sum/count).toFixed(2);
this.gradeWeightedAverage = (sumWeighted/sumEcts).toFixed(2);
}
},
watch: {
lvs: {
handler() {
this.calculateAverages();
},
deep: true,
immediate: true
}
},
mounted(){
this.calculateAverages();
},
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="existingGrades">
<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="existingGrades" 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>
`
}
@@ -73,11 +73,11 @@ export default {
}, },
grade() { grade() {
const languageIndex = this.$p.user_language.value === 'English' ? 1 : 0 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) { if(this.benotung && this.znotebez?.length) {
return this.znotebez[languageIndex] return this.znotebez[languageIndex]
} else if(this.benotung && this.lvnotebez?.length) { }
return this.lvnotebez[languageIndex] else return null
} else return null
}, },
LvHasPruefungenInformation(){ LvHasPruefungenInformation(){
return this.pruefungenData && this.pruefungenData.length > 0; return this.pruefungenData && this.pruefungenData.length > 0;
@@ -9,6 +9,7 @@ import QuickLinks from "./ProfilComponents/QuickLinks.js";
import ProfilEmails from "./ProfilComponents/ProfilEmails.js"; import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
import RoleInformation from "./ProfilComponents/RoleInformation.js"; import RoleInformation from "./ProfilComponents/RoleInformation.js";
import ProfilInformation from "./ProfilComponents/ProfilInformation.js"; import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
import CalendarSync from "./ProfilComponents/CalendarSync.js";
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js'; import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
import { dateFilter } from '../../../tabulator/filters/Dates.js'; import { dateFilter } from '../../../tabulator/filters/Dates.js';
@@ -26,6 +27,7 @@ export default {
ProfilEmails, ProfilEmails,
RoleInformation, RoleInformation,
ProfilInformation, ProfilInformation,
CalendarSync,
}, },
inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"], inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"],
@@ -103,7 +105,6 @@ export default {
}, },
], ],
}, },
betriebsmittel_table_options: { betriebsmittel_table_options: {
persistenceID: "filterTableMaProfilBetriebsmittel", persistenceID: "filterTableMaProfilBetriebsmittel",
persistence: { persistence: {
@@ -160,6 +161,7 @@ export default {
props: { props: {
data: Object, data: Object,
editData: Object, editData: Object,
calendarSyncUrls: Array,
}, },
methods: { methods: {
@@ -296,6 +298,11 @@ export default {
} }
}; };
}, },
quickLinks() {
let quickLinks = [];
//
return quickLinks;
},
}, },
created() { created() {
@@ -313,7 +320,6 @@ export default {
}); });
//? sorts the profil Updates: pending -> accepted -> rejected //? sorts the profil Updates: pending -> accepted -> rejected
this.data.profilUpdates?.sort(this.sortProfilUpdates); this.data.profilUpdates?.sort(this.sortProfilUpdates);
}, },
watch: { watch: {
'data.funktionen'(newVal) { 'data.funktionen'(newVal) {
@@ -331,12 +337,6 @@ export default {
<edit-profil v-if="showModal" ref="editModal" :isMitarbeiter="true" @hideBsModal="hideEditProfilModal" :value="JSON.parse(JSON.stringify(filteredEditData))" :titel="$p.t('profil','profilBearbeiten')"></edit-profil> <edit-profil v-if="showModal" ref="editModal" :isMitarbeiter="true" @hideBsModal="hideEditProfilModal" :value="JSON.parse(JSON.stringify(filteredEditData))" :titel="$p.t('profil','profilBearbeiten')"></edit-profil>
<div class="row"> <div class="row">
<div class="d-md-none col-12 "> <div class="d-md-none col-12 ">
<!--TODO: uncomment when implemented
<div class="row mb-3">
<div class="col">
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
</div>
</div>-->
<!-- Bearbeiten Button --> <!-- Bearbeiten Button -->
<div v-if="isEditable" class="row mb-3 "> <div v-if="isEditable" class="row mb-3 ">
<div class="col"> <div class="col">
@@ -465,17 +465,11 @@ export default {
</div> </div>
<!-- START OF SIDE PANEL --> <!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" > <div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!--TODO: uncomment when implemented <div v-if="quickLinks.length" class="row mb-4">
<div class="row d-none d-md-block mb-3"> <div class="col">
<quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
<div class="col"> </div>
</div>
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
</div>
</div>-->
<!-- Bearbeiten Button --> <!-- Bearbeiten Button -->
<div class="row d-none d-md-block "> <div class="row d-none d-md-block ">
<div class="col mb-3"> <div class="col mb-3">
@@ -501,12 +495,17 @@ export default {
<ausweis-status :data="data.zutrittsdatum"></ausweis-status> <ausweis-status :data="data.zutrittsdatum"></ausweis-status>
</div> </div>
</div> </div>
<div class="row"> <div class="row mb-3">
<div class="col"> <div class="col">
<!-- MAILVERTEILER --> <!-- MAILVERTEILER -->
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler> <mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
</div> </div>
</div> </div>
<div class="row">
<div class="col">
<calendar-sync :uid="$props.data.username" :calendarSyncUrls="$props.calendarSyncUrls"></calendar-sync>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1,30 +1,30 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js"; import { CoreFilterCmpt } from "../../../components/filter/Filter.js";
import Mailverteiler from "./ProfilComponents/Mailverteiler.js"; import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
import QuickLinks from "./ProfilComponents/QuickLinks.js";
import RoleInformation from "./ProfilComponents/RoleInformation.js"; import RoleInformation from "./ProfilComponents/RoleInformation.js";
import ProfilEmails from "./ProfilComponents/ProfilEmails.js"; import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
import ProfilInformation from "./ProfilComponents/ProfilInformation.js"; import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
import QuickLinks from "./ProfilComponents/QuickLinks.js";
import { dateFilter } from '../../../tabulator/filters/Dates.js'; import { dateFilter } from "../../../tabulator/filters/Dates.js";
export default { export default {
components: { components: {
CoreFilterCmpt, CoreFilterCmpt,
Mailverteiler, Mailverteiler,
QuickLinks,
RoleInformation, RoleInformation,
ProfilEmails, ProfilEmails,
ProfilInformation, ProfilInformation,
QuickLinks,
}, },
inject: ["collapseFunction", "language"], inject: ["collapseFunction", "language"],
data() { data() {
return { return {
collapseIconFunktionen: true, collapseIconFunktionen: true,
preloadedPhrasen:{}, preloadedPhrasen: {},
funktionen_table_options: { funktionen_table_options: {
persistenceID: "filterTableMaViewProfilFunktionen", persistenceID: "filterTableMaViewProfilFunktionen",
persistence: { persistence: {
columns: false columns: false,
}, },
minHeight: 300, minHeight: 300,
layout: "fitColumns", layout: "fitColumns",
@@ -35,58 +35,65 @@ export default {
//? option when wanting to hide the collapsed list //? option when wanting to hide the collapsed list
{ {
title: title: "<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
"<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
field: "collapse", field: "collapse",
headerSort: false, headerSort: false,
headerFilter: false, headerFilter: false,
formatter: "responsiveCollapse", formatter: "responsiveCollapse",
maxWidth: 40, maxWidth: 40,
headerClick: this.collapseFunction, headerClick: this.collapseFunction,
visible: true visible: true,
}, },
{ {
title: Vue.computed(() => this.$p.t('ui/bezeichnung')), title: Vue.computed(() => this.$p.t("ui/bezeichnung")),
field: "Bezeichnung", field: "Bezeichnung",
headerFilter: true, headerFilter: true,
minWidth: 200, minWidth: 200,
visible: true visible: true,
}, },
{ {
title: Vue.computed(() => this.$p.t('lehre/organisationseinheit')), title: Vue.computed(() =>
this.$p.t("lehre/organisationseinheit"),
),
field: "Organisationseinheit", field: "Organisationseinheit",
headerFilter: true, headerFilter: true,
minWidth: 200, minWidth: 200,
visible: true visible: true,
}, },
{ {
title: Vue.computed(() => this.$p.t('global/gueltigVon')), title: Vue.computed(() =>
this.$p.t("global/gueltigVon"),
),
field: "Gültig_von", field: "Gültig_von",
headerFilterFunc: 'dates', headerFilterFunc: "dates",
headerFilter: dateFilter, headerFilter: dateFilter,
resizable: true, resizable: true,
minWidth: 200, minWidth: 200,
visible: true, visible: true,
formatter:"datetime", formatter: "datetime",
formatterParams: this.datetimeFormatterParams() formatterParams: this.datetimeFormatterParams(),
}, },
{ {
title: Vue.computed(() => this.$p.t('global/gueltigBis')), title: Vue.computed(() =>
this.$p.t("global/gueltigBis"),
),
field: "Gültig_bis", field: "Gültig_bis",
headerFilterFunc: 'dates', headerFilterFunc: "dates",
headerFilter: dateFilter, headerFilter: dateFilter,
resizable: true, resizable: true,
minWidth: 200, minWidth: 200,
visible: true, visible: true,
formatter:"datetime", formatter: "datetime",
formatterParams: this.datetimeFormatterParams() formatterParams: this.datetimeFormatterParams(),
}, },
{ {
title: Vue.computed(() => this.$p.t('profil/wochenstunden')), title: Vue.computed(() =>
this.$p.t("profil/wochenstunden"),
),
field: "Wochenstunden", field: "Wochenstunden",
headerFilter: true, headerFilter: true,
minWidth: 200, minWidth: 200,
visible: true visible: true,
}, },
], ],
}, },
@@ -94,47 +101,56 @@ export default {
}, },
//? this is the prop passed to the dynamic component with the custom data of the view //? this is the prop passed to the dynamic component with the custom data of the view
props: ["data"], props: ["data", "permissions"],
methods: { methods: {
funktionenTableBuilt: function () { funktionenTableBuilt: function () {
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen); this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
}, },
datetimeFormatterParams: function() { datetimeFormatterParams: function () {
const params = { const params = {
inputFormat:"yyyy-MM-dd", inputFormat: "yyyy-MM-dd",
outputFormat:"dd.MM.yyyy", outputFormat: "dd.MM.yyyy",
invalidPlaceholder:"(invalid date)", invalidPlaceholder: "(invalid date)",
timezone:FHC_JS_DATA_STORAGE_OBJECT.timezone timezone: FHC_JS_DATA_STORAGE_OBJECT.timezone,
}; };
return params; return params;
} },
}, },
watch: { watch: {
'data.funktionen'(newVal) { "data.funktionen"(newVal) {
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setData(newVal); if (this.$refs.funktionenTable)
this.$refs.funktionenTable.tabulator.setData(newVal);
},
"language.value"(newVal) {
// reevaluates computed phrasen
if (this.$refs.funktionenTable)
this.$refs.funktionenTable.tabulator.setColumns(
this.funktionen_table_options.columns,
);
}, },
'language.value'(newVal) { // reevaluates computed phrasen
if(this.$refs.funktionenTable) this.$refs.funktionenTable.tabulator.setColumns(this.funktionen_table_options.columns)
}
}, },
computed: { computed: {
getTelefonValue() { getTelefonValue() {
if(this.data.standort_telefon?.kontakt) { if (this.data.standort_telefon?.kontakt) {
return this.data.standort_telefon.kontakt + " " + this.data.telefonklappe return (
} else if(this.data.standort_telefon) { this.data.standort_telefon.kontakt +
return this.data.standort_telefon + " " + this.data.telefonklappe " " +
this.data.telefonklappe
);
} else if (this.data.standort_telefon) {
return (
this.data.standort_telefon + " " + this.data.telefonklappe
);
} else { } else {
return this.data.telefonklappe return this.data.telefonklappe;
} }
}, },
fotoStatus() { fotoStatus() {
return this.data?.fotoStatus ?? null; return this.data?.fotoStatus ?? null;
}, },
personEmails() { personEmails() {
return this.data?.emails ? this.data.emails : []; return this.data?.emails ? this.data.emails : [];
}, },
profilInformation() { profilInformation() {
if (!this.data) { if (!this.data) {
return {}; return {};
@@ -151,42 +167,67 @@ export default {
foto: this.data.foto, foto: this.data.foto,
}; };
}, },
roleInformation() { roleInformation() {
if (!this.data) { if (!this.data) {
return {}; return {};
} }
return { return {
geburtsdatum: { geburtsdatum: {
label: `${this.$p.t('profil','Geburtsdatum')}`, label: `${this.$p.t("profil", "Geburtsdatum")}`,
value: this.data.gebdatum value: this.data.gebdatum,
}, },
geburtsort: { geburtsort: {
label: `${this.$p.t('profil','Geburtsort')}`, label: `${this.$p.t("profil", "Geburtsort")}`,
value: this.data.gebort value: this.data.gebort,
}, },
personenkennzeichen: { personenkennzeichen: {
label: `${this.$p.t('profil','Kurzzeichen')}`, label: `${this.$p.t("profil", "Kurzzeichen")}`,
value: this.data.kurzbz value: this.data.kurzbz,
}, },
telefon: { telefon: {
label: `${this.$p.t('profil','Telefon')}`, label: `${this.$p.t("profil", "Telefon")}`,
value: this.getTelefonValue value: this.getTelefonValue,
}, },
office: { office: {
label: `${this.$p.t('profil','Büro')}`, label: `${this.$p.t("profil", "Büro")}`,
value: this.data.ort_kurzbz value: this.data.ort_kurzbz,
} },
}; };
}, },
quickLinks() {
let quickLinks = [];
if (
this.$props.permissions &&
this.$props.permissions["basis/other_lv_plan"]
) {
quickLinks.push({
icon: "fa-calendar-days",
phrase: "lehre/stundenplan",
action: () => {
this.$router.push({
name: "OtherLvPlan",
params: { otherUid: this.$props.data.username },
});
},
});
}
return quickLinks;
},
}, },
created(){ created() {
this.$p.loadCategory(["ui", "lehre", "global", "profil"]).then(() => { this.$p.loadCategory(["ui", "lehre", "global", "profil"]).then(() => {
this.preloadedPhrasen.bezeichnungPhrase = this.$p.t('ui/bezeichnung'); this.preloadedPhrasen.bezeichnungPhrase =
this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t('lehre/organisationseinheit'); this.$p.t("ui/bezeichnung");
this.preloadedPhrasen.gueltigVonPhrase = this.$p.t('global/gueltigVon'); this.preloadedPhrasen.organisationseinheitPhrase = this.$p.t(
this.preloadedPhrasen.gueltigBisPhrase = this.$p.t('global/gueltigBis'); "lehre/organisationseinheit",
this.preloadedPhrasen.wochenstundenPhrase = this.$p.t('profil/wochenstunden'); );
this.preloadedPhrasen.gueltigVonPhrase =
this.$p.t("global/gueltigVon");
this.preloadedPhrasen.gueltigBisPhrase =
this.$p.t("global/gueltigBis");
this.preloadedPhrasen.wochenstundenPhrase = this.$p.t(
"profil/wochenstunden",
);
this.preloadedPhrasen.loaded = true; this.preloadedPhrasen.loaded = true;
}); });
}, },
@@ -242,7 +283,7 @@ export default {
</div> </div>
<!-- START OF THE SECOND PROFIL INFORMATION ROW --> <!-- START OF THE SECOND PROFIL INFORMATION ROW -->
<!-- ROW WITH PROFIL IMAGE AND INFORMATION END --> <!-- ROW WITH PROFIL IMAGE AND INFORMATION END -->
</div > </div>
<!-- SECOND ROW UNDER THE PROFIL IMAGE AND INFORMATION WITH THE TABLES --> <!-- SECOND ROW UNDER THE PROFIL IMAGE AND INFORMATION WITH THE TABLES -->
<div class="row"> <div class="row">
<!-- FIRST TABLE --> <!-- FIRST TABLE -->
@@ -256,26 +297,22 @@ export default {
<!-- START OF SIDE PANEL --> <!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" > <div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!-- VISIBLE UNTIL VIEWPORT MD --> <!-- VISIBLE UNTIL VIEWPORT MD -->
<!--TODO: uncomment when implemented <div v-if="quickLinks.length" class="row mb-4">
<div class="row d-none d-md-block mb-3"> <div class="col">
<div class="col"> <quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
</div>
<quick-links :title="$p.t('profil','quickLinks')" ></quick-links> </div>
<div class="row">
</div> <div class="col">
</div> <!-- MAILVERTEILER -->
--> <mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
<div class="row"> </div>
<div class="col"> </div>
<!-- MAILVERTEILER --> <!-- END OF SIDE PANEL -->
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler> </div>
</div>
</div>
<!-- END OF SIDE PANEL -->
</div>
<!-- END OF CONTAINER ROW--> <!-- END OF CONTAINER ROW-->
</div> </div>
<!-- END OF CONTAINER --> <!-- END OF CONTAINER -->
</div> </div>
`, `,
}; };
+84 -60
View File
@@ -4,8 +4,8 @@ import ViewStudentProfil from "./StudentViewProfil.js";
import ViewMitarbeiterProfil from "./MitarbeiterViewProfil.js"; import ViewMitarbeiterProfil from "./MitarbeiterViewProfil.js";
import Loading from "../../Loader.js"; import Loading from "../../Loader.js";
import ApiProfil from '../../../api/factory/profil.js'; import ApiProfil from "../../../api/factory/profil.js";
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js'; import ApiProfilUpdate from "../../../api/factory/profilUpdate.js";
Vue.$collapseFormatter = function (data) { Vue.$collapseFormatter = function (data) {
//data - an array of objects containing the column title and value for each cell //data - an array of objects containing the column title and value for each cell
@@ -35,7 +35,7 @@ Vue.$collapseFormatter = function (data) {
}; };
export const Profil = { export const Profil = {
name: 'Profil', name: "Profil",
components: { components: {
StudentProfil, StudentProfil,
MitarbeiterProfil, MitarbeiterProfil,
@@ -46,11 +46,8 @@ export const Profil = {
props: { props: {
uid: { uid: {
type: String, type: String,
required:false, required: false,
}, },
viewData: {
type: Object,
}
}, },
data() { data() {
return { return {
@@ -62,17 +59,19 @@ export const Profil = {
data: null, data: null,
// notfound is null by default, but contains an UID if no user exists with that UID // notfound is null by default, but contains an UID if no user exists with that UID
notFoundUID: null, notFoundUID: null,
isEditable: this.viewData.editable ?? false, isEditable: false,
authPermissions: null,
calendarSyncUrls: [],
}; };
}, },
provide() { provide() {
return { return {
isEditable: Vue.computed(()=>this.isEditable), isEditable: Vue.computed(() => this.isEditable),
profilUpdateStates: Vue.computed(() => profilUpdateStates: Vue.computed(() =>
this.profilUpdateStates ? this.profilUpdateStates : false this.profilUpdateStates ? this.profilUpdateStates : false,
), ),
profilUpdateTopic: Vue.computed(() => profilUpdateTopic: Vue.computed(() =>
this.profilUpdateTopic ? this.profilUpdateTopic : false this.profilUpdateTopic ? this.profilUpdateTopic : false,
), ),
setLoading: (newValue) => { setLoading: (newValue) => {
this.loading = newValue; this.loading = newValue;
@@ -130,8 +129,12 @@ export const Profil = {
//? if they have the same status the insert date is used for ordering //? if they have the same status the insert date is used for ordering
if (ele1.status === ele2.status) { if (ele1.status === ele2.status) {
result = result =
new Date(ele2.insertamum.split(".").reverse().join("-")) - new Date(
new Date(ele1.insertamum.split(".").reverse().join("-")); ele2.insertamum.split(".").reverse().join("-"),
) -
new Date(
ele1.insertamum.split(".").reverse().join("-"),
);
} }
return result; return result;
}, },
@@ -139,6 +142,8 @@ export const Profil = {
}, },
methods: { methods: {
async load() { async load() {
await this.fetchViewData();
// fetch profilUpdateStates to provide them to children components // fetch profilUpdateStates to provide them to children components
await this.$api await this.$api
.call(ApiProfilUpdate.getStatus()) .call(ApiProfilUpdate.getStatus())
@@ -157,20 +162,20 @@ export const Profil = {
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
}); });
},
async fetchViewData() {
this.$api let viewDataResult = await this.$api.call(
.call(ApiProfil.profilViewData(this.$route.params.uid??null)) ApiProfil.getProfilViewData(this.$route.params.uid ?? null),
.then((response) => response.data).then(data=>{ );
this.view = data?.profil_data.view;
this.data = data?.profil_data.data; const data = viewDataResult.data;
this.isEditable = data?.editable ?? false; if (!data) return;
})
.catch((error) => { this.view = data.profil_data.view;
console.error(error); this.isEditable = data.profil_data.editable;
}); this.data = data.profil_data.data;
this.calendarSyncUrls = data.calendar_sync_urls ?? [];
this.authPermissions = data.permissions;
}, },
zustellAdressenCount() { zustellAdressenCount() {
if (!this.data || !this.data.adressen) { if (!this.data || !this.data.adressen) {
@@ -186,7 +191,7 @@ export const Profil = {
}) })
.map((adresse) => { .map((adresse) => {
return adresse.requested_change.adresse_id; return adresse.requested_change.adresse_id;
}) }),
); );
} }
@@ -197,8 +202,9 @@ export const Profil = {
.every((adresse) => .every((adresse) =>
this.data.profilUpdates.some( this.data.profilUpdates.some(
(update) => (update) =>
update.requested_change.adresse_id == adresse.adresse_id update.requested_change.adresse_id ==
) adresse.adresse_id,
),
) )
) { ) {
adressenArray = adressenArray.concat( adressenArray = adressenArray.concat(
@@ -208,12 +214,11 @@ export const Profil = {
}) })
.map((adr) => { .map((adr) => {
return adr.adresse_id; return adr.adresse_id;
}) }),
); );
} }
return [...new Set(adressenArray)]; return [...new Set(adressenArray)];
}, },
zustellKontakteCount() { zustellKontakteCount() {
if (!this.data || !this.data.kontakte) { if (!this.data || !this.data.kontakte) {
@@ -226,14 +231,17 @@ export const Profil = {
kontakteArray = kontakteArray.concat( kontakteArray = kontakteArray.concat(
this.data.profilUpdates this.data.profilUpdates
.filter((update) => { .filter((update) => {
return update.status === 'Pending' && update.requested_change.zustellung; return (
update.status === "Pending" &&
update.requested_change.zustellung
);
}) })
.map((kontant) => { .map((kontant) => {
return { return {
kontakt_id: kontant.requested_change.kontakt_id, kontakt_id: kontant.requested_change.kontakt_id,
kontakttyp: kontant.requested_change.kontakttyp kontakttyp: kontant.requested_change.kontakttyp,
}; };
}) }),
); );
} }
@@ -244,8 +252,10 @@ export const Profil = {
.every((kontakt) => .every((kontakt) =>
this.data.profilUpdates.some( this.data.profilUpdates.some(
(update) => (update) =>
update.status === 'Pending' && update.requested_change.kontakt_id == kontakt.kontakt_id update.status === "Pending" &&
) update.requested_change.kontakt_id ==
kontakt.kontakt_id,
),
) )
) { ) {
kontakteArray = kontakteArray.concat( kontakteArray = kontakteArray.concat(
@@ -255,10 +265,10 @@ export const Profil = {
}) })
.map((kon) => { .map((kon) => {
return { return {
kontakt_id: kon.kontakt_id, kontakt_id: kon.kontakt_id,
kontakttyp: kon.kontakttyp kontakttyp: kon.kontakttyp,
}; };
}) }),
); );
} }
@@ -266,7 +276,6 @@ export const Profil = {
}, },
}, },
computed: { computed: {
filteredEditData() { filteredEditData() {
if (!this.data) { if (!this.data) {
return; return;
@@ -330,8 +339,12 @@ export const Profil = {
// excludes all contacts that are already used in pending profil update requests // excludes all contacts that are already used in pending profil update requests
return !this.data.profilUpdates?.some( return !this.data.profilUpdates?.some(
(update) => (update) =>
update.status === this.profilUpdateStates["Pending"] && update.status ===
update.requested_change?.kontakt_id === item.kontakt_id this.profilUpdateStates[
"Pending"
] &&
update.requested_change?.kontakt_id ===
item.kontakt_id,
); );
}) })
.map((kontakt) => { .map((kontakt) => {
@@ -347,12 +360,18 @@ export const Profil = {
topic: this.profilUpdateTopic?.["Private Adressen"], topic: this.profilUpdateTopic?.["Private Adressen"],
data: this.data.adressen data: this.data.adressen
?.filter((item) => { ?.filter((item) => {
return !this.data.profilUpdates?.some((update) => { return !this.data.profilUpdates?.some(
return ( (update) => {
update.status === this.profilUpdateStates["Pending"] && return (
update.requested_change?.adresse_id == item.adresse_id update.status ===
); this.profilUpdateStates[
}); "Pending"
] &&
update.requested_change
?.adresse_id == item.adresse_id
);
},
);
}) })
.map((adresse) => { .map((adresse) => {
return { return {
@@ -374,23 +393,28 @@ export const Profil = {
this.$refs.loadingModalRef.hide(); this.$refs.loadingModalRef.hide();
} }
}, },
uid (newVal, oldVal) { uid(newVal, oldVal) {
this.load() this.load();
} },
}, },
created() { created() {
this.load() this.load();
}, },
template: ` template: `
<div> <div class="pb-4">
<div v-if="notFoundUID"> <div v-if="notFoundUID">
<h3>Es wurde keine Person mit der UID {{this.notFoundUID}} gefunden</h3> <h3>Es wurde keine Person mit der UID {{this.notFoundUID}} gefunden</h3>
</div> </div>
<div v-else> <div v-else>
<loading ref="loadingModalRef" :timeout="0"></loading> <loading ref="loadingModalRef" :timeout="0"></loading>
<component :is="view" :data="data" :editData="filteredEditData" ></component> <component
:is="view"
:data="data"
:editData="filteredEditData"
:permissions="authPermissions"
:calendarSyncUrls="calendarSyncUrls"></component>
</div> </div>
</div>`, </div>`,
} };
export default Profil export default Profil;
@@ -0,0 +1,54 @@
export default {
name: "CalendarSync",
props: { uid: String, calendarSyncUrls: Array },
data() {
return {
syncInstructionsUrlWithoutParam:
FHC_JS_DATA_STORAGE_OBJECT.app_root +
"cms/content.php?content_id=",
};
},
methods: {
copyUrlToClipboard(url) {
navigator.clipboard.writeText(url);
this.$fhcAlert.alertSuccess(
this.$p.t("profil/calendar_sync_clipboard_copy_confirmation"),
);
},
},
template: `
<div class="card">
<div class="card-header">
{{ $p.t("profil/calendar_sync") }}
</div>
<div class="card-body">
<div class="d-flex flex-column gap-3">
<a
target="_blank"
:href="syncInstructionsUrlWithoutParam + $p.t('DMS-Link/lvplanSyncFAQ')"
class="fhc-link-color d-flex flex-row gap-2 align-items-center"
>
<span>
<i class="fa-solid fa-up-right-from-square ms-2"></i>
</span>
<span>
{{ $p.t("profil/calendar_sync_instructions") }}
</span>
</a>
<a
v-for="syncUrl in $props.calendarSyncUrls"
:key="syncUrl.identifier"
@click.prevent="copyUrlToClipboard(syncUrl.url)"
@contextmenu.prevent="copyUrlToClipboard(syncUrl.url)"
href="#"
class="fhc-link-color d-flex flex-row gap-2 align-items-center"
>
<span>
<i class="fa-regular fa-copy ms-2 text-decoration-none"></i>
</span>
{{ $p.t(syncUrl.labelPhrase) }}
</a>
</div>
</div>
</div>`,
};
@@ -1,53 +1,31 @@
export default { export default {
//TODO: To be implemented name: "QuickLinks",
props: { data() {
data: { return {};
type: String, },
}, props: {
title: { title: {
type: String, type: String,
required: true, required: true,
}, },
mobile: { links: {
type: Boolean, type: Array,
default: false, required: true,
}, },
}, },
methods: { template: `
hideCollapse: function () {
this.collapseOpen = false;
},
showCollapse: function () {
this.collapseOpen = true;
},
},
data() {
return {
collapseOpen: false,
};
},
template: /*html*/ `
<div class="card"> <div class="card">
<template v-if="mobile"> <div class="card-header">
<button class="btn btn-outline-primary" data-bs-toggle="collapse" data-bs-target="#quickLinks" :aria-expanded="collapseOpen" aria-controls="quickLinks" > {{title}}
{{title}} </div>
<i class="fa " :class="collapseOpen?'fa-chevron-up':'fa-chevron-down'"></i> <div class="card-body">
</button> <div class="d-flex flex-row align-items-center gap-3 flex-wrap">
<div @[\`show.bs.collapse\`]="collapseOpen=true;" @[\`hide.bs.collapse\`]="collapseOpen=false;" class="mt-1 collapse" id="quickLinks"> <div v-for="link in links" @click="link.action()" type="button" class="d-flex flex-row gap-2 px-3 py-2 border fhc-primary">
<div class="list-group"> <div><i class="fa" :class="link.icon"></i></div>
<a href="#" class="list-group-item list-group-item-action">{{$p.t('profil','zeitwuensche')}}</a> {{ $p.t(link.phrase) }}
<a href="#" class="list-group-item list-group-item-action">{{$p.t('profil','lehrveranstaltungen')}}</a> <div><i class="fa fa-arrow-up-right-from-square" style="color:var(--fhc-light) !important"></i></div>
<a href="#" class="list-group-item list-group-item-action ">{{$p.t('profil','zeitsperren')}}</a>
</div> </div>
</div> </div>
</template> </div>
<template v-else>
<div class="card-header">{{title}}</div>
<div class="card-body">
<a style="text-decoration:none" class="my-1 d-block" href="#">{{$p.t('profil','zeitwuensche')}}</a>
<a style="text-decoration:none" class="my-1 d-block" href="#">{{$p.t('profil','lehrveranstaltungen')}}</a>
<a style="text-decoration:none" class="my-1 d-block" href="#">{{$p.t('profil','zeitsperren')}}</a>
</div>
</template>
</div>`, </div>`,
}; };
@@ -1,7 +1,6 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js"; import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import Mailverteiler from "./ProfilComponents/Mailverteiler.js"; import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
import AusweisStatus from "./ProfilComponents/FhAusweisStatus.js"; import AusweisStatus from "./ProfilComponents/FhAusweisStatus.js";
import QuickLinks from "./ProfilComponents/QuickLinks.js";
import Adresse from "./ProfilComponents/Adresse.js"; import Adresse from "./ProfilComponents/Adresse.js";
import Kontakt from "./ProfilComponents/Kontakt.js"; import Kontakt from "./ProfilComponents/Kontakt.js";
import ProfilEmails from "./ProfilComponents/ProfilEmails.js"; import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
@@ -9,6 +8,8 @@ import RoleInformation from "./ProfilComponents/RoleInformation.js";
import ProfilInformation from "./ProfilComponents/ProfilInformation.js"; import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
import FetchProfilUpdates from "./ProfilComponents/FetchProfilUpdates.js"; import FetchProfilUpdates from "./ProfilComponents/FetchProfilUpdates.js";
import EditProfil from "./ProfilModal/EditProfil.js"; import EditProfil from "./ProfilModal/EditProfil.js";
import QuickLinks from "./ProfilComponents/QuickLinks.js";
import CalendarSync from "./ProfilComponents/CalendarSync.js";
import ApiProfilUpdate from '../../../api/factory/profilUpdate.js'; import ApiProfilUpdate from '../../../api/factory/profilUpdate.js';
import { dateFilter } from '../../../tabulator/filters/Dates.js'; import { dateFilter } from '../../../tabulator/filters/Dates.js';
@@ -18,7 +19,6 @@ export default {
CoreFilterCmpt, CoreFilterCmpt,
Mailverteiler, Mailverteiler,
AusweisStatus, AusweisStatus,
QuickLinks,
Adresse, Adresse,
Kontakt, Kontakt,
ProfilEmails, ProfilEmails,
@@ -26,6 +26,8 @@ export default {
ProfilInformation, ProfilInformation,
FetchProfilUpdates, FetchProfilUpdates,
EditProfil, EditProfil,
QuickLinks,
CalendarSync,
}, },
inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"], inject: ["sortProfilUpdates", "collapseFunction", "language","isEditable"],
data() { data() {
@@ -101,6 +103,7 @@ export default {
props: { props: {
data: Object, data: Object,
editData: Object, editData: Object,
calendarSyncUrls: Array,
}, },
provide() { provide() {
return { return {
@@ -240,6 +243,11 @@ export default {
} }
}; };
}, },
quickLinks() {
let quickLinks = [];
//
return quickLinks;
},
}, },
created() { created() {
// preload phrasen // preload phrasen
@@ -265,15 +273,7 @@ export default {
:value="JSON.parse(JSON.stringify(filteredEditData))" :titel="$p.t('profil','profilBearbeiten')"></edit-profil> :value="JSON.parse(JSON.stringify(filteredEditData))" :titel="$p.t('profil','profilBearbeiten')"></edit-profil>
<!-- ROW --> <!-- ROW -->
<div class="row"> <div class="row">
<!-- HIDDEN QUICK LINKS -->
<div class="d-md-none col-12 "> <div class="d-md-none col-12 ">
<!--TODO: uncomment when implemented
<div class="row py-2">
<div class="col">
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
</div>
</div>-->
<!-- Bearbeiten Button --> <!-- Bearbeiten Button -->
<div v-if="isEditable" class="row "> <div v-if="isEditable" class="row ">
<div class="col mb-3"> <div class="col mb-3">
@@ -403,12 +403,11 @@ export default {
</div> </div>
<!-- START OF SIDE PANEL --> <!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" > <div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!--TODO: uncomment when implemented <div v-if="quickLinks.length" class="row mb-4">
<div class="row d-none d-md-block mb-3">
<div class="col"> <div class="col">
<quick-links :title="$p.t('profil','quickLinks')"></quick-links> <quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
</div> </div>
</div>--> </div>
<!-- Bearbeiten Button --> <!-- Bearbeiten Button -->
<div class="row d-none d-md-block"> <div class="row d-none d-md-block">
<div class="col mb-3"> <div class="col mb-3">
@@ -434,13 +433,18 @@ export default {
</div> </div>
</div> </div>
<!-- START OF THE SECOND ROW IN THE SIDE PANEL --> <!-- START OF THE SECOND ROW IN THE SIDE PANEL -->
<div class="row"> <div class="row mb-3">
<div class="col"> <div class="col">
<!-- HIER SIND DIE MAILVERTEILER --> <!-- HIER SIND DIE MAILVERTEILER -->
<mailverteiler :title="$p.t('profil','mailverteiler')" :data="data?.mailverteiler"></mailverteiler> <mailverteiler :title="$p.t('profil','mailverteiler')" :data="data?.mailverteiler"></mailverteiler>
</div> </div>
<!-- END OF THE SECOND ROW IN THE SIDE PANEL --> <!-- END OF THE SECOND ROW IN THE SIDE PANEL -->
</div> </div>
<div class="row">
<div class="col">
<calendar-sync :uid="$props.data.username" :calendarSyncUrls="$props.calendarSyncUrls"></calendar-sync>
</div>
</div>
<!-- END OF SIDE PANEL --> <!-- END OF SIDE PANEL -->
</div> </div>
<!-- END OF CONTAINER ROW--> <!-- END OF CONTAINER ROW-->
@@ -1,30 +1,28 @@
import QuickLinks from "./ProfilComponents/QuickLinks.js";
import Mailverteiler from "./ProfilComponents/Mailverteiler.js"; import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
import ProfilEmails from "./ProfilComponents/ProfilEmails.js"; import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
import RoleInformation from "./ProfilComponents/RoleInformation.js"; import RoleInformation from "./ProfilComponents/RoleInformation.js";
import ProfilInformation from "./ProfilComponents/ProfilInformation.js"; import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
import QuickLinks from "./ProfilComponents/QuickLinks.js";
export default { export default {
data() {
return {};
},
components: { components: {
QuickLinks,
Mailverteiler, Mailverteiler,
ProfilEmails, ProfilEmails,
RoleInformation, RoleInformation,
ProfilInformation, ProfilInformation,
QuickLinks,
},
props: ["data", "permissions"],
data() {
return {};
}, },
props: ["data"],
provide() { provide() {
return { return {
studiengang_kz: Vue.computed({ get: () => this.data.studiengang_kz }), studiengang_kz: Vue.computed({
} get: () => this.data.studiengang_kz,
}),
};
}, },
methods: {},
computed: { computed: {
fotoStatus() { fotoStatus() {
return this.data?.fotoStatus ?? null; return this.data?.fotoStatus ?? null;
@@ -45,66 +43,67 @@ export default {
foto: this.data.foto, foto: this.data.foto,
}; };
}, },
personEmails() { personEmails() {
return this.data?.emails ? this.data.emails : []; return this.data?.emails ? this.data.emails : [];
}, },
roleInformation() { roleInformation() {
if (!this.data) { if (!this.data) {
return {}; return {};
} }
return { return {
geburtsdatum: { geburtsdatum: {
label: `${this.$p.t('profil','Geburtsdatum')}`, label: `${this.$p.t("profil", "Geburtsdatum")}`,
value: this.data.gebdatum value: this.data.gebdatum,
}, },
geburtsort: { geburtsort: {
label: `${this.$p.t('profil','Geburtsort')}`, label: `${this.$p.t("profil", "Geburtsort")}`,
value: this.data.gebort value: this.data.gebort,
}, },
personenkennzeichen: { personenkennzeichen: {
label: `${this.$p.t('person','personenkennzeichen')}`, label: `${this.$p.t("person", "personenkennzeichen")}`,
value: this.data.personenkennzeichen value: this.data.personenkennzeichen,
}, },
studiengang: { studiengang: {
label: `${this.$p.t('lehre','studiengang')}`, label: `${this.$p.t("lehre", "studiengang")}`,
value: this.data.studiengang value: this.data.studiengang,
}, },
semester: { semester: {
label: `${this.$p.t('lehre','semester')}`, label: `${this.$p.t("lehre", "semester")}`,
value: this.data.semester value: this.data.semester,
}, },
verband: { verband: {
label: `${this.$p.t('lehre','lehrverband')}`, label: `${this.$p.t("lehre", "lehrverband")}`,
value: this.data.verband value: this.data.verband,
}, },
gruppe: { gruppe: {
label: `${this.$p.t('lehre','gruppe')}`, label: `${this.$p.t("lehre", "gruppe")}`,
value: this.data.gruppe.trim() value: this.data.gruppe.trim(),
} },
}; };
}, },
quickLinks() {
let quickLinks = [];
if (this.$props.permissions && this.$props.permissions["basis/other_lv_plan"]) {
quickLinks.push({
icon: "fa-calendar-days",
phrase: "lehre/stundenplan",
action: () => {
this.$router.push({
name: "OtherLvPlan",
params: { otherUid: this.$props.data.username },
});
},
});
}
return quickLinks;
},
}, },
mounted() {
},
template: /*html*/ ` template: /*html*/ `
<div class="container-fluid text-break fhc-form" > <div class="container-fluid text-break fhc-form" >
<!-- ROW --> <!-- ROW -->
<div class="row"> <div class="row">
<!-- HIDDEN QUICK LINKS -->
<!-- uncomment when implemented
<div class="d-md-none col-12 ">
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
</div>-->
<!-- END OF HIDDEN QUCK LINKS -->
<!-- MAIN PANNEL --> <!-- MAIN PANNEL -->
<div class="col-sm-12 col-md-8 col-xxl-9 "> <div class="col-sm-12 col-md-8 col-xxl-9 ">
<!-- ROW WITH PROFIL IMAGE AND INFORMATION --> <!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
@@ -112,12 +111,18 @@ export default {
<!-- ROW WITH THE PROFIL INFORMATION --> <!-- ROW WITH THE PROFIL INFORMATION -->
<div class="row mb-4"> <div class="row mb-4">
<!-- FIRST KAESTCHEN --> <!-- FIRST KAESTCHEN -->
<div class="col-lg-12 col-xl-6 "> <div class="col-lg-12 col-xl-6 ">
<div class="row mb-4"> <div class="row mb-4">
<div class="col"> <div class="col">
<profil-information :data="profilInformation" :title="$p.t('profil','studentIn')" :fotoStatus="fotoStatus"></profil-information> <profil-information :data="profilInformation" :title="$p.t('profil','studentIn')" :fotoStatus="fotoStatus"></profil-information>
</div> </div>
</div> </div>
<!-- SECOND ROW OF FIRST COLUMN -->
<div class="row mb-4">
<div class="col">
</div>
</div>
<!-- START OF SECOND PROFIL INFORMATION COLUMN --> <!-- START OF SECOND PROFIL INFORMATION COLUMN -->
<!-- END OF PROFIL INFORMATION ROW --> <!-- END OF PROFIL INFORMATION ROW -->
<!-- INFORMATION CONTENT END --> <!-- INFORMATION CONTENT END -->
@@ -145,17 +150,12 @@ export default {
</div> </div>
<!-- START OF SIDE PANEL --> <!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" > <div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!-- SRART OF QUICK LINKS IN THE SIDE PANEL --> <!-- START OF THE FIRST ROW IN THE SIDE PANEL -->
<!-- START OF THE FIRDT ROW IN THE SIDE PANEL --> <div v-if="quickLinks.length" class="row mb-4">
<!-- THESE QUCK LINKS ARE ONLY VISIBLE UNTIL VIEWPORT MD --> <div class="col">
<!--TODO: uncomment when implemented <quick-links :title="$p.t('profil/quickLinks')" :links="quickLinks" />
<div class="row d-none d-md-block mb-3"> </div>
<div class="col"> </div>
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
</div>
</div>-->
<!-- START OF THE SECOND ROW IN THE SIDE PANEL --> <!-- START OF THE SECOND ROW IN THE SIDE PANEL -->
<div class="row"> <div class="row">
<div class="col"> <div class="col">
@@ -3,14 +3,12 @@ import VueDatePicker from '../../vueDatepicker.js.php';
import ApiOrt from '../../../api/factory/ort.js' import ApiOrt from '../../../api/factory/ort.js'
export const Raumsuche = { export const Raumsuche = {
name: "Raumsuche", name: "Raumsuche",
props: {
},
components: { components: {
VueDatePicker, VueDatePicker,
CoreFilterCmpt, CoreFilterCmpt,
InputNumber: primevue.inputnumber, InputNumber: primevue.inputnumber,
}, },
inject: ["isMobile"],
data() { data() {
return { return {
phrasenPromise: null, phrasenPromise: null,
@@ -194,8 +192,9 @@ export const Raumsuche = {
<h1 class="h3">{{$p.t('rauminfo/roomSearch')}}</h1> <h1 class="h3">{{$p.t('rauminfo/roomSearch')}}</h1>
<hr> <hr>
<div class="row"> <div class="row">
<div class="col-12 col-lg-2"> <div :class="{'pb-1': isMobile}" class="col-12 col-lg-2">
<VueDatePicker <VueDatePicker
@contextmenu="(e) => {if (isMobile) {e.preventDefault();}}"
:dark="isDarkMode" :dark="isDarkMode"
v-model="datum" v-model="datum"
:clearable="false" :clearable="false"
@@ -207,8 +206,9 @@ export const Raumsuche = {
auto-apply> auto-apply>
</VueDatePicker> </VueDatePicker>
</div> </div>
<div class="col-12 col-lg-1"> <div :class="{'pb-1': isMobile}" class="col-12 col-lg-1">
<VueDatePicker <VueDatePicker
@contextmenu="(e) => {if (isMobile) {e.preventDefault();}}"
:dark="isDarkMode" :dark="isDarkMode"
v-model="von" v-model="von"
:clearable="false" :clearable="false"
@@ -220,8 +220,9 @@ export const Raumsuche = {
> >
</VueDatePicker> </VueDatePicker>
</div> </div>
<div class="col-12 col-lg-1"> <div :class="{'pb-1': isMobile}" class="col-12 col-lg-1">
<VueDatePicker <VueDatePicker
@contextmenu="(e) => {if (isMobile) {e.preventDefault();}}"
:dark="isDarkMode" :dark="isDarkMode"
v-model="bis" v-model="bis"
:clearable="false" :clearable="false"
@@ -233,7 +234,7 @@ export const Raumsuche = {
</VueDatePicker> </VueDatePicker>
</div> </div>
<div class="col-12 col-lg-3"> <div :class="{'pb-1': isMobile}" class="col-12 col-lg-3">
<select ref="raumtyp" id="raumtypSelect" v-model="selectedType" class="form-select" <select ref="raumtyp" id="raumtypSelect" v-model="selectedType" class="form-select"
:aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setRoute($event.target.value)"> :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setRoute($event.target.value)">
<option :key="defaultType" selected :value="defaultType">{{defaultType.beschreibung}}</option> <option :key="defaultType" selected :value="defaultType">{{defaultType.beschreibung}}</option>
@@ -242,7 +243,7 @@ export const Raumsuche = {
</div> </div>
<div class="col-12 col-lg-3"> <div :class="{'pb-2': isMobile}" class="col-12 col-lg-3">
<InputNumber v-model="anzahl" <InputNumber v-model="anzahl"
:prefix="$p.t('rauminfo/minCapacity') + ': '" :prefix="$p.t('rauminfo/minCapacity') + ': '"
inputId="anzahlInput" :min="1" :max="1000" inputId="anzahlInput" :min="1" :max="1000"
@@ -31,7 +31,7 @@ export default {
this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n") this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n")
+ "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3]) + "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3])
].join(": ")); ].join(": "));
} else {; } else {
tooltipArray.push([ tooltipArray.push([
this.$p.t('lehre/lektor'), this.$p.t('lehre/lektor'),
this.event.lektor.map(lektor => lektor.kurzbz).join("\n") this.event.lektor.map(lektor => lektor.kurzbz).join("\n")
@@ -142,7 +142,6 @@ export default {
</tr> </tr>
</tbody> </tbody>
</table> </table>
<lv-menu v-if="lvMenu.length && $route.name === 'MyLvPlan'" :containerStyles="['p-0']" :rowStyles="['m-0']" :menu="lvMenu" />
<lv-menu :containerStyles="['p-0']" :rowStyles="['m-0']" v-if="lvMenu.length" :menu="lvMenu" />
</div>`, </div>`,
} }
+67 -31
View File
@@ -1,5 +1,6 @@
import LvUebersicht from "../Mylv/LvUebersicht.js"; import LvUebersicht from "../Mylv/LvUebersicht.js";
import ApiCisStudium from '../../../api/factory/cis/studium.js';
export default { export default {
data(){ data(){
@@ -26,6 +27,7 @@ export default {
} }
}, },
name: "OverviewStudiengaenge",
components: { components: {
LvUebersicht, LvUebersicht,
}, },
@@ -88,6 +90,27 @@ export default {
studienordnung.selectedIndex = newSelectIndex; studienordnung.selectedIndex = newSelectIndex;
this.changeSelectedStudienPlan(studienordnung.value); this.changeSelectedStudienPlan(studienordnung.value);
}, },
onStudiensemesterChange(event) {
const value = event.target.value;
this.setHash(value);
this.changeSelectedStudienSemester(value);
},
onStudiengangChange(event) {
const value = event.target.value;
this.setHash(value);
this.changeSelectedStudienGang(value);
},
onSemesterChange(event) {
const value = event.target.value;
this.setHash(value);
this.changeSelectedSemester(value);
},
onStudienordnungChange(event) {
const value = event.target.value;
this.setHash(value);
this.changeSelectedStudienPlan(value);
},
storeDataToLocalStorage(key,value){ storeDataToLocalStorage(key,value){
localStorage.setItem(key, value); localStorage.setItem(key, value);
@@ -97,28 +120,32 @@ export default {
return value; return value;
}, },
changeSelectedStudienSemester(studiensemester_kurzbz) { changeSelectedStudienSemester(studiensemester_kurzbz) {
this.$fhcApi.factory.studium.getAllStudienSemester(studiensemester_kurzbz, this.selectedStudiengang, this.selectedSemester, this.selectedStudienordnung) return this.$api
.call(ApiCisStudium.getAllStudienSemester(studiensemester_kurzbz, this.selectedStudiengang, this.selectedSemester, this.selectedStudienordnung))
.then(data => data.data) .then(data => data.data)
.then(res => { .then(res => {
this.extractPropertyValues(res); this.extractPropertyValues(res);
}) })
}, },
changeSelectedStudienGang(studiengang_kz) { changeSelectedStudienGang(studiengang_kz) {
this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, studiengang_kz, this.selectedSemester, this.selectedStudienordnung) return this.$api
.call(ApiCisStudium.getAllStudienSemester(this.selectedStudiensemester, studiengang_kz, this.selectedSemester, this.selectedStudienordnung))
.then(data => data.data) .then(data => data.data)
.then(res => { .then(res => {
this.extractPropertyValues(res); this.extractPropertyValues(res);
}) })
}, },
changeSelectedSemester(semester) { changeSelectedSemester(semester) {
this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, semester, this.selectedStudienordnung) return this.$api
.call(ApiCisStudium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, semester, this.selectedStudienordnung))
.then(data => data.data) .then(data => data.data)
.then(res => { .then(res => {
this.extractPropertyValues(res); this.extractPropertyValues(res);
}) })
}, },
changeSelectedStudienPlan(studienplan_id) { changeSelectedStudienPlan(studienplan_id) {
this.$fhcApi.factory.studium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, this.selectedSemester, studienplan_id) return this.$api
.call(ApiCisStudium.getAllStudienSemester(this.selectedStudiensemester, this.selectedStudiengang, this.selectedSemester, studienplan_id))
.then(data => data.data) .then(data => data.data)
.then(res => { .then(res => {
this.extractPropertyValues(res); this.extractPropertyValues(res);
@@ -160,6 +187,7 @@ export default {
this.studiengaenge = studiengang.all; this.studiengaenge = studiengang.all;
this.selectedStudiengang = studiengang.preselected?.studiengang_kz; this.selectedStudiengang = studiengang.preselected?.studiengang_kz;
this.semester = semester.all; this.semester = semester.all;
this.selectedSemester = semester?.preselected; this.selectedSemester = semester?.preselected;
@@ -195,7 +223,11 @@ export default {
}, },
studiengangTitel(studiengang) { studiengangTitel(studiengang) {
if (!studiengang) return ""; if (!studiengang) return "";
return `${studiengang?.kurzbzlang} (${studiengang?.bezeichnung})`; if(this.isGermanLanguage){
return `${studiengang?.kurzbzlang} (${studiengang?.bezeichnung})`;
}else{
return `${studiengang?.kurzbzlang} (${studiengang?.english})`;
}
}, },
studiensemesterTitel(studiensemester){ studiensemesterTitel(studiensemester){
if (!studiensemester) return ""; if (!studiensemester) return "";
@@ -213,9 +245,12 @@ export default {
}, },
computed:{ computed:{
isGermanLanguage(){
return this.$p.user_language.value == "German"
},
selectedLehrveranstaltungTitel(){ selectedLehrveranstaltungTitel(){
const studiengang = this.studiengaenge.find((studiengang) => studiengang.studiengang_kz == this.selectedStudiengang); const studiengang = this.studiengaenge.find((studiengang) => studiengang.studiengang_kz == this.selectedStudiengang);
return `${this.selectedLehrveranstaltung?.bezeichnung} ${this.selectedLehrveranstaltung?.lehrform_kurzbz} / ${studiengang.kurzbzlang}-${this.selectedSemester} ${this.selectedLehrveranstaltung?.orgform_kurzbz} (${this.selectedStudiensemester})`; return `${this.isGermanLanguage ? this.selectedLehrveranstaltung?.bezeichnung : this.selectedLehrveranstaltung?.bezeichnung_english} ${this.selectedLehrveranstaltung?.lehrform_kurzbz} / ${studiengang.kurzbzlang}-${this.selectedSemester} ${this.selectedLehrveranstaltung?.orgform_kurzbz} (${this.selectedStudiensemester})`;
}, },
computedStudienOrdnung(){ computedStudienOrdnung(){
if(!this.studienOrdnung) return null; if(!this.studienOrdnung) return null;
@@ -232,14 +267,14 @@ export default {
let result = []; let result = [];
Object.entries(this.computedStudienOrdnung).forEach(([key,value])=>{ Object.entries(this.computedStudienOrdnung).forEach(([key,value])=>{
result.push({ result.push({
bezeichnung: `Studienordnung: ${key}`, bezeichnung: `${this.$p.t('studium', 'studienordnung') }: ${key}`,
disabled: true, disabled: true,
}); });
value.forEach((studienplan)=>{ value.forEach((studienplan)=>{
result.push({ result.push({
studienplan:studienplan, studienplan:studienplan,
diabled: false, diabled: false,
bezeichnung: `${studienplan?.bezeichnung}-${studienplan?.orgform_kurzbz} ( ${studienplan?.orgform_bezeichnung}, ${studienplan?.sprache} )` bezeichnung: `${studienplan?.bezeichnung}-${studienplan?.orgform_kurzbz} ( ${this.isGermanLanguage ? studienplan?.orgform_bezeichnung : studienplan?.orgform_bezeichnung_english}, ${studienplan?.sprache} )`
}); });
}) })
@@ -256,22 +291,23 @@ export default {
const studienordnung = JSON.parse(this.getDataFromLocalStorage("studienordnung")) ?? undefined; const studienordnung = JSON.parse(this.getDataFromLocalStorage("studienordnung")) ?? undefined;
// only fetch default data if no data is stored in the local storage // only fetch default data if no data is stored in the local storage
this.$fhcApi.factory.studium.getAllStudienSemester(studiensemester, studiengang, semester, studienordnung) this.$api
.then(data => data.data) .call(ApiCisStudium.getAllStudienSemester(studiensemester, studiengang, semester, studienordnung))
.then(res => { .then(data => data.data)
this.extractPropertyValues(res); .then(res => {
}) this.extractPropertyValues(res);
})
}, },
template: ` template: /*html*/`
<div> <div>
<h2>Studium</h2> <h2>{{$p.t('studium','studium')}}</h2>
<hr> <hr>
<lv-uebersicht ref="lvUebersicht" :titel="selectedLehrveranstaltungTitel" :event="selectedLehrveranstaltung" :studiensemester="selectedStudiensemester" v-if="selectedLehrveranstaltung"> <lv-uebersicht ref="lvUebersicht" :titel="selectedLehrveranstaltungTitel" :event="selectedLehrveranstaltung" :studiensemester="selectedStudiensemester" v-if="selectedLehrveranstaltung">
<template #content> <template #content>
<div v-if="Array.isArray(selectedLehrveranstaltung.lektoren) && selectedLehrveranstaltung.lektoren.length>0" class="mb-4"> <div v-if="Array.isArray(selectedLehrveranstaltung.lektoren) && selectedLehrveranstaltung.lektoren.length>0" class="mb-4">
<h4>Lektoren:</h4> <h4>{{$p.t('studium','lektoren')}}:</h4>
<a :href="'mailto:'+lektor?.email" class="fhc-link-color mx-2" v-for="lektor in selectedLehrveranstaltung.lektoren">{{lektor.name}}</a> <a :href="'mailto:'+lektor?.email" class="fhc-link-color mx-2" v-for="lektor in selectedLehrveranstaltung.lektoren">{{lektor.name}}</a>
</div> </div>
<h4>Menu:</h4> <h4>Menu:</h4>
@@ -279,13 +315,13 @@ export default {
</lv-uebersicht> </lv-uebersicht>
<div class="lvOptions"> <div class="lvOptions">
<div> <div>
<h6>Studiensemester:</h6> <h6>{{$p.t('studium','studiensemester')}}:</h6>
<div class="input-group"> <div class="input-group">
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiensemester(1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')"> <button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiensemester(1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
<i class="fa fa-caret-left" aria-hidden="true"></i> <i class="fa fa-caret-left" aria-hidden="true"></i>
</button> </button>
<select ref="studiensemester" v-model="selectedStudiensemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)"> <select ref="studiensemester" v-model="selectedStudiensemester" @change="onStudiensemesterChange" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')">
<option v-for="semester in studienSemester" @click="changeSelectedStudienSemester(semester.studiensemester_kurzbz)" :key="semester" :value="semester.studiensemester_kurzbz">{{studiensemesterTitel(semester.studiensemester_kurzbz) }}</option> <option v-for="semester in studienSemester" :key="semester" :value="semester.studiensemester_kurzbz">{{studiensemesterTitel(semester.studiensemester_kurzbz) }}</option>
</select> </select>
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiensemester(-1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')"> <button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiensemester(-1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
<i class="fa fa-caret-right" aria-hidden="true"></i> <i class="fa fa-caret-right" aria-hidden="true"></i>
@@ -294,13 +330,13 @@ export default {
</div> </div>
<div> <div>
<h6>Studiengang:</h6> <h6>{{$p.t('lehre','studiengang')}}:</h6>
<div class="input-group"> <div class="input-group">
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiengang(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')"> <button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiengang(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
<i class="fa fa-caret-left" aria-hidden="true"></i> <i class="fa fa-caret-left" aria-hidden="true"></i>
</button> </button>
<select ref="studiengaenge" v-model="selectedStudiengang" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)"> <select ref="studiengaenge" v-model="selectedStudiengang" class="form-select" @change="onStudiengangChange" :aria-label="$p.t('global/studiensemester_auswaehlen')">
<option v-for="studiengang in studiengaenge" @click="changeSelectedStudienGang(studiengang.studiengang_kz)" :key="studiengang.studiengang_kz" :value="studiengang.studiengang_kz" >{{studiengangTitel(studiengang)}}</option> <option v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" :value="studiengang.studiengang_kz" >{{studiengangTitel(studiengang)}}</option>
</select> </select>
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiengang(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')"> <button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudiengang(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
<i class="fa fa-caret-right" aria-hidden="true"></i> <i class="fa fa-caret-right" aria-hidden="true"></i>
@@ -309,13 +345,13 @@ export default {
</div> </div>
<div> <div>
<h6>Semester:</h6> <h6>{{$p.t('lehre','semester')}}:</h6>
<div class="input-group"> <div class="input-group">
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeSemester(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')"> <button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeSemester(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
<i class="fa fa-caret-left" aria-hidden="true"></i> <i class="fa fa-caret-left" aria-hidden="true"></i>
</button> </button>
<select ref="semester" v-model="selectedSemester" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)"> <select ref="semester" v-model="selectedSemester" class="form-select" @change="onSemesterChange" :aria-label="$p.t('global/studiensemester_auswaehlen')">
<option v-for="sem in semester" @click="changeSelectedSemester(sem)" :key="semester" :value="sem">{{sem}}. Semester</option> <option v-for="sem in semester" :key="sem" :value="sem">{{sem}}. Semester</option>
</select> </select>
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeSemester(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')"> <button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeSemester(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
<i class="fa fa-caret-right" aria-hidden="true"></i> <i class="fa fa-caret-right" aria-hidden="true"></i>
@@ -324,13 +360,13 @@ export default {
</div> </div>
<div> <div>
<h6>Studienordnung:</h6> <h6>{{$p.t('studium','studienordnung')}}:</h6>
<div class="input-group"> <div class="input-group">
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudienordnung(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')"> <button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudienordnung(-1)" :aria-label="$p.t('global','previous')" :title="$p.t('global','previous')">
<i class="fa fa-caret-left" aria-hidden="true"></i> <i class="fa fa-caret-left" aria-hidden="true"></i>
</button> </button>
<select ref="studienordnung" v-model="selectedStudienordnung" class="form-select" :aria-label="$p.t('global/studiensemester_auswaehlen')" @change="setHash($event.target.value)"> <select ref="studienordnung" v-model="selectedStudienordnung" class="form-select" @change="onStudienordnungChange" :aria-label="$p.t('global/studiensemester_auswaehlen')">
<option v-for="ordnung in computedStudienOrdnungSelectValues" :disabled="ordnung.disabled" @click="changeSelectedStudienPlan(ordnung?.studienplan?.studienplan_id)" :key="ordnung?.studienplan?.bezeichnung " :value="ordnung?.studienplan?.studienplan_id">{{ordnung.bezeichnung}}</option> <option v-for="ordnung in computedStudienOrdnungSelectValues" :disabled="ordnung.disabled" :key="ordnung?.studienplan?.studienplan_id" :value="ordnung?.studienplan?.studienplan_id">{{ordnung.bezeichnung}}</option>
</select> </select>
<button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudienordnung(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')"> <button class="btn btn-outline-secondary" type="button" :disabled="false" @click="changeStudienordnung(1)" :aria-label="$p.t('global','next')" :title="$p.t('global','next')">
<i class="fa fa-caret-right" aria-hidden="true"></i> <i class="fa fa-caret-right" aria-hidden="true"></i>
@@ -345,13 +381,13 @@ export default {
<template v-for="lehrveranstaltung in lehrveranstaltungen" :key="lehrveranstaltung.lehrveranstaltung_id"> <template v-for="lehrveranstaltung in lehrveranstaltungen" :key="lehrveranstaltung.lehrveranstaltung_id">
<div class="card" v-if="Array.isArray(lehrveranstaltung.lehrveranstaltungen) && lehrveranstaltung.lehrveranstaltungen.length >0" > <div class="card" v-if="Array.isArray(lehrveranstaltung.lehrveranstaltungen) && lehrveranstaltung.lehrveranstaltungen.length >0" >
<div class="card-header"> <div class="card-header">
<h5 class=" card-title">{{lehrveranstaltung.bezeichnung}}</h5> <h5 class=" card-title">{{isGermanLanguage == 'German' ? lehrveranstaltung.bezeichnung : lehrveranstaltung.bezeichnung_english }}</h5>
<h6 class=" card-subtitle">{{lehrveranstaltung.lehrform_kurzbz}}</h6> <h6 class=" card-subtitle">{{lehrveranstaltung.lehrform_kurzbz}}</h6>
</div> </div>
<div class="card-body"> <div class="card-body">
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
<li class="d-flex list-group-item" v-for="lv in lehrveranstaltung.lehrveranstaltungen"> <li class="d-flex list-group-item" v-for="lv in lehrveranstaltung.lehrveranstaltungen">
<a class="fhc-link-color d-block me-auto" href="#" @click="openLvUebersicht(lv)">{{lv.bezeichnung}}</a> <a class="fhc-link-color d-block me-auto" href="#" @click="openLvUebersicht(lv)">{{isGermanLanguage == 'German' ? lv.bezeichnung : lv.bezeichnung_english}}</a>
<p>{{lv.lehrform_kurzbz}}</p> <p>{{lv.lehrform_kurzbz}}</p>
</li> </li>
</ul> </ul>
+92 -45
View File
@@ -4,7 +4,6 @@ import DashboardAdminWidgets from "./Admin/Widgets.js";
import DashboardAdminPresets from "./Admin/Presets.js"; import DashboardAdminPresets from "./Admin/Presets.js";
import ApiDashboardBoard from "../../api/factory/dashboard/board.js"; import ApiDashboardBoard from "../../api/factory/dashboard/board.js";
import ApiDashboardWidget from "../../api/factory/dashboard/widget.js";
export default { export default {
name: 'DashboardAdmin', name: 'DashboardAdmin',
@@ -16,7 +15,7 @@ export default {
provide() { provide() {
return { return {
adminMode: true, adminMode: true,
widgetsSetup: Vue.computed(() => this.dashboards[this.current] ? this.dashboards[this.current].widgetSetup : null) widgetsSetup: Vue.computed(() => this.dashboard ? this.dashboard.widgetSetup : null)
}; };
}, },
data() { data() {
@@ -34,33 +33,32 @@ export default {
methods: { methods: {
dashboardAdd() { dashboardAdd() {
let _name = ''; let _name = '';
BsPrompt.popup('New Dashboard name').then( BsPrompt
name => { .popup('New Dashboard name')
_name = name; .then(dashboard_kurzbz => {
const params = { const params = {
dashboard_kurzbz: name dashboard_kurzbz
}; };
return this.$api return this.$api
.call(ApiDashboardBoard.add(params)) .call(ApiDashboardBoard.add(params))
.then(response =>{ .then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
let newDashboard = { let newDashboard = {
dashboard_id: response.data, dashboard_id: response.data,
dashboard_kurzbz: _name, dashboard_kurzbz,
beschreibung: '' beschreibung: ''
}; };
this.dashboards.push(newDashboard); this.dashboards.push(newDashboard);
this.current = newDashboard.dashboard_id; this.current = newDashboard.dashboard_id;
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}); });
}, },
dashboardUpdate(dashboard) { dashboardUpdate(dashboard) {
return this.$api this.$api
.call(ApiDashboardBoard.update(dashboard)) .call(ApiDashboardBoard.update(dashboard))
.then(response =>{ .then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave')); this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
let old = this.dashboards.find(el => el.dashboard_id == dashboard.dashboard_id); let old = this.dashboards.find(el => el.dashboard_id == dashboard.dashboard_id);
@@ -70,73 +68,122 @@ export default {
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
dashboardDelete(dashboard_id) { dashboardDelete(dashboard_id) {
return this.$api this.$api
.call(ApiDashboardBoard.delete(dashboard_id)) .call(ApiDashboardBoard.delete(dashboard_id))
.then(response => { .then(response => {
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete')); this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
})
.catch(this.$fhcAlert.handleSystemError)
.finally(() => {
this.current = -1; this.current = -1;
this.dashboards = this.dashboards.filter(el => el.dashboard_id != dashboard_id); this.dashboards = this.dashboards.filter(el => el.dashboard_id != dashboard_id);
}); })
.catch(this.$fhcAlert.handleSystemError);
}, },
assignWidgets(widgets) { assignWidgets(widgets) {
this.widgets = widgets; this.widgets = widgets;
/*while (this.widgets.length)
this.widgets.pop();
for (var i in widgets)
this.widgets.push(widgets[i]);*/
} }
}, },
created() { created() {
this.$api this.$api
.call(ApiDashboardBoard.list()) .call(ApiDashboardBoard.list())
.then(result => { .then(result => {
this.dashboards = result.data.retval; this.dashboards = result.data;
for (const dashboard of this.dashboards) {
this.$api
.call(ApiDashboardWidget.list(dashboard.dashboard_id))
.then(res => {
dashboard.widgetSetup = res.data;
})
.catch(this.$fhcAlert.handleSystemError);
}
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
template: `<div class="dashboard-admin"> template: /* html */`
<div class="dashboard-admin">
<div class="input-group"> <div class="input-group">
<label for="dashboard-select" class="input-group-text">Dashboard:</label> <label for="dashboard-select" class="input-group-text">
<select id="dashboard-select" class="form-select" v-model="current"> Dashboard:
<option v-for="dashboard in dashboards" :key="dashboard.dashboard_id" :value="dashboard.dashboard_id">{{dashboard.dashboard_kurzbz}}</option> </label>
<select id="dashboard-select" v-model="current" class="form-select">
<option
v-for="dashboard in dashboards"
:key="dashboard.dashboard_id"
:value="dashboard.dashboard_id"
>{{ dashboard.dashboard_kurzbz }}</option>
</select> </select>
<button class="btn btn-outline-secondary" type="button" @click="dashboardAdd"><i class="fa-solid fa-plus"></i></button> <button
class="btn btn-outline-secondary"
type="button"
@click="dashboardAdd"
><i class="fa-solid fa-plus"></i></button>
</div> </div>
<div v-if="dashboard"> <div v-if="dashboard">
<ul class="nav nav-tabs mt-3" role="tablist"> <ul class="nav nav-tabs mt-3" role="tablist">
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="edit-tab" data-bs-toggle="tab" data-bs-target="#edit" type="button" role="tab" aria-controls="edit" aria-selected="false">{{this.$p.t('ui', 'bearbeiten')}}</button> <button
id="edit-tab"
class="nav-link"
data-bs-toggle="tab"
data-bs-target="#edit"
type="button"
role="tab"
aria-controls="edit"
aria-selected="false"
>{{ this.$p.t('ui', 'bearbeiten') }}</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link active" id="widgets-tab" data-bs-toggle="tab" data-bs-target="#widgets" type="button" role="tab" aria-controls="widgets" aria-selected="true">Widgets</button> <button
id="widgets-tab"
class="nav-link active"
data-bs-toggle="tab"
data-bs-target="#widgets"
type="button"
role="tab"
aria-controls="widgets"
aria-selected="true"
>Widgets</button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button class="nav-link" id="presets-tab" data-bs-toggle="tab" data-bs-target="#presets" type="button" role="tab" aria-controls="presets" aria-selected="false">Presets</button> <button
class="nav-link"
id="presets-tab"
data-bs-toggle="tab"
data-bs-target="#presets"
type="button"
role="tab"
aria-controls="presets"
aria-selected="false"
>Presets</button>
</li> </li>
</ul> </ul>
<div class="tab-content pt-3"> <div class="tab-content pt-3">
<div class="tab-pane fade" id="edit" role="tabpanel" aria-labelledby="edit-tab"> <div
<dashboard-admin-edit v-bind="dashboard" :key="dashboard.dashboard_id" @change="dashboardUpdate($event)" @delete="dashboardDelete($event)"></dashboard-admin-edit> id="edit"
class="tab-pane fade"
role="tabpanel"
aria-labelledby="edit-tab"
>
<dashboard-admin-edit
v-bind="dashboard"
@change="dashboardUpdate($event)"
@delete="dashboardDelete($event)"
></dashboard-admin-edit>
</div> </div>
<div class="tab-pane fade show active" id="widgets" role="tabpanel" aria-labelledby="widgets-tab"> <div
<dashboard-admin-widgets :key="dashboard.dashboard_id" :dashboard_id="dashboard.dashboard_id" :widgets="widgets" @assign-widgets="assignWidgets"></dashboard-admin-widgets> id="widgets"
class="tab-pane fade show active"
role="tabpanel"
aria-labelledby="widgets-tab"
>
<dashboard-admin-widgets
:dashboard_id="dashboard.dashboard_id"
:widgets="widgets"
@assign-widgets="assignWidgets"
></dashboard-admin-widgets>
</div> </div>
<div class="tab-pane fade" id="presets" role="tabpanel" aria-labelledby="presets-tab"> <div
<dashboard-admin-presets :dashboard="dashboard.dashboard_kurzbz" :widgets="widgets"></dashboard-admin-presets> id="presets"
class="tab-pane fade"
role="tabpanel"
aria-labelledby="presets-tab"
>
<dashboard-admin-presets
:dashboard="dashboard.dashboard_kurzbz"
:widgets="widgets"
></dashboard-admin-presets>
</div> </div>
</div> </div>
</div> </div>
+34 -13
View File
@@ -1,15 +1,15 @@
import BsConfirm from '../../Bootstrap/Confirm.js'; import BsConfirm from '../../Bootstrap/Confirm.js';
export default { export default {
emits: [
"change",
"delete"
],
props: { props: {
dashboard_id: Number, dashboard_id: Number,
dashboard_kurzbz: String, dashboard_kurzbz: String,
beschreibung: String beschreibung: String
}, },
emits: [
"change",
"delete"
],
data() { data() {
return { return {
kurzbz: this.dashboard_kurzbz, kurzbz: this.dashboard_kurzbz,
@@ -18,22 +18,43 @@ export default {
}, },
methods: { methods: {
sendDelete() { sendDelete() {
BsConfirm.popup(this.$p.t('ui', 'confirm_delete') + " " + this.$p.t('ui', 'deleteInfo')) BsConfirm
.then(() => this.$emit('delete', this.dashboard_id)).catch(); .popup(this.$p.t('ui', 'confirm_delete') + " " + this.$p.t('ui', 'deleteInfo'))
.then(() => this.$emit('delete', this.dashboard_id))
.catch();
} }
}, },
template: `<div class="dashboard-admin-edit px-3"> template: /* html */`
<div class="dashboard-admin-edit px-3">
<div class="mb-3"> <div class="mb-3">
<label for="dashboard-admin-edit-kurzbz">Kurz Bezeichnung</label> <label for="dashboard-admin-edit-kurzbz">{{ $p.t('dashboard/kurzbz') }}</label>
<input id="dashboard-admin-edit-kurzbz" type="text" class="form-control" v-model="kurzbz"> <input
id="dashboard-admin-edit-kurzbz"
v-model="kurzbz"
type="text"
class="form-control"
>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="dashboard-admin-edit-beschreibung">Beschreibung</label> <label for="dashboard-admin-edit-beschreibung">{{ $p.t('global/beschreibung') }}</label>
<textarea id="dashboard-admin-edit-beschreibung" class="form-control" v-model="desc"></textarea> <textarea
id="dashboard-admin-edit-beschreibung"
v-model="desc"
class="form-control"
></textarea>
</div> </div>
<div> <div>
<button class="btn btn-danger" @click="sendDelete">{{this.$p.t('ui', 'loeschen')}}</button> <button class="btn btn-danger" @click="sendDelete">
<button class="btn btn-primary" @click="$emit('change', {dashboard_id,dashboard_kurzbz:kurzbz,beschreibung:desc})">{{this.$p.t('ui', 'btnAktualisieren')}}</button> {{ this.$p.t('ui', 'loeschen') }}
</button>
<button
class="btn btn-primary"
@click="$emit('change', {
dashboard_id,
dashboard_kurzbz: kurzbz,
beschreibung: desc
})"
>{{ this.$p.t('ui', 'btnAktualisieren') }}</button>
</div> </div>
</div>` </div>`
} }
+94 -33
View File
@@ -12,20 +12,61 @@ export default {
dashboard: String, dashboard: String,
widgets: Array widgets: Array
}, },
data: () => ({ data() {
funktionen: {}, return {
sections: [], funktionen: {},
tmpLoading: '' sections: [],
}), selectedFunktionen: [],
abortController: null
};
},
computed: { computed: {
pickerWidgets() { pickerWidgets() {
return this.widgets.filter(widget => widget.allowed); return this.widgets.filter(widget => widget.allowed);
},
sizeLimits() {
return Object.fromEntries(this.widgets.map(({ setup, widget_id: type }) => {
const result = {}; // work on a copy
if (setup.height === undefined)
result.height = { min: 1, max: undefined };
else if (Number.isInteger(setup.height))
result.height = { min: setup.height, max: setup.height };
else
result.height = {
min: setup.height.min ?? 1,
max: setup.height.max
};
if (setup.width === undefined)
result.width = { min: 1, max: undefined };
else if (Number.isInteger(setup.width))
result.width = { min: setup.width, max: setup.width };
else
result.width = {
min: setup.width.min ?? 1,
max: setup.width.max
};
return [type, result];
}));
}
},
watch: {
dashboard() {
this.loadSections();
this.loadFunktionen();
} }
}, },
methods: { methods: {
widgetAdd(section_name, widget) { widgetAdd(widget, section_name) {
this.$refs.widgetpicker.getWidget().then(widget_id => { this.$refs.widgetpicker.getWidget().then(widget_id => {
widget.widget = widget_id; widget.widget = widget_id;
// NOTE(chris): min size
widget.place = Object.fromEntries(Object.entries(widget.place).map(([key, value]) => {
value.w = this.sizeLimits[widget_id].width.min;
value.h = this.sizeLimits[widget_id].height.min;
return [key, value];
}));
widget.id = 'loading_' + String((new Date()).valueOf()); widget.id = 'loading_' + String((new Date()).valueOf());
delete widget.custom; delete widget.custom;
widget.preset = 1; widget.preset = 1;
@@ -36,6 +77,8 @@ export default {
section.widgets.push(loading); section.widgets.push(loading);
}); });
delete widget.id;
const params = { const params = {
dashboard: this.dashboard, dashboard: this.dashboard,
funktion_kurzbz: section_name, funktion_kurzbz: section_name,
@@ -64,22 +107,29 @@ export default {
}) })
.catch(() => {}); .catch(() => {});
}, },
widgetUpdate(section_name, payload) { widgetUpdate(payload, section_name) {
payload = payload[section_name];
for (var k in payload) { for (var k in payload) {
const section = this.sections.find(section => section.name == section_name); const section = this.sections.find(section => section.name == section_name);
for (var wid in section.widgets) { for (var wid in section.widgets) {
if (section.widgets[wid].id == k) { if (section.widgets[wid].id == k) {
payload[k] = ObjectUtils.mergeDeep(section.widgets[wid], payload[k]); const copy = ObjectUtils.mergeDeep(section.widgets[wid], payload[k]);
if (payload[k].config)
copy.config = payload[k].config;
payload[k] = copy;
// NOTE(chris): remove internal props // NOTE(chris): remove internal props
for (var prop of ['_x', '_y', '_w', '_h', 'index', 'id']) for (var prop of ['_x', '_y', '_w', '_h', 'index', 'id', 'custom'])
if (payload[k][prop]) if (payload[k][prop])
delete payload[k][prop]; delete payload[k][prop];
break; break;
} }
} }
if (payload[k].place) {
Object.values(payload[k].place).forEach(place => {
if (place.pinned === false)
delete place.pinned;
});
}
payload[k].widgetid = k; payload[k].widgetid = k;
delete payload[k].custom;
} }
this.$api this.$api
.call(Object.entries(payload).map(([key, widget]) => [ .call(Object.entries(payload).map(([key, widget]) => [
@@ -106,7 +156,7 @@ export default {
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
widgetRemove(section_name, id) { widgetRemove(id, section_name) {
const params = { const params = {
db: this.dashboard, db: this.dashboard,
funktion_kurzbz: section_name, funktion_kurzbz: section_name,
@@ -122,21 +172,22 @@ export default {
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
loadSections(evt) { loadSections() {
let funktionen = Array.from(evt.target.querySelectorAll("option:checked"),e=>e.value);
this.sections = [];
this.tmpLoading = funktionen.join('###');
const params = { const params = {
db: this.dashboard, db: this.dashboard,
funktionen funktionen: this.selectedFunktionen
}; };
if (this.abortController)
this.abortController.abort();
this.abortController = new AbortController();
const signal = this.abortController.signal;
this.sections = [];
return this.$api return this.$api
.call(ApiDashboardPreset.getBatch(params)) .call(ApiDashboardPreset.getBatch(params), { signal })
.then(result => { .then(result => {
if (this.tmpLoading !== funktionen.join('###'))
return; // NOTE(chris): prevent race condition
for (var section in result.data) { for (var section in result.data) {
let widgets = []; let widgets = [];
for (var wid in result.data[section]) { for (var wid in result.data[section]) {
@@ -151,7 +202,6 @@ export default {
} }
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
loadFunktionen() { loadFunktionen() {
this.$api this.$api
@@ -165,17 +215,17 @@ export default {
created() { created() {
this.loadFunktionen(); this.loadFunktionen();
}, },
watch: { template: /* html */`
dashboard() { <div class="dashboard-admin-presets">
// TODO(chris): this should be done without a watcher
this.loadSections({target:this.$refs.funktionenList});
this.loadFunktionen();
}
},
template: `<div class="dashboard-admin-presets">
<div class="row"> <div class="row">
<div class="col-3"> <div class="col-3">
<select ref="funktionenList" style="height:30em" class="form-control" multiple @input="loadSections"> <select
v-model="selectedFunktionen"
class="form-control"
style="height:30em"
multiple
@change="loadSections"
>
<option <option
v-for="funktion in funktionen" v-for="funktion in funktionen"
:key="funktion.funktion_kurzbz" :key="funktion.funktion_kurzbz"
@@ -185,9 +235,20 @@ export default {
</select> </select>
</div> </div>
<div class="col-9"> <div class="col-9">
<dashboard-section v-for="section in sections" :key="section.name" :name="section.name" :widgets="section.widgets" @widget-add="widgetAdd" @widget-update="widgetUpdate" @widget-remove="widgetRemove"></dashboard-section> <dashboard-section
v-for="section in sections"
:key="section.name"
:name="section.name"
:widgets="section.widgets"
@widget-add="widgetAdd"
@widget-update="widgetUpdate"
@widget-remove="widgetRemove"
></dashboard-section>
</div> </div>
</div> </div>
<dashboard-widget-picker ref="widgetpicker" :widgets="pickerWidgets"></dashboard-widget-picker> <dashboard-widget-picker
ref="widgetpicker"
:widgets="pickerWidgets"
></dashboard-widget-picker>
</div>` </div>`
} }
@@ -1,14 +1,14 @@
import ApiDashboardWidget from "../../../api/factory/dashboard/widget.js"; import ApiDashboardWidget from "../../../api/factory/dashboard/widget.js";
export default { export default {
emits: [
"change",
"assignWidgets"
],
props: { props: {
dashboard_id: Number, dashboard_id: Number,
widgets: Array widgets: Array
}, },
emits: [
"change",
"assignWidgets"
],
methods: { methods: {
sendChange(widget_id) { sendChange(widget_id) {
let allow = !this.widgets.find(el => el.widget_id == widget_id).allowed; let allow = !this.widgets.find(el => el.widget_id == widget_id).allowed;
@@ -29,11 +29,27 @@ export default {
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
template: ` template: /* html */`
<div class="dashboard-admin-widgets"> <div class="dashboard-admin-widgets">
<div v-for="widget in widgets" :key="widget.widget_id" class="form-check form-switch"> <div
<input class="form-check-input" type="checkbox" role="switch" :id="'dashboard-admin-widgets-' + widget.widget_id" v-model="widget.allowed" @input.prevent="sendChange(widget.widget_id)"> v-for="widget in widgets"
<label class="form-check-label" :for="'dashboard-admin-widgets-' + widget.widget_id">{{(widget.setup && widget.setup.name) || widget.widget_kurzbz}}</label> :key="widget.widget_id"
class="form-check form-switch"
>
<input
:id="'dashboard-admin-widgets-' + widget.widget_id"
v-model="widget.allowed"
class="form-check-input"
type="checkbox"
role="switch"
@input.prevent="sendChange(widget.widget_id)"
>
<label
class="form-check-label"
:for="'dashboard-admin-widgets-' + widget.widget_id"
>
{{ (widget.setup && widget.setup.name) || widget.widget_kurzbz }}
</label>
</div> </div>
</div>` </div>`
} }
+87 -26
View File
@@ -2,6 +2,7 @@ import DashboardSection from "./Section.js";
import DashboardWidgetPicker from "./Widget/Picker.js"; import DashboardWidgetPicker from "./Widget/Picker.js";
import ObjectUtils from "../../helpers/ObjectUtils.js"; import ObjectUtils from "../../helpers/ObjectUtils.js";
import ApiDashboard from '../../api/factory/cis/dashboard.js';
import ApiDashboardWidget from '../../api/factory/dashboard/widget.js'; import ApiDashboardWidget from '../../api/factory/dashboard/widget.js';
import ApiDashboardUser from '../../api/factory/dashboard/user.js'; import ApiDashboardUser from '../../api/factory/dashboard/user.js';
@@ -17,40 +18,70 @@ export default {
required: true, required: true,
default: 'CIS' default: 'CIS'
}, },
viewData: {
type: Object,
required: true,
validator(value) {
return value && value.name && value.timezone
}
}
}, },
data() { data() {
return { return {
widgets: [], widgets: [],
originalWidgets: {}, originalWidgets: {},
widgetsSetup: null, widgetsSetup: null,
editMode: false editMode: false,
timezone: null,
userFirstName: null,
} }
}, },
provide() { provide() {
return { return {
editMode: Vue.computed(()=>this.editMode), editMode: Vue.computed(() => this.editMode),
widgetsSetup: Vue.computed(() => this.widgetsSetup), widgetsSetup: Vue.computed(() => this.widgetsSetup),
timezone: Vue.computed(() => this.viewData.timezone) timezone: this.timezone
}
},
computed: {
sizeLimits() {
return Object.fromEntries(this.widgetsSetup.map(({ setup, widget_id: type }) => {
const result = {}; // work on a copy
if (setup.height === undefined)
result.height = { min: 1, max: undefined };
else if (Number.isInteger(setup.height))
result.height = { min: setup.height, max: setup.height };
else
result.height = {
min: setup.height.min ?? 1,
max: setup.height.max
};
if (setup.width === undefined)
result.width = { min: 1, max: undefined };
else if (Number.isInteger(setup.width))
result.width = { min: setup.width, max: setup.width };
else
result.width = {
min: setup.width.min ?? 1,
max: setup.width.max
};
return [type, result];
}));
} }
}, },
methods: { methods: {
widgetAdd(section_name, widget) { widgetAdd(widget) {
// TODO(chris): remove section_name? (change order of params => get rid of it)
this.$refs.widgetpicker this.$refs.widgetpicker
.getWidget() .getWidget()
.then(widget_id => { .then(widget_id => {
widget.widget = widget_id; widget.widget = widget_id;
// NOTE(chris): min size
widget.place = Object.fromEntries(Object.entries(widget.place).map(([key, value]) => {
value.w = this.sizeLimits[widget_id].width.min;
value.h = this.sizeLimits[widget_id].height.min;
return [key, value];
}));
widget.id = 'loading_' + String((new Date()).valueOf()); widget.id = 'loading_' + String((new Date()).valueOf());
let loading = { ...widget }; let loading = { ...widget };
loading.loading = true; loading.loading = true;
this.widgets.push(loading); this.widgets.push(loading);
delete widget.id;
this.$api this.$api
.call(ApiDashboardUser.addWidget(this.dashboard, widget)) .call(ApiDashboardUser.addWidget(this.dashboard, widget))
@@ -64,19 +95,27 @@ export default {
}) })
.catch(() => {}); .catch(() => {});
}, },
widgetUpdate(section_name, payload) { widgetUpdate(payload) {
payload = payload[section_name];
for (var k in payload) { for (var k in payload) {
for (var wid in this.widgets) { for (var wid in this.widgets) {
if (this.widgets[wid].id == k) { if (this.widgets[wid].id == k) {
payload[k] = ObjectUtils.mergeDeep(this.widgets[wid], payload[k]); const copy = ObjectUtils.mergeDeep(this.widgets[wid], payload[k]);
if (payload[k].config)
copy.config = payload[k].config;
payload[k] = copy;
// NOTE(chris): remove internal props // NOTE(chris): remove internal props
for (var prop of ['_x','_y','_w','_h','index','id','preset']) for (var prop of ['_x', '_y', '_w', '_h', 'index', 'id', 'preset'])
if (payload[k][prop]) if (payload[k][prop])
delete payload[k][prop]; delete payload[k][prop];
break; break;
} }
} }
if (payload[k].place) {
Object.values(payload[k].place).forEach(place => {
if (place.pinned === false)
delete place.pinned;
});
}
payload[k].widgetid = k; payload[k].widgetid = k;
} }
this.$api this.$api
@@ -113,18 +152,26 @@ export default {
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
widgetRemove(section_name, id) { widgetRemove(id) {
this.$api this.$api
.call(ApiDashboardUser.removeWidget(this.dashboard, id)) .call(ApiDashboardUser.removeWidget(this.dashboard, id))
.then(() => { .then(() => {
this.widgets = this.widgets.filter(widget => widget.id != id); this.widgets = this.widgets.filter(widget => widget.id != id);
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
},
async fetchViewData() {
let viewDataResult = await this.$api.call(ApiDashboard.getViewData());
const viewData = viewDataResult.data;
this.timezone = viewData?.timezone;
this.userFirstName = viewData?.name;
} }
}, },
created() { async created() {
this.$p.loadCategory('dashboard'); this.$p.loadCategory('dashboard');
await this.fetchViewData();
this.$api this.$api
.call(ApiDashboardWidget.listAllowed(this.dashboard)) .call(ApiDashboardWidget.listAllowed(this.dashboard))
.then(res => { .then(res => {
@@ -138,8 +185,8 @@ export default {
const widgets = []; const widgets = [];
const remove = []; const remove = [];
for (var wid in res.data.general.widgets) { for (var wid in res.data) {
let widget = res.data.general.widgets[wid]; let widget = res.data[wid];
widget.id = wid; widget.id = wid;
if (widget.custom || widget.preset) { if (widget.custom || widget.preset) {
widgets.push(widget); widgets.push(widget);
@@ -149,19 +196,33 @@ export default {
} }
} }
remove.forEach(wid => this.widgetRemove('general', wid)); remove.forEach(wid => this.widgetRemove(wid));
this.widgets = widgets; this.widgets = widgets;
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
template: ` template: /* html */`
<div class="core-dashboard"> <div class="core-dashboard">
<h3> <h3>
{{ $p.t('global/personalGreeting', [ viewData?.name ]) }} {{ userFirstName ? $p.t('global/personalGreeting', [ userFirstName ]) : '' }}
<button style="margin-left: 8px;" class="btn" @click="editMode = !editMode" aria-label="edit dashboard" v-tooltip="{showDelay:1000,value:'edit dashboard'}"><i class="fa-solid fa-gear" aria-hidden="true"></i></button> <button
class="btn ms-2"
aria-label="edit dashboard"
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/edit') }"
@click="editMode = !editMode"
><i class="fa-solid fa-gear" aria-hidden="true"></i></button>
</h3> </h3>
<dashboard-section :seperator="0" name="general" :widgets="widgets" @widgetAdd="widgetAdd" @widgetUpdate="widgetUpdate" @widgetRemove="widgetRemove"></dashboard-section> <dashboard-section
<dashboard-widget-picker ref="widgetpicker" :widgets="widgetsSetup"></dashboard-widget-picker> name="general"
:widgets="widgets"
@widget-add="widgetAdd"
@widget-update="widgetUpdate"
@widget-remove="widgetRemove"
></dashboard-section>
<dashboard-widget-picker
ref="widgetpicker"
:widgets="widgetsSetup"
></dashboard-widget-picker>
</div>` </div>`
} }
+296 -133
View File
@@ -1,7 +1,13 @@
import BsModal from "../Bootstrap/Modal.js"; import BsModal from "../Bootstrap/Modal.js";
import { useCachedWidgetLoader } from "../../composables/Dashboard/CachedWidgetLoader.js";
import HeightTransition from "../Tranistion/HeightTransition.js"; import HeightTransition from "../Tranistion/HeightTransition.js";
import { enableDragDropTouch } from "../../../../vendor/drag-drop-touch-js/dragdroptouch/dist/drag-drop-touch.esm.min.js";
if (!document.dragDropTouchActive) {
enableDragDropTouch();
document.dragDropTouchActive = true;
}
export default { export default {
name: 'Item', name: 'Item',
components: { components: {
@@ -11,18 +17,14 @@ export default {
data: () => ({ data: () => ({
component: "", component: "",
arguments: null, arguments: null,
target: false,
widget: null,
tmpConfig: {}, tmpConfig: {},
isLoading: false, isLoading: false,
hasConfig: false, hasConfig: false,
sharedData: null, sharedData: null
}), }),
emits: [ emits: [
"change", "change",
"remove", "remove",
"dragstart",
"resizestart",
"configOpened", "configOpened",
"configClosed", "configClosed",
"pinItem", "pinItem",
@@ -30,41 +32,85 @@ export default {
], ],
props: [ props: [
"id", "id",
"widgetID",
"config", "config",
"width", "width",
"height", "height",
"custom", "custom",
"hidden", "hidden",
"editMode", "editMode",
"loading", "loading", // widget got added and is waiting for backend to save in db
"item_data", "item_data",
"place", "place",
"setup", "widgetTemplate",
"dragstate", "source"
"resizeOverlay",
"additionalRow"
], ],
computed: { computed: {
maxHeight(){ sourceInfoTooltip() {
return this.setup?.height?.max; switch (this.source) {
}, case null:
maxWidth(){ return '';
if (Object.prototype.toString.call(this.setup?.width) == "[object Number]"){ case 'general':
return this.setup?.width; return this.$p.t('dashboard', 'widgetFromGeneralSection');
case 'custom':
return this.$p.t('dashboard', 'widgetFromCustomSection');
default:
return this.$p.t('dashboard', 'widgetFromFunktionSection', [this.source]);
} }
return this.setup?.width?.max;
}, },
minHeight() { isResizeableHorizontal() {
return this.setup?.height?.min; if (this.widgetTemplate.setup.width === undefined)
return true;
if (Object.prototype.toString.call(this.widgetTemplate.setup.width) == "[object Number]")
return false;
if (this.widgetTemplate.setup.width.min === undefined) {
if (this.widgetTemplate.setup.width.max === undefined)
return true;
return this.widgetTemplate.setup.width.max > 1;
}
if (this.widgetTemplate.setup.width.max === undefined)
return true;
return this.widgetTemplate.setup.width.max > this.widgetTemplate.setup.width.min;
}, },
minWidth() { isResizeableVertical() {
return this.setup?.width?.min; if (this.widgetTemplate.setup.height === undefined)
return true;
if (Object.prototype.toString.call(this.widgetTemplate.setup.height) == "[object Number]")
return false;
if (this.widgetTemplate.setup.height.min === undefined) {
if (this.widgetTemplate.setup.height.max === undefined)
return true;
return this.widgetTemplate.setup.height.max > 1;
}
if (this.widgetTemplate.setup.height.max === undefined)
return true;
return this.widgetTemplate.setup.height.max > this.widgetTemplate.setup.height.min;
}, },
isResizeable(){ isResizeable() {
return this.maxWidth >1 || this.maxHeight >1; return this.isResizeableVertical || this.isResizeableHorizontal;
}, },
isPinned(){ resizeClasses() {
const classes = {
icon: 'fa-up-right-and-down-left-from-center mirror-x',
button: 'cursor-nw-resize'
};
if (!this.isResizeableHorizontal) {
classes.icon = 'fa-up-down pe-2';
classes.button = 'cursor-ns-resize';
} else if (!this.isResizeableVertical) {
classes.icon = 'fa-left-right pe-2';
classes.button = 'cursor-ew-resize';
}
return classes;
},
isPinned() {
return this.place?.pinned ? true : false; return this.place?.pinned ? true : false;
}, },
ready() { ready() {
@@ -80,16 +126,16 @@ export default {
} }
}, },
methods: { methods: {
unpin(){ unpin() {
// Unpinning is only possible in edit mode // Unpinning is only possible in edit mode
if(!this.editMode) if (!this.editMode)
return; return;
let result = { item: this.item_data, x: this.item_data.x, y: this.item_data.y }; let result = { item: this.item_data, pinned: false };
this.$emit('unPinItem', [result]); this.$emit('unPinItem', [result]);
}, },
pinItem(){ pinItem() {
let result = { item: this.item_data, x: this.item_data.x, y: this.item_data.y}; let result = { item: this.item_data, pinned: true };
this.$emit('pinItem',[result]); this.$emit('pinItem', [result]);
}, },
getWidgetC4Link(widget) { getWidgetC4Link(widget) {
return (FHC_JS_DATA_STORAGE_OBJECT.app_root + return (FHC_JS_DATA_STORAGE_OBJECT.app_root +
@@ -101,22 +147,6 @@ export default {
handleHideBsModal() { handleHideBsModal() {
this.$emit('configClosed') this.$emit('configClosed')
}, },
mouseDown(e) {
this.target = e.target;
},
startDrag(e) {
if (this.$refs.dragHandle.contains(this.target)) {
this.$emit("dragstart", e);
} else if (
this.isResizeable &&
this.$refs.resizeHandle.contains(this.target)
) {
if (this.isResizeable) this.$emit("resizestart", e);
else e.preventDefault();
} else {
e.preventDefault();
}
},
openConfig() { openConfig() {
this.tmpConfig = { ...this.arguments }; this.tmpConfig = { ...this.arguments };
this.$refs.config.show(); this.$refs.config.show();
@@ -135,110 +165,243 @@ export default {
}, },
sendChangeConfig(config) { sendChangeConfig(config) {
for (var k in config) { for (var k in config) {
if (this.widget.arguments[k] == config[k]) { if (this.widgetTemplate.arguments[k] == config[k]) {
delete config[k]; delete config[k];
} }
} }
this.$emit("change", config); this.$emit("change", config);
}, },
async initializeComponent() {
if (
this.widgetTemplate
&& this.widgetTemplate.setup
&& this.widgetTemplate.widget_id
&& this.widgetTemplate.arguments
) {
let component = (await import(this.widgetTemplate.setup.file)).default;
this.$options.components["widget" + this.widgetTemplate.widget_id] = component;
this.component = "widget" + this.widgetTemplate.widget_id;
this.arguments = { ...this.widgetTemplate.arguments, ...this.config };
this.tmpConfig = { ...this.arguments };
}
}
}, },
watch: { watch: {
config() { config() {
this.arguments = { ...this.widget?.arguments, ...this.config }; this.arguments = { ...this.widgetTemplate?.arguments, ...this.config };
this.tmpConfig = { ...this.arguments }; this.tmpConfig = { ...this.arguments };
this.$refs.config && this.$refs.config.hide(); this.$refs.config && this.$refs.config.hide();
this.isLoading = false; this.isLoading = false;
}, },
widgetTemplate() {
this.initializeComponent();
}
}, },
setup() { created() {
const { actions } = useCachedWidgetLoader(); this.initializeComponent();
return {
loadWidget: actions.load
};
},
async created() {
this.widget = await this.loadWidget(this.id);
let component = (await import(this.widget.setup.file)).default;
this.$options.components["widget" + this.widget.widget_id] = component;
this.component = "widget" + this.widget.widget_id;
this.arguments = { ...this.widget.arguments, ...this.config };
this.tmpConfig = { ...this.arguments };
}, },
template: /*html*/ ` template: /*html*/ `
<div v-if="loading"> <article
<div class="d-flex justify-content-center align-items-center h-100"> v-if="!hidden || editMode"
class="dashboard-item card overflow-hidden h-100 position-relative"
:class="{
'hidden-widget': hidden,
[arguments?.className]: arguments && arguments.className
}"
>
<div v-if="loading" class="d-flex justify-content-center align-items-center h-100">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i> <i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div> </div>
</div> <template v-else>
<div v-else-if="!hidden || editMode" :id="widgetID" class="dashboard-item card overflow-hidden h-100 position-relative" :class="{'hiddenWidget':hidden, 'draggedItem':dragstate, 'dashboard-item-overlay':resizeOverlay, [arguments?.className]:arguments && arguments.className}"> <header
<div v-show="!dragstate" class="h-100 card border-0"> v-if="widgetTemplate"
<div v-if="widget" class="card-header d-flex ps-0 pe-2 align-items-center"> class="card-header d-flex ps-0 pe-2 align-items-center"
<Transition> >
<span type="button" v-if="editMode && !isPinned" drag-action="move" class="col-auto mx-2 px-2 cursor-move" aria-label="move widget" v-tooltip="{showDelay:1000, value:'move widget'}"><i class="fa-solid fa-grip-vertical" aria-hidden="true"></i></span> <!-- move handle -->
</Transition> <Transition>
<span class="col mx-2 px-2">{{ widget.setup.name }}</span> <span
<template v-if="isPinned"> v-if="editMode && !isPinned"
<div type="button" role="button" v-if="editMode" pinned="true" @click="unpin" title="unpin item" aria-label="unpin item" class="pin cursor-pointer col-auto me-2"> type="button"
<i class="fa-solid fa-thumbtack " aria-hidden="true"></i> drag-action="move"
</div> class="col-auto mx-2 px-2 cursor-move"
<div v-else class="col-auto me-2"> draggable="true"
<i class="fa-solid fa-thumbtack "></i> aria-hidden="true"
</div> :aria-label="$p.t('dashboard/widget_move')"
</template> v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_move') }"
<template v-else> >
<div type="button" role="button" v-if="editMode" class="col-auto me-2 pin" @click="pinItem" aria-label="pin item" title="pin item"> <i class="fa-solid fa-grip-vertical" aria-hidden="true"></i>
<i class="fa-solid fa-thumbtack" aria-hidden="true" style="color:lightgray;"></i>
</div>
</template>
<a type="button" v-if="widget.setup.cis4link" :href="getWidgetC4Link(widget)" aria-label="widget link" v-tooltip="{showDelay:1000, value:'widget link'}" class="col-auto ms-auto ">
<i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true"></i>
</a>
<a type="button" v-if="hasConfig" class="col-auto px-1" href="#" @click.prevent="openConfig" aria-label="configure widget" v-tooltip="{showDelay:1000,value:'configure widget'}"><i class="fa-solid fa-gear" aria-hidden="true"></i></a>
<a type="button" v-if="custom && editMode" class="col-auto px-1" aria-label="delete widget" v-tooltip="{showDelay:1000,value:'delete widget'}" href="#" @click.prevent="$emit('remove')">
<i class="fa-solid fa-trash" aria-hidden="true"></i>
</a>
<Transition>
<div v-if="!custom && editMode" class="col-auto px-1 form-switch">
<input class="form-check-input ms-0" type="checkbox" role="switch" aria-label="toggle widget" id="flexSwitchCheckChecked" v-model="visible" :value="true">
</div>
</Transition>
</div>
<div v-if="ready" class="card-body overflow-hidden p-0">
<component :is="component" v-model:shared-data="sharedData" :config="arguments" :width="width" :height="height" @setConfig="setConfig" @change="changeConfigManually"></component>
</div>
<div v-else class="card-body overflow-hidden text-center d-flex flex-column justify-content-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
<bs-modal v-if="hasConfig" ref="config" @hideBsModal="handleHideBsModal" @showBsModal="handleShowBsModal">
<template v-slot:title>
{{ widget ? 'Config for ' + widget.setup.name : '' }}
</template>
<template v-slot:default>
<component v-if="ready && !isLoading" :is="component" v-model:shared-data="sharedData" :config="tmpConfig" @change="changeConfig" :configMode="true"></component>
<div v-else class="text-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div>
</template>
<template v-if="!widget?.setup?.hideFooter" v-slot:footer>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" @click="changeConfig">Save changes</button>
</template>
</bs-modal>
<height-transition>
<div v-if="editMode && isResizeable && !isPinned " class="card-footer d-flex justify-content-end p-0">
<template v-if="maxWidth < 2">
<span type="button" drag-action="resize" class="col-auto px-1 cursor-ns-resize" aria-label="resize widget" v-tooltip="{showDelay:1000, value:'resize widget'}">
<i class="fa-solid fa-up-down pe-2" aria-hidden="true"></i>
</span>
</template>
<template v-else-if="maxHeight < 2">
<span type="button" drag-action="resize" class="col-auto px-1 cursor-ew-resize" aria-label="resize widget" v-tooltip="{showDelay:1000, value:'resize widget'}">
<i class="fa-solid fa-left-right pe-2" aria-hidden="true"></i>
</span> </span>
</Transition>
<!-- TITLE -->
<h4 class="col mb-0 mx-2 px-2 fs-6 lh-base">
{{ widgetTemplate.setup.name }}
</h4>
<!-- source info -->
<div
v-if="source"
class="col-auto me-2"
:aria-label="sourceInfoTooltip"
v-tooltip="{ class: 'w-100', value: sourceInfoTooltip }"
>
<i class="fa-solid fa-circle-info" aria-hidden="true"></i>
</div>
<!-- pin button -->
<template v-if="isPinned">
<div
v-if="editMode"
type="button"
role="button"
class="pin cursor-pointer col-auto me-2"
:title="$p.t('dashboard/widget_unpin')"
aria-hidden="true"
:aria-label="$p.t('dashboard/widget_unpin')"
pinned="true"
@click="unpin"
>
<i class="fa-solid fa-thumbtack" aria-hidden="true"></i>
</div>
<div v-else class="col-auto me-2" aria-hidden="true">
<i class="fa-solid fa-thumbtack"></i>
</div>
</template> </template>
<template v-else> <template v-else>
<span type="button" drag-action="resize" class="col-auto px-1 cursor-nw-resize" aria-label="resize widget" v-tooltip="{showDelay:1000, value:'resize widget'}"> <div
<i class="fa-solid fa-up-right-and-down-left-from-center mirror-x" aria-hidden="true"></i> v-if="editMode"
</span> type="button"
role="button"
class="col-auto me-2 pin"
:title="$p.t('dashboard/widget_pin')"
aria-hidden="true"
:aria-label="$p.t('dashboard/widget_pin')"
@click="pinItem"
>
<i class="fa-solid fa-thumbtack" aria-hidden="true" style="color:lightgray;"></i>
</div>
</template> </template>
<!-- widget link -->
<a
v-if="widgetTemplate.setup.cis4link"
:href="getWidgetC4Link(widgetTemplate)"
class="col-auto ms-auto"
:aria-label="$p.t('dashboard/widget_link')"
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_link') }"
>
<i class="fa fa-arrow-up-right-from-square me-1" aria-hidden="true"></i>
</a>
<!-- config button -->
<a
v-if="hasConfig"
href="#"
class="col-auto px-1"
:aria-label="$p.t('dashboard/widget_configure')"
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_configure') }"
@click.prevent="openConfig"
>
<i class="fa-solid fa-gear" aria-hidden="true"></i>
</a>
<!-- delete button -->
<a
v-if="custom && editMode"
href="#"
class="col-auto px-1"
:aria-label="$p.t('dashboard/widget_delete')"
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_delete') }"
@click.prevent="$emit('remove')"
>
<i class="fa-solid fa-trash" aria-hidden="true"></i>
</a>
<!-- hide button -->
<Transition>
<div v-if="!custom && editMode" class="col-auto px-1 form-switch">
<input
type="checkbox"
role="switch"
v-model="visible"
class="form-check-input ms-0"
:value="true"
:aria-label="$p.t('dashboard/widget_toggle_visibility')"
>
</div>
</Transition>
</header>
<!-- TODO Manu rename/remove-->
<div v-if="ready" class="card-body overflow-hidden p-0">
<component
:is="component"
v-model:shared-data="sharedData"
:config="arguments"
:width="width"
:height="height"
:widget-id="item_data.id"
:item_data="item_data"
@setConfig="setConfig"
@change="changeConfigManually"
></component>
</div> </div>
</height-transition> <div
</div> v-else
</div>`, class="card-body overflow-hidden text-center d-flex flex-column justify-content-center"
>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<bs-modal
v-if="hasConfig"
ref="config"
@hideBsModal="handleHideBsModal"
@showBsModal="handleShowBsModal"
>
<template v-slot:title>
{{ widgetTemplate ? $p.t('dashboard/widget_config_title', widgetTemplate.setup) : '' }}
</template>
<template v-slot:default>
<component
:is="component"
v-if="ready && !isLoading"
v-model:shared-data="sharedData"
:config="tmpConfig"
@change="changeConfig"
:configMode="true"
></component>
<div v-else class="text-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</template>
<template v-if="!widgetTemplate?.setup?.hideFooter" v-slot:footer>
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>{{ $p.t('ui/schliessen') }}</button>
<button
type="button"
class="btn btn-primary"
@click="changeConfig"
>{{ $p.t('ui/speichern') }}</button>
</template>
</bs-modal>
<height-transition>
<footer
v-if="editMode && isResizeable && !isPinned"
class="card-footer d-flex justify-content-end p-0"
>
<span
type="button"
drag-action="resize"
class="col-auto px-1"
:class="resizeClasses.button"
draggable="true"
aria-hidden="true"
:aria-label="$p.t('dashboard/widget_resize')"
v-tooltip="{ showDelay: 1000, value: $p.t('dashboard/widget_resize') }"
>
<i
class="fa-solid"
:class="resizeClasses.icon"
aria-hidden="true"
></i>
</span>
</footer>
</height-transition>
</template>
</article>`,
}; };
+136 -123
View File
@@ -1,9 +1,12 @@
import BsConfirm from "../Bootstrap/Confirm.js"; import BsConfirm from "../Bootstrap/Confirm.js";
import DropGrid from '../Drop/Grid.js' import DropGrid from '../Drop/Grid.js'
import DashboardItem from "./Item.js"; import DashboardItem from "./Item.js";
import { useCachedWidgetLoader } from "../../composables/Dashboard/CachedWidgetLoader.js";
import WidgetIcon from "./Widget/WidgetIcon.js" import WidgetIcon from "./Widget/WidgetIcon.js"
import dragClick from '../../directives/dragClick.js';
import ObjectUtils from "../../helpers/ObjectUtils.js";
export default { export default {
name: 'Section', name: 'Section',
components: { components: {
@@ -11,8 +14,11 @@ export default {
DashboardItem, DashboardItem,
WidgetIcon, WidgetIcon,
}, },
directives: {
dragClick
},
inject: { inject: {
widgetsSetup:{ widgetsSetup: {
type: Array, type: Array,
default: [], default: [],
}, },
@@ -39,9 +45,8 @@ export default {
configOpened: false, configOpened: false,
gridWidth: 1, gridWidth: 1,
gridHeight: null, gridHeight: null,
draggedItem:null, additionalRow: false
additionalRow:false, };
}
}, },
provide() { provide() {
return { return {
@@ -49,22 +54,40 @@ export default {
this.editModeIsActive this.editModeIsActive
), ),
sectionName: Vue.computed(() => this.name), sectionName: Vue.computed(() => this.name),
} };
}, },
computed: { computed: {
computedWidgetsSetup(){ sectionNameTranslation() {
if(!this.widgetsSetup) return {}; switch (this.name) {
return this.widgetsSetup.reduce((acc, setup)=>{ case "general":
acc[setup.widget_id] = setup.setup; return this.$p.t('dashboard', this.name);
case "custom":
return this.$p.t('dashboard', this.name);
default:
return this.name;
}
},
showSectionInformation() {
switch (this.name) {
case "general":
return this.$p.t('dashboard', 'dashboardGeneralSectionDescription');
case "custom":
return this.$p.t('dashboard', 'dashboardCustomSectionDescription');
default:
return this.$p.t('dashboard', 'dashboardSectionDescription', [this.name]);
}
},
indexedWidgetsTemplates() {
if (!this.widgetsSetup)
return {};
return this.widgetsSetup.reduce((acc, setup) => {
acc[setup.widget_id] = setup;
return acc; return acc;
},{}) }, {});
}, },
editModeIsActive() { editModeIsActive() {
return (this.editMode || this.adminMode) && !this.configOpened return (this.editMode || this.adminMode) && !this.configOpened
}, },
getSectionStyle() {
return 'margin-bottom: 8px;';
},
items() { items() {
// reuses the nearest placement of the widget from another viewport // reuses the nearest placement of the widget from another viewport
/* const computeNearestPlace = (item, gridWidth) =>{ /* const computeNearestPlace = (item, gridWidth) =>{
@@ -85,76 +108,55 @@ export default {
if(!item?.widgetid && item?.id){ if(!item?.widgetid && item?.id){
item.widgetid = item.id; item.widgetid = item.id;
} }
return { ...item, reorder: false, ...(item.place[this.gridWidth] || { reorder: true, ...{ x: 0, y: 0, w: 1, h: 1 } })};
let weight = 5;
if (!item.source)
weight = 6;
else if (item.source == 'general')
weight = 4;
let placement = item.place[this.gridWidth];
if (!placement) {
weight -= 3;
placement = {};
}
return { ...item, ...placement, weight };
}); });
return placedItems;
if (this.editModeIsActive)
}, return placedItems;
return placedItems.filter(item => !item.hidden);
}
},
watch: {
items() {
this.additionalRow = false;
}
}, },
methods: { methods: {
sectionNameTranslation(){
switch(this.name){
case "general":
return this.$p.t('dashboard',this.name);
break;
case "custom":
return this.$p.t('dashboard',this.name);
break;
default:
return this.name;
break;
}
},
showSectionInformation(){
if (this.name == "general"){
return this.$p.t('dashboard', 'dashboardGeneralSectionDescription');
}
else if(this.name == "custom"){
return this.$p.t('dashboard', 'dashboardCustomSectionDescription');
}
else{
return this.$p.t('dashboard', 'dashboardSectionDescription', [this.name]);
}
},
handleConfigOpened() { handleConfigOpened() {
this.configOpened = true this.configOpened = true
}, },
handleConfigClosed() { handleConfigClosed() {
this.configOpened = false this.configOpened = false
}, },
checkResizeLimit(item, w, h) {
// NOTE(chris): widgets needs to be loaded for this to work
let widget = this.widgetState[item.widget];
if (widget) {
let minmaxW = { ...widget.setup.width };
if (minmaxW.max)
minmaxW.min = minmaxW.min || 1;
else
minmaxW = { min: minmaxW, max: minmaxW };
if (w < minmaxW.min)
w = minmaxW.min;
if (w > minmaxW.max)
w = minmaxW.max;
let minmaxH = { ...widget.setup.height };
if (minmaxH.max)
minmaxH.min = minmaxH.min || 1;
else
minmaxH = { min: minmaxH, max: minmaxH };
if (h < minmaxH.min)
h = minmaxH.min;
if (h > minmaxH.max)
h = minmaxH.max;
}
return [w, h];
},
removeWidget(item, revert) { removeWidget(item, revert) {
if (item.custom) { if (item.custom) {
BsConfirm.popup(this.$p.t('dashboard', 'alert_deleteWidget')).then(() => this.$emit('widgetRemove', this.name, item.id)); BsConfirm.popup(this.$p.t('dashboard', 'alert_deleteWidget')).then(() => this.$emit('widgetRemove', item.id, this.name));
} else { } else {
let update = {}; let update = {};
update[item.id] = { hidden: !revert }; update[item.id] = { hidden: !revert };
if (!revert) {
// NOTE(chris): move to last line
update[item.id].place = [];
let y = this.gridHeight;
if (this.additionalRow)
y--;
update[item.id].place[this.gridWidth] = { x: 0, y };
}
this.updatePreset(update); this.updatePreset(update);
} }
}, },
@@ -163,49 +165,42 @@ export default {
payload[item.id] = { config }; payload[item.id] = { config };
this.updatePreset(payload); this.updatePreset(payload);
}, },
updatePositions(updated, pinned=false) { updatePositions(updated) {
let result = {}; let result = {};
updated.forEach(update => { updated.forEach(update => {
let item = structuredClone(ObjectUtils.deepToRaw(update.item));
let item = {...update.item};
if (!item.placeholder) {
if (!item.place[this.gridWidth])
item.place[this.gridWidth] = {x: 0, y: 0, w: 1, h: 1};
delete item.x;
delete item.y;
delete item.w;
delete item.h;
delete item.place[this.gridWidth].pinned;
if (update.x !== undefined)
item.place[this.gridWidth].x = update.x;
if (update.y !== undefined)
item.place[this.gridWidth].y = update.y;
if (update.w !== undefined)
item.place[this.gridWidth].w = update.w;
if (update.h !== undefined)
item.place[this.gridWidth].h = update.h;
if (pinned){
item.place[this.gridWidth].pinned = true;
}
result[item.id] = item; if (!item.placeholder) {
if (!item.place[this.gridWidth])
item.place[this.gridWidth] = { x: 0, y: 0, w: 1, h: 1 };
delete item.x;
delete item.y;
delete item.w;
delete item.h;
delete item.pinned;
delete item.weight;
if (update.x !== undefined)
item.place[this.gridWidth].x = update.x;
if (update.y !== undefined)
item.place[this.gridWidth].y = update.y;
if (update.w !== undefined)
item.place[this.gridWidth].w = update.w;
if (update.h !== undefined)
item.place[this.gridWidth].h = update.h;
if (update.pinned !== undefined)
item.place[this.gridWidth].pinned = update.pinned;
result[item.id] = item;
} }
}); });
this.updatePreset(result); this.updatePreset(result);
}, },
updatePreset(update) { updatePreset(update) {
let payload = {}; this.$emit('widgetUpdate', update, this.name);
payload[this.name] = update;
this.$emit('widgetUpdate', this.name, payload);
} }
}, },
setup() {
const { state: widgetState } = useCachedWidgetLoader();
return {
widgetState
};
},
mounted() { mounted() {
let self = this; let self = this;
let cont = self.$refs.container; let cont = self.$refs.container;
@@ -215,44 +210,62 @@ export default {
self.gridWidth = parseInt(window.getComputedStyle(cont).getPropertyValue('--fhc-dashboard-grid-size')); self.gridWidth = parseInt(window.getComputedStyle(cont).getPropertyValue('--fhc-dashboard-grid-size'));
}); });
}, },
template: ` template: /* html */`
<div class="dashboard-section position-relative pb-3 border-bottom" ref="container" :style="getSectionStyle"> <section
<h4 v-if="editModeIsActive" class=" mb-2"> class="dashboard-section position-relative pb-3 mb-3 border-bottom"
<i v-tooltip="showSectionInformation(name)" class="fa-solid fa-circle-info section-info" ></i> ref="container"
{{sectionNameTranslation()}}: :class="{ 'edit-active': editModeIsActive }"
</h4> >
<button v-tooltip="$p.t('dashboard','addLine')" v-if="!additionalRow && editModeIsActive" @click="additionalRow=true" class="btn btn-outline-secondary rounded-circle newGridRow d-flex justify-content-center align-items-center">+</button> <h3 v-if="adminMode" class="h4">
<drop-grid v-model:cols="gridWidth" v-model:additionalRow="additionalRow" :items="items" :itemsSetup="computedWidgetsSetup" :active="editModeIsActive" :resize-limit="checkResizeLimit" :margin-for-extra-row=".01" @draggedItem="draggedItem=$event" @rearrange-items="updatePositions" @gridHeight="gridHeight=$event" > <i v-tooltip="showSectionInformation" class="fa-solid fa-circle-info section-info"></i>
{{ sectionNameTranslation }}:
</h3>
<button
v-tooltip="$p.t('dashboard/addLine')"
v-if="!additionalRow && editModeIsActive"
class="btn btn-outline-secondary rounded-circle newGridRow d-flex justify-content-center align-items-center"
@click="additionalRow=true"
v-drag-click="() => additionalRow=true"
>+</button>
<drop-grid
v-model:cols="gridWidth"
:additional-row="additionalRow"
:items="items"
:items-setup="indexedWidgetsTemplates"
:active="editModeIsActive"
@rearrange-items="updatePositions"
@grid-height="gridHeight=$event"
>
<template #default="item"> <template #default="item">
<div v-if="item.placeholder" class="empty-tile-hover" @pointerdown="$emit('widgetAdd', name, { widget: 1, config: {}, place: {[gridWidth]: {x:item.x,y:item.y,w:1,h:1}}, custom: 1 })"></div> <div
v-if="item.placeholder"
class="empty-tile-hover"
@click="$emit('widgetAdd', { config: [], place: {[gridWidth]: { x: item.x, y: item.y, w: 1, h: 1 } }, custom: 1 }, name)"
></div>
<dashboard-item <dashboard-item
v-else v-else
:id="item.widget" :id="item.widget"
:dragstate="item.blank || (item.widgetid && item.widgetid == draggedItem?.data.widgetid)"
:resizeOverlay="item.resizeOverlay"
:widgetID="item.id"
:width="item.w" :width="item.w"
:height="item.h" :height="item.h"
:item_data="{config:item.config, custom:item.custom, h:item.h, w:item.w,id:item.id,reorder:item.reorder,place:item.place,widget:item.widget,widgetid:item.widgetid,x:item.x,y:item.y}" :item_data="{config:item.config, custom:item.custom, h:item.h, w:item.w,id:item.id,place:item.place,widget:item.widget,widgetid:item.widgetid,x:item.x,y:item.y}"
:loading="item.loading" :loading="item.loading"
:config="item.config" :config="item.config"
:custom="item.custom" :custom="item.custom"
:hidden="item.hidden" :hidden="item.hidden"
:editMode="editModeIsActive" :editMode="editModeIsActive"
:place="item.place[gridWidth]" :place="item.place[gridWidth]"
:setup="computedWidgetsSetup[item.widget]" :widget-template="indexedWidgetsTemplates[item.widget]"
:source="adminMode ? null : item.source || 'custom'"
@change="saveConfig($event, item)" @change="saveConfig($event, item)"
@remove="removeWidget(item, $event)" @remove="removeWidget(item, $event)"
@config-opened="handleConfigOpened" @config-opened="handleConfigOpened"
@config-closed="handleConfigClosed" @config-closed="handleConfigClosed"
@pinItem="updatePositions($event,true)" @pin-item="updatePositions"
@unPinItem="updatePositions"> @un-pin-item="updatePositions"
</dashboard-item> ></dashboard-item>
</template> </template>
</drop-grid> </drop-grid>
</div>` </section>`
} }
/* /*
@@ -32,19 +32,31 @@ export default {
}, },
}, },
template: `<div class="dashboard-widget-picker"> template: /* html */`
<bs-modal ref="modal" class="fade" :dialog-class="{'modal-fullscreen-sm-down': 1, 'modal-xl': widgets && widgets.length > 0}" @hiddenBsModal="close"> <div class="dashboard-widget-picker">
<template v-slot:title>Create new widget</template> <bs-modal
ref="modal"
class="fade"
:dialog-class="{ 'modal-fullscreen-sm-down': 1, 'modal-xl': widgets && widgets.length > 0 }"
@hiddenBsModal="close"
>
<template v-slot:title>{{ $p.t('dashboard/createWidget') }}</template>
<template v-slot:default> <template v-slot:default>
<div v-if="widgets" class="row g-2"> <div v-if="widgets" class="row g-2">
<div v-if="!widgets.length"> <div v-if="!widgets.length">
No Widgets available {{ $p.t('dashboard/noWidgetsAvailable') }}
</div> </div>
<div v-for="widget in widgets" :key="widget.widget_id" class="widget-icon-container col-sm-6 col-md-4 col-lg-3 col-xl-2"> <div
<widget-icon @select="pick" :widget="widget" ></widget-icon> v-for="widget in widgets"
:key="widget.widget_id"
class="widget-icon-container col-sm-6 col-md-4 col-lg-3 col-xl-2"
>
<widget-icon @select="pick" :widget="widget"></widget-icon>
</div> </div>
</div> </div>
<div v-else class="text-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></div> <div v-else class="text-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</template> </template>
</bs-modal> </bs-modal>
</div>` </div>`
@@ -5,7 +5,9 @@ export default {
"height", "height",
"configMode", "configMode",
"sharedData", "sharedData",
"widgetInfo" "widgetInfo",
"widgetId",
"item_data"
], ],
emits: [ emits: [
"setConfig", "setConfig",
@@ -11,9 +11,6 @@ export default {
mixins: [ mixins: [
AbstractWidget AbstractWidget
], ],
inject: [
"timezone"
],
methods: { methods: {
getPromiseFunc(start, end) { getPromiseFunc(start, end) {
return [ return [
@@ -27,6 +24,6 @@ export default {
}, },
template: /*html*/` template: /*html*/`
<div class="dashboard-widget-lvplan d-flex flex-column h-100"> <div class="dashboard-widget-lvplan d-flex flex-column h-100">
<fhc-calendar :timezone="timezone" :get-promise-func="getPromiseFunc" /> <fhc-calendar :get-promise-func="getPromiseFunc" />
</div>` </div>`
} }
+383 -120
View File
@@ -3,23 +3,27 @@ import FormInput from "../Form/Input.js";
import BsModal from "../Bootstrap/Modal.js"; import BsModal from "../Bootstrap/Modal.js";
import AbstractWidget from './Abstract.js'; import AbstractWidget from './Abstract.js';
import ApiBookmark from '../../api/factory/widget/bookmark.js'; import { useUrlStore } from '../../composables/Pseudostore/DashboardWidget/UrlStore.js';
export default { export default {
name: "WidgetsUrl", name: "WidgetsUrl",
mixins: [AbstractWidget],
inject: {
editModeIsActive: {
type: Boolean,
default: false
}
},
components:{ components:{
CoreForm, CoreForm,
FormInput, FormInput,
BsModal BsModal,
PvChips: primevue.chips,
PvMultiSelect: primevue.multiselect,
PvAutoComplete: primevue.autocomplete,
},
mixins: [AbstractWidget],
inject: {
adminMode: {
from: 'adminMode',
default: false
}
}, },
data: () => ({ data: () => ({
ready: false,
bookmark_id: null, bookmark_id: null,
title_input: "", title_input: "",
url_input: "", url_input: "",
@@ -27,29 +31,53 @@ export default {
invalidURL: false, invalidURL: false,
invalidTitel: false, invalidTitel: false,
}, },
selectedTags: [],
selectedFilters: [],
filteredArray: []
}), }),
computed: { computed: {
tagName() { availableTags() {
return this.config.tag !== undefined && this.config.tag.length > 0 return (this.bookmarks || [])
? this.config.tag .map(bookmark => JSON.parse(bookmark.tag))
: this.$p.t("bookmark", "myBookmarks"); .flat()
.filter((v, i, a) => v && a.indexOf(v) === i);
}, },
emptyBookmarks() { filteredBookmarks() {
if (this.shared instanceof Array && this.shared.length == 0) return true; if (!this.bookmarks)
return [];
if (!this.shared) return true; if (!this.config.tags || !this.config.tags.length)
return this.bookmarks;
return false; return this.bookmarks.filter(bookmark => {
const tags = JSON.parse(bookmark.tag || "[]");
return tags.some(tag => this.config.tags.includes(tag));
});
}, },
newSort() {
if (this.bookmarks.length == 0)
return 1;
else
return Math.max(...this.bookmarks.map(b => b.sort)) + 1;
},
maxSort() {
if (this.bookmarks.length == 0)
return 0;
else
return Math.max(...this.filteredBookmarks.map(b => b.sort));
},
minSort() {
if (this.bookmarks.length == 0)
return 0;
else
return Math.min(...this.filteredBookmarks.map(b => b.sort));
}
}, },
methods: { methods: {
stopDrag(event){ clearInputs() {
event.preventDefault();
},
clearInputs(){
this.title_input = ""; this.title_input = "";
this.url_input = ""; this.url_input = "";
this.selectedTags = [];
}, },
openCreateModal() { openCreateModal() {
this.$refs.createModal.show() this.$refs.createModal.show()
@@ -58,23 +86,23 @@ export default {
this.title_input = bookmark.title; this.title_input = bookmark.title;
this.url_input = bookmark.url; this.url_input = bookmark.url;
this.bookmark_id = bookmark.bookmark_id; this.bookmark_id = bookmark.bookmark_id;
this.$refs.editModal.show() this.selectedTags = JSON.parse(bookmark.tag);
this.$refs.editModal.show();
}, },
editBookmark(event){ editBookmark() {
event.preventDefault(); if (!this.bookmark_id || !this.url_input || !this.title_input)
if(!this.bookmark_id || !this.url_input || !this.title_input) return; return;
this.$api this.actions
.call(ApiBookmark.update({ .update(
bookmark_id: this.bookmark_id, this.bookmark_id,
title: this.title_input, this.title_input,
url: this.url_input, this.url_input,
})) this.selectedTags
.then((res) => res.data) )
.then((result) => { .then(() => {
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkUpdated")); this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkUpdated"));
// refetch the bookmarks to see the updates
this.fetchBookmarks();
// reset the values for the title and url inputs // reset the values for the title and url inputs
this.clearInputs(); this.clearInputs();
this.$refs.editModal.hide(); this.$refs.editModal.hide();
@@ -82,10 +110,7 @@ export default {
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
insertBookmark() {
insertBookmark(event) {
event.preventDefault();
// reset is-invalid css on url input field // reset is-invalid css on url input field
for (let key of Object.keys(this.validation)) { for (let key of Object.keys(this.validation)) {
this.validation[key] = false; this.validation[key] = false;
@@ -94,24 +119,24 @@ export default {
// early return if validation failed // early return if validation failed
if (!this.isValidationSuccessfull()) return; if (!this.isValidationSuccessfull()) return;
this.$api // get highest Sort
.call(ApiBookmark.insert({ const sort = this.newSort;
tag: this.config.tag,
title: this.title_input, this.actions
url: this.url_input, .insert(
})) this.title_input,
.then((res) => res.data) this.url_input,
.then((result) => { this.selectedTags,
sort
)
.then(() => {
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkAdded")); this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkAdded"));
// refetch the bookmarks to see the updates
this.fetchBookmarks();
this.$refs.createModal.hide(); this.$refs.createModal.hide();
// reset the values for the title and url inputs // reset the values for the title and url inputs
this.clearInputs(); this.clearInputs();
}) })
.catch(this.$fhcAlert.handleSystemError); .catch(this.$fhcAlert.handleSystemError);
}, },
isValidationSuccessfull() { isValidationSuccessfull() {
// validate the input fields // validate the input fields
if (this.title_input.length === 0) { if (this.title_input.length === 0) {
@@ -125,111 +150,349 @@ export default {
return !Object.values(this.validation).some(value => value === true); return !Object.values(this.validation).some(value => value === true);
}, },
async fetchBookmarks() {
await this.$api
.call(ApiBookmark.getBookmarks())
.then((res) => res.data)
.then((result) => {
this.shared = result;
})
.catch(this.$fhcAlert.handleSystemError);
},
async removeLink(bookmark_id) { async removeLink(bookmark_id) {
let isConfirmed = await this.$fhcAlert.confirmDelete(); let isConfirmed = await this.$fhcAlert.confirmDelete();
// early return if the confirm dialog was not confirmed // early return if the confirm dialog was not confirmed
if (!isConfirmed) return; if (!isConfirmed)
return;
this.$api const errors = await this.actions.remove(bookmark_id);
.call(ApiBookmark.delete(bookmark_id))
.then((res) => res.data) if (!errors) {
.then((result) => { this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkDeleted"));
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkDeleted")); }
// refetch the bookmarks to see the updates
this.fetchBookmarks();
})
.catch(this.$fhcAlert.handleSystemError);
}, },
sortDown(bookmark_id) {
const current = this.filteredBookmarks.find(b => b.bookmark_id === bookmark_id);
const next = this.filteredBookmarks
.filter(b => b.sort > current.sort)
.sort((a, b) => a.sort - b.sort)[0];
if (!next) {
console.log("lowest sort item, no change");
return;
}
this.actions.swap(current.bookmark_id, next.bookmark_id);
},
sortUp(bookmark_id) {
const current = this.filteredBookmarks.find(b => b.bookmark_id === bookmark_id);
const next = this.filteredBookmarks
.filter(b => b.sort < current.sort)
.sort((a, b) => a.sort + b.sort)[0];
if (!next) {
console.log("highest sort item, no change");
return;
}
this.actions.swap(current.bookmark_id, next.bookmark_id);
},
hasTags(link) {
if (!link || !link.tag) return false;
let tags = link.tag;
if (typeof tags === 'string') {
try {
tags = JSON.parse(tags)
} catch {
return false;
}
}
if (Array.isArray(tags) && tags.length > 0) {
return tags.join(' ');
}
},
openFilterModal() {
if (this.config.tags && this.config.tags.length)
this.selectedFilters = [ ...this.config.tags ];
else
this.selectedFilters = [];
this.$refs.filterModal.show();
},
async handleAddingTagFilter() {
this.config.tags = this.selectedFilters;
this.$emit('change');
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "filterUpdated"));
this.$refs.filterModal.hide();
},
search(event) {
const query = event.query ?? "";
// Filter for text
this.filteredArray = this.availableTags.filter(item =>
item.toLowerCase().includes(query.toLowerCase())
);
// input if search not successful
if (this.filteredArray.length === 0 && query) {
this.filteredArray = [query];
}
}
},
setup() {
const {
bookmarks,
getters: { tags },
actions
} = useUrlStore();
return { bookmarks, tags, actions }
}, },
async mounted() { async mounted() {
await this.fetchBookmarks(); if (!this.adminMode) {
}, await this.actions.fetch();
created() { this.ready = true;
// }
// this.$emit('setConfig', true); -> use this to enable widget config mode if needed
}, },
template: /*html*/ ` template: /*html*/ `
<div class="widgets-url w-100 h-100 overflow-auto" style="padding: 1rem 1rem;"> <div class="widgets-url w-100 h-100 overflow-auto p-3">
<div class="d-flex flex-column justify-content-between">
<button class="btn btn-outline-secondary btn-sm w-100 mt-2 card" @click="openCreateModal" type="button">{{$p.t('bookmark','newLink')}}</button>
<template v-if="shared"> <div v-if="!adminMode" class="d-flex mt-2">
<button
class="btn btn-outline-secondary btn-sm flex-grow-1 me-2"
@click="openCreateModal"
>
{{ $p.t('bookmark', 'newLink') }}
</button>
<button
v-if="config.tags && config.tags.length"
class="btn btn-secondary btn-sm"
:title="$p.t('bookmark/editFilter')"
@click="openFilterModal"
>
<i class="fa-solid fa-filter-circle-xmark"></i>
</button>
<button
v-else
class="btn btn-outline-secondary btn-sm"
:title="$p.t('bookmark/filterByTags')"
@click="openFilterModal"
>
<i class="fa-solid fa-filter"></i>
</button>
</div>
<template v-if="!emptyBookmarks"> <div
<div v-for="link in shared" :key="link.id" class="d-flex mt-2"> v-if="adminMode"
<a target="_blank" :href="link.url"> class="h-100 d-flex align-items-center text-center"
<i class="fa fa-solid fa-arrow-up-right-from-square me-1"></i>{{ link.title }} >
{{ $p.t('bookmark/adminMode') }}
</div>
<template v-else-if="ready">
<template v-if="filteredBookmarks.length">
<div
v-for="link in filteredBookmarks"
:key="link.id"
class="d-flex mt-2"
>
<a target="_blank" :href="link.url" class="me-1">
<i
class="fa fa-solid fa-arrow-up-right-from-square me-1"
aria-hidden="true"
></i>
{{ link.title }}
</a>
<span
v-if="hasTags(link)"
:title="hasTags(link)"
style="color: silver;"
>
<i
class="fa fa-solid fa-tag text-gray-500"
aria-hidden="true"
></i>
</span>
<div class="ms-auto">
<!--EDIT BOOKMARK-->
<a
type="button"
href="#"
:title="$p.t('bookmark/editBookmark')"
aria-label="edit bookmark"
@click.prevent="openEditModal(link)"
>
<i class="fa fa-edit me-1" aria-hidden="true"></i>
</a>
<!--DELETE BOOKMARK-->
<a
type="button"
href="#"
:title="$p.t('bookmark/deleteBookmark')"
aria-label="delete bookmark"
@click.prevent="removeLink(link.bookmark_id)"
>
<i class="fa fa-regular fa-trash-can" aria-hidden="true"></i>
</a>
<!--SORT BOOKMARKS-->
<a
v-if="filteredBookmarks.length > 1"
type="button"
href="#"
:title="$p.t('bookmark/sortDownwards')"
aria-label="sortdown bookmark"
@click.prevent="sortDown(link.bookmark_id)"
>
<i
class="fa fa-arrow-down me-1"
:class="{ 'text-light pointer-events-none': link.sort === maxSort }"
></i>
</a>
<a
v-if="filteredBookmarks.length > 1"
type="button"
href="#"
:title="$p.t('bookmark/sortToTop')"
aria-label="sortup bookmark"
@click.prevent="sortUp(link.bookmark_id)"
>
<i
class="fa fa-arrow-up me-1"
:class="{ 'text-light pointer-events-none': link.sort === minSort }"
></i>
</a> </a>
<div class="ms-auto">
<!--EDIT BOOKMARK-->
<a type="button" href="#" @click.prevent="openEditModal(link)" aria-label="edit bookmark" v-tooltip="{showDelay:1000,value:'edit bookmark'}">
<i class="fa fa-edit me-1" aria-hidden="true"></i>
</a>
<!--DELETE BOOKMARK-->
<a type="button" id="deleteBookmark" href="#" aria-label="delete bookmark" v-tooltip="{showDelay:1000,value:'delete bookmark'}" @click.prevent="removeLink(link.bookmark_id)">
<i class="fa fa-regular fa-trash-can" aria-hidden="true"></i>
</a>
</div>
</div> </div>
</template> </div>
</template>
<div v-else class="d-flex mt-2"> <div v-else class="d-flex mt-2">
<span>{{$p.t('bookmark','emptyBookmarks')}}</span> <span>{{ $p.t('bookmark', 'emptyBookmarks') }}</span>
</div> </div>
</template>
</template> <template v-else>
<p v-for="i in 4" class="placeholder-glow">
<template v-else> <span class="placeholder col-9"></span>
<p v-for="i in 4" class="placeholder-glow"> </p>
<span class="placeholder" :class="{'col-9' : true}"></span> </template>
</p> </div>
</template>
</div>
</div>
<!--EDIT MODAL--> <!--EDIT MODAL-->
<teleport to="body"> <teleport to="body">
<bs-modal @[\`hide.bs.modal\`]="bookmark_id=null; clearInputs();" ref="editModal"> <bs-modal
ref="editModal"
@hide-bs-modal="bookmark_id=null; clearInputs();"
>
<template #title> <template #title>
<h2>{{$p.t('bookmark','editLink')}}</h2> <h2>{{ $p.t('bookmark', 'editLink') }}</h2>
</template> </template>
<template #default> <template #default>
<form-input :label="$p.t('profil','Titel')" :title="$p.t('profil','Titel')" id="editTitle" v-model="title_input" name="title" class="mb-2"></form-input>
<form-input label="Url" title="Url" id="editUrl" v-model="url_input" name="url"></form-input> <form-input
id="editTitle"
name="title"
v-model="title_input"
:label="$p.t('profil', 'Titel')"
:title="$p.t('profil', 'Titel')"
class="mb-2"
></form-input>
<form-input
id="editUrl"
name="url"
v-model="url_input"
label="Url"
title="Url"
></form-input>
<label class="mt-2">Tags</label>
<div class="mt-2">
<pv-auto-complete
v-model="selectedTags"
multiple
dropdown
:suggestions="filteredArray"
@complete="search"
/>
</div>
</template> </template>
<template #footer> <template #footer>
<button @click="editBookmark" class="btn btn-primary">{{$p.t('bookmark','saveLink')}}</button> <button class="btn btn-primary" @click.prevent="editBookmark">
{{ $p.t('bookmark', 'saveLink') }}
</button>
</template> </template>
</bs-modal> </bs-modal>
</teleport> </teleport>
<!--CREATE MODAL--> <!--CREATE MODAL-->
<teleport to="body"> <teleport to="body">
<bs-modal @[\`hide.bs.modal\`]="clearInputs();" ref="createModal"> <bs-modal ref="createModal" @hide-bs-modal="clearInputs();">
<template #title> <template #title>
<h2>{{$p.t('bookmark','newLink')}}</h2> <h2>{{ $p.t('bookmark', 'newLink') }}</h2>
</template> </template>
<template #default> <template #default>
<form-input :label="$p.t('profil','Titel')" :title="$p.t('profil','Titel')" id="insertTitle" v-model="title_input" name="title" class="mb-2"></form-input>
<form-input label="Url" title="Url" id="insertUrl" v-model="url_input" name="url"></form-input> <form-input
id="insertTitle"
name="title"
v-model="title_input"
:label="$p.t('profil', 'Titel')"
:title="$p.t('profil', 'Titel')"
class="mb-2"
></form-input>
<form-input
id="insertUrl"
name="url"
v-model="url_input"
label="Url"
title="Url"
></form-input>
<label class="mt-2">Tags</label>
<div class="mt-2">
<pv-auto-complete
v-model="selectedTags"
multiple
dropdown
:suggestions="filteredArray"
@complete="search"
/>
</div>
</template> </template>
<template #footer> <template #footer>
<button @click="insertBookmark" class="btn btn-primary">{{$p.t('bookmark','saveLink')}}</button> <button class="btn btn-primary" @click.prevent="insertBookmark">
{{ $p.t('bookmark', 'saveLink') }}
</button>
</template>
</bs-modal>
</teleport>
<!--FILTER MODAL-->
<teleport to="body">
<bs-modal ref="filterModal" @hide-bs-modal="clearInputs();">
<template #title>
<h2>{{ $p.t('bookmark', 'headerFilterBookmark') }}</h2>
</template>
<template #default>
<div class="mt-2 row">
<div class="col-10">
<pv-multi-select
id="tagFilterUrl"
v-model="selectedFilters"
:options="availableTags"
display="chip"
:placeholder="$p.t('bookmark', 'noFilter')"
:maxSelectedLabels="3"
class="p-inputtext-sm w-100 me-2"
/>
</div>
</div>
</template>
<template #footer>
<button
class="btn btn-secondary"
:title="$p.t('bookmark', 'filterByTags')"
@click="handleAddingTagFilter()"
>
{{ $p.t('ui/ok') }}
</button>
</template> </template>
</bs-modal> </bs-modal>
</teleport> </teleport>
`, `,
}; };
/* /*
Link JSON structure: Link JSON structure:
{ {
File diff suppressed because it is too large Load Diff
+9 -71
View File
@@ -1,88 +1,26 @@
export default { export default {
name:'GridItem', name:'GridItem',
components: {
},
inject: {
},
props: { props: {
item: Object, item: Object
active: Boolean
}, },
emits: [ emits: [
"mouseDown",
"mouseUp",
"startMove", "startMove",
"startResize", "startResize"
"dragging",
"endDrag",
"dropDrag",
"item",
"touchStart",
"touchEnd",
], ],
data() {
return {
dragAction: '',
dragging: false
}
},
computed: {
},
methods: { methods: {
registerDragAction(evt) { tryDragStart(evt) {
this.$emit('mouseDown', evt); let dragAction = evt.target.getAttribute('drag-action');
if (evt.target.hasAttribute('drag-action')) {
this.dragAction = evt.target.getAttribute('drag-action');
} else {
let parent = evt.target.closest('[drag-action]');
if (parent) {
this.dragAction = parent.getAttribute('drag-action');
} else {
this.dragAction = '';
}
}
},
tryDragStart(evt, item) {
let dragAction = this.dragAction || evt.target.getAttribute('drag-action');
if (dragAction) { if (dragAction) {
this.dragging = true;
if (dragAction == 'move') if (dragAction == 'move')
return this.$emit('startMove', evt, item); return this.$emit('startMove', evt, this.item);
else if (dragAction == 'resize') else if (dragAction == 'resize')
return this.$emit('startResize', evt, item); return this.$emit('startResize', evt, this.item);
}
//evt.preventDefault();
},
touchDragEnd(evt) {
if (!this.dragging)
return;
this.dragging = false;
this.$emit('touchEnd', evt);
},
touchStart(event){
this.$emit('touchStart', event);
this.registerDragAction(event);
this.tryDragStart(event, this.item);
},
touchMove(event){
if(this.dragging){
event.preventDefault();
this.$emit('dragging', event);
} }
} }
}, },
template: ` template: /* html */`
<div class="drop-grid-item" <li class="drop-grid-item" @dragstart="tryDragStart">
@mousedown="registerDragAction"
@mouseup="$emit('mouseUp', $event)"
@touchstart="touchStart"
@touchend="touchDragEnd"
@dragstart="tryDragStart($event, item)"
@drag="$emit('dragging',$event)"
@touchmove="touchMove"
@dragend="$emit('endDrag', $event); dragging = false"
:draggable="active && !item.placeholder">
<slot v-bind="item"></slot> <slot v-bind="item"></slot>
</div>` </li>`
} }
+52 -29
View File
@@ -1,37 +1,60 @@
export default { export default {
components: { components: {
paginator: primevue.paginator, paginator: primevue.paginator,
}, },
emits: ["update:rows"], emits: ["pageUpdated"],
props: { props: {
maxPageCount: { maxPageCount: {
type: Number, type: Number,
default: 0, default: 0,
}, },
page_size: { page_size: {
type: Number, type: Number,
required: true, required: true,
}, },
}, page: {
data() { type: Number | null,
return {}; default: null,
}, },
methods: { },
newPageEvent: function (data) { data() {
return {
}, first: 0,
}, rowsPerPageOptions: [10, 20, 30],
mounted() {}, };
template: /*html*/ ` },
<!-- Desktop --> watch: {
page(newValue) {
this.first = (newValue - 1) * this.$props.page_size;
},
},
methods: {
afterPageUpdated(data) {
this.$emit("pageUpdated", { ...data, page: data.page + 1 });
this.first = data.page * this.$props.page_size;
},
},
template: /*html*/ `
<!-- Desktop -->
<div class="d-none d-md-block"> <div class="d-none d-md-block">
<paginator v-model:rows="page_size" @page="(data)=>$emit('page',{...data, page:data.page+1})" :rows="page_size" :totalRecords="maxPageCount" :rowsPerPageOptions="[10, 20, 30]" > <paginator
</paginator> v-model:rows="page_size"
v-model:first="first"
@page="afterPageUpdated($event)"
:totalRecords="maxPageCount"
:rowsPerPageOptions="rowsPerPageOptions"
></paginator>
</div> </div>
<!-- Mobile --> <!-- Mobile -->
<div class="d-block d-md-none"> <div class="d-block d-md-none">
<paginator v-model:rows="page_size" @page="(data)=>$emit('page',{...data, page:data.page+1})" :rows="page_size" :totalRecords="maxPageCount" :rowsPerPageOptions="[10, 20, 30]" template="FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink RowsPerPageDropdown"> <paginator
</paginator> v-model:rows="page_size"
v-model:first="first"
@page="afterPageUpdated($event)"
:totalRecords="maxPageCount"
:rowsPerPageOptions="rowsPerPageOptions"
template="FirstPageLink PrevPageLink CurrentPageReport NextPageLink LastPageLink RowsPerPageDropdown"
></paginator>
</div> </div>
`, `,
}; };
+351 -303
View File
@@ -21,69 +21,71 @@ export default {
dms, dms,
cms, cms,
mergedStudent, mergedStudent,
mergedPerson mergedPerson,
}, },
props: { props: {
searchoptions: { searchoptions: {
type: Object, type: Object,
required: true required: true,
}, },
searchfunction: { searchfunction: {
type: Function, type: Function,
required: true required: true,
}, },
showBtnSubmit: Boolean showBtnSubmit: Boolean,
}, },
provide() { provide() {
return { return {
query: Vue.computed(() => this.lastQuery) query: Vue.computed(() => this.lastQuery),
}; };
}, },
data: function() { inject: ["isMobile"],
return { data: function () {
searchtimer: null, return {
hidetimer: null, searchtimer: null,
searchsettings: { hidetimer: null,
searchstr: this.getSearchStr(), searchsettings: {
types: this.getInitiallySelectedTypes(), searchstr: this.getSearchStr(),
}, types: this.getInitiallySelectedTypes(),
searchresult: [], },
searchmode: '', searchresult: [],
showresult: false, searchmode: "",
searching: false, showresult: false,
error: null, searching: false,
abortController: null, error: null,
abortController: null,
settingsDropdown: null, settingsDropdown: null,
lastQuery: '' lastQuery: "",
}; isSearchShownInMobileView: false,
}, };
},
computed: { computed: {
searchTypesPlaceholder() { searchTypesPlaceholder() {
if (!this.searchsettings.types.length) { if (!this.searchsettings.types.length) {
return Object.values(this.typeLabels).join(' / '); return Object.values(this.typeLabels).join(" / ");
} }
return this.searchsettings.types.map(type => this.typeLabels[type]).join(' / '); return this.searchsettings.types
.map((type) => this.typeLabels[type])
.join(" / ");
}, },
types() { types() {
if (!this.searchoptions.types) if (!this.searchoptions.types) return [];
return [];
if (Array.isArray(this.searchoptions.types)) if (Array.isArray(this.searchoptions.types))
return this.searchoptions.types; return this.searchoptions.types;
return Object.keys(this.searchoptions.types); return Object.keys(this.searchoptions.types);
}, },
typeLabels() { typeLabels() {
if (!this.searchoptions.types) if (!this.searchoptions.types) return {};
return {};
if (Array.isArray(this.searchoptions.types)) { if (Array.isArray(this.searchoptions.types)) {
return this.searchoptions.types.reduce((res, type) => { return this.searchoptions.types.reduce((res, type) => {
res[type] = type; res[type] = type;
return res return res;
}, {}); }, {});
} }
return this.searchoptions.types; return this.searchoptions.types;
} },
}, },
template: /*html*/` template: /*html*/ `
<form <form
ref="searchform" ref="searchform"
class="d-flex me-3" class="d-flex me-3"
@@ -92,80 +94,91 @@ export default {
@focusin="searchfocusin" @focusin="searchfocusin"
@focusout="searchfocusout" @focusout="searchfocusout"
> >
<div <span
ref="searchbox" v-if="isMobile"
class="h-100 input-group me-2 searchbar_searchbox" type="button"
:class="showresult ? 'open' : 'closed'" data-bs-toggle="collapse"
data-bs-target=".multi-collapse"
aria-controls="header-searchbar-collapsible header-options-collapsible header-usermenu-collapsible"
aria-expanded="false"
class="d-flex flex-row align-items-center ps-3 pe-1"
> >
<span class="input-group-text"> <i v-if="isSearchShownInMobileView" class="fa-solid fa-chevron-left"></i>
<i class="fa-solid fa-magnifying-glass"></i> <i v-else class="fa-solid fa-magnifying-glass"></i>
</span> </span>
<input
ref="input"
@keyup="search"
@focus="showsearchresult"
v-model="searchsettings.searchstr"
class="form-control searchbar_input"
type="search"
:placeholder="$p.t('search/input_search_label', { types: searchTypesPlaceholder })"
:aria-label="$p.t('search/input_search_label', { types: searchTypesPlaceholder })"
>
<button
v-if="searchsettings.searchstr"
type="button"
class="searchbar_input_clear btn btn-outline-secondary"
@click="clearInput"
@focusin.stop
>
<i class="fas fa-close"></i>
</button>
<button
v-if="showBtnSubmit"
type="submit"
class="btn btn-primary"
:title="$p.t('search/submit')"
:aria-label="$p.t('search/submit')"
>
<i class="fas fa-search"></i>
</button>
<button
data-bs-toggle="collapse"
data-bs-target="#searchSettings"
aria-expanded="false"
aria-controls="searchSettings"
ref="settingsbutton"
class="searchbar_setting_btn btn btn-secondary"
type="button"
:title="$p.t('search/button_filter_label')"
:aria-label="$p.t('search/button_filter_label')"
>
<i class="fas fa-cog"></i>
</button>
</div>
<div v-show="showresult" <div
class="searchbar_results" tabindex="-1"> :class="{'flex-grow-1': !isMobile, 'collapse multi-collapse collapse-horizontal': isMobile}"
<div class="searchbar_results_scroller" ref="result"> id="header-searchbar-collapsible"
<div class="searchbar_results_wrapper" ref="results"> @[\`show.bs.collapse\`]="isSearchShownInMobileView = true"
<div v-if="searching"> @[\`hidden.bs.collapse\`]="isSearchShownInMobileView = false"
<i class="fas fa-spinner fa-spin fa-2x"></i> >
</div> <div
<div v-else-if="this.error !== null">{{ error }}</div> :class="{open: showresult, closed: showresult, 'px-3': isMobile}"
<div v-else-if="searchresult.length < 1">{{ $p.t('search/error_no_results') }}</div> ref="searchbox"
<template v-else v-for="res in searchresult"> class="h-100 input-group me-2 searchbar_searchbox"
<component :style="isMobile ? 'width: ' + getMaxWidthOfSearchbarInMobileView() : ''"
v-if="isValidRenderer(res.renderer)" >
:is="res.renderer" <span class="input-group-text">
:mode="searchmode" <i class="fa-solid fa-magnifying-glass color-white"></i>
:res="res" </span>
:actions="getActions(res)" <input
@actionexecuted="hideresult" ref="input"
></component> @keyup="search"
<div v-else class="searchbar-result text-danger fw-bold">{{ $p.t('search/error_unknown_type', res) }}</div> @focus="showsearchresult"
</template> v-model="searchsettings.searchstr"
</div> class="form-control searchbar_input"
</div> type="search"
</div> :placeholder="$p.t('search/input_search_label', { types: searchTypesPlaceholder })"
:aria-label="$p.t('search/input_search_label', { types: searchTypesPlaceholder })"
>
<button
v-if="showBtnSubmit"
type="submit"
class="btn btn-primary"
:title="$p.t('search/submit')"
:aria-label="$p.t('search/submit')"
>
<i class="fas fa-search"></i>
</button>
<button
data-bs-toggle="collapse"
data-bs-target="#searchSettings"
aria-expanded="false"
aria-controls="searchSettings"
ref="settingsbutton"
class="searchbar_setting_btn btn btn-secondary"
type="button"
:title="$p.t('search/button_filter_label')"
:aria-label="$p.t('search/button_filter_label')"
>
<i class="fas fa-filter"></i>
</button>
</div>
<div v-show="showresult"
class="searchbar_results" tabindex="-1">
<div class="searchbar_results_scroller" ref="result">
<div class="searchbar_results_wrapper" ref="results">
<div v-if="searching">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</div>
<div v-else-if="this.error !== null">{{ error }}</div>
<div v-else-if="searchresult.length < 1">{{ $p.t('search/error_no_results') }}</div>
<template v-else v-for="res in searchresult">
<component
v-if="isValidRenderer(res.renderer)"
:is="res.renderer"
:mode="searchmode"
:res="res"
:actions="getActions(res)"
@actionexecuted="hideresult"
></component>
<div v-else class="searchbar-result text-danger fw-bold">{{ $p.t('search/error_unknown_type', res) }}</div>
</template>
</div>
</div>
</div>
</div>
<div <div
id="searchSettings" id="searchSettings"
@@ -207,34 +220,42 @@ export default {
</div> </div>
</form> </form>
`, `,
watch:{ watch: {
'searchsettings.searchstr': function (newSearchValue) { "searchsettings.searchstr": function (newSearchValue) {
if(this.searchoptions.origin){ if (this.searchoptions.origin) {
sessionStorage.setItem(`${this.searchoptions.origin}_searchstr`,newSearchValue); sessionStorage.setItem(
`${this.searchoptions.origin}_searchstr`,
newSearchValue,
);
} }
}, },
'searchsettings.types'(newValue) { "searchsettings.types"(newValue) {
if (Array.isArray(newValue) && newValue.length === 0) { if (Array.isArray(newValue) && newValue.length === 0) {
this.searchsettings.types = [...this.types]; this.searchsettings.types = [...this.types];
} }
// stores the search types in the localstorage, only if the newValue is also an array // stores the search types in the localstorage, only if the newValue is also an array
if (Array.isArray(newValue) && this.searchoptions.origin) { if (Array.isArray(newValue) && this.searchoptions.origin) {
localStorage.setItem(`${this.searchoptions.origin}_searchtypes`, JSON.stringify(newValue)); localStorage.setItem(
`${this.searchoptions.origin}_searchtypes`,
JSON.stringify(newValue),
);
} }
this.search(); this.search();
} },
}, },
mounted(){ mounted() {
this.settingsDropdown = new bootstrap.Collapse(this.$refs.settings, { this.settingsDropdown = new bootstrap.Collapse(this.$refs.settings, {
toggle: false toggle: false,
}); });
if (!this.searchoptions.origin){ if (!this.searchoptions.origin) {
console.warn("No origin defined in the searchoptions for the searchbar, please define the origin property in the searchbaroptions to allow reliable storage of searchstr and searchtypes accross applications."); console.warn(
"No origin defined in the searchoptions for the searchbar, please define the origin property in the searchbaroptions to allow reliable storage of searchstr and searchtypes accross applications.",
);
} }
}, },
updated() { updated() {
if(this.showresult) { if (this.showresult) {
Vue.nextTick(() => { Vue.nextTick(() => {
this.calcSearchResultHeight(); this.calcSearchResultHeight();
}); });
@@ -249,32 +270,34 @@ export default {
getInitiallySelectedTypes() { getInitiallySelectedTypes() {
let result = false; let result = false;
if (this.searchoptions.origin) { if (this.searchoptions.origin) {
let localStorageValue = localStorage.getItem(`${this.searchoptions.origin}_searchtypes`); let localStorageValue = localStorage.getItem(
`${this.searchoptions.origin}_searchtypes`,
);
if (localStorageValue) { if (localStorageValue) {
result = JSON.parse(localStorageValue); result = JSON.parse(localStorageValue);
} }
} }
if (result) if (result) return result;
return result; if (!this.searchoptions.types) return [];
if (!this.searchoptions.types)
return [];
if (Array.isArray(this.searchoptions.types)) if (Array.isArray(this.searchoptions.types))
return [...this.searchoptions.types]; return [...this.searchoptions.types];
return Object.keys(this.searchoptions.types); return Object.keys(this.searchoptions.types);
}, },
getSearchStr: function(){ getSearchStr: function () {
if (!this.searchoptions.origin) if (!this.searchoptions.origin) return "";
return ''; return (
return sessionStorage.getItem(`${this.searchoptions.origin}_searchstr`) ?? ''; sessionStorage.getItem(
`${this.searchoptions.origin}_searchstr`,
) ?? ""
);
}, },
checkSettingsVisibility: function(event) { checkSettingsVisibility: function (event) {
// hides the settings collapsible if the user clicks somewhere else // hides the settings collapsible if the user clicks somewhere else
if (!this.$refs.settings.contains(event.target)) if (!this.$refs.settings.contains(event.target)) {
{
this.settingsDropdown.hide(); this.settingsDropdown.hide();
} }
}, },
handleShowSettings: function() { handleShowSettings: function () {
// adds the event listener checkSettingsVisibility only when the collapsible is shown // adds the event listener checkSettingsVisibility only when the collapsible is shown
document.addEventListener("click", this.checkSettingsVisibility); document.addEventListener("click", this.checkSettingsVisibility);
}, },
@@ -282,183 +305,208 @@ export default {
// removes the event listener checkSettingsVisibility when the collapsible is hidden // removes the event listener checkSettingsVisibility when the collapsible is hidden
document.removeEventListener("click", this.checkSettingsVisibility); document.removeEventListener("click", this.checkSettingsVisibility);
}, },
calcSearchResultHeight: function() { calcSearchResultHeight: function () {
const rect = this.$refs.results.getBoundingClientRect(); const rect = this.$refs.results.getBoundingClientRect();
if( rect.height > 0 && rect.height < (window.innerHeight * 0.8) ) { if (rect.height > 0 && rect.height < window.innerHeight * 0.8) {
this.$refs.result.style.height = Math.ceil(rect.height + 16) + 'px'; this.$refs.result.style.height =
Math.ceil(rect.height + 16) + "px";
} else { } else {
this.$refs.result.style.height = Math.floor(window.innerHeight * 0.8) + 'px'; this.$refs.result.style.height =
Math.floor(window.innerHeight * 0.8) + "px";
} }
}, },
calcSearchResultExtent: function() { calcSearchResultExtent: function () {
if(!this.showresult) { if (!this.showresult) {
return; return;
} }
if(this.searchoptions?.calcheightonly === undefined if (
|| this.searchoptions.calcheightonly === false) { this.searchoptions?.calcheightonly === undefined ||
this.searchoptions.calcheightonly === false
) {
var rect = this.$refs.searchbox.getBoundingClientRect(); var rect = this.$refs.searchbox.getBoundingClientRect();
this.$refs.result.style.top = Math.floor(rect.bottom + 3) + 'px'; this.$refs.result.style.top =
this.$refs.result.style.right = Math.floor(rect.right) + 'px'; Math.floor(rect.bottom + 3) + "px";
this.$refs.result.style.width = Math.floor(rect.width) + 'px'; this.$refs.result.style.right = Math.floor(rect.right) + "px";
this.$refs.result.style.width = Math.floor(rect.width) + "px";
} }
this.calcSearchResultHeight(); this.calcSearchResultHeight();
}, },
search: function() { search: function () {
if(this.searchoptions?.nolivesearch === true) return; if (this.searchoptions?.nolivesearch === true) return;
this.abort(); this.abort();
if( this.searchsettings.searchstr.length >= 2 ) { if (this.searchsettings.searchstr.length >= 2) {
this.calcSearchResultExtent();
this.searchtimer = setTimeout(
this.callsearchapi,
500
);
} else {
this.showresult = false;
}
},
abort() {
if (this.searchtimer !== null) {
clearTimeout(this.searchtimer);
}
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
this.searchresult = [];
},
callsearchapi: function() {
this.error = null;
this.searchresult.splice(0, this.searchresult.length);
this.searching = true;
this.showsearchresult();
if(this.searchsettings.types.length === 0) {
this.error = this.$p.t('search/error_missing_type');
this.searching = false;
return;
}
if (this.abortController)
this.abortController.abort();
this.abortController = new AbortController();
this.searchfunction(this.searchsettings, { timeout: 50000, signal: this.abortController.signal })
.then(response=>{
if (!response.data) {
this.error = this.$p.t('search/error_general');
} else {
let res = response.data.map(el => el.data ? {...el, ...JSON.parse(el.data)} : el);
this.lastQuery = response.meta.searchstring;
if (this.searchoptions.mergeResults) {
let counter = 0;
let mergeTypes = [];
let mergedType = 'merged-';
let mergeKey = '';
switch (this.searchoptions.mergeResults) {
case 'student':
mergeTypes = ['student', 'prestudent'];
mergedType += this.searchoptions.mergeResults;
mergeKey = 'uid';
break;
case 'person':
mergeTypes = ['person', 'employee', 'student', 'prestudent'];
mergedType += this.searchoptions.mergeResults;
mergeKey = 'person_id';
break;
}
if (mergeTypes.length) {
res = Object.values(res.reduce((a, c) => {
if (!mergeTypes.includes(c.renderer)) {
a['nomerge' + counter++] = c;
} else if (c[mergeKey] === null) {
a['nomerge' + counter++] = c;
} else if (a[c[mergeKey]] === undefined) {
a[c[mergeKey]] = {
rank: c.rank,
renderer: mergedType,
type: mergedType,
list: [c]
};
} else {
a[c[mergeKey]].list.push(c);
if (c.rank > a[c[mergeKey]].rank)
a[c[mergeKey]].rank = c.rank;
}
return a;
}, {})).sort((a, b) => b.rank - a.rank);
}
}
this.searchresult = res;
this.searchmode = response.meta.mode;
}
this.searching = false;
this.retry = 0;
})
.catch(error=> {
if (error.code == "ERR_CANCELED") {
return this.retry = 0;
}
if (error.code == "ECONNABORTED" && this.retry) {
this.retry--;
return this.callsearchapi();
}
this.error = this.$p.t('search/error_general', error);
this.searching = false;
this.retry = 0;
});
},
refreshsearch: function() {
this.search();
this.togglesettings();
},
hideresult: function() {
this.showresult = false;
window.removeEventListener('resize', this.calcSearchResultExtent);
},
showsearchresult: function() {
if(this.searchoptions?.nolivesearch === true) return;
if( this.searchsettings.searchstr.length >= 2 ) {
this.showresult = true;
window.addEventListener('resize', this.calcSearchResultExtent);
this.calcSearchResultExtent(); this.calcSearchResultExtent();
} this.searchtimer = setTimeout(this.callsearchapi, 500);
}, } else {
searchfocusin: function(e) { this.showresult = false;
e.preventDefault(); }
e.stopPropagation(); },
if( this.hidetimer !== null ) { abort() {
clearTimeout(this.hidetimer); if (this.searchtimer !== null) {
} clearTimeout(this.searchtimer);
if (this.searchsettings.searchstr.length >= 2 }
&& this.searchresult.length === 0) { if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
this.searchresult = [];
},
callsearchapi: function () {
this.error = null;
this.searchresult.splice(0, this.searchresult.length);
this.searching = true;
this.showsearchresult();
if (this.searchsettings.types.length === 0) {
this.error = this.$p.t("search/error_missing_type");
this.searching = false;
return;
}
if (this.abortController) this.abortController.abort();
this.abortController = new AbortController();
this.searchfunction(this.searchsettings, {
timeout: 50000,
signal: this.abortController.signal,
})
.then((response) => {
if (!response.data) {
this.error = this.$p.t("search/error_general");
} else {
let res = response.data.map((el) =>
el.data ? { ...el, ...JSON.parse(el.data) } : el,
);
this.lastQuery = response.meta.searchstring;
if (this.searchoptions.mergeResults) {
let counter = 0;
let mergeTypes = [];
let mergedType = "merged-";
let mergeKey = "";
switch (this.searchoptions.mergeResults) {
case "student":
mergeTypes = ["student", "prestudent"];
mergedType +=
this.searchoptions.mergeResults;
mergeKey = "uid";
break;
case "person":
mergeTypes = [
"person",
"employee",
"student",
"prestudent",
];
mergedType +=
this.searchoptions.mergeResults;
mergeKey = "person_id";
break;
}
if (mergeTypes.length) {
res = Object.values(
res.reduce((a, c) => {
if (!mergeTypes.includes(c.renderer)) {
a["nomerge" + counter++] = c;
} else if (c[mergeKey] === null) {
a["nomerge" + counter++] = c;
} else if (
a[c[mergeKey]] === undefined
) {
a[c[mergeKey]] = {
rank: c.rank,
renderer: mergedType,
type: mergedType,
list: [c],
};
} else {
a[c[mergeKey]].list.push(c);
if (c.rank > a[c[mergeKey]].rank)
a[c[mergeKey]].rank = c.rank;
}
return a;
}, {}),
).sort((a, b) => b.rank - a.rank);
}
}
this.searchresult = res;
this.searchmode = response.meta.mode;
}
this.searching = false;
this.retry = 0;
})
.catch((error) => {
if (error.code == "ERR_CANCELED") {
return (this.retry = 0);
}
if (error.code == "ECONNABORTED" && this.retry) {
this.retry--;
return this.callsearchapi();
}
this.error = this.$p.t("search/error_general", error);
this.searching = false;
this.retry = 0;
});
},
refreshsearch: function () {
this.search();
this.togglesettings();
},
hideresult: function () {
this.showresult = false;
window.removeEventListener("resize", this.calcSearchResultExtent);
},
showsearchresult: function () {
if (this.searchoptions?.nolivesearch === true) return;
if (this.searchsettings.searchstr.length >= 2) {
this.showresult = true;
window.addEventListener("resize", this.calcSearchResultExtent);
this.calcSearchResultExtent();
}
},
searchfocusin: function (e) {
e.preventDefault();
e.stopPropagation();
if (this.hidetimer !== null) {
clearTimeout(this.hidetimer);
}
if (
this.searchsettings.searchstr.length >= 2 &&
this.searchresult.length === 0
) {
this.search(); this.search();
} }
}, },
searchfocusout: function(e) { searchfocusout: function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.hidetimer = setTimeout( this.hidetimer = setTimeout(this.hideresult, 100);
this.hideresult, },
100 dash2camelCase(string) {
); return string.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
}, },
dash2camelCase(string) { isValidRenderer(renderer) {
return string.replace(/-([a-z])/g, g => g[1].toUpperCase()); const camelCaseRenderer = this.dash2camelCase(renderer);
}, return Object.keys(this.$.components).includes(camelCaseRenderer);
isValidRenderer(renderer) { },
const camelCaseRenderer = this.dash2camelCase(renderer);
return Object.keys(this.$.components).includes(camelCaseRenderer);
},
getActions(res) { getActions(res) {
let actions = this.searchoptions.actions[this.dash2camelCase(res.renderer)]; let actions =
this.searchoptions.actions[this.dash2camelCase(res.renderer)];
if (actions) { if (actions) {
return actions; return actions;
} }
return this.searchoptions.actions[res.type]; return this.searchoptions.actions[res.type];
} },
} getMaxWidthOfSearchbarInMobileView() {
// body width - hardcoded chevron width; necessary for accurate collapse transition transition
return (
document.querySelector("body").getBoundingClientRect().width -
27 +
"px"
);
},
},
}; };
@@ -1,36 +0,0 @@
import ApiWidget from "../../api/factory/dashboard/widget.js";
const promises = Vue.ref([]);
const stateRef = Vue.ref([]);
const state = Vue.readonly(stateRef);
export function useCachedWidgetLoader() {
const $api = Vue.inject('$api');
const $fhcAlert = Vue.inject('$fhcAlert');
function load(id) {
if (state.value[id])
return Promise.resolve(state.value[id]);
if (!promises.value[id])
promises.value[id] = new Promise((resolve, reject) => {
$api
.call(ApiWidget.get(id))
.then(res => {
stateRef.value[id] = res.data;
promises.value[id] = undefined;
resolve(state.value[id]);
})
.catch($fhcAlert.handleSystemError);
});
return promises.value[id];
}
return {
state,
actions: {
load
}
};
}

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