mirror of
https://github.com/FH-Complete/FHC-Core.git
synced 2026-07-18 23:42:17 +00:00
Merge branch 'feature-63435/Studierendenverwaltung_Studierende_Verbandsgruppen_und_Spezialgruppen_zuordnen_Multiaktion' into demo-cis40
This commit is contained in:
@@ -110,7 +110,7 @@ class Config extends FHCAPI_Controller
|
||||
];
|
||||
$result['groups'] = [
|
||||
'title' => $this->p->t('stv', 'tab_groups'),
|
||||
'component' => './Stv/Studentenverwaltung/Details/Gruppen.js'
|
||||
'component' => './Stv/Studentenverwaltung/Details/Groups.js'
|
||||
];
|
||||
$result['messages'] = [
|
||||
'title' => $this->p->t('stv', 'tab_messages'),
|
||||
@@ -205,6 +205,10 @@ class Config extends FHCAPI_Controller
|
||||
'additionalCols' => []
|
||||
]
|
||||
];
|
||||
$result['groups'] = [
|
||||
'title' => $this->p->t('stv', 'tab_groups'),
|
||||
'component' => './Stv/Studentenverwaltung/Details/Groups.js'
|
||||
];
|
||||
$result['status'] = [
|
||||
'title' => 'Status',
|
||||
'component' => './Stv/Studentenverwaltung/Details/MultiStatus.js',
|
||||
|
||||
@@ -9,6 +9,8 @@ class Gruppen extends FHCAPI_Controller
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'add' => ['admin:rw', 'assistenz:rw'],
|
||||
'search' => ['admin:r', 'assistenz:r'],
|
||||
'getGruppen' => ['admin:r', 'assistenz:r'],
|
||||
'deleteGruppe' => ['admin:rw', 'assistenz:rw'],
|
||||
]);
|
||||
@@ -18,7 +20,9 @@ class Gruppen extends FHCAPI_Controller
|
||||
|
||||
// Load language phrases
|
||||
$this->loadPhrases([
|
||||
'ui', 'gruppenmanagement'
|
||||
'ui',
|
||||
'gruppenmanagement',
|
||||
'lehre'
|
||||
]);
|
||||
|
||||
// Load models
|
||||
@@ -26,15 +30,140 @@ class Gruppen extends FHCAPI_Controller
|
||||
$this->load->model('organisation/Gruppe_model', 'GruppeModel');
|
||||
}
|
||||
|
||||
public function add()
|
||||
{
|
||||
$this->load->library("form_validation");
|
||||
|
||||
$this->form_validation->set_rules(
|
||||
'gruppe_kurzbz',
|
||||
$this->p->t('gruppenmanagement', 'gruppe'),
|
||||
'required|is_in_db[organisation/Gruppe_model]',
|
||||
[
|
||||
'required' => $this->p->t('ui', 'error_fieldRequired'),
|
||||
'is_in_db' => $this->p->t('ui', 'error_fieldNotFound')
|
||||
]
|
||||
);
|
||||
$this->form_validation->set_rules(
|
||||
'uid',
|
||||
$this->p->t('ui', 'student_uid'),
|
||||
'required|is_in_db[crm/Student_model:student_uid]',
|
||||
[
|
||||
'required' => $this->p->t('ui', 'error_fieldRequired'),
|
||||
'is_in_db' => $this->p->t('ui', 'error_fieldNotFound')
|
||||
]
|
||||
);
|
||||
$this->form_validation->set_rules(
|
||||
'studiensemester_kurzbz',
|
||||
$this->p->t('lehre', 'studiensemester'),
|
||||
'required|is_in_db[organisation/Studiensemester_model]',
|
||||
[
|
||||
'required' => $this->p->t('ui', 'error_fieldRequired'),
|
||||
'is_in_db' => $this->p->t('ui', 'error_fieldNotFound')
|
||||
]
|
||||
);
|
||||
|
||||
if (!$this->form_validation->run())
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$gruppe_kurzbz = $this->input->post('gruppe_kurzbz');
|
||||
$uid = $this->input->post('uid');
|
||||
$studiensemester_kurzbz = $this->input->post('studiensemester_kurzbz');
|
||||
|
||||
$result = $this->BenutzergruppeModel->load([
|
||||
$gruppe_kurzbz,
|
||||
$uid
|
||||
]);
|
||||
$benutzergruppe = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
if ($benutzergruppe) {
|
||||
$this->terminateWithError(
|
||||
$this->p->t('gruppenmanagement', 'error_alreadyInGroup', [
|
||||
'uid' => $uid,
|
||||
'studiensemester_kurzbz' => current($benutzergruppe)->studiensemester_kurzbz
|
||||
]),
|
||||
self::ERROR_TYPE_GENERAL
|
||||
);
|
||||
}
|
||||
|
||||
$result = $this->BenutzergruppeModel->insert([
|
||||
'uid' => $uid,
|
||||
'gruppe_kurzbz' => $gruppe_kurzbz,
|
||||
'studiensemester_kurzbz' => $studiensemester_kurzbz,
|
||||
'insertamum' => date('c'),
|
||||
'insertvon' => getAuthUID()
|
||||
]);
|
||||
|
||||
$this->getDataOrTerminateWithError($result);
|
||||
|
||||
$this->terminateWithSuccess();
|
||||
}
|
||||
|
||||
public function search()
|
||||
{
|
||||
$query = $this->input->post('query');
|
||||
if (!$query)
|
||||
$this->terminateWithSuccess([]);
|
||||
|
||||
// add query to where clause
|
||||
$query = strtoupper($query);
|
||||
$query = $this->GruppeModel->db->escape_like_str($query);
|
||||
$query = '%' . str_replace(' ', '%', $query) . '%';
|
||||
|
||||
$this->GruppeModel->db->group_start();
|
||||
$this->GruppeModel->db->or_like('UPPER(gruppe_kurzbz)', $query, 'none', false);
|
||||
$this->GruppeModel->db->or_like('UPPER(bezeichnung)', $query, 'none', false);
|
||||
$this->GruppeModel->db->or_like('UPPER(beschreibung)', $query, 'none', false);
|
||||
$this->GruppeModel->db->group_end();
|
||||
|
||||
// add stg sorting 1
|
||||
$studiengang_kz = $this->input->post('studiengang_kz');
|
||||
$sort_stg = $studiengang_kz ? "WHEN studiengang_kz = " . $this->GruppeModel->escape($studiengang_kz) . " THEN 0" : "";
|
||||
|
||||
// add stg sorting 2
|
||||
$studiengang_kzs = [];
|
||||
$result = $this->permissionlib->getSTG_isEntitledFor('admin');
|
||||
if ($result)
|
||||
$studiengang_kzs = array_merge($studiengang_kzs, $result);
|
||||
$result = $this->permissionlib->getSTG_isEntitledFor('assistenz');
|
||||
if ($result)
|
||||
$studiengang_kzs = array_merge($studiengang_kzs, $result);
|
||||
|
||||
// selects
|
||||
$this->GruppeModel->addSelect("*");
|
||||
$this->GruppeModel->addSelect("CASE
|
||||
" . $sort_stg . "
|
||||
WHEN studiengang_kz IN (" . implode(",", $this->GruppeModel->db->escape($studiengang_kzs)) . ")
|
||||
THEN 1
|
||||
ELSE 2
|
||||
END AS sort_stg");
|
||||
|
||||
// ordering
|
||||
$this->GruppeModel->addOrder("sort_stg");
|
||||
$this->GruppeModel->addOrder("sort");
|
||||
$this->GruppeModel->addOrder("gruppe_kurzbz");
|
||||
|
||||
// default where clause & execute
|
||||
$result = $this->GruppeModel->loadWhere([
|
||||
'lehre' => true,
|
||||
'sichtbar' => true,
|
||||
'aktiv' => true,
|
||||
'direktinskription' => false
|
||||
]);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
$this->terminateWithSuccess($data);
|
||||
}
|
||||
|
||||
public function getGruppen($student_uid)
|
||||
{
|
||||
$this->BenutzergruppeModel ->addSelect('gruppe_kurzbz');
|
||||
$this->BenutzergruppeModel ->addSelect('bezeichnung');
|
||||
$this->BenutzergruppeModel ->addSelect('generiert');
|
||||
$this->BenutzergruppeModel ->addSelect('uid');
|
||||
$this->BenutzergruppeModel ->addSelect('studiensemester_kurzbz');
|
||||
$this->BenutzergruppeModel ->addJoin('public.tbl_gruppe', 'gruppe_kurzbz');
|
||||
$this->BenutzergruppeModel-> addOrder('bezeichnung', 'ASC');
|
||||
$this->BenutzergruppeModel->addSelect('gruppe_kurzbz');
|
||||
$this->BenutzergruppeModel->addSelect('bezeichnung');
|
||||
$this->BenutzergruppeModel->addSelect('generiert');
|
||||
$this->BenutzergruppeModel->addSelect('uid');
|
||||
$this->BenutzergruppeModel->addSelect('studiensemester_kurzbz');
|
||||
$this->BenutzergruppeModel->addJoin('public.tbl_gruppe', 'gruppe_kurzbz');
|
||||
$this->BenutzergruppeModel->addOrder('bezeichnung', 'ASC');
|
||||
|
||||
$result = $this->BenutzergruppeModel->loadWhere(
|
||||
array(
|
||||
@@ -49,29 +178,48 @@ class Gruppen extends FHCAPI_Controller
|
||||
|
||||
public function deleteGruppe()
|
||||
{
|
||||
$student_uid = $this->input->post('id');
|
||||
$this->load->library("form_validation");
|
||||
|
||||
$this->form_validation->set_rules(
|
||||
'uid',
|
||||
$this->p->t('person', 'UID'),
|
||||
'required',
|
||||
[
|
||||
'required' => $this->p->t('ui', 'error_fieldRequired')
|
||||
]
|
||||
);
|
||||
|
||||
$this->form_validation->set_rules(
|
||||
'gruppe_kurzbz',
|
||||
$this->p->t('gruppenmanagement', 'gruppe'),
|
||||
'required',
|
||||
[
|
||||
'required' => $this->p->t('ui', 'error_fieldRequired')
|
||||
]
|
||||
);
|
||||
|
||||
if (!$this->form_validation->run())
|
||||
$this->terminateWithValidationErrors($this->form_validation->error_array());
|
||||
|
||||
$uid = $this->input->post('uid');
|
||||
$gruppe_kurzbz = $this->input->post('gruppe_kurzbz');
|
||||
|
||||
//Validate if automatic group generation
|
||||
$result = $this->GruppeModel-> loadWhere(
|
||||
array(
|
||||
'gruppe_kurzbz' => $gruppe_kurzbz
|
||||
)
|
||||
);
|
||||
// Validate if automatic group generation
|
||||
$result = $this->GruppeModel->loadWhere([
|
||||
'gruppe_kurzbz' => $gruppe_kurzbz
|
||||
]);
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
$generation = current($data);
|
||||
|
||||
if($generation->generiert)
|
||||
if ($generation->generiert)
|
||||
{
|
||||
$this->terminateWithError($this->p->t('gruppenmanagement', 'error_deleteGeneratedGroups'), self::ERROR_TYPE_GENERAL);
|
||||
}
|
||||
|
||||
$result = $this->BenutzergruppeModel->delete(
|
||||
array(
|
||||
'gruppe_kurzbz' => $gruppe_kurzbz,
|
||||
'uid' => $student_uid
|
||||
)
|
||||
);
|
||||
$result = $this->BenutzergruppeModel->delete([
|
||||
'gruppe_kurzbz' => $gruppe_kurzbz,
|
||||
'uid' => $uid
|
||||
]);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
if (! defined('BASEPATH')) exit('No direct script access allowed');
|
||||
|
||||
|
||||
class Lehrverband extends FHCAPI_Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct([
|
||||
'hasOrgforms' => ['admin:r', 'assistenz:r'],
|
||||
'getTree' => ['admin:r', 'assistenz:r'],
|
||||
'getSpecialgroups' => ['admin:r', 'assistenz:r']
|
||||
]);
|
||||
}
|
||||
|
||||
public function hasOrgforms($studiengang_kz)
|
||||
{
|
||||
$this->load->model('organisation/Studiengang_model', 'StudiengangModel');
|
||||
|
||||
$result = $this->StudiengangModel->load($studiengang_kz);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
if ($data) {
|
||||
$data = current($data)->mischform;
|
||||
}
|
||||
|
||||
$this->terminateWithSuccess($data);
|
||||
}
|
||||
|
||||
public function getTree($studiengang_kz)
|
||||
{
|
||||
$this->load->model('organisation/Lehrverband_model', 'LehrverbandModel');
|
||||
|
||||
$result = $this->LehrverbandModel->loadWhere([
|
||||
'studiengang_kz' => $studiengang_kz,
|
||||
'aktiv' => true
|
||||
]);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
$this->terminateWithSuccess($data);
|
||||
}
|
||||
|
||||
public function getSpecialgroups($studiengang_kz)
|
||||
{
|
||||
$this->load->model('organisation/Gruppe_model', 'GruppeModel');
|
||||
|
||||
$where = [
|
||||
'studiengang_kz' => $studiengang_kz,
|
||||
'lehre' => true,
|
||||
'sichtbar' => true,
|
||||
'aktiv' => true,
|
||||
'direktinskription' => false
|
||||
];
|
||||
|
||||
$result = $this->GruppeModel->loadWhere($where);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
$this->terminateWithSuccess($data);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ class Student extends FHCAPI_Controller
|
||||
parent::__construct([
|
||||
'get' => ['admin:r', 'assistenz:r'],
|
||||
'save' => ['admin:rw', 'assistenz:rw'],
|
||||
'saveStudent' => ['admin:rw', 'assistenz:rw'],
|
||||
'check' => ['admin:rw', 'assistenz:rw'],
|
||||
'add' => ['admin:rw', 'assistenz:rw'] // TODO(chris): extra permissions
|
||||
]);
|
||||
@@ -414,6 +415,31 @@ class Student extends FHCAPI_Controller
|
||||
), ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves data to a prestudent using their student_uid
|
||||
*
|
||||
* @param string $student_uid
|
||||
* @param string $studiensemester_kurzbz
|
||||
* @return void
|
||||
*/
|
||||
public function saveStudent($student_uid, $studiensemester_kurzbz)
|
||||
{
|
||||
$this->load->model('crm/Student_model', 'StudentModel');
|
||||
|
||||
$result = $this->StudentModel->load([$student_uid]);
|
||||
|
||||
$data = $this->getDataOrTerminateWithError($result);
|
||||
|
||||
if (!$data)
|
||||
show_404(); // No Student with that ID
|
||||
|
||||
$student = current($data);
|
||||
|
||||
$this->checkPermissionsForPrestudent($student->prestudent_id, ['admin:rw', 'assistenz:rw']);
|
||||
|
||||
return $this->save($student->prestudent_id, $studiensemester_kurzbz);
|
||||
}
|
||||
|
||||
public function check()
|
||||
{
|
||||
$this->load->library('form_validation');
|
||||
|
||||
@@ -272,6 +272,7 @@ class Verband extends FHCAPI_Controller
|
||||
$this->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, verband, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester AND verband=v.verband ORDER BY gruppe LIMIT 1)) AS name", false);
|
||||
$this->StudiengangModel->addSelect("CASE WHEN MAX(gruppe)='' OR MAX(gruppe)=' ' THEN TRUE ELSE FALSE END AS leaf");
|
||||
|
||||
$this->StudiengangModel->addSelect($this->StudiengangModel->escape($semester) . ' AS semester');
|
||||
$this->StudiengangModel->addSelect('verband');
|
||||
$this->StudiengangModel->addSelect($this->StudiengangModel->escape($studiengang_kz) . '::integer AS stg_kz', false);
|
||||
|
||||
@@ -320,6 +321,8 @@ class Verband extends FHCAPI_Controller
|
||||
$this->StudiengangModel->addSelect("CONCAT(UPPER(CONCAT(typ, kurzbz)), '-', semester, verband, gruppe, (SELECT CASE WHEN bezeichnung IS NULL OR bezeichnung='' THEN ''::TEXT ELSE CONCAT(' (', bezeichnung, ')') END FROM public.tbl_lehrverband WHERE studiengang_kz=v.studiengang_kz AND semester=v.semester AND verband=v.verband AND gruppe=v.gruppe ORDER BY gruppe LIMIT 1)) AS name", false);
|
||||
$this->StudiengangModel->addSelect("TRUE AS leaf", false);
|
||||
|
||||
$this->StudiengangModel->addSelect('v.semester');
|
||||
$this->StudiengangModel->addSelect('v.verband');
|
||||
$this->StudiengangModel->addSelect('gruppe');
|
||||
$this->StudiengangModel->addSelect($this->StudiengangModel->escape($studiengang_kz) . '::integer AS stg_kz', false);
|
||||
|
||||
|
||||
@@ -515,3 +515,31 @@ function has_permissions_for_stg($studiengang_kz, $permissions = '')
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* check if an entry exists in the database
|
||||
*/
|
||||
function is_in_db($key, $model = '')
|
||||
{
|
||||
if (!$model)
|
||||
return false;
|
||||
|
||||
$field = strstr($model, ":");
|
||||
if ($field) {
|
||||
$model = strstr($model, ":", true);
|
||||
$field = substr($field, 1);
|
||||
}
|
||||
|
||||
$CI =& get_instance();
|
||||
$CI->load->model($model, $model);
|
||||
|
||||
if ($field) {
|
||||
$result = $CI->$model->loadWhere([
|
||||
$field => $key
|
||||
]);
|
||||
} else {
|
||||
$result = $CI->$model->load($key);
|
||||
}
|
||||
|
||||
return (isSuccess($result) && hasData($result));
|
||||
}
|
||||
|
||||
@@ -41,3 +41,4 @@ if (!defined('BASEPATH')) exit('No direct script access allowed');
|
||||
$lang['form_validation_has_write_permissions'] = 'You have no rights to edit {field} field.';
|
||||
$lang['form_validation_is_valid_date'] = 'The date format is invalid or out of range.';
|
||||
$lang['form_validation_has_permissions_for_stg'] = 'You have no rights for stg {field}.';
|
||||
$lang['form_validation_is_in_db'] = '{field} does not exist.';
|
||||
|
||||
@@ -5690,3 +5690,32 @@
|
||||
outline: 0;
|
||||
box-shadow: 0 0 0 .25rem rgba(13,110,253,.25);
|
||||
}
|
||||
|
||||
/* input-group */
|
||||
/* autocomplete */
|
||||
.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) > .p-autocomplete-input,
|
||||
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control > .p-autocomplete-input,
|
||||
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select > .p-autocomplete-input,
|
||||
.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) > .p-autocomplete-input,
|
||||
.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) > .p-autocomplete > .p-autocomplete-input,
|
||||
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control > .p-autocomplete > .p-autocomplete-input,
|
||||
.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select > .p-autocomplete > .p-autocomplete-input,
|
||||
.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating) > .p-autocomplete > .p-autocomplete-input {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) > .p-autocomplete-input,
|
||||
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) > .p-autocomplete > .p-autocomplete-input {
|
||||
margin-left: calc(var(--bs-border-width) * -1);
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.p-inputtext.is-invalid:enabled:hover {
|
||||
border-color: var(--bs-form-invalid-border-color);
|
||||
}
|
||||
.p-inputtext.is-invalid:enabled:focus,
|
||||
.was-validated .p-inputtext:invalid:focus {
|
||||
border-color: var(--bs-form-invalid-border-color);
|
||||
box-shadow: 0 0 0 .25rem rgba(var(--bs-danger-rgb),.25);
|
||||
}
|
||||
|
||||
@@ -35,4 +35,14 @@ export default {
|
||||
params
|
||||
};
|
||||
},
|
||||
saveStudent(student_uid, studiensemester_kurzbz, params) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/stv/student/saveStudent/'
|
||||
+ encodeURIComponent(student_uid)
|
||||
+ '/'
|
||||
+ encodeURIComponent(studiensemester_kurzbz),
|
||||
params
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -16,6 +16,27 @@
|
||||
*/
|
||||
|
||||
export default {
|
||||
add(uid, gruppe_kurzbz, studiensemester_kurzbz) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/stv/gruppen/add/',
|
||||
params: {
|
||||
uid,
|
||||
gruppe_kurzbz,
|
||||
studiensemester_kurzbz
|
||||
}
|
||||
};
|
||||
},
|
||||
search(query, studiengang_kz) {
|
||||
return {
|
||||
method: 'post',
|
||||
url: 'api/frontend/v1/stv/gruppen/search/',
|
||||
params: {
|
||||
query,
|
||||
studiengang_kz
|
||||
}
|
||||
};
|
||||
},
|
||||
getGruppen(id) {
|
||||
return {
|
||||
method: 'get',
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (C) 2025 fhcomplete.org
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export default {
|
||||
hasOrgforms(studiengang_kz) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/stv/lehrverband/hasOrgforms/' + studiengang_kz
|
||||
};
|
||||
},
|
||||
getTree(studiengang_kz) {
|
||||
return {
|
||||
method: 'get',
|
||||
url: 'api/frontend/v1/stv/lehrverband/getTree/' + studiengang_kz
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -96,8 +96,11 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onDragstart(evt) {
|
||||
DragAndDrop.setTransferData(evt.detail.originalEvent, evt.detail.item.orig);
|
||||
this.draggedInternalEvent = evt.detail.item;
|
||||
const data = DragAndDrop.convertToTransferData(evt.detail.item.orig);
|
||||
if (DragAndDrop.isValidDragObject(data)) {
|
||||
DragAndDrop.setTransferData(evt.detail.originalEvent, data);
|
||||
this.draggedInternalEvent = evt.detail.item;
|
||||
}
|
||||
},
|
||||
onDragend() {
|
||||
this.draggedInternalEvent = null;
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import GroupsLvb from './Groups/Lvb.js';
|
||||
import GroupsSpecial from './Groups/Special.js';
|
||||
import GroupsList from './Groups/List.js';
|
||||
|
||||
import ApiStvGroups from '../../../../api/factory/stv/group.js';
|
||||
import ApiStvDetails from '../../../../api/factory/stv/details.js';
|
||||
|
||||
export default {
|
||||
name: 'TabGroups',
|
||||
components: {
|
||||
GroupsLvb,
|
||||
GroupsSpecial,
|
||||
GroupsList
|
||||
},
|
||||
inject: {
|
||||
$reloadList: {
|
||||
from: '$reloadList',
|
||||
required: true
|
||||
},
|
||||
currentSemester: {
|
||||
form: 'currentSemester',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
modelValue: [Object, Array]
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hasOrgforms: false,
|
||||
lvbList: [],
|
||||
specialGroups: [],
|
||||
selectedOrgform: false,
|
||||
selectedSemester: false,
|
||||
selectedVerband: false,
|
||||
selectedGruppe: false,
|
||||
multiFormHandler: (form, errors) => {
|
||||
function _split_errors(result, [uid, errors]) {
|
||||
const gruppe_kurzbz = [];
|
||||
const studiensemester_kurzbz = [];
|
||||
const others = {};
|
||||
errors.forEach(error => {
|
||||
_split_messages(error.messages, gruppe_kurzbz, studiensemester_kurzbz, others);
|
||||
});
|
||||
if (gruppe_kurzbz.length) {
|
||||
if (!result.formFeedback.gruppe_kurzbz)
|
||||
result.formFeedback.gruppe_kurzbz = [];
|
||||
result.formFeedback.gruppe_kurzbz.push(...gruppe_kurzbz);
|
||||
}
|
||||
if (studiensemester_kurzbz.length) {
|
||||
if (!result.formFeedback.studiensemester_kurzbz)
|
||||
result.formFeedback.studiensemester_kurzbz = [];
|
||||
result.formFeedback.studiensemester_kurzbz.push(...studiensemester_kurzbz);
|
||||
}
|
||||
if (Object.keys(others).length) {
|
||||
result.toast[uid] = [
|
||||
{ type: 'validation', messages: others }
|
||||
];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function _split_messages(messages, gruppe_kurzbz, studiensemester_kurzbz, others) {
|
||||
Object.entries(messages).forEach(([field, msg]) => {
|
||||
if (field == 'gruppe_kurzbz') {
|
||||
gruppe_kurzbz.push(msg);
|
||||
} else if (field == 'studiensemester_kurzbz') {
|
||||
studiensemester_kurzbz.push(msg);
|
||||
} else {
|
||||
if (!others[field])
|
||||
others[field] = [];
|
||||
others[field].push(msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
const { formFeedback, toast } = Object.entries(errors)
|
||||
.reduce(_split_errors, { formFeedback: {}, toast: {} });
|
||||
|
||||
if (formFeedback.gruppe_kurzbz)
|
||||
formFeedback.gruppe_kurzbz = formFeedback.gruppe_kurzbz
|
||||
.filter((v,k,a) => a.indexOf(v) == k);
|
||||
if (formFeedback.studiensemester_kurzbz)
|
||||
formFeedback.studiensemester_kurzbz = formFeedback.studiensemester_kurzbz
|
||||
.filter((v,k,a) => a.indexOf(v) == k);
|
||||
|
||||
form.clearValidation();
|
||||
if (Object.keys(formFeedback)) {
|
||||
form.setFeedback(false, formFeedback);
|
||||
}
|
||||
|
||||
if (Object.keys(toast).length) {
|
||||
console.log(toast);
|
||||
this.$api.getErrorHandler().handler.toast(toast);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allAreStudents() {
|
||||
if (Array.isArray(this.modelValue))
|
||||
return this.modelValue.every(ps => ps.uid);
|
||||
return this.modelValue.uid;
|
||||
},
|
||||
sharedStg() {
|
||||
if (Array.isArray(this.modelValue)) {
|
||||
const first = this.modelValue.find(Boolean);
|
||||
if (this.modelValue.every(ps => ps.studiengang_kz === first.studiengang_kz))
|
||||
return first.studiengang_kz;
|
||||
return undefined;
|
||||
}
|
||||
return this.modelValue.studiengang_kz;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showNewGroupModal() {
|
||||
this.$refs.newGroupModal.show()
|
||||
},
|
||||
changeLvb(params) {
|
||||
let data = { semester: params.semester };
|
||||
if (params.verband && params.verband != " ") {
|
||||
data.verband = params.verband;
|
||||
if (params.gruppe && params.gruppe != " ")
|
||||
data.gruppe = params.gruppe;
|
||||
}
|
||||
|
||||
let endpoint;
|
||||
|
||||
if (Array.isArray(this.modelValue)) {
|
||||
endpoint = this.modelValue.map(student => [
|
||||
student.uid + ' (' + student.vorname + ' ' + student.nachname + ')',
|
||||
ApiStvDetails.save(
|
||||
student.prestudent_id,
|
||||
this.currentSemester,
|
||||
data
|
||||
)
|
||||
]);
|
||||
} else {
|
||||
endpoint = ApiStvDetails.save(
|
||||
this.modelValue.prestudent_id,
|
||||
this.currentSemester,
|
||||
data
|
||||
);
|
||||
}
|
||||
this.$api
|
||||
.call(endpoint)
|
||||
.then(() => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successSave'));
|
||||
this.$reloadList();
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
addSpecialGroup() {
|
||||
const gruppe_kurzbz = this.$refs.newGroupModal.value.gruppe_kurzbz || this.$refs.newGroupModal.value;
|
||||
|
||||
if (Array.isArray(this.modelValue)) {
|
||||
this.$refs.newGroupModal.$refs.form
|
||||
.call(
|
||||
this.modelValue.map(student => [
|
||||
student.uid + ' (' + student.vorname + ' ' + student.nachname + ')',
|
||||
ApiStvGroups.add(
|
||||
student.uid,
|
||||
gruppe_kurzbz,
|
||||
this.currentSemester
|
||||
)
|
||||
]),
|
||||
{
|
||||
errorHandling: {
|
||||
combine: { form: ['validation'] },
|
||||
handler: { form: this.multiFormHandler }
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(result => {
|
||||
const successes = result.filter(res => res.status == 'fulfilled');
|
||||
if (result.length == successes.length) {
|
||||
this.$refs.newGroupModal.hide();
|
||||
}
|
||||
if (successes.length) {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('gruppenmanagement/groups_added', { n: successes.length }));
|
||||
this.$refs.list.reload();
|
||||
}
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
} else {
|
||||
this.$refs.newGroupModal.$refs.form
|
||||
.call(ApiStvGroups.add(this.modelValue.uid, gruppe_kurzbz, this.currentSemester))
|
||||
.then(() => {
|
||||
this.$refs.newGroupModal.hide();
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('gruppenmanagement/groups_added', { n: 1 }));
|
||||
this.$refs.list.reload();
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
}
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="stv-details-groups h-100 d-flex flex-column">
|
||||
<h3>{{ $p.t('gruppenmanagement/special_groups') }}</h3>
|
||||
<groups-special
|
||||
ref="newGroupModal"
|
||||
:default-stg="sharedStg"
|
||||
@submit.capture.prevent="addSpecialGroup"
|
||||
/>
|
||||
<groups-list
|
||||
ref="list"
|
||||
class="mb-3"
|
||||
:students="modelValue"
|
||||
@new="showNewGroupModal"
|
||||
/>
|
||||
|
||||
<h3>{{ $p.t('lehre/lehrverband') }}</h3>
|
||||
<groups-lvb
|
||||
:students="modelValue"
|
||||
@submit="changeLvb"
|
||||
/>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import { CoreFilterCmpt } from "../../../../filter/Filter.js";
|
||||
|
||||
import ApiStvGroups from '../../../../../api/factory/stv/group.js';
|
||||
|
||||
export default {
|
||||
name: 'TabGroupsList',
|
||||
components: {
|
||||
CoreFilterCmpt
|
||||
},
|
||||
inject: [
|
||||
"currentSemester"
|
||||
],
|
||||
props: {
|
||||
students: Object
|
||||
},
|
||||
emits: [
|
||||
"new"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
phrasenLoaded: false,
|
||||
optionsReady: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tabulatorOptions() {
|
||||
let ajaxRequestFunc, ajaxResponse, initialFilter;
|
||||
if (Array.isArray(this.students)) {
|
||||
ajaxRequestFunc = () => {
|
||||
return this.$api.call(
|
||||
this.students.map(student => ApiStvGroups.getGruppen(student.uid))
|
||||
);
|
||||
};
|
||||
ajaxResponse = (url, params, response) => {
|
||||
return response.reduce((data, result) => [
|
||||
...data,
|
||||
...result.value.data
|
||||
], []);
|
||||
};
|
||||
initialFilter = [
|
||||
this.students.map(student => {
|
||||
return { field: "uid", type: "=", value: student.uid };
|
||||
}),
|
||||
[
|
||||
{ field: "studiensemester_kurzbz", type: "=", value: null },
|
||||
{ field: "studiensemester_kurzbz", type: "=", value: this.currentSemester }
|
||||
]
|
||||
];
|
||||
} else {
|
||||
ajaxRequestFunc = () => {
|
||||
return this.$api.call(
|
||||
ApiStvGroups.getGruppen(this.students.uid)
|
||||
);
|
||||
};
|
||||
ajaxResponse = (url, params, response) => {
|
||||
return response.data;
|
||||
};
|
||||
initialFilter = [
|
||||
{ field: "uid", type: "=", value: this.students.uid },
|
||||
[
|
||||
{ field: "studiensemester_kurzbz", type: "=", value: null },
|
||||
{ field: "studiensemester_kurzbz", type: "=", value: this.currentSemester }
|
||||
]
|
||||
];
|
||||
}
|
||||
return {
|
||||
ajaxURL: 'dummy',
|
||||
ajaxRequestFunc,
|
||||
ajaxResponse,
|
||||
initialFilter,
|
||||
columns: [
|
||||
{ title: this.$p.t('gruppenmanagement/gruppe'), field: "gruppe_kurzbz" },
|
||||
{ title: this.$p.t('ui/bezeichnung'), field: "bezeichnung" },
|
||||
{ title: this.$p.t('lehre/studiensemester'), field: "studiensemester_kurzbz" },
|
||||
{
|
||||
title: this.$p.t('gruppenmanagement/automatisch_generiert'),
|
||||
field: "generiert",
|
||||
formatter: "tickCross",
|
||||
hozAlign: "center",
|
||||
formatterParams: {
|
||||
tickElement: '<i class="fa fa-check text-success"></i>',
|
||||
crossElement: '<i class="fa fa-xmark text-danger"></i>'
|
||||
}
|
||||
},
|
||||
{ title: this.$p.t('ui/student_uid'), field: "uid" },
|
||||
{
|
||||
title: this.$p.t('global/actions'),
|
||||
field: 'actions',
|
||||
minWidth: 150, // Ensures Action-buttons will be always fully displayed
|
||||
formatter: (cell, formatterParams, onRendered) => {
|
||||
const container = document.createElement('div');
|
||||
container.className = "d-flex gap-2";
|
||||
|
||||
const data = cell.getData();
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'btn btn-outline-secondary btn-action';
|
||||
button.innerHTML = '<i class="fa fa-xmark"></i>';
|
||||
button.title = this.$p.t('ui', 'loeschen');
|
||||
button.addEventListener('click', () =>
|
||||
this.actionDeleteGroup(data)
|
||||
);
|
||||
if (data.generiert)
|
||||
button.disabled = true;
|
||||
container.append(button);
|
||||
|
||||
return container;
|
||||
},
|
||||
frozen: true
|
||||
}
|
||||
],
|
||||
height: 'auto',
|
||||
index: 'group_id',
|
||||
persistenceID: 'stv-details-group-list'
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tabulatorOptions() {
|
||||
// Refresh Tabulator if options have changed
|
||||
this.optionsReady = false;
|
||||
this.$nextTick(() => this.optionsReady = true);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
actionDeleteGroup(data) {
|
||||
this.$fhcAlert
|
||||
.confirmDelete()
|
||||
.then(result => result
|
||||
? data
|
||||
: Promise.reject({handled: true}))
|
||||
.then(this.deleteGroup)
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
deleteGroup(params) {
|
||||
return this.$api
|
||||
.call(ApiStvGroups.deleteGroup(params))
|
||||
.then(response => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
|
||||
this.reload();
|
||||
})
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
reload() {
|
||||
this.$refs.table.reloadTable();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$p
|
||||
.loadCategory(['global', 'lehre', 'ui', 'gruppenmanagement'])
|
||||
.then(() => {
|
||||
this.phrasenLoaded = true;
|
||||
});
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="stv-details-groups-list">
|
||||
<core-filter-cmpt
|
||||
v-if="phrasenLoaded && optionsReady"
|
||||
ref="table"
|
||||
:tabulator-options="tabulatorOptions"
|
||||
table-only
|
||||
:side-menu="false"
|
||||
reload
|
||||
:reload-btn-infotext="$p.t('table/reload')"
|
||||
new-btn-show
|
||||
:new-btn-label="$p.t('lehre/gruppe')"
|
||||
@click:new="$emit('new')"
|
||||
>
|
||||
</core-filter-cmpt>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
import FhcForm from "../../../../Form/Form.js";
|
||||
|
||||
import ApiStvLvb from '../../../../../api/factory/stv/lehrverband.js';
|
||||
|
||||
export default {
|
||||
name: 'TabGroupsLvb',
|
||||
components: {
|
||||
FhcForm
|
||||
},
|
||||
props: {
|
||||
students: Object
|
||||
},
|
||||
emits: [
|
||||
"submit"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
lvbList: [],
|
||||
selectedSemester: false,
|
||||
selectedVerband: false,
|
||||
selectedGruppe: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allAreStudents() {
|
||||
if (Array.isArray(this.students))
|
||||
return this.students.every(ps => ps.uid);
|
||||
return this.students.uid;
|
||||
},
|
||||
studiengang_kz() {
|
||||
if (Array.isArray(this.students)) {
|
||||
const first = this.students.find(Boolean);
|
||||
if (this.students.every(ps => ps.studiengang_kz === first.studiengang_kz))
|
||||
return first.studiengang_kz;
|
||||
return false;
|
||||
}
|
||||
return this.students.studiengang_kz;
|
||||
},
|
||||
semester: {
|
||||
get() {
|
||||
if (this.selectedSemester !== false) {
|
||||
if (this.lvbList.some(item => item.semester == this.selectedSemester))
|
||||
return this.selectedSemester;
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(this.students)) {
|
||||
const first = this.students.find(Boolean);
|
||||
if (this.lvbList.every(item => item.semester != first.semester))
|
||||
return false;
|
||||
if (this.students.every(ps => ps.semester === first.semester))
|
||||
return first.semester;
|
||||
return false;
|
||||
}
|
||||
if (this.lvbList.some(item => item.semester == this.students.semester))
|
||||
return this.students.semester;
|
||||
return false;
|
||||
},
|
||||
set(value) {
|
||||
this.selectedSemester = value;
|
||||
}
|
||||
},
|
||||
verband: {
|
||||
get() {
|
||||
if (this.semester === false)
|
||||
return false;
|
||||
if (this.selectedVerband !== false) {
|
||||
if (this.lvbListVerband.some(item => item.verband == this.selectedVerband))
|
||||
return this.selectedVerband;
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(this.students)) {
|
||||
const first = this.students.find(Boolean);
|
||||
if (this.lvbListVerband.every(item => item.verband != first.verband))
|
||||
return false;
|
||||
if (this.students.every(ps => ps.verband === first.verband))
|
||||
return first.verband;
|
||||
return false;
|
||||
}
|
||||
if (this.lvbListVerband.some(item => item.verband == this.students.verband))
|
||||
return this.students.verband;
|
||||
return false;
|
||||
},
|
||||
set(value) {
|
||||
this.selectedVerband = value;
|
||||
}
|
||||
},
|
||||
gruppe: {
|
||||
get() {
|
||||
if (this.verband === false)
|
||||
return false;
|
||||
if (this.selectedGruppe !== false) {
|
||||
if (this.lvbListGruppe.some(item => item.gruppe == this.selectedGruppe))
|
||||
return this.selectedGruppe;
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(this.students)) {
|
||||
const first = this.students.find(Boolean);
|
||||
if (this.lvbListGruppe.every(item => item.gruppe != first.gruppe))
|
||||
return false;
|
||||
if (this.students.every(ps => ps.gruppe === first.gruppe))
|
||||
return first.gruppe;
|
||||
return false;
|
||||
}
|
||||
if (this.lvbListGruppe.some(item => item.gruppe == this.students.gruppe))
|
||||
return this.students.gruppe;
|
||||
return false;
|
||||
},
|
||||
set(value) {
|
||||
this.selectedGruppe = value;
|
||||
}
|
||||
},
|
||||
stgSemester() {
|
||||
if (!this.lvbList.length)
|
||||
return [];
|
||||
|
||||
const semester = new Set(this.lvbList.map(lvb => lvb.semester));
|
||||
|
||||
return Array.from(semester).sort((a, b) => a - b);
|
||||
},
|
||||
lvbListVerband() {
|
||||
if (!this.lvbList.length)
|
||||
return [];
|
||||
if (this.semester === false)
|
||||
return [];
|
||||
|
||||
return this.lvbList.filter(lvb => this.semester == lvb.semester);
|
||||
},
|
||||
semesterVerband() {
|
||||
if (!this.lvbListVerband.length)
|
||||
return [];
|
||||
|
||||
const verband = new Set(this.lvbListVerband.map(lvb => lvb.verband.replace(/ /g, '')));
|
||||
|
||||
return Array.from(verband).filter(Boolean).sort();
|
||||
},
|
||||
lvbListGruppe() {
|
||||
if (!this.lvbListVerband.length)
|
||||
return [];
|
||||
if (this.verband === false)
|
||||
return [];
|
||||
|
||||
return this.lvbListVerband.filter(lvb => this.verband == lvb.verband);
|
||||
},
|
||||
verbandGruppe() {
|
||||
if (!this.lvbListGruppe.length)
|
||||
return [];
|
||||
|
||||
const gruppe = new Set(this.lvbListGruppe.map(lvb => lvb.gruppe.replace(/ /g, '')));
|
||||
|
||||
return Array.from(gruppe).filter(Boolean).sort();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
studiengang_kz() {
|
||||
this.loadGroupsForStg();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadGroupsForStg() {
|
||||
this.lvbList = [];
|
||||
|
||||
if (this.studiengang_kz === false)
|
||||
return;
|
||||
|
||||
this.$api
|
||||
.call(ApiStvLvb.getTree(this.studiengang_kz))
|
||||
.then(result => this.lvbList = result.data)
|
||||
.catch(this.$fhcAlert.handleSystemError)
|
||||
},
|
||||
onSubmit() {
|
||||
let params = {
|
||||
studiengang_kz: this.studiengang_kz,
|
||||
semester: this.semester,
|
||||
verband: this.verband,
|
||||
gruppe: this.gruppe
|
||||
};
|
||||
this.$emit("submit", params);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadGroupsForStg();
|
||||
},
|
||||
template: /* html */`
|
||||
<div class="stv-details-groups-lvb">
|
||||
<fhc-form
|
||||
v-if="allAreStudents && studiengang_kz"
|
||||
ref="form"
|
||||
class="input-group"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
<span class="input-group-text">
|
||||
{{ $p.t('lehre/semester') }}
|
||||
</span>
|
||||
<select
|
||||
v-model="semester"
|
||||
class="form-select"
|
||||
>
|
||||
<option v-for="semester in stgSemester">{{ semester }}</option>
|
||||
</select>
|
||||
<span
|
||||
class="input-group-text"
|
||||
:class="{'text-muted': semester === false}"
|
||||
>
|
||||
{{ $p.t('lehre/verband') }}
|
||||
</span>
|
||||
<select
|
||||
v-model="verband"
|
||||
class="form-select"
|
||||
:disabled="semester === false"
|
||||
>
|
||||
<option v-for="verband in semesterVerband">{{ verband }}</option>
|
||||
</select>
|
||||
<span
|
||||
class="input-group-text"
|
||||
:class="{'text-muted': verband === false}"
|
||||
>
|
||||
{{ $p.t('lehre/gruppe') }}
|
||||
</span>
|
||||
<select
|
||||
v-model="gruppe"
|
||||
class="form-select"
|
||||
:disabled="verband === false"
|
||||
>
|
||||
<option v-for="gruppe in verbandGruppe">{{ gruppe }}</option>
|
||||
</select>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="gruppe === false && verband === false"
|
||||
>
|
||||
{{ $p.t('ui/change') }}
|
||||
</button>
|
||||
</fhc-form>
|
||||
<div v-if="!allAreStudents" class="alert alert-danger">
|
||||
{{ $p.t('stv/groups_error_notallstudents') }}
|
||||
</div>
|
||||
<div v-if="!studiengang_kz" class="alert alert-danger">
|
||||
{{ $p.t('stv/groups_error_notsamestg') }}
|
||||
</div>
|
||||
</div>`
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import BsModal from "../../../../Bootstrap/Modal.js";
|
||||
import FhcForm from "../../../../Form/Form.js";
|
||||
import FormValidation from "../../../../Form/Validation.js";
|
||||
import FormInput from "../../../../Form/Input.js";
|
||||
|
||||
import ApiStvGroups from '../../../../../api/factory/stv/group.js';
|
||||
|
||||
export default {
|
||||
name: 'TabGroupsSpecial',
|
||||
components: {
|
||||
BsModal,
|
||||
FhcForm,
|
||||
FormValidation,
|
||||
FormInput,
|
||||
PvAutocomplete: primevue.autocomplete
|
||||
},
|
||||
props: {
|
||||
defaultStg: Number
|
||||
},
|
||||
emits: [
|
||||
"chosen"
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
groupSuggestions: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.$refs.popup.show();
|
||||
},
|
||||
hide() {
|
||||
this.$refs.popup.hide();
|
||||
},
|
||||
getGroupSuggestions({ query }) {
|
||||
this.$api
|
||||
.call(ApiStvGroups.search(query, this.defaultStg))
|
||||
.then(result => this.groupSuggestions = result.data)
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
},
|
||||
onSubmit(evt) {
|
||||
if (!evt.defaultPrevented) {
|
||||
evt.preventDefault();
|
||||
this.hide();
|
||||
}
|
||||
},
|
||||
onEnter() {
|
||||
/**
|
||||
* NOTE(chris): PrimeVue: AutoComplete: Enter does not submit form #5618
|
||||
* @see https://github.com/primefaces/primevue/issues/5618
|
||||
*
|
||||
* this is fixed in 3.52.0
|
||||
* until then this function fill fix it
|
||||
*/
|
||||
if (!this.$refs.autocomplete.$refs.input.overlayVisible) {
|
||||
this.$refs.form.$el.requestSubmit();
|
||||
}
|
||||
},
|
||||
modalOpened() {
|
||||
this.$refs.autocomplete.$refs.input.$refs.focusInput.focus();
|
||||
},
|
||||
modalClosed() {
|
||||
this.value = '';
|
||||
this.$refs.form.clearValidation();
|
||||
}
|
||||
},
|
||||
template: /* html */`
|
||||
<bs-modal
|
||||
ref="popup"
|
||||
class="stv-details-groups-special"
|
||||
@hide-bs-modal="modalClosed"
|
||||
@shown-bs-modal="modalOpened"
|
||||
>
|
||||
<template #title>
|
||||
{{ $p.t('gruppenmanagement/add_group') }}
|
||||
</template>
|
||||
<fhc-form ref="form" @submit="onSubmit">
|
||||
<form-validation />
|
||||
<div class="input-group">
|
||||
<form-input
|
||||
ref="autocomplete"
|
||||
type="autocomplete"
|
||||
name="gruppe_kurzbz"
|
||||
v-model="value"
|
||||
container-class="flex-grow-1"
|
||||
input-class="w-100"
|
||||
:suggestions="groupSuggestions"
|
||||
:option-label="el => el.gruppe_kurzbz + (el.bezeichnung ? ' (' + el.bezeichnung + ')' : '')"
|
||||
@complete="getGroupSuggestions"
|
||||
@keydown.enter.capture="onEnter"
|
||||
/>
|
||||
<button type="submit" class="btn btn-primary">{{ $p.t('ui/hinzufuegen') }}</button>
|
||||
</div>
|
||||
</fhc-form>
|
||||
</bs-modal>`
|
||||
};
|
||||
@@ -1,187 +0,0 @@
|
||||
import {CoreFilterCmpt} from "../../../../filter/Filter.js";
|
||||
|
||||
import ApiStvGroups from '../../../../../api/factory/stv/group.js';
|
||||
|
||||
export default {
|
||||
name: 'TblGroups',
|
||||
components: {
|
||||
CoreFilterCmpt,
|
||||
},
|
||||
inject: {
|
||||
currentSemester: {
|
||||
from: 'currentSemester',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
student: Object
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabulatorOptions: {
|
||||
ajaxURL: 'dummy',
|
||||
ajaxRequestFunc: () => this.$api.call(
|
||||
ApiStvGroups.getGruppen(this.student.uid)
|
||||
),
|
||||
ajaxResponse: (url, params, response) => response.data,
|
||||
initialFilter: {
|
||||
logic: "and",
|
||||
filters: [
|
||||
{ field: "uid", operator: "eq", value: this.student.uid },
|
||||
{
|
||||
logic: "or",
|
||||
filters: [
|
||||
{ field: "studiensemester_kurzbz", operator: "eq", value: null },
|
||||
{ field: "studiensemester_kurzbz", operator: "eq", value: this.currentSemester }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
columns: [
|
||||
{title: "Gruppe", field: "gruppe_kurzbz"},
|
||||
{title: "Bezeichnung", field: "bezeichnung"},
|
||||
{title: "Semester", field: "studiensemester_kurzbz"},
|
||||
{
|
||||
title: "automatisch generiert",
|
||||
field: "generiert",
|
||||
formatter: "tickCross",
|
||||
hozAlign: "center",
|
||||
formatterParams: {
|
||||
tickElement: '<i class="fa fa-check text-success"></i>',
|
||||
crossElement: '<i class="fa fa-xmark text-danger"></i>'
|
||||
}
|
||||
},
|
||||
{title: "UID", field: "uid"},
|
||||
{
|
||||
title: 'Aktionen', field: 'actions',
|
||||
minWidth: 150, // Ensures Action-buttons will be always fully displayed
|
||||
formatter: (cell, formatterParams, onRendered) => {
|
||||
const container = document.createElement('div');
|
||||
container.className = "d-flex gap-2";
|
||||
|
||||
const data = cell.getData();
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'btn btn-outline-secondary btn-action';
|
||||
button.innerHTML = '<i class="fa fa-xmark"></i>';
|
||||
button.title = this.$p.t('ui', 'loeschen');
|
||||
button.addEventListener('click', () =>
|
||||
this.actionDeleteGroup(data.gruppe_kurzbz)
|
||||
);
|
||||
if (data.generiert)
|
||||
button.disabled = true;
|
||||
container.append(button);
|
||||
|
||||
return container;
|
||||
},
|
||||
frozen: true
|
||||
},
|
||||
],
|
||||
layout: 'fitDataFill',
|
||||
height: 'auto',
|
||||
index: 'group_id',
|
||||
persistenceID: 'stv-details-gruppe'
|
||||
},
|
||||
tabulatorEvents: [
|
||||
{
|
||||
event: 'tableBuilt',
|
||||
handler: async () => {
|
||||
|
||||
await this.$p.loadCategory(['global', 'person', 'stv', 'ui', 'gruppenmanagement']);
|
||||
|
||||
let cm = this.$refs.table.tabulator.columnManager;
|
||||
|
||||
cm.getColumnByField('gruppe_kurzbz').component.updateDefinition({
|
||||
title: this.$p.t('gruppenmanagement', 'gruppe')
|
||||
});
|
||||
|
||||
cm.getColumnByField('bezeichnung').component.updateDefinition({
|
||||
title: this.$p.t('ui', 'bezeichnung')
|
||||
});
|
||||
|
||||
cm.getColumnByField('generiert').component.updateDefinition({
|
||||
title: this.$p.t('gruppenmanagement', 'automatisch_generiert')
|
||||
});
|
||||
|
||||
cm.getColumnByField('uid').component.updateDefinition({
|
||||
title: this.$p.t('ui', 'student_uid')
|
||||
});
|
||||
|
||||
//Interference with Filter if not commented out
|
||||
/*
|
||||
cm.getColumnByField('studiensemester_kurzbz').component.updateDefinition({
|
||||
title: this.$p.t('lehre', 'studiensemester')
|
||||
});*/
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
actionDeleteGroup(gruppe_kurzbz) {
|
||||
this.$fhcAlert
|
||||
.confirmDelete()
|
||||
.then(result => result
|
||||
? gruppe_kurzbz
|
||||
: Promise.reject({handled: true}))
|
||||
.then(this.deleteGroup)
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
|
||||
},
|
||||
deleteGroup(gruppe_kurzbz) {
|
||||
const group_id = {
|
||||
id: this.student.uid,
|
||||
gruppe_kurzbz: gruppe_kurzbz
|
||||
};
|
||||
|
||||
return this.$api
|
||||
.call(ApiStvGroups.deleteGroup(group_id))
|
||||
.then(response => {
|
||||
this.$fhcAlert.alertSuccess(this.$p.t('ui', 'successDelete'));
|
||||
}).catch(this.$fhcAlert.handleSystemError)
|
||||
.finally(() => {
|
||||
window.scrollTo(0, 0);
|
||||
this.reload();
|
||||
});
|
||||
},
|
||||
reload() {
|
||||
this.$refs.table.reloadTable();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentSemester(newVal) {
|
||||
if (newVal) {
|
||||
this.$refs.table.tabulator.clearFilter(); // Clear old filters
|
||||
|
||||
this.$refs.table.tabulator.setFilter((data) => {
|
||||
return (
|
||||
data.uid === this.student.uid &&
|
||||
(
|
||||
data.studiensemester_kurzbz === newVal ||
|
||||
data.studiensemester_kurzbz === null
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
student() {
|
||||
this.$refs.table.reloadTable();
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="stv-details-gruppen h-100 pb-3">
|
||||
<h5>{{$p.t('stv', 'tab_groups')}}</h5>
|
||||
|
||||
<core-filter-cmpt
|
||||
ref="table"
|
||||
:tabulator-options="tabulatorOptions"
|
||||
:tabulator-events="tabulatorEvents"
|
||||
table-only
|
||||
:side-menu="false"
|
||||
reload
|
||||
:reload-btn-infotext="this.$p.t('table', 'reload')"
|
||||
>
|
||||
</core-filter-cmpt>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import {CoreFilterCmpt} from "../../filter/Filter.js";
|
||||
import ListNew from './List/New.js';
|
||||
|
||||
import draggable from '../../../directives/draggable.js';
|
||||
|
||||
export default {
|
||||
name: "ListPrestudents",
|
||||
@@ -8,11 +9,18 @@ export default {
|
||||
CoreFilterCmpt,
|
||||
ListNew
|
||||
},
|
||||
directives: {
|
||||
draggable
|
||||
},
|
||||
inject: {
|
||||
'lists': {
|
||||
lists: {
|
||||
from: 'lists',
|
||||
required: true
|
||||
},
|
||||
$reloadList: {
|
||||
from: '$reloadList',
|
||||
required: true
|
||||
},
|
||||
currentSemester: {
|
||||
from: 'currentSemester',
|
||||
required: true
|
||||
@@ -51,8 +59,12 @@ export default {
|
||||
{title:"Vornamen", field:"vornamen", visible:false, headerFilter: true},
|
||||
{title:"TitelPost", field:"titelpost", headerFilter: "list", headerFilterParams: {valuesLookup:true, listOnEmpty:true, autocomplete:true, sort:"asc"}},
|
||||
{title:"Ersatzkennzeichen", field:"ersatzkennzeichen", headerFilter: true},
|
||||
{title:"Geburtsdatum", field:"gebdatum", formatter:dateFormatter,
|
||||
headerFilter: true, headerFilterFunc: function(headerValue, rowValue, rowData, filterParams) {
|
||||
{
|
||||
title: "Geburtsdatum",
|
||||
field: "gebdatum",
|
||||
formatter: dateFormatter,
|
||||
headerFilter: true,
|
||||
headerFilterFunc(headerValue, rowValue) {
|
||||
const matches = headerValue.match(/^(([0-9]{2})\.)?([0-9]{2})\.([0-9]{4})?$/);
|
||||
let comparestr = headerValue;
|
||||
if(matches !== null) {
|
||||
@@ -165,6 +177,39 @@ export default {
|
||||
currentEndpointRawUrl: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
countsToHTML: function() {
|
||||
return this.$p.t('global/ausgewaehlt')
|
||||
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gefiltert')
|
||||
+ ': '
|
||||
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gesamt')
|
||||
+ ': <strong>' + (this.count || 0) + '</strong>';
|
||||
},
|
||||
selectedDragObject() {
|
||||
return this.selected.map(item => {
|
||||
let type, id;
|
||||
if (item.uid) {
|
||||
type = 'student';
|
||||
id = item.uid;
|
||||
} else if (item.prestudent_id) {
|
||||
type = 'prestudent';
|
||||
id = item.prestudent_id;
|
||||
} else if (item.person_id) {
|
||||
type = 'person';
|
||||
id = item.person_id;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
type,
|
||||
id
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reload() {
|
||||
this.$refs.table.reloadTable();
|
||||
@@ -172,10 +217,19 @@ export default {
|
||||
actionNewPrestudent() {
|
||||
this.$refs.new.open();
|
||||
},
|
||||
rowSelectionChanged(data) {
|
||||
rowSelectionChanged(data, rows) {
|
||||
this.selectedcount = data.length;
|
||||
this.lastSelected = this.selected;
|
||||
this.$emit('update:selected', data);
|
||||
|
||||
// set selected elements draggable
|
||||
const tableEl = this.$refs.table?.$refs?.table;
|
||||
if (tableEl) {
|
||||
const oldDragables = tableEl.querySelectorAll('[draggable]');
|
||||
for (const draggable of oldDragables)
|
||||
draggable.removeAttribute('draggable');
|
||||
}
|
||||
rows.forEach(row => row.getElement().draggable = true);
|
||||
},
|
||||
autoSelectRows(data) {
|
||||
if (this.lastSelected) {
|
||||
@@ -234,14 +288,22 @@ export default {
|
||||
} else
|
||||
this.$refs.table.tabulator.setData(endpoint.url, params);
|
||||
},
|
||||
dragCleanup(evt) {
|
||||
if (evt.dataTransfer.dropEffect == 'none')
|
||||
return; // aborted or wrong target
|
||||
|
||||
this.$reloadList();
|
||||
},
|
||||
onKeydown(e) { // TODO(chris): this should be in the filter component
|
||||
if (!this.focusObj)
|
||||
return;
|
||||
|
||||
var next;
|
||||
switch (e.code) {
|
||||
case 'Enter':
|
||||
case 'Space':
|
||||
e.preventDefault();
|
||||
const e2 = new Event('click', e);
|
||||
var e2 = new Event('click', e);
|
||||
e2.altKey = e.altKey;
|
||||
e2.ctrlKey = e.ctrlKey;
|
||||
e2.shiftKey = e.shiftKey;
|
||||
@@ -250,13 +312,13 @@ export default {
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
var next = this.focusObj.previousElementSibling;
|
||||
next = this.focusObj.previousElementSibling;
|
||||
if (next)
|
||||
this.changeFocus(this.focusObj, next);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
var next = this.focusObj.nextElementSibling;
|
||||
next = this.focusObj.nextElementSibling;
|
||||
if (next)
|
||||
this.changeFocus(this.focusObj, next);
|
||||
break;
|
||||
@@ -296,24 +358,19 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
countsToHTML: function() {
|
||||
return this.$p.t('global/ausgewaehlt')
|
||||
+ ': <strong>' + (this.selectedcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gefiltert')
|
||||
+ ': '
|
||||
+ '<strong>' + (this.filteredcount || 0) + '</strong>'
|
||||
+ ' | '
|
||||
+ this.$p.t('global/gesamt')
|
||||
+ ': <strong>' + (this.count || 0) + '</strong>';
|
||||
}
|
||||
},
|
||||
// TODO(chris): focusin, focusout, keydown and tabindex should be in the filter component
|
||||
// TODO(chris): filter component column chooser has no accessibilty features
|
||||
template: `
|
||||
<div class="stv-list h-100 pt-3">
|
||||
<div class="tabulator-container d-flex flex-column h-100" :class="{'has-filter': filterKontoCount0 || filterKontoMissingCounter}" tabindex="0" @focusin="onFocus" @keydown="onKeydown">
|
||||
<div
|
||||
class="tabulator-container d-flex flex-column h-100"
|
||||
:class="{'has-filter': filterKontoCount0 || filterKontoMissingCounter}"
|
||||
tabindex="0"
|
||||
@focusin="onFocus"
|
||||
@keydown="onKeydown"
|
||||
v-draggable:copyLink.capture="selectedDragObject"
|
||||
@dragend="dragCleanup"
|
||||
>
|
||||
<core-filter-cmpt
|
||||
ref="table"
|
||||
:description="countsToHTML"
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
import ApiStvVerband from '../../../api/factory/stv/verband.js';
|
||||
import drop from '../../../directives/drop.js';
|
||||
import dragClick from '../../../directives/dragClick.js';
|
||||
|
||||
import ApiStvGroups from '../../../api/factory/stv/group.js';
|
||||
import ApiStvDetails from '../../../api/factory/stv/details.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PvTreetable: primevue.treetable,
|
||||
PvColumn: primevue.column
|
||||
},
|
||||
directives: {
|
||||
drop,
|
||||
dragClick
|
||||
},
|
||||
inject: {
|
||||
$reloadList: {
|
||||
from: '$reloadList',
|
||||
required: true
|
||||
},
|
||||
currentSemester: {
|
||||
from: 'currentSemester',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'selectVerband'
|
||||
],
|
||||
@@ -225,6 +243,51 @@ export default {
|
||||
|
||||
this.selectedKey = {[currentNode.key]: true};
|
||||
this.onSelectTreeNode(currentNode);
|
||||
},
|
||||
async toggleTreeNode(node) {
|
||||
if (this.expandedKeys[node.key]) {
|
||||
delete this.expandedKeys[node.key];
|
||||
} else if (!node.leaf) {
|
||||
await this.onExpandTreeNode(node);
|
||||
this.expandedKeys[node.key] = true;
|
||||
}
|
||||
},
|
||||
getStudentAjaxId(student) {
|
||||
let res = student.id;
|
||||
if (student.vorname && student.nachname)
|
||||
res += ' (' + student.vorname + ' ' + student.nachname + ')';
|
||||
return res;
|
||||
},
|
||||
dropStudents(node, students) {
|
||||
const data = node.data;
|
||||
|
||||
let endpoint;
|
||||
if (data.gruppe_kurzbz) {
|
||||
endpoint = students.map(student => [
|
||||
this.getStudentAjaxId(student),
|
||||
ApiStvGroups.add(
|
||||
student.id,
|
||||
data.gruppe_kurzbz,
|
||||
this.currentSemester
|
||||
)
|
||||
]);
|
||||
} else {
|
||||
const { semester, verband, gruppe } = data;
|
||||
const params = { semester, verband, gruppe };
|
||||
endpoint = students.map(student => [
|
||||
this.getStudentAjaxId(student),
|
||||
ApiStvDetails.saveStudent(
|
||||
student.id,
|
||||
this.currentSemester,
|
||||
params
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
return this.$api
|
||||
.call(endpoint)
|
||||
.then(this.$reloadList)
|
||||
.catch(this.$fhcAlert.handleSystemError);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -286,10 +349,21 @@ export default {
|
||||
</template>
|
||||
<template #body="{ node }">
|
||||
<span
|
||||
v-if="['semester', 'verband', 'gruppe', 'gruppe_kurzbz'].some(key => node.data.hasOwnProperty(key))"
|
||||
:data-tree-item-key="node.key"
|
||||
:title="node.data.studiengang_kz"
|
||||
v-drag-click="() => toggleTreeNode(node)"
|
||||
v-drop:link-strict.student-collection="(evt, students) => dropStudents(node, students)"
|
||||
>
|
||||
{{node.data.name}}
|
||||
{{ node.data.name }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
:data-tree-item-key="node.key"
|
||||
:title="node.data.studiengang_kz"
|
||||
v-drag-click="() => toggleTreeNode(node)"
|
||||
>
|
||||
{{ node.data.name }}
|
||||
</span>
|
||||
</template>
|
||||
</pv-column>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { bindDragEnterLeave } from '../helpers/DragAndDrop.js';
|
||||
|
||||
export default {
|
||||
mounted(el, binding) {
|
||||
const delay = parseInt(binding.arg) || 300;
|
||||
|
||||
let timeout = null;
|
||||
function startCountdown() {
|
||||
timeout = window.setTimeout(binding.value, delay);
|
||||
}
|
||||
function stopCountdown() {
|
||||
if (timeout)
|
||||
window.clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
|
||||
let lastTarget;
|
||||
let lastX;
|
||||
let lastY;
|
||||
|
||||
function onDragOver(evt) {
|
||||
if (lastX != evt.offsetX || lastY != evt.offsetY || lastTarget != evt.target) {
|
||||
// moved
|
||||
lastTarget = evt.target;
|
||||
lastX = evt.offsetX;
|
||||
lastY = evt.offsetY;
|
||||
|
||||
stopCountdown();
|
||||
startCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
function onEnter(evt) {
|
||||
lastTarget = evt.target;
|
||||
lastX = evt.offsetX;
|
||||
lastY = evt.offsetY;
|
||||
|
||||
el.addEventListener('dragover', onDragOver);
|
||||
|
||||
startCountdown();
|
||||
}
|
||||
function onLeave() {
|
||||
stopCountdown();
|
||||
el.removeEventListener('dragover', onDragOver);
|
||||
}
|
||||
|
||||
el.fhcDragClickCleanup = bindDragEnterLeave(el, onEnter, onLeave);
|
||||
},
|
||||
beforeUnmount(el) {
|
||||
el.fhcDragClickCleanup();
|
||||
delete el.fhcDragClickCleanup;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { setTransferData, convertToValidDragObject } from '../helpers/DragAndDrop.js';
|
||||
|
||||
const EFFECTS = [
|
||||
'none',
|
||||
'copy',
|
||||
'copyLink',
|
||||
'copyMove',
|
||||
'link',
|
||||
'linkMove',
|
||||
'move',
|
||||
'all',
|
||||
'uninitialized'
|
||||
];
|
||||
|
||||
export default {
|
||||
mounted(el, binding) {
|
||||
updateValue(el, binding.value);
|
||||
updateEffectAllowed(el, binding.arg);
|
||||
|
||||
// if modifier capture is set we assume it's on a parent element
|
||||
// i.e: for dragging multiple elements
|
||||
// otherwise set draggable attribute
|
||||
if (!binding.modifiers.capture) {
|
||||
el.draggable = true;
|
||||
}
|
||||
|
||||
const bcc = new BroadcastChannel('fhc-dnd');
|
||||
let blocked = false;
|
||||
|
||||
function onStart(evt) {
|
||||
const value = el.dataset.fhcDraggableValue;
|
||||
if (value) {
|
||||
setTransferData(evt, JSON.parse(value), true);
|
||||
if (el.dataset.fhcEffectAllowed)
|
||||
evt.dataTransfer.effectAllowed = el.dataset.fhcEffectAllowed;
|
||||
|
||||
bcc.onmessage = e => {
|
||||
if (e.data == 'block') {
|
||||
blocked = true;
|
||||
} else if (e.data == 'release') {
|
||||
let evt = null;
|
||||
if (blocked && blocked.evt) {
|
||||
evt = blocked.evt;
|
||||
}
|
||||
blocked = false;
|
||||
if (evt)
|
||||
el.dispatchEvent(evt);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function onEnd(evt) {
|
||||
if (blocked) {
|
||||
blocked = {
|
||||
evt,
|
||||
dt: evt.dataTransfer
|
||||
};
|
||||
evt.stopPropagation();
|
||||
el.dispatchEvent(new DragEvent("beforedragend", evt));
|
||||
} else {
|
||||
bcc.onmessage = () => {};
|
||||
}
|
||||
}
|
||||
el.addEventListener('dragstart', onStart, binding.modifiers.capture);
|
||||
|
||||
el.addEventListener('dragend', onEnd, true);
|
||||
|
||||
el.fhcDraggableCleanup = () => {
|
||||
el.removeEventListener('dragstart', onStart, binding.modifiers.capture);
|
||||
el.removeEventListener('dragend', onEnd, true);
|
||||
};
|
||||
},
|
||||
updated(el, binding) {
|
||||
updateValue(el, binding.value);
|
||||
updateEffectAllowed(el, binding.arg);
|
||||
},
|
||||
beforeUnmount(el) {
|
||||
el.fhcDraggableCleanup();
|
||||
delete el.fhcDraggableCleanup;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function updateValue(el, value) {
|
||||
value = convertToValidDragObject(value);
|
||||
if (value) {
|
||||
el.dataset.fhcDraggableValue = JSON.stringify(value);
|
||||
} else if (el.dataset.fhcDraggableValue) {
|
||||
delete el.dataset.fhcDraggableValue;
|
||||
}
|
||||
}
|
||||
function updateEffectAllowed(el, effectAllowed) {
|
||||
if (effectAllowed && EFFECTS.includes(effectAllowed)) {
|
||||
el.dataset.fhcEffectAllowed = effectAllowed;
|
||||
} else if (el.dataset.fhcEffectAllowed) {
|
||||
delete el.dataset.fhcEffectAllowed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { getValidTransferData, eventHasTypes, bindDragEnterLeave } from '../helpers/DragAndDrop.js';
|
||||
|
||||
const EFFECTS = [
|
||||
'move',
|
||||
'copy',
|
||||
'link',
|
||||
'none'
|
||||
];
|
||||
|
||||
export default {
|
||||
mounted(el, binding) {
|
||||
const allowedTypes = Object.keys(binding.modifiers);
|
||||
allowedTypes.forEach(type => {
|
||||
if (type.substr(-11) == '-collection') {
|
||||
const singleType = type.substr(0, type.length-11);
|
||||
if (!allowedTypes.includes(singleType))
|
||||
allowedTypes.push(singleType);
|
||||
}
|
||||
});
|
||||
|
||||
const strict = binding.arg.match(/(strict-|-strict)/);
|
||||
const arg = binding.arg.replace(/(strict-|-strict)/, '');
|
||||
const effect = EFFECTS.includes(arg) ? arg : null;
|
||||
|
||||
const bcc = new BroadcastChannel('fhc-dnd');
|
||||
let allowed = false;
|
||||
|
||||
function onEnter(evt) {
|
||||
allowed = eventHasTypes(evt, allowedTypes, strict);
|
||||
if (allowed) {
|
||||
evt.preventDefault();
|
||||
bcc.postMessage('block');
|
||||
}
|
||||
}
|
||||
function onLeave(evt, wasDropped) {
|
||||
if (allowed && !wasDropped) {
|
||||
bcc.postMessage('release');
|
||||
}
|
||||
}
|
||||
function onOver(evt) {
|
||||
if (allowed) {
|
||||
evt.preventDefault();
|
||||
if (effect)
|
||||
evt.dataTransfer.dropEffect = effect;
|
||||
}
|
||||
}
|
||||
function onDrop(evt) {
|
||||
let result = getValidTransferData(evt, allowedTypes, strict);
|
||||
if (!Array.isArray(result) && !binding.modifiers[result.type] && allowedTypes.includes(result.type + '-collection'))
|
||||
result = [result];
|
||||
|
||||
const res = binding.value(evt, result);
|
||||
|
||||
if (res instanceof Promise) {
|
||||
res.then(r => {
|
||||
bcc.postMessage('release');
|
||||
return r;
|
||||
});
|
||||
} else {
|
||||
bcc.postMessage('release');
|
||||
}
|
||||
}
|
||||
|
||||
const cleanupEnterLeave = bindDragEnterLeave(el, onEnter, onLeave);
|
||||
el.addEventListener('dragover', onOver);
|
||||
el.addEventListener('drop', onDrop);
|
||||
el.fhcDropCleanup = () => {
|
||||
cleanupEnterLeave();
|
||||
el.removeEventListener('dragover', onOver);
|
||||
el.removeEventListener('drop', onDrop);
|
||||
};
|
||||
},
|
||||
beforeUnmount(el) {
|
||||
el.fhcDropCleanup();
|
||||
delete el.fhcDropCleanup;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,368 @@
|
||||
/**
|
||||
* TODO(chris): This is only a prototype!!!
|
||||
*/
|
||||
const DragAndDrop = {
|
||||
TYPE_LE: "lehreinheit",
|
||||
TYPE_VEVENT: "vevent",
|
||||
|
||||
getValidTransferData(event, allowedTypes) {
|
||||
const json = event.dataTransfer.getData('text');
|
||||
let obj;
|
||||
try {
|
||||
obj = JSON.parse(json);
|
||||
if (!obj.type)
|
||||
return null;
|
||||
if (allowedTypes && !allowedTypes.includes(obj.type))
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return obj;
|
||||
const TYPE_DEFINITION = {
|
||||
lehreinheit: {
|
||||
id: "lehreinheit_id",
|
||||
dragIcon: "fa-solid fa-chalkboard-user",
|
||||
extras: [
|
||||
"stundenblockung"
|
||||
]
|
||||
},
|
||||
isValidTransferData(event, allowedTypes) {
|
||||
return this.getValidTransferData(event, allowedTypes) ? true : false;
|
||||
vevent: {
|
||||
id: "uid",
|
||||
dragIcon: "fa-solid fa-calendar",
|
||||
extras: [
|
||||
"dtstart",
|
||||
"dtend",
|
||||
"summary"
|
||||
]
|
||||
},
|
||||
getTransferData(event) {
|
||||
const json = event.dataTransfer.getData('text');
|
||||
return JSON.parse(json);
|
||||
person: {
|
||||
id: "person_id",
|
||||
dragIcon: "fa-solid fa-user"
|
||||
},
|
||||
setTransferData(event, data) {
|
||||
switch (data.type) {
|
||||
case DragAndDrop.TYPE_LE:
|
||||
data = DragAndDrop.fromLe(data);
|
||||
break;
|
||||
default:
|
||||
if (data.dtstart && data.dtend && data.uid && data.summary) {
|
||||
data = DragAndDrop.fromVEvent(data);
|
||||
break;
|
||||
}
|
||||
return false; // No type found => abort
|
||||
}
|
||||
|
||||
event.dataTransfer.setData('text', JSON.stringify(data));
|
||||
return true;
|
||||
student: {
|
||||
id: "student_uid",
|
||||
dragIcon: "fa-solid fa-user-graduate"
|
||||
},
|
||||
fromLe(data) {
|
||||
const {
|
||||
type = DragAndDrop.TYPE_LE,
|
||||
lehreinheit_id: id,
|
||||
stundenblockung
|
||||
} = data;
|
||||
|
||||
return { type, id, stundenblockung };
|
||||
},
|
||||
fromVEvent(data) {
|
||||
const {
|
||||
type = DragAndDrop.TYPE_VEVENT,
|
||||
uid: id,
|
||||
dtstart,
|
||||
dtend,
|
||||
summary
|
||||
} = data;
|
||||
|
||||
return { type, id, dtstart, dtend, summary };
|
||||
prestudent: {
|
||||
id: "prestudent_id",
|
||||
dragIcon: "fa-solid fa-user-graduate text-muted"
|
||||
}
|
||||
// TODO: IMPLEMENT OTHER TYPES
|
||||
};
|
||||
|
||||
export default DragAndDrop;
|
||||
const VALID_TYPES = Object.keys(TYPE_DEFINITION);
|
||||
|
||||
const TYPE_CONSTANTS = Object.keys(TYPE_DEFINITION).reduce((res, type) => {
|
||||
res['TYPE_' + type.toUpperCase()] = type;
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
function isValidDragObject(value) {
|
||||
if (!value)
|
||||
return false;
|
||||
if (Array.isArray(value))
|
||||
return value.every(isValidDragObject);
|
||||
if (!value.type)
|
||||
return false;
|
||||
|
||||
if (value.type.substr(-11) == '-collection') {
|
||||
if (!Object.prototype.hasOwnProperty.call(value, 'values'))
|
||||
return false;
|
||||
|
||||
if (!VALID_TYPES.includes(value.type.substr(0, value.type.length-11)))
|
||||
return false;
|
||||
} else {
|
||||
if (!Object.prototype.hasOwnProperty.call(value, 'id'))
|
||||
return false;
|
||||
|
||||
if (!VALID_TYPES.includes(value.type))
|
||||
return false;
|
||||
|
||||
if (TYPE_DEFINITION[value.type].extras) {
|
||||
if (!TYPE_DEFINITION[value.type].extras.every(extra => Object.prototype.hasOwnProperty.call(value, extra)))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getValidTransferData(event, allowedTypes, strict) {
|
||||
let obj = null;
|
||||
|
||||
try {
|
||||
obj = getTransferData(event, strict);
|
||||
if (!obj)
|
||||
return null;
|
||||
|
||||
if (!strict && Array.isArray(obj)) {
|
||||
obj = obj.filter(isValidDragObject);
|
||||
if (!obj.length)
|
||||
return null;
|
||||
} else if (!isValidDragObject(obj))
|
||||
return null;
|
||||
|
||||
if (allowedTypes && allowedTypes.length) {
|
||||
if (Array.isArray(obj)) {
|
||||
if (strict && !obj.every(v => allowedTypes.includes(v.type))) {
|
||||
return null;
|
||||
} else if (!strict) {
|
||||
obj = obj.filter(v => allowedTypes.includes(v.type));
|
||||
if (!obj.length)
|
||||
return null;
|
||||
}
|
||||
} else if (!allowedTypes.includes(obj.type)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch(_error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj) && obj.length == 1)
|
||||
return obj.find(Boolean);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function isValidTransferData(event, allowedTypes, strict) {
|
||||
return getValidTransferData(event, allowedTypes, strict) ? true : false;
|
||||
}
|
||||
|
||||
function getTransferData(event, strict) {
|
||||
const result = [];
|
||||
|
||||
for (const type of event.dataTransfer.types) {
|
||||
if (type.substr(0, 16) != 'application/fhc-') {
|
||||
if (strict)
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
let base_type = type.substr(16);
|
||||
let collection = false;
|
||||
if (base_type.substr(-11) == '-collection') {
|
||||
base_type = base_type.substr(0, base_type.length-11);
|
||||
collection = true;
|
||||
}
|
||||
if (!VALID_TYPES.includes(base_type)) {
|
||||
if (strict)
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
let data = JSON.parse(event.dataTransfer.getData(type));
|
||||
if (collection)
|
||||
result.push(...data.values);
|
||||
else
|
||||
result.push(data);
|
||||
}
|
||||
|
||||
if (!result.length)
|
||||
return null;
|
||||
|
||||
if (result.length == 1)
|
||||
return result[0];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function convertToValidDragObject(data, strict) {
|
||||
if (Array.isArray(data)) {
|
||||
const converted = data.map(convertToValidDragObject).filter(Boolean);
|
||||
if (!converted.length)
|
||||
return undefined;
|
||||
if (strict && converted.length != data.length)
|
||||
return undefined;
|
||||
|
||||
const sorted = converted.reduce((res, item) => {
|
||||
if (!res[item.type])
|
||||
res[item.type] = [];
|
||||
res[item.type].push(item);
|
||||
return res;
|
||||
}, {});
|
||||
|
||||
return Object.entries(sorted).map(([type, values]) => {
|
||||
if (values.length > 1) {
|
||||
return {
|
||||
type: type + '-collection',
|
||||
values
|
||||
};
|
||||
}
|
||||
return values[0];
|
||||
});
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'type') && isValidDragObject(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const found = Object.entries(TYPE_DEFINITION).find(([ , typedef ]) => {
|
||||
if (!Object.prototype.hasOwnProperty.call(data, typedef.id))
|
||||
return false;
|
||||
if (typedef.extras) {
|
||||
if (!typedef.extras.every(extra => Object.prototype.hasOwnProperty.call(data, extra)))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [ type, typedef ] = found;
|
||||
|
||||
const newData = {};
|
||||
newData.type = type;
|
||||
newData.id = data[typedef.id];
|
||||
if (typedef.extras)
|
||||
typedef.extras.forEach(extra => newData[extra] = data[extra]);
|
||||
|
||||
return newData;
|
||||
}
|
||||
|
||||
function setTransferData(event, validDragObject, setDragImage = false) {
|
||||
if (setDragImage) {
|
||||
const dragItems = Array.isArray(validDragObject) ? validDragObject : [ validDragObject ];
|
||||
const dragElements = dragItems.map(item => {
|
||||
const icon = document.createElement('i');
|
||||
const label = document.createElement('span');
|
||||
const iconContainer = document.createElement('span');
|
||||
|
||||
iconContainer.className = 'btn btn-outline-dark bg-light';
|
||||
label.className = 'small';
|
||||
|
||||
if (TYPE_DEFINITION[item.type]) {
|
||||
icon.className = TYPE_DEFINITION[item.type].dragIcon || 'fa-solid fa-question';
|
||||
label.textContent = item.id;
|
||||
} else if (item.type.substr(-11) == '-collection' && TYPE_DEFINITION[item.type.substr(0, item.type.length-11)]) {
|
||||
iconContainer.style.boxShadow = '3px 3px var(--bs-btn-border-color)';
|
||||
icon.className = TYPE_DEFINITION[item.type.substr(0, item.type.length-11)].dragIcon || 'fa-solid fa-question';
|
||||
label.textContent = 'x' + item.values.length;
|
||||
} else {
|
||||
icon.className = 'fa-solid fa-question';
|
||||
label.textContent = item.id || '';
|
||||
}
|
||||
|
||||
iconContainer.append(icon);
|
||||
|
||||
const itemContainer = document.createElement('div');
|
||||
itemContainer.className = 'd-flex flex-column align-items-center gap-2 small';
|
||||
itemContainer.append(iconContainer, label);
|
||||
return itemContainer;
|
||||
});
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'd-flex flex-row gap-2 small';
|
||||
container.append(...dragElements);
|
||||
|
||||
document.body.append(container);
|
||||
event.dataTransfer.setDragImage(container, -25, 0);
|
||||
requestAnimationFrame(() => {
|
||||
document.body.removeChild(container);
|
||||
});
|
||||
}
|
||||
if (Array.isArray(validDragObject)) {
|
||||
return validDragObject.forEach(data => setTransferData(event, data));
|
||||
}
|
||||
|
||||
event.dataTransfer.setData('application/fhc-' + validDragObject.type, JSON.stringify(validDragObject));
|
||||
}
|
||||
|
||||
/**
|
||||
* check if the dataTransfer types are in the allowed types array
|
||||
* if strict is disabled at least one type must be the allowed array
|
||||
* otherwise all types have to be in the allowed array
|
||||
*
|
||||
* @param Event event
|
||||
* @param Array allowedTypes
|
||||
* @param Boolean strict
|
||||
*/
|
||||
function eventHasTypes(event, allowedTypes, strict) {
|
||||
if (!allowedTypes || !allowedTypes.length)
|
||||
allowedTypes = VALID_TYPES;
|
||||
allowedTypes = allowedTypes.map(type => 'application/fhc-' + type);
|
||||
|
||||
const dataTypes = [...event.dataTransfer.types];
|
||||
|
||||
// NOTE(chris): if dragging across browsers the dataTransfer object is
|
||||
// set to a default one without data. Since we do not support dragging
|
||||
// across browsers (yet) we return false which will disallow dropping.
|
||||
if (!dataTypes.length)
|
||||
return false;
|
||||
|
||||
if (!strict)
|
||||
return allowedTypes.some(type => [...event.dataTransfer.types].includes(type));
|
||||
|
||||
return [...event.dataTransfer.types].every(type => allowedTypes.includes(type));
|
||||
}
|
||||
|
||||
function bindDragEnterLeave(el, onEnter, onLeave) {
|
||||
// NOTE(chris): add save dragenter and dragleave events
|
||||
// that won't fire when hovering over child elements
|
||||
|
||||
let skipLeave = false;
|
||||
let skipLeaveParent = true;
|
||||
|
||||
function init(evt) {
|
||||
skipLeave = false;
|
||||
skipLeaveParent = true;
|
||||
// add global listeners
|
||||
window.addEventListener('dragenter', globalDragenter, true);
|
||||
window.addEventListener('dragleave', globalDragleave, true);
|
||||
window.addEventListener('drop', globalDrop, true);
|
||||
// call enter
|
||||
onEnter(evt);
|
||||
// remove self
|
||||
el.removeEventListener('dragenter', init);
|
||||
}
|
||||
|
||||
function cleanup(evt, wasDropped) {
|
||||
// remove global listeners
|
||||
window.removeEventListener('dragenter', globalDragenter, true);
|
||||
window.removeEventListener('dragleave', globalDragleave, true);
|
||||
window.removeEventListener('drop', globalDrop, true);
|
||||
// call leave
|
||||
onLeave(evt, wasDropped);
|
||||
// add init
|
||||
el.addEventListener('dragenter', init);
|
||||
}
|
||||
|
||||
function globalDragenter(evt) {
|
||||
skipLeaveParent = false;
|
||||
if (el != evt.target && !el.contains(evt.target)) {
|
||||
cleanup(evt);
|
||||
} else {
|
||||
skipLeave = true;
|
||||
}
|
||||
}
|
||||
function globalDragleave(evt) {
|
||||
if (el != evt.target && !el.contains(evt.target)) {
|
||||
if (skipLeaveParent) {
|
||||
skipLeaveParent = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (skipLeave) {
|
||||
skipLeave = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
cleanup(evt);
|
||||
}
|
||||
function globalDrop(evt) {
|
||||
cleanup(evt, true);
|
||||
}
|
||||
|
||||
el.addEventListener('dragenter', init);
|
||||
|
||||
return () => {
|
||||
// cleanup
|
||||
el.removeEventListener('dragenter', init);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
isValidDragObject,
|
||||
getValidTransferData,
|
||||
isValidTransferData,
|
||||
getTransferData,
|
||||
convertToValidDragObject,
|
||||
setTransferData,
|
||||
eventHasTypes,
|
||||
bindDragEnterLeave
|
||||
};
|
||||
export default {
|
||||
...TYPE_CONSTANTS,
|
||||
isValidDragObject,
|
||||
getValidTransferData,
|
||||
isValidTransferData,
|
||||
getTransferData,
|
||||
convertToValidDragObject,
|
||||
setTransferData,
|
||||
eventHasTypes,
|
||||
bindDragEnterLeave
|
||||
};
|
||||
|
||||
@@ -546,6 +546,9 @@ export default {
|
||||
} else {
|
||||
console.error("FhcApi: method not allowed:", method);
|
||||
}
|
||||
},
|
||||
getErrorHandler(config) {
|
||||
return get_error_handler(config);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1535,6 +1535,26 @@ $phrases = array(
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'ui',
|
||||
'phrase' => 'change',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Ändern',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Change',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'ui',
|
||||
@@ -25431,6 +25451,26 @@ array(
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'ui',
|
||||
'phrase' => 'error_fieldNotFound',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => '{field} nicht gefunden',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => '{field} not found',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'notiz',
|
||||
@@ -39324,6 +39364,47 @@ array(
|
||||
)
|
||||
)
|
||||
),
|
||||
// groups
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'stv',
|
||||
'phrase' => 'groups_error_notallstudents',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Fehler: Es können nur Studierende mit UID verschoben werden.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Error: Only students with UID can be moved.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'stv',
|
||||
'phrase' => 'groups_error_notsamestg',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Fehler: Alle Studierenden müssen im selben Studiengang sein.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Error: All students must be in the same program.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
//**************************** CORE/document_export
|
||||
array(
|
||||
@@ -43299,6 +43380,66 @@ array(
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'gruppenmanagement',
|
||||
'phrase' => 'special_groups',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Spezialgruppen',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Special groups',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'gruppenmanagement',
|
||||
'phrase' => 'add_group',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Gruppe hinzufügen',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Add group',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'gruppenmanagement',
|
||||
'phrase' => 'groups_added',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => '{n} Gruppen hinzufügen',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Added {n} groups',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'gruppenmanagement',
|
||||
@@ -43339,6 +43480,26 @@ array(
|
||||
)
|
||||
)
|
||||
),
|
||||
array(
|
||||
'app' => 'core',
|
||||
'category' => 'gruppenmanagement',
|
||||
'phrase' => 'error_alreadyInGroup',
|
||||
'insertvon' => 'system',
|
||||
'phrases' => array(
|
||||
array(
|
||||
'sprache' => 'German',
|
||||
'text' => 'Der Student {uid} ist bereits im {studiensemester_kurzbz} dieser Gruppe zugeteilt. Entfernen Sie vorher diese Zuteilung.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
),
|
||||
array(
|
||||
'sprache' => 'English',
|
||||
'text' => 'Student {uid} is already assigned to this group in {studiensemester_kurzbz}. Remove this assignment beforehand.',
|
||||
'description' => '',
|
||||
'insertvon' => 'system'
|
||||
)
|
||||
)
|
||||
),
|
||||
//////////// FHC4 Phrases Gruppen End ////////////
|
||||
|
||||
array(
|
||||
|
||||
Reference in New Issue
Block a user