Merge branch 'feature-25999/C4_cleanup' into merge_FHC4_C4

This commit is contained in:
Harald Bamberger
2025-01-27 18:34:42 +01:00
102 changed files with 3565 additions and 1779 deletions
+1 -1
View File
@@ -4,6 +4,6 @@ if (! defined('BASEPATH')) exit('No direct script access allowed');
// CMS Content Id for CIS4 Menu Root
$config['cis_menu_root_content_id'] = 11066;
$config['cis_menu_root_content_id'] = 11087;
// send Mails for ProfilUpdate
$config['cis_send_profil_update_mails'] = true;
+1 -2
View File
@@ -61,8 +61,7 @@ $route['api/v1/organisation/[O|o]rganisationseinheit/(:any)'] = 'api/v1/organisa
$route['api/v1/ressource/[B|b]etriebsmittelperson/(:any)'] = 'api/v1/ressource/betriebsmittelperson2/$1';
$route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1';
$route['CisVue'] = 'CisVue/dashboard';
$route['Cis/Stundenplan/(:any)'] = 'Cis/Stundenplan';
$route['Cis/Stundenplan/(:any)?'] = 'Cis/Stundenplan/index/$1';
// load routes from extensions
$subdir = 'application/config/extensions';
@@ -0,0 +1,30 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
*/
class InfoTerminal extends Auth_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r'],
]);
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
/**
* @return void
*/
public function index()
{
$this->load->view('Cis/InfoTerminal.php', []);
}
}
+6 -1
View File
@@ -26,7 +26,12 @@ class MyLv extends Auth_Controller
*/
public function index()
{
$this->load->view('Cis/MyLv');
$viewData = array(
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'MyLv']);
}
public function Info($studien_semester,$lvid)
+7 -2
View File
@@ -55,7 +55,10 @@ class Profil extends Auth_Controller
*/
public function index()
{
$this->load->view('Cis/Profil');
$viewData = array(
);
$this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'profilIndex']);
}
/**
@@ -65,7 +68,9 @@ class Profil extends Auth_Controller
*/
public function View($uid)
{
$this->load->view('Cis/Profil');
$viewData = array ('uid' => $uid);
$this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'profilViewUid']);
}
/**
+7 -2
View File
@@ -23,8 +23,13 @@ class Stundenplan extends Auth_Controller
/**
* @return void
*/
public function index()
public function index($lv_id = null)
{
$this->load->view('Cis/Stundenplan');
$viewData = array(
'lv_id' => $lv_id
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Stundenplan']);
}
}
+1 -1
View File
@@ -36,6 +36,6 @@ class Cis4 extends Auth_Controller
'person_id' => $personData->person_id
);
$this->load->view('CisVue/Dashboard.php',['viewData' => $viewData]);
$this->load->view('CisRouterView/CisRouterView.php',['viewData' => $viewData, 'route' => 'FhcDashboard']);
}
}
+12 -44
View File
@@ -60,7 +60,14 @@ class Cms extends Auth_Controller
$content = current($content);
$this->load->view('CisVue/Cms/Content', ['content_id' => $content_id, 'template_kurzbz' => $content->template_kurzbz, 'version' => $version, 'sprache' => $sprache, 'sichtbar' => $sichtbar]);
$viewData = array(
'content_id' => $content_id,
'template_kurzbz' => $content->template_kurzbz,
'version' => $version,
'sichtbar' => $sichtbar
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'Content']);
}
/**
@@ -76,49 +83,10 @@ class Cms extends Auth_Controller
*/
public function news($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true)
{
$this->load->view('CisVue/Cms/Content', ['infoscreen' => $infoscreen, 'studiengang_kz' => $studiengang_kz, 'semester' => $semester, 'mischen' => $mischen, 'titel' => $titel, 'edit' => $edit, 'sichtbar' => $sichtbar]);
}
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true)
{
$get_page = intval($this->input->get('page', true));
$get_page_size = intval($this->input->get('page_size', true));
if ($get_page) {
$page = $get_page;
}
if ($get_page_size) {
$page_size = $get_page_size;
} else {
$page_size = $this->page_size;
}
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, $mischen, $titel, $edit, $sichtbar, $page, $page_size);
if (isError($news)) {
$this->terminateWithJsonError(getError($news));
}
$news = hasData($news) ? getData($news) : null;
if ($news) {
echo json_encode($news);
} else {
show_error("News: No data found");
}
}
public function getNewsRowCount($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $fachbereich_kurzbz = null, $maxalter = 0, $edit = false, $sichtbar = true, $page = 1, $page_size = 10)
{
list($studiengang_kz, $semester) = $this->cmslib->getStgAndSem($studiengang_kz, $semester);
$all = $edit;
$num_rows = $this->NewsModel->countNewsWithContent(getSprache(), $studiengang_kz, $semester, $fachbereich_kurzbz, $sichtbar, $maxalter, $page, $this->page_size, $all, $mischen);
if (isError($num_rows)) {
$this->terminateWithJsonError(getError($num_rows));
}
$num_rows = hasData($num_rows) ? getData($num_rows) : null;
if ($num_rows) {
echo json_encode($num_rows);
} else {
show_error("News number rows: No data found");
}
// TODO: what are those parameters and what are they used for?
$this->load->view('CisRouterView/CisRouterView.php', ['route' => 'News']);
// $this->load->view('CisVue/Cms/Content', ['infoscreen' => $infoscreen, 'studiengang_kz' => $studiengang_kz, 'semester' => $semester, 'mischen' => $mischen, 'titel' => $titel, 'edit' => $edit, 'sichtbar' => $sichtbar]);
}
public function getRoomInformation($ort_kurzbz){
+6 -8
View File
@@ -29,17 +29,15 @@ class Dashboard extends Auth_Controller
{
$this->load->model('person/Person_model','PersonModel');
$begruesung = $this->PersonModel->getFirstName(getAuthUID());
if(isError($begruesung))
{
show_error("name couldn't be loaded for username ".getAuthUID());
}
$begruesung = getData($begruesung);
$personData = getData($this->PersonModel->getByUid(getAuthUID()))[0];
$viewData = array(
'name' => $begruesung
'uid' => getAuthUID(),
'name' => $personData->vorname,
'person_id' => $personData->person_id
);
$this->load->view('CisVue/Dashboard.php', ['viewData' => $viewData]);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData]);
}
}
@@ -30,6 +30,7 @@ class Bookmark extends FHCAPI_Controller
'getBookmarks' => self::PERM_LOGGED,
'delete' => self::PERM_LOGGED,
'insert' => self::PERM_LOGGED,
'update' => self::PERM_LOGGED,
]);
$this->load->model('dashboard/Bookmark_model', 'BookmarkModel');
@@ -50,7 +51,8 @@ class Bookmark extends FHCAPI_Controller
*/
public function getBookmarks()
{
$bookmarks = $this->BookmarkModel->loadWhere(["uid"=>$this->uid]);
$this->BookmarkModel->addOrder("bookmark_id");
$bookmarks = $this->BookmarkModel->loadWhere(["uid"=>$this->uid]);
$bookmarks = $this->getDataOrTerminateWithError($bookmarks);
@@ -104,6 +106,33 @@ class Bookmark extends FHCAPI_Controller
$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() == FALSE) $this->terminateWithValidationErrors($this->form_validation->error_array());
$url = $this->input->post('url',true);
$title = $this->input->post('title',true);
$now = new DateTime();
$now = $now->format('Y-m-d H:i:s');
$update_result = $this->BookmarkModel->update($bookmark_id,['url'=>$url, 'title'=>$title,'updateamum'=>$now]);
$update_result = $this->getDataOrTerminateWithError($update_result);
$this->terminateWithSuccess($update_result);
}
}
@@ -0,0 +1,58 @@
<?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 Cis4FhcApi extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getViewData' => self::PERM_LOGGED,
]);
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* fetches ViewData
*/
public function getViewData()
{
$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->terminateWithSuccess($viewData);
}
}
@@ -0,0 +1,58 @@
<?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 CisMenu extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getMenu' => self::PERM_LOGGED,
]);
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* fetches the menu for CIS from the database based on the userLanguage
*/
public function getMenu()
{
$this->load->model('content/Content_model', 'ContentModel');
$this->load->config('cis');
$cis4_content_id =$this->config->item('cis_menu_root_content_id');
$result = $this->ContentModel->getMenu($cis4_content_id, getAuthUID(),getUserLanguage());
$result = $this->getDataOrTerminateWithError($result);
$menu = $result->childs ?? [];
$this->terminateWithSuccess($menu);
}
}
+27 -15
View File
@@ -106,7 +106,6 @@ class Cms extends FHCAPI_Controller
$this->terminateWithSuccess($content_id);
}
//todo: there is the method news and getNews but only one should exist
public function news()
{
@@ -125,25 +124,35 @@ class Cms extends FHCAPI_Controller
//get the data or terminate with error
$news = $this->getDataOrTerminateWithError($news);
// array that keeps track of which news don't have a betreff and have to be removed from the news array
$newsToRemove = array();
// collect the content of the news
foreach($news as $news_element){
$this->addMeta("content_id",$news_element->content_id);
foreach($news as $index=>$news_element){
//todo: quick fix, for query builder error when fetching content
$this->NewsModel->resetQuery();
$content = $this->cmslib->getContent($news_element->content_id);
$content = getData($content);
$news_element->content_obj = $content;
if(isError($content))
{
// removes the news from the news array, so that the response does not include a invalid news
array_push($newsToRemove,$index);
//add the error to the api response? visual feedback
//$this->addError(print_r($content->retval,true));
continue;
}
$content = getData($content);
$news_element->content_obj = $content;
}
//removes all news that don't have a betreff
foreach($newsToRemove as $removeNewsIndex)
{
unset($news[$removeNewsIndex]);
}
$withContent = function($news) {
return $news->content_obj != null;
};
};
$newsWithContent = array_filter($news, $withContent);
$this->terminateWithSuccess($newsWithContent);
}
@@ -176,15 +185,18 @@ class Cms extends FHCAPI_Controller
// getting the GET parameters
$page = intval($this->input->get('page', true));
$page_size = intval($this->input->get('page_size', true));
$sprache = $this->input->get('sprache', true);
if(!$sprache)
{
$sprache = getUserLanguage();
}
// default value for the page_size is 10
$page_size = $page_size ?? 10;
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, $mischen, $titel, $edit, $sichtbar, $page, $page_size);
$news = $this->cmslib->getNews($infoscreen, $studiengang_kz, $semester, $mischen, $titel, $edit, $sichtbar, $page, $page_size, $sprache);
$news = $this->getDataOrTerminateWithError($news);
$this->addMeta('test', $this->p->t('global', 'studiengangsleitung'));
$this->addMeta('phrases', json_decode($this->p->getJson()));
$this->terminateWithSuccess($news);
@@ -30,6 +30,7 @@ class Lehre extends FHCAPI_Controller
parent::__construct([
'lvStudentenMail' => self::PERM_LOGGED,
'LV' => self::PERM_LOGGED,
'Pruefungen' => self::PERM_LOGGED,
]);
@@ -76,6 +77,23 @@ class Lehre extends FHCAPI_Controller
$this->terminateWithSuccess($result);
}
/**
* fetches all Pruefungen of a student for a specific lehrveranstaltung
* if the student passed the Pruefung on the first attempt, no information about the Pruefungen is stored in the database
* @param mixed $lehrveranstaltung_id
* @return void
*/
public function Pruefungen($lehrveranstaltung_id)
{
$this->load->model('education/Pruefung_model', 'PruefungModel');
$result = $this->PruefungModel->getByStudentAndLv(getAuthUID(), $lehrveranstaltung_id, getUserLanguage());
$result = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($result);
}
@@ -58,6 +58,22 @@ class LvMenu extends FHCAPI_Controller
//------------------------------------------------------------------------------------------------------------------
// Public methods
/**
* alternative function to get multiple lvMenus with a single http request
*/
public function getMultipleLvMenu($lvMenuOptionList){
$result =[];
foreach($lvMenuOptionList as $lvMenuOptions){
$lvMenu = $this->getLvMenu($lvMenuOptions['lvid'],$lvMenuOptions['studiensemester_kurzbz']);
if(isError($lvMenu)){
// TODO: some lvMenu threw an error, handle error here
}
$result[$lvMenuOptions['lvid']]=$lvMenu;
}
$this->terminateWithSuccess($result);
}
/**
*
*/
@@ -91,20 +107,24 @@ class LvMenu extends FHCAPI_Controller
$lvres = $this->Lehrveranstaltung_model->load($lvid);
if(!hasData($lvres))
{
$this->terminateWithError('LV ' . $lvid . ' not found.');
$this->terminateWithError('LV ' . $lvid . ' not found.');
}
$lv = (getData($lvres))[0];
$this->addMeta('lvInfo',$lv);
// define studiengang_kz / semester / lehrverzeichnis
$studiengang_kz = $lv->studiengang_kz;
$semester = $lv->semester;
$short = $lv->lehreverzeichnis;
// return empty menu for studiengang_kz = 0
if($studiengang_kz == 0){
$this->terminateWithSuccess("organisatorische_einheit");
}
// load studiengang
$stgres = $this->Studiengang_model->load($lv->studiengang_kz);
$stgres = $this->Studiengang_model->load(strval($studiengang_kz));
if(!hasData($stgres))
{
$this->terminateWithError('Stg ' . $lv->studiengang_kz . ' nof found.');
$this->terminateWithError('Stg ' . $lv->studiengang_kz . ' not found.');
}
$stg = (getData($stgres))[0];
$kurzbz = strtoupper($stg->typ . $stg->kurzbz);
@@ -284,24 +304,6 @@ class LvMenu extends FHCAPI_Controller
}
private function fhc_menu_digitale_anwesenheiten(&$menu, $angemeldet, $studiengang_kz, $semester, $lvid, $angezeigtes_stsem){
// DIGITALE ANWESENHEITEN
if (defined('CIS_LEHRVERANSTALTUNG_ANWESENHEIT_ANZEIGEN') && CIS_LEHRVERANSTALTUNG_ANWESENHEIT_ANZEIGEN && $angemeldet) {
$menu[] = array
(
'id' => 'core_menu_digitale_anwesenheitslisten',
'position' => '50',
'name' => $this->p->t('lehre', 'digiAnw'),
'c4_icon' => base_url('skin/images/button_kreuzerltool.png'),
'c4_link' => base_url("index.ci.php/extensions/FHC-Core-Anwesenheiten/?stg_kz=$studiengang_kz&sem=$semester&lvid=$lvid&sem_kurzbz=$angezeigtes_stsem&nav=false"),
'c4_linkList' => []
);
}
}
private function fhc_menu_lvinfo(&$menu, $lvid, $studiengang_kz, $lektor_der_lv, $is_lector, $lehrfach_oe_kurzbz_arr){
// LVINFO
@@ -73,11 +73,22 @@ class Phrasen extends FHCAPI_Controller
// gets all languages that are set as active in the database
public function getAllLanguages()
{
$langs = getDBActiveLanguages();
$this->load->model('system/Sprache_model', 'SprachenModel');
// Add order clause by index and select the sprache,bezeichnung and index column
$this->SprachenModel->addOrder('index');
$this->SprachenModel->addSelect('sprache, bezeichnung, index');
// Retrieves from public.tbl_sprache
$langs = $this->SprachenModel->loadWhere(array('content' => true));
$langs = $this->getDataOrTerminateWithError($langs);
$langs = array_map(function($lang){
return $lang->sprache;
$data = new stdClass();
$data->sprache = $lang->sprache;
$data->bezeichnung = $lang->bezeichnung[($lang->index-1)];
return $data;
}, $langs);
$this->terminateWithSuccess($langs);
}
@@ -0,0 +1,58 @@
<?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 Studgang extends FHCAPI_Controller
{
/**
* Object initialization
*/
public function __construct()
{
parent::__construct([
'getStudiengangInfo'=> self::PERM_LOGGED,
]);
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
// Loads phrases system
$this->loadPhrases([
'global'
]);
}
//------------------------------------------------------------------------------------------------------------------
// Public methods
public function getStudiengangInfo(){
// fetches the Studiengang Information which is used next the the news
$studiengangInfo = $this->StudiengangModel->getStudiengangInfoForNews();
$studiengangInfo= $this->getDataOrTerminateWithError($studiengangInfo);
$this->terminateWithSuccess($studiengangInfo);
}
//------------------------------------------------------------------------------------------------------------------
// Private methods
}
@@ -126,24 +126,33 @@ class Stundenplan extends FHCAPI_Controller
// storing the get parameter in local variables
$start_date = $this->input->get('start_date', TRUE);
$end_date = $this->input->get('end_date', TRUE);
$lv_id = $this->input->get('lv_id', TRUE);
$student_uid = getAuthUID();
if(is_null($student_uid))
{
$this->terminateWithError("No UID");
}
$semester_range = $this->studienSemesterErmitteln($start_date,$end_date);
$this->sortStudienSemester($semester_range);
$this->applyLoadUeberSemesterHaelfte($semester_range);
if($lv_id) { // fetch Stundenplan for lva, irrelevant of who is requesting it (for now)
$stundenplan_data = $this->StundenplanModel->getStundenplanLVA($start_date, $end_date, $lv_id);
$stundenplan_data = $this->getDataOrTerminateWithError($stundenplan_data) ?? [];
$this->expand_object_information($stundenplan_data);
$this->terminateWithSuccess($stundenplan_data);
}
$is_mitarbeiter = getData($this->MitarbeiterModel->isMitarbeiter($student_uid));
if($is_mitarbeiter)
{
$this->terminateWithError("Not possible to look at the Student Calendar as a Mitarbeiter");
}
$semester_range = $this->studienSemesterErmitteln($start_date,$end_date);
$this->sortStudienSemester($semester_range);
$this->applyLoadUeberSemesterHaelfte($semester_range);
// getting the gruppen_kurzbz of the student in the different studiensemester
$benutzer_gruppen = $this->fetchBenutzerGruppenFromStudiensemester($semester_range);
+17 -7
View File
@@ -87,18 +87,28 @@ function generateCSSsInclude($CSSs)
*/
function generateJSDataStorageObject($indexPage, $calledPath, $calledMethod)
{
$ci =& get_instance();
$ci->load->model('system/Sprache_model','SpracheModel');
$server_language = getData($ci->SpracheModel->loadWhere(['content' => true]));
$server_language = array_map(function($language){
return ['sprache'=>$language->sprache, 'LC_Time'=>$language->locale, 'bezeichnung'=>$language->bezeichnung[$language->index-1]];
}, $server_language);
$user_language = getUserLanguage();
$FHC_JS_DATA_STORAGE_OBJECT = array(
'app_root' => APP_ROOT,
'ci_router' => $indexPage,
'called_path' => $calledPath,
'called_method' => $calledMethod,
'server_languages' => $server_language,
'user_language' => $user_language,
'timezone' => date_default_timezone_get(),
);
$toPrint = "\n";
$toPrint .= '<script type="text/javascript">';
$toPrint .= '
var FHC_JS_DATA_STORAGE_OBJECT = {
app_root: "'.APP_ROOT.'",
ci_router: "'.$indexPage.'",
called_path: "'.$calledPath.'",
called_method: "'.$calledMethod.'",
user_language: "'.$user_language.'"
};';
var FHC_JS_DATA_STORAGE_OBJECT = '.json_encode($FHC_JS_DATA_STORAGE_OBJECT).';';
$toPrint .= "\n";
$toPrint .= '</script>';
$toPrint .= "\n\n";
+18 -6
View File
@@ -43,7 +43,7 @@ class CmsLib
* @param string $sprache
* @param boolean $sichtbar
*
* @return void
* @return stdClass
*/
public function getContent($content_id, $version = null, $sprache = null, $sichtbar = true)
{
@@ -105,7 +105,19 @@ class CmsLib
if($content->titel){
$betreff = $content->titel;
}else{
//DomDocument getElementsByTagName returns a DomNodeList
$betreff = $XML->getElementsByTagName('betreff');
//check if any betreff was found and if it is not empty
if($betreff->length > 0 && !empty($betreff->item(0)->nodeValue))
{
//DomNodeList item() return a DomNode, property nodeValue contains the value of the node
$betreff = $betreff->item(0)->nodeValue;
}
else
{
return error('no betreff found for the content');
}
}
$xsltemplate = new DOMDocument();
@@ -221,7 +233,7 @@ class CmsLib
*
* @return void
*/
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true, $page = 1, $page_size = 10)
public function getNews($infoscreen = false, $studiengang_kz = null, $semester = null, $mischen = true, $titel = '', $edit = false, $sichtbar = true, $page = 1, $page_size = 10, $sprache)
{
$this->ci->load->model('organisation/Studiengang_model', 'StudiengangModel');
list($studiengang_kz, $semester) = $this->getStgAndSem($studiengang_kz, $semester);
@@ -230,13 +242,13 @@ class CmsLib
$xml = '<?xml version="1.0" encoding="UTF-8"?><content>';
$this->ci->load->model('content/News_model', 'NewsModel');
$news = $this->ci->NewsModel->getNewsWithContent(getSprache(), $studiengang_kz, $semester, null, $sichtbar, 0, $page, $page_size, $all, $mischen);
$news = $this->ci->NewsModel->getNewsWithContent($sprache, $studiengang_kz, $semester, null, $sichtbar, 0, $page, $page_size, $all, $mischen);
if (isError($news))
return $news;
$news = getData($news);
//var_dump($news->maxPageCount);
foreach ($news as $newsobj) {
if ($studiengang_kz && $edit && !$newsobj->studiengang_kz)
continue;
@@ -247,7 +259,7 @@ class CmsLib
$xml .= "<newswrapper>" . $newsobj->content . $datum . $id . "</newswrapper>";
}
if ($studiengang_kz != 0) {
/* if ($studiengang_kz != 0) {
$stg_obj = $this->ci->StudiengangModel->load($studiengang_kz);
if (isError($stg_obj))
return $stg_obj;
@@ -262,7 +274,7 @@ class CmsLib
}
$xml .= '<studiengang_bezeichnung><![CDATA[' . $stg_obj->bezeichnung . ']]></studiengang_bezeichnung>';
}
}
} */
if ($titel != '') {
$xml .= '<news_titel>' . $titel . '</news_titel>';
+2 -2
View File
@@ -21,7 +21,7 @@ class News_model extends DB_Model
{
$this->addJoin("campus.tbl_content","content_id");
return $this->execReadOnlyQuery('
SELECT *
SELECT *, TO_CHAR(campus.tbl_news.datum, ?) as datumformatted
FROM campus.tbl_news
JOIN campus.tbl_content content ON content.content_id = campus.tbl_news.content_id
WHERE
@@ -29,7 +29,7 @@ class News_model extends DB_Model
datum <= NOW() AND (datum_bis IS NULL OR datum_bis >= now()::date)
ORDER BY datum DESC
LIMIT ' . $this->escape($limit)
);
, ['DD/MM/YYYY']);
}
public function getNewsContentIDs($limit=10){
@@ -609,7 +609,11 @@ class Lehrveranstaltung_model extends DB_Model
$this->addSelect($lvbezeichnung . ' AS bezeichnung');
$this->addSelect($sgbezeichnung . ' AS sg_bezeichnung');
$this->addSelect('UPPER(sg.typ::VARCHAR(1) || sg.kurzbz) AS studiengang_kuerzel');
//also adds returns the index of the grade
//TODO: ist zeugnissnote immer gleich wie die lvgesamtnote
$this->addSelect('COALESCE(zn.note::numeric,gn.note::numeric) as note_index');
$this->addSelect('COALESCE(znn.positiv,gnn.positiv) as positiv');
$this->addSelect('COALESCE(gnn.' . $bezeichnung . ', gnn.bezeichnung, gn.note::text) AS lvnote');
$this->addSelect('COALESCE(znn.' . $bezeichnung . ', znn.bezeichnung, zn.note::text) AS znote');
@@ -647,4 +647,139 @@ class Studiengang_model extends DB_Model
return $this->load();
}
/**
* @return stdClass
*/
public function getStudiengangInfoForNews()
{
$this->load->model('person/Benutzerfunktion_model', 'BenutzerfunktionModel');
$this->load->model('person/Person_model', 'PersonModel');
$addEmailProperty= function(&$benutzerfunktionen){
if(count($benutzerfunktionen) && defined('DOMAIN'))
{
$benutzerfunktionen = array_map(function($benutzer)
{
$benutzer->email = $benutzer->alias."@".DOMAIN;
return $benutzer;
},$benutzerfunktionen) ;
}
};
$addFotoProperty= function(&$collection){
$collection = array_map(function($item){
$person_id = $this->PersonModel->getByUid($item->uid);
if(isError($person_id))
return error($person_id);
$person_id = current(getData($person_id))->person_id;
$this->PersonModel->addSelect('foto');
$foto = $this->PersonModel->loadWhere(array('person_id'=>$person_id));
if(isError($foto))
return error($foto);
$foto = current(getData($foto))->foto;
$item->foto = $foto;
return $item;
},$collection);
};
$this->load->model('crm/Student_model', 'StudentModel');
$student = $this->StudentModel->loadWhere(['student_uid' => getAuthUID()]);
if (isError($student))
return error($student);
if (getData($student)) {
//TODO: if a mitarbeiter requests this site, empty data should be returned
if(count($student) == 0)
{
return new stdClass();
}
$student = current(getData($student));
$studiengang_kz = $student->studiengang_kz;
$semester = $student->semester;
}
$stg_obj = $this->load($studiengang_kz);
if(isError($stg_obj))
return error($stg_obj);
if(getData($stg_obj))
{
$stg_obj = current(getData($stg_obj));
}
$stg_ltg = $this->getLeitungDetailed($stg_obj->studiengang_kz);
if (isError($stg_ltg))
return $stg_ltg;
$stg_ltg = getData($stg_ltg) ?: [];
$addFotoProperty($stg_ltg);
$gf_ltg = $this->BenutzerfunktionModel->getBenutzerFunktionenDetailed('gLtg', $stg_obj->oe_kurzbz);
if (isError($gf_ltg))
return $gf_ltg;
$gf_ltg = getData($gf_ltg) ?: [];
$addEmailProperty($gf_ltg);
$stv_ltg = $this->BenutzerfunktionModel->getBenutzerFunktionenDetailed('stvLtg', $stg_obj->oe_kurzbz);
if (isError($stv_ltg))
return $stv_ltg;
$stv_ltg = getData($stv_ltg) ?: [];
$addEmailProperty($stv_ltg);
$ass = $this->BenutzerfunktionModel->getBenutzerFunktionenDetailed('ass', $stg_obj->oe_kurzbz);
if (isError($ass))
return $ass;
$ass = getData($ass) ?: [];
$addEmailProperty($ass);
$addFotoProperty($ass);
$hochschulvertr = $this->BenutzerfunktionModel->getBenutzerFunktionenDetailed('hsv');
if (isError($hochschulvertr))
return $hochschulvertr;
$hochschulvertr = getData($hochschulvertr) ?: [];
$addEmailProperty($hochschulvertr);
$stdv = $this->BenutzerfunktionModel->getBenutzerFunktionenDetailed('stdv', $stg_obj->oe_kurzbz);
if (isError($stdv))
return $stdv;
$stdv = getData($stdv) ?: [];
$addEmailProperty($stdv);
$jahrgangsvertr = $this->BenutzerfunktionModel->getBenutzerFunktionenDetailed('jgv', $stg_obj->oe_kurzbz, $semester);
if (isError($jahrgangsvertr))
return $jahrgangsvertr;
$jahrgangsvertr = getData($jahrgangsvertr) ?: [];
$addEmailProperty($jahrgangsvertr);
$result_object = new stdClass();
$result_object->studiengang = $stg_obj;
$result_object->semester = $semester;
$result_object->stg_ltg = $stg_ltg;
$result_object->gf_ltg = $gf_ltg;
$result_object->stv_ltg = $stv_ltg;
$result_object->ass = $ass;
$result_object->hochschulvertr = $hochschulvertr;
$result_object->stdv = $stdv;
$result_object->jahrgangsvertr = $jahrgangsvertr;
return success($result_object);
}
public function getLvaForStudiengangInStudiensemester($studiengang_kz, $orgform_kurzbz, $studiensemester_kurzbz) {
$qry = '
SELECT DISTINCT ON (lehre.tbl_lehrveranstaltung.lehrveranstaltung_id,
kurzbz, bezeichnung, semester,
lehre.tbl_lehrveranstaltung.sprache, orgform_kurzbz,
lehre.tbl_lehrveranstaltung.lehrform_kurzbz)
lehre.tbl_lehrveranstaltung.lehrveranstaltung_id, kurzbz, bezeichnung,
semester, lehre.tbl_lehrveranstaltung.sprache, orgform_kurzbz, lehre.tbl_lehrveranstaltung.lehrform_kurzbz
FROM lehre.tbl_lehrveranstaltung JOIN lehre.tbl_lehreinheit USING(lehrveranstaltung_id) JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id)
WHERE aktiv = TRUE AND studiengang_kz = ? AND orgform_kurzbz = ? AND tbl_lehreinheit.studiensemester_kurzbz IN ?';
return $this->execReadOnlyQuery($qry, array($studiengang_kz, $orgform_kurzbz, $studiensemester_kurzbz));
}
}
@@ -186,9 +186,63 @@ class Stundenplan_model extends DB_Model
return $query_result;
}
/**
* queries Stundenplan but for a whole lva, irrespective of who is requesting it
*
* @return void
*/
public function getStundenplanLVA($start_date, $end_date, $lv_id) {
return $this->execReadOnlyQuery("
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 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,
ort_kurzbz, studiengang_kz, titel,lehreinheit_id,lehrfach_id,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 <= ? AND sp.lehrveranstaltung_id = ?
) sp
JOIN lehre.tbl_stunde ON lehre.tbl_stunde.stunde = sp.stunde
) as subquery
GROUP BY unr, datum, beginn, ende, ort_kurzbz, titel, lehrform, lehrfach, lehrfach_bez, organisationseinheit, farbe, lehrveranstaltung_id
ORDER BY datum, beginn
", [$start_date, $end_date, $lv_id]);
}
/**
* NO STANDALONE FUNCTION - Generates a SQL query string to fetch 'stundenplan' events for a specific student within the current semester.
* @param string $uid the user id that is used to fetch the stundenplan rows from the lehre.vw_stundenplan table
*
* @return mixed
*/
+16
View File
@@ -0,0 +1,16 @@
<?php
$includesArray =array(
'title' => 'Infoterminal',
'tabulator5' => true,
'primevue3' => true,
);
$this->load->view(
'templates/CISVUE-Header',
$includesArray
);
?>
<iframe style="width:100%; height:100%;" id="Infoterminal" src="<?php echo base_url() . 'cis/infoterminal/'; ?>" name="Infoterminal" frameborder="0" >
No iFrames
</iframe>
<?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?>
-15
View File
@@ -1,15 +0,0 @@
<?php
$includesArray = array(
'title' => 'MyLv',
'customJSModules' => ['public/js/apps/Cis/MyLv/Student.js'],
'customCSSs' => ['public/css/components/MyLv.css']
);
$this->load->view('templates/CISVUE-Header', $includesArray);
?>
<div id="content">
<mylv-student></mylv-student>
</div>
<?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?>
+1 -1
View File
@@ -1,6 +1,6 @@
<?php
$includesArray = array(
'title' => 'Stundenplan',
'title' => 'Profil',
'customJSModules' => ['public/js/apps/Cis/Profil.js'],
'tabulator5' => true,
'primevue3' => true,
-15
View File
@@ -1,15 +0,0 @@
<?php
$includesArray = array(
'title' => 'Stundenplan',
'customJSModules' => ['public/js/apps/Cis/Stundenplan.js'],
'customCSSs' => ['public/css/components/calendar.css']
);
$this->load->view('templates/CISVUE-Header', $includesArray);
?>
<div id="content">
</div>
<?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?>
@@ -0,0 +1,40 @@
<?php
$includesArray = array(
'title' => 'Cis4',
'axios027' => true,
'bootstrap5' => true,
'fontawesome6' => true,
'tabulator5' => true,
'vue3' => true,
'primevue3' => true,
'customCSSs' => array(
'public/css/components/verticalsplit.css',
'public/css/components/searchbar.css',
'public/css/Fhc.css',
'public/css/components/dashboard.css',
'public/css/components/calendar.css',
'public/css/components/MyLv.css',
'public/css/components/FilterComponent.css',
'public/css/components/Profil.css',
'public/css/components/FormUnderline.css',
'public/css/Cis4/Cms.css',
),
'customJSs' => array(
'vendor/npm-asset/primevue/accordion/accordion.js',
'vendor/npm-asset/primevue/accordiontab/accordiontab.js'
),
'customJSModules' => array(
'public/js/apps/Dashboard/Fhc.js'
),
);
$this->load->view('templates/CISVUE-Header', $includesArray);
?>
<div id="fhccontent" route=<?php echo $route ?>>
<router-view
:view-data='<?php echo json_encode($viewData) ?>'
></router-view>
</div>
<?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?>
+2 -2
View File
@@ -18,8 +18,8 @@ if(defined('CIS4')){
}
?>
<div id="cms">
<?php echo (isset($content_id) ? '<cms-content :content_id="'.$content_id.'" :version="'.$version.'" :sprache="'.$sprache.'" :sichtbar="'.$sichtbar.'" />' : '<cms-news/>'); ?>
<div ref="cmsContainer" id="cms">
<?php echo (isset($content_id) ? '<router-view ref="cms" :content_id="'.$content_id.'" :version="'.$version.'" :sprache="'.$sprache.'" :sichtbar="'.$sichtbar.'" />' : '<router-view ref="cms" />'); ?>
</div>
<?php
-20
View File
@@ -1,20 +0,0 @@
<?php
$includesArray = array(
'title' => 'Dashboard',
'tabulator5'=>true,
'primevue3' => true,
'customJSModules' => ['public/js/apps/Dashboard/Fhc.js'],
'customCSSs' => [
'public/css/components/dashboard.css'
],
);
$this->load->view('templates/CISVUE-Header', $includesArray);
?>
<div id="content">
<fhc-dashboard dashboard="CIS" view-data-string='<?php echo json_encode($viewData) ?>' />
</div>
<?php $this->load->view('templates/CISVUE-Footer', $includesArray); ?>
+1 -10
View File
@@ -5,6 +5,7 @@ $includesArray = array(
'bootstrap5' => true,
'fontawesome6' => true,
'axios027' => true,
'primevue3' => true,
'customJSModules' => array_merge([
'public/js/apps/Cis.js'
], $customJSModules ?? []),
@@ -15,15 +16,6 @@ $includesArray = array(
$this->load->view('templates/FHC-Header', $includesArray);
if (!isset($menu)) {
$ci =& get_instance(); // get CI instance
$ci->load->model('content/Content_model', 'ContentModel');
$ci->load->config('cis');
$cis4_content_id = $ci->config->item('cis_menu_root_content_id');
$result = $ci->ContentModel->getMenu($cis4_content_id, getAuthUID());
$menu = getData($result)->childs ?? [];
}
?>
<script type="text/javascript">
@@ -39,7 +31,6 @@ if (!isset($menu)) {
logout-url="<?= site_url('Cis/Auth/logout'); ?>"
:searchbaroptions="searchbaroptions"
:searchfunction="searchfunction"
:menu="<?= htmlspecialchars(json_encode(array_values($menu)), ENT_QUOTES, 'UTF-8') ?>"
></cis-menu>
</header>
+8 -1
View File
@@ -30,7 +30,7 @@
// Generates the global object to pass phrases to javascripts
// NOTE: must be called before including the PhrasesLib.js
if ($phrases != null) generateJSPhrasesStorageObject($phrases);
// --------------------------------------------------------------------------------------------------------
// From vendor folder
@@ -123,6 +123,13 @@
}
generateJSsInclude('vendor/vuejs/vuerouter4/vue-router.global.js');
}
// Highcharts
if (isset($highcharts) && $highcharts === true)
{
generateJSsInclude('vendor/highcharts/highcharts-dist/highcharts.js');
generateJSsInclude('vendor/highcharts/highcharts-dist/modules/current-date-indicator.js');
}
// PrimeVue
if ($primevue3)
+29 -15
View File
@@ -351,23 +351,34 @@
{
"type": "package",
"package": {
"name": "vuejs/vuedatepicker_js",
"version": "7.2.0",
"dist": {
"url": "https://unpkg.com/@vuepic/vue-datepicker@7.2.0/dist/vue-datepicker.iife.js",
"type": "file"
}
"name": "vuejs/vuedatepicker_js",
"version": "7.2.0",
"dist": {
"url": "https://unpkg.com/@vuepic/vue-datepicker@7.2.0/dist/vue-datepicker.iife.js",
"type": "file"
}
}
},
{
"type": "package",
"package": {
"name": "vuejs/vuedatepicker_css",
"version": "7.2.0",
"dist": {
"url": "https://unpkg.com/@vuepic/vue-datepicker@7.2.0/dist/main.css",
"type": "file"
}
"name": "vuejs/vuedatepicker_css",
"version": "7.2.0",
"dist": {
"url": "https://unpkg.com/@vuepic/vue-datepicker@7.2.0/dist/main.css",
"type": "file"
}
}
},
{
"type": "package",
"package": {
"name": "highcharts/highcharts-dist",
"version": "7.1.2",
"dist": {
"url": "https://github.com/highcharts/highcharts-dist/archive/v7.1.2.zip",
"type": "zip"
}
}
},
{
@@ -402,6 +413,8 @@
"fortawesome/font-awesome6": "6.1.*",
"fzaninotto/faker": "1.*",
"highcharts/highcharts-dist": "^7.1.2",
"joeldbirch/superfish": "1.7.*",
"jquery/jquery1": "1.*",
"jquery/jquery2": "2.*",
@@ -442,13 +455,14 @@
"vuejs/vuejs3": "3.3.8",
"vuejs/vuerouter4": "4.1.3",
"vuejs/vuedatepicker_js": "7.2.0",
"vuejs/vuedatepicker_css": "7.2.0"
"vuejs/vuedatepicker_js": "7.2.0",
"vuejs/vuedatepicker_css": "7.2.0"
},
"config": {
"bin-dir": "vendor/bin"
},
"require-dev": {
"vuejs/vuejs3_dev": "3.3.8",
"squizlabs/php_codesniffer": "3.6.*",
Generated
+74 -63
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "01ea35652d88680f8262c5365828eb46",
"content-hash": "86d9ee6aa7cbf758f847f7aad13177a2",
"packages": [
{
"name": "afarkas/html5shiv",
@@ -322,9 +322,6 @@
"extra": {
"component": {
"name": "angularjs",
"scripts": [
"angular.js"
],
"files": [
"angular-animate.js",
"angular-animate.min.js",
@@ -597,6 +594,9 @@
"i18n/angular-locale_zh.js",
"i18n/angular-locale_zu-za.js",
"i18n/angular-locale_zu.js"
],
"scripts": [
"angular.js"
]
}
},
@@ -682,20 +682,20 @@
"extra": {
"component": {
"name": "jquery-ui",
"scripts": [
"jquery-ui.js"
],
"files": [
"ui/**",
"themes/**",
"jquery-ui.min.js"
],
"shim": {
"deps": [
"jquery"
],
"exports": "jQuery"
}
},
"files": [
"ui/**",
"themes/**",
"jquery-ui.min.js"
],
"scripts": [
"jquery-ui.js"
]
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -955,6 +955,15 @@
"abandoned": true,
"time": "2020-12-11T09:56:16+00:00"
},
{
"name": "highcharts/highcharts-dist",
"version": "7.1.2",
"dist": {
"type": "zip",
"url": "https://github.com/highcharts/highcharts-dist/archive/v7.1.2.zip"
},
"type": "library"
},
{
"name": "joeldbirch/superfish",
"version": "1.7.9",
@@ -1015,12 +1024,12 @@
"version": "1.3.7",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"url": "https://github.com/jsonrainbow/json-schema.git",
"reference": "87b54b460febed69726c781ab67462084e97a105"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/87b54b460febed69726c781ab67462084e97a105",
"url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/87b54b460febed69726c781ab67462084e97a105",
"reference": "87b54b460febed69726c781ab67462084e97a105",
"shasum": ""
},
@@ -1075,8 +1084,8 @@
"schema"
],
"support": {
"issues": "https://github.com/justinrainbow/json-schema/issues",
"source": "https://github.com/justinrainbow/json-schema/tree/master"
"issues": "https://github.com/jsonrainbow/json-schema/issues",
"source": "https://github.com/jsonrainbow/json-schema/tree/1.3.7"
},
"time": "2014-08-25T02:48:14+00:00"
},
@@ -1116,6 +1125,7 @@
"issues": "https://github.com/kingsquare/json-schema-form/issues",
"source": "https://github.com/kingsquare/json-schema-form/tree/master"
},
"abandoned": true,
"time": "2014-07-10T12:27:19+00:00"
},
{
@@ -1297,16 +1307,16 @@
},
{
"name": "mottie/tablesorter",
"version": "v2.31.3",
"version": "v2.32.0",
"source": {
"type": "git",
"url": "https://github.com/Mottie/tablesorter.git",
"reference": "7202d5faf8105a5ecd1a2b7a653777618713ffe5"
"reference": "1423f5408982f58d5baa97648d2e5ee0b55fd3b6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Mottie/tablesorter/zipball/7202d5faf8105a5ecd1a2b7a653777618713ffe5",
"reference": "7202d5faf8105a5ecd1a2b7a653777618713ffe5",
"url": "https://api.github.com/repos/Mottie/tablesorter/zipball/1423f5408982f58d5baa97648d2e5ee0b55fd3b6",
"reference": "1423f5408982f58d5baa97648d2e5ee0b55fd3b6",
"shasum": ""
},
"require": {
@@ -1315,10 +1325,10 @@
"type": "component",
"extra": {
"component": {
"files": "dist/",
"scripts": [
"dist/js/jquery.tablesorter.combined.js"
],
"files": "dist/"
]
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -1331,7 +1341,8 @@
},
{
"name": "Rob Garrison",
"email": "wowmotty@gmail.com"
"email": "wowmotty@gmail.com",
"homepage": "https://github.com/Mottie"
}
],
"description": "tablesorter (FORK) is a jQuery plugin for turning a standard HTML table with THEAD and TBODY tags into a sortable table without page refreshes. tablesorter can successfully parse and sort many types of data including linked data in a cell.",
@@ -1352,7 +1363,7 @@
"source": "https://github.com/Mottie/tablesorter",
"wiki": "https://github.com/Mottie/tablesorter/wiki"
},
"time": "2020-03-03T13:46:03+00:00"
"time": "2024-08-14T01:23:57+00:00"
},
{
"name": "nategood/httpful",
@@ -1520,16 +1531,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "2.0.46",
"version": "2.0.48",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "498e67a0c82bd5791fda9b0dd0f4ec8e8aebb02d"
"reference": "eaa7be704b8b93a6913b69eb7f645a59d7731b61"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/498e67a0c82bd5791fda9b0dd0f4ec8e8aebb02d",
"reference": "498e67a0c82bd5791fda9b0dd0f4ec8e8aebb02d",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/eaa7be704b8b93a6913b69eb7f645a59d7731b61",
"reference": "eaa7be704b8b93a6913b69eb7f645a59d7731b61",
"shasum": ""
},
"require": {
@@ -1610,7 +1621,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/2.0.46"
"source": "https://github.com/phpseclib/phpseclib/tree/2.0.48"
},
"funding": [
{
@@ -1626,7 +1637,7 @@
"type": "tidelift"
}
],
"time": "2023-12-29T01:52:43+00:00"
"time": "2024-12-14T21:03:54+00:00"
},
{
"name": "rmariuzzo/jquery-checkboxes",
@@ -1668,12 +1679,12 @@
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
},
"branch-alias": {
"dev-main": "1.19-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
@@ -3273,16 +3284,16 @@
},
{
"name": "sebastian/code-unit-reverse-lookup",
"version": "1.0.2",
"version": "1.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
"reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619"
"reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619",
"reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619",
"url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54",
"reference": "92a1a52e86d34cde6caa54f1b5ffa9fda18e5d54",
"shasum": ""
},
"require": {
@@ -3316,7 +3327,7 @@
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
"support": {
"issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
"source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
"source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.3"
},
"funding": [
{
@@ -3324,7 +3335,7 @@
"type": "github"
}
],
"time": "2020-11-30T08:15:22+00:00"
"time": "2024-03-01T13:45:45+00:00"
},
{
"name": "sebastian/comparator",
@@ -3682,16 +3693,16 @@
},
{
"name": "sebastian/object-enumerator",
"version": "3.0.4",
"version": "3.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
"reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2"
"reference": "ac5b293dba925751b808e02923399fb44ff0d541"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
"reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
"url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/ac5b293dba925751b808e02923399fb44ff0d541",
"reference": "ac5b293dba925751b808e02923399fb44ff0d541",
"shasum": ""
},
"require": {
@@ -3727,7 +3738,7 @@
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
"source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
"source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.5"
},
"funding": [
{
@@ -3735,20 +3746,20 @@
"type": "github"
}
],
"time": "2020-11-30T07:40:27+00:00"
"time": "2024-03-01T13:54:02+00:00"
},
{
"name": "sebastian/object-reflector",
"version": "1.1.2",
"version": "1.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-reflector.git",
"reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d"
"reference": "1d439c229e61f244ff1f211e5c99737f90c67def"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
"reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
"url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/1d439c229e61f244ff1f211e5c99737f90c67def",
"reference": "1d439c229e61f244ff1f211e5c99737f90c67def",
"shasum": ""
},
"require": {
@@ -3782,7 +3793,7 @@
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-reflector/issues",
"source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
"source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.3"
},
"funding": [
{
@@ -3790,7 +3801,7 @@
"type": "github"
}
],
"time": "2020-11-30T07:37:18+00:00"
"time": "2024-03-01T13:56:04+00:00"
},
{
"name": "sebastian/phpcpd",
@@ -3849,16 +3860,16 @@
},
{
"name": "sebastian/recursion-context",
"version": "3.0.1",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/recursion-context.git",
"reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb"
"reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb",
"reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb",
"url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/9bfd3c6f1f08c026f542032dfb42813544f7d64c",
"reference": "9bfd3c6f1f08c026f542032dfb42813544f7d64c",
"shasum": ""
},
"require": {
@@ -3900,7 +3911,7 @@
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
"source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
"source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.2"
},
"funding": [
{
@@ -3908,7 +3919,7 @@
"type": "github"
}
],
"time": "2020-11-30T07:34:24+00:00"
"time": "2024-03-01T14:07:30+00:00"
},
{
"name": "sebastian/resource-operations",
@@ -4530,12 +4541,12 @@
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
},
"branch-alias": {
"dev-main": "1.19-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
+2
View File
@@ -32,6 +32,8 @@ html {
--fhc-cis-menu-lvl-5-color: #000;
--fhc-cis-menu-lvl-5-bg-hover: #eceff1;
--fhc-cis-menu-lvl-5-color-hover: #000;
--fhc-cis-grade-positive: rgb(0, 128, 0);
--fhc-cis-grade-negative: rgb(128, 0, 0);
}
@media (min-width: 992px) {
:root {
+2 -2
View File
@@ -49,7 +49,8 @@
.fhc-calendar-week-page > div {
/*transform: translate(-0.75em, -0.75em);*/
/*width: calc(100% + 1.5em);*/
height: calc(100% + 1.5em);
/*height: calc(100% + 1.5em);*/
max-height: 100%;
}
.fhc-calendar-week-page > div > div {
padding-left: 3em;
@@ -105,7 +106,6 @@
.fhc-calendar-day-page .events {
display: grid;
grid-template-columns: 3em 1fr;
margin-left: -3em;
/*min-height: 266.6666666667%;*/
}
.fhc-calendar-day-page .events .day {
+71 -1
View File
@@ -2,6 +2,45 @@
--news-widget-height: 1;
}
/* media query for sm sizes */
@media (min-width :576px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding:0em 0em 0em 0em !important;
}
}
/* media query for md sizes */
@media (min-width :768px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding: 0.3em 0.3em 0.3em 0.3em !important;
}
}
/* media query for lg sizes */
@media (min-width :992px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding: 0.3em 0.3em 0.3em 0.3em !important;
}
}
/* media query for xl sizes */
@media (min-width :1200px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding: 1em 1em 0.3em 1em !important;
}
}
/* media query for xxl sizes */
@media (min-width :1400px) {
.fhc-news-menu-item {
/* padding: top right bottom left */
padding: 1.2em 1.2em 0.3em 1.2em !important;
}
}
.widgets-news img
{
max-width: 100%;
@@ -17,7 +56,6 @@
align-items: center;
background-color: #00649c;
border: 1px solid #f1f1f1;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease; /* Smooth transition */
}
@@ -70,3 +108,35 @@
.fhc-carousel .carousel-item-end {
transition: transform 0.44s ease-in-out, opacity 0.8s ease-in-out;
}
.carousel-control-prev,
.carousel-control-next {
opacity: 0;
transition: opacity 0.3s ease;
}
/* Show the buttons when the carousel is hovered */
.carousel:hover .carousel-control-prev,
.carousel:hover .carousel-control-next {
opacity: 1;
}
.fhc-news-text{
font-size: 0.75em;
}
.fhc-news-xs {
font-size: 0.6em;
}
.fhc-news-sm {
font-size: 0.7em;
}
.fhc-news-md {
font-size: 1.1em;
}
.fhc-news-lg {
font-size: 1.4em;
}
+10
View File
@@ -14,6 +14,16 @@ export default {
);
},
update: function ({ bookmark_id, url, title, tag=null}) {
return this.$fhcApi.post(
`/api/frontend/v1/Bookmark/update/${bookmark_id}`
, {
url: url,
title: title
}
);
},
insert: function ({url, title, tag}) {
return this.$fhcApi.post(
`/api/frontend/v1/Bookmark/insert`
+20 -10
View File
@@ -11,30 +11,40 @@ export default {
);
},
news(limit) {
return this.$fhcApi.get(
"/api/frontend/v1/Cms/news",
{
limit: limit
}
);
},
getNews(page = 1, page_size = 10) {
//api function used for the news View that renders the html
getNews(page = 1, page_size = 10, sprache) {
return this.$fhcApi.get(
"/api/frontend/v1/Cms/getNews",
{
page,
page_size,
sprache,
},
);
},
//api function used for the widget component
news(limit) {
return this.$fhcApi.get(
"/api/frontend/v1/Cms/news",
{
limit: limit
}
);
},
getNewsRowCount: function () {
return this.$fhcApi.get(
"/api/frontend/v1/Cms/getNewsRowCount",
{}
);
},
getNewsExtra: function(){
return this.$fhcApi.get(
"/api/frontend/v1/Cms/getStudiengangInfoForNews",
{}
);
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
async getViewData() {
const url = `/api/frontend/v1/Cis4FhcApi/getViewData`;
return this.$fhcApi.get(url, null, null)
},
}
+7 -1
View File
@@ -33,11 +33,15 @@ import ort from "./ort.js";
import cms from "./cms.js";
import lehre from "./lehre.js";
import addons from "./addons.js";
import studiengang from "./studiengang.js";
import menu from "./menu.js";
import dashboard from "./dashboard";
export default {
search,
phrasen,
navigation,
dashboard,
filter,
studstatus,
profil,
@@ -52,5 +56,7 @@ export default {
ort,
cms,
lehre,
addons
addons,
studiengang,
menu,
};
+6
View File
@@ -13,4 +13,10 @@ export default {
, {}
);
},
getStudentPruefungen(lehrveranstaltung_id){
return this.$fhcApi.get(
`/api/frontend/v1/Lehre/Pruefungen/${lehrveranstaltung_id}`
, {}
);
}
}
+9
View File
@@ -0,0 +1,9 @@
export default {
getMenu: function () {
return this.$fhcApi.get(
"/api/frontend/v1/CisMenu/getMenu",
{}
);
}
}
+9
View File
@@ -0,0 +1,9 @@
export default {
studiengangInformation: function () {
return this.$fhcApi.get(
"/api/frontend/v1/Studgang/getStudiengangInfo",
{}
);
}
}
+2 -2
View File
@@ -6,10 +6,10 @@ export default {
{ ort_kurzbz, start_date, end_date}
);
},
getStundenplan(start_date, end_date) {
getStundenplan(start_date, end_date, lv_id) {
return this.$fhcApi.get(
'/api/frontend/v1/Stundenplan/getStundenplan',
{ start_date, end_date }
{ start_date, end_date, lv_id }
);
},
getStunden() {
+6
View File
@@ -119,5 +119,11 @@ const app = Vue.createApp({
}
});
app.use(FhcApi);
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
tooltip: 8000
}
})
app.use(Phrasen);
app.mount('#cis-header');
-44
View File
@@ -1,44 +0,0 @@
import BsConfirm from "../../components/Bootstrap/Confirm.js";
import CmsNews from "../../components/Cis/Cms/News.js";
import CmsContent from "../../components/Cis/Cms/Content.js";
import Phrasen from "../../plugin/Phrasen.js";
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers";
const app = Vue.createApp({
name: 'CmsApp',
components: {
CmsNews,
CmsContent,
},
mounted() {
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
el.addEventListener("click", (evt) => {
evt.preventDefault();
BsConfirm.popup(el.dataset.confirm)
.then(() => {
Axios.get(el.href)
.then((res) => {
// TODO(chris): check for success then show message and/or reload
location = location;
})
.catch((err) => console.error("ERROR:", err));
})
.catch(() => {});
});
});
document.querySelectorAll("#cms [data-href]").forEach((el) => {
el.href = el.dataset.href.replace(
/^ROOT\//,
FHC_JS_DATA_STORAGE_OBJECT.app_root
);
});
},
});
setScrollbarWidth();
app.use(primevue.config.default, { zIndex: { overlay: 9999 } });
app.use(Phrasen, {reload: true});
app.mount("#cms");
//#cms [data-confirm], #cms [data-href]
+1 -1
View File
@@ -89,5 +89,5 @@ const app = Vue.createApp({
setScrollbarWidth();
app.use(Phrasen, {reload: true});
app.use(Phrasen);
app.mount('#content');
-13
View File
@@ -1,13 +0,0 @@
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info.js";
import Phrasen from "../../plugin/Phrasen.js";
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers";
const app = Vue.createApp({
components: {
Info
}
})
setScrollbarWidth();
app.use(Phrasen, { reload: true }).mount('#content');
-14
View File
@@ -1,14 +0,0 @@
import MylvStudent from "../../../components/Cis/Mylv/Student.js";
import Phrasen from "../../../plugin/Phrasen.js";
import {setScrollbarWidth} from "../../../helpers/CssVarCalcHelpers";
const app = Vue.createApp({
name: 'MyLvStudentApp',
components: {
MylvStudent
}
})
setScrollbarWidth();
app.use(Phrasen, {reload: true}).mount('#content');
@@ -1,6 +1,8 @@
import ProfilUpdateView from "../../components/Cis/ProfilUpdate/ProfilUpdateView.js";
import Phrasen from "../../plugin/Phrasen.js";
// TODO: sobald in verwendung den vue router pfad zu ProfilUpdateView definieren und diese app in component auslagern
const app = Vue.createApp({
name: 'ProfilUpdateRequestsApp',
components: {
-18
View File
@@ -1,18 +0,0 @@
import {CoreNavigationCmpt} from '../components/navigation/Navigation.js';
import CoreDashboard from '../components/Dashboard/Dashboard.js';
import FhcApi from '../../plugin/FhcApi.js';
import Phrasen from '../../plugin/Phrasen.js';
const app = Vue.createApp({
name: 'DashboardApp',
data: () => ({
appSideMenuEntries: {}
}),
components: {
CoreNavigationCmpt,
CoreDashboard
}
})
app.use(FhcApi);
app.use(Phrasen);
app.mount('#main');
+112 -10
View File
@@ -1,20 +1,122 @@
import FhcDashboard from '../../components/Dashboard/Dashboard.js';
import FhcApi from '../../plugin/FhcApi.js';
import Phrasen from '../../plugin/Phrasen.js';
import { setScrollbarWidth } from "../../helpers/CssVarCalcHelpers";
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers";
import Stundenplan from "../../components/Cis/Stundenplan/Stundenplan";
import MylvStudent from "../../components/Cis/Mylv/Student";
import Profil from "../../components/Cis/Profil/Profil";
import CmsNews from "../../components/Cis/Cms/News";
import CmsContent from "../../components/Cis/Cms/Content";
import Info from "../../components/Cis/Mylv/Semester/Studiengang/Lv/Info";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
const router = VueRouter.createRouter({
history: VueRouter.createWebHistory(`/${ciPath}`),
routes: [
{
path: `/Cis/Profil/View/:uid`,
name: 'ProfilView',
component: Profil,
props: true
},
{
path: `/Cis/Profil`,
name: 'Profil',
component: Profil,
props: true
},
{
path: `/CisVue/Cms/Content/:content_id`,
name: 'Content',
component: CmsContent,
props: true
},
{
path: `/CisVue/Cms/News`,
name: 'News',
component: CmsNews,
props: true
},
{
path: `/Cis/MyLv`,
name: 'MyLv',
component: MylvStudent,
props: true
},
{
path: `/Cis/MyLv/Info/:studien_semester/:lehrveranstaltung_id`,
name: 'LvInfo',
component: Info,
props: true
},
{
path: `/Cis/Stundenplan/:lv_id?`,
name: 'Stundenplan',
component: Stundenplan
},
{
path: `/`,
name: 'FhcDashboard',
component: FhcDashboard,
props: {dashboard: 'CIS'},
},
{
path: `/Cis4`,
name: 'Cis4',
component: FhcDashboard,
props: {dashboard: 'CIS'},
}
]
})
const app = Vue.createApp({
name: 'FhcApp',
data: () => ({
appSideMenuEntries: {}
}),
components: {
FhcDashboard
}
name: 'FhcApp',
data: () => ({
appSideMenuEntries: {}
}),
components: {},
methods: {
isInternalRoute(href) {
const internalBase = window.location.origin
return href.startsWith(internalBase);
},
handleClick(event) {
const target = event.target.closest('a');
if (target && this.isInternalRoute(target.href)) {
const path = new URL(target.href).pathname
const base = this.$router.options.history.base
const route = path.replace(base, '') || '/'
// let click event propagate normally if we dont route internally
const res = this.$router.resolve(route)
if(!res?.matched?.length) return
event.preventDefault(); // Prevent browser navigation
this.$router.push(route);
}
}
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeUnmount() {
document.removeEventListener('click', this.handleClick);
},
});
// kind of a bandaid for bad css on some pages to avoid horizontal scroll
setScrollbarWidth();
app.use(router);
app.use(FhcApi);
app.use(primevue.config.default, {
zIndex: {
overlay: 9000,
tooltip: 8000
}
})
app.use(Phrasen);
app.mount('#content');
app.mount('#fhccontent');
+1 -1
View File
@@ -21,5 +21,5 @@ const app = Vue.createApp({
}
});
app
.use(Phrasen, {reload: true})
.use(Phrasen)
.mount('#wrapper');
+5 -2
View File
@@ -6,7 +6,7 @@ import CalendarWeeks from './Weeks.js';
import CalendarDay from './Day.js';
import CalendarMinimized from './Minimized.js';
import CalendarDate from '../../composables/CalendarDate.js';
import CalendarDates from '../../composables/CalendarDates.js';
// TODO(chris): week/month toggle
@@ -89,7 +89,7 @@ export default {
}
}
});
}
},
},
emits: [
'select:day',
@@ -221,6 +221,9 @@ export default {
}
},
unmounted(){
CalendarDates.cleanup();
},
template: /*html*/`
<div ref="container" class="fhc-calendar card h-100" :class="sizeClass">
<component :is="'calendar-' + mode" @updateMode="mode = $event" @change:range="$emit('change:range',$event)" @input="handleInput" >
+1 -1
View File
@@ -13,7 +13,7 @@ export default {
},
computed: {
title() {
return this.focusDate.format({ year: 'numeric' }) + ' KW ' + this.focusDate.w;
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
}
},
methods: {
+109 -84
View File
@@ -1,18 +1,24 @@
import CalendarDate from '../../../composables/CalendarDate.js';
import LvModal from "../../../components/Cis/Mylv/LvModal.js";
function ggt(m, n) { return n == 0 ? m : ggt(n, m % n); }
function kgv(m, n) { return (m * n) / ggt(m, n); }
function ggt(m, n) {
return n == 0 ? m : ggt(n, m % n);
}
function kgv(m, n) {
return (m * n) / ggt(m, n);
}
export default {
components:{
name: 'DayPage',
components: {
LvModal,
},
data() {
return {
hourPosition: null,
hourPositionTime: null,
lvMenu:null,
lvMenu: null,
}
},
inject: [
@@ -39,20 +45,20 @@ export default {
'page:forward',
'input'
],
watch:{
watch: {
//TODO: on first render non of the day-page components are active and the watcher on selectedEvent does not fetch the lvMenu
//TODO: workaround is to watch the active state and refetch in case the lvMenu is empty
active:{
handler(value){
if(value){
if(!this.lvMenu){
active: {
handler(value) {
if (value) {
if (!this.lvMenu) {
this.fetchLvMenu(this.selectedEvent);
}
}
},
immediate:true,
immediate: true,
},
eventsPerDayAndHour:{
eventsPerDayAndHour: {
handler(newEvents) {
// if no event is selected, select the first event of the day
if (this.selectedEvent == null && newEvents[this.day.toDateString()]?.events.length > 0) {
@@ -64,29 +70,27 @@ export default {
},
immediate: true
},
selectedEvent:{
selectedEvent: {
handler(event) {
// return early if the day-page component is not the active carousel item
if(!this.active)
{
if (!this.active) {
return;
}
this.lvMenu = null;
this.fetchLvMenu(event);
},
immediate:true,
immediate: true,
},
isSliding:{
handler(value){
if(value)
{
isSliding: {
handler(value) {
if (value) {
this.setSelectedEvent(null);
}
}
}
},
computed: {
pageHeaderStyle(){
pageHeaderStyle() {
return {
'z-index': 4,
'grid-template-columns': 'repeat(' + this.day.length + ', 1fr)',
@@ -95,13 +99,21 @@ export default {
top: 0,
}
},
dayGridStyle(){
dayText(){
if(!this.size || !this.day)return {};
return {
heading: this.day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
tag: this.day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
datum: this.day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
}
},
dayGridStyle() {
return {
'grid-template-columns': '1 1fr',
'grid-template-rows': 'repeat(' + (this.hours.length * 60 / this.smallestTimeFrame) + ', 1fr)',
}
},
noLvStyle(){
noLvStyle() {
return {
top: (this.calendarScrollTop + 100) + 'px',
position: 'absolute',
@@ -111,19 +123,19 @@ export default {
'z-index': 1,
}
},
indicatorStyle(){
indicatorStyle() {
return {
'pointer-events': 'none',
'padding-left': '3.5rem',
'margin-top': '-1px',
'z-index': 2,
'border-color':'#00649C!important',
'border-color': '#00649C!important',
top: this.hourPosition + 'px',
left: 0,
right: 0,
}
}
},
noEventsCondition(){
noEventsCondition() {
return !this.isSliding && this.filteredEvents?.length === 0;
},
hours() {
@@ -138,17 +150,23 @@ export default {
if (this.isSliding) return {};
const res = {};
let key = this.day.toDateString();
let nextDay = new Date(this.day);
nextDay.setDate(nextDay.getDate() + 1);
nextDay.setMilliseconds(nextDay.getMilliseconds() - 1);
let d = { events: [], lanes: 1 };
let d = {events: [], lanes: 1};
if (this.events[key]) {
this.events[key].forEach(evt => {
let event = {
orig: evt, lane: 1, maxLane: 1, start: evt.start < this.day ? this.day : evt.start, end: evt.end > nextDay ? nextDay : evt.end, shared: [], setSharedMaxRecursive(doneItems) {
orig: evt,
lane: 1,
maxLane: 1,
start: evt.start < this.day ? this.day : evt.start,
end: evt.end > nextDay ? nextDay : evt.end,
shared: [],
setSharedMaxRecursive(doneItems) {
this.maxLane = Math.max(doneItems[0].maxLane, this.maxLane);
doneItems.push(this);
this.shared.filter(other => !doneItems.includes(other)).forEach(i => i.setSharedMaxRecursive(doneItems));
@@ -168,7 +186,7 @@ export default {
d.lanes = d.events.map(e => e.maxLane).reduce((res, i) => kgv(res, i), 1);
}
res[key] = d;
return res;
},
smallestTimeFrame() {
@@ -176,7 +194,7 @@ export default {
},
},
methods: {
fetchLvMenu(event){
fetchLvMenu(event) {
if (event && event.type == 'lehreinheit') {
this.$fhcApi.factory.stundenplan.getLehreinheitStudiensemester(event.lehreinheit_id[0]).then(
res => res.data
@@ -195,14 +213,14 @@ export default {
// this is the id attribute that is responsible to scroll the calender to the first event
return 'scroll' + hour + this.focusDate.d + this.week;
},
hourGridStyle(hour){
hourGridStyle(hour) {
return {
'pointer-events': 'none',
top: this.getAbsolutePositionForHour(hour),
left: 0,
right: 0,
'z-index': 0,
}
}
},
eventGridStyle(day, event) {
return {
@@ -237,26 +255,24 @@ export default {
// calculate minutes from float part of time
let minute = Math.round(60 * minutePercentage);
// convert minutes to 5 minute interval
if (minute % 5 != 0)
{
if (minute % 5 != 0) {
minute = Math.round(minute / 5) * 5;
}
// in case the rounding made the minutes 60, increase the hour and reset the minutes
if (minute == 60)
{
if (minute == 60) {
currentHour++;
minute = 0;
}
// ## after rounding the time to the nearest 5 Minute interval, we have to convert the time back to the relative position
// convert current time in minutes
currentMinutes = currentHour * 60 + minute;
// calculate the minutes percentage of the total minutes
timePercentage = ((currentMinutes - ( this.hours[0] * 60 ) ) / ( this.hours.length * 60 ) ) * 100;
timePercentage = ((currentMinutes - (this.hours[0] * 60)) / (this.hours.length * 60)) * 100;
// calculate the relative position of the time percentage
position = height * (timePercentage / 100);
this.hourPosition = position;
// add padding to minutes that consist of only one digit
minute.toString().length == 1 ? minute = "0" + minute : minute;
this.hourPositionTime = currentHour + ":" + minute;
@@ -275,58 +291,66 @@ export default {
dateToMinutesOfDay(day) {
return Math.floor(((day.getHours() - 7) * 60 + day.getMinutes()) / this.smallestTimeFrame) + 1;
},
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) container.style.overflow = 'hidden'
},
template: /*html*/`
<div class="fhc-calendar-day-page ">
<div class="row m-0">
<div class="col-12 col-xl-6 p-0">
<div class="d-flex flex-column">
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle" >
<div type="button" class="flex-grow-1" :title="day.toLocaleString(undefined, {dateStyle:'short'})" @click.prevent="changeToMonth(day)">
<div class="fw-bold">{{day.toLocaleString(undefined, {weekday: size < 2 ? 'narrow' : (size < 3 ? 'short' : 'long')})}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{day.toLocaleString(undefined, [{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{dateStyle:'short'}][this.size])}}</a>
</div>
</div>
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
<Transition>
<div v-if="hourPosition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
<div class="fhc-calendar-day-page h-100">
<div class="row m-0 h-100">
<div class="col-12 col-xl-6 p-0 h-100">
<div class="d-flex flex-column h-100">
<div ref="header" class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle">
<div type="button" class="flex-grow-1" :title="dayText.heading" @click.prevent="changeToMonth(day)">
<div class="fw-bold">{{dayText.tag}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{dayText.datum}}</a>
</div>
</Transition>
<div>
<h1 v-if="noEventsCondition" class="m-0 text-secondary" ref="noEventsText" :style="noLvStyle">Keine Lehrveranstaltungen</h1>
<div :class="{'fhc-calendar-no-events-overlay':noEventsCondition, 'events':true}">
<div class="hours">
<div v-for="hour in hours" style="min-height:100px" :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle">
<div v-for="event in day.events" :key="event" :style="eventGridStyle(day,event)" :class="{'selectedEvent':event.orig == selectedEvent}" class="mx-2 small rounded overflow-hidden " >
<!-- desktop version of the page template, parent receives slotProp mobile = false -->
<div class="d-none d-xl-block h-100 " @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="false">
<p>this is a slot placeholder</p>
</slot>
</div>
<div id="scroll g-0" style="height: 100%; overflow: scroll;">
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
<div :id="hourGridIdentifier(hour)" v-for="hour in hours" :key="hour" class="position-absolute box-shadow-border-top" :style="hourGridStyle(hour)"></div>
<Transition>
<div v-if="hourPosition && !noEventsCondition" class="position-absolute border-top small" :style="indicatorStyle">
<span class="border border-top-0 px-2 bg-white">{{hourPositionTime}}</span>
</div>
</Transition>
<div>
<h1 v-if="noEventsCondition" class="m-0 text-secondary" ref="noEventsText" :style="noLvStyle">Keine Lehrveranstaltungen</h1>
<div :class="{'fhc-calendar-no-events-overlay':noEventsCondition, 'events':true}">
<div class="hours">
<div v-for="hour in hours" style="min-height:100px" :key="hour" class="text-muted text-end small" :ref="'hour' + hour">{{hour}}:00</div>
</div>
<!-- mobile version of the page template, parent receives slotProp mobile = true -->
<div class="d-block d-xl-none h-100" @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="true">
<p>this is a slot placeholder</p>
</slot>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle">
<div v-for="event in day.events" :key="event" :style="eventGridStyle(day,event)" :class="{'selectedEvent':event.orig == selectedEvent}" class="mx-2 small rounded overflow-hidden " >
<!-- desktop version of the page template, parent receives slotProp mobile = false -->
<div class="d-none d-xl-block h-100 " @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="false">
<p>this is a slot placeholder</p>
</slot>
</div>
<!-- mobile version of the page template, parent receives slotProp mobile = true -->
<div class="d-block d-xl-none h-100" @click.prevent="eventClick(event)">
<slot name="dayPage" :event="event" :day="day" :mobile="true">
<p>this is a slot placeholder</p>
</slot>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="d-none d-xl-block col-xl-6 p-0">
<div style="z-index:0" class="p-5 sticky-top d-flex justify-content-center align-items-center flex-column">
<div style="max-height: calc(var(--fhc-calendar-pane-height) - 100px); overflow-y:auto;" class="w-100">
<div class="d-xl-block col-xl-6 p-0">
<div style="z-index:0" class="p-4 sticky-top d-flex justify-content-center align-items-center flex-column">
<div style="max-height: calc(var(--fhc-calendar-pane-height)); overflow-y:auto;" class="w-100">
<template v-if="selectedEvent && lvMenu">
<slot name="pageMobilContent" :lvMenu="lvMenu" >
<p>this is a slot placeholder</p>
@@ -345,7 +369,8 @@ export default {
</div>
</div>
</div>
</div>
</div>`
</div>
</div>
`
}
+1 -1
View File
@@ -3,9 +3,9 @@ export default {
return{
selected: this.mode,
modes:{
day: { mode_bezeichnung: "Tag", icon: "fa-calendar-day" , condition:true},
week: { mode_bezeichnung: "Woche", icon: "fa-calendar-week", condition: !this.noWeekView },
month: { mode_bezeichnung: "Monat", icon: "fa-calendar-days", condition: !this.noMonthView },
day: { mode_bezeichnung: "Tag", icon: "fa-sun" , condition:true},
},
headerPadding:null,
}
+1 -1
View File
@@ -17,7 +17,7 @@ export default {
},
computed: {
title() {
return this.focusDate.format({month: ['short','long','long','long'][this.size], year: 'numeric'});
return this.focusDate.format({month: ['short','long','long','long'][this.size], year: 'numeric'}, this.$p.user_locale.value);
}
},
methods: {
+14 -2
View File
@@ -28,6 +28,14 @@ export default {
'input'
],
computed: {
dayText(){
if (!this.size || !this.weeks[0]?.days) return {};
let dayTextMap ={};
this.weeks[0].days.forEach((day)=>{
dayTextMap[day] = day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 1 ? 'narrow' : (this.size < 3 ? 'short' : 'long') });
});
return dayTextMap;
},
weeks() {
let firstDayOfMonth = new CalendarDate(this.year, this.month, 1);
let startDay = firstDayOfMonth.firstDayOfCalendarMonth;
@@ -39,7 +47,7 @@ export default {
week.days.push(new Date(startDay));
if (week.days.length == 7) {
let d = new CalendarDate(week.days[res.length ? 0 : 6]);
let d = new CalendarDate(week.days[5]);
week.no = d.w;
week.y = d.y;
res.push(week);
@@ -79,11 +87,15 @@ export default {
this.selectDay(day);
}
},
mounted() {
const container = document.getElementById("calendarContainer")
if(container) container.style.overflow = 'scroll'
},
template: /*html*/`
<div class="fhc-calendar-month-page" :class="{'show-weeks': showWeeks}">
<div v-if="showWeeks" class=" bg-light fw-bold border-top border-bottom text-center"></div>
<div v-for="day in weeks[0].days" :key="day" class="bg-light fw-bold border-top border-bottom text-center">
{{day.toLocaleString(undefined, {weekday: size < 1 ? 'narrow' : (size < 3 ? 'short' : 'long')})}}
{{dayText[day]}}
</div>
<template v-for="week in weeks" :key="week.no">
<a href="#" v-if="showWeeks" class="fhc-calendar-month-page-weekday text-decoration-none text-end opacity-25" @click.prevent="changeToWeek(week)">{{week.no}}</a>
+1 -1
View File
@@ -17,7 +17,7 @@ export default {
return this.focusDate.format({year: 'numeric'});
},
months() {
return this.monthIndices.map(i => (new Date(0, i, 1)).toLocaleString(undefined, {month: this.size < 2 ? 'short' : 'long'}));
return this.monthIndices.map(i => (new Date(0, i, 1)).toLocaleString(this.$p.user_locale.value, {month: this.size < 2 ? 'short' : 'long'}));
}
},
template: `
+2 -2
View File
@@ -82,8 +82,8 @@ export default {
template: `
<div ref="carousel" class="calendar-pane carousel slide" @[\`slide.bs.carousel\`]="slide" @[\`slid.bs.carousel\`]="slid" :data-queue="queue">
<!--height calc function just for user testing purpose (has to be fixed)-->
<div @scroll="scrollCalendar" ref="calendarContainer" class="carousel-inner " style="height:var(--fhc-calendar-pane-height); overflow:scroll">
<div ref="carouselItems" v-for="i in [...Array(3).keys()]" :key="i" class="carousel-item">
<div id="calendarContainer" @scroll="scrollCalendar" ref="calendarContainer" class="carousel-inner " style="height:var(--fhc-calendar-pane-height);">
<div ref="carouselItems" v-for="i in [...Array(3).keys()]" :key="i" class="carousel-item" style="height:var(--fhc-calendar-pane-height);">
<slot :active="i == activeCarouselItemIndex" :index="i" :offset="offsets[i]" />
</div>
</div>
+2 -2
View File
@@ -12,7 +12,7 @@ export default {
},
computed: {
title() {
return this.focusDate.format({year: 'numeric'}) + ' KW ' + this.focusDate.w;
return this.focusDate.wYear + ' KW ' + this.focusDate.w;
}
},
methods: {
@@ -42,7 +42,7 @@ export default {
<div class="fhc-calendar-week">
<calendar-header :title="title" @prev="prev" @next="next" @updateMode="$emit('updateMode', $event)" @click="$emit('updateMode', 'weeks')"/>
<calendar-pane ref="pane" v-slot="slot" @slid="paneChanged">
<calendar-week-page :year="focusDate.y" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<calendar-week-page :year="focusDate.wYear" :week="focusDate.w+slot.offset" @updateMode="$emit('updateMode', $event)" @page:back="prev" @page:forward="next" @input="selectEvent" >
<template #weekPage="{event,day,isSelected}">
<slot name="weekPage" :event="event" :day="day" :isSelected="isSelected"></slot>
</template>
+20 -6
View File
@@ -56,6 +56,18 @@ export default {
// returns an array with elements starting at 7 and ending at 24
return [...Array(24).keys()].filter(hour => hour >= 7 && hour <= 24);
},
dayText() {
if (!this.size || !this.days) return {};
let dayTextMap ={};
this.days.forEach((day)=>{
dayTextMap[day] = {
heading: day.toLocaleString(this.$p.user_locale.value, { dateStyle: 'short' }),
tag: day.toLocaleString(this.$p.user_locale.value, { weekday: this.size < 2 ? 'narrow' : (this.size < 3 ? 'short' : 'long') }),
datum: day.toLocaleString(this.$p.user_locale.value, [{ day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { day: 'numeric', month: 'numeric' }, { dateStyle: 'short' }][this.size]),
};
});
return dayTextMap;
},
days() {
let tmpDate = new CalendarDate(this.year,1,1); // NOTE(chris): somewhere in the middle of the year
@@ -107,7 +119,7 @@ export default {
smallestTimeFrame() {
return [30,15,10,5][this.size];
}
},
},
methods: {
hourGridIdentifier(hour) {
// this is the id attribute that is responsible to scroll the calender to the first event
@@ -201,15 +213,17 @@ export default {
mounted() {
setTimeout(() => this.$refs.eventcontainer.scrollTop = this.$refs.eventcontainer.scrollHeight / 3 + 1, 0);
const container = document.getElementById("calendarContainer")
if(container) container.style.overflow = 'scroll'
},
template: /*html*/`
<div class="fhc-calendar-week-page">
<div class="d-flex flex-column">
<div class="fhc-calendar-week-page-header d-grid border-2 border-bottom text-center" :style="pageHeaderStyle" >
<div type="button" v-for="day in days" :key="day" class="flex-grow-1" :title="day.toLocaleString(undefined, {dateStyle:'short'})" @click.prevent="changeToMonth(day)">
<div class="fw-bold">{{day.toLocaleString(undefined, {weekday: size < 2 ? 'narrow' : (size < 3 ? 'short' : 'long')})}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{day.toLocaleString(undefined, [{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{day:'numeric',month:'numeric'},{dateStyle:'short'}][this.size])}}</a>
<div type="button" v-for="day in days" :key="day" class="flex-grow-1" :title="dayText[day]?.heading" @click.prevent="changeToMonth(day)">
<div class="fw-bold">{{dayText[day]?.tag}}</div>
<a href="#" class="small text-secondary text-decoration-none" >{{dayText[day]?.datum}}</a>
</div>
</div>
<div ref="eventcontainer" class="position-relative flex-grow-1" @mousemove="calcHourPosition" @mouseleave="hourPosition = null" >
@@ -225,7 +239,7 @@ export default {
</div>
<div v-for="day in eventsPerDayAndHour" :key="day" class=" day border-start" :style="dayGridStyle(day)">
<div v-for="event in day.events" :key="event" @click.prevent="weekPageClick(event.orig, day)" :style="eventGridStyle(day,event)" class="mx-2 small rounded overflow-hidden " >
<slot name="weekPage" :event="event" :day="day" :isSelected="event.orig == selectedEvent" >
<slot name="weekPage" :event="event" :day="day" :isSelected="event.orig == selectedEvent">
<p>this is a placeholder which means that no template was passed to the Calendar Page slot</p>
</slot>
</div>
+13 -5
View File
@@ -9,11 +9,11 @@ export default {
'focusDate'
],
data() {
return {
weeks: [...Array(this.focusDate.numWeeks).keys()].map(i => i+1)
}
},
computed: {
weeks(){
return [...Array(this.focusDate.numWeeks).keys()].map(i => i + 1);
},
title() {
return this.focusDate.format({year: 'numeric'});
}
@@ -23,11 +23,19 @@ export default {
// TODO(chris): test is there a week jump on year select? => yes there is if the same month/day are in different weeks ... should we prevent that?
this.focusDate.w = week;
this.$emit('updateMode', 'week');
}
},
prev(){
this.focusDate.y--;
this.focusDate._clean();
},
next() {
this.focusDate.y++;
this.focusDate._clean();
},
},
template: `
<div class="fhc-calendar-weeks">
<calendar-header :title="title" @prev="focusDate.y--" @next="focusDate.y++" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
<calendar-header :title="title" @prev="prev" @next="next" @click="$emit('updateMode', 'years')" @updateMode="$emit('updateMode', $event)" />
<div class="d-flex flex-wrap">
<div v-for="(week, key) in weeks" :key="key" class="d-grid col-2">
<button @click="setWeek(week)" class="btn btn-outline-secondary" :class="{'border-0': week != focusDate.w}">
+24
View File
@@ -0,0 +1,24 @@
export const FhcChart = {
name: 'FhcChart',
props: {
chartOptions: {
type: Object,
}
},
template: `
<div style="width:100%;height:100%;overflow:auto">
<div role="group" aria-label="Chart Modus">
<Button :class="(chartOptions.chart.type === 'pie' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='pie'"><i class="fa-solid fa-chart-pie"></i></Button>
<Button :class="(chartOptions.chart.type === 'bar' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='bar'"><i class="fa-solid fa-chart-bar"></i></Button>
<Button :class="(chartOptions.chart.type === 'column' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='column'"><i class="fa-solid fa-chart-simple"></i></Button>
<Button :class="(chartOptions.chart.type === 'line' ? 'active ' : '') + 'btn btn-outline-secondary'" style="width: 48px;" @click="chartOptions.chart.type='line'"><i class="fa-solid fa-chart-line"></i></Button>
</div>
<figure>
<highcharts class="chart" :options="chartOptions"></highcharts>
</figure>
</div>
`
};
export default FhcChart
+86 -51
View File
@@ -1,59 +1,94 @@
import raum_contentmittitel from './Content_types/Raum_contentmittitel.js'
import general from './Content_types/General.js'
import BsConfirm from "../../Bootstrap/Confirm";
export default {
props:{
content_id:{
type:Number,
required:true,
},
version:{
type:[String, Number],
default: null,
},
sprache:{
type:[String, Number],
default: null,
},
sichtbar:{
type:[String, Number],
default: null,
}
},
components:{
raum_contentmittitel,
general,
},
data() {
return {
content: null,
};
},
computed:{
computeContentType: function(){
switch(this.content_type){
case "raum_contentmittitel":
return "raum_contentmittitel";
default:
return "general";
};
},
},
created() {
this.$fhcApi.factory.cms.content(this.content_id,this.version, this.sprache, this.sichtbar).then(res =>{
this.content = res.data.content;
this.content_type=res.data.type;
});
},
mounted(){
},
template: /*html*/ `
name: "ContentComponent",
props: {
content_id: {
type: [Number, String],
required: true
},
version: {
type: [String, Number],
default: null,
},
sichtbar: {
type: [String, Number],
default: null,
}
},
components: {
raum_contentmittitel,
general,
},
data() {
return {
content: null,
content_id_internal: this.content_id
};
},
methods: {
fetchContent(){
return this.$fhcApi.factory.cms.content(this.content_id_internal, this.version, this.sprache, this.sichtbar).then(res => {
this.content = res.data.content;
this.content_type = res.data.type;
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
el.addEventListener("click", (evt) => {
evt.preventDefault();
BsConfirm.popup(el.dataset.confirm)
.then(() => {
Axios.get(el.href)
.then((res) => {
// TODO(chris): check for success then show message and/or reload
location = location;
})
.catch((err) => console.error("ERROR:", err));
})
.catch(() => {
});
});
});
document.querySelectorAll("#cms [data-href]").forEach((el) => {
el.href = el.dataset.href.replace(
/^ROOT\//,
FHC_JS_DATA_STORAGE_OBJECT.app_root
);
});
});
}
},
watch:{
sprache: function(sprache){
this.fetchContent();
},
'$route.params.content_id'(newVal) {
this.content_id_internal = newVal
this.fetchContent();
}
},
computed: {
sprache(){
return this.$p.user_language.value;
},
computeContentType: function () {
switch (this.content_type) {
case "raum_contentmittitel":
return "raum_contentmittitel";
default:
return "general";
};
},
},
created() {
this.fetchContent();
},
mounted() {
},
template: /*html*/ `
<!-- div that contains the content -->
<component :is="computeContentType" v-if="content" :content="content" :content_id="content_id" />
<component ref="content" :is="computeContentType" v-if="content" :content="content" :content_id="content_id_internal" />
<p v-else>No content is available to display</p>
`,
};
@@ -1,5 +1,6 @@
import { replaceRelativeLegacyLink } from "../../../../helpers/LegacyLinkReplaceHelper.js"
export default {
name: "GeneralComponent",
props:{
content:{
type:String,
@@ -98,6 +99,12 @@ export default {
FHC_JS_DATA_STORAGE_OBJECT.app_root
);
});
document.querySelectorAll("[href]").forEach((element) => {
let orignal_href = element.getAttribute("href");
let new_href = replaceRelativeLegacyLink(orignal_href);
element.href = new_href;
});
},
template: /*html*/ `
<!-- div that contains the content -->
@@ -1,5 +1,6 @@
export default {
name: "RaumComponent",
props:{
content:{
type:String,
+60 -7
View File
@@ -1,17 +1,62 @@
import Pagination from "../../Pagination/Pagination.js";
import StudiengangInformation from "./StudiengangInformation/StudiengangInformation.js";
import BsConfirm from "../../Bootstrap/Confirm";
export default {
name: "NewsComponent",
components: {
Pagination,
StudiengangInformation,
},
data() {
return {
content: null,
maxPageCount: 0,
page_size: 10,
page:1,
};
},
watch:{
'$p.user_language.value':function(sprache){
this.fetchNews();
}
},
computed:{
sprache: function(){
return this.$p.user_language.value;
},
},
methods: {
fetchNews: function(){
return this.$fhcApi.factory.cms.getNews(this.page, this.page_size, this.sprache)
.then(res => res.data)
.then(result => {
this.content = result;
document.querySelectorAll("#cms [data-confirm]").forEach((el) => {
el.addEventListener("click", (evt) => {
evt.preventDefault();
BsConfirm.popup(el.dataset.confirm)
.then(() => {
Axios.get(el.href)
.then((res) => {
// TODO(chris): check for success then show message and/or reload
location = location;
})
.catch((err) => console.error("ERROR:", err));
})
.catch(() => {
});
});
});
document.querySelectorAll("#cms [data-href]").forEach((el) => {
el.href = el.dataset.href.replace(
/^ROOT\//,
FHC_JS_DATA_STORAGE_OBJECT.app_root
);
});
});
},
loadNewPageContent: function (data) {
this.$fhcApi.factory.cms.getNews(data.page, data.rows)
.then(res => res.data)
@@ -22,11 +67,7 @@ export default {
},
},
created() {
this.$fhcApi.factory.cms.getNews(1, this.page_size)
.then(res => res.data)
.then(result => {
this.content = result;
});
this.fetchNews();
this.$fhcApi.factory.cms.getNewsRowCount()
.then(res => res.data)
@@ -37,8 +78,20 @@ export default {
template: /*html*/ `
<h2 >News</h2>
<hr/>
<pagination :page_size="page_size" @page="loadNewPageContent" :maxPageCount="maxPageCount">
<pagination v-show="content?true:false" :page_size="page_size" @page="page=$event.page; loadNewPageContent($event)" :maxPageCount="maxPageCount">
</pagination>
<div class="container-fluid">
<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>
<pagination v-show="content?true:false" :page_size="page_size" @page="loadNewPageContent" :maxPageCount="maxPageCount">
</pagination>
<div v-html="content"></div>
`,
};
@@ -0,0 +1,120 @@
import StudiengangPerson from "./StudiengangPerson";
import StudiengangVertretung from "./StudiengangVertretung";
export default {
data(){
return{
studiengang:null,
semester: null,
stg_ltg: null,
gf_ltg: null,
stv_ltg: null,
ass: null,
hochschulvertr: null,
stdv: null,
jahrgangsvertr: null,
}
},
props:{
displayWidget:{
type:Boolean,
default:false,
}
},
components:{
StudiengangPerson,
StudiengangVertretung,
},
template:/*html*/`
<template v-if="studiengang?.bezeichnung && semester">
<div class="card card-body mb-3">
<div class="mb-1">
<h2 class="mb-1 pb-0">{{$p.t('lehre','studiengang')}}:</h2>
<span class="mb-1">{{studiengang.bezeichnung}}</span>
</div>
<div class="mb-1">
<h2 class="mb-1 pb-0">Moodle:</h2>
<a class="mb-1" target="_blank" :href="moodleLink">{{studiengang.kurzbzlang}}</a>
</div>
<div :class="{'mb-1':studiengang?.zusatzinfo_html}">
<h2 class="mb-1 pb-0">{{$p.t('lehre','studiensemester')}}: </h2>
<span class="mb-1">{{semester}}</span>
</div>
<div v-if="studiengang?.zusatzinfo_html" v-html="studiengang?.zusatzinfo_html"></div>
</div>
</template>
<template v-for="{title, collection} in collection_array">
<template v-if="Array.isArray(collection) && collection.length !==0">
<h2 class="text-truncate">{{title}}</h2>
<template v-if="displayWidget">
<div class="d-flex flex-wrap flex-row mb-3 gap-2">
<template v-for="person in collection">
<studiengang-person displayWidget v-bind="person"></studiengang-person>
</template>
</div>
</template>
<template v-else>
<template v-for="person in collection">
<div class="mb-3">
<studiengang-person v-bind="person"></studiengang-person>
</div>
</template>
</template>
</template>
</template>
<template v-if="hochschulvertr && Array.isArray(hochschulvertr) && hochschulvertr.length >0">
<studiengang-vertretung showBezeichnung :title="$p.t('studiengangInformation', 'Hochschulvertretung')" :vertretungsList="hochschulvertr"></studiengang-vertretung>
</template>
<template v-if="stdv && Array.isArray(stdv) && stdv.length >0">
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Studienvertretung').concat(studiengang.kurzbzlang??'')" :vertretungsList="stdv"></studiengang-vertretung>
</template>
<template v-if="jahrgangsvertr && Array.isArray(jahrgangsvertr) && jahrgangsvertr.length >0">
<studiengang-vertretung :title="$p.t('studiengangInformation', 'Jahrgangsvertretung')" :vertretungsList="jahrgangsvertr"></studiengang-vertretung>
</template>
`,
computed:{
// this function concatenates the studiengangsleitung and the assistenz or the
// geschaeftsfuehrende-Stellvertretende Leitung if both collections only contain one person
collection_array: function(){
let returnData = [];
if (Array.isArray(this.stg_ltg) && this.stg_ltg.length == 1 && Array.isArray(this.ass) && this.ass.length == 1)
{
returnData.push({ title: `${this.$p.t('global', 'studiengangsleitung')}/${this.$p.t('studiengangInformation', 'assistenz')}` , collection: [...this.stg_ltg, ...this.ass] });
}
else
{
returnData.push({ title: this.$p.t('global', 'studiengangsleitung'), collection: this.stg_ltg });
returnData.push({ title: this.$p.t('studiengangInformation', 'assistenz'), collection: this.ass });
}
if (Array.isArray(this.gf_ltg) && this.gf_ltg.length == 1 && Array.isArray(this.stv_ltg) && this.stv_ltg.length == 1)
{
returnData.push({ title: this.$p.t('studiengangInformation', 'geschaeftsfuehrende_stellvertretende_leitung'), collection: [...this.gf_ltg, ...this.stv_ltg] });
}
else
{
returnData.push({ title: this.$p.t('studiengangInformation', 'geschaeftsfuehrende_leitung'), collection: this.gf_ltg });
returnData.push({ title: this.$p.t('studiengangInformation', 'stellvertretende_leitung'), collection: this.stv_ltg });
}
return returnData;
},
moodleLink: function(){
// early return if the studiengang information is not available
if(!this.studiengang || !this.studiengang.studiengang_kz) return;
return `https://moodle.technikum-wien.at/course/view.php?idnumber=dl` + this.studiengang.studiengang_kz;
},
},
mounted(){
this.$fhcApi.factory.studiengang.studiengangInformation()
.then(res => res.data)
.then(studiengangInformationen => {
Object.assign(this, studiengangInformationen);
});
},
};
@@ -0,0 +1,103 @@
export default {
props:{
uid:String,
vorname:String,
nachname:String,
titelpre:String,
kontakt:String,
telefoneklappe:String,
email:String,
planbezeichnung:String,
foto:String,
displayWidget:{
type:Boolean,
default:false,
}
},
template:/*html*/`
<div class="card" :style="{'width':displayWidget?'12rem':'15rem'}">
<div class="bg-dark d-flex justify-content-center">
<img :src="base64Image" alt="mitarbeiter_foto" style="width: 110px; height: auto; object-fir:scale-down;" class="card-img-top" >
</div>
<div class="card-body">
<h6 class="text-center card-title mb-0">{{fullname}} <a v-if="profilViewLink" :href="profilViewLink"><i class="ms-2 fa fa-arrow-up-right-from-square" style="color:#00649C"></i></a></h6>
</div>
<hr class="my-0">
<div class="card-body">
<div class="flex flex-column gap-3">
<div class="mb-3">
<span>
<i class="fa fa-phone me-2"></i>
<a :href="phone.link">{{phone.number}}</a>
</span>
</div>
<div class="mb-3">
<span>
<i class="fa fa-home me-2"></i>
{{ort}}
</span>
</div>
<div>
<span>
<i class="fa-regular fa-envelope me-2"></i>
<a :href="email_link" v-html="formattedEmail"></a>
</span>
</div>
</div>
</div>
</div>
`,
computed:{
formattedEmail: function(){
if(!this.email ) return null;
let emailString= this.email.replace("mailto:", "");
// when splitting a string, the letter that is used to split the string will be removed from the result
let emailArray = emailString.split('@');
// returns both parts of the splitted string in combination with the removed letter and a word break
return emailArray[0] + '@<wbr>' + emailArray[1];
},
fullname: function () {
if (this.titelpre && this.vorname && this.nachname) {
return `${this.titelpre} ${this.vorname} ${this.nachname}`;
}
else if (this.vorname && this.nachname) {
return `${this.vorname} ${this.nachname}`;
}
else if (this.nachname) {
return this.vorname;
}
else {
return null;
}
},
phone: function () {
if (this.kontakt && this.telefoneklappe) {
return {
link: "tel:".concat(this.kontakt).concat(" " + this.telefoneklappe),
number: this.kontakt.concat(" " + this.telefoneklappe),
}
}
else {
return this.kontakt ? {
link: "tel:".concat(this.kontakt),
number: this.kontakt,
} : null;
}
},
email_link: function () {
return this.email ? "mailto:".concat(this.email) : null;
},
base64Image:function(){
return this.foto ? 'data:image/png;base64,'.concat(this.foto) : null;
},
ort:function(){
return this.planbezeichnung ?? null;
},
profilViewLink: function(){
return this.uid ? FHC_JS_DATA_STORAGE_OBJECT.app_root.concat(FHC_JS_DATA_STORAGE_OBJECT.ci_router).concat("/Cis/Profil/View/").concat(this.uid): null;
},
},
}
@@ -0,0 +1,32 @@
export default {
props: {
title:String,
vertretungsList:Array,
showBezeichnung:Boolean,
},
template:/*html*/`
<div class="card mb-3">
<div class="card-header">
<span>{{title}}</span>
</div>
<div class="card-body">
<p v-for="vertretung in vertretungsList">
<a v-if="profilViewLink(vertretung.uid)" :href="profilViewLink(vertretung.uid)">
<i class="me-2 fa fa-arrow-up-right-from-square" style="color:#00649C"></i>
</a>
{{vertretungFormatedName(vertretung,false)}}
</p>
</div>
</div>
`,
methods: {
profilViewLink: function (uid) {
return uid ? FHC_JS_DATA_STORAGE_OBJECT.app_root.concat(FHC_JS_DATA_STORAGE_OBJECT.ci_router).concat("/Cis/Profil/View/").concat(uid) : null;
},
vertretungFormatedName: function (vertretung) {
if (!vertretung) return null;
return `${vertretung.vorname ?? ''} ${vertretung.nachname ?? ''} ${vertretung.bezeichnung && this.showBezeichnung ? '('.concat(vertretung.bezeichnung.replace("(", "").replace(")", "")).concat(")") : ''}`
},
},
}
+16 -5
View File
@@ -9,7 +9,6 @@ export default {
CisSprachen,
},
props: {
menu: Array,
rootUrl: String,
logoUrl: String,
avatarUrl: String,
@@ -51,6 +50,13 @@ export default {
}
},
methods: {
fetchMenu: function(){
return this.$fhcApi.factory.menu.getMenu()
.then(res => res.data)
.then(menu => {
this.entries = menu;
})
},
checkSettingsVisibility: function (event) {
// hides the settings collapsible if the user clicks somewhere else
if (!this.$refs.navUserDropdown.contains(event.target)) {
@@ -64,6 +70,10 @@ export default {
document.removeEventListener("click", this.checkSettingsVisibility);
},
makeParentContentActive(content_id, collection=this.entries, parent=null){
if(!collection) return;
if (typeof collection == 'object' && !Array.isArray(collection) && Object.entries(collection).length > 0) {
collection = Object.values(collection);
}
for(let entry of collection){
if(entry.content_id == content_id){
this.activeEntry = parent;
@@ -80,8 +90,10 @@ export default {
this.activeEntry = content_id;
},
},
created(){
this.fetchMenu();
},
mounted(){
this.entries = this.menu;
this.$p.loadCategory(['ui', 'global'])
this.navUserDropdown = new bootstrap.Collapse(this.$refs.navUserDropdown,{
toggle: false
@@ -91,7 +103,7 @@ export default {
<button id="nav-main-btn" class="navbar-toggler rounded-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#nav-main" aria-controls="nav-main" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<fhc-searchbar ref="searchbar" id="nav-search" class="fhc-searchbar w-100" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar>
<fhc-searchbar ref="searchbar" id="nav-search" style="background-color:var(--fhc-cis-primary)" class="fhc-searchbar w-100 p-1 p-lg-2" :searchoptions="searchbaroptions" :searchfunction="searchfunction"></fhc-searchbar>
<a id="nav-logo" class="d-none d-lg-block" :href="rootUrl">
<img :src="logoUrl" alt="Logo">
</a>
@@ -105,7 +117,7 @@ export default {
id="nav-user-menu" class="top-100 end-0 collapse list-unstyled" aria-labelledby="nav-user-btn">
<li class="btn-level-2"><a class="btn btn-level-2 rounded-0 d-block" :href="site_url + '/Cis/Profil'" id="menu-profil">Profil</a></li>
<li class="btn-level-2">
<cis-sprachen></cis-sprachen>
<cis-sprachen @languageChanged="fetchMenu"></cis-sprachen>
</li>
<li class="btn-level-2"><hr class="dropdown-divider m-0 "></li>
<li><a class="btn btn-level-2 rounded-0 d-block" :href="logoutUrl">Logout</a></li>
@@ -119,7 +131,6 @@ export default {
</button>
</div>
<div class="offcanvas-body p-0">
<div id="nav-main-menu" class="nav-menu-collapse collapse collapse-horizontal show">
<div>
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeEntry" v-for="entry in entries" :key="entry.content_id" :entry="entry" />
+7 -6
View File
@@ -55,11 +55,8 @@ export default {
this.makeParentContentActive(this.entry.content_id);
}
}
// debugging helpers - console.log(this.entry.titel, newValue ? "open" : "close")
},
},
computed: {
active: function () {
if (this.entry.menu_open){
@@ -77,6 +74,7 @@ export default {
return '';
let xmlDoc = (new DOMParser()).parseFromString(this.entry.content,"text/xml");
let url = xmlDoc.getElementsByTagName('url')[0];
if (!url)
return '';
// TODO(chris): replace get params
@@ -202,7 +200,9 @@ export default {
<template v-else>
<template v-if="hasChilds">
<div class="btn-group w-100">
<a :href="(entry.menu_open)?link:null" :target="target" @click="toggleCollapse"
<a :target="target"
:href="(entry.menu_open)?link:null"
@click="toggleCollapse"
:class="{
'btn btn-default rounded-0 text-start': true,
['btn-level-' + level]: true,
@@ -223,14 +223,15 @@ export default {
<cis-menu-entry :highestMatchingUrlCount="highestMatchingUrlCount" :activeContent="activeContent" v-for="child in entry.childs" :key="child" :entry="child" :level="level + 1"/>
</ul>
</template>
<a v-else
<a v-else
:href="link"
:target="target"
:class="{
'btn btn-default rounded-0 w-100 text-start': true,
['btn-level-' + level]: true,
'fw-bold':active
}">
}"
@mouseup="setActiveEntry(entry.content_id)">
{{ entry.titel }}
</a>
</template>`
@@ -28,17 +28,37 @@ export default {
sprache: String,
ects: String,
incoming: Number,
positiv: Boolean
positiv: Boolean,
note_index: String,
},
data: () => {
return {
pruefungen: null,
pruefungenData: null,
info: null,
menu: null,
preselectedMenuItem: null,
}
},
computed: {
gradeColor() {
// early return if value is null or undefined
if (this.positiv == null) return;
// returns a suitable color for the given grade
if (this.positiv)
{
return 'var(--fhc-cis-grade-positive)';
}
else
{
return 'var(--fhc-cis-grade-negative)';
}
},
is_organisatorische_einheit(){
return this.menu == "organisatorische_einheit";
},
emptyMenu(){
return !this.menu || !Array.isArray(this.menu) || Array.isArray(this.menu) && this.menu.length == 0;
},
bodyStyle() {return {};
const bodyStyle = {};
if (this.farbe)
@@ -47,9 +67,24 @@ export default {
},
grade() {
return this.benotung ? this.znote || this.lvnote || null : null;
}
},
LvHasPruefungenInformation(){
return this.pruefungenData && this.pruefungenData.length > 0;
},
},
methods: {
fetchMenu(lehrveranstaltung_id = this.lehrveranstaltung_id, studien_semester = this.studien_semester){
return this.$fhcApi.factory.addons.getLvMenu(lehrveranstaltung_id, studien_semester)
.then(res => {
this.menu = res.data;
})
.catch((error) => {
this.$fhcAlert.handleSystemError(error);
this.menu = [];
});
},
c4_link(menuItem) {
if (!menuItem) return null;
if (Array.isArray(menuItem.c4_moodle_links) && menuItem.c4_moodle_links.length) {
@@ -72,13 +107,13 @@ export default {
}
},
openPruefungen() {
if (!this.pruefungen) {
this.pruefungen = true;
LvPruefungen.popup({
lehrveranstaltung_id: this.lehrveranstaltung_id,
bezeichnung: this.bezeichnung
}).then(() => this.pruefungen = false).catch(() => this.pruefungen = false);
}
// early return if the pruefungenData is empty or not set
if (!this.LvHasPruefungenInformation) return;
LvPruefungen.popup({
pruefungenData: this.pruefungenData,
bezeichnung: this.bezeichnung
});
},
openInfos() {
if (!this.info) {
@@ -100,22 +135,19 @@ export default {
},
watch:{
studien_semester(newValue){
this.$fhcApi.factory.addons.getLvMenu(this.lehrveranstaltung_id, newValue)
.then(res => {
this.menu = res.data;
})
.catch((error) => this.$fhcAlert.handleSystemError);
this.fetchMenu(this.lehrveranstaltung_id, newValue);
}
},
created(){
this.$fhcApi.factory.lehre.getStudentPruefungen(this.lehrveranstaltung_id)
.then(res => res.data)
.then(pruefungen =>{
this.pruefungenData = pruefungen;
});
},
mounted() {
this.$fhcApi.factory.addons.getLvMenu(this.lehrveranstaltung_id, this.studien_semester)
.then(res => {
this.menu = res.data;
})
.catch((error) => {
this.$fhcAlert.handleSystemError(error);
this.menu = [];
});
this.fetchMenu(this.lehrveranstaltung_id, this.studien_semester);
},
template: /*html*/`<div class="mylv-semester-studiengang-lv card">
<lv-uebersicht ref="lvUebersicht" :preselectedMenu="preselectedMenuItem" :event="{
@@ -125,45 +157,56 @@ export default {
stg_kurzbzlang:studien_semester,
}"/>
<div class="card-header">
<div class="p-2" :class="is_organisatorische_einheit?'':'card-header'">
<!-- {{module}} if the module of the lv is important then query the module from the api endpoint for LV-->
<h6 class="card-title">{{bezeichnung}}</h6>
<h6 class="fw-bold" v-if="is_organisatorische_einheit" >Organisatorische Einheit:</h6>
<h6 class="mb-0">{{bezeichnung}}</h6>
</div>
<div class="card-body " :style="bodyStyle">
<ul class="list-group border-top-0 border-bottom-0 rounded-0">
<template v-if="menu">
<div v-if="!emptyMenu" class="card-body " :style="bodyStyle">
<template v-if="menu">
<ul class="list-group border-top-0 border-bottom-0 rounded-0">
<li :type="menuItem.c4_link ? 'button' : null" v-for="menuItem in menu" class="list-group-item border-0 " >
<div class="d-flex flex-row" :data-bs-toggle="menuItem.c4_moodle_links?.length ? 'dropdown' : null">
<div class="mx-4">
<i :class="[menuItem.c4_icon2 ? menuItem.c4_icon2 : 'fa-solid fa-pen-to-square', !menuItem.c4_link ? 'unavailable' : null ]"></i>
</div>
<a class="text-decoration-none text-truncate" :id="'moodle_links_'+lehrveranstaltung_id" :class="{'link-dark':menuItem.c4_link, 'unavailable':!menuItem.c4_link, 'dropdown-toggle':menuItem.c4_moodle_links?.length }" :target="menuItem.c4_target" :href="c4_link(menuItem) ? c4_link(menuItem) : null">
{{menuItem.name}}
<a
class="text-decoration-none text-truncate"
:id="'moodle_links_'+lehrveranstaltung_id"
:class="{'link-dark':menuItem.c4_link, 'unavailable':!menuItem.c4_link, 'dropdown-toggle':menuItem.c4_moodle_links?.length }"
:target="menuItem.c4_target"
:href="c4_link(menuItem) ? c4_link(menuItem) : null">
{{menuItem.name}}
</a>
</div>
<ul v-if="menuItem.c4_moodle_links?.length" class="dropdown-menu p-0" :aria-labelledby="'moodle_links_'+lehrveranstaltung_id">
<li v-for="item in menuItem.c4_moodle_links"><a class="dropdown-item border-bottom" :href="item.url">{{item.lehrform}}</a></li>
</ul>
</li>
</template>
<template v-else>
<li class="text-center"><i class="fa-solid fa-spinner fa-pulse fa-3x"></i></li>
</template>
</ul>
</ul>
</template>
<template v-else>
<div class="text-center d-flex justify-content-center align-items-center h-100" >
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
</template>
</div>
<div class="card-footer">
<div v-if="!emptyMenu" class="card-footer">
<div class="row">
<a href="#" class="col-auto text-start text-decoration-none" @click.prevent="openPruefungen">
<i class="fa fa-check text-success" v-if="positiv"></i>
{{ grade || p.t('lehre/noGrades') }}
</a>
<!--
Not used anymore because the lehrveranstaltungs informationen is available as a menu point in the lehrveranstaltungs optionen
<div v-if="lvinfo" class="col text-end">
<a class="card-link" href="#" @click.prevent="openInfos">
<i class="fa fa-info-circle" aria-hidden="true"></i>
<!-- template for the LV if there are multiple pruefungen -->
<template v-if="LvHasPruefungenInformation">
<a href="#" class="col-auto text-start text-decoration-none" @click.prevent="openPruefungen">
<i class="fa fa-check text-success" v-if="positiv"></i>
<span class="ps-1" :style="'color:'+gradeColor">{{ grade || p.t('lehre/noGrades') }}</span>
</a>
</div>-->
</template>
<!-- template for the LV with no pruefungen -->
<template v-else>
<span class="col-auto text-start text-decoration-none" >
<i class="fa fa-check text-success" v-if="positiv"></i>
<span class="ps-1" :style="'color:'+gradeColor">{{ grade || p.t('lehre/noGrades') }}</span>
</span>
</template>
</div>
</div>
</div>`
@@ -10,7 +10,7 @@ export default {
BsModal
],
props: {
lehrveranstaltung_id: Number,
pruefungenData: Array|null,
bezeichnung: String,
/*
* NOTE(chris):
@@ -26,16 +26,7 @@ export default {
},
data: () => ({
result: true,
pruefungen: null
}),
created() {
if (pruefungen[this.lehrveranstaltung_id])
this.pruefungen = pruefungen[this.lehrveranstaltung_id];
else
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Pruefungen/' + this.lehrveranstaltung_id).then(res => {
this.pruefungen = pruefungen[this.lehrveranstaltung_id] = res.data.retval || [];
});
},
mounted() {
this.modal = this.$refs.modalContainer.modal;
},
@@ -47,10 +38,10 @@ export default {
Prüfungen: {{bezeichnung}}
</template>
<template v-slot:default>
<div v-if="!pruefungen" class="text-center">
<div v-if="!pruefungenData" class="text-center">
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>
<p v-else-if="!pruefungen.length" class="alert alert-info mb-0">
<p v-else-if="!pruefungenData.length" class="alert alert-info mb-0">
Keine Prüfungen vorhanden!
</p>
<table v-else class="table table-hover">
@@ -60,7 +51,7 @@ export default {
<td class="text-end">Note</td>
</thead>
<tbody>
<tr v-for="pruefung in pruefungen" :key="pruefung.pruefung_id">
<tr v-for="pruefung in pruefungenData" :key="pruefung.pruefung_id">
<th>{{pruefung.pruefungstyp_kurzbz}}</th>
<td>{{pruefung.datum}}</td>
<td class="text-end">{{pruefung.note}}</td>
@@ -1,4 +1,4 @@
import { CoreFilterCmpt } from "../../../components/filter/Filter.js";
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import EditProfil from "./ProfilModal/EditProfil.js";
import Adresse from "./ProfilComponents/Adresse.js";
import Kontakt from "./ProfilComponents/Kontakt.js";
@@ -11,475 +11,409 @@ import RoleInformation from "./ProfilComponents/RoleInformation.js";
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
export default {
components: {
CoreFilterCmpt,
EditProfil,
Adresse,
Kontakt,
FetchProfilUpdates,
AusweisStatus,
Mailverteiler,
QuickLinks,
ProfilEmails,
RoleInformation,
ProfilInformation,
},
components: {
CoreFilterCmpt,
EditProfil,
Adresse,
Kontakt,
FetchProfilUpdates,
AusweisStatus,
Mailverteiler,
QuickLinks,
ProfilEmails,
RoleInformation,
ProfilInformation,
},
inject: ["sortProfilUpdates", "collapseFunction"],
inject: ["sortProfilUpdates", "collapseFunction"],
data() {
return {
showModal: false,
editDataFilter: null,
data() {
return {
showModal: false,
editDataFilter: null,
// tabulator options
funktionen_table_options: {
height: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
data: [
{
Bezeichnung: "",
Organisationseinheit: "",
Gültig_von: "",
Gültig_bis: "",
Wochenstunden: "",
},
],
columns: [
{
title:
"<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
field: "collapse",
headerSort: false,
headerFilter: false,
formatter: "responsiveCollapse",
maxWidth: 40,
headerClick: this.collapseFunction,
},
{
title: "Bezeichnung",
field: "Bezeichnung",
headerFilter: true,
minWidth: 200,
},
{
title: "Organisationseinheit",
field: "Organisationseinheit",
headerFilter: true,
minWidth: 200,
},
{
title: "Gültig_von",
field: "Gültig_von",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Gültig_bis",
field: "Gültig_bis",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Wochenstunden",
field: "Wochenstunden",
headerFilter: true,
minWidth: 200,
},
],
},
// tabulator options
funktionen_table_options: {
height: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
data: [
{
Bezeichnung: "",
Organisationseinheit: "",
Gültig_von: "",
Gültig_bis: "",
Wochenstunden: "",
},
],
columns: [
{
title:
"<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
field: "collapse",
headerSort: false,
headerFilter: false,
formatter: "responsiveCollapse",
maxWidth: 40,
headerClick: this.collapseFunction,
},
{
title: "Bezeichnung",
field: "Bezeichnung",
headerFilter: true,
minWidth: 200,
},
{
title: "Organisationseinheit",
field: "Organisationseinheit",
headerFilter: true,
minWidth: 200,
},
{
title: "Gültig_von",
field: "Gültig_von",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Gültig_bis",
field: "Gültig_bis",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Wochenstunden",
field: "Wochenstunden",
headerFilter: true,
minWidth: 200,
},
],
},
betriebsmittel_table_options: {
height: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
data: [{ betriebsmittel: "", Nummer: "", Ausgegeben_am: "" }],
columns: [
{
title:
"<i id='collapseIconBetriebsmittel' role='button' class='fa-solid fa-angle-down '></i>",
field: "collapse",
headerSort: false,
headerFilter: false,
formatter: "responsiveCollapse",
maxWidth: 40,
headerClick: this.collapseFunction,
},
{
title: "Betriebsmittel",
field: "betriebsmittel",
headerFilter: true,
minWidth: 200,
},
{
title: "Nummer",
field: "Nummer",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Ausgegeben_am",
field: "Ausgegeben_am",
headerFilter: true,
minWidth: 200,
},
],
},
};
},
betriebsmittel_table_options: {
height: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
data: [{betriebsmittel: "", Nummer: "", Ausgegeben_am: ""}],
columns: [
{
title:
"<i id='collapseIconBetriebsmittel' role='button' class='fa-solid fa-angle-down '></i>",
field: "collapse",
headerSort: false,
headerFilter: false,
formatter: "responsiveCollapse",
maxWidth: 40,
headerClick: this.collapseFunction,
},
{
title: "Betriebsmittel",
field: "betriebsmittel",
headerFilter: true,
minWidth: 200,
},
{
title: "Nummer",
field: "Nummer",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Ausgegeben_am",
field: "Ausgegeben_am",
headerFilter: true,
minWidth: 200,
},
],
},
};
},
props: {
data: Object,
editData: Object,
},
props: {
data: Object,
editData: Object,
},
methods: {
betriebsmittelTableBuilt: function () {
this.$refs.betriebsmittelTable.tabulator.setData(this.data.mittel);
},
funktionenTableBuilt: function () {
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
},
hideEditProfilModal: function () {
//? checks the editModal component property result, if the user made a successful request or not
if (this.$refs.editModal.result) {
this.$fhcApi.factory.profilUpdate.selectProfilRequest()
.then((request) => {
if (!request.error && request) {
this.data.profilUpdates = request.data;
this.data.profilUpdates.sort(this.sortProfilUpdates);
} else {
console.error("Error when fetching profile updates: " + res.data);
}
})
.catch((err) => {
console.error(err);
});
} else {
// when modal was closed without submitting request
}
this.showModal = false;
this.editDataFilter = null;
},
methods: {
betriebsmittelTableBuilt: function () {
this.$refs.betriebsmittelTable.tabulator.setData(this.data.mittel);
},
funktionenTableBuilt: function () {
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
},
hideEditProfilModal: function () {
//? checks the editModal component property result, if the user made a successful request or not
if (this.$refs.editModal.result) {
this.$fhcApi.factory.profilUpdate.selectProfilRequest()
.then((request) => {
if (!request.error && request) {
this.data.profilUpdates = request.data;
this.data.profilUpdates.sort(this.sortProfilUpdates);
} else {
console.error("Error when fetching profile updates: " + res.data);
}
})
.catch((err) => {
console.error(err);
});
} else {
// when modal was closed without submitting request
}
this.showModal = false;
this.editDataFilter = null;
},
showEditProfilModal(view) {
if (view) {
this.editDataFilter = view;
}
showEditProfilModal(view) {
if (view) {
this.editDataFilter = view;
}
this.showModal = true;
Vue.nextTick(() => {
this.$refs.editModal.show();
});
this.showModal = true;
Vue.nextTick(() => {
this.$refs.editModal.show();
});
// after a state change, wait for the DOM updates to complete
},
// after a state change, wait for the DOM updates to complete
},
fetchProfilUpdates: function () {
this.$fhcApi.factory.profilUpdate.selectProfilRequest().then((res) => {
if (!res.error && res) {
this.data.profilUpdates = res.data?.length
? res.data.sort(this.sortProfilUpdates)
: null;
}
});
},
},
fetchProfilUpdates: function () {
this.$fhcApi.factory.profilUpdate.selectProfilRequest().then((res) => {
if (!res.error && res) {
this.data.profilUpdates = res.data?.length
? res.data.sort(this.sortProfilUpdates)
: null;
}
});
},
},
computed: {
editable() {
return this.data?.editAllowed ?? false;
},
filteredEditData() {
return this.editDataFilter
? this.editData.data[this.editDataFilter]
: this.editData;
},
profilInformation() {
if (!this.data) {
return {};
}
computed: {
editable() {
return this.data?.editAllowed ?? false;
},
filteredEditData() {
return this.editDataFilter
? this.editData.data[this.editDataFilter]
: this.editData;
},
profilInformation() {
if (!this.data) {
return {};
}
return {
Vorname: this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
Titel: this.data.titel,
Postnomen: this.data.postnomen,
foto_sperre: this.data.foto_sperre,
foto: this.data.foto,
};
},
return {
Vorname: this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
Titel: this.data.titel,
Postnomen: this.data.postnomen,
foto_sperre: this.data.foto_sperre,
foto: this.data.foto,
};
},
roleInformation() {
if (!this.data) {
return {};
}
roleInformation() {
if (!this.data) {
return {};
}
return {
Geburtsdatum: this.data.gebdatum,
Geburtsort: this.data.gebort,
Kurzzeichen: this.data.kurzbz,
Telefon:
(this.data.standort_telefon ? this.data.standort_telefon + " " + this.data.telefonklappe : this.data.telefonklappe),
Büro: this.data.ort_kurzbz,
};
},
},
return {
[`${this.$p.t('profil', 'Geburtsdatum')}`]: this.data.gebdatum,
[`${this.$p.t('profil', 'Geburtsort')}`]: this.data.gebort,
[`${this.$p.t('profil', 'Kurzzeichen')}`]: this.data.kurzbz,
[`${this.$p.t('profil', 'Telefon')}`]:
(this.data.standort_telefon ? this.data.standort_telefon + " " + this.data.telefonklappe : this.data.telefonklappe),
[`${this.$p.t('profil', 'Büro')}`]: this.data.ort_kurzbz,
};
},
},
created() {
//? sorts the profil Updates: pending -> accepted -> rejected
this.data.profilUpdates?.sort(this.sortProfilUpdates);
},
mounted(){
},
created() {
//? sorts the profil Updates: pending -> accepted -> rejected
this.data.profilUpdates?.sort(this.sortProfilUpdates);
template: /*html*/ `
<div class="container-fluid text-break fhc-form" >
},
mounted() {
},
template: /*html*/ `
<div class="container-fluid text-break fhc-form" >
<edit-profil v-if="showModal" ref="editModal" @hideBsModal="hideEditProfilModal" :value="JSON.parse(JSON.stringify(filteredEditData))" :title="$p.t('profil','profilBearbeiten')"></edit-profil>
<div class="row">
<div class="d-md-none col-12 ">
<div class="row mb-3">
<div class="row">
<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 -->
<div class="row mb-3 ">
<div class="col">
<!-- MOBILE QUICK LINKS -->
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true"></quick-links>
</div>
</div>
<!-- Bearbeiten Button -->
<div class="row mb-3 ">
<div class="col">
<button @click="()=>showEditProfilModal()" type="button" class="text-start w-100 btn btn-outline-secondary" >
<div class="row">
<div class="col-auto">
<i class="fa fa-edit"></i>
</div>
<div class="col-auto">{{$p.t('ui','bearbeiten')}}</div>
</div>
</button>
</div>
</div>
<div v-if="data.profilUpdates" class="row mb-3">
<div class="col">
<!-- MOBILE PROFIL UPDATES -->
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates" ></fetch-profil-updates>
</div>
</div>
</div>
<!-- END OF HIDDEN ROW (HIDDEN IN VIEWPORTS GREATER THEN-EQUAL MD) -->
<!-- MAIN PANNEL -->
<div class="col-sm-12 col-md-8 col-xxl-9 ">
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
<!-- INFORMATION CONTENT START -->
<!-- ROW WITH THE PROFIL INFORMATION -->
<div class="row mb-4">
<div class="col-lg-12 col-xl-6 ">
<div class="row mb-4">
<div class="col">
<!-- PROFIL INFORMATION -->
<profil-information @showEditProfilModal="showEditProfilModal" :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :editable="editable"></profil-information>
</div>
</div>
<div class="row mb-4">
<div class=" col-lg-12">
<!-- MITARBEITER INFO -->
<role-information :title="$p.t('profil','mitarbeiterInformation')" :data="roleInformation"></role-information>
</div>
</div>
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
</div>
<div class="col-xl-6 col-lg-12 ">
<div class="row mb-4">
<div class="col">
<!-- EMAILS -->
<profil-emails :title="this.$p.t('person','email')" :data="data.emails" ></profil-emails>
</div>
</div>
<div class="row mb-4 ">
<div class="col">
<!-- PRIVATE KONTAKTE-->
<div class="card">
<div class="card-header">
<div class="row">
<div @click="showEditProfilModal('Private_Kontakte')" class="col-auto" type="button">
<button @click="()=>showEditProfilModal()" type="button" class="text-start w-100 btn btn-outline-secondary" >
<div class="row">
<div class="col-auto">
<i class="fa fa-edit"></i>
</div>
<div class="col">
<span>{{$p.t('profil','privateKontakte')}}</span>
</div>
</div>
</div>
<div class="card-body ">
<div class="gy-3 row ">
<div v-for="element in data.kontakte" class="col-12">
<Kontakt :data="element"></Kontakt>
</div>
</div>
</div>
<div class="col-auto">{{$p.t('ui','bearbeiten')}}</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col">
<!-- PRIVATE ADRESSEN-->
<div class="card">
<div class="card-header">
<div class="row">
<div @click="showEditProfilModal('Private_Adressen')" class="col-auto" type="button">
<i class="fa fa-edit"></i>
</div>
<div class="col">
<span>{{$p.t('profil','privateAdressen')}}</span>
</div>
</div>
</div>
<div class="card-body">
<div class="gy-3 row ">
<div v-for="element in data.adressen" class="col-12">
<Adresse :data="element"></Adresse>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div >
<div class="row">
<div class="col-12 mb-4" >
<!-- FUNKTIONEN TABELLE -->
<core-filter-cmpt @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
</div>
<div class="col-12 mb-4" >
<!-- BETRIEBSMITTEL TABELLE -->
<core-filter-cmpt @tableBuilt="betriebsmittelTableBuilt" :title="$p.t('profil','entlehnteBetriebsmittel')" ref="betriebsmittelTable" :tabulator-options="betriebsmittel_table_options" tableOnly :sideMenu="false" />
</div>
</button>
</div>
</div>
<!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<div class="row d-none d-md-block mb-3">
</div>
<div v-if="data.profilUpdates" class="row mb-3">
<div class="col">
<!-- QUICK LINKS -->
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
</div>
<!-- MOBILE PROFIL UPDATES -->
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates" ></fetch-profil-updates>
</div>
<!-- Bearbeiten Button -->
<div class="row d-none d-md-block ">
<div class="col mb-3">
<button @click="()=>showEditProfilModal()" type="button" class="text-start w-100 btn btn-outline-secondary" >
<div class="row">
<div class="col-auto">
<i class="fa fa-edit"></i>
</div>
</div>
<!-- END OF HIDDEN ROW (HIDDEN IN VIEWPORTS GREATER THEN-EQUAL MD) -->
<!-- MAIN PANNEL -->
<div class="col-sm-12 col-md-8 col-xxl-9 ">
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
<!-- INFORMATION CONTENT START -->
<!-- ROW WITH THE PROFIL INFORMATION -->
<div class="row mb-4">
<div class="col-lg-12 col-xl-6 ">
<div class="row mb-4">
<div class="col">
<!-- PROFIL INFORMATION -->
<profil-information @showEditProfilModal="showEditProfilModal" :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :editable="editable"></profil-information>
</div>
</div>
<div class="col-auto">{{$p.t('ui','bearbeiten')}}</div>
</div>
</button>
<div class="row mb-4">
<div class=" col-lg-12">
<!-- MITARBEITER INFO -->
<role-information :title="$p.t('profil','mitarbeiterInformation')" :data="roleInformation"></role-information>
</div>
</div>
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
</div>
<div class="col-xl-6 col-lg-12 ">
<div class="row mb-4">
<div class="col">
<!-- EMAILS -->
<profil-emails :title="this.$p.t('person','email')" :data="data.emails" ></profil-emails>
</div>
</div>
<div class="row mb-4 ">
<div class="col">
<!-- PRIVATE KONTAKTE-->
<div class="card">
<div class="card-header">
<div class="row">
<div @click="showEditProfilModal('Private_Kontakte')" class="col-auto" type="button">
<i class="fa fa-edit"></i>
</div>
<div class="col">
<span>{{$p.t('profil','privateKontakte')}}</span>
</div>
</div>
</div>
<div class="card-body ">
<div class="gy-3 row ">
<div v-for="element in data.kontakte" class="col-12">
<Kontakt :data="element"></Kontakt>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col">
<!-- PRIVATE ADRESSEN-->
<div class="card">
<div class="card-header">
<div class="row">
<div @click="showEditProfilModal('Private_Adressen')" class="col-auto" type="button">
<i class="fa fa-edit"></i>
</div>
<div class="col">
<span>{{$p.t('profil','privateAdressen')}}</span>
</div>
</div>
</div>
<div class="card-body">
<div class="gy-3 row ">
<div v-for="element in data.adressen" class="col-12">
<Adresse :data="element"></Adresse>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="data.profilUpdates" class="row d-none d-md-block mb-3">
</div >
<div class="row">
<div class="col-12 mb-4" >
<!-- FUNKTIONEN TABELLE -->
<core-filter-cmpt @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
</div>
<div class="col-12 mb-4" >
<!-- BETRIEBSMITTEL TABELLE -->
<core-filter-cmpt @tableBuilt="betriebsmittelTableBuilt" :title="$p.t('profil','entlehnteBetriebsmittel')" ref="betriebsmittelTable" :tabulator-options="betriebsmittel_table_options" tableOnly :sideMenu="false" />
</div>
</div>
</div>
<!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!--TODO: uncomment when implemented
<div class="row d-none d-md-block mb-3">
<div class="col">
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
</div>
</div>-->
<!-- Bearbeiten Button -->
<div class="row d-none d-md-block ">
<div class="col mb-3">
<button @click="()=>showEditProfilModal()" type="button" class="text-start w-100 btn btn-outline-secondary" >
<div class="row">
<div class="col-auto">
<i class="fa fa-edit"></i>
</div>
<div class="col-auto">{{$p.t('ui','bearbeiten')}}</div>
</div>
</button>
</div>
</div>
<div v-if="data.profilUpdates" class="row d-none d-md-block mb-3">
<div class="col mb-3">
<!-- PROFIL UPDATES -->
<fetch-profil-updates v-if="data.profilUpdates && data.profilUpdates.length" @fetchUpdates="fetchProfilUpdates" :data="data.profilUpdates"></fetch-profil-updates>
</div>
</div>
<div class="row mb-3" >
</div>
<div class="row mb-3" >
<div class="col-12">
<!-- AUSWEIS STATUS -->
<ausweis-status :data="data.zutrittsdatum"></ausweis-status>
</div>
<!-- AUSWEIS STATUS -->
<ausweis-status :data="data.zutrittsdatum"></ausweis-status>
</div>
<div class="row">
<div class="col">
<!-- MAILVERTEILER -->
</div>
<div class="row">
<div class="col">
<!-- MAILVERTEILER -->
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`,
};
@@ -1,4 +1,4 @@
import { CoreFilterCmpt } from "../../../components/filter/Filter.js";
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
import Mailverteiler from "./ProfilComponents/Mailverteiler.js";
import QuickLinks from "./ProfilComponents/QuickLinks.js";
import RoleInformation from "./ProfilComponents/RoleInformation.js";
@@ -6,278 +6,219 @@ import ProfilEmails from "./ProfilComponents/ProfilEmails.js";
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
export default {
components: {
CoreFilterCmpt,
Mailverteiler,
QuickLinks,
RoleInformation,
ProfilEmails,
ProfilInformation,
},
inject: ["collapseFunction"],
data() {
return {
collapseIconFunktionen: true,
components: {
CoreFilterCmpt,
Mailverteiler,
QuickLinks,
RoleInformation,
ProfilEmails,
ProfilInformation,
},
inject: ["collapseFunction"],
data() {
return {
collapseIconFunktionen: true,
funktionen_table_options: {
height: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
data: [
{
Bezeichnung: "",
Organisationseinheit: "",
Gültig_von: "",
Gültig_bis: "",
Wochenstunden: "",
},
],
columns: [
//? option when wanting to hide the collapsed list
funktionen_table_options: {
height: 300,
layout: "fitColumns",
responsiveLayout: "collapse",
responsiveLayoutCollapseUseFormatters: false,
responsiveLayoutCollapseFormatter: Vue.$collapseFormatter,
data: [
{
Bezeichnung: "",
Organisationseinheit: "",
Gültig_von: "",
Gültig_bis: "",
Wochenstunden: "",
},
],
columns: [
//? option when wanting to hide the collapsed list
{
title:
"<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
field: "collapse",
headerSort: false,
headerFilter: false,
formatter: "responsiveCollapse",
maxWidth: 40,
headerClick: this.collapseFunction,
},
{
title: "Bezeichnung",
field: "Bezeichnung",
headerFilter: true,
minWidth: 200,
},
{
title: "Organisationseinheit",
field: "Organisationseinheit",
headerFilter: true,
minWidth: 200,
},
{
title: "Gültig_von",
field: "Gültig_von",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Gültig_bis",
field: "Gültig_bis",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Wochenstunden",
field: "Wochenstunden",
headerFilter: true,
minWidth: 200,
},
],
},
};
},
{
title:
"<i id='collapseIconFunktionen' role='button' class='fa-solid fa-angle-down '></i>",
field: "collapse",
headerSort: false,
headerFilter: false,
formatter: "responsiveCollapse",
maxWidth: 40,
headerClick: this.collapseFunction,
},
{
title: "Bezeichnung",
field: "Bezeichnung",
headerFilter: true,
minWidth: 200,
},
{
title: "Organisationseinheit",
field: "Organisationseinheit",
headerFilter: true,
minWidth: 200,
},
{
title: "Gültig_von",
field: "Gültig_von",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Gültig_bis",
field: "Gültig_bis",
headerFilter: true,
resizable: true,
minWidth: 200,
},
{
title: "Wochenstunden",
field: "Wochenstunden",
headerFilter: true,
minWidth: 200,
},
],
},
};
},
//? this is the prop passed to the dynamic component with the custom data of the view
props: ["data"],
methods: {
funktionenTableBuilt: function () {
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
},
},
//? this is the prop passed to the dynamic component with the custom data of the view
props: ["data"],
methods: {
funktionenTableBuilt: function () {
this.$refs.funktionenTable.tabulator.setData(this.data.funktionen);
},
},
computed: {
editable() {
return this.data?.editAllowed ?? false;
},
personEmails() {
return this.data?.emails ? this.data.emails : [];
},
computed: {
editable() {
return this.data?.editAllowed ?? false;
},
profilInformation() {
if (!this.data) {
return {};
}
personEmails() {
return this.data?.emails ? this.data.emails : [];
},
return {
Vorname: this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
Titel: this.data.titel,
Postnomen: this.data.postnomen,
foto_sperre: this.data.foto_sperre,
foto: this.data.foto,
};
},
profilInformation() {
if (!this.data) {
return {};
}
roleInformation() {
if (!this.data) {
return {};
}
return {
Vorname: this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
Titel: this.data.titel,
Postnomen: this.data.postnomen,
foto_sperre: this.data.foto_sperre,
foto: this.data.foto,
};
},
return {
Geburtsdatum: this.data.gebdatum,
Geburtsort: this.data.gebort,
Kurzzeichen: this.data.kurzbz,
Telefon:
(this.data.standort_telefon ? this.data.standort_telefon + " " + this.data.telefonklappe : this.data.telefonklappe),
Büro: this.data.ort_kurzbz,
};
},
},
roleInformation() {
if (!this.data) {
return {};
}
template: /*html*/ `
return {
[`${this.$p.t('profil', 'Geburtsdatum')}`]: this.data.gebdatum,
[`${this.$p.t('profil', 'Geburtsort')}`]: this.data.gebort,
[`${this.$p.t('profil', 'Kurzzeichen')}`]: this.data.kurzbz,
[`${this.$p.t('profil', 'Telefon')}`]:
(this.data.standort_telefon ? this.data.standort_telefon + " " + this.data.telefonklappe : this.data.telefonklappe),
[`${this.$p.t('profil', 'Büro')}`]: this.data.ort_kurzbz,
};
},
},
<div class="container-fluid text-break fhc-form" >
<!-- ROW -->
<div class="row">
<!-- HIDDEN QUICK LINKS -->
<div class="d-md-none col-12 ">
<quick-links :title="$p.t('profil','quickLinks')" :mobile="true" ></quick-links>
template: /*html*/ `
</div>
<!-- END OF HIDDEN QUCK LINKS -->
<!-- MAIN PANNEL -->
<div class="col-sm-12 col-md-8 col-xxl-9 ">
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
<!-- INFORMATION CONTENT START -->
<!-- ROW WITH THE PROFIL INFORMATION -->
<div class="container-fluid text-break fhc-form" >
<!-- ROW -->
<div class="row">
<!-- HIDDEN QUICK LINKS -->
<!-- TODO: 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 -->
<div class="col-sm-12 col-md-8 col-xxl-9 ">
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
<!-- INFORMATION CONTENT START -->
<!-- ROW WITH THE PROFIL INFORMATION -->
<div class="row mb-4">
<!-- FIRST KAESTCHEN -->
<div class="col-lg-12 col-xl-6 ">
<div class="row mb-4">
<!-- FIRST KAESTCHEN -->
<div class="col-lg-12 col-xl-6 ">
<div class="row mb-4">
<div class="col">
<!-- Profil Informationen -->
<profil-information :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :editable="editable"></profil-information>
</div>
<div class="col">
<!-- Profil Informationen -->
<profil-information :title="$p.t('profil','mitarbeiterIn')" :data="profilInformation" :editable="editable"></profil-information>
</div>
</div>
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
<!-- END OF PROFIL INFORMATION ROW -->
<!-- INFORMATION CONTENT END -->
</div>
<div class="col-xl-6 col-lg-12 ">
</div>
<div class="col-xl-6 col-lg-12 ">
<div class="row mb-4">
<div class="col">
<!-- EMAILS -->
<profil-emails :title="this.$p.t('person','email')" :data="personEmails"></profil-emails>
</div></div>
<div class="col">
<!-- EMAILS -->
<profil-emails :title="this.$p.t('person','email')" :data="personEmails"></profil-emails>
</div>
</div>
<!-- SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
<div class="row mb-4">
<div class=" col-lg-12">
<!-- roleInformation -->
<role-information :data="roleInformation" :title="$p.t('profil','mitarbeiterInformation')"></role-information>
</div>
</div>
<!-- END OF SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
<!-- END OF THE SECOND INFORMATION COLUMN -->
<div class=" col-lg-12">
<!-- roleInformation -->
<role-information :data="roleInformation" :title="$p.t('profil','mitarbeiterInformation')"></role-information>
</div>
</div>
<!-- START OF THE SECOND PROFIL INFORMATION ROW -->
<!-- ROW WITH PROFIL IMAGE AND INFORMATION END -->
</div >
<!-- SECOND ROW UNDER THE PROFIL IMAGE AND INFORMATION WITH THE TABLES -->
<div class="row">
<!-- FIRST TABLE -->
<div class="col-12 mb-4" >
<core-filter-cmpt @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
</div>
<!-- END OF THE ROW WITH THE TABLES UNDER THE PROFIL INFORMATION -->
<!-- END OF SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
<!-- END OF THE SECOND INFORMATION COLUMN -->
</div>
<!-- END OF MAIN CONTENT COL -->
</div>
<!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!-- VISIBLE UNTIL VIEWPORT MD -->
<!-- START OF THE SECOND PROFIL INFORMATION ROW -->
<!-- ROW WITH PROFIL IMAGE AND INFORMATION END -->
</div >
<!-- SECOND ROW UNDER THE PROFIL IMAGE AND INFORMATION WITH THE TABLES -->
<div class="row">
<!-- FIRST TABLE -->
<div class="col-12 mb-4" >
<core-filter-cmpt @tableBuilt="funktionenTableBuilt" :title="$p.t('person','funktionen')" ref="funktionenTable" :tabulator-options="funktionen_table_options" tableOnly :sideMenu="false" />
</div>
<!-- END OF THE ROW WITH THE TABLES UNDER THE PROFIL INFORMATION -->
</div>
<!-- END OF MAIN CONTENT COL -->
</div>
<!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!-- VISIBLE UNTIL VIEWPORT MD -->
<!--TODO: uncomment when implemented
<div class="row d-none d-md-block mb-3">
<div class="col">
<!-- QUICKLINKS -->
<quick-links :title="$p.t('profil','quickLinks')" ></quick-links>
</div>
</div>
<div class="row">
<div class="col">
<!-- MAILVERTEILER -->
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
<quick-links :title="$p.t('profil','quickLinks')" ></quick-links>
</div>
</div>
<!-- END OF SIDE PANEL -->
</div>
<!-- END OF CONTAINER ROW-->
</div>
<!-- END OF CONTAINER -->
</div>
`,
-->
<div class="row">
<div class="col">
<!-- MAILVERTEILER -->
<mailverteiler :data="data?.mailverteiler" :title="$p.t('profil','mailverteiler')"></mailverteiler>
</div>
</div>
<!-- END OF SIDE PANEL -->
</div>
<!-- END OF CONTAINER ROW-->
</div>
<!-- END OF CONTAINER -->
</div>
`,
};
@@ -1,11 +1,8 @@
import StudentProfil from "../../components/Cis/Profil/StudentProfil.js";
import MitarbeiterProfil from "../../components/Cis/Profil/MitarbeiterProfil.js";
import ViewStudentProfil from "../../components/Cis/Profil/StudentViewProfil.js";
import ViewMitarbeiterProfil from "../../components/Cis/Profil/MitarbeiterViewProfil.js";
import Loading from "../../components/Loader.js";
import Phrasen from "../../plugin/Phrasen.js";
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers";
import StudentProfil from "./StudentProfil.js";
import MitarbeiterProfil from "./MitarbeiterProfil.js";
import ViewStudentProfil from "./StudentViewProfil.js";
import ViewMitarbeiterProfil from "./MitarbeiterViewProfil.js";
import Loading from "../../Loader.js";
Vue.$collapseFormatter = function (data) {
//data - an array of objects containing the column title and value for each cell
@@ -34,8 +31,8 @@ Vue.$collapseFormatter = function (data) {
return Object.keys(data).length ? container : "";
};
const profilApp = Vue.createApp({
name: 'ProfilApp',
export const Profil = {
name: 'Profil',
components: {
StudentProfil,
MitarbeiterProfil,
@@ -43,7 +40,12 @@ const profilApp = Vue.createApp({
ViewMitarbeiterProfil,
Loading,
},
props: {
uid: {
type: String,
default: 'Profil'
}
},
data() {
return {
//? loading property is used for showing/hiding the loading modal
@@ -56,8 +58,6 @@ const profilApp = Vue.createApp({
notFoundUID: null,
};
},
//? use function syntax for provide so that we can access `this`
provide() {
return {
profilUpdateStates: Vue.computed(() =>
@@ -129,8 +129,36 @@ const profilApp = Vue.createApp({
},
};
},
methods: {
async load() {
// fetch profilUpdateStates to provide them to children components
await this.$fhcApi.factory.profilUpdate.getStatus()
.then((response) => {
this.profilUpdateStates = response.data;
})
.catch((error) => {
console.error(error);
});
this.$fhcApi.factory.profilUpdate.getTopic()
.then((response) => {
this.profilUpdateTopic = response.data;
})
.catch((error) => {
console.error(error);
});
let uid = this.uid ?? location.pathname.split("/").pop();
this.$fhcApi.factory.profil.getView(uid).then((res) => {
if (!res.data) {
this.notFoundUID = uid;
} else {
this.view = res.data?.view;
this.data = res.data?.data;
}
});
},
zustellAdressenCount() {
if (!this.data || !this.data.adressen) {
return null;
@@ -172,22 +200,13 @@ const profilApp = Vue.createApp({
}
return [...new Set(adressenArray)];
// OLD version
// return this.data.adressen.filter(adresse => {
// return adresse.zustelladresse;
// }).map(adr => {
// return adr.adresse_id;
// });
},
zustellKontakteCount() {
if (!this.data || !this.data.kontakte) {
return null;
}
//TODO: not sure if how it is implemented is the best way
let kontakteArray = [];
if (this.data.profilUpdates?.length) {
@@ -225,16 +244,8 @@ const profilApp = Vue.createApp({
}
return [...new Set(kontakteArray)];
// OLD version
// return this.data.kontakte.filter(kontakt => {
// return kontakt.zustellung;
// }).map(kon =>{
// return kon.kontakt_id;
// });
},
},
computed: {
filteredEditData() {
if (!this.data) {
@@ -331,60 +342,26 @@ const profilApp = Vue.createApp({
};
}),
},
// Password_Change : {
// title:"Passwort Ändern",
// view:"ChangePassword",
// data:{
// test:"testvalue",
// }
// },
},
};
},
},
watch: {
loading: function (newValue, oldValue) {
loading: function (newValue) {
if (newValue) {
this.$refs.loadingModalRef.show();
} else {
this.$refs.loadingModalRef.hide();
}
},
uid (newVal, oldVal) {
console.log('watch uid', newVal)
this.load()
}
},
async created() {
// fetch profilUpdateStates to provide them to children components
await this.$fhcApi.factory.profilUpdate.getStatus()
.then((response) => {
this.profilUpdateStates = response.data;
})
.catch((error) => {
console.error(error);
});
this.$fhcApi.factory.profilUpdate.getTopic()
.then((response) => {
this.profilUpdateTopic = response.data;
})
.catch((error) => {
console.error(error);
});
//? uid contains the last part of the uri
let uid = location.pathname.split("/").pop();
this.$fhcApi.factory.profil.getView(uid).then((res) => {
if (!res.data) {
this.notFoundUID = uid;
} else {
this.view = res.data?.view;
this.data = res.data?.data;
}
});
this.load()
},
template: `
<div>
<div v-if="notFoundUID">
@@ -395,9 +372,6 @@ const profilApp = Vue.createApp({
<component :is="view" :data="data" :editData="filteredEditData" ></component>
</div>
</div>`,
});
setScrollbarWidth();
profilApp
.use(primevue.config.default, {zIndex: {overlay: 9999}})
.use(Phrasen)
.mount("#content");
}
export default Profil
@@ -19,7 +19,7 @@ export default{
<div :class="{...(data.anmerkung? {'col-11':true, 'col-md-6':true, 'col-xl-11':true, 'col-xxl-6':true} : {'col-10':true, 'col-xl-9':true, 'col-xxl-10':true})}">
<!-- rendering KONTAKT emails -->
<div class="form-underline ">
<div class="form-underline-titel">{{data.kontakttyp}}</div>
<div class="form-underline-titel">{{$p.t('profil',data.kontakttyp.toUpperCase())}}</div>
<a v-if="data.kontakt.includes('@')" role="link" :aria-disabled="view?true:false" :href="!view?('mailto:'+data.kontakt):null" class="form-underline-content">{{data.kontakt}} </a>
<a v-else role="link" :aria-disabled="view?true:false" :href="!view?('tel:'+data.kontakt):null" class="form-underline-content">{{data.kontakt}} </a>
</div>
@@ -172,13 +172,13 @@ export default {
}
return {
Geburtsdatum: this.data.gebdatum,
Geburtsort: this.data.gebort,
Personenkennzeichen: this.data.personenkennzeichen,
Studiengang: this.data.studiengang,
Semester: this.data.semester,
Verband: this.data.verband,
Gruppe: this.data.gruppe.trim(),
[`${this.$p.t('profil','Geburtsdatum')}`]: this.data.gebdatum,
[`${this.$p.t('profil', 'Geburtsort')}`]: this.data.gebort,
[`${this.$p.t('person', 'personenkennzeichen')}`]: this.data.personenkennzeichen,
[`${this.$p.t('lehre', 'studiengang')}`]: this.data.studiengang,
[`${this.$p.t('lehre', 'semester')}`]: this.data.semester,
[`${this.$p.t('lehre', 'lehrverband')}`]: this.data.verband,
[`${this.$p.t('lehre', 'gruppe')}`]: this.data.gruppe.trim(),
};
},
},
@@ -194,11 +194,12 @@ export default {
<div class="row">
<!-- HIDDEN QUICK LINKS -->
<div class="d-md-none col-12 ">
<div class="row py-2">
<!--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>
</div>-->
<!-- Bearbeiten Button -->
<div v-if="editable" class="row ">
@@ -314,12 +315,12 @@ export default {
</div>
<!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!--TODO: uncomment when implemented
<div class="row d-none d-md-block mb-3">
<div class="col">
<!-- QUICK LINKS -->
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
</div>
</div>
</div>-->
<!-- Bearbeiten Button -->
<div class="row d-none d-md-block">
<div class="col mb-3">
@@ -5,182 +5,142 @@ import RoleInformation from "./ProfilComponents/RoleInformation.js";
import ProfilInformation from "./ProfilComponents/ProfilInformation.js";
export default {
data() {
return {};
},
components: {
QuickLinks,
Mailverteiler,
ProfilEmails,
RoleInformation,
ProfilInformation,
},
data() {
return {};
},
components: {
QuickLinks,
Mailverteiler,
ProfilEmails,
RoleInformation,
ProfilInformation,
},
props: ["data"],
methods: {},
props: ["data"],
methods: {},
computed: {
editable() {
return this.data?.editAllowed ?? false;
},
profilInformation() {
if (!this.data) {
return {};
}
computed: {
editable() {
return this.data?.editAllowed ?? false;
},
profilInformation() {
if (!this.data) {
return {};
}
return {
Vorname: this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
Titel: this.data.titel,
Postnomen: this.data.postnomen,
foto_sperre: this.data.foto_sperre,
foto: this.data.foto,
};
},
return {
Vorname: this.data.vorname,
Nachname: this.data.nachname,
Username: this.data.username,
Anrede: this.data.anrede,
Titel: this.data.titel,
Postnomen: this.data.postnomen,
foto_sperre: this.data.foto_sperre,
foto: this.data.foto,
};
},
personEmails() {
return this.data?.emails ? this.data.emails : [];
},
personEmails() {
return this.data?.emails ? this.data.emails : [];
},
roleInformation() {
if (!this.data) {
return {};
}
roleInformation() {
if (!this.data) {
return {};
}
return {
Geburtsdatum: this.data.gebdatum,
Geburtsort: this.data.gebort,
Personenkennzeichen: this.data.personenkennzeichen,
Studiengang: this.data.studiengang,
Semester: this.data.semester,
Verband: this.data.verband,
Gruppe: this.data.gruppe.trim(),
};
},
},
return {
Geburtsdatum: this.data.gebdatum,
Geburtsort: this.data.gebort,
Personenkennzeichen: this.data.personenkennzeichen,
Studiengang: this.data.studiengang,
Semester: this.data.semester,
Verband: this.data.verband,
Gruppe: this.data.gruppe.trim(),
};
},
},
mounted() {},
mounted() {
},
template: /*html*/ `
template: /*html*/ `
<div class="container-fluid text-break fhc-form" >
<div class="container-fluid text-break fhc-form" >
<!-- ROW -->
<div class="row">
<!-- HIDDEN QUICK LINKS -->
<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 -->
<div class="col-sm-12 col-md-8 col-xxl-9 ">
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
<!-- INFORMATION CONTENT START -->
<!-- ROW WITH THE PROFIL INFORMATION -->
<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 -->
<div class="col-sm-12 col-md-8 col-xxl-9 ">
<!-- ROW WITH PROFIL IMAGE AND INFORMATION -->
<!-- INFORMATION CONTENT START -->
<!-- ROW WITH THE PROFIL INFORMATION -->
<div class="row mb-4">
<!-- FIRST KAESTCHEN -->
<div class="col-lg-12 col-xl-6 ">
<div class="row mb-4">
<!-- FIRST KAESTCHEN -->
<div class="col-lg-12 col-xl-6 ">
<div class="row mb-4">
<div class="col">
<profil-information :data="profilInformation" :title="$p.t('profil','studentIn')" :editable="editable"></profil-information>
</div>
<div class="col">
<profil-information :data="profilInformation" :title="$p.t('profil','studentIn')" :editable="editable"></profil-information>
</div>
</div>
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
<!-- START OF SECOND PROFIL INFORMATION COLUMN -->
<!-- END OF PROFIL INFORMATION ROW -->
<!-- INFORMATION CONTENT END -->
</div>
<div class="col-xl-6 col-lg-12 ">
</div>
<div class="col-xl-6 col-lg-12 ">
<div class="row mb-4">
<div class="col">
<!-- EMAILS -->
<profil-emails :title="this.$p.t('person','email')" :data="personEmails"></profil-emails>
</div></div>
<!-- SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
<div class=" row mb-4">
<div class=" col-lg-12">
<role-information :title="$p.t('profil','studentInformation')" :data="roleInformation"></role-information>
</div>
</div>
<!-- END OF SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
<!-- END OF THE SECOND INFORMATION COLUMN -->
<div class="col">
<!-- EMAILS -->
<profil-emails :title="this.$p.t('person','email')" :data="personEmails"></profil-emails>
</div>
</div>
<!-- START OF THE SECOND PROFIL INFORMATION ROW -->
<!-- ROW WITH PROFIL IMAGE AND INFORMATION END -->
</div >
<!-- END OF MAIN CONTENT COL -->
</div>
<!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!-- SRART OF QUICK LINKS IN THE SIDE PANEL -->
<!-- START OF THE FIRDT ROW IN THE SIDE PANEL -->
<!-- THESE QUCK LINKS ARE ONLY VISIBLE UNTIL VIEWPORT MD -->
<!-- SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
<div class=" row mb-4">
<div class=" col-lg-12">
<role-information :title="$p.t('profil','studentInformation')" :data="roleInformation"></role-information>
</div>
</div>
<!-- END OF SECOND ROW OF SECOND COLUMN IN MAIN CONTENT -->
<!-- END OF THE SECOND INFORMATION COLUMN -->
</div>
<!-- START OF THE SECOND PROFIL INFORMATION ROW -->
<!-- ROW WITH PROFIL IMAGE AND INFORMATION END -->
</div >
<!-- END OF MAIN CONTENT COL -->
</div>
<!-- START OF SIDE PANEL -->
<div class="col-md-4 col-xxl-3 col-sm-12 text-break" >
<!-- SRART OF QUICK LINKS IN THE SIDE PANEL -->
<!-- START OF THE FIRDT ROW IN THE SIDE PANEL -->
<!-- THESE QUCK LINKS ARE ONLY VISIBLE UNTIL VIEWPORT MD -->
<!--TODO: uncomment when implemented
<div class="row d-none d-md-block mb-3">
<div class="col">
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
</div>
</div>
<!-- START OF THE SECOND ROW IN THE SIDE PANEL -->
<div class="row">
<div class="col">
<quick-links :title="$p.t('profil','quickLinks')"></quick-links>
<div class="col">
<!-- HIER SIND DIE MAILVERTEILER -->
</div>
</div>-->
<!-- START OF THE SECOND ROW IN THE SIDE PANEL -->
<div class="row">
<div class="col">
<!-- HIER SIND DIE MAILVERTEILER -->
<mailverteiler :title="$p.t('profil','mailverteiler')" :data="data?.mailverteiler"></mailverteiler>
</div>
<!-- END OF THE SECOND ROW IN THE SIDE PANEL -->
</div>
<!-- END OF SIDE PANEL -->
</div>
<!-- END OF CONTAINER ROW-->
</div>
<!-- END OF CONTAINER -->
</div>
<!-- END OF THE SECOND ROW IN THE SIDE PANEL -->
</div>
<!-- END OF SIDE PANEL -->
</div>
<!-- END OF CONTAINER ROW-->
</div>
<!-- END OF CONTAINER -->
</div>
`,
};
+10 -13
View File
@@ -1,30 +1,27 @@
export default {
data(){
return {
allActiveLanguages: null,
allActiveLanguages: FHC_JS_DATA_STORAGE_OBJECT.server_languages,
}
},
emits: ['languageChanged'],
methods:{
changeLanguage: function(lang){
if(this.allActiveLanguages.some(l => l === lang))
if(this.allActiveLanguages.some(l => l.sprache === lang))
{
this.$p.setLanguage(lang, this.$fhcApi);
this.$p.setLanguage(lang, this.$fhcApi)
.then(res => res.data)
.then(data =>
{
this.$emit('languageChanged', lang);
})
}
},
},
mounted(){
this.$fhcApi.factory.phrasen.getActiveDbLanguages()
.then(res => res.data)
.then(
(langs) => {
this.allActiveLanguages = langs;
}
);
},
template:/*html*/`
<div class="container">
<div class="row justify-content-center align-items-center flex-nowrap overflow-hidden">
<button v-for="lang in allActiveLanguages" @click.prevent="changeLanguage(lang)" class="col text-white fhc-entry btn text-center w-100" :selected="$p.user_language.value==lang?'':null">{{lang}}</button>
<button v-for="lang in allActiveLanguages" @click.prevent="changeLanguage(lang.sprache)" class="col text-white fhc-entry btn text-center w-100" :selected="$p.user_language.value==lang.sprache?'':null">{{lang.bezeichnung}}</button>
</div>
</div>
`,
@@ -1,27 +1,33 @@
import FhcCalendar from "../../components/Calendar/Calendar.js";
import Phrasen from "../../plugin/Phrasen.js";
import CalendarDate from "../../composables/CalendarDate.js";
import LvModal from "../../components/Cis/Mylv/LvModal.js";
import LvInfo from "../../components/Cis/Mylv/LvInfo.js"
import LvMenu from "../../components/Cis/Mylv/LvMenu.js"
import FhcCalendar from "../../Calendar/Calendar.js";
import CalendarDate from "../../../composables/CalendarDate.js";
import LvModal from "../Mylv/LvModal.js";
import LvInfo from "../Mylv/LvInfo.js"
import LvMenu from "../Mylv/LvMenu.js"
const app = Vue.createApp({
name: 'StundenplanApp',
export const Stundenplan = {
name: 'Stundenplan',
data() {
return {
lv_id: null,
events: null,
calendarDate: new CalendarDate(new Date()),
currentlySelectedEvent: null,
currentDay: new Date(),
minimized: false,
viewData: JSON.parse(this.viewDataString ?? '{}'),
}
},
props: [
"viewDataString"
],
components: {
FhcCalendar, LvModal, LvMenu, LvInfo
},
computed:{
lv_id() { // computed so we can theoretically change path/lva selection and reload without page refresh
const pathParts = window.location.pathname.split('/').filter(Boolean);
const id = pathParts[pathParts.length - 1];
return id && !isNaN(Number(id)) ? id : null; // only return id if it is a number string since the path might contain invalid elements
},
weekFirstDay: function () {
return this.calendarDateToString(this.calendarDate.cdFirstDayOfWeek);
},
@@ -34,15 +40,20 @@ const app = Vue.createApp({
monthLastDay: function () {
return this.calendarDateToString(this.calendarDate.cdLastDayOfCalendarMonth);
},
},
methods:{
convertTime: function([hour,minute]){
let date = new Date();
date.setHours(hour);
date.setMinutes(minute);
// returns date string as hh:mm
return date.toLocaleTimeString(this.$p.user_locale, { hour: '2-digit', minute: '2-digit', hour12:false});
},
setSelectedEvent: function (event) {
this.currentlySelectedEvent = event;
},
getLvID: function () {
this.lv_id = window.location.pathname
},
selectDay: function(day){
this.currentDay = day;
},
@@ -76,7 +87,7 @@ const app = Vue.createApp({
},
loadEvents: function(){
Promise.allSettled([
this.$fhcApi.factory.stundenplan.getStundenplan(this.monthFirstDay, this.monthLastDay),
this.$fhcApi.factory.stundenplan.getStundenplan(this.monthFirstDay, this.monthLastDay, this.viewData.lv_id),
this.$fhcApi.factory.stundenplan.getStundenplanReservierungen(this.monthFirstDay, this.monthLastDay)
]).then((result) => {
let promise_events = [];
@@ -110,7 +121,6 @@ const app = Vue.createApp({
created()
{
this.loadEvents();
this.getLvID();
},
template:/*html*/`
<h2>{{$p.t('lehre/stundenplan')}}</h2>
@@ -123,14 +133,31 @@ const app = Vue.createApp({
</span>
</template>
<template #weekPage="{event,day,isSelected}">
<div @click="showModal(event?.orig)" type="button" :class="{'selectedEvent':isSelected}" class="fhc-entry border border-secondary border d-flex flex-column align-items-center justify-content-evenly h-100">
<span>{{event?.orig.topic}}</span>
<span v-for="lektor in event?.orig.lektor">{{lektor.kurzbz}}</span>
<span>{{event?.orig.ort_kurzbz}}</span>
<div @click="showModal(event?.orig); console.log(event.orig)" type="button" :class="{'selectedEvent':isSelected}"
class="fhc-entry position-relative border border-secondary border d-flex flex-row align-items-center
justify-content-evenly h-100" style="max-height: 75px; overflow: auto;">
<div v-if="event?.orig?.beginn && event?.orig?.ende" >
<div class="d-flex flex-column p-4 border-end border-secondary">
<span class="small">{{convertTime(event.orig.beginn.split(":"))}}</span>
<span class="small">{{convertTime(event.orig.ende.split(":"))}}</span>
</div>
</div>
<div class="d-flex flex-column flex-grow-1 align-items-center">
<span>{{event?.orig.topic}}</span>
<span v-for="lektor in event?.orig.lektor">{{lektor.kurzbz}}</span>
<span>{{event?.orig.ort_kurzbz}}</span>
</div>
</div>
</template>
<template #dayPage="{event,day,mobile}">
<div @click="mobile? showModal(event?.orig):null" type="button" class="fhc-entry border border-secondary border row m-0 h-100 justify-content-center align-items-center text-center">
<div class="col-auto" v-if="event?.orig?.beginn && event?.orig?.ende" >
<div class="d-flex flex-column p-4 border-end border-secondary">
<span class="small">{{convertTime(event.orig.beginn.split(":"))}}</span>
<span class="small">{{convertTime(event.orig.ende.split(":"))}}</span>
</div>
</div>
<div class="col ">
<p>Lehrveranstaltung:</p>
<p class="m-0">{{event?.orig.topic}}</p>
@@ -158,6 +185,6 @@ const app = Vue.createApp({
</template>
</fhc-calendar>
`
});
app.use(Phrasen, {reload: true});
app.mount('#content');
}
export default Stundenplan
+24 -8
View File
@@ -7,22 +7,32 @@ export default {
DashboardSection,
DashboardWidgetPicker
},
props: [
"dashboard",
"viewDataString"
],
props: {
dashboard: {
type: String,
required: true,
default: 'CIS'
},
viewData: {
type: Object,
required: true,
default: () => ({name: '', uid: ''}),
validator(value) {
return value && value.name && value.uid
}
}
},
data() {
return {
sections: [],
widgets: null,
viewData: JSON.parse(this.viewDataString),
editMode: false
editMode: false,
viewDataInternal: this.viewData
}
},
provide() {
return {
editMode: Vue.computed(()=>this.editMode),
viewData: Vue.computed(()=>Vue.reactive(this.viewData)),
}
},
computed: {
@@ -151,10 +161,16 @@ export default {
}
}).catch(err => console.error('ERROR:', err));
},
async beforeMount() {
if(!this.viewData.name || !this.viewData.uid) {
const res = await this.$fhcApi.factory.dashboard.getViewData()
this.viewDataInternal = res.data
}
},
template: `
<div class="core-dashboard">
<h3>
{{ $p.t('global/personalGreeting', [ viewData?.name ]) }}
{{ $p.t('global/personalGreeting', [ viewDataInternal?.name ]) }}
<button style="margin-left: 8px;" class="btn" @click="editMode = !editMode"><i class="fa-solid fa-gear"></i></button>
</h3>
<dashboard-section v-for="(section, index) in sections" :key="section.name" :seperator="index" :name="section.name" :widgets="section.widgets" @widgetAdd="widgetAdd" @widgetUpdate="widgetUpdate" @widgetRemove="widgetRemove"></dashboard-section>
+99 -87
View File
@@ -11,10 +11,14 @@ export default {
data: () => ({
allNewsList: [],
singleNews: {},
selected: null
selected: null,
size:0,
}),
mixins: [AbstractWidget],
computed: {
sizeClass() {
return 'fhc-news-' + ['xs', 'sm', 'md', 'lg'][this.size];
},
getNewsWidgetStyle() {
return this.width == 1 ? "padding: 1rem 1rem;" : "padding: 0px;"
},
@@ -30,41 +34,29 @@ export default {
return this.allNewsList.slice(0, quantity);
},
placeHolderImgURL: function () {
return (
FHC_JS_DATA_STORAGE_OBJECT.app_root +
"skin/images/fh_technikum_wien_illustration_klein.png"
);
},
activeNews() {
return this.allNewsList.find(news => news.minimized === false) ?? this.allNewsList[0] ?? null
carouselItems() {
return this.allNewsList.reduce((acc, cur) => {
const el = document.getElementById('card-'+cur.news_id)
acc.push(el);
return acc
}, [])
}
},
created() {
this.$fhcApi.factory.cms
.news(MAX_LOADED_NEWS)
.then((res) => {
this.allNewsList = Array.from(Object.values(res.data));
this.selected = this.allNewsList.length ? this.allNewsList[0] : null
})
.catch((err) => {
console.error("ERROR: ", err.response.data);
});
this.$emit("setConfig", false);
},
methods: {
isString(value){
return Object.prototype.toString.call(value) === '[object String]';
},
setNext(){
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id)
const nextIndex = thisIndex == (this.allNewsList.length - 1) ? 0 : thisIndex + 1
this.setSelected(this.allNewsList[nextIndex])
this.setSelected(this.allNewsList[nextIndex])
},
setPrev() {
const thisIndex = this.allNewsList.findIndex(n=>n.news_id == this.selected.news_id)
const prevIndex = thisIndex ? thisIndex - 1 : this.allNewsList.length - 1
this.setSelected(this.allNewsList[prevIndex], 'prev')
this.setSelected(this.allNewsList[prevIndex])
},
getMenuItemClass(news) {
let classString = ''
@@ -73,45 +65,23 @@ export default {
}
return classString
},
getDynClassCarouselItem(news, index) {
// sets classes prev/active/next for bootstrap carousel
let classString = ''
async setSelected(news) {
let clickedElement = document.getElementById('card-'+news.news_id);
let clickedElementIndex = this.allNewsList.indexOf(news);
let oldElementIndex = this.allNewsList.indexOf(this.selected);
// return active class to news === selected OR very first news
if((this.selected.news_id === news.news_id) || (this.selected === null && index === 0)) {
classString = 'active';
} else { // set prev/next class for news
const selectedIndex = this.newsList.indexOf(this.selected)
const ownIndex = this.newsList.indexOf(news)
const isPrev = (ownIndex + 1) === selectedIndex || (ownIndex === this.newsList.length - 1 && selectedIndex === 0)
if(isPrev) {
classString += ' carousel-item-prev'
}
const isNext = (ownIndex - 1) === selectedIndex || (ownIndex === 0 && selectedIndex === this.newsList.length - 1)
if(isNext) {
classString += ' carousel-item-next'
}
}
return classString;
},
setSelected(news, direction = "next") {
if (this.selected && news && this.selected === news) return
const oldCard = document.getElementById('card-'+this.selected.news_id)
// TODO: to show animation of non neighbour item through menu reapply css classes
if(direction === 'next') {
// set nextCard .carousel-item-next.carousel-item-start
oldCard.classList.add('carousel-item-start')
} else {
// set prevCard .carousel-item-prev.carousel-item-end
oldCard.classList.add('carousel-item-end')
//if the clicked element is already active, do nothing
if(clickedElementIndex === oldElementIndex) return;
//add prev/next class to the clicked element
if(clickedElementIndex > oldElementIndex){
clickedElement.classList.add('carousel-item-next');
}else{
clickedElement.classList.add('carousel-item-prev');
}
this.selected = news
// move to clicked element
await Vue.nextTick(() => { this.carouselInstance.to(clickedElementIndex); })
this.selected = news;
},
contentURI: function (content_id) {
return (
@@ -133,10 +103,52 @@ export default {
this.$refs.newsModal.show();
},
},
created() {
this.$emit("setConfig", false);
this.$fhcApi.factory.cms
.news(MAX_LOADED_NEWS)
.then(res => res.data)
.then((news) => {
this.allNewsList = Array.from(Object.values(news));
this.selected = this.allNewsList.length ? this.allNewsList[0] : null
Vue.nextTick(()=>{
if (Array.isArray(this.$refs.carouselItems) && this.$refs.carouselItems.length >0) {
this.$refs.carouselItems[0].classList.add("active")
}
})
})
.catch((err) => {
console.error("ERROR: ", err);
});
},
mounted() {
if (this.$refs.container) {
new ResizeObserver(entries => {
for (const entry of entries) {
let w = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width;
// TODO(chris): rework sizing
if (w > 600)
this.size = 3;
else if (w > 350)
this.size = 2;
else if (w > 250)
this.size = 1;
else
this.size = 0;
}
}).observe(this.$refs.container);
}
this.carouselInstance = new bootstrap.Carousel(this.$refs.carousel, {
wrap: false, // keep this off even though it actually wraps
interval: false
});
},
template: /*html*/ `
<div class="widgets-news h-100" :style="getNewsWidgetStyle">
<div ref="container" class="widgets-news h-100" :class="sizeClass" :style="getNewsWidgetStyle">
<div class="d-flex flex-column h-100">
<div class="h-100" style="overflow-y: auto" v-if="width == 1">
<div v-for="(news, index) in newsList" :key="news.news_id" class="mt-2">
<div v-if="index > 0 " class="fhc-seperator"></div>
@@ -144,40 +156,40 @@ export default {
<span class="small text-muted">{{ formatDateTime(news.insertamum) }}</span>
</div>
</div>
<div v-else class="row h-100">
<!-- TODO: mobile responsiveness of this part-->
<div v-else class="row h-100 g-0">
<div :class="'col-'+(width == 2? 6 : 4) + ' h-100 g-0'" style="overflow: auto;">
<template v-for="news in newsList" :key="'menu-'+news.news_id">
<div class="row fhc-news-menu-item" @click="setSelected(news)" :class="getMenuItemClass(news)" style="margin-right: 0px; margin-left: 0px;">
<div class="col-8 fhc-news-menu-item-betreff" style="overflow-y: hidden;"><p>{{news.content_obj.betreff ?? ''}}</p></div>
<span class="fhc-news-menu-item-date fw-bold"
>{{ news.datum ?? ''}}</span>
<div class="position-relative">
<div class="row fhc-news-menu-item " @click="setSelected(news)" :class="getMenuItemClass(news)" style="margin-right: 0px; margin-left: 0px; overflow-y: hidden;">
<div class="col-8 fhc-news-menu-item-betreff">
<p class="fhc-news-text mb-0">
{{news.content_obj.betreff ?? ''}}
</p>
</div>
<span style="top:2px; right:0" class=" position-absolute d-none d-xl-block fhc-news-text fhc-news-menu-item-date fw-bold">
{{ news.datumformatted ?? ''}}
</span>
</div>
</div>
</template>
</div>
<div :class="'col-'+(width == 2? 6 : 8) + ' h-100'" style="padding-left: 0px; padding-right: 0px;" ref="htmlContent">
<div class="container h-100" style="padding: 0px;" ref="carocontainer">
<div id="carouselExample" style="height: 100%;" class="carousel slide fhc-carousel" data-bs-ride="carousel"
data-bs-interval="false"
ref="carocontrols">
<div class="carousel-indicators">
<button v-for="(news, index) in newsList" :id="'indicator-'+news_news_id" type="button" data-bs-target="#carouselExample" data-bs-slide-to="index"></button>
</div>
<div id="FhcCarouselContainer" style="height: 100%;" ref="carousel" class="carousel slide fhc-carousel" data-bs-ride="carousel" data-bs-interval="false">
<div class="carousel-inner" style="height: 100%; max-width: 100%;">
<div v-for="(news, index) in newsList" class="carousel-item" :class="getDynClassCarouselItem(news, index)" style="overflow-y: auto; height: 100%;" :id="'card-'+news.news_id" v-html="news.content_obj.content">
<div class="carousel-inner" ref="carouselInner" style="height: 100%; max-width: 100%;">
<div ref="carouselItems" v-for="(news, index) in newsList" class="carousel-item " style="overflow-y: auto; height: 100%;" :id="'card-'+news.news_id" v-html="news.content_obj.content">
</div>
</div>
<!-- TODO: prev/next button styling && placement-->
<button @click="setPrev" style="z-index: 9999; color: black; opacity: 1;" data-bs-target="#carouselExample" class="carousel-control-prev" type="button">
<i class="fa fa-chevron-left"></i>
<button @click="setPrev" @focus="$event.target.blur()" style="z-index: 100; color: black; overflow: hidden; margin-left: 10px; width:35px;" data-bs-target="#FhcCarouselContainer" class="carousel-control-prev" type="button">
<div class="border rounded-circle" style="padding-left: 0.4rem; padding-right: 0.4rem; background-color:rgba(138,138,138,0.4)">
<i class="fa fa-chevron-left"></i>
</div>
</button>
<button @click="setNext" style="z-index: 9999; color: black; opacity: 1;" data-bs-target="#carouselExample" class="carousel-control-next" type="button">
<i class="fa fa-chevron-right"></i>
<button @click="setNext" @focus="$event.target.blur()" style="z-index: 100; color: black; overflow: hidden; margin-right: 10px; width:35px;" data-bs-target="#FhcCarouselContainer" class="carousel-control-next" type="button">
<div class="border rounded-circle" style="padding-left: 0.4rem; padding-right: 0.4rem; background-color:rgba(138,138,138,0.4)">
<i class="fa fa-chevron-right"></i>
</div>
</button>
</div>
</div>
@@ -0,0 +1,20 @@
import AbstractWidget from './Abstract.js';
import StudiengangInformation from '../Cis/Cms/StudiengangInformation/StudiengangInformation.js';
export default {
data(){
return {};
},
components:{
StudiengangInformation,
},
mixins:[AbstractWidget],
mounted(){
this.$emit('setConfig', false);
},
template:/*html*/`
<div class="p-3 h-100 overflow-scroll">
<studiengang-information displayWidget ></studiengang-information>
</div>
`
};
+131 -52
View File
@@ -1,3 +1,6 @@
import CoreForm from "../Form/Form.js";
import FormInput from "../Form/Input.js";
import BsModal from "../Bootstrap/Modal.js";
import AbstractWidget from './Abstract';
export default {
@@ -9,13 +12,19 @@ export default {
default: false
}
},
components:{
CoreForm,
FormInput,
BsModal
},
data: () => ({
bookmark_id: null,
title_input: "",
url_input: "",
validation: {
invalidURL: false,
invalidTitel: false,
}
},
}),
computed: {
@@ -33,6 +42,74 @@ export default {
},
},
methods: {
stopDrag(event){
event.preventDefault();
},
clearInputs(){
this.title_input = "";
this.url_input = "";
},
openCreateModal() {
this.$refs.createModal.show()
},
openEditModal(bookmark) {
this.title_input = bookmark.title;
this.url_input = bookmark.url;
this.bookmark_id = bookmark.bookmark_id;
this.$refs.editModal.show()
},
editBookmark(event){
event.preventDefault();
if(!this.bookmark_id || !this.url_input || !this.title_input) return;
this.$fhcApi.factory.bookmark
.update({
bookmark_id: this.bookmark_id,
title: this.title_input,
url: this.url_input,
})
.then((res) => res.data)
.then((result) => {
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
this.clearInputs();
this.$refs.editModal.hide();
this.bookmark_id = null;
})
.catch(this.$fhcAlert.handleSystemError);
},
insertBookmark(event) {
event.preventDefault();
// reset is-invalid css on url input field
for (let key of Object.keys(this.validation)) {
this.validation[key] = false;
}
// early return if validation failed
if (!this.isValidationSuccessfull()) return;
this.$fhcApi.factory.bookmark
.insert({
tag: this.config.tag,
title: this.title_input,
url: this.url_input,
})
.then((res) => res.data)
.then((result) => {
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkAdded"));
// refetch the bookmarks to see the updates
this.fetchBookmarks();
this.$refs.createModal.hide();
// reset the values for the title and url inputs
this.clearInputs();
})
.catch(this.$fhcAlert.handleSystemError);
},
isValidationSuccessfull() {
// validate the input fields
if (this.title_input.length === 0) {
@@ -55,33 +132,6 @@ export default {
})
.catch(this.$fhcAlert.handleSystemError);
},
addLink() {
// reset is-invalid css on url input field
for (let key of Object.keys(this.validation)) {
this.validation[key] = false;
}
// early return if validation failed
if (!this.isValidationSuccessfull()) return;
this.$fhcApi.factory.bookmark
.insert({
tag: this.config.tag,
title: this.title_input,
url: this.url_input,
})
.then((res) => res.data)
.then((result) => {
this.$fhcAlert.alertInfo(this.$p.t("bookmark", "bookmarkAdded"));
// refetch the bookmarks to see the updates
this.fetchBookmarks();
})
.catch(this.$fhcAlert.handleSystemError);
// reset the values for the title and url inputs
this.title_input = "";
this.url_input = "";
},
async removeLink(bookmark_id) {
let isConfirmed = await this.$fhcAlert.confirmDelete();
@@ -107,50 +157,79 @@ export default {
// this.$emit('setConfig', true); -> use this to enable widget config mode if needed
},
template: /*html*/ `
<div class="widgets-url w-100 h-100" style="padding: 1rem 1rem;">
<div class="widgets-url w-100 h-100 overflow-scroll" style="padding: 1rem 1rem;">
<div class="d-flex flex-column justify-content-between">
<!-- todo: widgetTag ?? -->
<template v-if="shared">
<template v-if="!emptyBookmarks">
<button v-if="editModeIsActive" class="btn btn-outline-secondary btn-sm w-100 mt-2" @click="openCreateModal" type="button">{{$p.t('bookmark','newLink')}}</button>
<div v-for="link in shared" :key="link.id" class="d-flex mt-2">
<a target="_blank" :href="link.url">
<i class="fa fa-solid fa-arrow-up-right-from-square"></i>{{ link.title }}
</a>
<a class="ms-auto" href="#" @click.prevent="removeLink(link.bookmark_id)" v-show="configMode || editModeIsActive">
<i class="fa fa-regular fa-trash-can" style="color: #e01b24;"></i>
<i class="fa fa-solid fa-arrow-up-right-from-square me-1"></i>{{ link.title }}
</a>
<div class="ms-auto">
<!--EDIT BOOKMARK-->
<a href="#" @click.prevent="openEditModal(link)" v-show="configMode || editModeIsActive">
<i class="fa fa-edit me-1"></i>
</a>
<!--DELETE BOOKMARK-->
<a href="#" @click.prevent="removeLink(link.bookmark_id)" v-show="configMode || editModeIsActive">
<i class="fa fa-regular fa-trash-can" style="color: #e01b24;"></i>
</a>
</div>
</div>
</template>
<div v-else class="d-flex mt-2">
<span>{{$p.t('bookmark','emptyBookmarks')}}</span>
</div>
<div v-if="editModeIsActive " class="mt-2">
<div class="form-group">
<input maxlength="255" required class="form-control form-control-sm" :class="{'is-invalid':validation.invalidTitel}" placeholder="Titel" type="text" v-model="title_input" name="title" >
<!-- validation html for titel -->
<div class="invalid-feedback">
{{$p.t("bookmark", "invalidTitel")}}.
</div>
</div>
<div class="form-group">
<input required id="bookmark_link" class="form-control form-control-sm mt-2" :class="{'is-invalid':validation.invalidURL}" type="url" placeholder="URL" v-model="url_input" name="url">
<!-- validation html for url -->
<div class="invalid-feedback">
{{$p.t("bookmark", "invalidUrl")}}.
</div>
</div>
<button class="btn btn-outline-secondary btn-sm w-100 mt-2" @click="addLink" type="button">{{$p.t('bookmark','saveLink')}}</button>
</div>
</template>
<template v-else>
<p v-for="i in 4" class="placeholder-glow">
<span class="placeholder" :class="{'col-9' : true}"></span>
</p>
</template>
</div>
</div>`,
</div>
<!--EDIT MODAL-->
<core-form draggable="true" @dragstart="stopDrag" @drag="stopDrag" @dragend="stopDrag" v-if="editModeIsActive " ref="editForm">
<bs-modal @[\`hide.bs.modal\`]="bookmark_id=null; clearInputs();" ref="editModal">
<template #title>
<h2>{{$p.t('bookmark','editLink')}}</h2>
</template>
<template #default>
<label class="form-label" for="editTitle">Title</label>
<form-input id="editTitle" v-model="title_input" name="title" class="mb-2"></form-input>
<label class="form-label" for="editUrl">Url</label>
<form-input id="editUrl" v-model="url_input" name="url"></form-input>
</template>
<template #footer>
<button @click="editBookmark" class="btn btn-primary">{{$p.t('bookmark','editLink')}}</button>
</template>
</bs-modal>
</core-form>
<!--CREATE MODAL-->
<core-form draggable="true" @dragstart="stopDrag" @drag="stopDrag" @dragend="stopDrag" v-if="editModeIsActive " ref="createForm">
<bs-modal @[\`hide.bs.modal\`]="clearInputs();" ref="createModal">
<template #title>
<h2>{{$p.t('bookmark','newLink')}}</h2>
</template>
<template #default>
<label class="form-label" for="insertTitle">Title</label>
<form-input id="insertTitle" v-model="title_input" name="title" class="mb-2"></form-input>
<label class="form-label" for="insertUrl">Url</label>
<form-input id="insertUrl" v-model="url_input" name="url"></form-input>
</template>
<template #footer>
<button @click="insertBookmark" class="btn btn-primary">{{$p.t('bookmark','saveLink')}}</button>
</template>
</bs-modal>
</core-form>`,
};
/*
+50 -11
View File
@@ -12,8 +12,8 @@ export default {
searchtimer: null,
hidetimer: null,
searchsettings: {
searchstr: '',
types: [],
searchstr: this.getSearchStr(),
types: this.getSearchTypes(),
},
searchresult: [],
showresult: false,
@@ -32,12 +32,14 @@ export default {
},
template: /*html*/`
<form ref="searchform" class="d-flex me-3" :class="searchoptions.cssclass" action="javascript:void(0);"
@focusin="this.searchfocusin" @focusout="this.searchfocusout">
<div ref="searchbox" class="h-100 input-group me-2 bg-white">
@focusin="this.searchfocusin" @focusout="this.searchfocusout">
<div ref="searchbox" class="h-100 input-group me-2 bg-white">
<span style="background-color:inherit" class="input-group-text">
<i class="fa-solid fa-magnifying-glass"></i>
</span>
<input @keyup="this.search" @focus="this.showsearchresult"
v-model="this.searchsettings.searchstr" class="form-control"
type="search" :placeholder="'Search: '+ searchsettings.types.join(' / ')" aria-label="Search">
type="search" :placeholder="'Search: '+ search_types_string" aria-label="Search">
<button data-bs-toggle="collapse" data-bs-target="#searchSettings" aria-expanded="false" aria-controls="searchSettings" ref="settingsbutton" class="btn btn-outline-secondary" type="button" id="search-filter"><i class="fas fa-cog"></i></button>
</div>
@@ -81,12 +83,32 @@ export default {
</form>
`,
watch:{
'searchsettings.types': function (newValue){
this.search();
},
'searchsettings.searchstr': function (newSearchValue, oldSearchValue) {
sessionStorage.setItem('searchstr',newSearchValue);
},
},
computed:{
search_types_string(){
if (Array.isArray(this.searchsettings.types) && this.searchsettings.types.length > 0){
return this.searchsettings.types.join(' / ');
}else{
return JSON.stringify(this.searchsettings.types);
}
},
},
beforeMount: function() {
this.updateSearchOptions();
this.$watch('searchsettings.types', (newValue, oldValue) => {
if (Array.isArray(newValue) && newValue.length === 0){
this.searchsettings.types = this.allSearchTypes();
}
// stores the search types in the localstorage, only if the newValue is also an array
if(Array.isArray(newValue)){
localStorage.setItem('searchtypes', JSON.stringify(newValue));
}
this.search();
});
},
mounted(){
this.settingsDropdown = new bootstrap.Collapse(this.$refs.settings, {
@@ -101,6 +123,23 @@ export default {
}
},
methods: {
getSearchTypes: function () {
let result = this.allSearchTypes();
if (localStorage.getItem('searchtypes')) {
result = JSON.parse(localStorage.getItem('searchtypes'));
}
return result;
},
allSearchTypes() {
let allTypes = [];
for (const idx in this.searchoptions.types) {
allTypes.push(this.searchoptions.types[idx]);
};
return allTypes;
},
getSearchStr: function(){
return sessionStorage.getItem('searchstr') ?? '';
},
checkSettingsVisibility: function(event) {
// hides the settings collapsible if the user clicks somewhere else
if (!this.$refs.settings.contains(event.target))
@@ -116,7 +155,7 @@ export default {
// removes the event listener checkSettingsVisibility when the collapsible is hidden
document.removeEventListener("click", this.checkSettingsVisibility);
},
updateSearchOptions: function() {
allSearchOptions: function() {
this.searchsettings.types = [];
for( const idx in this.searchoptions.types ) {
this.searchsettings.types.push(this.searchoptions.types[idx]);
+103 -25
View File
@@ -1,8 +1,23 @@
import {user_locale} from "../plugin/Phrasen.js";
import CalendarDates from "./CalendarDates.js";
class CalendarDate {
constructor(y, m, d) {
this.weekStart = CalendarDate.getWeekStart();
this.watchLocale = Vue.watch(
user_locale,
(newLocale, oldLocale, onCleanup) =>{
this.weekStart = CalendarDate.getWeekStart();
this._clean();
onCleanup((cleanup)=>{
console.log(cleanup,"HERE IA M")
});
},
);
this.set(y, m, d);
this._clean();
CalendarDates.subscribe(this);
}
get y() { return this._y }
set y(v) { this._y = v; this._clean() }
@@ -49,22 +64,33 @@ class CalendarDate {
}
}
}
get wYear() {
if( this.w === 1 ) {
return this.cdLastDayOfWeek.format({ year: 'numeric' });
}
return this.cdFirstDayOfWeek.format({ year: 'numeric' });
}
get wd() {
if (this._wd === null) {
// the .getDay() method from js Date object ALWAYS returns values from 0 to 6, where 0 is Sunday and 6 is Saturday
// aligns the getDay() result of the Date to the weekStart of the CalendarDate
this._wd = ((new Date(this.y, this.m, this.d)).getDay()+7-this.weekStart)%7;
}
return this._wd;
}
get firstDayOfWeek() {
let firstDayOfWeek = new Date(this.y, this.m, this.d);
// to ensure that firstDayOfWeek.getDay() is always greater than this.weekStart we add 7 and wrap the result around with %7 to avoid negative numbers
firstDayOfWeek.setDate(this.d -(firstDayOfWeek.getDay()+7-this.weekStart)%7);
return firstDayOfWeek;
}
get cdFirstDayOfWeek() {
return new CalendarDate(this.firstDayOfWeek);
let FirstDayOfWeek = new CalendarDate(this.firstDayOfWeek);
return FirstDayOfWeek;
}
get lastDayOfWeek() {
let lastDayOfWeek = new Date(this.y, this.m, this.d);
// uses the calculation from firstDayOfWeek and adds 6 days to the result to get the last day of the week
lastDayOfWeek.setDate(this.d -(lastDayOfWeek.getDay()+7-this.weekStart)%7 +6);
return lastDayOfWeek;
}
@@ -78,27 +104,38 @@ class CalendarDate {
return days
}
get cdLastDayOfWeek() {
return new CalendarDate(this.lastDayOfWeek);
let LastDayOfWeek = new CalendarDate(this.lastDayOfWeek);
return LastDayOfWeek;
}
get firstDayOfCalendarMonth() {
let firstDayOfMonth = new Date(this.y, this.m, 1);
return new Date(this.y, this.m, 1-(firstDayOfMonth.getDay() + 7 - this.weekStart)%7);
let offset = (firstDayOfMonth.getDay() + 7 - this.weekStart) % 7;
// offset will be greater than 1 most of the time, using a negative number for a date returns a date in the past
return new Date(this.y, this.m, 1-offset);
}
get cdFirstDayOfCalendarMonth() {
let firstDayOfMonth = new Date(this.y, this.m, 1);
return new CalendarDate(this.y, this.m, 1-(firstDayOfMonth.getDay() + 7 - this.weekStart)%7);
let offset = (firstDayOfMonth.getDay() + 7 - this.weekStart) % 7;
let FirstDayOfCalendarMonth = new CalendarDate(this.y, this.m, 1 - offset);
return FirstDayOfCalendarMonth;
}
get lastDayOfCalendarMonth() {
// In JavaScript, the Date constructor interprets: A day of 0 as the last day of the previous month
let lastDayOfMonth = new Date(this.y, this.m+1, 0);
return new Date(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth(), lastDayOfMonth.getDate()+6-(lastDayOfMonth.getDay() + 7 - this.weekStart)%7);
let offset = (lastDayOfMonth.getDay() + 7 - this.weekStart) % 7;
return new Date(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth(), lastDayOfMonth.getDate()+6-offset);
}
get cdLastDayOfCalendarMonth() {
let lastDayOfMonth = new Date(this.y, this.m+1, 0);
return new CalendarDate(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth(), lastDayOfMonth.getDate()+6-(lastDayOfMonth.getDay() + 7 - this.weekStart)%7);
let offset = (lastDayOfMonth.getDay() + 7 - this.weekStart) % 7;
let LasyDayOfCalendarMonth = new CalendarDate(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth(), lastDayOfMonth.getDate() + 6 - offset);
return LasyDayOfCalendarMonth;
}
get cdLastDayOfNextCalendarMonth() {
let lastDayOfMonth = new Date(this.y, this.m+1, 0);
return new CalendarDate(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth() + 1, lastDayOfMonth.getDate()+6-(lastDayOfMonth.getDay() + 7 - this.weekStart)%7);
let offset = (lastDayOfMonth.getDay() + 7 - this.weekStart) % 7;
let LastDayOfNextCalendarMonth = new CalendarDate(lastDayOfMonth.getFullYear(), lastDayOfMonth.getMonth() + 1, lastDayOfMonth.getDate() + 6 - offset);
return LastDayOfNextCalendarMonth;
}
get nextSevenDays() {
const days = []
@@ -109,16 +146,30 @@ class CalendarDate {
return days
}
get numWeeks() {
return (new CalendarDate(this.y+1,0,this.weekStart == 1 ? -3 : 0)).w;
// if the week starts with Monday we have to go 3 days in the past from the start of the next year to get the correct numWeek of the current year
// this is because for example 30.12.2024 - 05.01.2025 is the first calendarWeek of 2025
let lastCalendarWeek = new CalendarDate(this.y + 1, 0, this.weekStart == 1 ? -3 : 0);
return lastCalendarWeek.w;
}
set(y,m,d,noClean) {
if (y !== undefined && (m === undefined || m === true) && d === undefined) {
if (Object.prototype.toString.call(y) === '[object Date]')
if (this.isDate(y))
{
// set year/month/day from date object
return this.set(y.getFullYear(), y.getMonth(), y.getDate(), m);
}
if (y.y !== undefined && y.m !== undefined && y.d !== undefined)
{
// set year/month/day from CalendarDate object
return this.set(y.y, y.m, y.d, m);
}
}
[this._y,this._m,this._d] = [y || 0, m || 0, d || 0];
// initialize year/month/day
this._y = y ?? 0;
this._m = m ?? 0;
this._d = d ?? 0;
if (!noClean)
this._clean();
}
@@ -127,11 +178,11 @@ class CalendarDate {
this._w = null;
this._wd = null;
}
format(options) {
return (new Date(this._y, this._m, this._d)).toLocaleString(undefined, options);
format(options, lang=undefined) {
return (new Date(this._y, this._m, this._d)).toLocaleString(lang, options);
}
compare(d) {
if (Object.prototype.toString.call(d) === '[object Date]')
if (this.isDate(d))
return (this.y === d.getFullYear() && this.m === d.getMonth() && this.d === d.getDate());
return (this.y === d.y && this.m === d.m && this.d === d.d);
}
@@ -151,6 +202,13 @@ class CalendarDate {
setLocale(locale) {
this.weekStart = CalendarDate.getWeekStart(locale);
}
// method that checks if the parameter is of type Date
isDate(obj){
return Object.prototype.toString.call(obj) === '[object Date]';
}
cleanup(){
this.watchLocale.stop();
}
}
/**
* Returns the weekday number (Date.getDay()) on which the week starts depending on the locale.
@@ -163,19 +221,39 @@ class CalendarDate {
* @return integer
*/
CalendarDate.getWeekStart = function(locale) {
locale = locale || navigator.language;
const parts = locale.match(/^([a-z]{2,3})(?:-([a-z]{3})(?=$|-))?(?:-([a-z]{4})(?=$|-))?(?:-([a-z]{2}|\d{3})(?=$|-))?/i);
const regionSat = 'AEAFBHDJDZEGIQIRJOKWLYOMQASDSY'.match(/../g);
const regionSun = 'AGARASAUBDBRBSBTBWBZCACNCODMDOETGTGUHKHNIDILINJMJPKEKHKRLAMHMMMOMTMXMZNINPPAPEPHPKPRPTPYSASGSVTHTTTWUMUSVEVIWSYEZAZW'.match(/../g);
const languageSat = ['ar','arq','arz','fa'];
const languageSun = 'amasbndzengnguhehiidjajvkmknkolomhmlmrmtmyneomorpapssdsmsnsutatethtnurzhzu'.match(/../g);
return (
parts[4] ? (
regionSun.includes(parts[4]) ? 0 :
regionSat.includes(parts[4]) ? 6 : 1) : (
languageSun.includes(parts[1]) ? 0 :
languageSat.includes(parts[1]) ? 6 : 1));
locale = user_locale.value || locale || navigator.language;
const parts = locale.match(/^([a-z]{2,3})(?:-([a-z]{3})(?=$|-))?(?:-([a-z]{4})(?=$|-))?(?:-([a-z]{2}|\d{3})(?=$|-))?/i);
const language_code = parts[1];
const language_starting_Sat = ['ar','arq','arz','fa'];
const language_starting_Sun = 'amasbndzengnguhehiidjajvkmknkolomhmlmrmtmyneomorpapssdsmsnsutatethtnurzhzu'.match(/../g);
const region_code = parts[4];
const region_starting_Sat = 'AEAFBHDJDZEGIQIRJOKWLYOMQASDSY'.match(/../g);
const region_starting_Sun = 'AGARASAUBDBRBSBTBWBZCACNCODMDOETGTGUHKHNIDILINJMJPKEKHKRLAMHMMMOMTMXMZNINPPAPEPHPKPRPTPYSASGSVTHTTTWUMUSVEVIWSYEZAZW'.match(/../g);
if (region_code){
if (region_starting_Sun.includes(region_code))
return 0;
else if (region_starting_Sat.includes(region_code))
return 6;
else
return 1;
}
else if(language_code)
{
if (language_starting_Sun.includes(language_code))
return 0;
else if (language_starting_Sat.includes(language_code))
return 6;
else
return 1;
}
else
{
return 1;
}
}
+16
View File
@@ -0,0 +1,16 @@
class CalendarDates{
static subscribers_array = [];
static subscribe(subscriber){
this.subscribers_array.push(subscriber);
}
static unsubscribe(subscriber){
this.subscribers_array = this.subscribers_array.filter(sub => !sub.compare(subscriber));
}
static cleanup(){
this.subscribers_array.forEach(sub => { sub.cleanup() });
}
}
export default CalendarDates;
+21
View File
@@ -0,0 +1,21 @@
export default {
data(){
return {
}
},
props:{
data:{
type:Object|String,
}
},
computed:{
componentData: function(){
if(!this.data){
return "Pass data to be printed by adding the :data prop on the component";
}
return JSON.stringify(this.data, null, 2);
},
},
template:`<pre class="p-2 bg-secondary text-white" >{{componentData}}</pre>`
}
@@ -0,0 +1,93 @@
// collection of relative and absolute regex to replace legacy links
const GROUP_REPLACEMENT_STRATEGIES = {
QUERY_PARAMETERS:'QUERY_PARAMETERS',
PATH_SEGMENTS:'PATH_SEGMENTS',
}
const regexList = {
relative:[
{
priority: 1,
regex: new RegExp(/^\.\.\/cms\/content\.php\?content_id=([0-9]+)/),
replacement: FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/content',
group_replacement_strategy:GROUP_REPLACEMENT_STRATEGIES.PATH_SEGMENTS,
},
{
priority: 2,
regex: new RegExp(/^\.\.\/cms\/news\.php/),
replacement: FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/CisVue/Cms/news',
},
{
priority: 3,
regex: new RegExp(/^\.\.\/index\.ci\.php\//),
replacement: FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router,
},
{
priority: 10,
regex: new RegExp("/^\.\.\//"),
replacement: FHC_JS_DATA_STORAGE_OBJECT.app_root,
},
],
absolute:[
{}
]
};
// sorts the relative regex array by priority ascending
const relative_regex = regexList.relative
.sort((a, b) => {
return a.priority - b.priority;
})
.map(regex => {
return {
regex: regex.regex,
replacement: regex.replacement,
group_replacement_strategy: regex.group_replacement_strategy,
}
});
// sorts the absolute regex array by priority ascending
const absolute_regex = regexList.absolute
.sort((a, b) => {
return a.priority - b.priority;
})
.map(regex => {
return {
regex: regex.regex,
replacement: regex.replacement,
group_replacement_strategy: regex.group_replacement_strategy,
}
})
export function replaceRelativeLegacyLink(relativeLegacyLink){
for (let {regex,replacement,group_replacement_strategy} of relative_regex){
// if any of the regex matches the relativeLegacyLink, replace the matched part with the new app_root path
let match = relativeLegacyLink.match(regex);
if (match) {
let new_link = relativeLegacyLink.replace(regex, replacement);
switch (group_replacement_strategy){
case 'QUERY_PARAMETERS':
//TODO: this doesn't really work yet because the query parameter are key/value pairs
new_link = new_link.concat(`?${match[1]}`);
for (let query_parameter of match.slice(2)) {
new_link = new_link.concat(`&${query_parameter}`);
}
break;
case 'PATH_SEGMENTS':
for (let query_parameter of match.slice(1)) {
new_link = new_link.concat(`/${query_parameter}`);
}
break;
default:
break;
}
return new_link;
}
}
// if none of the regex matched with the string return the original path
return relativeLegacyLink;
}
+5
View File
@@ -263,6 +263,10 @@ export default {
return false;
},
handleSystemError(error) {
// don't show an error message to the user if the error was an aborted request
if(error.hasOwnProperty('name') && error.name.toLowerCase() === "AbortError".toLowerCase())
return;
// Error is string
if (typeof error === 'string')
return $fhcAlert.alertSystemError(error);
@@ -279,6 +283,7 @@ export default {
if (typeof error === 'object' && error !== null) {
let errMsg = '';
if (error.hasOwnProperty('response') && error.response?.data?.retval)
errMsg += 'Error Message: ' + (error.response.data.retval.message || error.response.data.retval) + '\r\n';
else if (error.hasOwnProperty('message'))
+22 -6
View File
@@ -4,6 +4,12 @@ import FhcApiFactory from '../api/fhcapifactory.js';
export default {
install: (app, options) => {
if (app.config.globalProperties.$fhcApi) {
if (options?.factory) {
app.config.globalProperties.$fhcApi.factory.addEndpoints(options.factory);
}
return;
}
app.use(FhcAlert);
function _get_config(form, uri, data, config) {
@@ -293,29 +299,39 @@ export default {
class FhcApiFactoryWrapper {
constructor(factorypart, root) {
if (root === undefined)
if (root === undefined) {
this.$fhcApi = app.config.globalProperties.$fhcApi;
else
this.$fhcApi.factory = this;
} else {
Object.defineProperty(this, '$fhcApi', {
get() {
return (root || this).$fhcApi;
}
})
}
this.addEndpoints(factorypart)
}
addEndpoints(factorypart) {
Object.keys(factorypart).forEach(key => {
Object.defineProperty(this, key, {
get() {
if (typeof factorypart[key] == 'function')
return factorypart[key].bind(this);
return new FhcApiFactoryWrapper(factorypart[key], root || this);
return new FhcApiFactoryWrapper(factorypart[key], this.$fhcApi.factory);
}
});
});
}
}
const mergedFhcApiFactory = options?.factory ? {...FhcApiFactory, ...options.factory} : FhcApiFactory;
const factory = new FhcApiFactoryWrapper(FhcApiFactory);
if (options?.factory)
factory.addEndpoints(options.factory);
app.config.globalProperties.$fhcApi.factory = new FhcApiFactoryWrapper(mergedFhcApiFactory);
app.provide('$fhcApi', app.config.globalProperties.$fhcApi);
app.config.globalProperties.$fhcApi.factory = factory;
app.provide('$fhcApi', app.config.globalProperties.$fhcApi);
}
};
+8 -4
View File
@@ -1,9 +1,12 @@
import FhcApi from './FhcApi.js';
const categories = Vue.reactive({});
const user_language = Vue.ref(FHC_JS_DATA_STORAGE_OBJECT.user_language);
const loadingModules = {};
let reload = false;
let user_language = Vue.ref(FHC_JS_DATA_STORAGE_OBJECT.user_language);
export let user_locale = Vue.computed(()=>{
if(!user_language.value) return null;
return FHC_JS_DATA_STORAGE_OBJECT.server_languages.find(language => language.sprache == user_language.value).locale;
});
function extractCategory(obj, category) {
return obj.filter(e => e.category == category).reduce((res, elem) => {
@@ -22,10 +25,11 @@ function getValueForLoadedPhrase(category, phrase, params) {
}
const phrasen = {
user_language,
user_locale,
setLanguage(language, api) {
const catArray = Object.keys(categories)
return api.factory.phrasen.setLanguage(catArray, language).then(res => {
if(reload) window.location.reload()
res.data.forEach(row => {
categories[row.category][row.phrase] = row.text
@@ -79,13 +83,13 @@ const phrasen = {
export default {
install(app, options) {
reload = options?.reload ?? reload
app.use(FhcApi, options?.fhcApi || undefined);
app.config.globalProperties.$p = {
t: phrasen.t,
loadCategory: cat => phrasen.loadCategory.call(app, cat),
setLanguage: phrasen.setLanguage,
user_language: user_language,
user_locale,
t_ref: phrasen.t_ref
};
app.provide('$p', app.config.globalProperties.$p);
+113
View File
@@ -0,0 +1,113 @@
function doCopy (copy, original, copyArray) {
// Callback function to iterate on array or object elements
function callback (value, key) {
// Copy the contents of objects
if (Highcharts.isObject(value, !copyArray) &&
!Highcharts.isClass(value) &&
!Highcharts.isDOMElement(value)
) {
copy[key] = doCopy(copy[key] || Highcharts.isArray(value) ? [] : {}, value, copyArray)
} else {
// Primitives are copied over directly
copy[key] = original[key]
}
}
if (Highcharts.isArray(original)) {
original.forEach(callback)
} else {
Highcharts.objectEach(original, callback)
}
return copy
}
const copyObject = function (obj, copyArray) {
return doCopy({}, obj, copyArray)
}
const highchartsPlugin = {
install(app, options) {
function destroyChart() {
if (this.chart) {
this.chart.destroy()
}
}
function generateVueComponent(Highcharts, VueVersion) {
const VUE_MAJOR = VueVersion.split('.')[0]
const VERSION_DEPENDENT_PROPS = VUE_MAJOR < 3
? {
// Fallback options for Vue v2 to keep backward compatibility.
render: (createElement) => createElement('div', {
ref: 'chart'
}),
beforeDestroy: destroyChart
// The new Vue's 3 syntax.
} : {
render () {
return Vue.h('div', { ref: 'chart' })
},
beforeUnmount: destroyChart
}
return {
template: '<div ref="chart"></div>',
props: {
constructorType: {
type: String,
default: 'chart'
},
options: {
type: Object,
required: true
},
callback: Function,
updateArgs: {
type: Array,
default: () => [true, true]
},
highcharts: {
type: Object
},
deepCopyOnUpdate: {
type: Boolean,
default: true
}
},
watch: {
options: {
handler (newValue) {
this.chart.update(copyObject(newValue, this.deepCopyOnUpdate), ...this.updateArgs)
},
deep: true
}
},
mounted () {
let HC = this.highcharts || Highcharts
// Check whether the chart configuration object is passed, as well as the constructor is valid.
if (this.options && HC[this.constructorType]) {
this.chart = HC[this.constructorType](
this.$refs.chart,
copyObject(this.options, true), // Always pass the deep copy when generating a chart. #80
this.callback ? this.callback : null
)
} else {
(!this.options) ? console.warn('The "options" parameter was not passed.') : console.warn(`'${this.constructorType}' constructor-type is incorrect. Sometimes this error is caused by the fact, that the corresponding module wasn't imported.`)
}
},
...VERSION_DEPENDENT_PROPS
}
}
app.component(
options.tagName || 'highcharts',
generateVueComponent(options.highcharts || Highcharts, Vue.version)
)
},
}
export default highchartsPlugin
+1
View File
@@ -68,6 +68,7 @@ require_once('dbupdate_3.4/33683_digitale_anwesenheitsliste_und_entschuldigungsm
require_once('dbupdate_3.4/40717_lv_faktor.php');
require_once('dbupdate_3.4/48526_pep_tagging.php');
require_once('dbupdate_3.4/41950_perm_gehaelter.php');
require_once('dbupdate_3.4/25999_locale_update.php');
// *** Pruefung und hinzufuegen der neuen Attribute und Tabellen
echo '<H2>Pruefe Tabellen und Attribute!</H2>';

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