Compare commits

..

9 Commits

Author SHA1 Message Date
Johann Hoffmann 6dea54c786 horizontalsplit component analog to verticalsplit; also built it into Studentenverwaltung.js menu; added defaultRatio prop to both components which defaults to 50/50 seperation 2026-04-09 14:30:35 +02:00
chfhtw f06e59b362 Merge branch 'master' into feature-69146/CIS4_Anzeige_LVPLan_Studiengang_Semester_Verband_Gruppe 2026-04-08 14:13:02 +02:00
Harald Bamberger b2538075ee use STV_TAGS_ENABLED config when preparing sql statement for students list to query tags only if enabled 2026-04-07 10:35:06 +02:00
ma0068 ed69bd74ed apply validation logic for dropdowns also for OverviewLvPlan.js 2025-12-03 10:39:41 +01:00
ma0068 f0641ddd6d refactor Dropdown
- load dropdown only if dropdown before was chosen
- validation if field in between is empty
- ensure no null value gets into route
2025-12-02 17:30:11 +01:00
Harald Bamberger 6c2a2e4665 work in progress, add reset function to EventLoader composable, to be able to reload when external parameters are changed 2025-11-27 18:15:38 +01:00
ma0068 5e929df966 stgOrg: calendar and dropdown selection on one page 2025-11-25 12:01:49 +01:00
ma0068 455e0533fe Merge branch 'master' into feature-69146/CIS4_Anzeige_LVPLan_Studiengang_Semester_Verband_Gruppe 2025-11-21 08:53:36 +01:00
ma0068 a8c4fc7607 CIS4 #69146 Lvplan for View StG/Semester/Verband/Gruppe
- new API Endpoint lvPlan/eventsStgOrg
        - new view Cis/LvPlan/StgOrg
        - new component LvPlan/StgOrg.js
        - new overview Cis/OverviewLvPlan with Dropdown
	- phrases
2025-11-20 14:06:12 +01:00
36 changed files with 1668 additions and 741 deletions
+3
View File
@@ -64,6 +64,9 @@ $route['api/v1/system/[S|s]prache/(:any)'] = 'api/v1/system/sprache2/$1';
$route['Cis/LvPlan/.*'] = 'Cis/LvPlan/index/$1';
$route['Cis/MyLvPlan/.*'] = 'Cis/MyLvPlan/index/$1';
$route['Cis/MyLv/.*'] = 'Cis/MyLv/index/$1';
//Routes for Lvplan Verband and page for dropdown Stg/Semester/Verband/Gruppe
$route['Cis/StgOrgLvPlan/.*'] = 'Cis/StgOrgLvPlan/index/$1';
$route['Cis/OverviewLvPlan/.*'] = 'Cis/OverviewLvPlan/index/$1';
$route['Abgabetool/Assistenz'] = 'Cis/Abgabetool/Assistenz';
$route['Abgabetool/Assistenz/(:any)'] = 'Cis/Abgabetool/Assistenz/$1';
@@ -0,0 +1,39 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
*/
class OverviewLvPlan extends Auth_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r']
]);
// Load Config
$this->load->config('calendar');
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
/**
* @return void
*/
public function index()
{
$viewData = array(
'uid'=>getAuthUID(),
'timezone' => $this->config->item('timezone')
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'OverviewLvPlan']);
}
}
@@ -0,0 +1,39 @@
<?php
if (! defined('BASEPATH')) exit('No direct script access allowed');
/**
*
*/
class StgOrgLvPlan extends Auth_Controller
{
/**
* Constructor
*/
public function __construct()
{
parent::__construct([
'index' => ['basis/cis:r']
]);
// Load Config
$this->load->config('calendar');
}
// -----------------------------------------------------------------------------------------------------------------
// Public methods
/**
* @return void
*/
public function index()
{
$viewData = array(
'uid'=>getAuthUID(),
'timezone' => $this->config->item('timezone')
);
$this->load->view('CisRouterView/CisRouterView.php', ['viewData' => $viewData, 'route' => 'StgOrgLvPlan']);
}
}
@@ -36,8 +36,7 @@ class LvMenu extends FHCAPI_Controller
public function __construct()
{
parent::__construct([
'getLvMenu' => self::PERM_LOGGED,
'getMultipleLvMenu' => self::PERM_LOGGED
'getLvMenu' => self::PERM_LOGGED
]);
$this->load->model("ressource/Mitarbeiter_model");
@@ -62,23 +61,24 @@ class LvMenu extends FHCAPI_Controller
/**
* alternative function to get multiple lvMenus with a single http request
* not yet working as intended as the menu_lv.inc.php scripts called by the
* lvMenuBuild event have logic coupled to require_once import which results in
* a wrong logic after the first invocation -> faulty results for lvinfo, moodle
* and several others
*/
public function getMultipleLvMenu(){
$lvMenuOptionList = $this->input->post('lvMenuOptionList', true);
public function getMultipleLvMenu($lvMenuOptionList){
$result =[];
foreach($lvMenuOptionList as $lvMenuOptions){
$lvMenu = $this->getLvMenuInternal($lvMenuOptions['lvid'],$lvMenuOptions['studiensemester_kurzbz']);
$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);
}
private function getLvMenuInternal($lvid, $studiensemester_kurzbz) {
/**
*
*/
public function getLvMenu($lvid, $studiensemester_kurzbz)
{
// return early if parameters are missing
if(!isset($lvid) || !isset($studiensemester_kurzbz))
@@ -89,14 +89,14 @@ class LvMenu extends FHCAPI_Controller
// get the user
if (!$user=getAuthUID())
$this->terminateWithError($this->p->t('global', 'nichtAngemeldet'));
$this->terminateWithError($this->p->t('global', 'nichtAngemeldet'));
// check if is_lector
$is_lector = false;
$mares = $this->Mitarbeiter_model->isMitarbeiter($user);
if(hasData($mares))
{
$is_lector = getData($mares);
$is_lector = getData($mares);
}
// definition of user_is_allowed_to_upload
@@ -105,7 +105,7 @@ class LvMenu extends FHCAPI_Controller
// load lehrveranstaltung
$lvres = $this->Lehrveranstaltung_model->load($lvid);
if(!hasData($lvres))
if(!hasData($lvres))
{
$this->terminateWithError('LV ' . $lvid . ' not found.');
}
@@ -124,7 +124,7 @@ class LvMenu extends FHCAPI_Controller
$stgres = $this->Studiengang_model->load(strval($studiengang_kz));
if(!hasData($stgres))
{
$this->terminateWithError('Stg ' . $lv->studiengang_kz . ' not found.');
$this->terminateWithError('Stg ' . $lv->studiengang_kz . ' not found.');
}
$stg = (getData($stgres))[0];
$kurzbz = strtoupper($stg->typ . $stg->kurzbz);
@@ -139,7 +139,7 @@ class LvMenu extends FHCAPI_Controller
$angemeldet = false;
$lesres = $this->Lehreinheit_model->getLehreinheitenForStudentAndStudienSemester(
$lvid, $user, $angezeigtes_stsem
$lvid, $user, $angezeigtes_stsem
);
if(hasData($lesres) && count(getData($lesres)) > 0)
@@ -148,7 +148,7 @@ class LvMenu extends FHCAPI_Controller
// lehrfach
$lehrfach_id='';
if(defined('CIS_LEHRVERANSTALTUNG_LEHRFACH_ANZEIGEN') && CIS_LEHRVERANSTALTUNG_LEHRFACH_ANZEIGEN)
{
// Wenn der eingeloggte User zu einer der Lehreinheiten zugeteilt ist
@@ -211,8 +211,8 @@ class LvMenu extends FHCAPI_Controller
foreach($fbs as $row)
{
$lehrfach_oe_kurzbz_arr[] = $row->oe_kurzbz;
if($this->PermissionLib->isBerechtigt('lehre', null, $row->oe_kurzbz)
|| $this->PermissionLib->isBerechtigt('assistenz', null, $stg->oe_kurzbz))
if($this->PermissionLib->isBerechtigt('lehre', null, $row->oe_kurzbz)
|| $this->PermissionLib->isBerechtigt('assistenz', null, $stg->oe_kurzbz))
{
$user_is_allowed_to_upload=true;
}
@@ -224,21 +224,21 @@ class LvMenu extends FHCAPI_Controller
$menu = array();
$this->fhc_menu_lvinfo($menu, $lvid, $studiengang_kz, $lektor_der_lv, $is_lector, $lehrfach_oe_kurzbz_arr);
$this->fhc_menu_feedback($menu, $angemeldet, $lvid);
$this->fhc_menu_gesamtnote($menu, $angemeldet, $lvid, $lv, $is_lector, $angezeigtes_stsem);
$this->fhc_menu_emailStudierende($menu, $user, $angemeldet, $lvid, $angezeigtes_stsem);
$this->fhc_menu_abmeldung($menu, $user, $is_lector, $lvid, $angezeigtes_stsem);
$this->fhc_menu_lehretools($menu, $lvid, $angezeigtes_stsem, $sprache);
$this->fhc_menu_anrechnungStudent($menu, $lvid, $angezeigtes_stsem);
$this->fhc_menu_anrechnungLector($menu, $angezeigtes_stsem);
// Addons Menu Logic
// ##########################################################################################
@@ -272,18 +272,18 @@ class LvMenu extends FHCAPI_Controller
'permissionLib' => &$this->PermissionLib,
'phrasesLib' => &$this->PhrasesLib
];
Events::trigger('lvMenuBuild',
// passing $menu per reference
function & () use (&$menu) {
return $menu;
},
$params
Events::trigger('lvMenuBuild',
// passing $menu per reference
function & () use (&$menu) {
return $menu;
},
$params
);
// Menu sortieren
// ##########################################################################################
foreach ($menu as $key => $row){
// removes menu points that are not needed in the c4 lvUebersicht
@@ -291,7 +291,7 @@ class LvMenu extends FHCAPI_Controller
unset($menu[$key]);
continue;
}
// fills pos array to sort the menu
$pos[$key] = $row['position'];
@@ -299,18 +299,11 @@ class LvMenu extends FHCAPI_Controller
array_multisort($pos, SORT_ASC, SORT_NUMERIC, $menu);
return $menu;
}
/**
*
*/
public function getLvMenu($lvid, $studiensemester_kurzbz)
{
$menu = $this->getLvMenuInternal($lvid, $studiensemester_kurzbz);
// HTTP response
// ##########################################################################################
$this->terminateWithSuccess($menu);
}
private function fhc_menu_lvinfo(&$menu, $lvid, $studiengang_kz, $lektor_der_lv, $is_lector, $lehrfach_oe_kurzbz_arr){
@@ -41,7 +41,12 @@ class LvPlan extends FHCAPI_Controller
'getLehreinheitStudiensemester' => self::PERM_LOGGED,
'studiensemesterDateInterval' => self::PERM_LOGGED,
'getLvPlanForStudiensemester' => self::PERM_LOGGED,
'getLv' => self::PERM_LOGGED
'getLv' => self::PERM_LOGGED,
'eventsStgOrg' => self::PERM_LOGGED,
'fetchFerienEvents' => self::PERM_LOGGED,
'getStudiengaenge' => self::PERM_LOGGED,
'getLehrverband' => self::PERM_LOGGED,
]);
$this->load->library('LogLib');
@@ -83,7 +88,7 @@ class LvPlan extends FHCAPI_Controller
// form validation
$this->form_validation->set_rules('start_date', "start_date", "required");
$this->form_validation->set_rules('end_date', "end_date", "required");
if (!$this->form_validation->run())
$this->terminateWithValidationErrors($this->form_validation->error_array());
@@ -100,7 +105,7 @@ class LvPlan extends FHCAPI_Controller
// fetching ferien events
$ferienEvents = $this->fetchFerienEvents($start_date, $end_date);
$this->terminateWithSuccess(array_merge(
$lvplanEvents,
@@ -109,6 +114,49 @@ class LvPlan extends FHCAPI_Controller
));
}
/**
* fetches LvPlan for studiengang / semester / verband / gruppe
*
* @access public
*/
public function eventsStgOrg()
{
$this->load->library('StundenplanLib');
// form validation
$this->form_validation->set_rules('start_date', "start_date", "required");
$this->form_validation->set_rules('end_date', "end_date", "required");
//$this->form_validation->set_rules('stg_kz', "stg_kz", "required"); //no validation show empty calendar
if (!$this->form_validation->run())
{
$this->terminateWithValidationErrors($this->form_validation->error_array());
$stgOrgEvents = [];
$ferienEvents = [];
}
else
{
$start_date = $this->input->post('start_date', true);
$end_date = $this->input->post('end_date', true);
$stg_kz = $this->input->post('stg_kz', true);
$sem = $this->input->post('sem', true);
$verband = $this->input->post('verband', true);
$gruppe = $this->input->post('gruppe', true);
$result = $this->stundenplanlib->getEventsStgOrg($start_date, $end_date, $stg_kz, $sem, $verband, $gruppe);
$stgOrgEvents = $this->getDataOrTerminateWithError($result);
$result = $this->stundenplanlib->fetchFerienTageEvents($start_date, $end_date, $stg_kz);
$ferienEvents = $this->getDataOrTerminateWithError($result);
}
$this->terminateWithSuccess(array_merge(
$stgOrgEvents,
$ferienEvents
));
}
/**
* fetches LvPlan and Ferien events together for the lv
*
@@ -137,7 +185,6 @@ class LvPlan extends FHCAPI_Controller
// fetching ferien events
$ferienEvents = $this->fetchFerienEvents($start_date, $end_date);
$this->terminateWithSuccess(array_merge(
$lvplanEvents,
@@ -287,6 +334,48 @@ class LvPlan extends FHCAPI_Controller
return $this->terminateWithSuccess(current($result));
}
public function getStudiengaenge()
{
$this->load->model('organisation/Studiengang_model','StudiengangModel');
$this->StudiengangModel->addOrder('typ');
$this->StudiengangModel->addOrder('kurzbz');
$result = $this->StudiengangModel->loadWhere([
'aktiv' => true
]);
$data = $this->getDataOrTerminateWithError($result);
return $this->terminateWithSuccess($data);
}
public function getLehrverband($studiengang_kz, $semester=null, $verband=null)
{
$this->load->model('organisation/Lehrverband_model','LehrverbandModel');
$where = [
'aktiv' => true,
'studiengang_kz' => $studiengang_kz,
];
if ($semester !== null && $semester !== 'null' && $semester !== 'undefined') {
$where['semester'] = $semester;
}
if ($verband !== null && $verband !== 'null' && $verband !== 'undefined') {
$where['verband'] = $verband;
}
$this->LehrverbandModel->addOrder('studiengang_kz');
$this->LehrverbandModel->addOrder('semester');
$this->LehrverbandModel->addOrder('verband');
$this->LehrverbandModel->addOrder('gruppe');
$result = $this->LehrverbandModel->loadWhere($where);
$data = $this->getDataOrTerminateWithError($result);
return $this->terminateWithSuccess($data);
}
/**
* fetch moodle events
*
@@ -338,7 +427,7 @@ class LvPlan extends FHCAPI_Controller
$currentStudiensemester = $this->StudiensemesterModel->getByDate($start_date);
$currentStudiensemester = $this->getDataOrTerminateWithError($currentStudiensemester);
if ($currentStudiensemester) {
$studentsemester_kurzbz = current($currentStudiensemester)->studiensemester_kurzbz;
@@ -347,7 +436,7 @@ class LvPlan extends FHCAPI_Controller
"studiensemester_kurzbz" => $studentsemester_kurzbz
]);
$studiengang = $this->getDataOrTerminateWithError($studiengang);
if ($studiengang)
$studiengang_kz = current($studiengang)->studiengang_kz;
else
@@ -357,7 +446,7 @@ class LvPlan extends FHCAPI_Controller
}
$ferienEvents = $this->stundenplanlib->fetchFerienTageEvents($start_date, $end_date, $studiengang_kz);
return $this->getDataOrTerminateWithError($ferienEvents);
}
}
@@ -626,7 +626,7 @@ class Students extends FHCAPI_Controller
$this->addFilter($studiensemester_kurzbz);
$result = $this->PrestudentModel->loadWhere($where);
$data = $this->getDataOrTerminateWithError($result);
$this->terminateWithSuccess($data);
@@ -851,40 +851,44 @@ class Students extends FHCAPI_Controller
$stdsemEsc = $studiensemester_kurzbz ? $this->PrestudentModel->escape($studiensemester_kurzbz) : 'NULL';
$this->load->config('stv');
$tags = $this->config->item('stv_prestudent_tags');
$whereTags = '';
if (is_array($tags) && !isEmptyArray($tags)) {
$tags = array_keys($tags);
if(defined('STV_TAGS_ENABLED') && STV_TAGS_ENABLED)
{
$tags = $this->config->item('stv_prestudent_tags');
foreach ($tags as $key => $tag) {
$tags[$key] = $this->db->escape($tag);
$whereTags = '';
if (is_array($tags) && !isEmptyArray($tags)) {
$tags = array_keys($tags);
foreach ($tags as $key => $tag) {
$tags[$key] = $this->db->escape($tag);
}
$whereTags = " AND nt.typ_kurzbz IN (" . implode(",", $tags) . ")";
}
$whereTags = " AND nt.typ_kurzbz IN (" . implode(",", $tags) . ")";
$subQueryTag = "
(
SELECT
tag.prestudent_id,
COALESCE(json_agg(tag ORDER BY tag.done), '[]'::json) AS tags
FROM (
SELECT DISTINCT ON (n.notiz_id)
n.notiz_id AS id,
nt.typ_kurzbz,
array_to_json(nt.bezeichnung_mehrsprachig)->>0 AS beschreibung,
n.text AS notiz,
nt.style,
n.erledigt AS done,
nz.prestudent_id
FROM public.tbl_notizzuordnung AS nz
JOIN public.tbl_notiz AS n ON nz.notiz_id = n.notiz_id
JOIN public.tbl_notiz_typ AS nt ON n.typ = nt.typ_kurzbz "
. $whereTags .
"
) AS tag
GROUP BY tag.prestudent_id
) AS tag_data_agg
";
}
$subQueryTag = "
(
SELECT
tag.prestudent_id,
COALESCE(json_agg(tag ORDER BY tag.done), '[]'::json) AS tags
FROM (
SELECT DISTINCT ON (n.notiz_id)
n.notiz_id AS id,
nt.typ_kurzbz,
array_to_json(nt.bezeichnung_mehrsprachig)->>0 AS beschreibung,
n.text AS notiz,
nt.style,
n.erledigt AS done,
nz.prestudent_id
FROM public.tbl_notizzuordnung AS nz
JOIN public.tbl_notiz AS n ON nz.notiz_id = n.notiz_id
JOIN public.tbl_notiz_typ AS nt ON n.typ = nt.typ_kurzbz "
. $whereTags .
"
) AS tag
GROUP BY tag.prestudent_id
) AS tag_data_agg
";
$this->PrestudentModel->addJoin('public.tbl_studiengang stg', 'studiengang_kz', 'LEFT');
$this->PrestudentModel->addJoin('public.tbl_person p', 'person_id');
@@ -907,11 +911,17 @@ class Students extends FHCAPI_Controller
AND ps.studiensemester_kurzbz=public.get_stdsem_prestudent(tbl_prestudent.prestudent_id, ' . $stdsemEsc . ')
AND ps.ausbildungssemester=public.get_absem_prestudent(tbl_prestudent.prestudent_id, ' . $stdsemEsc . ')', 'LEFT');
$this->PrestudentModel->addJoin($subQueryTag, 'tag_data_agg.prestudent_id = tbl_prestudent.prestudent_id', 'LEFT');
if(defined('STV_TAGS_ENABLED') && STV_TAGS_ENABLED)
{
$this->PrestudentModel->addJoin($subQueryTag, 'tag_data_agg.prestudent_id = tbl_prestudent.prestudent_id', 'LEFT');
}
$this->PrestudentModel->addSelect("b.uid");
$this->PrestudentModel->addSelect('tag_data_agg.tags');
if(defined('STV_TAGS_ENABLED') && STV_TAGS_ENABLED)
{
$this->PrestudentModel->addSelect('tag_data_agg.tags');
}
$this->PrestudentModel->addSelect('titelpre');
$this->PrestudentModel->addSelect('nachname');
$this->PrestudentModel->addSelect('vorname');
+13 -42
View File
@@ -13,13 +13,12 @@ class Mylv extends Auth_Controller
*/
public function __construct()
{
parent::__construct([
'Student' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'], // TODO(chris): permissions?
'Studiensemester' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'], // TODO(chris): permissions?
'Lvs' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'], // TODO(chris): permissions?
'Info' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'], // TODO(chris): permissions?
'Pruefungen' => ['student/anrechnung_beantragen:r','user:r', 'basis/cis:r'] // TODO(chris): permissions?
'Student' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions?
'Studiensemester' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions?
'Lvs' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions?
'Info' => ['student/anrechnung_beantragen:r','user:r'], // TODO(chris): permissions?
'Pruefungen' => ['student/anrechnung_beantragen:r','user:r'] // TODO(chris): permissions?
]);
}
@@ -45,27 +44,13 @@ class Mylv extends Auth_Controller
public function Studiensemester()
{
$this->load->model('organisation/Studiensemester_model', 'StudiensemesterModel');
$this->load->model('crm/Student_model', 'StudentModel');
$this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
$isMitarbeiter = getData($this->MitarbeiterModel->isMitarbeiter(getAuthUID())) ?? false;
if($isMitarbeiter) {
$result = $this->StudiensemesterModel->getWhereMitarbeiterHasLvs(getAuthUID());
$result = $this->StudiensemesterModel->getWhereStudentHasLvs(getAuthUID());
if (isError($result))
return $this->outputJsonError(getError($result));
if (isError($result))
return $this->outputJsonError(getError($result));
$this->outputJsonSuccess(getData($result));
} else if(getData($this->StudentModel->isStudent(getAuthUID())) ?? false) { // $isStudent
$result = $this->StudiensemesterModel->getWhereStudentHasLvs(getAuthUID());
if (isError($result))
return $this->outputJsonError(getError($result));
$this->outputJsonSuccess(getData($result));
} else {
$this->outputJsonError('neither student or mitarbeiter');
}
$this->outputJsonSuccess(getData($result));
}
/**
@@ -73,27 +58,13 @@ class Mylv extends Auth_Controller
public function Lvs($studiensemester_kurzbz)
{
$this->load->model('education/Lehrveranstaltung_model', 'LehrveranstaltungModel');
$this->load->model('crm/Student_model', 'StudentModel');
$this->load->model('ressource/Mitarbeiter_model', 'MitarbeiterModel');
$isMitarbeiter = getData($this->MitarbeiterModel->isMitarbeiter(getAuthUID())) ?? false;
if($isMitarbeiter) {
$result = $this->LehrveranstaltungModel->getLvsByMitarbeiterInSemester(getAuthUID(), $studiensemester_kurzbz);
$result = $this->LehrveranstaltungModel->getLvsByStudentWithGrades(getAuthUID(), $studiensemester_kurzbz, getUserLanguage());
if (isError($result))
return $this->outputJsonError(getError($result));
if (isError($result))
return $this->outputJsonError(getError($result));
$this->outputJsonSuccess(getData($result));
} else if(getData($this->StudentModel->isStudent(getAuthUID())) ?? false) { // $isStudent
$result = $this->LehrveranstaltungModel->getLvsByStudentWithGrades(getAuthUID(), $studiensemester_kurzbz, getUserLanguage());
if (isError($result))
return $this->outputJsonError(getError($result));
$this->outputJsonSuccess(getData($result));
} else {
$this->outputJsonError('neither student or mitarbeiter');
}
$this->outputJsonSuccess(getData($result));
}
/**
+18
View File
@@ -445,6 +445,24 @@ class StundenplanLib
return success($ferienEventsFlattened);
}
public function getEventsStgOrg( $start, $end, $stg_kz, $sem, $verband, $gruppe)
{
$this->_ci =& get_instance();
$this->_ci->load->model('ressource/Stundenplan_model', 'StundenplanModel');
$stundenplan_data = $this->_ci->StundenplanModel->getStundenplanStudiengang($start, $end, $stg_kz, $sem, $verband, $gruppe);
if (isError($stundenplan_data))
return $stundenplan_data;
$stundenplan_data = getData($stundenplan_data) ?? [];
$function_error = $this->expandObjectInformation($stundenplan_data);
if ($function_error)
return $function_error;
return success($stundenplan_data);
}
// start of the private functions ########################################################################################################
// function used to sort an array of studiensemester strings
@@ -1343,45 +1343,4 @@ class Lehrveranstaltung_model extends DB_Model
return $this->execQuery($qry, $params);
}
// used for cis4 mylv mitarbeiter
public function getLvsByMitarbeiterInSemester($mitarbeiter_uid, $sem_kurzbz) {
$qry = "SELECT * FROM (
SELECT DISTINCT ON (lehre.tbl_lehrveranstaltung.lehrveranstaltung_id)
public.tbl_studiengang.studiengang_kz,
lehre.tbl_lehrveranstaltung.semester,
public.tbl_studiengang.bezeichnung as sg_bezeichnung,
public.tbl_studiengang.english as sg_bezeichnung_eng,
UPPER(tbl_studiengang.typ::varchar(1) || tbl_studiengang.kurzbz) as studiengang_kuerzel,
lehre.tbl_lehrveranstaltung.lehrveranstaltung_id,
lehre.tbl_lehrveranstaltung.bezeichnung,
lehre.tbl_lehrveranstaltung.bezeichnung_english as bezeichnung_eng,
lehre.tbl_lehrveranstaltung.farbe,
lehre.tbl_lehrveranstaltung.lvinfo,
lehre.tbl_lehrveranstaltung.benotung,
lehre.tbl_lehrveranstaltung.orgform_kurzbz,
lehre.tbl_lehrveranstaltung.sprache,
lehre.tbl_lehrveranstaltung.ects,
lehre.tbl_lehrveranstaltung.incoming
FROM
lehre.tbl_lehreinheit JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id)
JOIN lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id)
JOIN public.tbl_studiengang USING(studiengang_kz)
JOIN lehre.tbl_lehrveranstaltung as lehrfach ON(tbl_lehreinheit.lehrfach_id=lehrfach.lehrveranstaltung_id)
WHERE
tbl_lehreinheit.studiensemester_kurzbz = ?
AND mitarbeiter_uid = ?) as distincted_by_lva_id
JOIN (
SELECT lehrveranstaltung_id, TRUNC(SUM(lehre.tbl_lehreinheitmitarbeiter.semesterstunden)) as semesterstunden
FROM lehre.tbl_lehreinheit
JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id)
JOIN lehre.tbl_lehrveranstaltung USING(lehrveranstaltung_id)
WHERE tbl_lehreinheit.studiensemester_kurzbz = ?
AND mitarbeiter_uid = ?
GROUP BY lehrveranstaltung_id
) semesterstundenAggregatedSubquery USING(lehrveranstaltung_id)
ORDER BY studiengang_kuerzel, semester, bezeichnung";
return $this->execReadOnlyQuery($qry, [$sem_kurzbz, $mitarbeiter_uid, $sem_kurzbz, $mitarbeiter_uid]);
}
}
@@ -242,30 +242,6 @@ class Studiensemester_model extends DB_Model
return $this->loadWhere(['uid' => $student_uid, 'v.lehre' => true]);
}
public function getWhereMitarbeiterHasLvs($uid) {
// first order by year with last 2 letter from right,
// then order by WS/SS inside the years
// query it asc so the ordering magic in cis4 turns it around again
$qry = "WITH unique_semesters AS (
SELECT DISTINCT ON (studiensemester_kurzbz)
studiensemester_kurzbz,
start,
ende,
bezeichnung,
studienjahr_kurzbz
FROM lehre.tbl_lehreinheit
JOIN lehre.tbl_lehreinheitmitarbeiter USING(lehreinheit_id)
JOIN public.tbl_studiensemester USING(studiensemester_kurzbz)
WHERE mitarbeiter_uid = ?
)
SELECT * FROM unique_semesters
ORDER BY
RIGHT(studiensemester_kurzbz, 2) ASC,
LEFT(studiensemester_kurzbz, 2) ASC;";
return $this->execReadOnlyQuery($qry, [$uid]);
}
public function getAktAndFutureSemester()
{
$query = 'SELECT studiensemester_kurzbz
@@ -388,6 +388,84 @@ class Stundenplan_model extends DB_Model
ORDER BY datum, beginn", [$start_date, $end_date, $ma_uid]);
}
/**
* queries Stundenplan and filters by studiengang, semester, verband gruppe
*
* @return void
*/
public function getStundenplanStudiengang($start_date, $end_date, $stg_kz, $sem, $verband, $gruppe) {
$qry_params = [$start_date, $end_date, $stg_kz];
$qry = "
SELECT
'lehreinheit' as type, beginn, ende, datum,
CONCAT(lehrfach,'-',lehrform) as topic,
array_agg(DISTINCT lektor) as lektor,
array_agg(DISTINCT (gruppe,verband,semester,studiengang_kz,gruppen_kuerzel)) as gruppe,
string_agg(DISTINCT ort_kurzbz, '/') as ort_kurzbz,
array_agg(DISTINCT lehreinheit_id) as lehreinheit_id,
titel, lehrfach, lehrform, lehrfach_bez, organisationseinheit, farbe, lehrveranstaltung_id
FROM
(
SELECT unr,datum,beginn, ende,
CASE
WHEN sp.mitarbeiter_kurzbz IS NOT NULL THEN sp.mitarbeiter_kurzbz
ELSE sp.lektor
END as lektor,
CASE
WHEN gruppe_kurzbz IS NOT NULL THEN gruppe_kurzbz
ELSE CONCAT(UPPER(sp.stg_typ),UPPER(sp.stg_kurzbz),'-',COALESCE(CAST(sp.semester AS varchar),'/'),COALESCE(CAST(sp.verband AS varchar),'/'))
END as gruppen_kuerzel,
(SELECT bezeichnung
FROM public.tbl_organisationseinheit
WHERE oe_kurzbz IN(
SELECT oe_kurzbz
FROM lehre.tbl_lehrveranstaltung
WHERE lehrveranstaltung_id = sp.lehrveranstaltung_id
)) as organisationseinheit,
sp.ort_kurzbz, sp.studiengang_kz, sp.titel,sp.lehreinheit_id,sp.lehrfach_id,sp.anmerkung,fix,lehrveranstaltung_id,stg_kurzbzlang,stg_bezeichnung,stg_typ,fachbereich_kurzbz,lehrfach,lehrfach_bez,farbe,lehrform,anmerkung_lehreinheit,gruppe, verband, semester,stg_kurzbz
FROM (
SELECT sp.*
FROM lehre.vw_stundenplan sp
WHERE
sp.datum >= ?
AND sp.datum <= ?
) sp
JOIN lehre.tbl_stunde ON lehre.tbl_stunde.stunde = sp.stunde
WHERE studiengang_kz = ? ";
if($sem != NULL)
{
$qry_params[] = $sem;
$qry .= " AND (semester = ? OR semester IS NULL)";
}
if($verband != NULL)
{
$qry_params[] = $verband;
$qry .= " AND (verband = ? OR verband IS NULL OR verband = '0' OR verband = '')";
}
if($gruppe != NULL)
{
$qry_params[] = $gruppe;
$qry .= " AND (gruppe = ? OR gruppe IS NULL OR gruppe = '0' OR gruppe = '') ";
}
$qry.= " AND (
gruppe_kurzbz is null OR EXISTS(
SELECT 1
FROM
public.tbl_gruppe WHERE gruppe_kurzbz = sp.gruppe_kurzbz AND direktinskription = false
)
)";
$qry.= " ) as subquery
GROUP BY unr, datum, beginn, ende, titel, lehrform, lehrfach, lehrfach_bez, organisationseinheit, farbe, lehrveranstaltung_id
ORDER BY datum, beginn; ";
return $this->execReadOnlyQuery($qry, $qry_params);
}
/**
* NO STANDALONE FUNCTION - Generates a SQL query string to fetch 'stundenplan' events for a specific student within the current semester.
+1
View File
@@ -2,6 +2,7 @@
@import './SvgIcons.css';
@import './components/searchbar/searchbar.css';
@import './components/verticalsplit.css';
@import './components/horizontalsplit.css';
@import './components/FilterComponent.css';
@import './components/Tabs.css';
@import './components/Notiz.css';
-10
View File
@@ -6,13 +6,3 @@
color: var(--fhc-myLv-disabled) !important;
cursor: default;
}
/* adjustment to have bs5 dropdownmenus rendered properly over a tabulator table */
.mylv-semester-table .tabulator-cell {
overflow: unset;
}
.mylv-semester-table .tabulator-cell .action-col {
/*min-height: 2.5rem;*/
align-items: flex-start; /* so wrapped rows don't stretch vertically */
}
+73
View File
@@ -0,0 +1,73 @@
:root {
--fhc-horizontalsplit-hsplitter-bg-color: var(--fhc-background, #eee);
--fhc-horizontalsplit-hsplitter-border-color: var(--fhc-border, #eee);
--fhc-horizontalsplit-hsplitter-splitactions-color: var(--fhc-dark, #000);
}
.horizontalsplit-container {
display: flex;
flex-direction: row;
overflow: hidden;
max-height: 100%;
padding: 0px;
}
.horizontalsplitted {
overflow: auto;
flex-shrink: 0;
}
.horizontalsplitter {
flex-shrink: 0;
width: 8px;
cursor: col-resize;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
}
.horizontalsplitter.left {
border-left: solid 3px var(--fhc-horizontalsplit-hsplitter-border-color);
}
.horizontalsplitter.right {
border-right: solid 3px var(--fhc-horizontalsplit-hsplitter-border-color);
}
.splitactions.horizontal {
background-color: var(--fhc-horizontalsplit-hsplitter-bg-color);
color: var(--fhc-horizontalsplit-hsplitter-splitactions-color);
display: flex;
flex-direction: column;
padding: 5px 0 5px 0;
}
.splitactions.horizontal.left {
border-radius: 0 40% 40% 0;
}
.splitactions.horizontal.right {
border-radius: 40% 0 0 40%;
}
.splitactions.horizontal .splitaction {
width: auto;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
}
.splitactions.horizontal .splitaction.resize {
cursor: col-resize;
}
#content {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
#content > div:first-child {
margin-top: 30px;
}
-15
View File
@@ -21,20 +21,5 @@ export default {
method: 'get',
url: `/api/frontend/v1/LvMenu/getLvMenu/${lvid}/${studiensemester_kurzbz}`
};
},
getMultipleLvMenu(lvas, studiensemester_kurzbz) {
// format params for backend bulk function
const lvMenuOptionList = lvas.map(lva => {
return {
lvid: lva.lehrveranstaltung_id,
studiensemester_kurzbz
}
})
return {
method: 'post',
url: `/api/frontend/v1/LvMenu/getMultipleLvMenu`,
params: { lvMenuOptionList }
};
}
};
+27 -1
View File
@@ -92,5 +92,31 @@ export default {
method: 'get',
url: '/api/frontend/v1/LvPlan/getLv/' + lehrveranstaltung_id
};
}
},
eventsStgOrg(start_date, end_date, stg_kz, sem, verband, gruppe) {
return {
method: 'post',
url: '/api/frontend/v1/lvPlan/eventsStgOrg',
params: { start_date, end_date, stg_kz, sem, verband, gruppe }
};
},
getStudiengaenge(){
return {
method: 'get',
url: '/api/frontend/v1/lvPlan/getStudiengaenge'
}
},
getLehrverband(stg_kz, sem){
return {
method: 'get',
url: `/api/frontend/v1/lvPlan/getLehrverband/${stg_kz}/${sem}`
}
},
getGruppe(stg_kz, sem, verband){
return {
method: 'get',
url: `/api/frontend/v1/lvPlan/getLehrverband/${stg_kz}/${sem}/${verband}`
}
},
};
+25 -18
View File
@@ -5,7 +5,7 @@ import contrast from '../../directives/contrast.js';
import {setScrollbarWidth} from "../../helpers/CssVarCalcHelpers.js";
import LvPlan from "../../components/Cis/LvPlan/Lehrveranstaltung.js";
import MyLvPlan from "../../components/Cis/LvPlan/Personal.js";
import Mylv from "../../components/Cis/Mylv/MyLv.js";
import MylvStudent from "../../components/Cis/Mylv/Student.js";
import Profil from "../../components/Cis/Profil/Profil.js";
import Raumsuche from "../../components/Cis/Raumsuche/Raumsuche.js";
import CmsNews from "../../components/Cis/Cms/News.js";
@@ -17,11 +17,12 @@ import AbgabetoolMitarbeiter from "../../components/Cis/Abgabetool/AbgabetoolMit
import AbgabetoolAssistenz from "../../components/Cis/Abgabetool/AbgabetoolAssistenz.js";
import DeadlineOverview from "../../components/Cis/Abgabetool/DeadlineOverview.js";
import Studium from "../../components/Cis/Studium/Studium.js";
import StgOrgLvPlan from "../../components/Cis/LvPlan/StgOrg.js";
import OverviewLvPlan from "../../components/Cis/LvPlan/OverviewLvPlan.js";
import ApiRenderers from '../../api/factory/renderers.js';
import ApiRouteInfo from '../../api/factory/routeinfo.js';
import {capitalize} from "../../helpers/StringHelpers.js";
import ApiAuthinfo from "../../api/factory/authinfo.js";
const ciPath = FHC_JS_DATA_STORAGE_OBJECT.app_root.replace(/(https:|)(^|\/\/)(.*?\/)/g, '') + FHC_JS_DATA_STORAGE_OBJECT.ci_router;
@@ -150,7 +151,7 @@ const router = VueRouter.createRouter({
{
path: `/Cis/MyLv/:studiensemester?`,
name: 'MyLv',
component: Mylv,
component: MylvStudent,
props: true,
},
{
@@ -198,6 +199,26 @@ const router = VueRouter.createRouter({
};
}
},
{
path: `/Cis/StgOrgLvPlan/:mode?/:focus_date?/:stgkz?/:sem?/:verband?/:gruppe?`,
name: 'StgOrgLvPlan',
component: StgOrgLvPlan,
props(route) {
return {
propsViewData: route.params
};
}
},
{
path: `/Cis/OverviewLvPlan`,
name: 'OverviewLvPlan',
component: OverviewLvPlan,
props(route) {
return {
propsViewData: route.params
};
}
},
{
path: `/Cis4`,
name: 'Cis4',
@@ -232,9 +253,6 @@ const app = Vue.createApp({
data: () => ({
appSideMenuEntries: {},
renderers: null,
uid: '',
isStudent: null,
isMitarbeiter: null
}),
components: {},
computed: {
@@ -248,10 +266,7 @@ const app = Vue.createApp({
return { // provide injectable & watchable language property
language: Vue.computed(() => this.$p.user_language),
renderers: Vue.computed(() => this.renderers),
isMobile: this.isMobile,
uid: Vue.computed(() => this.uid),
isStudent: Vue.computed(() => this.isStudent),
isMitarbeiter: Vue.computed(() => this.isMitarbeiter)
isMobile: this.isMobile
}
},
methods: {
@@ -328,14 +343,6 @@ const app = Vue.createApp({
this.renderers[rendertype].calendarEvent = calendarEvent;
}
});
await this.$api
.call(ApiAuthinfo.getAuthInfo())
.then(res => {
this.uid = res.data.uid;
this.isMitarbeiter = res.data.isMitarbeiter;
this.isStudent = res.data.isStudent;
});
},
mounted() {
document.addEventListener('click', this.handleClick);
+6 -2
View File
@@ -88,12 +88,15 @@ export default {
updateRange(rangeInterval) {
this.rangeInterval = rangeInterval;
this.$emit('update:range', rangeInterval);
},
resetEventLoader() {
this.reset();
}
},
setup(props, context) {
const rangeInterval = Vue.ref(null);
const { events, lv } = useEventLoader(rangeInterval, props.getPromiseFunc);
const { events, lv, reset } = useEventLoader(rangeInterval, props.getPromiseFunc);
Vue.watch(lv, newValue => {
context.emit('update:lv', newValue);
@@ -102,7 +105,8 @@ export default {
return {
rangeInterval,
events,
lv
lv,
reset
};
},
created() {
@@ -0,0 +1,194 @@
import FormForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import ApiLvPlan from '../../../api/factory/lvPlan.js';
export default {
name: "OverviewLvPlan",
components: {
FormForm,
FormInput,
},
props: {
viewData: Object,
propsViewData: Object
},
data() {
return {
formData: {
stgkz: null,
sem: null,
verband: null,
gruppe: null,
},
listStg: [],
listSem: [1,2,3,4,5,6,7,8,9,10],
listVerband: [],
listGroup: [],
};
},
methods: {
loadLvPlan() {
if (!this.formData.stgkz) {
this.$fhcAlert.alertError(this.$p.t('LvPlan', 'chooseStg'));
return;
}
if (!this.formData.sem && (this.formData.verband || this.formData.gruppe)) {
this.$fhcAlert.alertError(this.$p.t('LvPlan', 'error_SemMissing'));
return;
}
if (!this.formData.verband && this.formData.gruppe) {
this.$fhcAlert.alertError(this.$p.t('LvPlan', 'error_VerbandMissing'));
return;
}
const params = {
mode: this.currentMode,
focus_date: this.currentDay,
stgkz: this.formData.stgkz,
sem: this.formData.sem,
verband: this.formData.verband,
gruppe: this.formData.gruppe,
};
//ensure logic: no value after a null value in route
if (params.sem == null) {
params.verband = null;
params.gruppe = null;
}
if (params.verband == null) {
params.gruppe = null;
}
//delete all null values to avoid null in router
Object.keys(params).forEach(
key => params[key] == null && delete params[key]
);
this.$router.push({
name: "StgOrgLvPlan",
params,
});
},
loadListSem(){
this.listSem = [...Array(this.maxSemester).keys()].map(i => i + 1);
},
loadListVerband(){
this.$api
.call(ApiLvPlan.getLehrverband(this.formData.stgkz, this.formData.sem, this.formData.verband))
.then(result => {
const data = result.data;
const mappedData = data.map(item => item.verband);
this.listVerband = [...new Set(mappedData.filter(v =>
v !== null &&
v !== undefined &&
String(v).trim() !== ""
))]
.sort();
})
.catch(this.$fhcAlert.handleSystemError);
},
loadListGroup(){
this.$api
.call(ApiLvPlan.getGruppe(this.formData.stgkz, this.formData.sem, this.formData.verband))
.then(result => {
const data = result.data;
const mappedData = data.map(item => item.gruppe);
this.listGroup = [...new Set(mappedData.filter(v =>
v !== null &&
v !== undefined &&
String(v).trim() !== ""))]
.sort();
})
.catch(this.$fhcAlert.handleSystemError);
}
},
computed: {
maxSemester(){
const currentStg = this.listStg.find(
item => item.studiengang_kz === this.formData.stgkz
);
return currentStg.max_semester;
},
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
return this.propsViewData?.focus_date;
},
},
created(){
this.$api
.call(ApiLvPlan.getStudiengaenge())
.then(result => {
this. listStg = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
},
template: `
<div cis-lvplan-stg-org-ues d-flex flex-column h-100>
<div class="mt-3">
<form-form class="row row-cols-1 row-cols-md-2 row-cols-lg-4 row-cols-xl-5 g-3 mb-3">
<form-input
type="select"
v-model="formData.stgkz"
@change="loadListSem(formData.stgkz)"
>
<option :value="null" selected>{{ $p.t('LvPlan/chooseStg') }}</option>
<option
v-for="stg in listStg"
:key="stg.studiengang_kz"
:value="stg.studiengang_kz"
>
{{ stg.kurzbzlang }} ({{ stg.bezeichnung }})
</option>
</form-input>
<form-input
type="select"
v-model="formData.sem"
@change="loadListVerband()"
@click="loadListSem(formData.stgkz)"
>
<option :value="null" selected>Semester</option>
<option
v-for="sem in listSem"
:key="sem"
:value="sem"
>
{{ sem }}
</option>
</form-input>
<form-input
type="select"
v-model="formData.verband"
@change="loadListGroup()"
>
<option :value="null" selected>{{ $p.t('lehre/verband') }} </option>
<option
v-for="verband in listVerband"
:key="verband"
:value="verband"
>
{{ verband }}
</option>
</form-input>
<form-input
type="select"
v-model="formData.gruppe"
>
<option :value="null" selected>{{ $p.t('gruppenmanagement/gruppe') }}</option>
<option
v-for="group in listGroup"
:key="group"
:value="group"
>
{{ group }}
</option>
</form-input>
<button type="button" class="btn btn-secondary" @click="loadLvPlan">{{ $p.t('LvPlan/loadLvPlan') }}</button>
</form-form>
</div>
</div>
`,
};
+336
View File
@@ -0,0 +1,336 @@
import FormForm from '../../Form/Form.js';
import FormInput from '../../Form/Input.js';
import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../.././../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN = 'Week';
export default {
name: 'LvPlanStgOrg',
components: {
FormForm,
FormInput,
FhcCalendar,
},
props: {
viewData: Object,
propsViewData: Object
},
data() {
return {
localProps: {},
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
uid: null,
isMitarbeiter: false,
isStudent: false,
currentStgBezeichnung: null,
formData: {
stgkz: null,
sem: null,
verband: null,
gruppe: null,
},
listStg: [],
listSem: [1,2,3,4,5,6,7,8,9,10],
listVerband: [],
listGroup: [],
rangeIntervalFirst: null
};
},
computed: {
maxSemester(){
const currentStg = this.listStg.find(
item => item.studiengang_kz === this.formData.stgkz
);
return currentStg.max_semester;
},
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
return this.propsViewData?.focus_date;
},
currentMode() {
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
return DEFAULT_MODE_LVPLAN;
return this.propsViewData?.mode;
},
downloadLinks() {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
return false;
let type = false;
type = this.isStudent ? 'student' : type;
type = this.isMitarbeiter ? 'lektor' : type;
if (false === type)
{
return;
}
const opts = { zone: this.viewData.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.toUnixInteger();
const ende = luxon.DateTime
.fromISO(this.studiensemester_ende, opts)
.toUnixInteger();
const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'cis/private/lvplan/stpl_kalender.php'
+ '?type=' + type
+ '&pers_uid=' + this.uid
+ '&begin=' + start
+ '&ende=' + ende;
return [
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link + '&format=excel' },
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link + '&format=csv' },
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' },
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' }
];
}
},
methods: {
loadLvPlan(){
if(!this.formData.stgkz){
this.$fhcAlert.alertError(this.$p.t('LvPlan', 'chooseStg'));
return;
}
if(!this.formData.sem && (this.formData.verband || this.formData.gruppe)){
this.$fhcAlert.alertError(this.$p.t('LvPlan', 'error_SemMissing'));
return;
}
if(!this.formData.verband && this.formData.gruppe){
this.$fhcAlert.alertError(this.$p.t('LvPlan', 'error_VerbandMissing'));
return;
}
const params = {
mode: this.currentMode,
focus_date: this.currentDay,
stgkz: this.formData.stgkz,
sem: this.formData.sem,
verband: this.formData.verband,
gruppe: this.formData.gruppe,
};
//ensure logic: no value after a null value in route
if(params.sem == null)
{
params.verband = null;
params.gruppe = null;
}
if(params.verband == null) {
params.gruppe = null;
}
//delete all null values to avoid null in router
Object.keys(params).forEach(
key => params[key] == null && delete params[key]
);
this.$router.push({
name: "StgOrgLvPlan",
params,
});
this.$refs['calendar'].resetEventLoader();
},
loadListSem(){
this.listSem = [...Array(this.maxSemester).keys()].map(i => i + 1);
},
loadListVerband(){
this.$api
.call(ApiLvPlan.getLehrverband(this.formData.stgkz, this.formData.semester, this.formData.verband))
.then(result => {
const data = result.data;
const mappedData = data.map(item => item.verband);
this.listVerband = [...new Set(mappedData.filter(v =>
v !== null &&
v !== undefined &&
String(v).trim() !== ""
))]
.sort();
})
.catch(this.$fhcAlert.handleSystemError);
},
loadListGroup(){
this.$api
.call(ApiLvPlan.getGruppe(this.formData.stgkz, this.formData.semester, this.formData.verband))
.then(result => {
const data = result.data;
const mappedData = data.map(item => item.gruppe);
this.listGroup = [...new Set(mappedData.filter(v =>
v !== null &&
v !== undefined &&
String(v).trim() !== ""))]
.sort();
})
.catch(this.$fhcAlert.handleSystemError);
},
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
},
handleChangeMode(newMode, day) {
const mode = newMode[0].toUpperCase() + newMode.slice(1);
const focus_date = day.toISODate();
this.$router.push({
name: "StgOrgLvPlan",
params: {
mode,
focus_date,
stgkz: this.formData.stgkz,
sem: this.formData.sem,
verband: this.formData.verband,
gruppe: this.formData.gruppe,
},
});
},
updateRange(rangeInterval) {
this.$api
.call(ApiLvPlan.studiensemesterDateInterval(
rangeInterval.end.startOf('week').toISODate()
))
.then(res => {
this.studiensemester_kurzbz = res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende;
});
},
getPromiseFunc(start, end) {
return [
this.$api.call(ApiLvPlan.eventsStgOrg(start, end, this.formData.stgkz, this.formData.sem, this.formData.verband, this.formData.gruppe))
];
},
},
created(){
this.$api
.call(ApiAuthinfo.getAuthInfo())
.then(res => {
this.uid = res.data.uid;
this.isMitarbeiter = res.data.isMitarbeiter;
this.isStudent = res.data.isStudent;
});
this.$api
.call(ApiLvPlan.getStudiengaenge())
.then(result => {
this. listStg = result.data;
})
.catch(this.$fhcAlert.handleSystemError);
if(this.propsViewData) {
this.formData.stgkz = this.propsViewData.stgkz ? this.propsViewData.stgkz: null;
this.formData.sem = this.propsViewData.sem ? this.propsViewData.sem: null;
this.formData.verband = this.propsViewData.verband ? this.propsViewData.verband: null;
this.formData.gruppe = this.propsViewData.gruppe ? this.propsViewData.gruppe: null;
}
},
template: `
<div class="cis-lvplan-stg-org d-flex flex-column h-100">
<div class="mt-3">
<form-form class="row row-cols-1 row-cols-md-2 row-cols-lg-4 row-cols-xl-5 g-3 mb-3">
<form-input
type="select"
v-model="formData.stgkz"
@change="loadListSem(formData.stgkz)"
>
<option :value="null" selected>{{ $p.t('LvPlan/chooseStg') }}</option>
<option
v-for="stg in listStg"
:key="stg.studiengang_kz"
:value="stg.studiengang_kz"
>
{{ stg.kurzbzlang }} ({{ stg.bezeichnung }})
</option>
</form-input>
<form-input
type="select"
v-model="formData.sem"
@change="loadListVerband()"
@click="loadListSem(formData.stgkz)"
>
<option :value="null" selected>Semester</option>
<option
v-for="sem in listSem"
:key="sem"
:value="sem"
>
{{ sem }}
</option>
</form-input>
<form-input
type="select"
v-model="formData.verband"
@change="loadListGroup()"
>
<option :value="null" selected>{{ $p.t('lehre/verband') }} </option>
<option
v-for="verband in listVerband"
:key="verband"
:value="verband"
>
{{ verband }}
</option>
</form-input>
<form-input
type="select"
v-model="formData.gruppe"
>
<option :value="null" selected>{{ $p.t('gruppenmanagement/gruppe') }}</option>
<option
v-for="group in listGroup"
:key="group"
:value="group"
>
{{ group }}
</option>
</form-input>
<button type="button" class="btn btn-secondary" @click="loadLvPlan">{{ $p.t('LvPlan/loadLvPlan') }}</button>
</form-form>
</div>
<fhc-calendar
v-show="propsViewData && propsViewData.stgkz"
ref="calendar"
v-model:lv="formData"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@update:range="updateRange"
class="responsive-calendar"
>
<div
v-if="downloadLinks"
class="d-flex gap-1 justify-items-start"
>
<div v-for="{ title, icon, link } in downloadLinks">
<a
:href="link"
:aria-label="title"
class="py-1 btn btn-outline-secondary"
>
<div class="d-flex flex-column">
<i aria-hidden="true" :class="icon"></i>
<span style="font-size:.5rem">{{ title }}</span>
</div>
</a>
</div>
</div>
</fhc-calendar>
</div>
`,
};
@@ -0,0 +1,187 @@
import FhcCalendar from "../../Calendar/LvPlan.js";
import ApiLvPlan from '../.././../api/factory/lvPlan.js';
import ApiAuthinfo from '../../../api/factory/authinfo.js';
export const DEFAULT_MODE_LVPLAN = 'Week';
export default {
name: 'LvPlanStgOrg',
inject: {
cisRoot: {
from: 'cisRoot'
},
},
components: {
FhcCalendar,
},
props: {
viewData: Object,
propsViewData: Object
},
data() {
return {
studiensemester_kurzbz: null,
studiensemester_start: null,
studiensemester_ende: null,
uid: null,
isMitarbeiter: false,
isStudent: false,
listStg: [],
currentStgBezeichnung: null
};
},
computed:{
currentDay() {
if (!this.propsViewData?.focus_date || isNaN(new Date(this.propsViewData?.focus_date)))
return luxon.DateTime.now().setZone(this.viewData.timezone).toISODate();
return this.propsViewData?.focus_date;
},
currentMode() {
if (!this.propsViewData?.mode || !['day', 'week', 'month'].includes(this.propsViewData?.mode.toLowerCase()))
return DEFAULT_MODE_LVPLAN;
return this.propsViewData?.mode;
},
downloadLinks() {
if (!this.studiensemester_start || !this.studiensemester_ende || !this.uid)
return false;
let type = false;
type = this.isStudent ? 'student' : type;
type = this.isMitarbeiter ? 'lektor' : type;
if (false === type)
{
return;
}
const opts = { zone: this.viewData.timezone };
const start = luxon.DateTime
.fromISO(this.studiensemester_start, opts)
.toUnixInteger();
const ende = luxon.DateTime
.fromISO(this.studiensemester_ende, opts)
.toUnixInteger();
const download_link = FHC_JS_DATA_STORAGE_OBJECT.app_root
+ 'cis/private/lvplan/stpl_kalender.php'
+ '?type=' + type
+ '&pers_uid=' + this.uid
+ '&begin=' + start
+ '&ende=' + ende;
return [
{ title: "excel", icon: 'fa-solid fa-file-excel', link: download_link + '&format=excel' },
{ title: "csv", icon: 'fa-solid fa-file-csv', link: download_link + '&format=csv' },
{ title: "ical1", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=1&target=ical' },
{ title: "ical2", icon: 'fa-regular fa-calendar', link: download_link + '&format=ical&version=2&target=ical' }
];
}
},
methods: {
handleChangeDate(day, newMode) {
return this.handleChangeMode(newMode, day);
},
handleChangeMode(newMode, day) {
const mode = newMode[0].toUpperCase() + newMode.slice(1)
const focus_date = day.toISODate();
this.$router.push({
name: "StgOrgLvPlan",
params: {
mode,
focus_date,
stgkz: this.propsViewData.stgkz,
sem: this.propsViewData.sem,
verband: this.propsViewData.verband,
gruppe: this.propsViewData.gruppe,
}
});
},
updateRange(rangeInterval) {
this.$api
.call(ApiLvPlan.studiensemesterDateInterval(
rangeInterval.end.startOf('week').toISODate()
))
.then(res => {
this.studiensemester_kurzbz = res.data.studiensemester_kurzbz;
this.studiensemester_start = res.data.start;
this.studiensemester_ende = res.data.ende;
});
},
getPromiseFunc(start, end) {
return [
this.$api.call(ApiLvPlan.eventsStgOrg(start.toISODate(), end.toISODate(), this.propsViewData.stgkz, this.propsViewData.sem, this.propsViewData.verband, this.propsViewData.gruppe)),
//local for test
/* this.$api.call(ApiLvPlan.eventsStgOrg(start.toISODate(), end.toISODate(), this.stgkz, this.sem, this.verband, this.gruppe))*/
];
},
backToDropdown(){
this.$router.push({
name: "OverviewLvPlan",
});
}
},
created(){
this.$api
.call(ApiAuthinfo.getAuthInfo())
.then(res => {
this.uid = res.data.uid;
this.isMitarbeiter = res.data.isMitarbeiter;
this.isStudent = res.data.isStudent;
});
if(this.propsViewData.stgkz) {
this.$api
.call(ApiLvPlan.getStudiengaenge())
.then(result => {
const currentStg = result.data.find(
item => item.studiengang_kz == this.propsViewData.stgkz
);
this.currentStgBezeichnung = currentStg.kurzbzlang + " - " + currentStg.bezeichnung;
})
.catch(this.$fhcAlert.handleSystemError);
}
},
template: `
<div class="cis-lvplan-stg-org d-flex flex-column h-100">
<h2>{{ $p.t('LvPlan/headerLvPlanLvVerband') }}</h2>
<p v-if="propsViewData.stgkz"><span><strong>{{currentStgBezeichnung}}</strong></span>
<span v-if="propsViewData.sem" style="margin-left:10px;"> Semester: {{propsViewData.sem}} </span>
<span v-if="propsViewData.verband" style="margin-left:10px;"> Verband: {{propsViewData.verband}} </span>
<span v-if="propsViewData.gruppe" style="margin-left:10px;"> Gruppe: {{propsViewData.gruppe}} </span>
<button class="py-1 btn btn-outline-secondary" :title="this.$p.t('LvPlan', 'backToDropdown')" style="margin-left:10px;"> <i class="fa fa-arrow-left fa-xs" @click="backToDropdown"></i></button>
</p>
<p v-else>{{ $p.t('LvPlan/noStgProvided') }}</p>
<fhc-calendar
ref="calendar"
:timezone="viewData.timezone"
:get-promise-func="getPromiseFunc"
:date="currentDay"
:mode="currentMode"
@update:date="handleChangeDate"
@update:mode="handleChangeMode"
@update:range="updateRange"
class="responsive-calendar"
>
<div
v-if="downloadLinks"
class="d-flex gap-1 justify-items-start"
>
<div v-for="{ title, icon, link } in downloadLinks">
<a
:href="link"
:aria-label="title"
class="py-1 btn btn-outline-secondary"
>
<div class="d-flex flex-column">
<i aria-hidden="true" :class="icon"></i>
<span style="font-size:.5rem">{{ title }}</span>
</div>
</a>
</div>
</div>
</fhc-calendar>
</div>
`,
};
-1
View File
@@ -1,6 +1,5 @@
export default {
name: 'LvMenu',
props:{
menu:{
type:Array,
@@ -4,7 +4,7 @@ import LvMenu from "./LvMenu.js";
import ApiAddons from '../../../api/factory/addons.js';
export default {
name: 'LvUebersicht',
props:{
event:{
type:Object,
+1 -3
View File
@@ -1,7 +1,6 @@
import MylvSemesterStudiengang from "./Semester/Studiengang.js";
export default {
name: 'Semester',
components: {
MylvSemesterStudiengang
},
@@ -37,8 +36,7 @@ export default {
return this.lvs.filter(lv => lv.studiengang_kz == studiengang.studiengang_kz && lv.semester == studiengang.semester);
}
},
template: `
<div class="mylv-semester" v-if="ready">
template: `<div class="mylv-semester" v-if="ready">
<mylv-semester-studiengang v-for="studiengang in studiengaenge" :key="studiengang.studiengang_kz" v-bind="studiengang" :lvs="lvsForStudiengang(studiengang)"/>
</div>
<div class="mylv-semester text-center" v-else>
@@ -2,7 +2,6 @@ import MylvSemesterStudiengangLv from "./Studiengang/Lv.js";
import Phrasen from "../../../../mixins/Phrasen.js";
export default {
name: 'Studiengang',
components: {
MylvSemesterStudiengangLv
},
@@ -32,8 +31,7 @@ export default {
return lv.benotung ? lv.znote || lv.lvnote || null : null;
}
},
template: `
<div class="card mb-3">
template: `<div class="card mb-3">
<div class="card-body">
<h4 class="card-title mb-3">{{$p.user_language.value === 'English' ? sg_bezeichnung_eng : bezeichnung}} - {{kuerzel}}
<small>{{semester}}.{{$p.t('lehre/semester')}}</small>
@@ -1,4 +1,7 @@
import LvPruefungen from "./Lv/Pruefungen.js";
import LvInfo from "./Lv/Info.js";
import Phrasen from "../../../../../mixins/Phrasen.js";
import LvUebersicht from "../../LvUebersicht.js";
import ApiLehre from '../../../../../api/factory/lehre.js';
import ApiAddons from '../../../../../api/factory/addons.js';
@@ -6,11 +9,15 @@ import ApiAddons from '../../../../../api/factory/addons.js';
// TODO(chris): L10n
export default {
name: 'Lv',
inject: ['studien_semester', 'type'],
components:{
LvUebersicht,
},
mixins: [
Phrasen
],
inject: ['studien_semester'],
props: {
lehrveranstaltung_id: [Number, String],
semesterstunden: [Number, String],
lehrveranstaltung_id: Number,
bezeichnung: String,
bezeichnung_eng: String,
module: String,
@@ -28,13 +35,13 @@ export default {
ects: String,
incoming: Number,
positiv: Boolean,
note_index: String,
menu: [Array, String]
note_index: String
},
data: () => {
return {
pruefungenData: null,
info: null,
menu: null,
preselectedMenuItem: null,
}
},
@@ -58,6 +65,12 @@ export default {
emptyMenu(){
return !this.menu || !Array.isArray(this.menu) || Array.isArray(this.menu) && this.menu.length == 0;
},
bodyStyle() {return {};
/*const bodyStyle = {};
if (this.farbe)
bodyStyle['background-color'] = '#' + this.farbe;
return bodyStyle;*/
},
grade() {
const languageIndex = this.$p.user_language.value === 'English' ? 1 : 0
if(this.benotung && this.znotebez?.length) {
@@ -71,6 +84,7 @@ export default {
},
},
methods: {
fetchMenu(lehrveranstaltung_id = this.lehrveranstaltung_id, studien_semester = this.studien_semester) {
return this.$api
.call(ApiAddons.getLvMenu(lehrveranstaltung_id, studien_semester))
@@ -82,18 +96,28 @@ export default {
this.menu = [];
});
},
c4_target(menuItem) {
if (menuItem.c4_moodle_links?.length > 0) return null;
return menuItem.c4_target ?? null;
},
c4_link(menuItem) {
if (!menuItem) return null;
if (Array.isArray(menuItem.c4_moodle_links) && menuItem.c4_moodle_links.length) {
return '#';
} else {
}
else {
return menuItem.c4_link ?? null;
}
},
openLvOption(menuItem){
if (menuItem.id == "core_menu_mailanstudierende"){
window.location.href = menuItem.c4_link;
} else if (menuItem.id == "core_menu_digitale_anwesenheitslisten") {
window.location.href = menuItem.c4_link;
} else{
this.preselectedMenuItem = menuItem;
Vue.nextTick(() => {
this.$refs.lvUebersicht.show();
});
}
},
openPruefungen() {
// early return if the pruefungenData is empty or not set
if (!this.LvHasPruefungenInformation) return;
@@ -102,65 +126,75 @@ export default {
pruefungenData: this.pruefungenData,
bezeichnung: this.bezeichnung
});
},
openInfos() {
if (!this.info) {
this.info = true;
// TODO(chris): load all this params on ajax?
LvInfo.popup({
lehrveranstaltung_id: this.lehrveranstaltung_id,
bezeichnung: this.bezeichnung,
bezeichnung_eng: this.bezeichnung_eng,
studiengang_kuerzel: this.studiengang_kuerzel,
semester: this.semester,
studien_semester: this.studien_semester,
orgform_kurzbz: this.orgform_kurzbz,
sprache: this.sprache,
ects: this.ects,
incoming: this.incoming
}).then(() => this.info = false).catch(() => this.info = false);
}
}
},
watch:{
studien_semester(newValue){
this.fetchMenu(this.lehrveranstaltung_id, newValue);
}
},
created() {
if(this.type == 'student') {
this.$api
.call(ApiLehre.getStudentPruefungen(this.lehrveranstaltung_id))
.then(res => res.data)
.then(pruefungen => {
this.pruefungenData = pruefungen;
});
}
this.$api
.call(ApiLehre.getStudentPruefungen(this.lehrveranstaltung_id))
.then(res => res.data)
.then(pruefungen => {
this.pruefungenData = pruefungen;
});
},
template: /*html*/`
<div class="mylv-semester-studiengang-lv card">
mounted() {
this.fetchMenu(this.lehrveranstaltung_id, this.studien_semester);
},
template: /*html*/`<div class="mylv-semester-studiengang-lv card">
<lv-uebersicht ref="lvUebersicht" :preselectedMenu="preselectedMenuItem" :event="{
lehrveranstaltung_id: lehrveranstaltung_id,
studiensemester_kurzbz:studien_semester,
lehrfach_bez:studien_semester,
stg_kurzbzlang:studien_semester,
}"/>
<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="fw-bold" v-if="is_organisatorische_einheit" >{{ $p.t('lehre/organisationseinheit') }}:</h6>
<h6 class="mb-0">{{$p.user_language.value === 'English' ? bezeichnung_eng : bezeichnung}}</h6>
</div>
<div v-if="!emptyMenu" class="card-body ">
<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, index) in menu" :key="index" class="list-group-item border-0 " >
<div class="d-flex flex-row">
<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 :id="menuItem.name"
<a
class="fhc-body text-decoration-none text-truncate"
:class="{ 'unavailable':!menuItem.c4_link }"
:id="'moodle_links_'+lehrveranstaltung_id"
:class="{ 'unavailable':!menuItem.c4_link, 'dropdown-toggle':menuItem.c4_moodle_links?.length }"
:target="menuItem.c4_target"
:href="c4_link(menuItem) ? c4_link(menuItem) : null">
{{ menuItem.phrase ? $p.t(menuItem.phrase) : menuItem.name}}
</a>
<div v-if="(menuItem.c4_moodle_links?.length || menuItem.c4_linkList?.length) && menuItem.c4_link" class="dropdown">
<button
class="btn btn-sm dropdown-toggle dropdown-toggle-split border-0"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul v-if="menuItem.c4_moodle_links?.length" class="dropdown-menu dropdown-menu p-0">
<li v-for="item in menuItem.c4_moodle_links" :key="item.url">
<a class="dropdown-item border-bottom" :href="item.url" target="#">{{ item.lehrform }}</a>
</li>
</ul>
<ul v-else class="dropdown-menu dropdown-menu p-0">
<li v-for="([text, link], i) in menuItem.c4_linkList" :key="i">
<a class="dropdown-item border-bottom" :href="link" target="#">{{ text }}</a>
</li>
</ul>
</div>
</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>
</ul>
</template>
@@ -170,7 +204,7 @@ export default {
</div>
</template>
</div>
<div v-if="!emptyMenu && type == 'student'" class="card-footer">
<div v-if="!emptyMenu" class="card-footer">
<div class="row">
<!-- template for the LV if there are multiple pruefungen -->
<template v-if="LvHasPruefungenInformation">
@@ -188,12 +222,5 @@ export default {
</template>
</div>
</div>
<div v-else-if="!emptyMenu && type == 'employee'" class="card-footer">
<div class="row">
<div class="col-auto">
<span class="ps-1">{{ $p.t('lehre/semesterstunden') }}: {{ semesterstunden }}</span>
</div>
</div>
</div>
</div>`
};
@@ -1,37 +1,25 @@
import MylvSemesterCards from "./Semester.js";
import MylvTable from "./Table.js";
import ApiAddons from "../../../api/factory/addons.js"
import MylvSemester from "./Semester.js";
import Phrasen from "../../../mixins/Phrasen.js";
// TODO(chris): phrase: global/studiensemester_auswaehlen
// TODO(chris): phrase: next & prev +aria-label
export default {
name: 'MyLv',
components: {
MylvSemesterCards,
MylvTable
MylvSemester
},
mixins: [
Phrasen
],
data: () => {
return {
firstLoad: true,
studiensemester: null,
lvs: {},
currentSemester: null,
mode: localStorage.getItem('myLvaDefaultMode') ?? 'cards'
currentSemester: null
};
},
provide() {
return {
type: Vue.computed(() => this.type),
}
},
inject: ['isStudent', 'isMitarbeiter'],
computed: {
type() {
if(this.isStudent) return 'student'
if(this.isMitarbeiter) return 'employee'
return null
},
ready() {
return this.studiensemester !== null && (!this.firstLoad || this.current.lvs !== null);
},
@@ -46,22 +34,7 @@ export default {
axios.get(FHC_JS_DATA_STORAGE_OBJECT.app_root + FHC_JS_DATA_STORAGE_OBJECT.ci_router + '/components/Cis/Mylv/Lvs/' + this.currentSemester).then(res => {
this.lvs[this.currentSemester].lvs = res.data.retval || [];
this.firstLoad = false;
this.lvs[this.currentSemester].lvs.forEach(lv=>{
this.$api.call(ApiAddons.getLvMenu(lv.lehrveranstaltung_id, this.currentSemester)).then(res => {
if(res.data) {
const lvProp = this.lvs[this.currentSemester].lvs.find(lv2 => lv2.lehrveranstaltung_id == lv.lehrveranstaltung_id)
lvProp.menu = res.data
}
})
})
})
});
}
return this.lvs[this.currentSemester];
},
@@ -94,10 +67,6 @@ export default {
}
},
methods: {
clickMode(evt, mode) {
localStorage.setItem('myLvaDefaultMode', mode)
this.mode = mode
},
prevSem() {
this.$refs.studiensemester.selectedIndex--;
this.$refs.studiensemester.dispatchEvent(new Event('change', { bubbles: true }));
@@ -130,7 +99,7 @@ export default {
<h2>{{$p.t('lehre/myLV')}}</h2>
<hr>
<div class="mylv" v-if="ready">
<div class="mylv-student" v-if="ready">
<div v-if="currentSemester" class="row justify-content-center mb-3">
<div class="col-auto d-none">
<label class="col-form-label">{{$p.t('lehre/studiensemester')}}</label>
@@ -148,34 +117,13 @@ export default {
</button>
</div>
</div>
<div class=" col-auto my-lva-modes">
<div class="d-flex gap-1 justify-content-end" role="group">
<button
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'cards'}"
@click="clickMode($event, 'cards')"
>
<i class="fa fa-grip"></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
:class="{active: mode === 'table'}"
@click="clickMode($event, 'table')"
>
<i class="fa fa-table"></i>
</button>
</div>
</div>
</div>
<div class="alert alert-danger" role="alert" v-else>
{{$p.t('lehre/noLvFound')}}
</div>
<mylv-semester-cards v-if="mode == 'cards'" v-bind="current"/>
<mylv-table v-else-if="mode == 'table'" v-bind="current"/>
<mylv-semester v-bind="current"/>
</div>
<div class="mylv text-center" v-else>
<div class="mylv-student text-center" v-else>
<i class="fa-solid fa-spinner fa-pulse fa-3x"></i>
</div>`
};
-345
View File
@@ -1,345 +0,0 @@
import {CoreFilterCmpt} from "../../../components/filter/Filter.js";
export default {
name: 'MylvTable',
components: {
CoreFilterCmpt
},
props: {
semester: [String],
lvs: Array,
},
data() {
return {
phrasenPromise: null,
phrasenResolved: false,
tabulatorUuid: null,
tableBuiltResolve: null,
tableBuiltPromise: null,
mylvTableOptions: {
height: Vue.ref(400),
index: 'lehrveranstaltung_id',
layout: 'fitDataStretch',
placeholder: this.$p.t('global/noDataAvailable'),
columns: [
{title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/studiengang'))), field: 'sg_bezeichnung', widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('global/bezeichnung'))), field: 'bezeichnung', widthGrow: 2},
{title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/orgform'))), field: 'orgform_kurzbz', widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/kurzbz'))), field: 'studiengang_kuerzel', widthGrow: 1},
{title: Vue.computed(() => this.$capitalize(this.$p.t('lehre/semesterstunden'))), field: 'semesterstunden',
bottomCalc: this.semesterstundenCalc, widthGrow: 1, visible: false},
{title: Vue.computed(() => this.$capitalize(this.$p.t('global/actions'))), headerSort: false,
field: 'menu', formatter: this.actionFormatter, widthGrow: 1, tooltip: this.spoofingFunc}
],
persistence: false,
persistenceID: "mylv_2026_04_17"
},
mylvTableEventHandlers: [
]
}
},
computed: {
ready() { return this.lvs !== null; },
},
methods: {
semesterstundenCalc(values, data) {
let sum = 0
values.forEach(val => {
sum += Number(val)
})
return sum
},
spoofingFunc() {
// intentionally send empty tooltip info so tabulator tooltip doesnt get rendered but hover event propagates
// to individual button tooltips
return ''
},
c4_link(menuItem) {
if (!menuItem) return null;
if (Array.isArray(menuItem.c4_moodle_links) && menuItem.c4_moodle_links.length) {
return '#';
} else {
return menuItem.c4_link ?? null;
}
},
handleUuidDefined(uuid) {
this.tabulatorUuid = uuid
},
tableResolve(resolve) {
this.tableBuiltResolve = resolve
},
actionFormatter(cell) {
let container = document.createElement('div');
container.className = "d-flex gap-2";
const data = cell.getData()
if(data.menu && data.menu.length) {
container.className = "d-flex flex-wrap gap-2"
data.menu.forEach((lvLink) => {
// render dropdown if we have a link and some some linklist
const hasDropdown = (lvLink.c4_moodle_links?.length || lvLink.c4_linkList?.length) && lvLink.c4_link;
if (hasDropdown) {
// button group
const group = document.createElement('div');
group.className = 'btn-group';
// main action button
const button= this.createActionButton(lvLink)
// toggle button
const toggle = document.createElement('button');
toggle.className = 'btn btn-sm dropdown-toggle dropdown-toggle-split border-0';
toggle.type = 'button';
toggle.dataset.bsToggle = 'dropdown'; // uses absolute position which gets clipped by tabulator
toggle.ariaExpanded = 'false';
toggle.innerHTML = '<span class="visually-hidden">Toggle Dropdown</span>';
// dropdown menu
const dropMenu = document.createElement('ul');
dropMenu.className = 'dropdown-menu dropdown-menu p-0';
// moodle links have priority to be dropdown items but both can be!
const items = lvLink.c4_moodle_links?.length
? lvLink.c4_moodle_links.map(item => ({ text: item.lehrform, href: item.url }))
: lvLink.c4_linkList.map(([text, link]) => ({ text, href: link }));
items.forEach(({ text, href }) => {
const li = document.createElement('li');
const a = document.createElement('a');
a.className = 'dropdown-item border-bottom';
a.href = href;
a.target = '#';
a.textContent = text;
li.appendChild(a);
dropMenu.appendChild(li);
});
group.appendChild(button);
group.appendChild(toggle);
group.appendChild(dropMenu);
container.appendChild(group);
} else {
container.appendChild(this.createActionButton(lvLink));
}
})
}
return container;
},
createActionButton(lvLink){
const button = document.createElement('a');
button.className = 'fhc-body text-decoration-none text-truncate';
if (!lvLink.c4_link) button.classList.add('unavailable');
button.id = `${lvLink.name}_${lvLink.lehrveranstaltung_id}`;
const icon = lvLink.c4_icon2 ?? 'fa-solid fa-pen-to-square';
const label = lvLink.phrase ? this.$p.t(lvLink.phrase) : lvLink.name;
button.title = label;
button.innerHTML = `<i class="${icon}"></i><span style="margin-left:2px;">${label}</span>`;
button.addEventListener('click', (event) => {
event.preventDefault();
const url = this.c4_link(lvLink);
if (url) {
const target = lvLink.c4_target || '_blank';
if (target === '_blank') {
window.open(url, '_blank', 'noopener,noreferrer');
} else {
window.location.href = url;
}
} else {
console.warn("Link is unavailable for:", lvLink.name);
}
});
return button
},
loadState() {
return JSON.parse(localStorage.getItem(this.mylvTableOptions.persistenceID) || "null");
},
saveState(table) {
// avoid storing state after first restore part happened
if(!this.stateRestored) return
const rawLayout = table.getColumnLayout();
const state = {
columns: rawLayout.map(col => ({
field: col.field,
visible: col.visible,
width: col.width,
})),
sort: table.getSorters().map(s => ({
field: s.field,
dir: s.dir,
})),
filters: table.getFilters(),
headerFilters: table.getHeaderFilters()
};
localStorage.setItem(this.mylvTableOptions.persistenceID, JSON.stringify(state));
},
handleTableBuilt() {
const table = this.$refs.mylvTable.tabulator
this.tableBuiltResolve()
table.on("columnMoved", () => {
this.saveState(table);
});
table.on("columnResized", () => {
this.saveState(table);
});
table.on("columnVisibilityChanged", () => {
this.saveState(table);
});
table.on("filterChanged", () => {
this.saveState(table);
});
table.on("headerFilterChanged", () => {
this.saveState(table);
});
table.on("dataSorted", () => {
this.saveState(table);
});
table.on("columnSorted", () => {
this.saveState(table);
});
table.on("sortersChanged", () => {
this.saveState(table);
});
const saved = this.loadState();
table.on("renderComplete", () => {
if(!this.stateRestored) {
if (saved?.columns && !this.colLayoutRestored) {
const layout = saved.columns.map(col => ({
field: col.field,
width: col.width,
visible: col.visible,
// add more if needed, but keep it simple
}));
table.setColumnLayout(layout);
this.colLayoutRestored = true;
}
if (saved?.filters && !this.filtersRestored) {
this.filtersRestored = true // instantly avoid retriggers
table.setFilter(saved.filters);
}
if (saved?.headerFilters && !this.headerFiltersRestored) {
this.headerFiltersRestored = true // instantly avoid retriggers
for (let hf of saved.headerFilters) {
table.setHeaderFilterValue(hf.field, hf.value);
}
}
if (saved?.sort?.length && !this.sortRestored) {
this.sortRestored = true;
setTimeout(() => {
const sortList = saved.sort.map(s => {
const col = table.columnManager.findColumn(s.field);
if (!col) {
return null;
}
return { column: col, dir: s.dir };
}).filter(Boolean);
table.setSort(sortList);
}, 100);
}
this.stateRestored = true
}
});
},
async setupData() {
this.$refs.mylvTable.tabulator.setData(this.lvs);
},
async setupMounted() {
this.tableBuiltPromise = new Promise(this.tableResolve)
await this.tableBuiltPromise
this.setupData()
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
if(!tableDataSet) return
const rect = tableDataSet.getBoundingClientRect();
const h = window.visualViewport.height - rect.top - 50
if(this.$refs.mylvTable) {
this.$refs.mylvTable.$refs.table.style.setProperty('height', h+'px')
// necessary so the wrapping action row resolves to the full rowHeight required
// without the redraw here actions past the initial rowHeight would be clipped off
this.$refs.mylvTable.tabulator.redraw(true)
}
}
},
created() {
this.phrasenPromise = this.$p.loadCategory(['global', 'lehre', 'lvinfo'])
this.phrasenPromise.then(()=> {this.phrasenResolved = true})
},
mounted() {
this.setupMounted()
},
watch: {
lvs: {
async handler(newVal) {
await this.tableBuiltPromise;
if(!this.$refs.mylvTable?.tabulator) return
this.$refs.mylvTable.tabulator.setData(newVal);
const tableID = this.tabulatorUuid ? ('-' + this.tabulatorUuid) : ''
const tableDataSet = document.getElementById('filterTableDataset' + tableID);
if(!tableDataSet) return
const rect = tableDataSet.getBoundingClientRect();
const h = window.visualViewport.height - rect.top - 50
if(this.$refs.mylvTable) {
this.$refs.mylvTable.$refs.table.style.setProperty('height', h+'px')
}
},
deep: true
}
},
template: `
<div class="mylv-semester-table" v-if="ready">
<core-filter-cmpt
v-if="phrasenResolved"
@uuidDefined="handleUuidDefined"
:title="''"
ref="mylvTable"
:tabulator-options="mylvTableOptions"
:tabulator-events="mylvTableEventHandlers"
@tableBuilt="handleTableBuilt"
tableOnly
:sideMenu="false"
/>
</div>
<div v-if="tabulatorUuid === null" 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>
`
};
@@ -70,8 +70,8 @@ export const Raumsuche = {
handler: async () => {
this.tableBuiltResolve()
}
}]
};
}
]};
},
methods: {
tableResolve(resolve) {
@@ -31,7 +31,7 @@ export default {
this.event.lektor.slice(0, 3).map(lektor => lektor.kurzbz).join("\n")
+ "\n" + this.$p.t('lehre/weitereLektoren', [this.event.lektor.length - 3])
].join(": "));
} else {;
} else {
tooltipArray.push([
this.$p.t('lehre/lektor'),
this.event.lektor.map(lektor => lektor.kurzbz).join("\n")
@@ -2,7 +2,6 @@ import LvUebersicht from "../Mylv/LvUebersicht.js";
export default {
name: 'Studium',
data(){
return {
studienSemester :[],
+30 -17
View File
@@ -18,6 +18,7 @@
import CoreSearchbar from "../searchbar/searchbar.js";
import NavLanguage from "../navigation/Language.js";
import VerticalSplit from "../verticalsplit/verticalsplit.js";
import HorizontalSplit from "../horizontalsplit/horizontalsplit.js";
import AppMenu from "../AppMenu.js";
import AppConfig from "../AppConfig.js";
import StvVerband from "./Studentenverwaltung/Verband.js";
@@ -37,6 +38,7 @@ export default {
CoreSearchbar,
NavLanguage,
VerticalSplit,
HorizontalSplit,
AppMenu,
AppConfig,
StvVerband,
@@ -235,6 +237,10 @@ export default {
}
}
}
},
sidebarCollapsed(newVal) {
if(newVal) this.$refs.hSplit.collapseLeft()
else this.$refs.hSplit.showBoth()
}
},
methods: {
@@ -632,23 +638,30 @@ export default {
</app-menu>
</div>
</aside>
<nav id="sidebarMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100">
<div class="offcanvas-header justify-content-end px-1 d-md-none">
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
</div>
<stv-verband :preselectedKey="studiengangKz ? '' + studiengangKz : null" :endpoint="verbandEndpoint" @select-verband="onSelectVerband" class="col" style="height:0%"></stv-verband>
<stv-studiensemester v-model:studiensemester-kurzbz="studiensemesterKurzbz" @update:studiensemester-kurzbz="studiensemesterChanged"></stv-studiensemester>
</nav>
<main class="col-md-8 ms-sm-auto col-lg-9 col-xl-10">
<vertical-split>
<template #top>
<stv-list ref="stvList" v-model:selected="selected" :studiengang-kz="studiengangKz" :studiensemester-kurzbz="studiensemesterKurzbz" @filterActive="handleCustomFilter"></stv-list>
</template>
<template #bottom>
<stv-details ref="details" :students="selected" @reload="reloadList"></stv-details>
</template>
</vertical-split>
</main>
<horizontal-split ref="hSplit" :defaultRatio="[15, 85]">
<template #left>
<nav id="sidebarMenu" class="bg-light offcanvas offcanvas-start col-md p-md-0 h-100 w-100">
<div class="offcanvas-header justify-content-end px-1 d-md-none">
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" :aria-label="$p.t('ui/schliessen')"></button>
</div>
<stv-verband :preselectedKey="studiengangKz ? '' + studiengangKz : null" :endpoint="verbandEndpoint" @select-verband="onSelectVerband" class="col" style="height:0%"></stv-verband>
<stv-studiensemester v-model:studiensemester-kurzbz="studiensemesterKurzbz" @update:studiensemester-kurzbz="studiensemesterChanged"></stv-studiensemester>
</nav>
</template>
<template #right>
<main>
<vertical-split :defaultRatio="[50, 50]">
<template #top>
<stv-list ref="stvList" v-model:selected="selected" :studiengang-kz="studiengangKz" :studiensemester-kurzbz="studiensemesterKurzbz" @filterActive="handleCustomFilter"></stv-list>
</template>
<template #bottom>
<stv-details ref="details" :students="selected" @reload="reloadList"></stv-details>
</template>
</vertical-split>
</main>
</template>
</horizontal-split>
</div>
</div>
<app-config ref="config" v-model="appconfig" :endpoints="configEndpoints"></app-config>
@@ -0,0 +1,147 @@
export default {
name: 'HorizontalSplit',
props: {
defaultRatio: {
type: Array,
default: () => [50, 50]
}
},
data: function () {
return {
availWidth: 0,
leftwidth: 0,
rightwidth: 0,
mousePosX: 0,
resize: false,
hsplitterOffset: 0,
selfOffsetLeft: 0
};
},
template: `
<div ref="horizontalsplit" class="horizontalsplit-container">
<div ref="leftpanel" class="horizontalsplitted"
:style="{ width: this.leftwidthcss }">
<slot name="left">
<p>Left Panel</p>
</slot>
</div>
<div ref="hsplitter" class="horizontalsplitter"
:class="this.leftOrRightClass" @mousedown="this.dragStart">
<div class="splitactions horizontal" :class="this.leftOrRightClass">
<span @click="this.collapseRight" class="splitaction">
<i class="fas fa-angle-right text-muted"></i>
</span>
<span @dblclick="this.showBoth" class="splitaction resize">
<i class="fas fa-grip-lines-vertical text-muted"></i>
</span>
<span @click="this.collapseLeft" class="splitaction">
<i class="fas fa-angle-left text-muted"></i>
</span>
</div>
</div>
<div ref="rightpanel" class="horizontalsplitted"
:style="{ width: this.rightwidthcss }">
<slot name="right">
<p/>
</slot>
</div>
</div>
`,
mounted: function () {
this.calcWidths();
this.trackHorizontalSplitterOffsetLeft();
window.addEventListener('resize', this.calcWidths);
},
updated: function () {
this.trackHorizontalSplitterOffsetLeft();
},
beforeDestroy: function () {
window.removeEventListener('resize', this.calcWidths);
},
methods: {
calcWidths: function () {
var oldavailWidth = this.availWidth;
this.selfOffsetLeft = this.$refs.horizontalsplit.offsetLeft;
this.availWidth = this.$refs.horizontalsplit.offsetWidth - this.$refs.hsplitter.offsetWidth;
if ((this.leftwidth === 0 && this.rightwidth === 0) || oldavailWidth === 0) {
this.leftwidth = Math.floor(this.availWidth * (this.defaultRatio[0] / 100));
} else {
this.leftwidth = Math.floor(((this.leftwidth * 100) / oldavailWidth) / 100 * this.availWidth);
}
this.rightwidth = this.availWidth - this.leftwidth;
},
collapseLeft: function () {
this.calcWidths();
this.leftwidth = 0;
this.rightwidth = this.availWidth;
},
collapseRight: function () {
this.calcWidths();
this.leftwidth = this.availWidth;
this.rightwidth = 0;
},
showBoth: function () {
this.leftwidth = Math.floor(this.availWidth * (this.defaultRatio[0] / 100));
this.rightwidth = this.availWidth - this.leftwidth;
},
isCollapsed: function () {
if (this.leftwidth === 0) {
return 'left';
} else if (this.rightwidth === 0) {
return 'right';
} else {
return false;
}
},
dragStart: function (e) {
e.preventDefault();
e.stopPropagation();
window.addEventListener('mouseup', this.dragEnd);
window.addEventListener('mousemove', this.drag);
this.resize = true;
this.mousePosX = e.clientX;
},
drag: function (e) {
if (!this.resize) {
return;
}
e.preventDefault();
e.stopPropagation();
var offsetX = e.clientX - this.mousePosX;
this.leftwidth = this.leftwidth + offsetX;
if (this.leftwidth < 0) {
this.leftwidth = 0;
}
if (this.leftwidth > this.availWidth) {
this.leftwidth = this.availWidth;
}
this.rightwidth = this.availWidth - this.leftwidth;
this.mousePosX = e.clientX;
},
dragEnd: function (e) {
e.preventDefault();
e.stopPropagation();
window.removeEventListener('mousemove', this.drag);
window.removeEventListener('mouseup', this.dragEnd);
this.resize = false;
this.mousePosX = e.clientX;
},
trackHorizontalSplitterOffsetLeft: function () {
this.hsplitterOffset = this.$refs.hsplitter.offsetLeft;
}
},
computed: {
leftOrRightClass: function () {
return ((this.hsplitterOffset - this.selfOffsetLeft) <= Math.floor(this.availWidth / 2))
? 'left'
: 'right';
},
leftwidthcss: function () {
return this.leftwidth + 'px';
},
rightwidthcss: function () {
return this.rightwidth + 'px';
}
}
};
@@ -1,5 +1,11 @@
export default {
name: 'VerticalSplit',
props: {
defaultRatio: {
type: Array,
default: () => [50, 50]
}
},
data: function() {
return {
availHeight: 0,
@@ -50,17 +56,22 @@ export default {
updated: function() {
this.trackVerticalSplitterOffsetTop();
},
beforeDestroy: function () {
window.removeEventListener('resize', this.calcHeights);
},
methods: {
calcHeights: function() {
var windowheight = window.innerHeight;
var oldavailHeight = this.availHeight;
this.selfOffsetTop = this.$refs.verticalsplit.offsetTop;
this.availHeight = windowheight - this.selfOffsetTop - this.$refs.vsplitter.offsetHeight;
if( (this.topheight === 0 && this.bottomheight === 0) || oldavailHeight === 0 ) {
this.topheight = Math.floor(this.availHeight/2);
this.topheight = Math.floor(this.availHeight * (this.defaultRatio[0] / 100));
} else {
this.topheight = Math.floor( ((((this.topheight * 100) / oldavailHeight) / 100) * this.availHeight) );
}
this.bottomheight = this.availHeight - this.topheight;
},
collapseTop: function() {
@@ -74,8 +85,8 @@ export default {
this.bottomheight = 0;
},
showBoth: function() {
this.topheight = Math.floor(this.availHeight/2);
this.bottomheight = Math.floor(this.availHeight/2);
this.topheight = Math.floor(this.availHeight * (this.defaultRatio[0] / 100));
this.bottomheight = this.availHeight - this.topheight
},
isCollapsed: function() {
if( this.topheight === 0 ) {
+14 -3
View File
@@ -111,7 +111,7 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
return mergePromiseArr(getPromiseFunc(start, end), result);
};
Vue.watchEffect(() => {
const reload = () => {
const range = Vue.toValue(rangeInterval);
if (!(range instanceof luxon.Interval))
return;
@@ -132,7 +132,18 @@ export function useEventLoader(rangeInterval, getPromiseFunc) {
}
})
});
})
};
return { events: allEvents, lv }
Vue.watchEffect(reload);
const reset = () => {
loading_id = 0;
events.value = [];
loadingEvents.value = [];
eventsLoaded.splice(0, eventsLoaded.length);
reload();
}
return { events: allEvents, lv, reset }
}
+143
View File
@@ -57277,6 +57277,149 @@ I have been informed that I am under no obligation to consent to the transmissio
)
),
// ### Phrases Dashboard Admin END
// ### LvPlanStgOrg START ###
array(
'app' => 'core',
'category' => 'LvPlan',
'phrase' => 'noStgProvided',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'kein Studiengang hinterlegt',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'no degree-program provided',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'LvPlan',
'phrase' => 'headerLvPlanLvVerband',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Lv-Plan Lehrverband',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Course-Plan Teaching Association',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'LvPlan',
'phrase' => 'chooseStg',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Studiengang auswählen...',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Select a degree-program...',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'LvPlan',
'phrase' => 'loadLvPlan',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Lehrverband laden',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Load Course-Plan',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'LvPlan',
'phrase' => 'backToDropdown',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Zurück zum Auswahl-Dropdown',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Back to Dropdown',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'LvPlan',
'phrase' => 'error_SemMissing',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Semester wählen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Select a semester',
'description' => '',
'insertvon' => 'system'
)
)
),
array(
'app' => 'core',
'category' => 'LvPlan',
'phrase' => 'error_VerbandMissing',
'insertvon' => 'system',
'phrases' => array(
array(
'sprache' => 'German',
'text' => 'Verband wählen',
'description' => '',
'insertvon' => 'system'
),
array(
'sprache' => 'English',
'text' => 'Select a verband',
'description' => '',
'insertvon' => 'system'
)
)
),
// ### LvPlanStgOrg END ###
);